@akanjs/cli 3.0.0-beta.3 → 3.0.0-beta.31

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 (194) hide show
  1. package/.build-stamp +1 -1
  2. package/CodeAgent-tbdpdjat.js +9 -0
  3. package/CodeAgentRpcHost-p7h6n2hp.js +7 -0
  4. package/CodeAgentRpcListener-7p7z5dd7.js +97 -0
  5. package/CodeTui-my2x5fah.js +1457 -0
  6. package/README.md +3 -0
  7. package/TunnelShare-cwh9ezsm.js +9 -0
  8. package/{agent.command-d2gfxfck.js → agent.command-vz60x05c.js} +8 -9
  9. package/akanCode.js +102 -0
  10. package/application.command-pxqfjrqm.js +241 -0
  11. package/{applicationBuildRunner-kxwbssgn.js → applicationBuildRunner-shnfyfdy.js} +121 -77
  12. package/buildBatch.proc.js +120 -32
  13. package/cloud.command-9v9tpgps.js +95 -0
  14. package/cloud.script-baqdthnz.js +20 -0
  15. package/code.command-13h14j25.js +154 -0
  16. package/commandManifest.json +1 -1
  17. package/consoleToStderr-h5bryj94.js +7 -0
  18. package/context.command-x1hbasm5.js +48 -0
  19. package/context.runner-kqbjjea1.js +21 -0
  20. package/{dependencyScanner-shs82ynp.js → dependencyScanner-06z2dmrr.js} +2 -2
  21. package/{devTui-40ggmd99.js → devTui-q7k7bakd.js} +122 -40
  22. package/fileSys-wzzrntkw.js +9 -0
  23. package/guideline.command-vfk11hpj.js +42 -0
  24. package/guidelines/agentRule/agentRule.instruction.md +209 -33
  25. package/guidelines/conventions/conventions.instruction.md +144 -41
  26. package/guidelines/fieldRule/fieldRule.instruction.md +1 -0
  27. package/guidelines/mcpRule/mcpRule.instruction.md +7 -5
  28. package/guidelines/queryRule/queryRule.instruction.md +21 -7
  29. package/guidelines/runtimeRule/runtimeRule.instruction.md +271 -6
  30. package/guidelines/ssrRule/ssrRule.instruction.md +4 -2
  31. package/guidelines/transportRule/transportRule.instruction.md +51 -0
  32. package/guidelines/workspaceOnboarding/workspaceOnboarding.instruction.md +20 -0
  33. package/guidelines/workspaceRecipes/workspaceRecipes.instruction.md +66 -3
  34. package/incrementalBuilder.proc.js +564 -109
  35. package/{index-mak3cd28.js → index-1skctzbq.js} +137 -255
  36. package/{index-kcxmv48c.js → index-2ahj896a.js} +45 -79
  37. package/index-2wc41k5w.js +49 -0
  38. package/index-3hx1z96c.js +87 -0
  39. package/{index-hwkygn9s.js → index-48k96p55.js} +325 -36
  40. package/{index-q0y2jq9j.js → index-52w0fvh8.js} +21 -4
  41. package/index-7md2g9vw.js +6 -0
  42. package/index-7yf7rnjg.js +53 -0
  43. package/{index-gyd220p2.js → index-84r80zdx.js} +554 -577
  44. package/{index-zry5nz81.js → index-8psse8dw.js} +51 -30
  45. package/index-947t6zm4.js +1119 -0
  46. package/index-94s080qm.js +266 -0
  47. package/index-9dp031by.js +12 -0
  48. package/index-9s5zga3q.js +4300 -0
  49. package/{index-ws5e0j51.js → index-arnkqb6f.js} +43 -26
  50. package/{index-3d9y066c.js → index-bjwxs904.js} +9 -8
  51. package/index-c6eggbee.js +123 -0
  52. package/{index-4edzf1gr.js → index-cxafpw23.js} +77 -150
  53. package/index-d2242ksy.js +99 -0
  54. package/index-d5yn19ef.js +700 -0
  55. package/{index-92zztqy1.js → index-dy0jx9ay.js} +2 -17
  56. package/{index-2tv3fdx4.js → index-e52z7psd.js} +1212 -1458
  57. package/{index-a64esmtb.js → index-ew8dna9s.js} +523 -1022
  58. package/{index-7737gxcp.js → index-f50dy7yq.js} +107 -241
  59. package/index-f8q701fb.js +1003 -0
  60. package/{index-by5gpp8x.js → index-hj6w4ka1.js} +2 -1
  61. package/{index-j9yhg404.js → index-ksq4t16z.js} +4 -7
  62. package/index-nhhrjvk1.js +187 -0
  63. package/index-p6pbz9n5.js +25 -0
  64. package/index-ptshj7y1.js +2729 -0
  65. package/{index-fz5q3gd7.js → index-q9q7x4zj.js} +10 -17
  66. package/{index-pnqbkf5p.js → index-qk9vv9hm.js} +6 -2
  67. package/{index-600zhnve.js → index-rqxa9r98.js} +75 -203
  68. package/{index-ne2qy0nk.js → index-smbecayj.js} +12 -12
  69. package/{index-ktb5njgy.js → index-vcdxpmsx.js} +49 -99
  70. package/index-wa5jbxj1.js +760 -0
  71. package/index-ymnqrsxm.js +69 -0
  72. package/{index-v0qh106m.js → index-zc90rh75.js} +961 -892
  73. package/{index-259dsnz3.js → index-zvr7wfyg.js} +5 -8
  74. package/{index-h7yw2z9n.js → index-zzvvc845.js} +8 -14
  75. package/index.js +23 -22
  76. package/{library.command-nbsky672.js → library.command-m52jqc0s.js} +4 -5
  77. package/{localRegistry.command-yeq57wjr.js → localRegistry.command-x5vs1z43.js} +23 -42
  78. package/{module.command-29vqzrh8.js → module.command-ty3k6rwd.js} +8 -11
  79. package/{package.command-axv0abmn.js → package.command-hphdh4g4.js} +6 -7
  80. package/package.json +15 -7
  81. package/{page.command-vs9s3zrh.js → page.command-5aq8mzvq.js} +4 -5
  82. package/{primitive.command-cbxmqbtc.js → primitive.command-adwj84kx.js} +9 -22
  83. package/prompter-76k56b37.js +8 -0
  84. package/{quality.command-gmbs7nws.js → quality.command-y10kea2f.js} +205 -377
  85. package/{recipeScanner-fpagtgc1.js → recipeScanner-4b69ybe0.js} +1 -1
  86. package/{repair.command-bqsjptsj.js → repair.command-v7mwqvjh.js} +11 -35
  87. package/{routeSourceValidator-apyqw7wk.js → routeSourceValidator-7ycnfz0e.js} +0 -6
  88. package/{scalar.command-6k1vhe9s.js → scalar.command-pg3gneer.js} +7 -10
  89. package/skills/akan-code/SKILL.md +130 -0
  90. package/skills/akan-debug/SKILL.md +42 -0
  91. package/skills/akan-deps/SKILL.md +50 -0
  92. package/skills/akan-err/SKILL.md +39 -0
  93. package/skills/akan-module/SKILL.md +45 -0
  94. package/skills/akan-scaffold/SKILL.md +76 -0
  95. package/skills/akan-scalar/SKILL.md +52 -0
  96. package/skills/akan-service/SKILL.md +34 -0
  97. package/skills/akan-store/SKILL.md +67 -0
  98. package/skills/akan-ui/SKILL.md +63 -0
  99. package/skills/akan-validate/SKILL.md +44 -0
  100. package/{subspace.command-fwjfcts7.js → subspace.command-ejdmvbq7.js} +138 -67
  101. package/templates/__scalar/__model__/__model__.abstract.md.template +8 -0
  102. package/templates/appSample/lib/__scalar/workHistory/workHistory.abstract.ts +8 -39
  103. package/templates/appSample/lib/_noti/noti.abstract.ts +6 -29
  104. package/templates/appSample/lib/task/task.abstract.ts +12 -32
  105. package/templates/appSample/lib/task/task.signal.spec.ts +15 -18
  106. package/templates/appSample/lib/task/task.signal.test.ts +15 -1
  107. package/templates/appSample/lib/task/task.signal.ts +1 -1
  108. package/templates/appSample/srvkit/{AuthGuard.ts → guards.ts} +7 -4
  109. package/templates/appSample/srvkit/{SessionInternalArg.ts → internalArgs.ts} +6 -4
  110. package/templates/client.ts +7 -30
  111. package/templates/crudPages/[__model__Id]/edit/page.tsx +2 -1
  112. package/templates/crudPages/[__model__Id]/page.tsx +2 -1
  113. package/templates/crudPages/new/page.tsx +2 -1
  114. package/templates/crudPages/page.tsx +2 -1
  115. package/templates/crudSinglePage/page.tsx +2 -1
  116. package/templates/lib/__lib/lib.dictionary.ts +0 -2
  117. package/templates/lib/__lib/lib.document.ts +0 -2
  118. package/templates/lib/sig.ts +8 -30
  119. package/templates/localDev/docker-compose.yaml.template +3 -14
  120. package/templates/module/__model__.signal.ts +12 -4
  121. package/templates/pkgRoot/tsconfig.json.template +2 -7
  122. package/templates/server.ts +1 -3
  123. package/templates/service/__model__.abstract.md.template +8 -0
  124. package/templates/workspaceRoot/.gitignore.template +0 -1
  125. package/templates/workspaceRoot/package.json.template +1 -3
  126. package/tunnel.command-rz9qf230.js +129 -0
  127. package/{typeChecker-3xgrzc2v.js → typeChecker-px49mezc.js} +1 -1
  128. package/typecheck.proc.js +1 -1
  129. package/workflow.command-6r809r2z.js +79 -0
  130. package/{workspace.command-8s050m0h.js → workspace.command-8g3cw7jd.js} +77 -109
  131. package/abstractCompactor-mjh5ppsq.js +0 -101
  132. package/application.command-pj2m7584.js +0 -204
  133. package/applicationReleasePackager-4yasxwv9.js +0 -244
  134. package/capacitorApp-36rbr858.js +0 -12
  135. package/cloud.command-jshwdq22.js +0 -93
  136. package/context.command-ez8a0zyz.js +0 -80
  137. package/devTuiView-c08ccbat.js +0 -9
  138. package/getDirname-dr3jz1xg.js +0 -7
  139. package/guideline.command-xmqwx8zm.js +0 -359
  140. package/guidelines/agentRule/agentRule.generate.json +0 -31
  141. package/guidelines/componentRule/componentRule.generate.json +0 -31
  142. package/guidelines/conventions/conventions.generate.json +0 -25
  143. package/guidelines/cssRule/cssRule.generate.json +0 -31
  144. package/guidelines/docPageRule/docPageRule.generate.json +0 -31
  145. package/guidelines/docPageRule/docPageRule.instruction.md +0 -25
  146. package/guidelines/docSyncRule/docSyncRule.generate.json +0 -31
  147. package/guidelines/docSyncRule/docSyncRule.instruction.md +0 -25
  148. package/guidelines/enumConstant/enumConstant.generate.json +0 -31
  149. package/guidelines/enumConstant/enumConstant.instruction.md +0 -28
  150. package/guidelines/fieldRule/fieldRule.generate.json +0 -31
  151. package/guidelines/framework/framework.generate.json +0 -32
  152. package/guidelines/mcpRule/mcpRule.generate.json +0 -35
  153. package/guidelines/modelConstant/modelConstant.generate.json +0 -32
  154. package/guidelines/modelDictionary/modelDictionary.generate.json +0 -32
  155. package/guidelines/modelDocument/modelDocument.generate.json +0 -31
  156. package/guidelines/modelService/modelService.generate.json +0 -31
  157. package/guidelines/modelSignal/modelSignal.generate.json +0 -31
  158. package/guidelines/modelStore/modelStore.generate.json +0 -31
  159. package/guidelines/modelTemplate/modelTemplate.generate.json +0 -31
  160. package/guidelines/modelUnit/modelUnit.generate.json +0 -31
  161. package/guidelines/modelUtil/modelUtil.generate.json +0 -31
  162. package/guidelines/modelView/modelView.generate.json +0 -31
  163. package/guidelines/modelZone/modelZone.generate.json +0 -31
  164. package/guidelines/moduleCodegen/moduleCodegen.generate.json +0 -31
  165. package/guidelines/moduleCodegen/moduleCodegen.instruction.md +0 -28
  166. package/guidelines/moduleOverview/moduleOverview.generate.json +0 -31
  167. package/guidelines/queryRule/queryRule.generate.json +0 -31
  168. package/guidelines/recipeRule/recipeRule.generate.json +0 -31
  169. package/guidelines/runtimeRule/runtimeRule.generate.json +0 -25
  170. package/guidelines/scalarConstant/scalarConstant.generate.json +0 -32
  171. package/guidelines/scalarDictionary/scalarDictionary.generate.json +0 -32
  172. package/guidelines/scalarModule/scalarModule.generate.json +0 -31
  173. package/guidelines/sharedUiUsage/sharedUiUsage.generate.json +0 -31
  174. package/guidelines/sharedUiUsage/sharedUiUsage.instruction.md +0 -25
  175. package/guidelines/ssrRule/ssrRule.generate.json +0 -31
  176. package/guidelines/transportRule/transportRule.generate.json +0 -31
  177. package/guidelines/utilUiUsage/utilUiUsage.generate.json +0 -31
  178. package/guidelines/utilUiUsage/utilUiUsage.instruction.md +0 -25
  179. package/guidelines/workspaceOnboarding/workspaceOnboarding.generate.json +0 -24
  180. package/guidelines/workspaceRecipes/workspaceRecipes.generate.json +0 -33
  181. package/index-2hq52jq3.js +0 -465
  182. package/index-47pj3aee.js +0 -76
  183. package/index-6wwzs1bc.js +0 -7
  184. package/index-8pm30m73.js +0 -841
  185. package/index-kn1h7p1x.js +0 -560
  186. package/index-meh558jy.js +0 -65
  187. package/index-rwg2aa4f.js +0 -229
  188. package/index-w8kmx1b6.js +0 -2176
  189. package/index-wt7qrtd4.js +0 -1516
  190. package/templates/__scalar/__model__/__model__.abstract.md +0 -28
  191. package/templates/module/__model__.abstract.md +0 -28
  192. package/templates/service/__model__.abstract.md +0 -28
  193. package/templates/workspaceRoot/patches/react-dom@19.2.7.patch.template +0 -62
  194. package/workflow.command-nwpcdc8p.js +0 -107
