@akanjs/cli 3.0.0-beta.17 → 3.0.0-beta.19
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.
- package/.build-stamp +1 -1
- package/{CodeAgent-gjqnc2mg.js → CodeAgent-6cm97kay.js} +2 -2
- package/{CodeAgentRpcHost-qvwfv0n9.js → CodeAgentRpcHost-p7h6n2hp.js} +1 -1
- package/CodeAgentRpcListener-7p7z5dd7.js +97 -0
- package/{CodeTui-nd9hqvrw.js → CodeTui-za4jsm41.js} +205 -34
- package/{TunnelShare-r5tc2qk5.js → TunnelShare-2j6s511d.js} +2 -2
- package/{agent.command-n664dtcv.js → agent.command-e0wpbbyg.js} +6 -6
- package/akanCode.js +7 -5
- package/{application.command-t5542jte.js → application.command-8wyxqc8q.js} +28 -15
- package/{applicationBuildRunner-x4gbfn7y.js → applicationBuildRunner-n9gzaxd0.js} +35 -10
- package/{applicationReleasePackager-z61ep017.js → applicationReleasePackager-vpggfqgq.js} +1 -1
- package/buildBatch.proc.js +3 -3
- package/{capacitorApp-shv3ajh5.js → capacitorApp-q3z0qht0.js} +2 -2
- package/{cloud.command-dk5tj39k.js → cloud.command-4zwa5dsx.js} +34 -12
- package/cloud.script-fwx3vb34.js +20 -0
- package/{code.command-88sebgnn.js → code.command-468c9d7s.js} +41 -19
- package/commandManifest.json +1 -1
- package/{context.command-pqsm5jrk.js → context.command-dsnw0jby.js} +14 -14
- package/context.runner-rs4rj15d.js +22 -0
- package/{dependencyScanner-shs82ynp.js → dependencyScanner-0z2h2tks.js} +1 -1
- package/{devTui-gy6mendg.js → devTui-ffjsarqa.js} +3 -3
- package/{devTuiView-y1548wy6.js → devTuiView-1q1mdgrv.js} +1 -1
- package/{guideline.command-rpc2emw8.js → guideline.command-vhw13pqr.js} +8 -13
- package/guidelines/agentRule/agentRule.instruction.md +27 -10
- package/guidelines/conventions/conventions.instruction.md +53 -26
- package/guidelines/queryRule/queryRule.instruction.md +17 -7
- package/guidelines/runtimeRule/runtimeRule.instruction.md +74 -0
- package/guidelines/ssrRule/ssrRule.instruction.md +4 -2
- package/guidelines/transportRule/transportRule.instruction.md +10 -8
- package/guidelines/workspaceRecipes/workspaceRecipes.instruction.md +4 -3
- package/incrementalBuilder.proc.js +3 -2
- package/{index-4n4821gp.js → index-10dkrrww.js} +4 -4
- package/{index-bzakk0c2.js → index-14zrtgfa.js} +5 -3
- package/{index-v17gj504.js → index-18zybgkc.js} +2 -2
- package/{index-qh7brn20.js → index-22xtgmhn.js} +6 -2
- package/{index-qhtj9y6j.js → index-2446k7b4.js} +74 -21
- package/{index-g9zghs4h.js → index-3jrp517k.js} +12 -12
- package/{index-vpx68e4x.js → index-4m929vvn.js} +2 -2
- package/{index-4cvpzkq6.js → index-53176s7j.js} +3 -3
- package/{index-jwrpsrg3.js → index-667w0yah.js} +1 -1
- package/{index-3mg4h788.js → index-7ep8qtvp.js} +2 -2
- package/{index-w8kmx1b6.js → index-89hy31kf.js} +30 -7
- package/index-9d6nvkz2.js +656 -0
- package/index-9dp031by.js +12 -0
- package/{index-zn8qgeqx.js → index-a1w6934z.js} +3 -3
- package/{index-p0tew3fq.js → index-ab0c38ea.js} +10 -5
- package/index-bc88j1ry.js +1166 -0
- package/{index-ns128c4q.js → index-bftkz6cp.js} +280 -73
- package/{index-kcxmv48c.js → index-bhtmj233.js} +4 -1
- package/{index-kvzjda3v.js → index-bwf965h6.js} +5 -5
- package/{index-p4p9t45x.js → index-crmeqnmf.js} +56 -44
- package/{index-8k16atqy.js → index-d2242ksy.js} +23 -6
- package/{index-2sq06vv0.js → index-e4v6vhxq.js} +417 -105
- package/{index-xjc7gg65.js → index-esk82rvk.js} +23 -33
- package/{index-sn7dnzq4.js → index-kwaq3n9n.js} +1 -1
- package/{index-gm76f0qb.js → index-n3cv0tm4.js} +1 -1
- package/{index-6na0sd2b.js → index-qdgzsnhg.js} +5 -2
- package/{index-twq9p2sp.js → index-s5rhm04y.js} +0 -6
- package/{index-n8kt2wh6.js → index-sq2dzkzm.js} +35 -10
- package/{index-myj01w9r.js → index-srmay5jw.js} +15 -2
- package/{index-2pm1j01n.js → index-txr7dj2g.js} +4 -4
- package/{index-3cymrzsb.js → index-vqx8bdxz.js} +17 -3
- package/{index-te0y1c0m.js → index-zqgj2rt2.js} +12 -7
- package/index.js +21 -21
- package/{library.command-m8p2r1dh.js → library.command-cqr175dy.js} +3 -3
- package/{localRegistry.command-yppdqrxv.js → localRegistry.command-49z55qqz.js} +9 -9
- package/{module.command-mfzvqt0n.js → module.command-9p8mm3b0.js} +5 -5
- package/{package.command-cgp2p2v5.js → package.command-wxyz909t.js} +5 -5
- package/package.json +2 -2
- package/{page.command-taenb02b.js → page.command-gsrsfqqs.js} +3 -3
- package/{primitive.command-7kpncmkk.js → primitive.command-ns1mptje.js} +6 -6
- package/{prompter-9mn3kp36.js → prompter-1f8d9pcc.js} +1 -1
- package/{quality.command-1ar94hrw.js → quality.command-e76k2q9k.js} +18 -17
- package/{repair.command-n08t6j8p.js → repair.command-pkrsm3cw.js} +5 -5
- package/{routeSourceValidator-apyqw7wk.js → routeSourceValidator-7ycnfz0e.js} +0 -6
- package/{scalar.command-mq8qfp4t.js → scalar.command-0cpt3fc8.js} +4 -4
- package/skills/akan-code/SKILL.md +122 -0
- package/skills/akan-module/SKILL.md +2 -2
- package/skills/akan-store/SKILL.md +6 -2
- package/{subspace.command-jqv5n79w.js → subspace.command-fktpwp0g.js} +20 -10
- package/templates/__scalar/__model__/__model__.abstract.md.template +8 -0
- package/templates/appSample/lib/__scalar/workHistory/workHistory.abstract.ts +8 -39
- package/templates/appSample/lib/_noti/noti.abstract.ts +6 -29
- package/templates/appSample/lib/task/task.abstract.ts +12 -32
- package/templates/appSample/lib/task/task.signal.spec.ts +15 -18
- package/templates/appSample/lib/task/task.signal.test.ts +15 -1
- package/templates/appSample/lib/task/task.signal.ts +1 -1
- package/templates/appSample/srvkit/AuthGuard.ts +4 -1
- package/templates/appSample/srvkit/SessionInternalArg.ts +3 -1
- package/templates/crudPages/[__model__Id]/edit/page.tsx +2 -1
- package/templates/crudPages/[__model__Id]/page.tsx +2 -1
- package/templates/crudPages/new/page.tsx +2 -1
- package/templates/crudPages/page.tsx +2 -1
- package/templates/crudSinglePage/page.tsx +2 -1
- package/templates/localDev/docker-compose.yaml.template +3 -14
- package/templates/module/__model__.signal.ts +12 -4
- package/templates/pkgRoot/tsconfig.json.template +2 -7
- package/templates/service/__model__.abstract.md.template +8 -0
- package/{tunnel.command-ht2es78c.js → tunnel.command-d5jy9gk8.js} +4 -4
- package/{workflow.command-tvz4nm9n.js → workflow.command-6tkcysmx.js} +9 -9
- package/{workspace.command-txv1yv5e.js → workspace.command-gv84eh1p.js} +25 -24
- package/cloud.script-ch3xrw3s.js +0 -20
- package/context.runner-bys6ykcp.js +0 -22
- package/guidelines/agentRule/agentRule.generate.json +0 -31
- package/guidelines/componentRule/componentRule.generate.json +0 -31
- package/guidelines/conventions/conventions.generate.json +0 -25
- package/guidelines/cssRule/cssRule.generate.json +0 -31
- package/guidelines/docPageRule/docPageRule.generate.json +0 -31
- package/guidelines/docPageRule/docPageRule.instruction.md +0 -25
- package/guidelines/docSyncRule/docSyncRule.generate.json +0 -31
- package/guidelines/docSyncRule/docSyncRule.instruction.md +0 -25
- package/guidelines/enumConstant/enumConstant.generate.json +0 -31
- package/guidelines/enumConstant/enumConstant.instruction.md +0 -28
- package/guidelines/fieldRule/fieldRule.generate.json +0 -31
- package/guidelines/framework/framework.generate.json +0 -32
- package/guidelines/mcpRule/mcpRule.generate.json +0 -35
- package/guidelines/modelConstant/modelConstant.generate.json +0 -32
- package/guidelines/modelDictionary/modelDictionary.generate.json +0 -32
- package/guidelines/modelDocument/modelDocument.generate.json +0 -31
- package/guidelines/modelService/modelService.generate.json +0 -31
- package/guidelines/modelSignal/modelSignal.generate.json +0 -31
- package/guidelines/modelStore/modelStore.generate.json +0 -31
- package/guidelines/modelTemplate/modelTemplate.generate.json +0 -31
- package/guidelines/modelUnit/modelUnit.generate.json +0 -31
- package/guidelines/modelUtil/modelUtil.generate.json +0 -31
- package/guidelines/modelView/modelView.generate.json +0 -31
- package/guidelines/modelZone/modelZone.generate.json +0 -31
- package/guidelines/moduleCodegen/moduleCodegen.generate.json +0 -31
- package/guidelines/moduleCodegen/moduleCodegen.instruction.md +0 -28
- package/guidelines/moduleOverview/moduleOverview.generate.json +0 -31
- package/guidelines/queryRule/queryRule.generate.json +0 -31
- package/guidelines/recipeRule/recipeRule.generate.json +0 -31
- package/guidelines/runtimeRule/runtimeRule.generate.json +0 -25
- package/guidelines/scalarConstant/scalarConstant.generate.json +0 -32
- package/guidelines/scalarDictionary/scalarDictionary.generate.json +0 -32
- package/guidelines/scalarModule/scalarModule.generate.json +0 -31
- package/guidelines/sharedUiUsage/sharedUiUsage.generate.json +0 -31
- package/guidelines/sharedUiUsage/sharedUiUsage.instruction.md +0 -25
- package/guidelines/ssrRule/ssrRule.generate.json +0 -31
- package/guidelines/transportRule/transportRule.generate.json +0 -31
- package/guidelines/utilUiUsage/utilUiUsage.generate.json +0 -31
- package/guidelines/utilUiUsage/utilUiUsage.instruction.md +0 -25
- package/guidelines/workspaceOnboarding/workspaceOnboarding.generate.json +0 -24
- package/guidelines/workspaceRecipes/workspaceRecipes.generate.json +0 -34
- package/index-57p9p179.js +0 -461
- package/index-qt8dqgnf.js +0 -250
- package/templates/__scalar/__model__/__model__.abstract.md +0 -28
- package/templates/module/__model__.abstract.md +0 -28
- package/templates/service/__model__.abstract.md +0 -28
|
@@ -96,15 +96,21 @@ apps and libs never import it directly (`no-import-external-library`) — everyt
|
|
|
96
96
|
nothing about, and it stays text-only until `option.setLlm({ accepts })` says otherwise — handing bytes to a
|
|
97
97
|
model that cannot decode them kills the whole turn on a 400, where text-only degrades them to a note the model
|
|
98
98
|
can repeat back.
|
|
99
|
-
- **A refusal is
|
|
100
|
-
rides in `reason` and the hostname in `provider
|
|
101
|
-
|
|
102
|
-
|
|
99
|
+
- **A refusal is `agent.error.llmRequestFailed`, and it names the host** — unless the prompt did not fit. The
|
|
100
|
+
provider's own sentence rides in `reason` and the hostname in `provider`. `LlmOverflow.refusal(host, status,
|
|
101
|
+
reason)` builds it, and reads the one refusal a chat can undo — a prompt past the window, recognized from each
|
|
102
|
+
provider's own sentence — as `agent.error.contextOverflow` with the `limit` it named, which the relay flags so
|
|
103
|
+
the chat compacts and asks again. An adaptor an app wrote throws through the same function, so its refusals
|
|
104
|
+
translate and recover the way the shipped ones do without a key in the framework's dictionary it cannot add.
|
|
103
105
|
- **`accepts` is answered per model, through `option.setLlm({ accepts })`.** An adaptor answers for an API and one
|
|
104
106
|
API serves models that differ, so the override rides beside the `model` it is a fact about. It is not a table
|
|
105
107
|
the framework keeps: a table is a claim about models that ship after it, and getting this wrong is the worst
|
|
106
108
|
failure available — a provider handed bytes it cannot decode either refuses the turn or accepts it having seen
|
|
107
109
|
nothing, and the model then answers confidently about a file it never read.
|
|
110
|
+
- **`contextWindow` is declared the same way: `option.setLlm({ contextWindow })`.** Every turn relays it with the
|
|
111
|
+
answer ceiling the adaptor actually sends (`LlmAdaptor.limits` — `AnthropicLlm` always sends one, `OpenaiLlm`
|
|
112
|
+
none), and the chat guards its window with it. It is not a table either; a model id is retired or renamed long
|
|
113
|
+
before a table notices.
|
|
108
114
|
- **An adaptor answers `null` for "not configured" and *throws* for a refusal it can explain.** The two are
|
|
109
115
|
different things to be told: collapsing both into `null`, the way the adapter convention otherwise reads, left a
|
|
110
116
|
user reading `llmUnavailable` — "no model is configured" — about a conversation that had merely outgrown the
|
|
@@ -539,18 +545,29 @@ apps and libs never import it directly (`no-import-external-library`) — everyt
|
|
|
539
545
|
- **A long conversation summarizes itself, because nothing else is keeping it inside the model's window.** The
|
|
540
546
|
loop runs in the browser and the relay holds no session, so an uncompacted chat grows until the provider
|
|
541
547
|
refuses the whole request — a refusal, never a shorter answer, which is why compaction runs *before* the turn
|
|
542
|
-
that would have overflowed
|
|
543
|
-
characters to a token, over the JSON the turn posts
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
548
|
+
that would have overflowed. **Two triggers, each before every assistant turn.** `compact.at` is a ceiling on
|
|
549
|
+
cost: past that many estimated transcript tokens (four characters to a token, over the JSON the turn posts —
|
|
550
|
+
except an inlined picture, counted at the ~1,600 tokens a provider bills for its pixels rather than its base64;
|
|
551
|
+
24k by default) the history compacts, because the relay resends the whole transcript every turn and the app
|
|
552
|
+
pays for each one. The **window guard** compacts once the prompt nears the window the server reports —
|
|
553
|
+
`window − answer ceiling (8,192 when unreported) − compact.buffer (13,000, the margin Claude Code leaves)` —
|
|
554
|
+
measured by the provider's own count of the last turn (`done.usage`, kept on that assistant message) plus the
|
|
555
|
+
four-character estimate for what arrived since, so a Korean transcript the character rule reads as small is
|
|
556
|
+
still caught. The window comes from `option.setLlm({ contextWindow })` or from the first refusal that names it;
|
|
557
|
+
with neither, only the ceiling runs. **A refusal for length is answered once**: a relay flags it `overflow` on
|
|
558
|
+
the wire, and the session compacts and sends the same turn again, and a second refusal in the same send fails
|
|
559
|
+
with `agent.error.contextOverflow`. Either way the history above the last `keep` messages
|
|
560
|
+
becomes one message standing in for it, flagged `summary` on the wire — `<Agent.Chat compact={{ at, keep, buffer }} />`
|
|
561
|
+
tunes it, `{ at: Infinity }` leaves only the window guard, `{ at: 0 }` turns off the ceiling, the guard and the recovery alike, and `/compact` does
|
|
562
|
+
the same on demand keeping nothing. The header draws the estimate against whichever trigger is nearer.
|
|
547
563
|
**The cut never lands on a `tool` message**: the kept half may not open with a result whose call was summarized
|
|
548
564
|
away, a shape every provider dialect rejects. A user message is preferred where the tail holds one — everything
|
|
549
565
|
above it is settled — but one assistant turn that ran ten tools has none, and that is the transcript that
|
|
550
566
|
outgrows the window, so a cut there opens on the assistant message instead. The summarizing
|
|
551
567
|
turn carries no tools and no screen context — it summarizes the conversation, it does not act on it — and it is
|
|
552
568
|
fed a *bounded* digest rather than the transcript itself, since the transcript being summarized is the one that
|
|
553
|
-
no longer fits.
|
|
569
|
+
no longer fits. The previous summary is the exception: it rides the digest whole, under its own label and outside
|
|
570
|
+
the bound, because it is the only record of what was compacted before it. A summary that cannot be produced leaves the transcript alone, the turn goes out as it would
|
|
554
571
|
have, and the next one asks again — a summarizer that was momentarily unreachable says nothing about whether
|
|
555
572
|
this transcript can shrink. One that *landed* and shrank nothing does, so that one is not retried until another
|
|
556
573
|
threshold's worth has been added. On the
|
|
@@ -28,7 +28,7 @@ you fetch on demand — `get_guideline` with the name, or `akan guideline show <
|
|
|
28
28
|
| name | covers |
|
|
29
29
|
|---|---|
|
|
30
30
|
| `ssrRule` | server-share targets, the `akan.ssr.*` warnings, the client-boundary playbook |
|
|
31
|
-
| `runtimeRule` | `web` / `csr` surfaces, gateway vs solo processes, logging, the generated image, shipped assets |
|
|
31
|
+
| `runtimeRule` | `web` / `csr` surfaces, gateway vs solo processes, logging, the generated image, shipped assets, database modes |
|
|
32
32
|
| `queryRule` | slices and hydration, the generated filter methods, full-text search, cascade removal |
|
|
33
33
|
| `transportRule` | guards across HTTP and websocket, socket identity and cleanup, binary pubsub, mutation verbs |
|
|
34
34
|
| `mcpRule` | MCP configuration, wire behaviour, resource URIs, OAuth metadata, protocol revisions |
|
|
@@ -273,8 +273,10 @@ are, because those are the ones the server could have done.
|
|
|
273
273
|
SEO snapshots, prerendering and pre-hydration E2E read, so await what the page needs immediately and stream
|
|
274
274
|
the rest. `xListInY` / `xInsightInY` hold hydrated model instances that React Flight refuses as client props:
|
|
275
275
|
consume them in a server component, never as a `Zone` prop.
|
|
276
|
-
6. **Use named `ReactNode` slots, not just `children`** —
|
|
277
|
-
|
|
276
|
+
6. **Use named `ReactNode` slots, not just `children`** — a client shell that takes one slot per region lets a
|
|
277
|
+
server component supply each of them, instead of absorbing the whole subtree into the bundle. `Layout.Navbar`
|
|
278
|
+
declares `title` / `left` / `right` alongside `back` and `children`, but renders only the last two today, so
|
|
279
|
+
copy the shape rather than the component.
|
|
278
280
|
7. **Let the server do the derived work.** Display and predicate logic belongs on `Light<Model>`; enum→class
|
|
279
281
|
lookups belong in a module-scope `as const` map.
|
|
280
282
|
8. **Gate auth on the server** — `getSelf({ unauthorize: "/signin" })` in `_layout.tsx`, before any HTML is sent.
|
|
@@ -335,6 +337,14 @@ Full contract: `get_guideline` with `runtimeRule`, or `akan guideline show runti
|
|
|
335
337
|
- **`assets: { pruneFonts, keepFonts }`** trims from the `dist` copy of `public/` the fonts nothing reads; source
|
|
336
338
|
trees are never touched. A font with `optimize` on is a build input, not a runtime asset. `keepFonts` belongs to
|
|
337
339
|
the `akan.config.ts` that owns the font, written against that scope's own `public/`.
|
|
340
|
+
- **Database modes are `database: { modes: [...] }`** — `single` (SQLite + Solid), `multiple` (one SQLite file on
|
|
341
|
+
a host volume + Redis), `cluster` (Postgres + Redis). The build carries the drivers of every declared mode and a
|
|
342
|
+
deployment picks one with `AKAN_DATABASE_MODE` — a declared one, named whenever the build declares several.
|
|
343
|
+
Where the data lives (`POSTGRES_URL`, `REDIS_URI`, `SQLITE_DATABASE_PATH`) is the deployment's env, ahead of
|
|
344
|
+
`env.server.ts`. `q.raw` is written in the dialect's own SQL, so an app running in two modes avoids it.
|
|
345
|
+
- **Backups run through the image's own `main.js`**: `bun main.js ops snapshot` / `ops restore` (offline), and
|
|
346
|
+
`/_akan/ops/*` for a control plane, mounted only when `AKAN_OPS_PUBLIC_KEY` is set and authenticated by a short
|
|
347
|
+
EdDSA token that key verifies. `GET /_akan/app/info` is public. `operationMode` gates none of it.
|
|
338
348
|
|
|
339
349
|
## React Components And Styling (`**/*.tsx`)
|
|
340
350
|
|
|
@@ -357,7 +367,7 @@ Full contract: `get_guideline` with `runtimeRule`, or `akan guideline show runti
|
|
|
357
367
|
nothing — they covered five control types, keyed on the translated label, and restored over server data.
|
|
358
368
|
- Static class strings stay plain strings. Reach for `cn` only for a conditional or to merge an incoming `className`, and merge the caller last: `cn("base classes", cond && "extra", className)`. `cn` comes from `akanjs/client` (token-aware tailwind-merge) and is the only class-combining function — no `clsx` (removed), no raw `twMerge` imports, no object syntax (`{ x: cond }` → `cond && "x"`).
|
|
359
369
|
- Multi-slot components take extra named props (`wrapperClassName`, `bodyClassName`), never a `classNames` object.
|
|
360
|
-
- Hoist enum→class lookups to a module-scope `as const` map typed `{ [key in cnst.XStatus["value"]]: string }`. Do not use `Record<...>`.
|
|
370
|
+
- Hoist enum→class lookups to a module-scope `as const` map typed `{ [key in cnst.XStatus["value"]]: string }`. Do not use `Record<...>`. When a second module needs it, the classes become a variant axis of a recipe in `ui/Recipe/`; only a map from the enum to another recipe's variant name (`BadgeVariants["variant"]`) moves to `webkit/`.
|
|
361
371
|
- Use `<Link>` from `akanjs/ui` for internal navigation; `<a>` only for `mailto:` and external links.
|
|
362
372
|
|
|
363
373
|
## Naming And Language
|
|
@@ -421,8 +431,11 @@ stores need none, because state and CRUD actions are generated. The body is thre
|
|
|
421
431
|
`this.setX(...)` → toast. The optimistic shape is mutate the client model, `void fetch.*`, then commit. Use
|
|
422
432
|
`this.pick(...)` when the value must exist, `this.get()` when it may not, and `this.set({...})` to write. Mutate lists
|
|
423
433
|
through the collection API (`this.set({ xList: xList.set(x).save() })`), not array spread. **An action returns
|
|
424
|
-
nothing** (`no-return-in-store-action.grit`). **
|
|
425
|
-
`
|
|
434
|
+
nothing** (`no-return-in-store-action.grit`). **Reach another store through `this`:**
|
|
435
|
+
`import type { RootStore } from "../st"`, then `(this as unknown as RootStore).logout()` to call its action or
|
|
436
|
+
`(this as unknown as RootStore).set({ … })` / `.get()` to write or read its state. Every store is mixed into one root
|
|
437
|
+
at runtime, so the cast only tells the type what `this` already is. Keep it `import type` — `st.ts` imports every
|
|
438
|
+
store, so a value import is a cycle.
|
|
426
439
|
|
|
427
440
|
**`<model>.dictionary.ts`** — fixed chain, with empty stages still written:
|
|
428
441
|
`.of() → .model() → .insight() → .query() → .sort() → .enum() → .slice() → .endpoint() → .error() → .translate()`.
|
|
@@ -475,13 +488,14 @@ Full contract — credential handshake, room revalidation, socket cleanup scopin
|
|
|
475
488
|
never bounded. **Losing the race does not cancel the work**: the handler runs to completion with nobody
|
|
476
489
|
holding its result, so a deadline is an answer to the caller, not an undo.
|
|
477
490
|
- **A cached answer is the endpoint's `{ cache: <ms> }`, and only a `query` taking no internal argument may have
|
|
478
|
-
one.**
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
handler's result, so `resolveReturn` still masks fields and resolves
|
|
483
|
-
is down is warned about and the call runs uncached. The default
|
|
484
|
-
|
|
491
|
+
one.** It is a step of the call, not a middleware, and stands aside for every endpoint that declares nothing.
|
|
492
|
+
Internal arguments are how a call learns who is asking (`.with(Self)`), so an endpoint that has them answers
|
|
493
|
+
per caller and one shared entry would be one caller's answer handed to the next — that endpoint and every
|
|
494
|
+
`mutation` are named in the log and left uncached. The lookup runs after the guards, so a hit reaches only a
|
|
495
|
+
caller they admitted; what is cached is the handler's result, so `resolveReturn` still masks fields and resolves
|
|
496
|
+
relations per call; a cache backend that is down is warned about and the call runs uncached. The default
|
|
497
|
+
middleware chain is `Logging → Timeout → <lib middlewares>`, then guards → internal arguments → cache →
|
|
498
|
+
handler, and there is no `Retry` middleware.
|
|
485
499
|
|
|
486
500
|
### Authorization Defaults
|
|
487
501
|
|
|
@@ -548,8 +562,11 @@ Full contract — filter-arg `ref` pickers, `getQueryMeta` summary counters, `la
|
|
|
548
562
|
first "more". `isCumulativeOf<Model>` is the mode.
|
|
549
563
|
- **"Is there more" is `hasMoreOf<Model>`, never `count > page * limit`.** It comes from the length of the batch
|
|
550
564
|
the server returned, so it survives `{ insight: false }` and cannot drift from a count a live event moved.
|
|
551
|
-
- **Hydrated vs raw:** server queries return hydrated `cnst.<Model>` instances (with `set`/`save`/`refresh`)
|
|
552
|
-
client fetch results
|
|
565
|
+
- **Hydrated vs raw:** server queries return hydrated `cnst.<Model>` instances (with `set`/`save`/`refresh`),
|
|
566
|
+
and so do client fetch results — `FetchClient` parses every return with `crystalize` defaulting to **true**
|
|
567
|
+
(`pkgs/akanjs/fetch/client/fetchClient.ts`). The raw `GetStateObject` shape is what the `<ref>Obj` /
|
|
568
|
+
`<ref>ObjList` / `<ref>ObjInsight` handles carry, because those pass `crystalize: false` on purpose so the
|
|
569
|
+
value can cross the RSC boundary as a client prop.
|
|
553
570
|
- Every filter generates fourteen methods: `list` · `listIds` · `find` · `findId` · `pick` · `pickId` · `exists` ·
|
|
554
571
|
`count` · `insight` · `query` · **`remove`** · **`removeOne`** · **`update`** · **`updateOne`**. The last four
|
|
555
572
|
are query-level writes — one atomic UPDATE, **no hooks**, and therefore no `_postRemove` and no cascade. Use
|
|
@@ -587,7 +604,7 @@ Full contract — relevance ordering, `columns` / `weights`: `get_guideline` wit
|
|
|
587
604
|
- Injected dependencies resolve by field-name convention: a field named `<refName>Service` resolves to the service registered under `<refName>`, and `<refName>Signal` likewise (`pkgs/akanjs/service/injectInfo.ts`).
|
|
588
605
|
- The `Service`/`Signal` suffix is required — the injector strips it to derive the registry lookup key. Name the field after the target refName plus the suffix, not arbitrarily.
|
|
589
606
|
- Preference order inside a service: `service<srv.XService>()` for another module's service · `plug(AdapterClass)` or `plug(StorageAdaptorRole)` for an adapter · `use<T>()` only to reach an `option.ts`-registered legacy singleton · `env(...)` for config.
|
|
590
|
-
- **`memory(...)` takes a scalar or model class, not only a primitive** — `memory(Map, { of: CallStateInput })` serializes through the constant and travels as JSON text, so the Redis and sqlite-backed caches round-trip the same declaration. Never hand-encode JSON into a `String` memory.
|
|
607
|
+
- **`memory(...)` takes a scalar or model class, not only a primitive** — `memory(Map, { of: CallStateInput })` serializes through the constant and travels as JSON text, so the Redis and sqlite-backed caches round-trip the same declaration. Never hand-encode JSON into a `String` memory. A memory is scoped to the service or adaptor that declares it; `ttl` (milliseconds) sets how long each write lives unless the `set` call names its own `expireAt`, and a `Map` entry expires on its own.
|
|
591
608
|
|
|
592
609
|
### Adapters — `adapt()` And `plug()`
|
|
593
610
|
|
|
@@ -663,9 +680,11 @@ it.
|
|
|
663
680
|
names itself as the issuer, so `/mcp` demands a bearer token, verifies its signature, and refuses one carrying no
|
|
664
681
|
`aud`. A token is the app's own access JWT plus `aud`/`iss`/`client_id`, so `AccountMiddleware` and the guards
|
|
665
682
|
judge it unchanged; there is no scope. Per-app knobs live in `env.server.*` under `oauth` (`consentPath` and
|
|
666
|
-
`signinPath` with the basePath, `clients`, `dynamicRegistration`, `allowedRedirectSchemes`, `
|
|
667
|
-
`
|
|
668
|
-
|
|
683
|
+
`signinPath` with the basePath, `clients`, `dynamicRegistration`, `allowedRedirectSchemes`, `accessTokenSeconds`,
|
|
684
|
+
`clientIdMetadata`, `enabled`, plus `issuer` / `resource` — set those when a tunnel or an edge makes the derived
|
|
685
|
+
origin wrong, because an MCP client compares the issuer byte for byte). `/mcp` reads the `Authorization` header only — a cookie is dropped at the door — and `JWT_SECRET` (or
|
|
686
|
+
`security.jwtSecret`) is mandatory whenever `operationMode` is not `local`, because the derived fallback is
|
|
687
|
+
forgeable from three public names; `AKAN_ALLOW_DERIVED_JWT_SECRET=1` accepts that risk explicitly. A grant is revoked
|
|
669
688
|
whole — `POST /oauth/revoke` (RFC 7009) by the client, `fetch.revokeOAuthConnection(sessionId)` by the account's
|
|
670
689
|
owner, `fetch.listOAuthConnections()` to see them — and a revoked access token is refused at its next call.
|
|
671
690
|
- **A person may, a model may not.** An agent's token names its `client_id` and `aud`; a browser session has
|
|
@@ -782,7 +801,8 @@ Full contract — the trigger-maintained mirror, tokenizer changes, `AKAN_SEARCH
|
|
|
782
801
|
backstop, including for a `text` field *underneath* one of those. Do not work around either.
|
|
783
802
|
- The role works on a relation (`image: field(File, { text: "thumb" })`) and on an array; an array of objects
|
|
784
803
|
indexes by leaf key. A field inside a `Map` indexes nothing — there is no fixed path to extract it from.
|
|
785
|
-
- Search runs
|
|
804
|
+
- Search runs in every database mode — fts5 on SQLite/libSQL, a weighted `tsvector` (`pg_trgm` for `trigram`) on
|
|
805
|
+
Postgres. The same text matches the same documents; only the order may differ on Postgres.
|
|
786
806
|
|
|
787
807
|
### Image & File Fields
|
|
788
808
|
|
|
@@ -831,10 +851,11 @@ shape, so `cascade` never means "related" — it means one of exactly these:
|
|
|
831
851
|
- Dynamic segments use `[id]`; route groups use directories like `(user)`, `(public)`, `(tab)`, or `(detail)`.
|
|
832
852
|
- **A route file exports one thing**: `export default page()…render(…)` for a page, `layout()…render(…)` for a
|
|
833
853
|
`_layout.tsx`, `rootLayout()…render(…)` for the app's (or a basePath's) root `_layout.tsx`. Every route setting
|
|
834
|
-
is a stage of the chain — `.param()`, `.search()`, `.config()`, `.head()
|
|
835
|
-
`.
|
|
836
|
-
|
|
837
|
-
|
|
854
|
+
is a stage of the chain — `.param()`, `.search()`, `.config()`, `.head()`, `.loading()`; `.notFound()` /
|
|
855
|
+
`.error()` on a layout; `.fonts()` / `.theme()` / `.manifest()` / `.reconnect()` / `.wsConnect()` /
|
|
856
|
+
`.layoutStyle()` on the root layout; `.prompt()` on a page. A named export beside the chain, or `page()` in a
|
|
857
|
+
`_layout.tsx`, fails the build. `.head()` takes JSX — `<title>`, `<meta>`, `<link>` — or a function of the
|
|
858
|
+
route's args returning it; there is no metadata object, and analytics is the app's own script, not a stage.
|
|
838
859
|
- **A page names every `[x]` segment of its path with `.param("x", Type)`** and reads a query key only through
|
|
839
860
|
`.search("k", Type)`. Values arrive typed — `ID`/`String` → string, `Int`/`Float` → number, `Boolean`, `Date` →
|
|
840
861
|
Dayjs, an `enumOf` class → its union, `[T]` → array — a path value the type refuses answers not-found, and a
|
|
@@ -906,10 +927,13 @@ export default page()
|
|
|
906
927
|
| `common/` | pure, isomorphic, zero-dependency; may import only sibling `common/*` and `akanjs/base`. Cannot import `Err`, so keep throwing code out of it. | camelCase file, filename equals the single export |
|
|
907
928
|
| `webkit/` | touches `window` / `navigator` / Capacitor, or is a React hook | `use<Thing>.tsx` — `.tsx` even with no JSX |
|
|
908
929
|
| `srvkit/` | touches `node:*`, `Bun`, `process.env`, a secret, or a server SDK | camelCase file, PascalCase class |
|
|
909
|
-
| `ui/` | renders JSX and is not bound to one model | PascalCase component, camelCase sidecar (`swipeCard.util.ts`) |
|
|
930
|
+
| `ui/` | renders JSX, or defines a look (a recipe in `Recipe/`, a lib's `tokens.css`), and is not bound to one model | PascalCase component, camelCase sidecar (`swipeCard.util.ts`), `Recipe/<name>.ts` |
|
|
910
931
|
| `plugin/` | build- or CLI-time `AkanPlugin` | `<name>.plugin.ts`, registered in `akan.config.ts` |
|
|
911
932
|
|
|
912
933
|
- Hooks return a named object of async closures, never a tuple.
|
|
934
|
+
- `ui/` is the presentation layer — markup and the looks it is built from — so a recipe lives there although it is
|
|
935
|
+
neither a component nor a hook. Not `webkit/`: its hooks are `"use client"`, the opposite signal for a function
|
|
936
|
+
server components call. Not `common/`: it cannot import `akanjs/ui`, where `recipe` / `tv` come from.
|
|
913
937
|
- `libs/<lib>/ui/tokens.css` is the one CSS file a lib owns: plain `:root` custom properties for colors that must **not** follow the theme (a vendor brand color, a fixed surface). Every app whose pages reach that lib compiles it automatically, ahead of the app's own stylesheets, so nothing is imported by hand and no app can forget it. Reference them as `bg-[var(--kakao)]`; `@theme` extensions stay in the app stylesheet, because the color vocabulary is closed per stylesheet. Theme-following colors are the app's, not the lib's.
|
|
914
938
|
- A layer-root `index.ts` is generated, but a `ui/<Folder>/index.tsx` that builds a namespace is hand-written source. The distinguishing test is that a generated barrel contains nothing but `export * from "./X";` lines.
|
|
915
939
|
- `ui/<Folder>/index_.tsx` (trailing underscore) is the `"use client"` + `lazy()` boundary, with a server-safe `index.tsx` beside it. Collapsing the pair into one file breaks RSC.
|
|
@@ -922,12 +946,15 @@ when two shapes disagree.
|
|
|
922
946
|
- `children: any` — newer files use `children: ReactNode`.
|
|
923
947
|
- Hard-coded API keys or secrets in source; they belong in `option.ts` or env.
|
|
924
948
|
- Large blocks of commented-out code left in place.
|
|
925
|
-
- Cross-store writes through a `RootStore` cast — it collides with the Bun SSR-bundler segfault.
|
|
926
949
|
- Stale `// TODO: Implement …` comments above implemented methods.
|
|
927
950
|
- `{cond && <X/>}` in JSX, hard-coded Korean bypassing `l()`, and `window.alert(...)` for user feedback.
|
|
928
951
|
- Bare `/* eslint-disable */` blocks — use `// biome-ignore lint/<rule>: <why>`.
|
|
929
952
|
- Raw palette grays such as `text-gray-400` instead of the semantic tokens (`text-foreground/70`).
|
|
930
953
|
|
|
954
|
+
A store calling `(this as unknown as RootStore).<action>()` or `.set({ … })` over
|
|
955
|
+
`import type { RootStore } from "../st"` is **not** one of these — it is the sanctioned way to reach another store's
|
|
956
|
+
actions and state (see `<model>.store.ts` above).
|
|
957
|
+
|
|
931
958
|
## Secrets And Env Safety (`.env`, `infra/**`, `*secret*`, `*credential*`)
|
|
932
959
|
|
|
933
960
|
- Never print, summarize, commit, or expose real secret values, credentials, tokens, private keys, or `.env` contents.
|
|
@@ -49,7 +49,7 @@ with it. `conventions` carries the invariants — this is the full contract behi
|
|
|
49
49
|
`findBy<Filter>(...args, { select: { secretField: true } })` takes it inside the option object. The shapes do not
|
|
50
50
|
swap: `{ select: … }` handed to the facade projects a field named `select`, which no model has, and the read comes
|
|
51
51
|
back empty with no error. This is the only way to read a `field.secret(...)` value, which is otherwise stripped.
|
|
52
|
-
- **Hydrated vs raw:** server queries return hydrated `cnst.<Model>` instances (with `set`/`save`/`refresh`)
|
|
52
|
+
- **Hydrated vs raw:** server queries return hydrated `cnst.<Model>` instances (with `set`/`save`/`refresh`), and so do client fetch results — `FetchClient` parses every return with `crystalize` defaulting to **true** (`pkgs/akanjs/fetch/client/fetchClient.ts`). The raw `GetStateObject` shape (functions stripped, `pkgs/akanjs/base/types.ts`) is what the `<ref>Obj` / `<ref>ObjList` / `<ref>ObjInsight` handles carry, because those pass `crystalize: false` on purpose so the value can cross the RSC boundary as a client prop.
|
|
53
53
|
- Every filter generates fourteen methods: `list` · `listIds` · `find` · `findId` · `pick` · `pickId` · `exists` ·
|
|
54
54
|
`count` · `insight` · `query` · **`remove`** · **`removeOne`** · **`update`** · **`updateOne`**. The last four are
|
|
55
55
|
query-level writes — one atomic UPDATE, **no hooks**, and therefore no `_postRemove` and no cascade. Use them on a
|
|
@@ -90,14 +90,15 @@ with it. `conventions` carries the invariants — this is the full contract behi
|
|
|
90
90
|
- **A slice endpoint never reaches "unspecified".** The resolver fills `latest` before the query is built, so a
|
|
91
91
|
client asking for the score order has to name `relevance`; leaving `sort` off gets `latest`, not relevance.
|
|
92
92
|
- Scope a search with `columns` (`q.search(text, { columns: ["title"] })`) and re-weight with `weights`, a tuple of
|
|
93
|
-
finite numbers positional over `["title", "desc", "tag", "filter"]
|
|
93
|
+
finite, non-negative numbers positional over `["title", "desc", "tag", "filter"]` — Postgres ranks by weight
|
|
94
|
+
class and has no reading for a negative one.
|
|
94
95
|
|
|
95
96
|
|
|
96
97
|
## Text Search Fields — the `text` role
|
|
97
98
|
|
|
98
99
|
- A field joins the full-text index by declaring one of five roles: `field(String, { text: "title" })`, and likewise
|
|
99
100
|
`"desc"`, `"tag"`, `"thumb"`, `"filter"`. Nothing else opts a field in, and there is no per-model switch.
|
|
100
|
-
- Pick the role by what the value *is*, because
|
|
101
|
+
- Pick the role by what the value *is*, because the rank weights them positionally (`title` 10, `tag` 3, `desc` 1,
|
|
101
102
|
`filter` 0): `title` is the one line a human scans for, `desc` is prose, `tag` is a keyword list, `filter` is a
|
|
102
103
|
scoping value (status, owner, role) that must be matchable but must never outrank a real title hit.
|
|
103
104
|
- `thumb` is mirrored for rendering a hit and is **not** indexed — never expect it to match.
|
|
@@ -114,15 +115,24 @@ with it. `conventions` carries the invariants — this is the full contract behi
|
|
|
114
115
|
- Declaring roles is all the wiring there is. Mirror rows are maintained by SQL triggers — not document hooks —
|
|
115
116
|
because `updateOneByQuery` and friends fire no hooks, and most searchable-field mutations go through exactly that
|
|
116
117
|
path.
|
|
117
|
-
- Search runs on
|
|
118
|
+
- Search runs in every database mode: fts5 on SQLite/libSQL, a weighted `tsvector` under a GIN index on Postgres.
|
|
119
|
+
For the same text the same documents match — punctuation splits words the way unicode61 does, and a phrase never
|
|
120
|
+
runs from one column into the next — but the order may differ, since Postgres ranks without document frequency.
|
|
121
|
+
Assert on the match set, never on a Postgres order the weights alone do not decide.
|
|
122
|
+
- Postgres reads `AKAN_SEARCH_TOKENIZER` as `unicode61 [remove_diacritics 0|1|2]` (`simple`, with `unaccent` unless
|
|
123
|
+
`0`) or `trigram [case_sensitive 0|1]` (`pg_trgm`); any other spelling is refused. The extension is created when
|
|
124
|
+
the role may, otherwise a role that may runs `CREATE EXTENSION`. It indexes the first 20,000 characters of
|
|
125
|
+
`title`, `tag` and `filter` and 200,000 of `desc` — a tsvector refuses more than 1MB, and the refusal would fail
|
|
126
|
+
the model write. A database whose `LC_CTYPE` is `C` keeps non-ASCII case apart; use a UTF-8 ctype.
|
|
118
127
|
- `AKAN_SEARCH_ENABLED=0` switches the index off process-wide; unset means on. It never deletes mirror data, and
|
|
119
128
|
re-enabling reconciles every ref. **Give every process the same value** — a process cannot drop triggers for models
|
|
120
129
|
it does not mount, so a mixed fleet leaves stale triggers behind.
|
|
121
130
|
- The tokenizer is `AKAN_SEARCH_TOKENIZER` (or `database.search.tokenizer`, which wins), defaulting to
|
|
122
131
|
`unicode61 remove_diacritics 2`. Changing it rebuilds the index from the mirror on the next boot — the model
|
|
123
|
-
tables are never re-read — so it is a safe knob, unlike a `text` role change, which re-reads every row. The
|
|
124
|
-
|
|
125
|
-
|
|
132
|
+
tables are never re-read — so it is a safe knob, unlike a `text` role change, which re-reads every row. The schema
|
|
133
|
+
is ensured in one write turn — a SQLite write transaction, a Postgres schema lock — so of a fleet restarted at
|
|
134
|
+
once the first rebuilds and the rest find it current. A SQLite process waits for that turn up to its
|
|
135
|
+
`busy_timeout`, so stagger the restart when the mirror is large.
|
|
126
136
|
|
|
127
137
|
## Cascade Remove — the `cascade` option
|
|
128
138
|
|
|
@@ -184,6 +184,80 @@ const config: AppConfig = { docker: "FROM oven/bun:1-slim\n…" }; // verbatim,
|
|
|
184
184
|
- `AkanAppConfig.docker` is the resolved declaration; `AkanAppConfig.dockerfile` is the text `akan build` writes
|
|
185
185
|
to `dist/apps/<app>/Dockerfile`.
|
|
186
186
|
|
|
187
|
+
## Database Modes — `database` In `akan.config.ts`
|
|
188
|
+
|
|
189
|
+
| mode | database | cache · queue · pubsub | runs as |
|
|
190
|
+
|---|---|---|---|
|
|
191
|
+
| `single` | SQLite file | SQLite files (Solid) | one container |
|
|
192
|
+
| `multiple` | one SQLite file (WAL) on a host volume every container on that host opens | Redis | containers on one host |
|
|
193
|
+
| `cluster` | Postgres | Redis | pods on several servers |
|
|
194
|
+
|
|
195
|
+
- **`database: { modes: ["single", "cluster"] }` is what the build can run in**; the first is what `akan start`
|
|
196
|
+
runs. `akan build` carries the drivers of every declared mode (`multiple` → `bullmq`, `ioredis`; `cluster` adds
|
|
197
|
+
`postgres`).
|
|
198
|
+
- **A deployment names its mode with `AKAN_DATABASE_MODE`**, and only a declared one. With one declared mode it may
|
|
199
|
+
leave it out; with several it must name one — the fallback would be a SQLite file inside each pod. `akan start`,
|
|
200
|
+
`build`, `script` and `console` resolve it the same way from the shell. One image can serve an edge site in
|
|
201
|
+
`single` and a cloud cluster in `cluster`: declare both.
|
|
202
|
+
- **Where the data lives is the deployment's env, ahead of `env.server.ts`**: `SQLITE_DATABASE_PATH`,
|
|
203
|
+
`AKAN_SOLID_DB_PATH`, `POSTGRES_URL` (or `POSTGRES_HOST`/`PORT`/`DATABASE`/`USER`/`PASSWORD`),
|
|
204
|
+
`POSTGRES_INSIGHT_URL`, `LIBSQL_URL`. `REDIS_URI` is required outside local development. Pool size, SSL and
|
|
205
|
+
prepared statements ride the Postgres URL (`?max=20&ssl=require`, `prepare=false` behind PgBouncer).
|
|
206
|
+
- **Uploads**: a deployed `multiple`/`cluster` app uses object storage, or a volume every instance mounts with
|
|
207
|
+
`AKAN_STORAGE_SHARED=true`.
|
|
208
|
+
- **`runAdminSql` on Postgres reads as its own login role** holding base-column `SELECT` and nothing else:
|
|
209
|
+
`CREATE ROLE <name> LOGIN PASSWORD '…'` with no other role granted, `POSTGRES_INSIGHT_URL` logging in as it, and
|
|
210
|
+
`GRANT USAGE ON SCHEMA` from the DBA when the app does not own its schema. Every model table grants the role its
|
|
211
|
+
base columns at boot. Without it the console refuses on Postgres; `_doc` is never readable either way.
|
|
212
|
+
- **`akan dbup`** starts what the workspace's apps declare (`--mode multiple` is Redis, `cluster` adds Postgres).
|
|
213
|
+
**`akan db-export <app>` / `akan db-import <app>`** move every model table as stored rows through NDJSON files
|
|
214
|
+
(`--dir`, default `local/transfer`) between whatever the shell's `AKAN_DATABASE_MODE` points at; a rerun
|
|
215
|
+
replaces rows, search is rebuilt after, and sessions, queued jobs and uploaded files do not move.
|
|
216
|
+
- **`q.raw(sql)` is the dialect's own SQL** — `json_extract("_doc", '$.f')` on SQLite, `("_doc" #>> '{f}')` on
|
|
217
|
+
Postgres. An app that runs in both modes avoids it.
|
|
218
|
+
|
|
219
|
+
## Operations — Snapshots, Restore And The `/_akan/ops` Channel
|
|
220
|
+
|
|
221
|
+
`GET /_akan/app/info` answers `{ appName, repoName, environment, operationMode }` on every deployment — the same
|
|
222
|
+
`AKAN_PUBLIC_*` values the client bundle already carries — so a control plane can check which app a host is
|
|
223
|
+
serving without a key. Everything else is behind the ops channel.
|
|
224
|
+
|
|
225
|
+
| surface | what it does |
|
|
226
|
+
|---|---|
|
|
227
|
+
| `bun main.js ops snapshot [--id] [--out] [--include-solid] [--json]` | `VACUUM INTO` a consistent copy, `integrity_check`, sha256, gzip, optional age encryption, `manifest.json` |
|
|
228
|
+
| `bun main.js ops restore <manifest.json> [--identity <file>]` | offline only: verify, stage, keep the old file as `<db>.pre-restore-<ts>` (its `-wal`/`-shm` beside it), swap |
|
|
229
|
+
| `GET /_akan/ops/info` | the public info plus `akanVersion`, `buildId`, `serverMode`, `databaseMode`, `solo`, `startedAt`, `replicaIdx` |
|
|
230
|
+
| `POST /_akan/ops/snapshot` `{ id, includeSolid?, uploadUrls: { main, solid?, manifest } }` | 202; a child process snapshots, then PUTs each file and the manifest **last** |
|
|
231
|
+
| `GET /_akan/ops/snapshot/:id` | `running` → `uploading` → `done` \| `failed`; kept in memory, so a restart answers 404 |
|
|
232
|
+
|
|
233
|
+
- **The entry is the image's own `main.js`** — a production image carries no `akan` CLI. `AkanApp.start()` hands
|
|
234
|
+
`argv[2] === "ops"` to the command before it boots anything. It reads the container's env — the `AKAN_PUBLIC_*`
|
|
235
|
+
identity and the database paths — so run it with that env (`docker compose exec`), never a bare shell's.
|
|
236
|
+
- **Restore refuses while an app answers `/_akan/app/health` on `PORT`** and names it by its `/_akan/app/info`;
|
|
237
|
+
`--force` is for a port some other app holds.
|
|
238
|
+
- **The channel exists only when `AKAN_OPS_PUBLIC_KEY` is set** (Ed25519: PEM, JWK, base64 SPKI or raw 32 bytes).
|
|
239
|
+
The control plane signs a short EdDSA JWS — `aud`, `iat`, `exp` at most 300s after `iat`, a `jti` the process
|
|
240
|
+
refuses twice — and the host holds only the public half, so a machine someone carries off mints nothing.
|
|
241
|
+
`aud` is `<appName>/<environment>`, or `<appName>/<environment>/<AKAN_OPS_INSTANCE>` when that is set, and then
|
|
242
|
+
only that: a token minted for the app as a whole would replay across every host sharing the name. 30 calls a
|
|
243
|
+
minute per process; every refusal is the same 401. A malformed key leaves the app serving with the channel off.
|
|
244
|
+
- **The app holds no storage credential.** It PUTs to the presigned URLs it was handed (`application/octet-stream`,
|
|
245
|
+
the manifest `application/json`), plain http only to loopback. The manifest landing is the commit marker.
|
|
246
|
+
- **Snapshots cover the SQLite modes only.** `single` copies the main file and, with `includeSolid`, the Solid file;
|
|
247
|
+
`multiple` copies the main file; `cluster` is refused. Paths follow the adaptors' env precedence
|
|
248
|
+
(`SQLITE_DATABASE_PATH`, `AKAN_SOLID_DB_PATH`, `AKAN_SQLITE_DIR`); a solo process uses the path its adaptor
|
|
249
|
+
actually opened, so a gateway deployment that moved the file in `env.server.ts` names it in the env too.
|
|
250
|
+
- **`AKAN_BACKUP_RECIPIENT=age1…[,age1…]` encrypts before upload** — age v1 X25519, so `age -d -i key.txt` opens
|
|
251
|
+
it. Restore takes `--identity` / `AKAN_BACKUP_IDENTITY_FILE`, or a file an operator already decrypted with
|
|
252
|
+
`age -d -o <name without .age>`.
|
|
253
|
+
- **The host keeps the newest `AKAN_OPS_SNAPSHOT_KEEP` (default 2) snapshots** under `AKAN_OPS_SNAPSHOT_DIR`
|
|
254
|
+
(default `<db dir>/snapshots/<id>/`) — the copy an edge still has while its uplink or the bucket is down.
|
|
255
|
+
- **`buildId` is `AKAN_BUILD_ID` when the deployment sets it**, else the git sha `akan build` wrote into
|
|
256
|
+
`akan.build.json` beside `main.js` (`-dirty` when the tree was).
|
|
257
|
+
- **`operationMode` does not gate any of this** — the key does. `edge` means a production host outside the
|
|
258
|
+
cluster: the console refuses it like `cloud`, and `init` / `interval` / `cron` take `operationMode: ["edge"]` to
|
|
259
|
+
run only there. Everything else behaves as `cloud`; `local` is a developer machine.
|
|
260
|
+
|
|
187
261
|
## Shipped Assets — `assets` In `akan.config.ts`
|
|
188
262
|
|
|
189
263
|
`akan build` copies the whole `public/` tree into `dist`, lib assets dereferenced, and that copy is the image.
|
|
@@ -129,8 +129,10 @@ eight queries threw; per-section boundaries put each rejection at its own bounda
|
|
|
129
129
|
soft-redirect script, so `getSelf({ unauthorize: "/signin" })` stays in `_layout.tsx` or the page body — never
|
|
130
130
|
inside a streamed section.
|
|
131
131
|
|
|
132
|
-
**⑥ Use named `ReactNode` slots, not just `children`.**
|
|
133
|
-
|
|
132
|
+
**⑥ Use named `ReactNode` slots, not just `children`.** A client shell that takes one slot per region lets a
|
|
133
|
+
server component supply each of them, instead of absorbing the whole subtree into the bundle. `Layout.Navbar`
|
|
134
|
+
declares `title` / `left` / `right` alongside `back` and `children`, but its render body uses only `back` and
|
|
135
|
+
`children` — copy the shape, not that component.
|
|
134
136
|
|
|
135
137
|
**⑦ Let the server do the derived work.** Display and predicate logic belongs on `Light<Model>` (`isNew()`,
|
|
136
138
|
`canWrite(user?)`, `formatTimes()`), and enum→class lookups belong in a module-scope `as const` map. Both sides call
|
|
@@ -58,23 +58,25 @@ mutation is mounted under. `conventions` carries the invariants — this is the
|
|
|
58
58
|
|
|
59
59
|
## Cached Answers
|
|
60
60
|
|
|
61
|
-
- **`{ cache: <ms> }` in an endpoint's signal option is the whole cache surface.** The
|
|
62
|
-
|
|
63
|
-
declared value for a hard-coded 60 seconds, which is the same shape the `timeout` bug had.
|
|
61
|
+
- **`{ cache: <ms> }` in an endpoint's signal option is the whole cache surface.** The lookup is a step of every
|
|
62
|
+
call and stands aside unless an endpoint declares one.
|
|
64
63
|
- **Only a `query` that takes no internal argument may carry one.** The handler's inputs are its declared
|
|
65
64
|
arguments plus its internal ones, so an endpoint with none of the latter answers the same thing to everyone who
|
|
66
65
|
may read it — which is the only answer a shared entry can hold. An endpoint that takes `.with(Self)` answers
|
|
67
66
|
per caller, and one entry would be one caller's answer handed to the next; that endpoint and every `mutation`
|
|
68
67
|
are named once in the log and left uncached rather than silently ignored.
|
|
69
|
-
- **The guards run
|
|
70
|
-
|
|
71
|
-
|
|
68
|
+
- **The guards run before every hit.** The lookup sits after the guards and the internal arguments, inside the
|
|
69
|
+
call, so every middleware — the account resolution a lib registers among them — has already run and a hit
|
|
70
|
+
reaches only a caller the guards admitted. It used to be a middleware ahead of the lib's, and a guard reading
|
|
71
|
+
the account refused a signed-in caller on every hit. Shared is not public.
|
|
72
|
+
- **The entry is the handler's result, not the response.** The lookup wraps the handler only, so `resolveReturn`
|
|
72
73
|
still runs per call on the cached value — field masking, `hidden`/`secret` stripping and relation resolution are
|
|
73
74
|
never cached, and a relation still costs its own load. The cache saves the handler, not the serialization.
|
|
74
75
|
- **A cache backend that is down does not take the endpoint down.** A failed read is warned and the call runs
|
|
75
76
|
uncached; a failed write is warned and dropped.
|
|
76
|
-
- The default middleware chain is `Logging → Timeout →
|
|
77
|
-
|
|
77
|
+
- The default middleware chain is `Logging → Timeout → <lib middlewares>`, and the call inside it runs guards →
|
|
78
|
+
internal arguments → cache → handler: a hung cache backend is bounded by the same deadline as the handler it
|
|
79
|
+
stands in for. **`Retry` was removed** — an automatic re-run of an endpoint
|
|
78
80
|
whose failure it cannot classify replays whatever the first attempt already did.
|
|
79
81
|
|
|
80
82
|
## Binary Pubsub
|
|
@@ -376,15 +376,16 @@ export default page()
|
|
|
376
376
|
|---|---|
|
|
377
377
|
| `interface PageProps { params }` + `[x]` folder | `.param("x", ID \| String \| Int \| …)` — one per `[x]` segment, every one of them, in path order |
|
|
378
378
|
| `searchParams.k` | `.search("k", Type)`; `[Type]` for a repeated key; the value arrives typed and optional |
|
|
379
|
-
| `params.lang` | `lang` — on every route's `.render()`, `.head()
|
|
379
|
+
| `params.lang` | `lang` — on every route's `.render()`, `.head()` and `.loading()` with no stage; `.param("lang")` is refused |
|
|
380
380
|
| `export const pageConfig = {…}` | `.config({…})` — `devOnly` stays a literal |
|
|
381
381
|
| `export const head = <…/>` / `generateHead({ params })` | `.head(<…/>)` / `.head(({ x }) => <…/>)` |
|
|
382
|
-
| `export const metadata` / `generateMetadata` | `.
|
|
382
|
+
| `export const metadata` / `generateMetadata` | `.head(<><title>…</title><meta name="description" content="…" /></>)` — there is no object form |
|
|
383
383
|
| `export function Loading()` | `.loading((args) => …)` |
|
|
384
384
|
| `export default async function Page({ params, searchParams })` | `.render(async ({ x, k }) => { …same body… })` — `async` only if it awaits |
|
|
385
385
|
| `_layout.tsx`: `export default function Layout({ children })` | `layout().render(({ children }) => …)` |
|
|
386
386
|
| `export function NotFound` / `export function Error` | `.notFound(fn)` / `.error(fn)` on `layout()` |
|
|
387
|
-
| root `_layout.tsx`: `fonts`, `theme`, `manifest`, `reconnect`, `wsConnect`, `layoutStyle
|
|
387
|
+
| root `_layout.tsx`: `fonts`, `theme`, `manifest`, `reconnect`, `wsConnect`, `layoutStyle` | `rootLayout().fonts([…]).theme("dark").manifest({…}).reconnect(false).wsConnect(false).layoutStyle("web")` |
|
|
388
|
+
| root `_layout.tsx`: `gaTrackingId` | none — render the app's own `"use client"` analytics component from the root layout's `.render()` |
|
|
388
389
|
| `prompt()` on `endpoint()` (removed) | `.prompt(name, description)` on the page that shows the same screen |
|
|
389
390
|
|
|
390
391
|
Rules the loader and `akan sync` enforce: a chain module has no other export; `page()` belongs in a page file and
|
|
@@ -3,7 +3,7 @@ import {
|
|
|
3
3
|
CodegenLock,
|
|
4
4
|
WorkspaceExecutor,
|
|
5
5
|
AppExecutor
|
|
6
|
-
} from "./index-
|
|
6
|
+
} from "./index-crmeqnmf.js";
|
|
7
7
|
import"./index-q0y2jq9j.js";
|
|
8
8
|
import"./index-6wwzs1bc.js";
|
|
9
9
|
import {
|
|
@@ -12,7 +12,7 @@ import {
|
|
|
12
12
|
AutoImportSync,
|
|
13
13
|
DevChangePlanner,
|
|
14
14
|
DevGeneratedIndexSync
|
|
15
|
-
} from "./index-
|
|
15
|
+
} from "./index-89hy31kf.js";
|
|
16
16
|
import {
|
|
17
17
|
HmrWatcher,
|
|
18
18
|
WatchRootResolver
|
|
@@ -173,6 +173,7 @@ class IncrementalBuilder {
|
|
|
173
173
|
app: this.#app,
|
|
174
174
|
routeId: msg.routeId,
|
|
175
175
|
seeds: msg.seeds,
|
|
176
|
+
graphSeeds: msg.graphSeeds,
|
|
176
177
|
artifact: this.#artifact,
|
|
177
178
|
knownEntries: new Set(msg.knownEntries),
|
|
178
179
|
discovery: this.#discovery
|
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
// @bun
|
|
2
2
|
import {
|
|
3
3
|
jsonText
|
|
4
|
-
} from "./index-
|
|
4
|
+
} from "./index-2446k7b4.js";
|
|
5
5
|
import {
|
|
6
6
|
akanMcpInstallTargets
|
|
7
|
-
} from "./index-
|
|
7
|
+
} from "./index-18zybgkc.js";
|
|
8
8
|
import {
|
|
9
9
|
script
|
|
10
|
-
} from "./index-
|
|
10
|
+
} from "./index-qdgzsnhg.js";
|
|
11
11
|
import {
|
|
12
12
|
ContextRunner2
|
|
13
|
-
} from "./index-
|
|
13
|
+
} from "./index-3jrp517k.js";
|
|
14
14
|
|
|
15
15
|
// pkgs/@akanjs/cli/context/context.script.ts
|
|
16
16
|
import { Logger } from "akanjs/common";
|
|
@@ -249,7 +249,7 @@ class CodeTuiCommands2 {
|
|
|
249
249
|
static all = [
|
|
250
250
|
{ name: "help", desc: "list these commands and the keys" },
|
|
251
251
|
{ name: "tools", desc: "what this session can call" },
|
|
252
|
-
{ name: "model", desc: "switch model", arg: "<provider>/<id>" },
|
|
252
|
+
{ name: "model", desc: "switch model, or list what is available", arg: "[<provider>/<id>]" },
|
|
253
253
|
{ name: "effort", desc: "how hard the model thinks before it answers", arg: "<level>" },
|
|
254
254
|
{ name: "thinking", desc: "show the model's reasoning, or fold it away again" },
|
|
255
255
|
{ name: "compact", desc: "summarise the conversation to free the window", arg: "[notes]" },
|
|
@@ -258,7 +258,7 @@ class CodeTuiCommands2 {
|
|
|
258
258
|
{ name: "name", desc: "name this session", arg: "<text>" },
|
|
259
259
|
{ name: "fork", desc: "continue this conversation twice; the copy takes over here", arg: "[name]" },
|
|
260
260
|
{ name: "agents", desc: "what a sub-agent may be, and what one may spend" },
|
|
261
|
-
{ name: "mcp", desc: "the MCP servers this session reached", arg: "[add|remove|reload]" },
|
|
261
|
+
{ name: "mcp", desc: "the MCP servers this session reached", arg: "[add|remove|login|reload]" },
|
|
262
262
|
{ name: "peers", desc: "the other akan code sessions running here" },
|
|
263
263
|
{ name: "msg", desc: "hand a message to another session", arg: "<peer> <text>" },
|
|
264
264
|
{ name: "quit", desc: "leave the session" }
|
|
@@ -292,7 +292,7 @@ class CodeTuiCommands2 {
|
|
|
292
292
|
"",
|
|
293
293
|
"Images: cmd+v attaches what is on the clipboard \xB7 ^v does too, without the terminal in the way",
|
|
294
294
|
"",
|
|
295
|
-
'shift+enter needs a terminal that reports it \u2014 in VS Code bind it to send "
|
|
295
|
+
'shift+enter needs a terminal that reports it \u2014 in VS Code bind it to send "\\\\u001b\\r".'
|
|
296
296
|
].join(`
|
|
297
297
|
`);
|
|
298
298
|
}
|
|
@@ -335,6 +335,8 @@ class CodeTuiEditor2 {
|
|
|
335
335
|
this.set(this.#text.slice(0, this.#caret) + clean + this.#text.slice(this.#caret), this.#caret + clean.length);
|
|
336
336
|
}
|
|
337
337
|
newline() {
|
|
338
|
+
if (this.#text.slice(0, this.#caret).endsWith("\\"))
|
|
339
|
+
this.set(this.#text.slice(0, this.#caret - 1) + this.#text.slice(this.#caret), this.#caret - 1);
|
|
338
340
|
this.insert(`
|
|
339
341
|
`);
|
|
340
342
|
}
|
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
isScannedRootEntry,
|
|
8
8
|
AppExecutor,
|
|
9
9
|
LibExecutor
|
|
10
|
-
} from "./index-
|
|
10
|
+
} from "./index-crmeqnmf.js";
|
|
11
11
|
import {
|
|
12
12
|
FileSys
|
|
13
13
|
} from "./index-q0y2jq9j.js";
|
|
@@ -19,7 +19,7 @@ import {
|
|
|
19
19
|
import {
|
|
20
20
|
workflowRunArtifactPath,
|
|
21
21
|
workflowSyncDir
|
|
22
|
-
} from "./index-
|
|
22
|
+
} from "./index-2446k7b4.js";
|
|
23
23
|
|
|
24
24
|
// pkgs/@akanjs/devkit/akanContext.ts
|
|
25
25
|
import { readdir } from "fs/promises";
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import {
|
|
3
3
|
runner,
|
|
4
4
|
script
|
|
5
|
-
} from "./index-
|
|
5
|
+
} from "./index-qdgzsnhg.js";
|
|
6
6
|
|
|
7
7
|
// pkgs/@akanjs/cli/page/page.runner.ts
|
|
8
8
|
class PageRunner extends runner("page") {
|
|
@@ -10,7 +10,11 @@ class PageRunner extends runner("page") {
|
|
|
10
10
|
await app.applyTemplate({
|
|
11
11
|
basePath: basePath ?? `page/(${app.name})/(public)/${module.name}`,
|
|
12
12
|
template: single ? "crudSinglePage" : "crudPages",
|
|
13
|
-
dict: {
|
|
13
|
+
dict: {
|
|
14
|
+
model: module.name,
|
|
15
|
+
appName: module.sys.name,
|
|
16
|
+
clientPath: `@${module.sys.type}s/${module.sys.name}/client`
|
|
17
|
+
}
|
|
14
18
|
});
|
|
15
19
|
}
|
|
16
20
|
}
|