@akanjs/cli 3.1.0-rc.4 → 3.1.0-rc.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (89) hide show
  1. package/.build-stamp +1 -1
  2. package/{CodeAgent-ymte1tv8.js → CodeAgent-rkjyxgec.js} +2 -2
  3. package/{CodeTui-4rhdm1qw.js → CodeTui-h7pyj6xa.js} +1 -1
  4. package/TunnelShare-661cxz5e.js +14 -0
  5. package/{agent.command-r5x1mch5.js → agent.command-3qxf4xcj.js} +5 -5
  6. package/akanCode.js +2 -2
  7. package/{application.command-3dkr7rdz.js → application.command-m1kcd7jf.js} +12 -11
  8. package/{applicationBuildRunner-h0j2swx1.js → applicationBuildRunner-dj0xkrep.js} +16 -9
  9. package/buildBatch.proc.js +7 -7
  10. package/{cloud.command-4kyxfw0y.js → cloud.command-3jzym8q7.js} +17 -16
  11. package/cloud.script-r4nskbc6.js +24 -0
  12. package/{code.command-sjvzt8fc.js → code.command-0ad6gw2a.js} +6 -6
  13. package/{context.command-j9frz0dz.js → context.command-0qx447mb.js} +17 -16
  14. package/context.runner-1vsjztjg.js +25 -0
  15. package/{dependencyScanner-06z2dmrr.js → dependencyScanner-3afj2vt1.js} +1 -1
  16. package/{guideline.command-1epw6hey.js → guideline.command-cv5bmrx2.js} +2 -2
  17. package/guidelines/conventions/conventions.instruction.md +450 -1084
  18. package/guidelines/cssRule/cssRule.instruction.md +16 -1
  19. package/guidelines/fieldRule/fieldRule.instruction.md +57 -0
  20. package/guidelines/framework/framework.instruction.md +4 -7
  21. package/guidelines/mcpRule/mcpRule.instruction.md +11 -8
  22. package/guidelines/modelConstant/modelConstant.instruction.md +2 -1
  23. package/guidelines/modelStore/modelStore.instruction.md +9 -0
  24. package/guidelines/pageRule/pageRule.instruction.md +80 -0
  25. package/guidelines/queryRule/queryRule.instruction.md +35 -8
  26. package/guidelines/runtimeRule/runtimeRule.instruction.md +89 -2
  27. package/guidelines/ssrRule/ssrRule.instruction.md +13 -0
  28. package/guidelines/transportRule/transportRule.instruction.md +76 -8
  29. package/guidelines/workspaceOnboarding/workspaceOnboarding.instruction.md +4 -4
  30. package/guidelines/workspaceRecipes/workspaceRecipes.instruction.md +49 -4
  31. package/incrementalBuilder.proc.js +4 -4
  32. package/{index-jw6st2rd.js → index-192ha4xh.js} +356 -307
  33. package/{index-1hkr7mrb.js → index-1p98exm2.js} +2 -2
  34. package/{index-8byzcvxp.js → index-1xj2t7cb.js} +7 -7
  35. package/{index-er3hekv9.js → index-20bxxyvn.js} +80 -80
  36. package/{index-xe3x2kw5.js → index-35hc95h5.js} +1 -1
  37. package/{index-p5nrc5he.js → index-4mhy6qme.js} +4 -4
  38. package/{index-0qc5nd61.js → index-5edzv8fn.js} +5 -5
  39. package/{index-k9rrten6.js → index-5nd1wtkv.js} +92 -94
  40. package/index-62md4dwy.js +260 -0
  41. package/{index-z3v7abrk.js → index-665zp9rs.js} +38 -15
  42. package/{index-1t2xw0p0.js → index-6xn2n442.js} +4 -4
  43. package/{index-jg3fj8h0.js → index-86jt2a0e.js} +1 -1
  44. package/{index-k33me5sp.js → index-8fp3b47h.js} +419 -418
  45. package/{index-shastb7m.js → index-9z426msb.js} +17 -12
  46. package/{index-gdybjw3x.js → index-awbdfcf2.js} +4 -4
  47. package/{index-3hx1z96c.js → index-bqbf654f.js} +1 -1
  48. package/{index-jppthsff.js → index-cpb3nvma.js} +57 -58
  49. package/{index-b60ccq2s.js → index-d47ypebv.js} +17 -15
  50. package/{index-gc05vqd2.js → index-d72hfmdm.js} +13 -13
  51. package/{index-akha197v.js → index-fmzcs8v6.js} +4 -4
  52. package/{index-28ftaz3d.js → index-fyq1z6kt.js} +29 -27
  53. package/{index-vwmv64ad.js → index-g1mbppa9.js} +238 -236
  54. package/{index-hedt951c.js → index-h47cf1gg.js} +7 -7
  55. package/{index-hfqs9jvc.js → index-j4mkhz93.js} +3 -3
  56. package/{index-08a5ga6n.js → index-jw1zm273.js} +28 -30
  57. package/{index-q958tqqg.js → index-m9jj544v.js} +1 -1
  58. package/{index-8935h3n1.js → index-mdjc93e1.js} +1 -1
  59. package/{index-2wyn17fa.js → index-pydbnfez.js} +2 -2
  60. package/{index-bjh4wbtk.js → index-qwa5k1j8.js} +1 -1
  61. package/{index-81a1ywp6.js → index-tamvyvj3.js} +3 -3
  62. package/{index-dez4t5sd.js → index-trpasv9q.js} +260 -259
  63. package/{index-2ahj896a.js → index-tvehn2sp.js} +1 -1
  64. package/{index-2k0t8p3p.js → index-va06nr10.js} +1086 -1075
  65. package/{index-9c3cd767.js → index-vk0pa0mn.js} +20 -21
  66. package/{index-qyaqzh8e.js → index-w44cy4vs.js} +2216 -2216
  67. package/index.js +22 -22
  68. package/{library.command-n58dgakb.js → library.command-erp2f56n.js} +3 -3
  69. package/{localRegistry.command-f2768sww.js → localRegistry.command-xxpqgxjw.js} +15 -14
  70. package/{module.command-xzjg5k7n.js → module.command-gs2r615e.js} +5 -5
  71. package/{package.command-24sh1j1q.js → package.command-7343f74r.js} +4 -4
  72. package/package.json +2 -2
  73. package/{page.command-fqnm88na.js → page.command-4h4a8pnh.js} +3 -3
  74. package/{pipeline.command-vn6taz75.js → pipeline.command-a2nrk26m.js} +3 -3
  75. package/{primitive.command-4j1g1a0d.js → primitive.command-w6t0asqz.js} +6 -6
  76. package/{projectTypecheck-3y7ayty0.js → projectTypecheck-f534enqj.js} +1 -1
  77. package/{quality.command-5hqmcdwt.js → quality.command-77x7vbbx.js} +4 -4
  78. package/{repair.command-rw5tk460.js → repair.command-y6sstev7.js} +5 -5
  79. package/{scalar.command-0jpap45m.js → scalar.command-20b8cy0k.js} +4 -4
  80. package/{subspace.command-y7n9sghk.js → subspace.command-92xkc0s0.js} +11 -10
  81. package/templates/lib/cnst.ts +0 -1
  82. package/templates/workspaceRoot/.gitignore.template +24 -53
  83. package/{tunnel.command-5gjwh1f8.js → tunnel.command-2kw8vt9g.js} +8 -7
  84. package/{workflow.command-3t22f7eb.js → workflow.command-2djwmpqf.js} +10 -10
  85. package/{workspace.command-cq5xg2jq.js → workspace.command-5se6spqb.js} +28 -27
  86. package/TunnelShare-p8ymkww8.js +0 -13
  87. package/cloud.script-jc3hc5d9.js +0 -23
  88. package/context.runner-v0def5my.js +0 -24
  89. package/index-n68jx79w.js +0 -120
