@akanjs/cli 3.0.0-alpha.9 → 3.0.0-alpha.90

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 (135) hide show
  1. package/.build-stamp +1 -1
  2. package/README.ko.md +1 -1
  3. package/README.md +1 -1
  4. package/{abstractCompactor-e5yq60qz.js → abstractCompactor-cefxzggj.js} +3 -5
  5. package/{agent.command-f0gqkfgz.js → agent.command-r93wmq7m.js} +11 -13
  6. package/{application.command-x903m2qq.js → application.command-5nqfqgdn.js} +62 -38
  7. package/{applicationBuildRunner-ha3y8ew8.js → applicationBuildRunner-b21m07fs.js} +23 -20
  8. package/{applicationReleasePackager-gg8ccbeh.js → applicationReleasePackager-3wh58n2m.js} +8 -9
  9. package/buildBatch.proc.js +12 -14
  10. package/capacitorApp-gzcx6txb.js +12 -0
  11. package/{cloud.command-zs6kyf83.js → cloud.command-g0pk0k60.js} +15 -18
  12. package/commandManifest.json +1 -1
  13. package/{context.command-fmepgjzg.js → context.command-qd8kqrnh.js} +19 -23
  14. package/dependencyScanner-shs82ynp.js +8 -0
  15. package/devTui-2xf0meaq.js +552 -0
  16. package/devTuiView-7jy8f30g.js +9 -0
  17. package/getDirname-dr3jz1xg.js +7 -0
  18. package/{guideline.command-n6sw0a0c.js → guideline.command-f38x8jhw.js} +16 -18
  19. package/guidelines/agentRule/agentRule.generate.json +31 -0
  20. package/guidelines/agentRule/agentRule.instruction.md +533 -0
  21. package/guidelines/conventions/conventions.instruction.md +406 -197
  22. package/guidelines/cssRule/cssRule.instruction.md +7 -0
  23. package/guidelines/framework/framework.generate.json +3 -2
  24. package/guidelines/framework/framework.instruction.md +1 -9
  25. package/guidelines/mcpRule/mcpRule.generate.json +25 -0
  26. package/guidelines/mcpRule/mcpRule.instruction.md +217 -0
  27. package/guidelines/modelStore/modelStore.instruction.md +12 -0
  28. package/guidelines/modelTemplate/modelTemplate.instruction.md +5 -0
  29. package/guidelines/queryRule/queryRule.generate.json +31 -0
  30. package/guidelines/queryRule/queryRule.instruction.md +143 -0
  31. package/guidelines/runtimeRule/runtimeRule.generate.json +25 -0
  32. package/guidelines/runtimeRule/runtimeRule.instruction.md +213 -0
  33. package/guidelines/ssrRule/ssrRule.instruction.md +49 -5
  34. package/guidelines/transportRule/transportRule.generate.json +31 -0
  35. package/guidelines/transportRule/transportRule.instruction.md +60 -0
  36. package/guidelines/workspaceOnboarding/workspaceOnboarding.instruction.md +51 -760
  37. package/guidelines/workspaceRecipes/workspaceRecipes.generate.json +33 -0
  38. package/guidelines/workspaceRecipes/workspaceRecipes.instruction.md +402 -0
  39. package/incrementalBuilder.proc.js +26 -25
  40. package/{index-774gcx29.js → index-0emb9eat.js} +41 -34
  41. package/index-3d9y066c.js +76 -0
  42. package/{index-3vta1tqa.js → index-3vvbv974.js} +32 -32
  43. package/{index-76rn3g2c.js → index-47pj3aee.js} +4 -4
  44. package/{index-y4vkyhvv.js → index-5xdqxbn9.js} +14 -14
  45. package/{index-1577bej2.js → index-6wwzs1bc.js} +2 -2
  46. package/{index-wnp7hwq7.js → index-92zztqy1.js} +7 -7
  47. package/{index-jhpa13zj.js → index-9bwhxrqf.js} +57 -57
  48. package/{index-bjpxzr6s.js → index-9wtbw98m.js} +6 -3
  49. package/{index-fgc8r6dj.js → index-a5vmm6x4.js} +3 -1
  50. package/{index-xys926f2.js → index-by5gpp8x.js} +4 -4
  51. package/{index-kdh76j26.js → index-cezy7yb3.js} +100 -32
  52. package/{index-62955kn5.js → index-d61rcfa2.js} +758 -127
  53. package/{index-82jrh4hj.js → index-deekbtkj.js} +13 -13
  54. package/{index-rxkpk7n4.js → index-e82dyeqy.js} +4 -4
  55. package/{index-qzdjd4ct.js → index-h759vjfa.js} +18 -18
  56. package/{index-j37qq1f2.js → index-h7yw2z9n.js} +7 -7
  57. package/{index-qb61h0k7.js → index-jydtyzva.js} +1098 -1360
  58. package/{index-46tjzh6s.js → index-kcxmv48c.js} +29 -29
  59. package/{index-4m0bphac.js → index-kfnr6c19.js} +31 -30
  60. package/{index-xmmzf150.js → index-mpmyqm1y.js} +1 -1
  61. package/{index-xjd3rqzj.js → index-n68jx79w.js} +6 -15
  62. package/{index-0cj2zxbm.js → index-n85fer99.js} +12 -11
  63. package/{index-779ske1y.js → index-pgsswppj.js} +27 -27
  64. package/index-pvb4vdwr.js +159 -0
  65. package/{index-67546d0j.js → index-q0y2jq9j.js} +14 -2
  66. package/{index-z8ptfkzv.js → index-q8sn827n.js} +969 -240
  67. package/{index-5sn5w2mt.js → index-r50qen0z.js} +94 -61
  68. package/{index-eyyf2149.js → index-rntcdss1.js} +36 -37
  69. package/{index-gcv8ncfh.js → index-sjak5v5b.js} +16 -12
  70. package/{index-j9cxndc0.js → index-w8kmx1b6.js} +20 -11
  71. package/{index-jf2ya7r2.js → index-y74k5anj.js} +8 -8
  72. package/{index-d43t8af2.js → index-zkvpaggp.js} +58 -25
  73. package/index.js +22 -25
  74. package/{library.command-cv9gdbmg.js → library.command-em103wtn.js} +11 -10
  75. package/{localRegistry.command-d8fg36zc.js → localRegistry.command-gz8apn4y.js} +20 -23
  76. package/{module.command-qyr1s9cn.js → module.command-451pztkc.js} +13 -16
  77. package/{package.command-wsjx0k47.js → package.command-rfhyf38t.js} +8 -10
  78. package/package.json +5 -7
  79. package/{page.command-rdzg65rm.js → page.command-kt19da5g.js} +7 -9
  80. package/{primitive.command-89g38b53.js → primitive.command-pgy69ere.js} +13 -16
  81. package/{quality.command-0cs2z92d.js → quality.command-gpe2vqb5.js} +226 -105
  82. package/recipeScanner-fpagtgc1.js +11 -0
  83. package/{repair.command-6fphxfwj.js → repair.command-2sbrz187.js} +12 -14
  84. package/{routeSourceValidator-e4jmbfcx.js → routeSourceValidator-ze6x894c.js} +0 -2
  85. package/{scalar.command-xfer69s6.js → scalar.command-jcf4e5sv.js} +11 -14
  86. package/subspace.command-kqzqtk0p.js +755 -0
  87. package/templates/app/env/env.client.debug.ts.template +1 -5
  88. package/templates/app/env/env.client.develop.ts.template +1 -5
  89. package/templates/app/env/env.client.local.ts.template +1 -5
  90. package/templates/app/env/env.client.main.ts.template +1 -5
  91. package/templates/app/env/env.client.testing.ts.template +1 -5
  92. package/templates/app/env/env.client.type.ts +2 -6
  93. package/templates/app/env/env.server.debug.ts.template +1 -5
  94. package/templates/app/env/env.server.develop.ts.template +1 -5
  95. package/templates/app/env/env.server.local.ts.template +1 -5
  96. package/templates/app/env/env.server.main.ts.template +1 -5
  97. package/templates/app/main.ts +3 -1
  98. package/templates/appSample/lib/task/task.signal.ts +11 -6
  99. package/templates/appSample/srvkit/AuthGuard.ts +9 -5
  100. package/templates/appSample/srvkit/SessionInternalArg.ts +4 -3
  101. package/templates/crudPages/[__model__Id]/edit/page.tsx +5 -5
  102. package/templates/crudPages/[__model__Id]/page.tsx +10 -13
  103. package/templates/crudPages/new/page.tsx +5 -5
  104. package/templates/crudPages/page.tsx +9 -9
  105. package/templates/crudSinglePage/page.tsx +11 -7
  106. package/templates/env/_env.server.type.ts +7 -5
  107. package/templates/lib/db.ts +1 -1
  108. package/templates/lib/dict.ts +1 -1
  109. package/templates/lib/sig.ts +1 -1
  110. package/templates/lib/srv.ts +6 -2
  111. package/templates/module/__model__.dictionary.ts +4 -1
  112. package/templates/module/__model__.signal.ts +2 -3
  113. package/templates/module/__model__.store.ts +1 -2
  114. package/templates/workspaceRoot/.gitignore.template +0 -1
  115. package/templates/workspaceRoot/.husky/pre-commit.template +16 -0
  116. package/templates/workspaceRoot/.vscode/settings.json.template +1 -1
  117. package/templates/workspaceRoot/AGENTS.md.template +4 -0
  118. package/templates/workspaceRoot/CLAUDE.md.template +19 -0
  119. package/templates/workspaceRoot/biome.json.template +2 -1
  120. package/templates/workspaceRoot/docs/AI-DEVELOPMENT.md.template +10 -14
  121. package/templates/workspaceRoot/docs/GENERATED.md.template +12 -10
  122. package/templates/workspaceRoot/package.json.template +2 -4
  123. package/typeChecker-3xgrzc2v.js +7 -0
  124. package/typecheck.proc.js +4 -5
  125. package/{workflow.command-2ygfqt4f.js → workflow.command-m4z25a2p.js} +22 -25
  126. package/{workspace.command-88m0zs64.js → workspace.command-dqgj9zbk.js} +96 -81
  127. package/capacitorApp-9xswcgx7.js +0 -61
  128. package/dependencyScanner-f6grrep3.js +0 -9
  129. package/getDirname-m9s0cmhq.js +0 -8
  130. package/index-mxvakhsm.js +0 -66
  131. package/index-r24hmh0q.js +0 -4
  132. package/recipeScanner-tghkzrrt.js +0 -12
  133. package/typeChecker-kravn7ns.js +0 -8
  134. /package/{index-0wae5ebk.js → index-meh558jy.js} +0 -0
  135. /package/{index-ss469dec.js → index-nz8prx9g.js} +0 -0
@@ -19,44 +19,80 @@
19
19
  - Prefer established nearby patterns over introducing a new abstraction.
20
20
  - Add new abstractions only when they remove real duplication or match an existing project pattern.
21
21
 
22
+ ## Deep-Dive Guidelines
23
+
24
+ This file carries the rules. The reasoning, the measurements, and the per-feature edge cases live in guidelines
25
+ you fetch on demand — `get_guideline` with the name, or `akan guideline show <name>`. Fetch the relevant one
26
+ **before** a deep pass on that area; a section below that points at one is a summary, not the whole contract.
27
+
28
+ | name | covers |
29
+ |---|---|
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 |
32
+ | `queryRule` | slices and hydration, the generated filter methods, full-text search, cascade removal |
33
+ | `transportRule` | guards across HTTP and websocket, socket identity and cleanup, binary pubsub, mutation verbs |
34
+ | `mcpRule` | MCP configuration, wire behaviour, resource URIs, OAuth metadata, protocol revisions |
35
+ | `agentRule` | the in-page agent's chat options, zones, slash commands, transcript compaction |
36
+ | `cssRule` | the full semantic token set for a theme pass |
37
+ | `componentRule` | the `_overrides.tsx` slot list and drop-in component patterns |
38
+ | `recipeRule` | authoring and consuming Tailwind-variant recipes |
39
+ | `workspaceRecipes` | worked step-by-step recipes and the auto-generated API reference |
40
+
22
41
  ## Lint-Enforced Rules (These Break The Build)