@@ -28,7 +28,7 @@ you fetch on demand — `get_guideline` with the name, or `akan guideline show <
28
28
  | name | covers |
29
29
  |---|---|
30
30
  | `ssrRule` | server-share targets, the `akan.ssr.*` warnings, the client-boundary playbook |
31
- | `runtimeRule` | `web` / `csr` surfaces, gateway vs solo processes, logging, the generated image, shipped assets |
31
+ | `runtimeRule` | `web` / `csr` surfaces, gateway vs solo processes, logging, the generated image, native apps and a desktop app's server, shipped assets, database modes |
32
32
  | `queryRule` | slices and hydration, the generated filter methods, full-text search, cascade removal |
33
33
  | `transportRule` | guards across HTTP and websocket, socket identity and cleanup, binary pubsub, mutation verbs |
34
34
  | `mcpRule` | MCP configuration, wire behaviour, resource URIs, OAuth metadata, protocol revisions |
@@ -57,8 +57,9 @@ back.
57
57
  `no-inline-color`, `no-interpolated-arbitrary-class`)
58
58
  - **Never `throw new Error`.** Throw `new Err("<module>.error.<key>")` and register the key as `[en, ko]` in that
59
59
  module's dictionary `.error({})`. Import `Err` from `"../dict"` on the server and from `"@libs/<lib>/client"` or
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.
60
+ `"@apps/<app>/client"` in UI. `no-throw-raw-error.grit` exempts tests, `*.constant.ts`, `common/**`, `env/**`, and
61
+ a root `native/` folder — `common/` and `env/` have no legal `Err` import path, so keep throwing code out of them,
62
+ and a native plugin throws `AkanNativeError`, the error its bridge carries back to the page.
62
63
  - **Never import a third-party package** from `page/**`, from any barrel, or from any
63
64
  `*.{constant,dictionary,document,service,signal,store}.ts` / `*.{Template,Unit,Util,View,Zone}.tsx`
64
65
  (`no-import-external-library.grit`). Re-export the symbol through a lib first. One-line re-export shims in a lib's
@@ -76,6 +77,19 @@ back.
76
77
  `*.constant.ts`, `*.store.ts`, and the five module component suffixes (`no-bang-comment-in-client.grit`). Bun
77
78
  classifies `//!` and `/*!` as legal comments and keeps them through minification, so the note ships to every
78
79
  visitor. Use `// FIXME:` there; `//!` stays legal in server, `srvkit/`, and CLI files.
80
+ - **Never touch `document.cookie`, `localStorage` or `sessionStorage` in app or lib code** (`no-document-cookie.grit`,
81
+ `no-web-storage.grit`). A native shell serves the page from `app://localhost` on iOS, macOS and Linux, which keeps
82
+ no cookies: `document.cookie` reads `""` and a write is dropped, so the value exists in the browser and vanishes in
83
+ the app. Web Storage does work in the shells, but it bypasses the store akanjs picks per platform (the shell's
84
+ Preferences in an app) and throws during SSR. Use `getCookie` / `setCookie` / `removeCookie`, `storage`, and
85
+ `secretStorage` for tokens and credentials, all from `akanjs/client`.
86
+ - **Browser APIs an app WebView lacks live in `webkit/`** (`no-web-only-api-outside-webkit.grit`, a warning):
87
+ `navigator.share` / `canShare` (Android's WebView has none), `navigator.serviceWorker` and
88
+ `Notification.requestPermission` / `.permission` (no app page has them), `navigator.geolocation` (use
89
+ `useGeoLocation` from `akanjs/webkit`) and `navigator.vibrate` (iOS has none; use `haptics`). They are legal on the
90
+ web, so the rule does not ban them: it keeps them in a `webkit/` hook that branches on `isNativeApp()` from
91
+ `akanjs/client/native`, which is where the app side goes too. A root `native/` folder is out of its scope: a
92
+ plugin's web part is where those APIs belong.
79
93
  - **Never return a value from a store action** (`no-return-in-store-action.grit`). Every method of a `store(...)`
80
94
  class dispatches through `st.do.<action>()`, typed `void` / `Promise<void>`, so the value is unreachable — write
81
95
  it into state with `this.set({ ... })`. A bare `return;` guard, a `return` inside a nested callback, a getter,
@@ -216,6 +230,8 @@ Do not narrate code. Do document the thing the code cannot say. Both halves are
216
230
  ## Client / Server Boundaries (`apps/**`, `libs/**`, `pkgs/akanjs/**`)
217
231
 
218
232
  - Use `"use client";` at the top of client component files.
233
+ - A `"use client"` file re-exports by name (`export { A, B } from "./x"`), never with `export *`: the server knows a
234
+ client module only by the export names it spells out, so `akan start` and `akan build` stop on a star re-export.
219
235
  - Be careful when importing client-only code from page or layout modules.
220
236
  - Keep page props serializable unless the existing route pattern clearly allows otherwise.
221
237
  - In domain UI the boundary is mechanical, not a judgment call: `Template`, `Zone`, and `Util` are always client components with `"use client"` on line 1; `Unit` and `View` are always server components and never carry the directive.
@@ -273,8 +289,10 @@ are, because those are the ones the server could have done.
273
289
  SEO snapshots, prerendering and pre-hydration E2E read, so await what the page needs immediately and stream
274
290
  the rest. `xListInY` / `xInsightInY` hold hydrated model instances that React Flight refuses as client props:
275
291
  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.
292
+ 6. **Use named `ReactNode` slots, not just `children`** — a client shell that takes one slot per region lets a
293
+ server component supply each of them, instead of absorbing the whole subtree into the bundle. `Layout.Navbar`
294
+ declares `title` / `left` / `right` alongside `back` and `children`, but renders only the last two today, so
295
+ copy the shape rather than the component.
278
296
  7. **Let the server do the derived work.** Display and predicate logic belongs on `Light<Model>`; enum→class
279
297
  lookups belong in a module-scope `as const` map.
280
298
  8. **Gate auth on the server** — `getSelf({ unauthorize: "/signin" })` in `_layout.tsx`, before any HTML is sent.
@@ -291,11 +309,11 @@ What an app serves, how many processes it runs, where its logs go, and what ship
291
309
  declared in `akan.config.ts` and narrowed — never widened — by an env at boot. Domain code reaches none of it.
292
310
  Full contract: `get_guideline` with `runtimeRule`, or `akan guideline show runtimeRule`.
293
311
 
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.
312
+ - **Web surfaces are `web: true | false | { csr: boolean }`.** `{ csr: false }` drops the CSR bundle the native
313
+ apps ship, `false` is an API-only build that serves nothing under `page/`. There is no CSR-without-SSR option, by
314
+ type — the CSR bundle inlines the stylesheet the SSR base artifact compiles. `AKAN_SSR` / `AKAN_CSR` narrow
315
+ further at runtime and can never switch a surface the build left out back on. `akan start` ignores `web` and
316
+ keeps the whole dev surface.
299
317
  - **One traffic replica runs in the container's only process.** `AKAN_REPLICA=0,0,1` — the default everywhere —
300
318
  has nothing to balance, so there is no gateway and no unix-socket proxy hop. Two or more replicas, a batch-only
301
319
  replica, `AKAN_SOLO=false`, passing `replica` to `new AkanApp(...)`, or `akan start` all bring the gateway back.
@@ -304,7 +322,7 @@ Full contract: `get_guideline` with `runtimeRule`, or `akan guideline show runti
304
322
  rotating log file; nothing supervises it but the orchestrator's probes.
305
323
  - **Route prefixes move together or not at all.** `new AkanApp({ prefix, websocketPrefix })` in `main.ts` moves
306
324
  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
325
+ `fetchClient` in every tab the server renders. A prebuilt CSR shell or a native app bundle is never rendered by
308
326
  a server, so `api: { prefix, websocketPrefix }` in `akan.config.ts` is what those follow; declare it there too
309
327
  when the app ships either. Read the value with `getApiPrefix()` / `getWsPrefix()` from `akanjs/base` — never
310
328
  write `/api` or `/ws` as a literal, and never try to hand it down as a React prop: `FetchClient` fixes its
@@ -332,9 +350,40 @@ Full contract: `get_guideline` with `runtimeRule`, or `akan guideline show runti
332
350
  assembles one from; the string form takes no contributions. The generated image installs `ca-certificates` and
333
351
  `tzdata` and nothing else, so an app needing `ffmpeg` or Chromium declares it in `preRuns` / `postRuns`. A lib
334
352
  declares the steps its own runtime needs and every mounting app inherits them.
353
+ - **Native apps are the `native` section** — iOS, Android and a desktop app (macOS, Windows, Linux), each shipping
354
+ the CSR bundle on the native runtime. Shared fields (`appName`, `appId`, `version`, `indexPath`, `permissions`,
355
+ `plugins`, `deepLinks`, `updates`, …) sit beside one section per platform: `ios`, `android`, `desktop`, and a
356
+ platform section's own `indexPath` wins on that platform (`desktop: { indexPath: "/" }`). Without
357
+ `targets` the app has one target, `default`, and an app with no basePaths leaves `basePath` out; a target takes
358
+ the same shape and overrides the section field by field — objects merge key by key, a list or any other value
359
+ replaces the section's. `akan start-ios` / `start-android` / `start-desktop` run a dev build that loads its pages
360
+ from `akan start`; `build-ios` / `build-android` / `build-desktop` write
361
+ `apps/<app>/.akan/native/<target>/build/<platform>`, a desktop app only for the computer that builds it
362
+ (`--installer true` adds a Windows setup). With `updates: { url, publicKey }` an installed app takes the signed
363
+ releases `akan publish-update` makes; `akan update-keygen` makes the key and prints its `publicKey`.
364
+ - **A desktop app that carries its server (`desktop: { server: true }` in `native`, or in one target) gets nothing
365
+ from `docker`.** That server is API-only, runs in database mode `single`, and listens on a loopback port any
366
+ program on the computer can call, so guard its endpoints as a network server's. An executable the app spawns
367
+ goes in `bin` — per platform, a download checked against its `sha256` or a file beside the config, put first on
368
+ the app's PATH so the server's `spawn("ffmpeg")` finds it, and in `ctx.binDir` for a native plugin — and a package
369
+ that builds itself at install goes in `trustedDependencies`, which the image honours too. Carry a static LGPL
370
+ ffmpeg: a `--enable-nonfree` build may not be redistributed. A file the user picks with
371
+ `filePicker.pickFiles({ forServer: true })` reaches it as a grant, never a copy or a path:
372
+ `NativeFile.resolve(grant, "read")` in `akanjs/server` asks the shell for it. What it carries is readable on the
373
+ user's computer — `private/` (each lib's too, under `private/libs/<lib>`), the one `env.server.<env>.ts` of its
374
+ `--env` and its libs' server env defaults (`env.server.testing.ts`) — and it has no `public/`: read runtime files
375
+ from `AKAN_APP_DIR`, never `process.cwd()`.
335
376
  - **`assets: { pruneFonts, keepFonts }`** trims from the `dist` copy of `public/` the fonts nothing reads; source
336
377
  trees are never touched. A font with `optimize` on is a build input, not a runtime asset. `keepFonts` belongs to
337
378
  the `akan.config.ts` that owns the font, written against that scope's own `public/`.
379
+ - **Database modes are `database: { modes: [...] }`** — `single` (SQLite + Solid), `multiple` (one SQLite file on
380
+ a host volume + Redis), `cluster` (Postgres + Redis). The build carries the drivers of every declared mode and a
381
+ deployment picks one with `AKAN_DATABASE_MODE` — a declared one, named whenever the build declares several.
382
+ Where the data lives (`POSTGRES_URL`, `REDIS_URI`, `SQLITE_DATABASE_PATH`) is the deployment's env, ahead of
383
+ `env.server.ts`. `q.raw` is written in the dialect's own SQL, so an app running in two modes avoids it.
384
+ - **Backups run through the image's own `main.js`**: `bun main.js ops snapshot` / `ops restore` (offline), and
385
+ `/_akan/ops/*` for a control plane, mounted only when `AKAN_OPS_PUBLIC_KEY` is set and authenticated by a short
386
+ EdDSA token that key verifies. `GET /_akan/app/info` is public. `operationMode` gates none of it.
338
387
 
339
388
  ## React Components And Styling (`**/*.tsx`)
340
389
 
@@ -357,7 +406,7 @@ Full contract: `get_guideline` with `runtimeRule`, or `akan guideline show runti
357
406
  nothing — they covered five control types, keyed on the translated label, and restored over server data.