@@ -1,13 +1,20 @@
1
- # Transport Rule — Guards, Sockets, Binary Frames, HTTP Verbs
1
+ # Transport Rule — Guards, Sockets, Deadlines, Rate Limits, Server-To-Server Calls, Bytes, HTTP Verbs
2
2
 
3
3
  How a call reaches a signal: what a guard may read on either transport, how a websocket carries its credential
4
- and its per-connection state, when a pubsub payload leaves JSON for a binary frame, and which HTTP verb a
5
- mutation is mounted under. `conventions` carries the invariants — this is the full contract behind them.
4
+ and its per-connection state, how long a call may take and what it may cache or count, how one server calls
5
+ another, which body types and bytes cross the wire, when a pubsub payload leaves JSON for a binary frame, and which
6
+ HTTP verb a mutation is mounted under. `conventions` carries the invariants — this is the full contract behind them.
6
7
 
7
8
  ## Guards And Transports
8
9
 
9
10
  - Guards run on both HTTP and websocket calls. Read the caller with `context.get<T>("account")` (`pkgs/akanjs/signal/signalContext.ts`) instead of branching on `getHttpContext()` / `getWebSocketContext()`.
10
11
  - Slice-level `guards` only reach the generated query/mutation endpoints. A `pubsub`/`message` endpoint is unguarded unless it declares its own `guards` in its signal option.