23
42
 
24
- Enforced by `biome.json` and the grit plugins in `pkgs/@akanjs/devkit/lint/`. Several of them produce output
25
- that looks wrong; do not "fix" it back.
43
+ Enforced by `biome.json`, which extends `@akanjs/devkit/biome.base.json` — that file scopes every grit plugin in
44
+ `@akanjs/devkit/lint/` to the paths it applies to. Several rules produce output that looks wrong; do not "fix" it
45
+ back.
26
46
 
27
47
  - **Never hand-order Tailwind classes.** `nursery/useSortedClasses` is an error and also sorts the string
28
- arguments to `cn()`. Sorter output such as `font-bold text-2xl text-base-content` or
29
- `border-base-content/5 border-t` is correct. Write the classes in any order, run the formatter, leave the result.
30
- - **Stay inside the color vocabulary.** Vocabulary closure strips the raw Tailwind palette, so these render as
31
- no CSS and fail lint (`no-raw-palette-class.grit`, `no-arbitrary-color.grit`, `no-daisyui-legacy-class.grit`,
32
- `no-inline-color.grit`): raw palette classes (`bg-blue-500`), arbitrary color values (`bg-[#3b82f6]`), daisyUI
33
- legacy classes (`btn-primary`, `card-body`), and color literals in `style={{...}}`. Use semantic tokens
48
+ arguments to `cn()`. Sorter output such as `font-bold text-2xl text-foreground` or `border-foreground/5 border-t`
49
+ is correct. Write the classes in any order, run the formatter, leave the result.
50
+ - **Stay inside the color vocabulary.** Vocabulary closure strips the raw Tailwind palette, so these render as no
51
+ CSS and fail lint: raw palette classes (`bg-blue-500`), arbitrary color values (`bg-[#3b82f6]`), an arbitrary
52
+ class built by interpolation, daisyUI legacy classes (`btn-primary`, `card-body`) and its dropped color slots
53
+ (`bg-base-100`, `text-base-content`, `text-primary-content`, `bg-error` — use `background`/`muted`/`border`,
54
+ `foreground`, `<color>-foreground`, `destructive`), and color literals in `style={{...}}`. Use semantic tokens
34
55
  (`bg-primary`, `text-foreground/70`). A legitimate fixed color (OS-chrome mockups, data-viz) takes a
35
- `// biome-ignore lint/plugin: <reason>` with the reason spelled out. `apps/akan/page/v1/**` is excluded.
56
+ `// biome-ignore lint/plugin: <reason>`. (`no-raw-palette-class`, `no-arbitrary-color`, `no-daisyui-legacy-class`,
57
+ `no-inline-color`, `no-interpolated-arbitrary-class`)
36
58
  - **Never `throw new Error`.** Throw `new Err("<module>.error.<key>")` and register the key as `[en, ko]` in that
37
59
  module's dictionary `.error({})`. Import `Err` from `"../dict"` on the server and from `"@libs/<lib>/client"` or
38
- `"@apps/<app>/client"` in UI. `no-throw-raw-error.grit` exempts `*.test.ts`, `*.spec.ts`, `*.constant.ts`,
39
- `common/**`, and `apps/akan/env/**` — `common/` and `env/` have no legal `Err` import path, so keep throwing code
40
- out of them.
60
+ `"@apps/<app>/client"` in UI. `no-throw-raw-error.grit` exempts tests, `*.constant.ts`, `common/**`, and `env/**`
61
+ — the last two have no legal `Err` import path, so keep throwing code out of them.
41
62
  - **Never import a third-party package** from `page/**`, from any barrel, or from any
42
63
  `*.{constant,dictionary,document,service,signal,store}.ts` / `*.{Template,Unit,Util,View,Zone}.tsx`
43
- (`no-import-external-library.grit`). Re-export the symbol through a lib first. One-line re-export shims such as
44
- `libs/<lib>/base/<pkg>.ts` and `libs/<lib>/webkit/<hook>.ts` exist for exactly this reason — they are
45
- load-bearing, not cruft. Do not delete them.
64
+ (`no-import-external-library.grit`). Re-export the symbol through a lib first. One-line re-export shims in a lib's
65
+ `common/`, `webkit/`, or `ui/` exist for exactly this reason — they are load-bearing, not cruft.
46
66
  - **`#private` is banned in exactly four file suffixes:** `*.constant.ts`, `*.document.ts`, `*.service.ts`, and
47
67
  `*.store.ts` (`no-js-private-class-method.grit`). The rule is scoped by file path, not by class shape, so
48
68
  `#private` remains the house style everywhere under `srvkit/`, including `adapt()` adapter classes.
49
69
  - **No `console.log` / `console.debug`.** Only `assert`, `error`, `info`, and `warn` are allowed. Server code uses
50
70
  the injected `this.logger.*` or `new Logger("ClassName")`.
71
+ - **Never call `logger.log()` / `Logger.log()`** (`no-deprecated-log-level.grit`). The ladder is `trace verbose
72
+ debug info warn error`; `log` was a seventh tier *below* `info` that `AKAN_PUBLIC_LOG_LEVEL=info` silently
73
+ dropped. The method is kept and emits at `info`, so a call reads like a distinct level and is not one — write
74
+ `.info()`.
51
75
  - **Never write a `//!` marker in browser-reachable code** — `ui/`, `webkit/`, `common/`, `page/**/*.tsx`,
52
76
  `*.constant.ts`, `*.store.ts`, and the five module component suffixes (`no-bang-comment-in-client.grit`). Bun
53
77
  classifies `//!` and `/*!` as legal comments and keeps them through minification, so the note ships to every
54
78
  visitor. Use `// FIXME:` there; `//!` stays legal in server, `srvkit/`, and CLI files.
55
79
  - **Never return a value from a store action** (`no-return-in-store-action.grit`). Every method of a `store(...)`
56
- class dispatches through `st.do.<action>()`, which is typed `void` / `Promise<void>`, so the value is
57
- unreachable — write it into state with `this.set({ ... })`. A bare `return;` guard, a `return` inside a nested
58
- callback, a getter, and a `static` helper are all still fine.
80
+ class dispatches through `st.do.<action>()`, typed `void` / `Promise<void>`, so the value is unreachable — write
81
+ it into state with `this.set({ ... })`. A bare `return;` guard, a `return` inside a nested callback, a getter,
82
+ and a `static` helper are all still fine.
59
83
  - **Never redeclare a generated CRUD endpoint name** in `*.signal.ts` (`no-redeclare-predefined-endpoint.grit`).
84
+ - **Never type a `*.Util.tsx` / `*.Zone.tsx` prop as a `cnst` model** (`no-model-type-in-util-zone.grit`). Those two
85
+ roles are always client components, so a `cnst.Banner` / `cnst.LightBanner` prop is a class instance the server
86
+ has to hand across the boundary; take `bannerId: string` and read the model from the store instead. Only prop
87
+ positions are read, and only shapes that are actually an instance. Exempt: an indexed enum access
88
+ (`cnst.<Enum>["value"]`), a `ClientInit` / `ClientView` / `ClientEdit` or `ModelsProps<…>` type argument, a
89
+ function-typed prop, and any `cnst` type that never leaves the file. Other indexed accesses
90
+ (`ModelProps<"setting", cnst.LightSetting>`, `cnst.Banner["image"]`) stay flagged.
91
+ - **Never wrap a form setter in a pass-through arrow** (`no-unpublished-form-setter.grit`).
92
+ `onChange={(type) => st.do.setTypeOnTicket(type)}` runs identically to `onChange={st.do.setTypeOnTicket}`, but
93
+ the arrow is a fresh anonymous closure, so the control emits no `data-akan-action` and publishes no agent tool
94
+ for the field — a silent failure in two lines that read the same. A wrapper that transforms the value, adds a
95
+ statement, or writes a nested path with `writeOnX` stays legal; publish that one with an explicit `st.tool`.
60
96
  - **No deep imports past a barrel** (`no-deep-internal-import.grit`). Cross-module constant references such as
61
97
  `../map/map.constant` are the sanctioned exception.