358
407
  - Static class strings stay plain strings. Reach for `cn` only for a conditional or to merge an incoming `className`, and merge the caller last: `cn("base classes", cond && "extra", className)`. `cn` comes from `akanjs/client` (token-aware tailwind-merge) and is the only class-combining function — no `clsx` (removed), no raw `twMerge` imports, no object syntax (`{ x: cond }` → `cond && "x"`).
359
408
  - Multi-slot components take extra named props (`wrapperClassName`, `bodyClassName`), never a `classNames` object.
360
- - Hoist enum→class lookups to a module-scope `as const` map typed `{ [key in cnst.XStatus["value"]]: string }`. Do not use `Record<...>`. Escalate the map to `webkit/` when a second module needs it.
409
+ - Hoist enum→class lookups to a module-scope `as const` map typed `{ [key in cnst.XStatus["value"]]: string }`. Do not use `Record<...>`. When a second module needs it, the classes become a variant axis of a recipe in `ui/Recipe/`; only a map from the enum to another recipe's variant name (`BadgeVariants["variant"]`) moves to `webkit/`.
361
410
  - Use `<Link>` from `akanjs/ui` for internal navigation; `<a>` only for `mailto:` and external links.
362
411
 
363
412
  ## Naming And Language
@@ -421,8 +470,11 @@ stores need none, because state and CRUD actions are generated. The body is thre
421
470
  `this.setX(...)` → toast. The optimistic shape is mutate the client model, `void fetch.*`, then commit. Use