12
+ - **Every endpoint that declares no guards is named in a boot `warn`**, since it answers anyone over HTTP and
13
+ WebSocket — a named slice's `init()`, a root slice, a `pubsub`/`message`/`query`/`mutation`. Write
14
+ `guards: [Public]` to keep one open on purpose.
15
+ - **A generated verb is taken off HTTP with `guards: { cru: false }`** (or `get`/`create`/`update`/`remove:
16
+ false`), and a verb whose guards resolve to none is not mounted at all. `mcp: { cru: false }` does **not** do
17
+ this: it keeps the verbs off the agent shelf while HTTP serves them to whoever passes their guards.
11
18
  - A pubsub room is authorized once, at subscribe. When a socket's credential changes the framework re-runs each room's guards and unsubscribes the ones that now fail (`SignalResolver.revalidateWsRooms`), so guards must stay side-effect free and safe to re-run. Each re-check runs on a fresh `SignalContext`, so a guard that memoizes per context answers anew.
12
19
  - **Arguments are parsed after the `"account"` guards** (`SignalContext.init` reads the body, `exec` parses): the order is middlewares → `account` guards → parse → `resource` guards → internal arguments → handler, whatever order `guards: [...]` lists them in. A caller an account guard refuses never reaches a scalar parser, which is what keeps a slow parser from being a pre-auth denial of service. A guard or middleware that reads `context.args` / `getArg()` earlier still gets the value, parsed on first read — a mismarked account guard keeps working. One visible consequence: an unauthenticated call with a malformed argument answers 401/403 instead of 400.
13
20
  - A websocket carries its credential in the handshake snapshot on `ws.data` (`AppWsData`); clients that hold the token in memory send it with `fetch.setJwt(...)`, which forwards an auth frame over the socket.
@@ -24,7 +31,8 @@ mutation is mounted under. `conventions` carries the invariants — this is the
24
31
  - **A device or another server proves itself with a machine token.** `MachineToken.issue({ kind, id, version },
25
32
  { expiresAt? })` in `akanjs/server` signs an EdDSA JWT (`typ: akan-machine+jwt`, `aud: <app>/<environment>`) with
26
33
  `AKAN_MACHINE_PRIVATE_KEY`; it never expires unless asked. `akan machine-keygen <file>` writes that key to a file
27
- and prints only the public one. A server with either key verifies the bearer of every call — HTTP, and the
34
+ and prints only the public one. A server with either key (`AKAN_MACHINE_PRIVATE_KEY` or
35
+ `AKAN_MACHINE_PUBLIC_KEY`) verifies the bearer of every call — HTTP, and the
28
36
  websocket's auth frame — after every lib middleware, and sets `account.machine = { kind, id, version }` from a
29
37
  verified token; it takes off a `machine` any other middleware or a session's claims put there, so a staff token
30
38
  can never pass for one, and a session middleware already reads the EdDSA token as anonymous. `.with(Machine)` hands
@@ -51,6 +59,26 @@ mutation is mounted under. `conventions` carries the invariants — this is the
51
59
  socket's close, arrives while the subscribe is still running, the subscribe answers `subscribe: false` and runs its
52
60
  `unsubscribe` cleanup (and its `disconnect` one when the socket closed) instead of joining the room.
53
61
 