62
98
  - **Never import across the client/server boundary.** Client files (`ui/`, `webkit/`, `page/`, `*.store.ts`, every
@@ -66,12 +102,35 @@ that looks wrong; do not "fix" it back.
66
102
  `*.store.ts`, a module component, `ui/`, `webkit/`, a package `client` entrypoint, or the `st` / `store` /
67
103
  `useClient` barrels (`no-import-client-in-server.grit`). Shared files — `common/` and `*.constant.ts` — are held
68
104
  to **both**, so they reach neither side. `import type` is erased before bundling and stays legal in every
69
- direction; a mixed value-and-type import is not exempt. Scoped to `apps/**` and `libs/**`: `pkgs/akanjs/**`
70
- implements the boundary and is where the two graphs legitimately meet.
105
+ direction; a mixed value-and-type import is not exempt. Scoped to `apps/**` and `libs/**`.
71
106
  - **Server-component discipline** is enforced on `page/**`, `*.Unit.tsx`, and `*.View.tsx`
72
107
  (`no-import-client-functions.grit`, `no-use-client-in-server.grit`, `non-scalar-props-restricted.grit`).
108
+ - **Never write an async component outside `page/`** (`no-async-component-in-ui.grit`, scoped to
109
+ `{apps,libs}/**/ui/**/*.tsx`). React has no async client component, so a `ui/` component that awaits breaks as
110
+ soon as a client parent renders it, and the load drops below the route, which could have started it before the
111
+ first byte. Await in the page — or hand an unawaited `fetch.*` field to a `Zone` as an `init` / `view` prop, or
112
+ to a `<Load.Stream of={…}>` that awaits it behind a boundary of its own — and take the resolved data as a prop.
113
+ Only a PascalCase binding whose own initializer is `async` is matched, so an async handler inside a synchronous
114
+ component and `lazy(async () => import(…))` are untouched.
115
+ - **Never call `fetch.init*` from a client file** (`no-init-fetch-in-client.grit`). `fetch.init<Model><Suffix>` and
116
+ `fetch.get<Model>Init<Suffix>` compose the slice's list and insight queries into the hydration snapshot that
117
+ `Load.Units` / `Load.View` seed the store from. From a route it resolves before the first byte; after hydration
118
+ it is two extra round-trips for a shell the browser already painted. Load it in the page and pass the result
119
+ down as an `init` prop — or hand the unawaited `x<Model>Init<Suffix>` promise across — and reload from the client
120
+ through the generated `st.do.init<Model><Suffix>()`. Gated on the real `"use client"` directive plus
121
+ `*.store.ts`, and matched by shape (`init` + `Capitalize<refName>` + `Capitalize<suffix>`), so a hand-written
122
+ `initPayment` is out.
73
123
  - `noArrayIndexKey` and `useExhaustiveDependencies` are **off** on purpose: `key={idx}` for embedded scalars and
74
124
  short dependency arrays are intentional, not oversights.
125
+ - **A grit plugin diagnostic is suppressed as `lint/plugin`, not `plugin`** — `// biome-ignore lint/plugin: <reason>`
126
+ for one line, `// biome-ignore-all lint/plugin: <reason>` for a file. The bare `// biome-ignore plugin:` form
127
+ Biome's own category name suggests does nothing. The module-convention plugins apply to `apps/**` and `libs/**`
128
+ only, so a plain package under `pkgs/` never needs the escape hatch.
129
+ - **`biome.json` is strict JSON — a comment in it breaks config resolution.** Biome does not report the parse
130
+ error; it falls back to discovery and aborts on whatever nested config the walk finds. Rename the file to
131
+ `biome.jsonc` to document a disabled rule.
132
+ - **`akan lint` prints up to 200 diagnostics** (`--max-diagnostics <n>`, `0` for no limit). Biome's own default is
133
+ 20 with no count, which reads as progress when the mix of findings merely changed.
75
134
 
76
135
  ## Coding Style (`**/*.{ts,tsx}`)
77
136
 
@@ -110,7 +169,7 @@ that looks wrong; do not "fix" it back.
110
169
  - Prefer focused behavior tests for public contracts and edge cases over implementation-detail assertions.
111
170
  - Run package suites with `bun run akan test <pkg>` from the repo root, or `cd <pkg> && bun test --isolate`. Plain `bun test` without `--isolate` shares one global object across test files and fails dozens of tests from cross-file state pollution (`bunfig.toml` `[test] isolate` is not honored as of Bun 1.3), and running `bun test` from the repo root breaks subprocess stdio pipes.
112
171
  - Split signal tests in two. `<model>.signal.spec.ts` holds reusable fixtures built on `sampleOf(cnst.XInput)` with explicit `Promise<cnst.X>` return types and **no assertions**. `<model>.signal.test.ts` holds the assertions: `describe("<Model> Signal")`, `let` fixtures at describe scope, one `beforeAll`, story-ordered `it`s, and negatives via `await expect(p).rejects.toThrow()`.
113
- - `lib/user/user.signal.spec.ts` is the one place agent types are re-exported and re-typed; import `UserAgent` / `AdminAgent` from there rather than from the shared lib directly.
172
+ - `lib/user/user.signal.spec.ts` is the one place agent types are re-exported and re-typed; import `UserAgent` / `AdminAgent` from there rather than from the owning lib directly.
114
173
  - A placeholder `it` with a descriptive title is an acceptable floor. Write a real suite when the behaviour is security-relevant.
115
174
 
116
175
  ## Comments
@@ -141,7 +200,7 @@ Do not narrate code. Do document the thing the code cannot say. Both halves are
141
200
  - Use Bun and ESM assumptions from the root `tsconfig.json`.
142
201
  - Prefer path aliases over deep relative imports when crossing package boundaries.
143
202
  - Use `akanjs/*` for framework facets, `@apps/*` for apps, `@libs/*` for shared libs, and `@contract/*` for contract code.
144
- - Respect existing client/server entrypoints such as `@libs/shared/client`, `@libs/shared/server`, `@apps/akasys/client`, and `@apps/akasys/server`.
203
+ - Respect existing client/server entrypoints — `@libs/<lib>/client` / `@libs/<lib>/server` and `@apps/<app>/client` / `@apps/<app>/server`.
145
204
  - Let Biome organize imports instead of manually reshuffling unrelated imports.
146
205
  - Namespace the generated barrels in backend `.ts` files: `import * as cnst from "../cnst"`, `* as db`, `* as srv`. Use `import type * as srv` in services so the runtime graph stays lazy, and a value import in signals.
147
206
  - In `.tsx` files use one flat named import from the package client path (`import { cnst, fetch, st, Ticket, usePage } from "@apps/<app>/client"`) — never a relative `../` import.
@@ -196,62 +255,86 @@ are, because those are the ones the server could have done.
196
255
 
197
256
  ### Server-Side Implementation Playbook
198
257
 
199
- **① Wrap the interaction, not the UI.** The smallest useful client component is a shell that adds one behaviour and
200
- renders `children` untouched. The children stay server components, so the markup inside them never reaches the
201
- bundle. `libs/shared/ui/Only/User.tsx` is the shape: it reads auth state on the client and returns `{children}`.
202
-
203
- ```tsx
204
- "use client";
205
- export const ClickWrapper = ({ children, onPick }: ClickWrapperProps) => (
206
- <div onClick={onPick}>{children}</div>
207
- );
208
- ```
209
-
210
- **② Split compound components so panels stay on the server.** A tab, accordion, or disclosure needs client state
211
- only for *which* part is visible — never for what the parts contain. Split into a context provider plus menu and
212
- panel pieces, and take panel content as `children`. `Tab` / `Tab.Menus` / `Tab.Menu` / `Tab.Panel` in `akanjs/ui`
213
- is exactly this: only the provider and the menu hold state, and `<Tab.Panel>` renders its children as-is, so a
214
- server `Unit`/`View` passed in stays server-rendered. Never reach for one `"use client"` file with a mode
215
- `useState` and every panel body inlined.
216
-
217
- **③ Sync state instead of fetching it.** A server component cannot hold state, so render the initial data on the
218
- server and hand it across the boundary as a serializable object. That is what `init` / `view` props are: the route
219
- calls `fetch.initXInY(...)` / `fetch.viewX(...)`, passes the result into a `Zone`, and `Load.Units` / `Load.View`
220
- hydrate the store from it. Never replace that with a `useEffect(…, [])` that fetches on mount — it renders an empty
221
- shell, hydrates, then round-trips for data the server already had.
222
-
223
- **④ Push the boundary down to the leaf that needs it.** When a `Zone` reads the store, it should hold *zero*
224
- markup and delegate to a server `View`. `User.Zone.Self` is one line — `st.use.self()` into
225
- `<User.View.General user={self} />` — so the whole detail surface renders server-side wherever a route uses the
226
- `View` directly.
227
-
228
- **⑤ Hand the promise across, not the awaited value.** `ClientInit` / `ClientView` are `PromiseOrObject<T>`, so a
229
- route may pass an unawaited `fetch.initX(...)`; `Load.*` renders a skeleton and resolves it. `await` in the route
230
- blocks the shell for data the page needs immediately; passing the promise streams the rest. Independent fetches
231
- still go through one `Promise.all`.
232
-
233
- **⑥ Use named `ReactNode` slots, not just `children`.** A client shell can take several server-rendered subtrees:
234
- `Layout.Navbar` accepts `title`, `back`, `left`, `right`, and `children`, so a client navbar composes server
235
- content in five places instead of absorbing it.
236
-
237
- **⑦ Let the server do the derived work.** Display and predicate logic belongs on `Light<Model>` (`isNew()`,
238
- `canWrite(user?)`, `formatTimes()`), and enum→class lookups belong in a module-scope `as const` map. Both sides
239
- call the same method, so the server can render the result — a client component that exists only to compute a label
240
- is markup in the wrong place.
241
-
242
- **⑧ Gate auth on the server.** `getSelf({ unauthorize: "/signin" })` in `_layout.tsx` redirects before any HTML is
243
- sent. A client-side auth check costs a hydration round-trip and flashes the wrong UI first.
244
-
245
- **⑨ Prefer CSS over client state for pure visibility.** Toggling with a `data-*` attribute plus `group-data-[…]`
246
- variants (see `libs/util/ui/Grid/*`) or with `<details>`/`<summary>` keeps both branches server-rendered. Reach for
247
- `useState` when the state is real, not when a variant would do.
248
-
249
- **⑩ Keep the heavy island out of the first load.** A large client-only widget goes behind the
250
- `ui/<Folder>/index_.tsx` + `lazy()` pair so the server renders the page around it. `usePage()` and `l()` work in
251
- server components, so translation never forces a boundary.
252
-
253
- Full version with code, the `Tab` composition example, and a review checklist: `get_guideline` with `ssrRule`, or
254
- `akan guideline show ssrRule`.
258
+ 1. **Wrap the interaction, not the UI.** The smallest useful client component adds one behaviour and renders
259
+ `children` untouched, so the markup inside never reaches the bundle.
260
+ 2. **Split compound components so panels stay on the server.** `Tab` / `Tab.Menus` / `Tab.Menu` / `Tab.Panel` in
261
+ `akanjs/ui` is the shape: only the provider and menu hold state. Never one `"use client"` file with a mode
262
+ `useState` and every panel body inlined.
263
+ 3. **Sync state instead of fetching it.** The route calls `fetch.initXInY(...)` / `fetch.viewX(...)` and passes the
264
+ result into a `Zone` as an `init` / `view` prop; `Load.Units` / `Load.View` hydrate the store from it. Never a
265
+ `useEffect(…, [])` that fetches on mount.
266
+ 4. **Push the boundary down to the leaf that needs it.** A store-reading `Zone` should hold zero markup and
267
+ delegate to a server `View`.
268
+ 5. **Hand each promise across, not the awaited value.** `fetch.init<Model><Suffix>` / `view<Model>` / `edit<Model>`
269
+ are awaitable *and* destructurable: `const { xInitInY, xListInY } = fetch.initXInY(id)` hands out one promise
270
+ per field with both queries already in flight. Give `xInitInY` to a `Zone` and `xListInY` to a
271
+ `<Load.Stream of={…}>`; each renders behind its own boundary as its own data lands, so the page never waits
272
+ for the slowest. `await` still returns the old shape and keeps the whole section in the shell — which is what
273
+ SEO snapshots, prerendering and pre-hydration E2E read, so await what the page needs immediately and stream
274
+ the rest. `xListInY` / `xInsightInY` hold hydrated model instances that React Flight refuses as client props:
275
+ consume them in a server component, never as a `Zone` prop.
276
+ 6. **Use named `ReactNode` slots, not just `children`** — `Layout.Navbar` takes `title`, `back`, `left`, `right`,
277
+ and `children`, so a client shell composes server content in five places instead of absorbing it.
278
+ 7. **Let the server do the derived work.** Display and predicate logic belongs on `Light<Model>`; enum→class
279
+ lookups belong in a module-scope `as const` map.
280
+ 8. **Gate auth on the server** — `getSelf({ unauthorize: "/signin" })` in `_layout.tsx`, before any HTML is sent.
281
+ 9. **Prefer CSS over client state for pure visibility.** A `data-*` attribute plus `group-data-[…]` variants, or
282
+ `<details>`/`<summary>`, keeps both branches server-rendered.
283
+ 10. **Keep the heavy island out of the first load** — behind the `ui/<Folder>/index_.tsx` + `lazy()` pair.
284
+ `usePage()` and `l()` work in server components, so translation never forces a boundary.
285
+
286
+ Full version with code, the `Tab` composition example, and a review checklist: `get_guideline` with `ssrRule`.
287
+
288
+ ## Runtime & Build Configuration
289
+
290
+ What an app serves, how many processes it runs, where its logs go, and what ships in its image. All of it is
291
+ declared in `akan.config.ts` and narrowed — never widened — by an env at boot. Domain code reaches none of it.
292
+ Full contract: `get_guideline` with `runtimeRule`, or `akan guideline show runtimeRule`.
293
+
294
+ - **Web surfaces are `web: true | false | { csr: boolean }`.** `{ csr: false }` drops the mobile/CSR bundle,
295
+ `false` is an API-only build that serves nothing under `page/`. There is no CSR-without-SSR option, by type —
296
+ the CSR bundle inlines the stylesheet the SSR base artifact compiles. `AKAN_SSR` / `AKAN_CSR` narrow further at
297
+ runtime and can never switch a surface the build left out back on. `akan start` ignores `web` and keeps the
298
+ whole dev surface.
299
+ - **One traffic replica runs in the container's only process.** `AKAN_REPLICA=0,0,1` — the default everywhere —
300
+ has nothing to balance, so there is no gateway and no unix-socket proxy hop. Two or more replicas, a batch-only
301
+ replica, `AKAN_SOLO=false`, passing `replica` to `new AkanApp(...)`, or `akan start` all bring the gateway back.
302
+ The RSC worker is always its own process (`--conditions react-server`). A solo process answers
303
+ `/_akan/app/health`, `/_akan/app/metrics` and `/_akan/bench/ping` in the gateway's own shape, and owns the
304
+ rotating log file; nothing supervises it but the orchestrator's probes.
305
+ - **Route prefixes move together or not at all.** `new AkanApp({ prefix, websocketPrefix })` in `main.ts` moves
306
+ the server's routes, the gateway's websocket upgrade and — through the SSR bootstrap script — the
307
+ `fetchClient` in every tab the server renders. A prebuilt CSR shell or a Capacitor bundle is never rendered by
308
+ a server, so `api: { prefix, websocketPrefix }` in `akan.config.ts` is what those follow; declare it there too
309
+ when the app ships either. Read the value with `getApiPrefix()` / `getWsPrefix()` from `akanjs/base` — never
310
+ write `/api` or `/ws` as a literal, and never try to hand it down as a React prop: `FetchClient` fixes its
311
+ origin before the first component renders. A blank prefix, a bare `/`, and one that shadows a basePath are all
312
+ refused.
313
+ - **`main.ts` imports `AkanApp` from `akanjs/server/akanApp`, not the barrel** — the barrel re-exports
314
+ `AkanServer`, whose graph the gateway never runs. Keep entrypoint imports at the leaf.
315
+ - **Every `Logger` call builds a `LogRecord`** carrying `traceId`, `endpoint` and `origin` alongside the message,
316
+ so one request's lines share a trace. Request context is on by default, production included
317
+ (`AKAN_LOG_CONTEXT=0` opts out). **Give every sink a floor** — `Logger.addSink(sink, { minLevel })`; a sink with
318
+ none makes every `verbose` call render. **Never log per delivered record in anything that delivers records.**
319
+ Tail a running server with `akan logs <app>` (`--level --grep --endpoint --trace --child --role --origin
320
+ --since`, `--json`) or `.tail` / `.trace <id>` inside `akan console`.
321
+ - **`AKAN_LOG_FORMAT=ndjson` makes the container's stdout one JSON record per line, and the hub owner (gateway or
322
+ solo replica) its only writer** — every other server process turns its console off and forwards from
323
+ `AKAN_LOG_STDOUT_LEVEL` (default: the console level); a child's or the RSC worker's crash stack is wrapped as a
324
+ `raw=true` record instead of breaking the stream. Text stays the default; turn it on in the deployment.
325
+ `AKAN_LOG_STREAM_TOKEN` mounts `/_akan/app/logs` (SSE, `Last-Event-ID` resume, explicit `gap` events); unset,
326
+ the route does not exist. `AKAN_LOG_CANONICAL=1|slow` writes one `ok|error <endpoint> ms= status=` line per call;
327
+ `AKAN_LOG_FLIGHT=1` keeps each call's last 64 sub-level records and promotes them (`flight=true`) only when it
328
+ failed or ran past `AKAN_LOG_FLIGHT_MS`; `x-akan-debug` lowers one request to `trace` — in `local`, or with the
329
+ `AKAN_LOG_DEBUG_HEADER` secret. Structured values go in `LogRecord.attrs` via `Logger.emit(...)`; a key naming a
330
+ secret is `[redacted]` before the record exists.
331
+ - **`docker` is `string | { image, preRuns, postRuns, command }`** — a whole Dockerfile, or the parts Akan
332
+ assembles one from; the string form takes no contributions. The generated image installs `ca-certificates` and
333
+ `tzdata` and nothing else, so an app needing `ffmpeg` or Chromium declares it in `preRuns` / `postRuns`. A lib
334
+ declares the steps its own runtime needs and every mounting app inherits them.
335
+ - **`assets: { pruneFonts, keepFonts }`** trims from the `dist` copy of `public/` the fonts nothing reads; source
336
+ trees are never touched. A font with `optimize` on is a build input, not a runtime asset. `keepFonts` belongs to
337
+ the `akan.config.ts` that owns the font, written against that scope's own `public/`.
255
338
 