422
471
  `this.pick(...)` when the value must exist, `this.get()` when it may not, and `this.set({...})` to write. Mutate lists
423
472
  through the collection API (`this.set({ xList: xList.set(x).save() })`), not array spread. **An action returns
424
- nothing** (`no-return-in-store-action.grit`). **Never `import type { RootStore } from "../st"`** — it crashes
425
- `akan build` with a Bun SSR segfault.
473
+ nothing** (`no-return-in-store-action.grit`). **Reach another store through `this`:**
474
+ `import type { RootStore } from "../st"`, then `(this as unknown as RootStore).logout()` to call its action or
475
+ `(this as unknown as RootStore).set({ … })` / `.get()` to write or read its state. Every store is mixed into one root
476
+ at runtime, so the cast only tells the type what `this` already is. Keep it `import type` — `st.ts` imports every
477
+ store, so a value import is a cycle.
426
478
 
427
479
  **`<model>.dictionary.ts`** — fixed chain, with empty stages still written:
428
480
  `.of() → .model() → .insight() → .query() → .sort() → .enum() → .slice() → .endpoint() → .error() → .translate()`.
@@ -464,6 +516,25 @@ Full contract — credential handshake, room revalidation, socket cleanup scopin
464
516
  - **A cleanup registered with `ws.on("disconnect" | "unsubscribe", fn)` is scoped to the call that registered it**
465
517
  — a room for a `pubsub` subscribe, the socket for a `message` handler. Cleanup that must happen either way
466
518
  registers for both.