62
+ ## Writing A Guard
63
+
64
+ - **Resource guards are `Can<Verb><Model>` classes in `srvkit/guards.ts`** that `implements Guard` with an
65
+ `async canPass(context)`. They **fail closed**: no resource named ⇒ `false`; a load that throws ⇒ `logger.warn`
66
+ then `false`. Admin bypass goes first.
67
+ - **Keep `static name = "User";` on guard classes.** `fetch` serializes guard names and the API explorer filters
68
+ on them; it looks like dead code, and deleting it breaks the UI. Comment it so the next reader knows.
69
+ - **Every guard class declares `static scope: GuardScope`, required with no default.** `"account"` means the
70
+ verdict reads the caller and nothing about the call, so it can be evaluated with no arguments — which is what
71
+ lets an MCP listing hide what this caller certainly cannot use. `"resource"` means it needs the call's arguments
72
+ (`context.getArg()`) and fails closed without them, so it is never evaluated for a listing: the entry stays
73
+ visible and is stopped at call time. Getting it wrong is not a type error, so the marker is mandatory rather than
74
+ defaulted — `SignedIn` / `Admin` / role checks are `"account"`, and every `Can<Verb><Model>` is `"resource"`.
75
+ What a mismarked guard does to a listing, and `static agents = false` on `Person`: `mcpRule`.
76
+ - **Guards ship with the library that owns the model** and are imported by its own signals through the package
77
+ path, so a mounting app inherits authorization and cannot forget it.
78
+ - The acting user arrives via `.with(Self)` / `.with(CurrentUserId)` / `.with(Me)`; never trust a client-supplied
79
+ id. Services re-check ownership even when a guard already gated the call — two independent gates.
80
+ - `srvkit/guards.ts` earns real comments: explain what would leak without each guard.
81
+
54
82
  ## Request Deadlines
55
83
 
56
84
  - **`{ timeout: <ms> }` in an endpoint's signal option is the whole configuration surface**, and it reaches both
@@ -65,7 +93,9 @@ mutation is mounted under. `conventions` carries the invariants — this is the
65
93
  five-minute budget that is a five-minute hold on the event loop for a call that answered in a millisecond.
66
94
  - **Without a declaration the client's own default applies — 30 seconds** (`DEFAULT_TIMEOUT_MS`,
67
95
  `fetch/client/httpClient.ts`), and the server imposes nothing. Nothing else bounds a request: `Bun.serve` runs
68
- with `idleTimeout: 0`, so a long call is only ever ended by one of these two.
96
+ with `idleTimeout: 0`, so a long call is only ever ended by one of these two. An endpoint whose real work runs
97
+ longer — provisioning, a firmware flow, an external orchestration — therefore has to declare one, or it dies
98
+ mid-flight with a transport error that does not say whether the server ever received it.
69
99
  - **Precedence is caller, then endpoint, then client default.** `fetch.x(…, { timeout: 5_000 })` overrides what
70
100
  the endpoint declared; `{ timeout: false }` waits as long as the runtime will. An app moves its own default
71
101
  with `fetch.instance.setTimeout(ms)` — `lib/useClient.ts` is generated, so the `timeout` option on
@@ -102,9 +132,10 @@ mutation is mounted under. `conventions` carries the invariants — this is the
102
132
  values for its TTL, as the handler returned them.
103
133
  - **A cache backend that is down does not take the endpoint down.** A failed read is warned and the call runs
104
134
  uncached; a failed write is warned and dropped.
105
- - The default middleware chain is `Logging → Timeout → <lib middlewares>`, and the call inside it runs an
106
- `account` rate limit → account guards → argument parse → resource guards → internal arguments → cache →
107
- handler: a hung cache backend is bounded by the same deadline as the handler it stands in for. **`Retry` was removed** — an automatic re-run of an endpoint
135
+ - A call runs the cross-site check and an `ip` rate limit, then the body read, then the default middleware chain
136
+ `Logging → Timeout → <lib middlewares>`, and inside it an `account` rate limit → account guards → argument
137
+ parse → resource guards → internal arguments → cache → handler: a hung cache backend is bounded by the same
138
+ deadline as the handler it stands in for. **`Retry` was removed** — an automatic re-run of an endpoint
108
139
  whose failure it cannot classify replays whatever the first attempt already did.
109
140
 
110
141
  ## Repeated Mutations
@@ -157,6 +188,43 @@ mutation is mounted under. `conventions` carries the invariants — this is the
157
188
  `{ ok: true } | { ok: false, retryAfterMs }` and is never switched off — throw the module's own `Err` with the
158
189
  wait when it refuses.