256
339
  ## React Components And Styling (`**/*.tsx`)
257
340
 
@@ -259,13 +342,21 @@ Full version with code, the `Tab` composition example, and a review checklist: `
259
342
  - Never `React.FC`, never `defaultProps`, never `PropsWithChildren`. Defaults go in the destructuring (`prefix = ""`); children are typed `children: ReactNode`.
260
343
  - `"use client"` on line 1 above the imports is mechanical by file role: every `.Zone.tsx`, `.Template.tsx`, and `.Util.tsx` has it; no `.Unit.tsx` or `.View.tsx` ever does. `usePage()` is legal in server files.
261
344
  - Conditional render is `cond ? <X/> : null`. Never `{cond && <X/>}` — in a `className` context it renders the literal string `"false"`. Early `return null` is for guard clauses only.
262
- - Never hand-roll loading, empty, or list states. Use `Load.Units` / `Load.View` / `Load.Edit` with `renderItem`, `renderList`, `renderView`, and `renderEmpty`; `<Empty />` for a bare placeholder; and `Model.New` / `Model.Edit` / `Model.SureToRemove` for CRUD modals.
345
+ - Never hand-roll loading, empty, or list states. Use `Load.Units` / `Load.View` / `Load.Edit` with `renderItem`, `renderList`, `renderView`, and `renderEmpty`; `<Empty />` for a bare placeholder; and `Model.New` / `Model.Edit` / `Model.SureToRemove` for CRUD modals. For data no `Load.*` covers, `<Load.Stream of={promise} fallback={…}>{(value) => …}</Load.Stream>` awaits one promise behind its own Suspense boundary; a resolved value renders in the shell with no boundary at all, so the same call site works either way.
263
346
  - Avoid hooks. `useState` is for modal-open, tab, draft-input, and drag state only — never for server data. `useEffect` must be a genuine effect such as subscribe-with-cleanup or one-shot init. Prefer `Tab` over a `useState` mode switch. `.Template.tsx` files contain zero `useState`.
264
- - Forms are entirely store-driven: `value={xForm.field}` with `onChange={st.do.setFieldOnX}`, the setter passed by reference. Always use `Field.*`, never a bare `<input>` for a model field. Nested rows use `st.do.writeOnX("payments.3.name", v)` plus the generated `add<Field>OnX` / `sub<Field>OnX`.
347
+ - Forms are entirely store-driven: `value={xForm.field}` with `onChange={st.do.setFieldOnX}`, the setter passed by reference. Always use `Field.*`, never a bare `<input>` for a model field. Nested rows use `st.do.writeOnX("payments.3.name", v)` plus the generated `add<Field>OnX` / `sub<Field>OnX`. **Passing the setter by reference is also what makes the framework emit `data-akan-action` / `data-akan-state`** on the control — the annotation an in-page agent, an E2E selector, and an external browser agent all read. Wrapping it in an inline arrow (`onChange={(v) => st.do.setFieldOnX(v)}`) silently drops that: a closure the caller wrote says nothing about what it does. Never hand-write a `data-akan-*` attribute.
265
348
  - Read with `st.use.*` and write with `st.do.*`. Client components do not call `fetch.*`.
349
+ - **An edit shell recovers its own form — never persist form values yourself.** `Load.Edit`, `Model.EditModal` and
350
+ `Model.New` save the whole `<model>Form` as the user types and offer it back on the next open, so a modal closed
351
+ by accident, a route change, or a killed app costs nothing. The scope is the record id for an edit and the seed
352
+ plus the route for a new form, both keyed to the signed-in user, so another record or another parent never loads
353
+ someone else's half-finished form. A new form's draft is applied on open with a "start over" chip; an edit whose
354
+ record moved in the meantime is *not* applied, and asks. Turn it off with `draft={false}`, or name the scope
355
+ yourself with `draft="<scope>"` when the context is in neither the id nor the seed. `field.secret` and
356
+ `field.hidden` values are never saved. The old per-field `cache` / `cacheKey` props are deprecated and store
357
+ nothing — they covered five control types, keyed on the translated label, and restored over server data.
266
358
  - 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"`).
267
359
  - Multi-slot components take extra named props (`wrapperClassName`, `bodyClassName`), never a `classNames` object.
268
- - Use daisyUI semantic tokens with opacity modifiers (`text-base-content/60`, `border-base-content/10`, `bg-base-100/70`, `bg-primary/10`). Never use `dark:` — theming is the daisyUI theme block in `page/*/styles.css`. Raw hex belongs only in marketing surfaces; match the neighbouring files.
269
360
  - Hoist enum→class lookups to a module-scope `as const` map typed `{ [key in cnst.XStatus["value"]]: string }`. Do not use `Record<...>`. Escalate the map to `webkit/` when a second module needs it.
270
361
  - Use `<Link>` from `akanjs/ui` for internal navigation; `<a>` only for `mailto:` and external links.
271
362
 
@@ -293,23 +384,26 @@ Full version with code, the `Tab` composition example, and a review checklist: `
293
384
 
294
385
  **`<model>.constant.ts`** — five classes in order with one blank line between them and `enumOf("camelName", [...] as const)`
295
386
  classes above: `XInput → XObject → LightX → X → XInsight`. Write `XInsight` even when it is empty. Put display and
296
- predicate logic on `LightX` (`isNew()`, `canWrite(user?)`, `formatTimes()`, `isCancellable()`) — the Light class is the
297
- one both server and client hold, so shared logic belongs there instead of in a util module. This is the most commonly
298
- missed rule in the codebase. Collection-level helpers go `static` on the full model. Give any field whose business
299
- meaning is not obvious a short trailing comment.
387
+ predicate logic on `LightX` (`isNew()`, `canWrite(user?)`, `formatTimes()`) — the Light class is the one both server
388
+ and client hold, so shared logic belongs there instead of in a util module. **This is the most commonly missed rule
389
+ in the codebase.** Collection-level helpers go `static` on the full model. Give any field whose business meaning is
390
+ not obvious a short trailing comment.
391
+
392
+ **A model instance's `Date` fields are prototype accessors, not own properties.** `user.createdAt` is still a `Dayjs`,
393
+ built on first read from a native `Date` the instance holds under a symbol. So `Object.keys(user)` and `{ ...user }`
394
+ do **not** include them, while `"createdAt" in user`, `for...in`, `JSON.stringify(user)`, `plainFieldsOf(user)`
395
+ (`akanjs/common`), `immerify`, and `deepObjectify` all do. To copy a model, `new cnst.X().set(user)`, never a spread.
300
396
 
301
397
  **`<model>.document.ts`** — fixed order: `XFilter extends from(...)` → `X extends by(...)` → `XModel extends into(...)`,
302
398
  with `sort: {}` always present. Chain methods validate → mutate → `return this`, and never `save()`; the caller saves,
303
399
  so chains compose (`org.removeUser(id).removeInvite(id).save()`). Put a one-line comment above each stating the
304
400
  transition. Atomic counters live on the Model class with the updater-callback form, returning `!!modifiedCount`.
305
401
  Indexes and derived totals go in `static override _onSchema`, not in the service. **Removal is always soft** — the
306
- model facade's `removeMany(query)` and the store's `removeManyByQuery` stamp `removedAt` like `remove(id)` does; the
307
402
  framework has no hard delete for a model table, and `delete` is deliberately left unused so it can mean one later.
308
- The facade keeps `Many`/`One` spelled out on its writes (`updateOne` / `updateMany` / `removeOne` / `removeMany`):
309
- a bare `update`/`remove` would read like the document-path `update(id)` / `doc.remove()` while hitting every match.
310
- Only the count was shortened — `count(query)`, with `countDocuments` kept as `@deprecated`. `updateById(id, update)`
311
- and `removeById(id)` are those same query-level writes narrowed to one id, **not** the document path: they fire no
312
- hooks either, so a model whose removal cascades or carries a `_postRemove` still goes through `remove<Model>(id)`.
403
+ The facade spells `Many`/`One` out on its writes (`updateOne` / `updateMany` / `removeOne` / `removeMany`); only the
404
+ count was shortened to `count(query)`, with `countDocuments` kept as `@deprecated`. `updateById(id, update)` and
405
+ `removeById(id)` are those same query-level writes narrowed to one id, **not** the document path: they fire no hooks,
406
+ so a model whose removal cascades or carries a `_postRemove` still goes through `remove<Model>(id)`.
313
407
 
314
408
  **`<model>.service.ts`** — keep methods to a few lines: load → chain → `return await ….save()`. Write `return await`
315
409
  explicitly in tail position; do not "optimize" it away. Side effects belong in `override async _preUpdate` /
@@ -325,9 +419,8 @@ stores need none, because state and CRUD actions are generated. The body is thre
325
419
  `this.setX(...)` → toast. The optimistic shape is mutate the client model, `void fetch.*`, then commit. Use
326
420
  `this.pick(...)` when the value must exist, `this.get()` when it may not, and `this.set({...})` to write. Mutate lists
327
421
  through the collection API (`this.set({ xList: xList.set(x).save() })`), not array spread. **An action returns
328
- nothing** — `st.do.<action>()` is typed `void` / `Promise<void>`, so hand the result to `this.set({...})` rather
329
- than returning it (`no-return-in-store-action.grit`); a bare `return;` guard stays fine. **Never
330
- `import type { RootStore } from "../st"`** — it crashes `akan build` with a Bun SSR segfault.
422
+ nothing** (`no-return-in-store-action.grit`). **Never `import type { RootStore } from "../st"`** — it crashes
423
+ `akan build` with a Bun SSR segfault.
331
424
 
332
425
  **`<model>.dictionary.ts`** — fixed chain, with empty stages still written:
333
426
  `.of() → .model() → .insight() → .query() → .sort() → .enum() → .slice() → .endpoint() → .error() → .translate()`.
@@ -350,17 +443,34 @@ workflow changes.
350
443
 
351
444
  ### Guards And Transports
352
445
 
353
- - 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()`.
354
- - 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.
355
- - 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.
356
- - 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.
446
+ Full contract — credential handshake, room revalidation, socket cleanup scoping: `get_guideline` with
447
+ `transportRule`, or `akan guideline show transportRule`.
448
+
449
+ - Guards run on both HTTP and websocket calls. Read the caller with `context.get<T>("account")` instead of
450
+ branching on `getHttpContext()` / `getWebSocketContext()`, and keep them side-effect free and safe to re-run —
451
+ a pubsub room's guards are re-run whenever the socket's credential changes.
452
+ - Slice-level `guards` only reach the generated query/mutation endpoints. A `pubsub`/`message` endpoint is
453
+ unguarded unless it declares its own `guards` in its signal option.
454
+ - **Never read the caller's IP off the socket or the request peer — take `.with(Ip)`** (`context.getClientIp()`
455
+ inside a guard or middleware). Behind a federation gateway, `ws.remoteAddress` and the child's own peer are the
456
+ *gateway* (`127.0.0.1`) for every caller and for the whole life of every socket. `Ip` reads what a proxy
457
+ recorded, is unwrapped from its `::ffff:` form, and is `null` rather than a placeholder when genuinely unknown.
458
+ - **Every socket carries a `socketId`, and only the framework mints one** — read it off the `Ws` internal arg
459
+ (`.with(Ws)` hands `{ ws, socketId, subscribe, on, off }`). Never mint your own from `ws.data`: the room
460
+ bookkeeping keys on the framework's id and a second one fails to match it silently. It identifies a
461
+ **connection**, not a caller, so per-user state keys on the account.
462
+ - **A cleanup registered with `ws.on("disconnect" | "unsubscribe", fn)` is scoped to the call that registered it**
463
+ — a room for a `pubsub` subscribe, the socket for a `message` handler. Cleanup that must happen either way
464
+ registers for both.
357
465
 