519
+ - **A call's deadline is the endpoint's `{ timeout: <ms> }`, and there is nowhere else to put one.** It bounds
520
+ both ends: the `Timeout` middleware — registered by default, and standing aside for every endpoint that
521
+ declares none — rejects the call with `base.error.gatewayTimeout`, and the same value is serialized to the
522
+ client as that call's request budget. Declared nowhere, a call takes the client's own default of **30
523
+ seconds**, so an endpoint whose real work runs longer (provisioning, a firmware flow, an external
524
+ orchestration) has to declare one or it dies mid-flight with a transport error that does not say whether the
525
+ server ever received it. A caller overrides it per call with `fetch.x(…, { timeout })` — `false` waits as long
526
+ as the runtime will — and an app moves its own default with `fetch.instance.setTimeout(ms)`. An upload is
527
+ never bounded. **Losing the race does not cancel the work**: the handler runs to completion with nobody
528
+ holding its result, so a deadline is an answer to the caller, not an undo.
529
+ - **A cached answer is the endpoint's `{ cache: <ms> }`, and only a `query` taking no internal argument may have
530
+ one.** It is a step of the call, not a middleware, and stands aside for every endpoint that declares nothing.
531
+ Internal arguments are how a call learns who is asking (`.with(Self)`), so an endpoint that has them answers
532
+ per caller and one shared entry would be one caller's answer handed to the next — that endpoint and every
533
+ `mutation` are named in the log and left uncached. The lookup runs after the guards, so a hit reaches only a
534
+ caller they admitted; what is cached is the handler's result, so `resolveReturn` still masks fields and resolves
535
+ relations per call; a cache backend that is down is warned about and the call runs uncached. The default
536
+ middleware chain is `Logging → Timeout → <lib middlewares>`, then guards → internal arguments → cache →
537
+ handler, and there is no `Retry` middleware.
467
538
 
468
539
  ### Authorization Defaults
469
540
 
@@ -530,8 +601,11 @@ Full contract — filter-arg `ref` pickers, `getQueryMeta` summary counters, `la
530
601
  first "more". `isCumulativeOf<Model>` is the mode.
531
602
  - **"Is there more" is `hasMoreOf<Model>`, never `count > page * limit`.** It comes from the length of the batch
532
603
  the server returned, so it survives `{ insight: false }` and cannot drift from a count a live event moved.
533
- - **Hydrated vs raw:** server queries return hydrated `cnst.<Model>` instances (with `set`/`save`/`refresh`);
534
- client fetch results are raw `GetStateObject` plain data, functions stripped.
604
+ - **Hydrated vs raw:** server queries return hydrated `cnst.<Model>` instances (with `set`/`save`/`refresh`),
605
+ and so do client fetch results — `FetchClient` parses every return with `crystalize` defaulting to **true**
606
+ (`pkgs/akanjs/fetch/client/fetchClient.ts`). The raw `GetStateObject` shape is what the `<ref>Obj` /
607
+ `<ref>ObjList` / `<ref>ObjInsight` handles carry, because those pass `crystalize: false` on purpose so the
608
+ value can cross the RSC boundary as a client prop.
535
609
  - Every filter generates fourteen methods: `list` · `listIds` · `find` · `findId` · `pick` · `pickId` · `exists` ·
536
610
  `count` · `insight` · `query` · **`remove`** · **`removeOne`** · **`update`** · **`updateOne`**. The last four
537
611
  are query-level writes — one atomic UPDATE, **no hooks**, and therefore no `_postRemove` and no cascade. Use
@@ -569,7 +643,7 @@ Full contract — relevance ordering, `columns` / `weights`: `get_guideline` wit
569
643
  - Injected dependencies resolve by field-name convention: a field named `<refName>Service` resolves to the service registered under `<refName>`, and `<refName>Signal` likewise (`pkgs/akanjs/service/injectInfo.ts`).
570
644
  - The `Service`/`Signal` suffix is required — the injector strips it to derive the registry lookup key. Name the field after the target refName plus the suffix, not arbitrarily.
571
645
  - Preference order inside a service: `service<srv.XService>()` for another module's service · `plug(AdapterClass)` or `plug(StorageAdaptorRole)` for an adapter · `use<T>()` only to reach an `option.ts`-registered legacy singleton · `env(...)` for config.
572
- - **`memory(...)` takes a scalar or model class, not only a primitive** — `memory(Map, { of: CallStateInput })` serializes through the constant and travels as JSON text, so the Redis and sqlite-backed caches round-trip the same declaration. Never hand-encode JSON into a `String` memory.
646
+ - **`memory(...)` takes a scalar or model class, not only a primitive** — `memory(Map, { of: CallStateInput })` serializes through the constant and travels as JSON text, so the Redis and sqlite-backed caches round-trip the same declaration. Never hand-encode JSON into a `String` memory. A memory is scoped to the service or adaptor that declares it; `ttl` (milliseconds) sets how long each write lives unless the `set` call names its own `expireAt`, and a `Map` entry expires on its own.
573
647
 
574
648
  ### Adapters — `adapt()` And `plug()`
575
649
 
@@ -644,10 +718,14 @@ it.
644
718
  (RFC 7591; Cursor's `cursor://` redirect admitted) plus the consent page `libs/shared/page/oauth/consent` — and
645
719
  names itself as the issuer, so `/mcp` demands a bearer token, verifies its signature, and refuses one carrying no
646
720
  `aud`. A token is the app's own access JWT plus `aud`/`iss`/`client_id`, so `AccountMiddleware` and the guards
647
- judge it unchanged; there is no scope. Per-app knobs live in `env.server.*` under `oauth` (`consentPath` and
648
- `signinPath` with the basePath, `clients`, `dynamicRegistration`, `allowedRedirectSchemes`, `clientIdMetadata`,
649
- `enabled`). `/mcp` reads the `Authorization` header only — a cookie is dropped at the door — and `JWT_SECRET` is
650
- mandatory outside `local`, because the derived fallback is forgeable from three public names. A grant is revoked
721
+ judge it unchanged. The tokens this built-in server mints carry no scope; `auth.scopes` / `AKAN_MCP_SCOPES` are for
722
+ tokens an external issuer mints, and declared beside this server they refuse every one of its tokens with 403
723
+ `insufficient_scope`. Per-app knobs live in `env.server.*` under `oauth` (`consentPath` and
724
+ `signinPath` with the basePath, `clients`, `dynamicRegistration`, `allowedRedirectSchemes`, `accessTokenSeconds`,
725
+ `clientIdMetadata`, `enabled`, plus `issuer` / `resource` — set those when a tunnel or an edge makes the derived
726
+ origin wrong, because an MCP client compares the issuer byte for byte). `/mcp` reads the `Authorization` header only — a cookie is dropped at the door — and `JWT_SECRET` (or
727
+ `security.jwtSecret`) is mandatory whenever `operationMode` is not `local`, because the derived fallback is
728
+ forgeable from three public names; `AKAN_ALLOW_DERIVED_JWT_SECRET=1` accepts that risk explicitly. A grant is revoked
651
729
  whole — `POST /oauth/revoke` (RFC 7009) by the client, `fetch.revokeOAuthConnection(sessionId)` by the account's
652
730
  owner, `fetch.listOAuthConnections()` to see them — and a revoked access token is refused at its next call.
653
731
  - **A person may, a model may not.** An agent's token names its `client_id` and `aud`; a browser session has
@@ -696,9 +774,12 @@ what every developer must know even when not building one.
696
774
  every tool runs in the caller's own browser session, gated by guards and the approval card. Its guard is
697
775
  `AgentRelayAccess`, which refuses every call until the app names a guard of its own —
698
776
  `option.setAgentAccess(SignedIn)`. **The LLM is configured in `option.ts`, never through the environment**
699
- (`option.setLlm({ apiKey, model, host })`). Three providers ship — `DeepseekLlm` (the default, text only),
700
- `OpenaiLlm` and `AnthropicLlm` (both read images) — and swapping one in is
701
- `option.applyAdaptor(LlmAdaptorRole, AnthropicLlm)`.
777
+ (`option.setLlm({ apiKey, model, host })`). Two adaptors ship, one per wire: `OpenaiLlm` is the default and
778
+ speaks the chat-completions dialect to whatever `host` names — OpenAI, DeepSeek, Groq, OpenRouter, Ollama — and
779
+ `AnthropicLlm` speaks the Messages API and reads a PDF as well as a picture. Both require `model`. A provider
780
+ neither covers is an `adapt()` class in `srvkit/` implementing `LlmAdaptor`, applied with
781
+ `option.applyAdaptor(LlmAdaptorRole, TheClass)`; whatever settings it needs beyond `LlmOption` ride `setLlm`
782
+ too, read back with `use<MyLlmOption>()`.
702
783
  - **Declare the tool beside the control that already does it**, never as a separate surface:
703
784
  `st.tool("x").desc("…").arg("id", ID).exec(fn)` returns the callable to hand to `onClick` — one handler for the
704
785
  person and the agent. Publish it only where the screen already renders the control; a falsy name declares the
@@ -727,6 +808,7 @@ what every developer must know even when not building one.
727
808
  - Put display and predicate logic on the `Light<Model>` class rather than in a util module — see Module File Playbook.
728
809
  - Defaults are a literal for scalars and a thunk for anything constructed. Arrays are `field([T])`; optional is the postfix `.optional()`.
729
810
  - **`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.
811
+ - **A `field.hidden` / `field.secret` value reads `null` on the client, and the type still declares it.** `SignalContext.resolveReturn` skips both field types while it builds an endpoint's response, and hydration writes `null` over the key rather than leaving it absent — the first branch of the loop in `constant/getDefault.ts`, ahead of the field's own default. So the value is a deliberate `null` behind a type promising a `string`, every use of it typechecks, and the failure lands wherever it is finally dereferenced instead of where it was read. **Guard with `??` or `== null`**: `=== undefined`, a destructuring default and an optional parameter default all catch only a missing key and sail straight past this one. A projection (`pickById(id, { secret: true })`) widens the *server's* read, never the response. If a screen needs the value, the field is neither hidden nor secret; if it only needs to be cheap for a model rather than unseen, that is `field.visual`.
730
812
 
731
813
  ### Scalar & Field Type Reference
732
814
 
@@ -760,7 +842,8 @@ Full contract — the trigger-maintained mirror, tokenizer changes, `AKAN_SEARCH
760
842
  backstop, including for a `text` field *underneath* one of those. Do not work around either.
761
843
  - The role works on a relation (`image: field(File, { text: "thumb" })`) and on an array; an array of objects
762
844
  indexes by leaf key. A field inside a `Map` indexes nothing — there is no fixed path to extract it from.
763
- - Search runs on sqlite/libsql only. `q.search()` against Postgres throws, loudly, rather than returning every row.
845
+ - Search runs in every database mode — fts5 on SQLite/libSQL, a weighted `tsvector` (`pg_trgm` for `trigram`) on
846
+ Postgres. The same text matches the same documents; only the order may differ on Postgres.
764
847
 
765
848
  ### Image & File Fields
766
849
 
@@ -809,10 +892,17 @@ shape, so `cascade` never means "related" — it means one of exactly these:
809
892
  - Dynamic segments use `[id]`; route groups use directories like `(user)`, `(public)`, `(tab)`, or `(detail)`.
810
893
  - **A route file exports one thing**: `export default page()…render(…)` for a page, `layout()…render(…)` for a
811
894
  `_layout.tsx`, `rootLayout()…render(…)` for the app's (or a basePath's) root `_layout.tsx`. Every route setting
812
- is a stage of the chain — `.param()`, `.search()`, `.config()`, `.head()` / `.metadata()`, `.loading()`;
813
- `.notFound()` / `.error()` on a layout; `.fonts()` / `.theme()` / `.manifest()` / `.reconnect()` / `.wsConnect()`
814
- / `.layoutStyle()` / `.gaTrackingId()` on the root layout; `.prompt()` on a page. A named export beside the chain,
815
- or `page()` in a `_layout.tsx`, fails the build.
895
+ is a stage of the chain — `.param()`, `.search()`, `.config()`, `.head()`, `.loading()`; `.notFound()` /
896
+ `.error()` on a layout; `.fonts()` / `.theme()` / `.manifest()` / `.reconnect()` / `.wsConnect()` /
897
+ `.layoutStyle()` on the root layout; `.prompt()` on a page. A named export beside the chain, or `page()` in a
898
+ `_layout.tsx`, fails the build. `.head()` takes JSX — `<title>`, `<meta>`, `<link>` — or a function of the
899
+ route's args returning it; there is no metadata object, and analytics is the app's own script, not a stage.
900
+ - **A CSR bundle (a native app, `?csr=true`) never shows a root layout's own markup.** Pages render into the
901
+ frame, and what the root layout draws around `{children}` stays in a hidden container, so its wrappers cannot
902
+ stack a second screen beside the frame. Its components still mount and their effects run (`<Auth.User />` works).
903
+ Anything a person must see from a root layout — an overlay, a gate, a banner — renders through
904
+ `createPortal(…, document.body)`, as `<Agent.Chat />` and the dialogs already do. A non-root `_layout.tsx` has no
905
+ such limit.
816
906
  - **A page names every `[x]` segment of its path with `.param("x", Type)`** and reads a query key only through
817
907
  `.search("k", Type)`. Values arrive typed — `ID`/`String` → string, `Int`/`Float` → number, `Boolean`, `Date` →
818
908
  Dayjs, an `enumOf` class → its union, `[T]` → array — a path value the type refuses answers not-found, and a
@@ -855,10 +945,10 @@ export default page()
855
945
 
856
946
  ## Akan Sync Conventions (`apps/**`, `libs/**`)
857
947
 
858
- - `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`.
859
- - `apps/<appName>` root may only contain these folders: `.akan`, `android`, `common`, `env`, `ios`, `lib`, `mobile`, `page`, `plugin`, `private`, `public`, `script`, `secrets`, `srvkit`, `ui`, `webkit`.
948
+ - `apps/<appName>` root may only contain these files: `AGENTS.md`, `CLAUDE.md`, `akan.app.json`, `akan.config.ts`, `client.ts`, `main.ts`, `package.json`, `server.ts`, `tsconfig.json`, `tsconfig.tsbuildinfo`.
949
+ - `apps/<appName>` root may only contain these folders: `.akan`, `common`, `env`, `lib`, `native`, `page`, `plugin`, `private`, `public`, `script`, `secrets`, `srvkit`, `ui`, `webkit`.
860
950
  - `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`.