159
190
 
191
+ ## Calling Another Server
192
+
193
+ - **A server-side `fetch.clone({ origin })` opens no websocket unless `connect: true`**, and neither does a clone
194
+ of such a clone: it makes one-off calls to another server, and a socket it opened would reconnect forever with
195
+ nobody to close it. It never carries the visitor's session either: a call to another origin sends only the `jwt`
196
+ its caller gave the clone.
197
+ - **Retry a call with `retry`, never with a loop of your own.** `fetch.clone({ origin, retry: {} })` (or a call's
198
+ own `{ retry }`) tries again on an unreachable server, a 502/503/504 and a 429 after its Retry-After, backing off
199
+ with jitter until the call's one deadline runs out. A mutation retries under one `Idempotency-Key` it mints, or
200
+ the `idempotencyKey` you name, and the server runs it once per key, caller and arguments (Repeated Mutations
201
+ above); a signed-out call, one whose handler returns a `Response`, and a call through MCP run every time. A 4xx
202
+ is never retried.
203
+ - **A failure another process reported travels as itself.** A server-to-server `fetch.<endpoint>(..., { origin })`
204
+ restores the remote `Err` — the same class, key and `data` — so a hop that rethrows it answers its own caller
205
+ with that error and the dictionary translates it. Never wrap the catch in a `new Error`, and never re-key it as a
206
+ `new Err` of your own: both discard what the endpoint chose to say, and a bare `Error` is generalized to
207
+ `Internal Server Error` on the way out.
208
+ - `String(err)` and `${err}` read `Err: <key> {…data}`, the data as one line of JSON with secret-named keys masked,
209
+ so a ``logger.warn(`…: ${String(error)}`)`` keeps the exit code or status the `Err` carried.
210
+
211
+ ## Body Types And Bytes
212
+
213
+ - `.body(...)` / `.param(...)` args accept `ConstantFieldTypeInput` only: scalars, model refs, or `enumOf(...)`.
214
+ Numbers are `Int` or `Float` — `Number` is rejected (`pkgs/akanjs/signal/endpointInfo.ts`).
215
+ - **`Upload` is valid only inside a mutation flagged for file upload**:
216
+ `mutation([cnst.File], { fileUpload: true }).body("files", [Upload])`, as the `file` module does. It is not a
217
+ model field type. The other body args of such a mutation arrive typed as in a JSON body. A file's `type` on the
218
+ server is what its name implies, not its bytes — check the bytes when the type matters.
219
+ - **`Binary` is raw bytes on the wire.** It is `Uint8Array` on both sides — a Node `Buffer` is one, so a server
220
+ handler may pass one straight in — and it accepts base64 in either direction, so the same declaration serves a
221
+ JSON body and a binary frame. A `Binary` return travels as base64 in an HTTP body. It is never a model field
222
+ (`fieldRule`).
223
+ - **Never send bytes as `Any`.** `Any` passes a `Buffer` through untouched and `JSON.stringify` then spells it
224
+ `{ type: "Buffer", data: number[] }`, which `JSON.parse` never restores; the result is 3.6x the wire and a shape
225
+ that only breaks at the first byte-offset read. A return declared `Any` that turns out to be bytes fails instead
226
+ of reaching the caller as `{}`, and MCP refuses a `Binary` return for the same reason it refuses `Any`.
227
+
160
228
  ## Binary Pubsub
161
229
 
162
230
  - **`pubsub(Binary)` sends its payload in a websocket binary frame**, skipping the JSON `{ type: "pub" }`
@@ -230,10 +230,10 @@ Six worked recipes carry the full code for the most frequent Akan changes, toget
230
230
  | Recipe | Covers |
231
231
  |---|---|
232
232
  | 1. Adding a field to a model | the five `constant.ts` classes, the dictionary entry, the store form, `akan sync` |
233
- | 2. Injecting a dependency into a service | `service<srv.XService>()`, `plug(Adapter)`, `env(...)`, `use<T>()` |
233
+ | 2. Injecting a dependency into a service | `service<srv.XService>()`, `plug(Adapter)`, `env(...)`, `use<T>()`, writing an `adapt()` adapter |
234
234
  | 3. Creating and using a slice | `slice()` guards, the generated `init<Model><Suffix>`, `Load.Units` hydration |