358
466
  ### Authorization Defaults
359
467
 
360
468
  - **Every `slice()` takes an explicit `{ guards: {…} }` second argument, and `root:` is always `Admin`.**
361
469
  - **Every custom `mutation` / `query` / `message` names its own `guards: [...]` array.** Never rely on the slice default. `Public` belongs on a slice `get:`, never on a mutation.
470
+ - **The guards are also the MCP exposure decision** — see MCP Exposure. An endpoint that names none is not published to agents at all, and a mutation whose only guard is `Public` is refused, so a missing `guards` array now costs visibility as well as authorization.
362
471
  - Resource guards are `Can<Verb><Model>` classes in `srvkit/guards.ts` that `implements Guard` with an `async canPass(context)`. They **fail closed**: no resource named ⇒ `false`; a load that throws ⇒ `logger.warn` then `false`. Admin bypass goes first.
363
472
  - Keep `static name = "User";` on guard classes. `fetch` serializes guard names and the API explorer filters on them; it looks like dead code, and deleting it breaks the UI. Comment it so the next reader knows.
473
+ - **Every guard class also declares `static scope: GuardScope`, and it is required with no default.** `"account"` means the verdict reads the caller and nothing about the call, so it can be evaluated with no arguments — which is what lets an MCP listing hide what this caller certainly cannot use. `"resource"` means it needs the call's arguments (`context.getArg()`) and fails closed without them, so it is never evaluated for a listing: the entry stays visible and is stopped at call time. Getting it wrong is not a type error, so the marker is mandatory rather than defaulted — `SignedIn` / `Admin` / role checks are `"account"`, and every `Can<Verb><Model>` is `"resource"`.
364
474
  - The acting user arrives via `.with(Self)` / `.with(CurrentUserId)` / `.with(Me)`. Never trust a client-supplied id.
365
475
  - Guards ship with the library that owns the model and are imported by its own signals through the package path, so a mounting app inherits authorization and cannot forget it.
366
476
  - Services re-check ownership even when a guard already gated the call — two independent gates.
@@ -370,7 +480,24 @@ workflow changes.
370
480
 
371
481
  - `.body(...)` / `.param(...)` args accept `ConstantFieldTypeInput` only: scalars, model refs, or `enumOf(...)`.
372
482
  - Numbers must use `Int` or `Float` — `Number` is rejected (`pkgs/akanjs/signal/endpointInfo.ts`).
373
- - `Upload` is valid only inside a mutation flagged for file upload: `mutation([cnst.File], { fileUpload: true }).body("files", [Upload])` (see `libs/shared/lib/file/file.signal.ts`). It is not a model field type.
483
+ - `Upload` is valid only inside a mutation flagged for file upload: `mutation([cnst.File], { fileUpload: true }).body("files", [Upload])`, as the `file` module does. It is not a model field type.
484
+ - Bytes are `Binary`, never `Any` — see Scalar & Field Type Reference.
485
+
486
+ ### Binary Pubsub And Mutation Verbs
487
+
488
+ Full contract — backpressure modes, the `Any`-carrying-bytes fallback, multi-verb paths: `get_guideline` with
489
+ `transportRule`.
490
+
491
+ - **`pubsub(Binary)` sends its payload in a websocket binary frame**, skipping the JSON envelope and the base64 a
492
+ JSON wire would need; the client's subscribe callback receives a `Uint8Array`. The optimization applies only
493
+ when the **whole** return is `Binary`. A declared `Binary` room **coalesces under backpressure** — keeping only
494
+ the newest frame — which is what telemetry and video want; declare `{ backpressure: "queue" }` when the frames
495
+ are a sequence a subscriber must see in full. A `pubsub(Any)` carrying bytes is framed too and warns once;
496
+ declare `Binary`.
497
+ - **A `mutation` is `POST`.** `{ method: "PATCH" | "PUT" | "DELETE" }` moves it, and one path may carry several
498
+ verbs; two endpoints claiming the same path **and** verb fail the boot. Reach for it only when a foreign wire
499
+ protocol forces the verb — Akan's own `fetch.*` client, the OpenAPI document, and the API explorer all follow
500
+ whatever is declared.
374
501
 
375
502
  ### Reserved Endpoint Names
376
503
 
@@ -379,45 +506,49 @@ workflow changes.
379
506
 
380
507
  ### Slices, Queries, and Hydration
381
508
 
382
- - A slice's `exec` returns a `QueryOf` (an opaque query descriptor, `pkgs/akanjs/constant/types.ts`); you **cannot** chain `.sort()`/`.limit()` on it.
383
- - Apply ordering/paging via the store `init` fetch option instead: `initX(..., { sort, page, limit })` (`pkgs/akanjs/fetch/fetchType/sliceFetch.type.ts`).
384
- - Generated list accessors like `listBy(...)` return `Promise<Doc[]>`. For a chainable builder (`.sort().skip().limit().select()`) use the model facade's `findMany`/`findOne` (`FindManyChain`, `pkgs/akanjs/document/into.ts`).
385
- - **Hydrated vs raw:** server queries return hydrated `cnst.<Model>` instances (with `set`/`save`/`refresh`); client fetch results are raw `GetStateObject` plain data (functions stripped, `pkgs/akanjs/base/types.ts`).
509
+ Full contract — filter-arg `ref` pickers, `getQueryMeta` summary counters, `labelOf`, and the `q.exists` /
510
+ `q.missing` / `q.empty` split: `get_guideline` with `queryRule`, or `akan guideline show queryRule`.
511
+
512
+ - **The root slice (`""`, generated by `slice()`) takes `queryKey` and `args`, not a query.** `initX("byOwner",
513
+ [ownerId])` names one of the model's own filters and passes that filter's args; no key at all is the `any`
514
+ filter every model carries. A raw query descriptor could not survive the query string — `String(value)` sends
515
+ `[object Object]`. It is an admin API: `root:` is always `Admin`.
516
+ - A slice's `exec` returns a `QueryOf` (an opaque query descriptor); you **cannot** chain `.sort()`/`.limit()` on
517
+ it. Apply ordering/paging via the store `init` fetch option instead: `initX(..., { sort, page, limit })`. For a
518
+ chainable builder use the model facade's `findMany`/`findOne`.
519
+ - **Hydrated vs raw:** server queries return hydrated `cnst.<Model>` instances (with `set`/`save`/`refresh`);
520
+ client fetch results are raw `GetStateObject` plain data, functions stripped.
386
521
  - Every filter generates fourteen methods: `list` · `listIds` · `find` · `findId` · `pick` · `pickId` · `exists` ·