861
- - `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.
951
+ - `libs/<libName>` root may only contain these folders: `common`, `env`, `lib`, `native`, `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`, `.akan`, `script`, `secrets`) are rejected there. A Capacitor-era `ios` / `android` / `mobile` folder or `capacitor.config.*` is refused in an app root too, named as a leftover: the native runtime generates its projects under `.akan/native/<target>`.
862
952
  - Both allowlists have one source — `pkgs/@akanjs/devkit/workspaceLayout.ts`. `akan sync` (error), `akan doctor`
863
953
  (diagnostic), and `akan quality scan` (warning) all read it, so add a new root entry there and mirror it into this
864
954
  list, never into one of the three call sites.
@@ -882,12 +972,18 @@ export default page()
882
972
  | Folder | Admission test | Naming |
883
973
  |---|---|---|
884
974
  | `common/` | pure, isomorphic, zero-dependency; may import only sibling `common/*` and `akanjs/base`. Cannot import `Err`, so keep throwing code out of it. | camelCase file, filename equals the single export |
885
- | `webkit/` | touches `window` / `navigator` / Capacitor, or is a React hook | `use<Thing>.tsx` — `.tsx` even with no JSX |
975
+ | `webkit/` | touches `window` / `navigator` / the native bridge (`akanjs/client/native`, or `akanjs/client/native/<plugin id>` for any builtin plugin), or is a React hook | `use<Thing>.tsx` — `.tsx` even with no JSX |
886
976
  | `srvkit/` | touches `node:*`, `Bun`, `process.env`, a secret, or a server SDK | camelCase file, PascalCase class |
887
- | `ui/` | renders JSX and is not bound to one model | PascalCase component, camelCase sidecar (`swipeCard.util.ts`) |
977
+ | `ui/` | renders JSX, or defines a look (a recipe in `Recipe/`, a lib's `tokens.css`), and is not bound to one model | PascalCase component, camelCase sidecar (`swipeCard.util.ts`), `Recipe/<name>.ts` |
888
978
  | `plugin/` | build- or CLI-time `AkanPlugin` | `<name>.plugin.ts`, registered in `akan.config.ts` |
979
+ | `native/` | a native plugin the app or lib owns: `native-plugin.json` beside its page API (`src/index.ts`, `definePlugin` from `akanjs/client/native`), its desktop part (`src/desktop.ts`, `defineDesktopPlugin` from `akanjs/native/desktop`), Kotlin and Swift. Every mobile and desktop target ships it; a lib's reach only the apps that depend on it, and the app's own wins an id | `native/<id>/`, the folder named after the plugin id |
889
980
 
890
981
  - Hooks return a named object of async closures, never a tuple.
982
+ - A `native/` plugin's page API is imported by a `webkit/` hook (`../native/<id>/src`) and nothing else; pages and
983
+ components call the hook, the same way they reach `akanjs/client/native`.
984
+ - `ui/` is the presentation layer — markup and the looks it is built from — so a recipe lives there although it is
985
+ neither a component nor a hook. Not `webkit/`: its hooks are `"use client"`, the opposite signal for a function
986
+ server components call. Not `common/`: it cannot import `akanjs/ui`, where `recipe` / `tv` come from.
891
987
  - `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.
892
988
  - 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.
893
989
  - `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.
@@ -900,12 +996,15 @@ when two shapes disagree.
900
996
  - `children: any` — newer files use `children: ReactNode`.
901
997
  - Hard-coded API keys or secrets in source; they belong in `option.ts` or env.
902
998
  - Large blocks of commented-out code left in place.
903
- - Cross-store writes through a `RootStore` cast — it collides with the Bun SSR-bundler segfault.
904
999
  - Stale `// TODO: Implement …` comments above implemented methods.
905
1000
  - `{cond && <X/>}` in JSX, hard-coded Korean bypassing `l()`, and `window.alert(...)` for user feedback.
906
1001
  - Bare `/* eslint-disable */` blocks — use `// biome-ignore lint/<rule>: <why>`.
907
1002
  - Raw palette grays such as `text-gray-400` instead of the semantic tokens (`text-foreground/70`).
908
1003
 
1004
+ A store calling `(this as unknown as RootStore).<action>()` or `.set({ … })` over
1005
+ `import type { RootStore } from "../st"` is **not** one of these — it is the sanctioned way to reach another store's
1006
+ actions and state (see `<model>.store.ts` above).
1007
+
909
1008
  ## Secrets And Env Safety (`.env`, `infra/**`, `*secret*`, `*credential*`)
910
1009
 
911
1010
  - Never print, summarize, commit, or expose real secret values, credentials, tokens, private keys, or `.env` contents.
@@ -924,7 +1023,9 @@ when two shapes disagree.
924
1023
  selected one's log beside it. `Tab` walks every row and `1`-`9` jump to that app (the rail shows each
925
1024
  app's digit), `↑↓` scroll (shift for a page, `G` back to following),
926
1025
  `/` greps, `e` shows stderr only, `c` clears, `y` copies, `Y` copies the log path, `o` opens the browser,
927
- `r` restarts that app, `q` quits.
1026
+ `r` restarts that app, `q` quits. Under `--share`, `s` copies the public URL — the selected app's, or every
1027
+ app's from the merged row — and the header carries it ahead of the local one, because the line `--share`
1028
+ printed before the view took the terminal is gone by its first repaint.
928
1029
  **`--plain` prints prefixed interleaved lines instead**, and a pipe, a redirect or an unsized terminal
929
1030
  downgrades to that on its own.
930
1031
  - **A supervised session writes `local/apps/<app>/runtime/dev.log`, and that path is how a log gets handed over.**
@@ -935,11 +1036,13 @@ when two shapes disagree.
935
1036
  other file. The previous session is kept beside it as `dev.prev.log`. Hand somebody — or an agent — the
936
1037
  path rather than a paste: it costs one line instead of a transcript, and it can be re-read after a fix.
937
1038
  In the view, `y` copies the lines the filters have already narrowed and `Y` copies the path.