235
235
  | 4. Creating a mutation endpoint | the document chain method, the service call, the signal `guards`, the store action |
236
- | 5. Internal triggers | `internal()` with interval and cron |
236
+ | 5. Internal triggers | `internal()` with interval and cron, queue retries and the dead letter |
237
237
  | 6. Creating an insight | the `XInsight` class, the aggregation, the dashboard tile |
238
238
  | Data Flow Summary | how a value travels page → store → signal → service → document → DB |
239
239
  | Auto-Generated API Reference | what signal, service, store, and document each generate — never hand-write these |
@@ -244,7 +244,7 @@ existing content first and change only the relevant sections — never rewrite a
244
244
  ## Modeling & Query Gotchas
245
245
 
246
246
  A short list of things the type system does not always catch. The full rules for text search, `cascade`, and the
247
- query-level writes are in the convention set above; these are the shapes that build green and fail later.
247
+ query-level writes are in `queryRule`; these are the shapes that build green and fail later.
248
248
 
249
249
  - **Slices return a query, not a list.** A slice `exec` must return `this.<model>Service.query<Filter>(...)`, never
250
250
  a `list<Filter>(...)` / `listBy...(...)` array. Returning an array type-checks but fails at runtime with
@@ -258,7 +258,7 @@ query-level writes are in the convention set above; these are the shapes that bu
258
258
  **bare** — `this.userModel.pickById(id, { passwordHash: true })`, likewise `pickOne` / `find` / `findOne` /
259
259
  `findById`. A generated filter accessor takes it **inside the option object** —
260
260
  `this.userModel.findByAccountId(accountId, statuses, { select: { passwordHash: true } })`. Handing `{ select: … }`
261
- to the facade selects a field named `select`, which no model has: the value arrives empty, with no error.
261
+ to the facade projects a field named `select`, which no model has, and throws `Unknown document field path`.
262
262
  - **`cascade` names a direction, and the wrong one is a data loss** — `removeRef` on the relation the owner holds,
263
263
  `removeWith` on the child's reference to its owner. A query-level removal fires no hooks and therefore no cascade.