387
- `count` · `insight` · `query` · **`remove`** · **`removeOne`** · **`update`** · **`updateOne`**. The last four are
388
- query-level writes — one atomic UPDATE, **no hooks**, and therefore no `_postRemove` and no cascade. Use them on a
389
- model that carries no removal side effect; otherwise remove documents one at a time.
522
+ `count` · `insight` · `query` · **`remove`** · **`removeOne`** · **`update`** · **`updateOne`**. The last four
523
+ are query-level writes — one atomic UPDATE, **no hooks**, and therefore no `_postRemove` and no cascade. Use
524
+ them on a model that carries no removal side effect; otherwise remove documents one at a time.
390
525
  - **`update<Filter>` / `updateOne<Filter>` are chains, not calls:** `await updateInRoot(rootId).set({ status:
391
- "archived" })`. The patch cannot trail the filter args — a filter's own args may be optional and no tuple type
392
- puts a required element after those — so it lands on a terminal `.set()`, mirroring the `UPDATE … SET …` it
393
- compiles to. Building the chain touches nothing; only `.set()` runs a query.
394
- - `removeOne` / `updateOne` hit the **newest** match — the subquery they compile to is ordered `createdAt` descending
395
- and there is no way to change that. They also report only counts, never which row they touched, so they are for
396
- "there is at most one of these", not for claiming the next item off a queue. Pass a query that matches one row.
397
- - **A filter may not be keyed after its own model.** Filter methods are assigned after CRUD, so a filter `chat` on
398
- model `chat` would silently swap the single-document `removeChat`/`updateChat` for a hookless query-level one. It
399
- throws at boot instead (`assertFilterFitsCrud`).
526
+ "archived" })`. The patch cannot trail the filter args, so it lands on a terminal `.set()`; building the chain
527
+ touches nothing.
528
+ - `removeOne` / `updateOne` hit the **newest** match, always, and report only counts — they are for "there is at
529
+ most one of these", not for claiming the next item off a queue.
530
+ - **A filter may not be keyed after its own model.** A filter `chat` on model `chat` would silently swap the
531
+ single-document `removeChat`/`updateChat` for a hookless query-level one; it throws at boot instead.
532
+ - **"Has no value" is `q.empty`, never `q.missing`.** `q.missing` is the key being *absent from the stored JSON*,
533
+ which a document written once and saved again no longer is. Reach for it only to find rows written before the
534
+ field was declared.
400
535
 
401
536
  ### Text Search In A Filter — `q.search()`
402
537
 
538
+ Full contract — relevance ordering, `columns` / `weights`: `get_guideline` with `queryRule`.
539
+
403
540
  - Text search is a filter query node like any other: `bySearch: filter().arg("text", String).query((text, q) =>
404
541
  q.search(text, { prefix: true }))`. The generated `listBySearch` / `countBySearch` / `queryBySearch` /
405
542
  `insightBySearch` come for free — you do **not** need a slice to make search usable.
406
543
  - **Only add a search slice when the model's data is safe to enumerate.** A filter is server-side; a slice is a
407
- client-callable endpoint, so on a model whose slice `get:` is `Public` a search slice hands anyone a way to walk the
408
- table. Leave that decision to the mounting app.
544
+ client-callable endpoint, so on a model whose slice `get:` is `Public` a search slice hands anyone a way to walk
545
+ the table. Leave that decision to the mounting app.
409
546
  - `q.search()` compiles to a JOIN, not a WHERE fragment, so it **must sit at an AND position**. Nesting it under
410
- `q.any()` or `q.not()` throws, and it is rejected in `updateOneByQuery` / `updateManyByQuery` — a query-level write
411
- takes no join, so ignoring the node would silently widen the write to every other matching row.
547
+ `q.any()` or `q.not()` throws, and it is rejected in `updateOneByQuery` / `updateManyByQuery`.
412
548
  - Blank or whitespace-only input matches **nothing**. Never "fix" that into a passthrough: a passthrough turns a
413
549
  search endpoint into a full listing.
414
- - Order by relevance with the built-in `relevance` sort key. It is an empty sort map, which the store reads as
415
- "unspecified": score order when a search join is present, `createdAt` descending otherwise. That fallback is the
416
- compiler's own, not a model-defined default — redefining `latest` on the model does not change it.
417
- - **A slice endpoint never reaches "unspecified".** The resolver fills `latest` before the query is built, so a
418
- client asking for the score order has to name `relevance`; leaving `sort` off gets `latest`, not relevance.
419
- - Scope a search with `columns` (`q.search(text, { columns: ["title"] })`) and re-weight with `weights`, a tuple of
420
- finite numbers positional over `["title", "desc", "tag", "filter"]`.
550
+ - Order by relevance with the built-in `relevance` sort key. A slice endpoint never reaches the unspecified
551
+ fallback — leaving `sort` off gets `latest`, so a client wanting score order has to name `relevance`.
421
552
 
422
553
  ### Service / Signal Injection
423
554
 
@@ -441,8 +572,8 @@ export class AdminNoti extends adapt("adminNoti" as const, ({ use, env, plug, me
441
572
  - Inject it with `plug(TheClass)` from a service or from another adapter. Destructure only the injectors you use, and write the registration key `as const`.
442
573
  - **Do not register an `adapt()` class in `lib/option.ts`.** It self-registers, and `plug(Class)` uses the class itself as the token. `option.ts` is now only for legacy constructor-style adapters and for widening the options type.
443
574
  - `this.logger` is provided; never construct a `Logger` inside an `adapt()` class. Lifecycle work goes in `override async onInit()`.
444
- - **`adapt()` is for singletons only.** A per-use value object stays a plain class you `new` at the call site. Ask whether there is exactly one per process that a service needs injected; if not, it is a plain class.
445
- - **Legacy shape — recognise it, do not copy it.** Plain classes with `constructor(options: XOptions)`, registered in `lib/option.ts` as `options.x ? new XApi(options.x) : null` and injected with `use<T>()`, still work. Migrate one to `adapt()` only when you are already changing it.
575
+ - **`adapt()` is for singletons only.** A per-use value object stays a plain class you `new` at the call site. If there is not exactly one per process that a service needs injected, it is a plain class.
576
+ - **Legacy shape — recognise it, do not copy it.** Plain classes with `constructor(options: XOptions)`, registered in `lib/option.ts` and injected with `use<T>()`, still work. Migrate one to `adapt()` only when you are already changing it.
446
577
 
447
578
  Conventions that hold for both shapes:
448
579
 
@@ -451,7 +582,7 @@ Conventions that hold for both shapes:
451
582
  - Resolve secrets as `process.env.X ?? options.x ?? deterministicGenerator(...)` **inside a function**, never at module scope.
452
583
  - Extend a function by appending an optional trailing parameter with a default, never by changing arity.
453
584
  - Parameters: up to three required primitives positional; optional flags in a trailing `{ … } = {}`; four or more parameters, or any two same-typed strings, in one named destructured object.
454
- - Release locks in `finally`. Load heavy optional dependencies through a module-level memoized promise (`sharpLoad ??= import("sharp")`).
585
+ - Release locks in `finally`. Load heavy optional dependencies through a module-level memoized promise (`puppeteerLoad ??= import("puppeteer")`).
455
586
 
456
587
  ### Error Placement
457
588
 
@@ -460,6 +591,85 @@ Conventions that hold for both shapes:
460
591
  - `try/catch` is rare and always converts an exception into a decision, never swallows one. Guards catch → `logger.warn` → `return false`; adapters catch → `logger.error` → `return null`; UI uses `try/finally` to reset a spinner. A bodyless `catch {}` is acceptable only with a one-line reason.
461
592
  - Store actions do not `try/catch` — let the framework toast the `Err`. Client-side validation failure is `msg.error("<key>")` plus an early return, never a throw.
462
593
 
594
+ ### MCP Exposure
595
+
596
+ Every signal is served to AI agents as an MCP server on `POST /mcp`. **`/mcp` is mounted by default and exposure
597
+ follows an endpoint's guards — there is no per-endpoint opt-in, and nothing to write in a signal file.** An
598
+ endpoint that declares a real guard is published; one that declares none is refused, and so is a mutation whose
599
+ only guard is `Public`. The guards are already the authorization decision, so a second switch would say nothing
600
+ they do not — while guaranteeing that every endpoint added later is invisible to agents until somebody remembers
601
+ it.
602
+
603
+ - **`mcp: false` keeps an endpoint off the shelf without touching its guards.** Guards answer "may an agent call
604
+ this"; they cannot answer "does this belong on a shelf", and a step of a UI-driven state machine
605
+ (`requestPhoneCodeForSignin`) is perfectly guarded and still a mistake for a model to reach. Write it in the
606
+ signal option (`mutation(Boolean, { guards: [Every], mcp: false })`, `init({ guards: [SignedIn], mcp: false })`),
607
+ or as an `mcp` map on `slice()` that **mirrors the `guards` map key for key** (`mcp: { cru: false }`) — root
608
+ slice and generated CRUD only, never a named slice or a custom endpoint. It is an opt-**out**, and it is
609
+ curation, not authorization: HTTP serves the endpoint exactly as before.
610
+ - **Narrow by cost, and read the boot log first.** MCP forbids a `$ref` across entries, so every entry inlines the
611
+ full schema of every model it mentions and the listing is re-sent whole to every agent that connects. A plain
612
+ 21-field model costs 12.6KB over eight entries; `mcp: { cru: false }` takes it to 4.7KB. `MCP catalogue: … ·
613
+ listing 214KB` plus a per-signal `MCP catalogue cost:` line says where it went.
614
+ - Settings live in the app's `lib/option.ts` — `option.setMcp({ enabled, readOnly, path, version, instructions,
615
+ allowedOrigins, pageSize, language, auth })`, **not `main.ts`**. Each field has an `AKAN_MCP_*` env spelling.
616
+ `AKAN_MCP_READONLY=true` is the read-only-deployment valve, not the exposure switch; `AKAN_MCP=false` takes the
617
+ whole surface off.
618
+ - **The refusals are fail-closed**: a declared `mcp: false`, an endpoint with no `guards`, a mutation with no real
619
+ guard, `pubsub` and `message`, an `Any` or `Upload` return, a file upload, a required `Any` argument, and the
620
+ generated `light<Model>` read. A `prompt` also refuses a list argument and any `Any` argument. **Every refusal
621
+ is named in the boot log**, along with every published entry missing a dictionary `.desc()` — an agent picks a
622
+ tool by its description.
623
+ - A refused endpoint answers the *same* "unknown tool" as one that does not exist, and a guard's refusal is
624
+ generalized. Never make either message more helpful — the difference is what enumerates your private surface.
625
+ - **A `field.visual` field is stripped from every MCP result** and from the readable schema so the two agree.
626
+ - **A structured result ships twice by default** — as `structuredContent` and as the same JSON in the text block.
627
+ `option.setMcp({ legacyTextBlock: false })` leaves a pointer there instead.
628
+ - **`prompt()`** is invoked by the *user* as a slash command, never chosen by the model. `exec` returns
629
+ `PromptMessage[]` built with `Msg.user` / `Msg.assistant` / `Msg.resource` / `Msg.image`; an embedded payload is
630
+ masked by the model you name, and one whose `hidden`/`secret` fields are populated with no model named is
631
+ refused. It is also mounted as a plain HTTP `GET` whether or not MCP is enabled, so guard it like any other read.
632
+
633
+ Full contract — configuration, wire behaviour, resource URIs, OAuth metadata, protocol revisions, and
634
+ `McpProgress.report`: `get_guideline` with `mcpRule`, or `akan guideline show mcpRule`.
635
+
636
+ ## In-Page Agent
637
+
638
+ Every akan app can host a component-level agent that reads the rendered screen and drives it. **A component
639
+ declaration is the surface, exactly**: `st.tool` publishes one action, and `st.use` / `st.sel` / `st.ref` make one
640
+ store key readable while the reading component is mounted. Nothing is derived from a store class — a lever the
641
+ screen does not offer the user is not one an agent may pull in their place. The React core is the `use-agentic`
642
+ package; apps and libs never import it directly (`no-import-external-library`) — everything reaches them through
643
+ `st.*` and `akanjs/ui`.
644
+
645
+ Read the full contract before building an agent surface — the chat's own options, `st.tool` / `st.expose` /
646
+ `st.useState` in full, zones and namespaces, attachments and speech, slash commands, transcript compaction, and
647
+ the built-in tool semantics: `get_guideline` with `agentRule`, or `akan guideline show agentRule`. What follows is
648
+ what every developer must know even when not building one.
649
+
650
+ - **Mount `<Agent.Chat />` once in a layout.** The endpoint is a stateless relay that **never executes tools**:
651
+ every tool runs in the caller's own browser session, gated by guards and the approval card. Its guard is
652
+ `AgentRelayAccess`, which refuses every call until the app names a guard of its own —
653
+ `option.setAgentAccess(SignedIn)`. **The LLM is configured in `option.ts`, never through the environment**
654
+ (`option.setLlm({ apiKey, model, host })`).
655
+ - **Declare the tool beside the control that already does it**, never as a separate surface:
656
+ `st.tool("x").desc("…").arg("id", ID).exec(fn)` returns the callable to hand to `onClick` — one handler for the
657
+ person and the agent. Publish it only where the screen already renders the control; a falsy name declares the
658
+ tool without publishing it, and a `disabled` control publishes nothing.
659
+ - **Forms publish themselves**: a `Field.*` / `Input.*` / `Select` / `Switch` handed `onChange={st.do.setXOnY}`
660
+ **by reference** publishes that setter, and `st.use.yForm()` adds `fillYForm(patch)`. An inline arrow publishes
661
+ nothing — normalize with the control's `transform` prop, and multi-write with a `_postSet<Field>` store method.
662
+ This is the same rule `no-unpublished-form-setter.grit` enforces.
663
+ - **Reading is per key, not per store.** `st.use.x({ agent: false })` keeps a subscribed key off the surface; a key
664
+ no component reads is unreadable. A value with populated `hidden`/`secret` fields is refused at read.
665
+ - **Return what answers the question, not the record.** A tool's value is capped at 20,000 characters before it
666
+ enters the transcript, and once there it rides *every* later turn. A field that is bulky and useless to a model
667
+ is fixed once at the model instead: `field.visual(String)`.
668
+ - **Route guidance is `<Agent.Guide instructions="..." />`** rendered from a `_layout.tsx` or page — the render
669
+ tree is the cascade. It is a component, not a `pageConfig` field, and `*.abstract.md` is never served to agents.
670
+ - **Model-facing text is English, always** — every `.desc()`, `instructions`, Guide text. The `l()` rule covers
671
+ strings a *user* reads.
672
+
463
673
  ## Scalar Modeling (`**/*.constant.ts`)
464
674
 
465
675
  - Define Akan models in `.constant.ts` files with `via` from `akanjs/constant`.
@@ -469,96 +679,79 @@ Conventions that hold for both shapes:
469
679
  - Follow the established model layering pattern in this order: `Input`, `Object`, `Light<Model>`, full `<Model>`, and `<Model>Insight`. Write all five, and write `<Model>Insight` even when it is empty.
470
680
  - Put display and predicate logic on the `Light<Model>` class rather than in a util module — see Module File Playbook.
471
681
  - Defaults are a literal for scalars and a thunk for anything constructed. Arrays are `field([T])`; optional is the postfix `.optional()`.
682
+ - **`field.visual(T)` is a field the page renders and an agent never sees** — a blur placeholder, a rendered HTML body, a serialized geometry. It stays an ordinary stored `property` (persistence, search, forms and the page response untouched) and is stripped wherever a value is masked for an AI caller: every in-page-agent read and every MCP result, along with the MCP readable schema. Unlike `hidden`/`secret` it is cost, not secrecy — nothing is refused over one. Reach for it whenever a field is bulky and useless to a model; that is cheaper than every tool learning to avoid it.
472
683
 
473
684
  ### Scalar & Field Type Reference
474
685
 
475
- - **Import from `akanjs/base`** (real classes/helpers, not globals): `Int`, `Float`, `ID`, `Any`, `Upload`, `enumOf`, and the `dayjs` factory. There is **no `JSON` scalar** — use `Any` for open/flexible payloads.
686
+ - **Import from `akanjs/base`** (real classes/helpers, not globals): `Int`, `Float`, `ID`, `Any`, `Binary`, `Upload`, `enumOf`, and the `dayjs` factory. There is **no `JSON` scalar** — use `Any` for open/flexible payloads.
476
687
  - **Use the JS globals directly (no import needed)**: `String`, `Boolean`, `Date`. They are monkey-patched to behave like scalars, so `field(String)` typechecks.
477
688
  - **`Number` is not a valid field/body type.** `NumberConstructor` is intentionally not augmented, so `field(Number)` / `.body("x", Number)` fails to typecheck. Use `Int` or `Float` instead.
478
689
  - Runtime resolution of every scalar (globals included) goes through `PrimitiveRegistry` by `refName` (`pkgs/akanjs/base/primitiveRegistry.ts`).
690
+ - **`Binary` is raw bytes on the wire, and never a model field.** It is `Uint8Array` on both sides — a Node
691
+ `Buffer` is one, so a server handler may pass one straight in — and it accepts base64 in either direction, so
692
+ the same declaration serves a JSON body and a binary frame. It is **not storable**: the class build refuses
693
+ `field(Binary)` naming the `File` model instead, because every non-base field lives in the `_doc` JSON column
694
+ and bytes would sit there as base64 and ride every read of the row. Never send bytes as `Any` — `Any` passes a
695
+ `Buffer` through untouched and `JSON.stringify` then spells it `{ type: "Buffer", data: number[] }`, which
696
+ `JSON.parse` never restores; the result is 3.6x the wire and a shape that only breaks at the first byte-offset
697
+ read. MCP refuses a `Binary` return for the same reason it refuses `Any`.
479
698
 
480
699
  ### Text Search Fields — the `text` role
481
700
 
482
- - A field joins the full-text index by declaring one of five roles: `field(String, { text: "title" })`, and likewise
483
- `"desc"`, `"tag"`, `"thumb"`, `"filter"`. Nothing else opts a field in, and there is no per-model switch.
701
+ Full contract — the trigger-maintained mirror, tokenizer changes, `AKAN_SEARCH_ENABLED`: `get_guideline` with
702
+ `queryRule`.
703
+
704
+ - A field joins the full-text index by declaring one of five roles: `field(String, { text: "title" })`, and
705
+ likewise `"desc"`, `"tag"`, `"thumb"`, `"filter"`. Nothing else opts a field in, and there is no per-model
706
+ switch. Declaring roles is all the wiring there is.
484
707
  - Pick the role by what the value *is*, because `bm25` weights them positionally (`title` 10, `tag` 3, `desc` 1,
485
708
  `filter` 0): `title` is the one line a human scans for, `desc` is prose, `tag` is a keyword list, `filter` is a
486
- scoping value (status, owner, role) that must be matchable but must never outrank a real title hit.
487
- - `thumb` is mirrored for rendering a hit and is **not** indexed — never expect it to match.
488
- - **A `secret`, `hidden`, or `resolve()` field with `text` throws at class-build time**, not at query time. That is
489
- deliberate: the mirror is plaintext, so an indexed secret would leak through search. Do not work around it. The
490
- same throw covers a `text` field *underneath* one of those — a scalar's own field is reachable through its parent,
491
- so `f.secret(Noti)` where `Noti.label` carries a role is rejected at the parent, not silently indexed.
492
- - The role works on a relation too (`image: field(File, { text: "thumb" })`) and on an array (`playing: field([String],
493
- { text: "tag" })`); an array of objects indexes by leaf key, including an array leaf (`works[*].tags`). A field
494
- inside a `Map` indexes nothing: there is no fixed path to extract it from.
495
- - Declaring roles is all the wiring there is. Mirror rows are maintained by SQL triggers — not document hooks —
496
- because `updateOneByQuery` and friends fire no hooks, and most searchable-field mutations go through exactly that
497
- path.
709
+ scoping value (status, owner, role) that must be matchable but must never outrank a real title hit. `thumb` is
710
+ mirrored for rendering a hit and is **not** indexed — never expect it to match.
711
+ - **`field.secret`, `field.hidden` and `resolve()` take no `text` role — it is a compile error**, because the
712
+ mirror is plaintext and an indexed secret would leak through search. The same refusal throws at runtime as a
713
+ backstop, including for a `text` field *underneath* one of those. Do not work around either.
714
+ - The role works on a relation (`image: field(File, { text: "thumb" })`) and on an array; an array of objects
715
+ indexes by leaf key. A field inside a `Map` indexes nothing — there is no fixed path to extract it from.
498
716
  - Search runs on sqlite/libsql only. `q.search()` against Postgres throws, loudly, rather than returning every row.
499
- - `AKAN_SEARCH_ENABLED=0` switches the index off process-wide; unset means on. It never deletes mirror data, and
500
- re-enabling reconciles every ref. **Give every process the same value** — a process cannot drop triggers for models
501
- it does not mount, so a mixed fleet leaves stale triggers behind.
502
- - The tokenizer is `AKAN_SEARCH_TOKENIZER` (or `database.search.tokenizer`, which wins), defaulting to
503
- `unicode61 remove_diacritics 2`. Changing it rebuilds the index from the mirror on the next boot — the model
504
- tables are never re-read — so it is a safe knob, unlike a `text` role change, which re-reads every row. The
505
- rebuild takes no cross-process claim, so a fleet restarted at once repeats it in every process; stagger the
506
- restart when the mirror is large.
507
717
 
508
718
  ### Image & File Fields
509
719
 
510
720
  - **Do not declare `Upload` as a model field.** `Upload` is a signal-body-only primitive (see Service And Signal Conventions). Models reference the `File` model instead.
511
- - Declare an image/file field as a relation to `File`: `image: field(File).optional()` for one, `images: field([File])` for many (see `libs/shared/lib/user/user.constant.ts`, `libs/shared/lib/banner/banner.constant.ts`).
721
+ - Declare an image/file field as a relation to `File`: `image: field(File).optional()` for one, `images: field([File])` for many.
512
722
  - The store then auto-generates an `upload<Field>On<Model>(fileList)` action that calls the framework upload mutation and polls file status until it leaves `"uploading"` (`pkgs/akanjs/store/action.ts`).
513
- - Storage is wired through the `StorageAdaptor` DI role (default `BlobStorage`, `pkgs/akanjs/service/predefinedAdaptor/storage.adaptor.ts`); the reference implementation is the `file` lib (`libs/shared/lib/file/*`). Do not hand-roll data-URL fallbacks.
723
+ - Storage is wired through the `StorageAdaptor` DI role (default `BlobStorage`, `pkgs/akanjs/service/predefinedAdaptor/storage.adaptor.ts`); the reference implementation is the `file` module. Do not hand-roll data-URL fallbacks.
514
724
 
515
725
  ### Cascade Remove — the `cascade` option
516
726
 
727
+ Full contract — bulk planning, the auto-created index, cycle cutting, boot-time validation: `get_guideline` with
728
+ `queryRule`.
729
+
517
730
  **The value names the direction, and getting it wrong is a data loss.** The two actions can sit on the same field
518
731
  shape, so `cascade` never means "related" — it means one of exactly these:
519
732
 
520
733
  - `removeRef` — *when I am removed, remove what this field points at.* Declared on the relation the owner holds:
521
- `image: field(File, { cascade: "removeRef" })`, arrays included. Only a relation accepts it; a primitive, a bare
522
- `ID`, and a scalar each fail the class build, because none of them names a document to remove.
734
+ `image: field(File, { cascade: "removeRef" })`, arrays included. Only a relation accepts it.
523
735
  - `removeWith` — *when what this field points at is removed, remove me.* Declared on the child's own reference to
524
- its owner, so the owner never learns about its children and a lib model can be extended by an app's. Three forms:
525
- a relation (`agentSession: field(AgentSession, { cascade: "removeWith" })`), an id with `ref`
526
- (`field(ID, { ref: "agentSession", cascade: "removeWith" })`), or a polymorphic id with `refPath`
527
- (`field(ID, { refPath: "parentType", cascade: "removeWith" })`). An array, a Map, `ref` together with `refPath`,
528
- and a field naming no owner each fail the class build.
529
- - **A `refPath` must name an `enumOf` field.** A free-form owner type is unknowable at build time, so every model's
530
- removal would have to sweep the polymorphic table on the chance it is the owner. The enum names the candidates and
531
- the reverse index reaches only them.
532
- - **A cascade goes through the target's service, never its model** — unless it provably makes no difference. The
533
- service path is what runs the target's `_postRemove`, which is where a module puts the side effect the removal has
534
- to carry (`FileService._postRemove` deletes the stored blob there).
535
- - **Bulk is decided at boot, per target model, for both directions.** When the target has no `remove` schema hook, no
536
- `_pre`/`_postRemove` (its own or a lib's), no cascade of its own, and no children, one `removeManyByQuery` leaves
537
- exactly the rows the loop would, so the framework takes it. Adding a `_postRemove` to that model silently flips it
538
- back to one document at a time — the boot log (`info` summary, `verbose` per edge) is the only place that shows.
539
- - **The plan is sealed after every service is live**, so a `listenPost("remove")` registered in `onInit` still counts
540
- and a `removeRef` target the app never mounted fails the boot rather than the first removal. An unmounted
541
- `removeWith` owner fails the boot too; an unmounted `refPath` candidate only warns, since that list spans optional
542
- modules by design.
736
+ its owner, so the owner never learns about its children and a lib model can be extended by an app's. Three
737
+ forms: a relation, an id with `ref`, or a polymorphic id with `refPath`. An array, a Map, `ref` together with
738
+ `refPath`, and a field naming no owner each fail the class build.
739
+ - **A `refPath` must name an `enumOf` field** — a free-form owner type is unknowable at build time.
740
+ - **A cascade goes through the target's service, never its model** — that path is what runs the target's
741
+ `_postRemove`, where a module puts the side effect the removal has to carry.
543
742
  - **Nothing checks whether another document still references the same target.** `File` in particular is deduped by
544
- `origin`, so two parents can share one row; declaring `removeRef` says the field owns its target exclusively, and
545
- that judgement is the declaring model's to make.
546
- - Removal is soft (`removedAt`) but the storage delete a `_postRemove` performs is not — a cascade is not
743
+ `origin`, so two parents can share one row; `removeRef` claims the field owns its target exclusively.
744
+ - Removal is soft (`removedAt`), but the storage delete a `_postRemove` performs is not — a cascade is not
547
745
  restorable, and reviving the owner does not revive what went with it.
548
- - A `removeWith` declaration **auto-creates its index** (`{ removedAt, fk }`, or `{ removedAt, typeKey, fk }` when
549
- polymorphic). Every non-base field lives in the `_doc` JSON column, so the lookup would otherwise scan the table
550
- on every owner removal.
551
- - **Query-level removes fire no hooks and therefore no cascade.** `removeManyByQuery` / `updateManyByQuery`, the
552
- generated `remove<Filter>` / `update<Filter>`, and the facade's `removeById` / `updateById` stamp
553
- `removedAt` in one atomic UPDATE, so nothing downstream runs. Remove one document at a time when it cascades.
554
- - Cascades are **idempotent**: `removedAt IS NULL` is ANDed into every query-level write, so a retry after a partial
555
- failure re-stamps nothing. Cycles are cut by a visited set carried down the whole chain, with a depth cap of 16.
746
+ - **Query-level removes fire no hooks and therefore no cascade** — `removeManyByQuery` / `updateManyByQuery`, the
747
+ generated `remove<Filter>` / `update<Filter>`, and the facade's `removeById` / `updateById`. Remove one document
748
+ at a time when it cascades.
556
749
 
557
750
  ## Akan Page Routing (`apps/**/page/**`)
558
751
 
559
752
  - `apps/<app>/page` may contain route modules only. Do not add helper logic or component-only files there.
560
753
  - Route source files under `page/` must use `.tsx`. Do not add `logic.ts`, `.js`, or `.jsx` files under `page/`.
561
- - Route pages use `_index.tsx`; layouts use `_layout.tsx`; per-route UI overrides use `_overrides.tsx`.
754
+ - A route page is either `<routeName>.tsx` (serving `/routeName`) or a directory's `_index.tsx`; layouts use `_layout.tsx`; per-route UI overrides use `_overrides.tsx`.
562
755
  - Reserved `_*.tsx` route filenames are limited to `_index.tsx`, `_layout.tsx`, and `_overrides.tsx`; do not add files like `_Component.tsx` or `_helper.tsx`.
563
756
  - Page filenames must not start with an uppercase letter. Move helper components like `Component.tsx` to app `ui`, `common`, or `lib` instead.
564
757
  - Dynamic segments use `[id]`; route groups use directories like `(user)`, `(public)`, `(tab)`, or `(detail)`.
@@ -567,9 +760,9 @@ shape, so `cascade` never means "related" — it means one of exactly these:
567
760
  - Prefer `export default function Page` or `export default async function Page` for page components.
568
761
  - `libs/<lib>/page` follows the same rules and ships routes to apps that opt in with `syncPageLibs` in `akan.config.ts`: `true` takes every lib dep that has a `page` folder, an array takes the libs listed, `false` (the default) syncs nothing.
569
762
  - `akan sync` links those routes into `apps/<app>/page/(libs)/(<lib>)` — once per basePath when the app declares subRoutes. The folder is generated and gitignored; edit the lib source, never the link.
570
- - Both path segments are route groups, so a lib route mounts at its own path (`libs/shared/page/login/_index.tsx` serves `/login`). Two synced routes that resolve to the same pattern are a sync-time error.
763
+ - Both path segments are route groups, so a lib route mounts at its own path (`libs/<lib>/page/login/_index.tsx` serves `/login`). Two synced routes that resolve to the same pattern are a sync-time error.
571
764
  - `export const pageConfig = { devOnly: true }` keeps a route out of `akan build` while it keeps serving under `akan start` and keeps being typechecked. On a `_layout.tsx` it excludes every route under that directory too. Write it as a literal `true`/`false` — the build reads it off the source without evaluating the module.
572
- - Before changing route behavior, check `pkgs/akanjs/server/src/routeTree.tsx` and nearby routes for the expected pattern.
765
+ - Before changing route behavior, check `pkgs/akanjs/server/routeTreeBuilder.ts` and nearby routes for the expected pattern.
573
766
 
574
767
  ### Page Body Shape
575
768
 
@@ -599,17 +792,19 @@ export const pageConfig = { transition: "stack" } satisfies PageConfig;
599
792
 
600
793
  ## Akan Sync Conventions (`apps/**`, `libs/**`)
601
794
 
602
- - `apps/<appName>` root may only contain these files: `AGENTS.md`, `CLAUDE.md`, `akan.app.json`, `akan.config.ts`, `capacitor.config.ts`, `client.ts`, `main.ts`, `package.json`, `server.ts`, `tsconfig.json`.
795
+ - `apps/<appName>` root may only contain these files: `AGENTS.md`, `CLAUDE.md`, `akan.app.json`, `akan.config.ts`, `capacitor.config.ts`, `client.ts`, `main.ts`, `package.json`, `server.ts`, `tsconfig.json`, `tsconfig.tsbuildinfo`.
603
796
  - `apps/<appName>` root may only contain these folders: `.akan`, `android`, `common`, `env`, `ios`, `lib`, `mobile`, `page`, `plugin`, `private`, `public`, `script`, `secrets`, `srvkit`, `ui`, `webkit`.
604
- - That allowlist has one source — `pkgs/@akanjs/devkit/workspaceLayout.ts`. `akan sync` (error), `akan doctor`
797
+ - `libs/<libName>` root may only contain these files: `AGENTS.md`, `CLAUDE.md`, `README.md`, `akan.config.ts`, `akan.lib.json`, `client.ts`, `index.ts`, `package.json`, `server.ts`, `tsconfig.json`, `tsconfig.spec.json`, `tsconfig.tsbuildinfo`.
798
+ - `libs/<libName>` root may only contain these folders: `common`, `env`, `lib`, `page`, `plugin`, `private`, `public`, `srvkit`, `ui`, `webkit`. A library is never booted or packaged as an app, so the run and mobile entries an app carries (`main.ts`, `capacitor.config.ts`, `.akan`, `android`, `ios`, `mobile`, `script`, `secrets`) are rejected there.
799
+ - Both allowlists have one source — `pkgs/@akanjs/devkit/workspaceLayout.ts`. `akan sync` (error), `akan doctor`
605
800
  (diagnostic), and `akan quality scan` (warning) all read it, so add a new root entry there and mirror it into this
606
801
  list, never into one of the three call sites.
607
802
  - `akan sync` maintains a scoped agent guide per app/lib: `apps/<app>/AGENTS.md` / `libs/<lib>/AGENTS.md`. The
608
803
  section between the `akan:agent` markers (the `## Recipes In Scope` index) is generated — do not hand-edit it;
609
804
  content outside the markers is yours. `akan lint` fails when the generated section is stale.
610
805
  - The `plugin/` facet holds Akan plugin declarations; files use the `<name>.plugin.ts` convention (e.g. `pushNotification.plugin.ts`) and are re-exported from the generated `plugin/index.ts` barrel.
611
- - Do not add `apps/*/base`; place shared app utilities under `apps/*/common`.
612
- - `apps/*/lib` and `libs/*/lib` root files are limited to generated/support files: `cnst.ts`, `db.ts`, `dict.ts`, `option.ts`, `sig.ts`, `srv.ts`, `st.ts`, `useClient.ts`, `useServer.ts`.
806
+ - Do not add `apps/*/base` or `libs/*/base`; place shared utilities under that app or lib's own `common/`.
807
+ - `apps/*/lib` and `libs/*/lib` root files are limited to generated/support files: `cnst.ts`, `db.ts`, `dict.ts`, `option.ts`, `sig.ts`, `srv.ts`, `st.ts`, `useClient.ts`, `useServer.ts` — plus a `<model>.signal.test.ts` / `.spec.ts`, the one hand-written file that belongs there because the suite boots the whole barrel.
613
808
  - Domain module folders are `lib/<model>` for database modules, `lib/_<service>` for service modules, and `lib/__scalar/<scalar>` for scalar modules.
614
809
  - Database module UI files are limited to `<Model>.Template.tsx`, `<Model>.Unit.tsx`, `<Model>.Util.tsx`, `<Model>.View.tsx`, and `<Model>.Zone.tsx`.
615
810
  - Service module UI files are limited to `<Service>.Util.tsx` and `<Service>.Zone.tsx`.
@@ -630,6 +825,7 @@ export const pageConfig = { transition: "stack" } satisfies PageConfig;
630
825
  | `plugin/` | build- or CLI-time `AkanPlugin` | `<name>.plugin.ts`, registered in `akan.config.ts` |
631
826
 
632
827
  - Hooks return a named object of async closures, never a tuple.
828
+ - `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.
633
829
  - 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.
634
830
  - `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.
635
831
 
@@ -645,7 +841,7 @@ when two shapes disagree.
645
841
  - Stale `// TODO: Implement …` comments above implemented methods.
646
842
  - `{cond && <X/>}` in JSX, hard-coded Korean bypassing `l()`, and `window.alert(...)` for user feedback.
647
843
  - Bare `/* eslint-disable */` blocks — use `// biome-ignore lint/<rule>: <why>`.