938
- - **How many apps boot at once is the machine's answer, not a constant.** A cold boot build is the builder's
939
- RSS peak (~900MB per app), so the default wave is what memory and cores allow — half the memory budget
940
- divided by that peak, and one app per four cores — which boots a laptop's apps together and still staggers
941
- them inside a small container. The session says which in one line. `--concurrency <n>` overrides it, and
942
- `AKAN_MEMORY_LIMIT` lowers the budget it derives from.
1039
+ - **How many apps boot at once is the machine's answer, not a constant.** A cold boot is an app's RSS peak
1040
+ (~1.8GB per app, most of it the build worker — the base build, then the SSR registry's), so the default wave
1041
+ is what memory and cores allow — half the memory budget divided by that peak, and one app per four cores —
1042
+ which boots a laptop's apps together and still staggers them inside a small container. The session says
1043
+ which in one line. The next wave starts once an app serves and its boot builds have settled, or 30 seconds
1044
+ after it serves, while the app shows as ready — and `--open` opens it — as soon as it serves.
1045
+ `--concurrency <n>` overrides the wave size, and `AKAN_MEMORY_LIMIT` lowers the budget it derives from.
943
1046
  - `--kill` frees the dev ports first — it resolves each port's listener, walks up to the top of that akan dev
944
1047
  tree and signals it, so another checkout's server or a stale orphan on the same port is reclaimed. A holder
945
1048
  that is not recognisably an akan process is reported and left alone.
@@ -9,6 +9,7 @@ Use this whenever generating constants or scalar constants. It defines the curre
9
9
  - `field(Type).optional()` marks a nullable or optional business value when absence is valid.
10
10
  - `field.hidden(Type).optional()` is for internal state that should not be treated as a normal public field.
11
11
  - `field.secret(Type).optional()` is for sensitive values that should not be selected by default.
12
+ - Neither survives an endpoint response: both are stripped on the way out and hydration writes `null` over the key, while the generated type still declares the field — so a client holds a deliberate `null` behind a type that promises a value, and only `??` / `== null` catch it. A field a browser has to read is neither.
12
13
  - `field([Type])` defines arrays; provide empty array defaults when the app expects list operations.
13
14
 
14
15
  ## Current Akan Patterns
@@ -189,11 +189,13 @@ export default page()
189
189
  - OAuth resource metadata is published at `/.well-known/oauth-protected-resource` (and at that path plus the mount
190
190
  path, the spelling most clients try first). `auth.authorizationServers` / `AKAN_MCP_AUTH_SERVERS`,
191
191
  `auth.scopes` / `AKAN_MCP_SCOPES`, `auth.resource` / `AKAN_MCP_RESOURCE` and `auth.verify` configure it;
192
- `insufficient_scope` is enforced only once scopes are declared. Naming an issuer changes three things at once: a
193
- request with no credential is answered 401 from `initialize` on (an MCP client starts its OAuth flow on nothing
194
- else), a token carrying no `aud` is refused (that issuer mints for its other resources too), and the challenge
195
- carries no `error` code (RFC 6750 §3.1 reserves it for a credential that was presented). A server naming no
196
- issuer keeps anonymous access and accepts a first-party token, which is bound by app and environment instead.
192
+ `insufficient_scope` is enforced only once scopes are declared. Declare them only for tokens an external issuer
193
+ mints: the built-in authorization server's tokens carry no scope, so beside it every one of them would be refused
194
+ with 403. Naming an issuer changes three things at once: a request with no credential is answered 401 from
195
+ `initialize` on (an MCP client starts its OAuth flow on nothing else), a token carrying no `aud` is refused (that
196
+ issuer mints for its other resources too), and the challenge carries no `error` code (RFC 6750 §3.1 reserves it
197
+ for a credential that was presented). A server naming no issuer keeps anonymous access and accepts a first-party
198
+ token, which is bound by app and environment instead.
197
199
  - **The boot log names every published entry with no dictionary `.desc()`.** An agent picks a tool by its
198
200
  description, so a missing one is a broken tool. What the framework generates has no text of its own and borrows
199
201
  the model's: the generated list reads the `.of()` label, and the base CRUD tools append the model's `.desc()` to
@@ -31,6 +31,10 @@ with it. `conventions` carries the invariants — this is the full contract behi
31
31
  A model with none renders ids and says so in the picker.
32
32
  - A slice's `exec` returns a `QueryOf` (an opaque query descriptor, `pkgs/akanjs/constant/types.ts`); you **cannot** chain `.sort()`/`.limit()` on it.
33
33
  - Apply ordering/paging via the store `init` fetch option instead: `initX(..., { sort, page, limit })` (`pkgs/akanjs/fetch/fetchType/sliceFetch.type.ts`).
34
+ - **Every sort ends with `id`, in the direction of its last key, and a missing value sorts below every other** —
35
+ first ascending, last descending, in every database mode and in the client placing a live row. Rows the declared
36
+ keys tie on (a midpoint priority dragged into one gap until the float runs out, two rows saved in one millisecond)
37
+ therefore keep one order across `loadMoreOf<Model>()` pages and live insertions.
34
38
  - **A slice list is either one window or an accumulated one, and the store says which.** `setPageOf<Model>` swaps
35
39
  the window and `pageOf<Model>` names it; `loadMoreOf<Model>()` appends the rows *after* the ones already loaded
36
40
  and takes no page number, because in an accumulated list no page number names anything. It skips by
@@ -49,7 +53,7 @@ with it. `conventions` carries the invariants — this is the full contract behi
49
53
  `findBy<Filter>(...args, { select: { secretField: true } })` takes it inside the option object. The shapes do not
50
54
  swap: `{ select: … }` handed to the facade projects a field named `select`, which no model has, and the read comes
51
55
  back empty with no error. This is the only way to read a `field.secret(...)` value, which is otherwise stripped.
52
- - **Hydrated vs raw:** server queries return hydrated `cnst.<Model>` instances (with `set`/`save`/`refresh`); client fetch results are raw `GetStateObject` plain data (functions stripped, `pkgs/akanjs/base/types.ts`).
56
+ - **Hydrated vs raw:** server queries return hydrated `cnst.<Model>` instances (with `set`/`save`/`refresh`), and so do client fetch results — `FetchClient` parses every return with `crystalize` defaulting to **true** (`pkgs/akanjs/fetch/client/fetchClient.ts`). The raw `GetStateObject` shape (functions stripped, `pkgs/akanjs/base/types.ts`) is what the `<ref>Obj` / `<ref>ObjList` / `<ref>ObjInsight` handles carry, because those pass `crystalize: false` on purpose so the value can cross the RSC boundary as a client prop.
53
57
  - Every filter generates fourteen methods: `list` · `listIds` · `find` · `findId` · `pick` · `pickId` · `exists` ·
54
58
  `count` · `insight` · `query` · **`remove`** · **`removeOne`** · **`update`** · **`updateOne`**. The last four are
55
59
  query-level writes — one atomic UPDATE, **no hooks**, and therefore no `_postRemove` and no cascade. Use them on a
@@ -90,14 +94,15 @@ with it. `conventions` carries the invariants — this is the full contract behi
90
94
  - **A slice endpoint never reaches "unspecified".** The resolver fills `latest` before the query is built, so a
91
95
  client asking for the score order has to name `relevance`; leaving `sort` off gets `latest`, not relevance.
92
96
  - Scope a search with `columns` (`q.search(text, { columns: ["title"] })`) and re-weight with `weights`, a tuple of
93
- finite numbers positional over `["title", "desc", "tag", "filter"]`.
97
+ finite, non-negative numbers positional over `["title", "desc", "tag", "filter"]` — Postgres ranks by weight
98
+ class and has no reading for a negative one.
94
99
 
95
100
 
96
101
  ## Text Search Fields — the `text` role
97
102
 
98
103
  - A field joins the full-text index by declaring one of five roles: `field(String, { text: "title" })`, and likewise
99
104
  `"desc"`, `"tag"`, `"thumb"`, `"filter"`. Nothing else opts a field in, and there is no per-model switch.
100
- - Pick the role by what the value *is*, because `bm25` weights them positionally (`title` 10, `tag` 3, `desc` 1,
105
+ - Pick the role by what the value *is*, because the rank weights them positionally (`title` 10, `tag` 3, `desc` 1,
101
106
  `filter` 0): `title` is the one line a human scans for, `desc` is prose, `tag` is a keyword list, `filter` is a
102
107
  scoping value (status, owner, role) that must be matchable but must never outrank a real title hit.
103
108
  - `thumb` is mirrored for rendering a hit and is **not** indexed — never expect it to match.
@@ -114,15 +119,24 @@ with it. `conventions` carries the invariants — this is the full contract behi
114
119
  - Declaring roles is all the wiring there is. Mirror rows are maintained by SQL triggers — not document hooks —
115
120
  because `updateOneByQuery` and friends fire no hooks, and most searchable-field mutations go through exactly that
116
121
  path.
117
- - Search runs on sqlite/libsql only. `q.search()` against Postgres throws, loudly, rather than returning every row.
122
+ - Search runs in every database mode: fts5 on SQLite/libSQL, a weighted `tsvector` under a GIN index on Postgres.
123
+ For the same text the same documents match — punctuation splits words the way unicode61 does, and a phrase never
124
+ runs from one column into the next — but the order may differ, since Postgres ranks without document frequency.
125
+ Assert on the match set, never on a Postgres order the weights alone do not decide.
126
+ - Postgres reads `AKAN_SEARCH_TOKENIZER` as `unicode61 [remove_diacritics 0|1|2]` (`simple`, with `unaccent` unless
127
+ `0`) or `trigram [case_sensitive 0|1]` (`pg_trgm`); any other spelling is refused. The extension is created when
128
+ the role may, otherwise a role that may runs `CREATE EXTENSION`. It indexes the first 20,000 characters of
129
+ `title`, `tag` and `filter` and 200,000 of `desc` — a tsvector refuses more than 1MB, and the refusal would fail
130
+ the model write. A database whose `LC_CTYPE` is `C` keeps non-ASCII case apart; use a UTF-8 ctype.
118
131
  - `AKAN_SEARCH_ENABLED=0` switches the index off process-wide; unset means on. It never deletes mirror data, and
119
132
  re-enabling reconciles every ref. **Give every process the same value** — a process cannot drop triggers for models
120
133
  it does not mount, so a mixed fleet leaves stale triggers behind.
121
134
  - The tokenizer is `AKAN_SEARCH_TOKENIZER` (or `database.search.tokenizer`, which wins), defaulting to
122
135
  `unicode61 remove_diacritics 2`. Changing it rebuilds the index from the mirror on the next boot — the model
123
- tables are never re-read — so it is a safe knob, unlike a `text` role change, which re-reads every row. The
124
- rebuild takes no cross-process claim, so a fleet restarted at once repeats it in every process; stagger the
125
- restart when the mirror is large.
136
+ tables are never re-read — so it is a safe knob, unlike a `text` role change, which re-reads every row. The schema
137
+ is ensured in one write turn — a SQLite write transaction, a Postgres schema lock — so of a fleet restarted at
138
+ once the first rebuilds and the rest find it current. A SQLite process waits for that turn up to its
139
+ `busy_timeout`, so stagger the restart when the mirror is large.
126
140
 
127
141
  ## Cascade Remove — the `cascade` option
128
142