264
264
  - **`q.search()` is a filter node, not a slice requirement.** `bySearch: filter().arg("text", String).query((text,
@@ -66,11 +66,12 @@ const form = st.use.taskForm();
66
66
 
67
67
  ### Recipe 2: Injecting a Dependency into a Service
68
68
 
69
- Three patterns: injecting an **external adapter** (`use<>()`), another **module's service** (`service<>()`),
70
- or a **predefined framework adapter** (`plug()`). A field named `<refName>Service` resolves to the service
71
- registered under `<refName>` — the `Service`/`Signal` suffix is required and stripped to derive the lookup key.
69
+ Four patterns: injecting an **external adapter** (`use<>()`), another **module's service** (`service<>()`),
70
+ a **predefined framework adapter** (`plug()`), or **an adapter you own** (`adapt()` + `plug()`). A field named
71
+ `<refName>Service` resolves to the service registered under `<refName>` — the `Service`/`Signal` suffix is required
72
+ and stripped to derive the lookup key.
72
73
 
73
- > **For a new adapter you own, prefer the `adapt()` shape in pattern C over the `option.ts` registration in
74
+ > **For a new adapter you own, prefer the `adapt()` shape in pattern D over the `option.ts` registration in
74
75
  > pattern A.** An `adapt()` class self-registers and is injected with `plug(TheClass)`, so it never touches
75
76
  > `option.ts`. Pattern A is the legacy constructor-style shape: recognise it, keep it working, and migrate one
76
77
  > only when you are already changing it.
@@ -147,6 +148,46 @@ storage directly: declare `image: field(File).optional()` (or `images: field([Fi
147
148
  store's generated `upload<Field>On<Model>(fileList)` action handle the upload. Add `{ cascade: "removeRef" }` to that
148
149
  field when the file belongs to the model alone, and removing the model removes the file and its stored object.
149
150
 
151
+ **D. An adapter you own — `adapt()` in `srvkit/`**
152
+
153
+ An injected singleton is an `adapt()` class in `srvkit/`. Write new adapters this way.
154
+
155
+ ```typescript
156
+ export class AdminNoti extends adapt("adminNoti" as const, ({ use, env, plug, memory }) => ({
157
+ discordApi: use<DiscordApi>(),
158
+ workspaceRoot: env(() => `~/build/${getEnv().environment}`),
159
+ masterHost: plug(MasterHost),
160
+ tokenMap: memory(Map, { of: String }),
161
+ })) {}
162
+ ```
163
+
164
+ - Inject it with `plug(TheClass)` from a service or from another adapter. Destructure only the injectors you use,
165
+ and write the registration key `as const`.
166
+ - **Do not register an `adapt()` class in `lib/option.ts`.** It self-registers, and `plug(Class)` uses the class
167
+ itself as the token. `option.ts` is now only for legacy constructor-style adapters and for widening the options
168
+ type.
169
+ - `this.logger` is provided; never construct a `Logger` inside an `adapt()` class. Lifecycle work goes in
170
+ `override async onInit()`.
171
+ - **`adapt()` is for singletons only.** A per-use value object stays a plain class you `new` at the call site. If
172
+ there is not exactly one per process that a service needs injected, it is a plain class.
173
+ - **`memory(...)` takes a scalar or model class, not only a primitive** — `memory(Map, { of: CallStateInput })`
174
+ serializes through the constant and travels as JSON text, so the Redis and sqlite-backed caches round-trip the
175
+ same declaration. Never hand-encode JSON into a `String` memory. A memory is scoped to the service or adaptor
176
+ that declares it; `ttl` (milliseconds) sets how long each write lives unless the `set` call names its own
177
+ `expireAt`, and a `Map` entry expires on its own.
178
+
179
+ Conventions that hold for both adapter shapes (A and D):
180
+
181
+ - Route every remote call through one private `#api<T>(path, init?)` with `signal: AbortSignal.timeout(20_000)`.
182
+ - Paginate with `for (let page = 1; ; page += 1)` broken by `if (pageItems.length < 100) break;`.
183
+ - Resolve secrets as `process.env.X ?? options.x ?? deterministicGenerator(...)` **inside a function**, never at
184
+ module scope.
185
+ - Extend a function by appending an optional trailing parameter with a default, never by changing arity.
186
+ - Parameters: up to three required primitives positional; optional flags in a trailing `{ … } = {}`; four or more
187
+ parameters, or any two same-typed strings, in one named destructured object.
188
+ - Release locks in `finally`. Load heavy optional dependencies through a module-level memoized promise
189
+ (`puppeteerLoad ??= import("puppeteer")`).
190
+
150
191
  ---
151
192
 
152
193
  ### Recipe 3: Creating and Using a Slice
@@ -285,6 +326,10 @@ async cleanupStaleTasks() {
285
326
  - `initialize()` — runs once on service startup
286
327
  - `process(Return).msg(Type)` — message queue consumer
287
328
 
329
+ A `process` job's `backoff: { type: "exponential", delay, maxDelay }` caps the wait between attempts, and the
330
+ internal's `onFailed(job, error)` runs once a job has failed for good — its last attempt, or bullmq's stall limit —
331
+ the dead letter, on either queue.
332
+
288
333
  ---
289
334
 
290
335
  ### Recipe 6: Creating an Insight (Aggregation / Dashboard Stats)
@@ -3,12 +3,12 @@ import {
3
3
  ApplicationBuildReporter,
4
4
  HmrWatcher,
5
5
  WatchRootResolver
6
- } from "./index-k9rrten6.js";
6
+ } from "./index-5nd1wtkv.js";
7
7
  import {
8
8
  CodegenLock,
9
9
  WorkspaceExecutor,
10
10
  AppExecutor
11
- } from "./index-2k0t8p3p.js";
11
+ } from "./index-va06nr10.js";
12
12
  import"./index-52w0fvh8.js";
13
13
  import {
14
14
  GraphClientEntryDiscovery,
@@ -19,10 +19,10 @@ import {
19
19
  DevChangePlanner,
20
20
  DevGeneratedIndexSync,
21
21
  SsrDevBundler
22
- } from "./index-qyaqzh8e.js";
22
+ } from "./index-w44cy4vs.js";
23
23
  import {
24
24
  ServerGraphFile
25
- } from "./index-jw6st2rd.js";
25
+ } from "./index-192ha4xh.js";
26
26
  import {
27
27
  prepareDevWatchBatch
28
28
  } from "./index-a5vmm6x4.js";