648
- - Raw palette grays such as `text-gray-400` instead of daisyUI semantic tokens.
844
+ - Raw palette grays such as `text-gray-400` instead of the semantic tokens (`text-foreground/70`).
649
845
 
650
846
  ## Secrets And Env Safety (`.env`, `infra/**`, `*secret*`, `*credential*`)
651
847
 
@@ -658,11 +854,24 @@ when two shapes disagree.
658
854
  ## Application Test Commands
659
855
 
660
856
  - After changing application source code, test the app with `bun run akan start <appName>`.
857
+ - **`akan start` takes several apps** — `akan start a,b`, `akan start a b`, `akan start all`, or no argument at
858
+ all for a checkbox that remembers the last pick.
859
+ - **It opens a full-screen view by default, at any app count.** A rail of apps with the processes inside each
860
+ one — `host` for the dev host, gateway and RSC worker, plus a row per replica the gateway forwards — and the
861
+ selected one's log beside it. `Tab` walks every row and `1`-`9` jump to that app (the rail shows each
862
+ app's digit), `↑↓` scroll (shift for a page, `G` back to following),
863
+ `/` greps, `e` shows stderr only, `c` clears, `o` opens the browser, `r` restarts that app, `q` quits.
864
+ **`--plain` prints prefixed interleaved lines instead**, and a pipe, a redirect or an unsized terminal
865
+ downgrades to that on its own.
866
+ - `--concurrency <n>` boots more than one app at a time; the default of 1 is what keeps two cold builder peaks
867
+ from overlapping. `--kill` frees the dev ports first — it resolves each port's listener, walks up to the top
868
+ of that akan dev tree and signals it, so another checkout's server or a stale orphan on the same port is
869
+ reclaimed. A holder that is not recognisably an akan process is reported and left alone.
661
870
  - Test production build generation with `bun run akan build <appName>`.
662
871
  - To test a built artifact locally, run it from the generated app directory with the required Akan runtime environment variables.
663
872
 
664
873
  ```bash
665
- cd dist/apps/akan && USE_AKANJS_PKGS=true AKAN_PUBLIC_REPO_NAME=akanjs AKAN_PUBLIC_SERVE_DOMAIN="akanjs.com" AKAN_PUBLIC_APP_NAME=akan AKAN_PUBLIC_ENV=local AKAN_PUBLIC_OPERATION_MODE=local SERVER_MODE=federation AKAN_PUBLIC_BASE_PATHS=akanjs,soft,office bun main.js
874
+ cd dist/apps/<appName> && USE_AKANJS_PKGS=true AKAN_PUBLIC_REPO_NAME=<repo> AKAN_PUBLIC_SERVE_DOMAIN="<domain>" \
875
+ AKAN_PUBLIC_APP_NAME=<appName> AKAN_PUBLIC_ENV=local AKAN_PUBLIC_OPERATION_MODE=local SERVER_MODE=federation \
876
+ AKAN_PUBLIC_BASE_PATHS=<basePaths> bun main.js
666
877
  ```
667
-
668
- - Adjust `<appName>`, `AKAN_PUBLIC_APP_NAME`, and `AKAN_PUBLIC_BASE_PATHS` to match the app being tested.