@akanjs/cli 3.0.0-beta.8 → 3.0.0

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 (224) hide show
  1. package/.build-stamp +1 -1
  2. package/CodeAgent-2vmp9qe0.js +9 -0
  3. package/CodeAgentRpcHost-p7h6n2hp.js +7 -0
  4. package/CodeAgentRpcListener-7p7z5dd7.js +97 -0
  5. package/CodeTui-5ed6bg2a.js +1457 -0
  6. package/README.ko.md +83 -28
  7. package/README.md +70 -27
  8. package/TunnelShare-cwh9ezsm.js +9 -0
  9. package/{agent.command-0q17pnrs.js → agent.command-gn73rhe7.js} +8 -9
  10. package/akanCode.js +102 -0
  11. package/application.command-e968t0hw.js +246 -0
  12. package/{applicationBuildRunner-kxwbssgn.js → applicationBuildRunner-64taxseg.js} +121 -77
  13. package/buildBatch.proc.js +120 -32
  14. package/cloud.command-0jm301rr.js +95 -0
  15. package/cloud.script-nemnzjpv.js +20 -0
  16. package/code.command-18kcm7hq.js +154 -0
  17. package/commandManifest.json +1 -1
  18. package/consoleToStderr-h5bryj94.js +7 -0
  19. package/context.command-trd9bn18.js +48 -0
  20. package/context.runner-y5hwh673.js +21 -0
  21. package/{dependencyScanner-shs82ynp.js → dependencyScanner-06z2dmrr.js} +2 -2
  22. package/{devTui-m1atgkc9.js → devTui-q7k7bakd.js} +122 -40
  23. package/fileSys-wzzrntkw.js +9 -0
  24. package/guideline.command-bdmdn2de.js +42 -0
  25. package/guidelines/agentRule/agentRule.instruction.md +154 -26
  26. package/guidelines/conventions/conventions.instruction.md +134 -48
  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 +291 -6
  30. package/guidelines/ssrRule/ssrRule.instruction.md +4 -2
  31. package/guidelines/transportRule/transportRule.instruction.md +10 -8
  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-0wpybxtr.js +2729 -0
  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-2tv3fdx4.js → index-3w94e7n3.js} +1228 -1458
  40. package/{index-hwkygn9s.js → index-48k96p55.js} +325 -36
  41. package/index-4npz05nm.js +187 -0
  42. package/{index-q0y2jq9j.js → index-52w0fvh8.js} +21 -4
  43. package/index-6he6gvfg.js +4300 -0
  44. package/index-7md2g9vw.js +6 -0
  45. package/{index-04bk599g.js → index-93j60em0.js} +10 -17
  46. package/index-94s080qm.js +266 -0
  47. package/index-9dp031by.js +12 -0
  48. package/{index-ktb5njgy.js → index-9z771ptz.js} +49 -99
  49. package/{index-y6n8nmny.js → index-abxpehf8.js} +75 -203
  50. package/{index-3d9y066c.js → index-bjwxs904.js} +9 -8
  51. package/{index-9077wzd2.js → index-by9pxkr0.js} +43 -26
  52. package/index-c6eggbee.js +123 -0
  53. package/index-d2242ksy.js +99 -0
  54. package/index-drehwpdt.js +1119 -0
  55. package/{index-92zztqy1.js → index-dy0jx9ay.js} +2 -17
  56. package/{index-ffvk10zz.js → index-f3c78nd6.js} +107 -241
  57. package/index-gbggbqcp.js +1058 -0
  58. package/{index-by5gpp8x.js → index-hj6w4ka1.js} +2 -1
  59. package/index-hsmtysz4.js +53 -0
  60. package/{index-j9yhg404.js → index-jmrn54tt.js} +4 -7
  61. package/{index-a64esmtb.js → index-kcdj1z5v.js} +523 -1022
  62. package/{index-d43r08tf.js → index-kqabytj7.js} +77 -150
  63. package/{index-vyvre9en.js → index-mbxbhay5.js} +6 -2
  64. package/{index-vxtad5bd.js → index-mgxpx3f2.js} +5 -8
  65. package/index-p6pbz9n5.js +25 -0
  66. package/{index-sj5vxr8f.js → index-p993efkq.js} +981 -897
  67. package/{index-2xp0kfkt.js → index-pmhqttv3.js} +37 -25
  68. package/index-tqm7jjcn.js +700 -0
  69. package/{index-0g7r8xz3.js → index-vpj42xwt.js} +114 -232
  70. package/index-wa5jbxj1.js +760 -0
  71. package/{index-k2j1hc4r.js → index-wprnsjms.js} +12 -12
  72. package/{index-gyd220p2.js → index-y3mphr1m.js} +554 -577
  73. package/index-yg7jhz8x.js +69 -0
  74. package/{index-h7yw2z9n.js → index-zzvvc845.js} +8 -14
  75. package/index.js +23 -22
  76. package/{library.command-qzzbj6d8.js → library.command-9h749a1x.js} +4 -5
  77. package/{localRegistry.command-vyrgg4xw.js → localRegistry.command-2bqqhxyz.js} +23 -43
  78. package/{module.command-rq5hq3ax.js → module.command-nv8nzgft.js} +8 -12
  79. package/{package.command-d9hd6v9n.js → package.command-mq4bggp6.js} +6 -7
  80. package/package.json +15 -7
  81. package/{page.command-126d56te.js → page.command-j639n707.js} +4 -5
  82. package/{primitive.command-taxd1538.js → primitive.command-kz6849th.js} +9 -23
  83. package/prompter-76k56b37.js +8 -0
  84. package/{quality.command-6gsna0a3.js → quality.command-4hyabwkb.js} +205 -377
  85. package/{recipeScanner-fpagtgc1.js → recipeScanner-4b69ybe0.js} +1 -1
  86. package/{repair.command-tq2dgy72.js → repair.command-a9awpbqr.js} +11 -35
  87. package/{routeSourceValidator-apyqw7wk.js → routeSourceValidator-7ycnfz0e.js} +0 -6
  88. package/{scalar.command-3bcmrrkv.js → scalar.command-ma4fpm9m.js} +7 -11
  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-eddzqpx3.js → subspace.command-3k2pmrs4.js} +53 -62
  101. package/templates/__scalar/__model__/__model__.abstract.md.template +8 -0
  102. package/templates/app/page/_index.tsx +257 -258
  103. package/templates/app/page/_layout.tsx +5 -1
  104. package/templates/app/page/_overrides.tsx +12 -0
  105. package/templates/app/page/styles.css.template +316 -14
  106. package/templates/app/public/apple-touch-icon.png +0 -0
  107. package/templates/app/public/favicon.ico +0 -0
  108. package/templates/app/public/jelly/cloud.webp +0 -0
  109. package/templates/app/public/jelly/comet.webp +0 -0
  110. package/templates/app/public/jelly/moon.webp +0 -0
  111. package/templates/app/public/jelly/planet.webp +0 -0
  112. package/templates/app/public/jelly/rocket.webp +0 -0
  113. package/templates/app/public/jelly/star.webp +0 -0
  114. package/templates/app/public/jelly/sugar.webp +0 -0
  115. package/templates/app/public/logo.png +0 -0
  116. package/templates/app/ui/Recipe/index.ts +9 -0
  117. package/templates/app/ui/Recipe/jellyButton.ts +63 -0
  118. package/templates/appSample/lib/__scalar/workHistory/workHistory.abstract.ts +8 -39
  119. package/templates/appSample/lib/_noti/noti.abstract.ts +6 -29
  120. package/templates/appSample/lib/task/Task.Unit.tsx +1 -1
  121. package/templates/appSample/lib/task/Task.Util.tsx +10 -13
  122. package/templates/appSample/lib/task/Task.View.tsx +2 -2
  123. package/templates/appSample/lib/task/Task.Zone.tsx +5 -2
  124. package/templates/appSample/lib/task/task.abstract.ts +12 -32
  125. package/templates/appSample/lib/task/task.signal.spec.ts +1 -15
  126. package/templates/appSample/lib/task/task.signal.test.ts +15 -1
  127. package/templates/appSample/lib/task/task.signal.ts +10 -8
  128. package/templates/appSample/page/task/[taskId]/_index.tsx +6 -6
  129. package/templates/appSample/page/task/[taskId]/edit.tsx +5 -4
  130. package/templates/appSample/page/task/_index.tsx +4 -3
  131. package/templates/appSample/page/task/_layout.tsx +1 -1
  132. package/templates/appSample/page/task/new.tsx +5 -4
  133. package/templates/appSample/srvkit/{AuthGuard.ts → guards.ts} +7 -4
  134. package/templates/appSample/srvkit/{SessionInternalArg.ts → internalArgs.ts} +6 -4
  135. package/templates/appSample/ui/GlobalLoading.tsx +2 -1
  136. package/templates/appSample/ui/QuantityControl.tsx +3 -3
  137. package/templates/client.ts +7 -30
  138. package/templates/crudPages/[__model__Id]/edit/page.tsx +2 -1
  139. package/templates/crudPages/[__model__Id]/page.tsx +2 -1
  140. package/templates/crudPages/new/page.tsx +2 -1
  141. package/templates/crudPages/page.tsx +2 -1
  142. package/templates/crudSinglePage/page.tsx +2 -1
  143. package/templates/lib/__lib/lib.dictionary.ts +0 -2
  144. package/templates/lib/__lib/lib.document.ts +0 -2
  145. package/templates/lib/sig.ts +8 -30
  146. package/templates/localDev/docker-compose.yaml.template +3 -14
  147. package/templates/module/__model__.signal.ts +12 -4
  148. package/templates/pkgRoot/tsconfig.json.template +2 -7
  149. package/templates/server.ts +1 -3
  150. package/templates/service/__model__.abstract.md.template +8 -0
  151. package/templates/workspaceRoot/.gitignore.template +0 -1
  152. package/templates/workspaceRoot/AGENTS.md.template +6 -0
  153. package/templates/workspaceRoot/package.json.template +1 -3
  154. package/tunnel.command-xwr46b66.js +129 -0
  155. package/{typeChecker-3xgrzc2v.js → typeChecker-px49mezc.js} +1 -1
  156. package/typecheck.proc.js +1 -1
  157. package/workflow.command-t7wqe4h7.js +79 -0
  158. package/{workspace.command-06wvstgr.js → workspace.command-4derhzrh.js} +79 -109
  159. package/abstractCompactor-gmkc0z8j.js +0 -102
  160. package/application.command-18y0s485.js +0 -204
  161. package/applicationReleasePackager-4yasxwv9.js +0 -244
  162. package/capacitorApp-36rbr858.js +0 -12
  163. package/cloud.command-av97ndh6.js +0 -94
  164. package/cloud.script-cm11xjdz.js +0 -20
  165. package/context.command-5v37ef3n.js +0 -81
  166. package/devTuiView-0hc2emhe.js +0 -9
  167. package/getDirname-dr3jz1xg.js +0 -7
  168. package/guideline.command-4pz9g1rt.js +0 -360
  169. package/guidelines/agentRule/agentRule.generate.json +0 -31
  170. package/guidelines/componentRule/componentRule.generate.json +0 -31
  171. package/guidelines/conventions/conventions.generate.json +0 -25
  172. package/guidelines/cssRule/cssRule.generate.json +0 -31
  173. package/guidelines/docPageRule/docPageRule.generate.json +0 -31
  174. package/guidelines/docPageRule/docPageRule.instruction.md +0 -25
  175. package/guidelines/docSyncRule/docSyncRule.generate.json +0 -31
  176. package/guidelines/docSyncRule/docSyncRule.instruction.md +0 -25
  177. package/guidelines/enumConstant/enumConstant.generate.json +0 -31
  178. package/guidelines/enumConstant/enumConstant.instruction.md +0 -28
  179. package/guidelines/fieldRule/fieldRule.generate.json +0 -31
  180. package/guidelines/framework/framework.generate.json +0 -32
  181. package/guidelines/mcpRule/mcpRule.generate.json +0 -35
  182. package/guidelines/modelConstant/modelConstant.generate.json +0 -32
  183. package/guidelines/modelDictionary/modelDictionary.generate.json +0 -32
  184. package/guidelines/modelDocument/modelDocument.generate.json +0 -31
  185. package/guidelines/modelService/modelService.generate.json +0 -31
  186. package/guidelines/modelSignal/modelSignal.generate.json +0 -31
  187. package/guidelines/modelStore/modelStore.generate.json +0 -31
  188. package/guidelines/modelTemplate/modelTemplate.generate.json +0 -31
  189. package/guidelines/modelUnit/modelUnit.generate.json +0 -31
  190. package/guidelines/modelUtil/modelUtil.generate.json +0 -31
  191. package/guidelines/modelView/modelView.generate.json +0 -31
  192. package/guidelines/modelZone/modelZone.generate.json +0 -31
  193. package/guidelines/moduleCodegen/moduleCodegen.generate.json +0 -31
  194. package/guidelines/moduleCodegen/moduleCodegen.instruction.md +0 -28
  195. package/guidelines/moduleOverview/moduleOverview.generate.json +0 -31
  196. package/guidelines/queryRule/queryRule.generate.json +0 -31
  197. package/guidelines/recipeRule/recipeRule.generate.json +0 -31
  198. package/guidelines/runtimeRule/runtimeRule.generate.json +0 -25
  199. package/guidelines/scalarConstant/scalarConstant.generate.json +0 -32
  200. package/guidelines/scalarDictionary/scalarDictionary.generate.json +0 -32
  201. package/guidelines/scalarModule/scalarModule.generate.json +0 -31
  202. package/guidelines/sharedUiUsage/sharedUiUsage.generate.json +0 -31
  203. package/guidelines/sharedUiUsage/sharedUiUsage.instruction.md +0 -25
  204. package/guidelines/ssrRule/ssrRule.generate.json +0 -31
  205. package/guidelines/transportRule/transportRule.generate.json +0 -31
  206. package/guidelines/utilUiUsage/utilUiUsage.generate.json +0 -31
  207. package/guidelines/utilUiUsage/utilUiUsage.instruction.md +0 -25
  208. package/guidelines/workspaceOnboarding/workspaceOnboarding.generate.json +0 -24
  209. package/guidelines/workspaceRecipes/workspaceRecipes.generate.json +0 -33
  210. package/index-47pj3aee.js +0 -76
  211. package/index-5ystnsd5.js +0 -841
  212. package/index-6wwzs1bc.js +0 -7
  213. package/index-a17x49vg.js +0 -229
  214. package/index-m0xccmmm.js +0 -228
  215. package/index-meh558jy.js +0 -65
  216. package/index-rtxxan88.js +0 -471
  217. package/index-te54x9zm.js +0 -338
  218. package/index-w8kmx1b6.js +0 -2176
  219. package/index-wt7qrtd4.js +0 -1516
  220. package/templates/__scalar/__model__/__model__.abstract.md +0 -28
  221. package/templates/module/__model__.abstract.md +0 -28
  222. package/templates/service/__model__.abstract.md +0 -28
  223. package/templates/workspaceRoot/patches/react-dom@19.2.7.patch.template +0 -62
  224. package/workflow.command-gtdnzvbz.js +0 -108
@@ -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,42 @@ 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 OS that builds it — `--arch` picks
362
+ a Windows or Linux app's CPU, and a macOS app is Apple silicon only. `--installer true` adds a Windows setup, a
363
+ macOS dmg or a Linux AppImage; `AKAN_NATIVE_MACOS_*` signs with a Developer ID and notarizes,
364
+ `AKAN_NATIVE_WINDOWS_*` signs with Authenticode. With `updates: { url, publicKey }` an installed app takes the signed
365
+ releases `akan publish-update` makes; `akan update-keygen` makes the key and prints its `publicKey`.
366
+ - **A desktop app that carries its server (`desktop: { server: true }` in `native`, or in one target) gets nothing
367
+ from `docker`** — and `server: { omit: [...] }` leaves out packages only the image needs. That server is API-only, runs in database mode `single`, and listens on a loopback port any
368
+ program on the computer can call, so guard its endpoints as a network server's. An executable the app spawns
369
+ goes in `bin` — per platform, a download checked against its `sha256` or a file beside the config, put first on
370
+ the app's PATH so the server's `spawn("ffmpeg")` finds it, and in `ctx.binDir` for a native plugin — and a package
371
+ that builds itself at install goes in `trustedDependencies`, which the image honours too. Carry a static LGPL
372
+ ffmpeg: a `--enable-nonfree` build may not be redistributed. A file the user picks with
373
+ `filePicker.pickFiles({ forServer: true })` reaches it as a grant, never a copy or a path:
374
+ `NativeFile.resolve(grant, "read")` in `akanjs/server` asks the shell for it. What it carries is readable on the
375
+ user's computer — `private/` (each lib's too, under `private/libs/<lib>`), the one `env.server.<env>.ts` of its
376
+ `--env` and its libs' server env defaults (`env.server.testing.ts`) — and it has no `public/`: read runtime files
377
+ from `AKAN_APP_DIR`, never `process.cwd()`.
335
378
  - **`assets: { pruneFonts, keepFonts }`** trims from the `dist` copy of `public/` the fonts nothing reads; source
336
379
  trees are never touched. A font with `optimize` on is a build input, not a runtime asset. `keepFonts` belongs to
337
380
  the `akan.config.ts` that owns the font, written against that scope's own `public/`.
381
+ - **Database modes are `database: { modes: [...] }`** — `single` (SQLite + Solid), `multiple` (one SQLite file on
382
+ a host volume + Redis), `cluster` (Postgres + Redis). The build carries the drivers of every declared mode and a
383
+ deployment picks one with `AKAN_DATABASE_MODE` — a declared one, named whenever the build declares several.
384
+ Where the data lives (`POSTGRES_URL`, `REDIS_URI`, `SQLITE_DATABASE_PATH`) is the deployment's env, ahead of
385
+ `env.server.ts`. `q.raw` is written in the dialect's own SQL, so an app running in two modes avoids it.
386
+ - **Backups run through the image's own `main.js`**: `bun main.js ops snapshot` / `ops restore` (offline), and
387
+ `/_akan/ops/*` for a control plane, mounted only when `AKAN_OPS_PUBLIC_KEY` is set and authenticated by a short
388
+ EdDSA token that key verifies. `GET /_akan/app/info` is public. `operationMode` gates none of it.
338
389
 
339
390
  ## React Components And Styling (`**/*.tsx`)
340
391
 
@@ -357,7 +408,7 @@ Full contract: `get_guideline` with `runtimeRule`, or `akan guideline show runti
357
408
  nothing — they covered five control types, keyed on the translated label, and restored over server data.
358
409
  - 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
410
  - 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.
411
+ - 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
412
  - Use `<Link>` from `akanjs/ui` for internal navigation; `<a>` only for `mailto:` and external links.
362
413
 
363
414
  ## Naming And Language
@@ -421,8 +472,11 @@ stores need none, because state and CRUD actions are generated. The body is thre
421
472
  `this.setX(...)` → toast. The optimistic shape is mutate the client model, `void fetch.*`, then commit. Use
422
473
  `this.pick(...)` when the value must exist, `this.get()` when it may not, and `this.set({...})` to write. Mutate lists
423
474
  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.
475
+ nothing** (`no-return-in-store-action.grit`). **Reach another store through `this`:**
476
+ `import type { RootStore } from "../st"`, then `(this as unknown as RootStore).logout()` to call its action or
477
+ `(this as unknown as RootStore).set({ … })` / `.get()` to write or read its state. Every store is mixed into one root
478
+ at runtime, so the cast only tells the type what `this` already is. Keep it `import type` — `st.ts` imports every
479
+ store, so a value import is a cycle.
426
480
 
427
481
  **`<model>.dictionary.ts`** — fixed chain, with empty stages still written:
428
482
  `.of() → .model() → .insight() → .query() → .sort() → .enum() → .slice() → .endpoint() → .error() → .translate()`.
@@ -475,13 +529,14 @@ Full contract — credential handshake, room revalidation, socket cleanup scopin
475
529
  never bounded. **Losing the race does not cancel the work**: the handler runs to completion with nobody
476
530
  holding its result, so a deadline is an answer to the caller, not an undo.
477
531
  - **A cached answer is the endpoint's `{ cache: <ms> }`, and only a `query` taking no internal argument may have
478
- one.** The `Cache` middleware is registered by default and stands aside for every endpoint that declares
479
- nothing. Internal arguments are how a call learns who is asking (`.with(Self)`), so an endpoint that has them
480
- answers per caller and one shared entry would be one caller's answer handed to the next — that endpoint and
481
- every `mutation` are named in the log and left uncached. Guards run on every hit; what is cached is the
482
- handler's result, so `resolveReturn` still masks fields and resolves relations per call; a cache backend that
483
- is down is warned about and the call runs uncached. The default chain is `Logging → Timeout → Cache → handler`,
484
- and there is no `Retry` middleware.
532
+ one.** It is a step of the call, not a middleware, and stands aside for every endpoint that declares nothing.
533
+ Internal arguments are how a call learns who is asking (`.with(Self)`), so an endpoint that has them answers
534
+ per caller and one shared entry would be one caller's answer handed to the next — that endpoint and every
535
+ `mutation` are named in the log and left uncached. The lookup runs after the guards, so a hit reaches only a
536
+ caller they admitted; what is cached is the handler's result, so `resolveReturn` still masks fields and resolves
537
+ relations per call; a cache backend that is down is warned about and the call runs uncached. The default
538
+ middleware chain is `Logging → Timeout → <lib middlewares>`, then guards → internal arguments → cache →
539
+ handler, and there is no `Retry` middleware.
485
540
 
486
541
  ### Authorization Defaults
487
542
 
@@ -548,8 +603,11 @@ Full contract — filter-arg `ref` pickers, `getQueryMeta` summary counters, `la
548
603
  first "more". `isCumulativeOf<Model>` is the mode.
549
604
  - **"Is there more" is `hasMoreOf<Model>`, never `count > page * limit`.** It comes from the length of the batch
550
605
  the server returned, so it survives `{ insight: false }` and cannot drift from a count a live event moved.
551
- - **Hydrated vs raw:** server queries return hydrated `cnst.<Model>` instances (with `set`/`save`/`refresh`);
552
- client fetch results are raw `GetStateObject` plain data, functions stripped.
606
+ - **Hydrated vs raw:** server queries return hydrated `cnst.<Model>` instances (with `set`/`save`/`refresh`),
607
+ and so do client fetch results — `FetchClient` parses every return with `crystalize` defaulting to **true**
608
+ (`pkgs/akanjs/fetch/client/fetchClient.ts`). The raw `GetStateObject` shape is what the `<ref>Obj` /
609
+ `<ref>ObjList` / `<ref>ObjInsight` handles carry, because those pass `crystalize: false` on purpose so the
610
+ value can cross the RSC boundary as a client prop.
553
611
  - Every filter generates fourteen methods: `list` · `listIds` · `find` · `findId` · `pick` · `pickId` · `exists` ·
554
612
  `count` · `insight` · `query` · **`remove`** · **`removeOne`** · **`update`** · **`updateOne`**. The last four
555
613
  are query-level writes — one atomic UPDATE, **no hooks**, and therefore no `_postRemove` and no cascade. Use
@@ -587,7 +645,7 @@ Full contract — relevance ordering, `columns` / `weights`: `get_guideline` wit
587
645
  - Injected dependencies resolve by field-name convention: a field named `<refName>Service` resolves to the service registered under `<refName>`, and `<refName>Signal` likewise (`pkgs/akanjs/service/injectInfo.ts`).
588
646
  - The `Service`/`Signal` suffix is required — the injector strips it to derive the registry lookup key. Name the field after the target refName plus the suffix, not arbitrarily.
589
647
  - Preference order inside a service: `service<srv.XService>()` for another module's service · `plug(AdapterClass)` or `plug(StorageAdaptorRole)` for an adapter · `use<T>()` only to reach an `option.ts`-registered legacy singleton · `env(...)` for config.
590
- - **`memory(...)` takes a scalar or model class, not only a primitive** — `memory(Map, { of: CallStateInput })` serializes through the constant and travels as JSON text, so the Redis and sqlite-backed caches round-trip the same declaration. Never hand-encode JSON into a `String` memory.
648
+ - **`memory(...)` takes a scalar or model class, not only a primitive** — `memory(Map, { of: CallStateInput })` serializes through the constant and travels as JSON text, so the Redis and sqlite-backed caches round-trip the same declaration. Never hand-encode JSON into a `String` memory. A memory is scoped to the service or adaptor that declares it; `ttl` (milliseconds) sets how long each write lives unless the `set` call names its own `expireAt`, and a `Map` entry expires on its own.
591
649
 
592
650
  ### Adapters — `adapt()` And `plug()`
593
651
 
@@ -662,10 +720,14 @@ it.
662
720
  (RFC 7591; Cursor's `cursor://` redirect admitted) plus the consent page `libs/shared/page/oauth/consent` — and
663
721
  names itself as the issuer, so `/mcp` demands a bearer token, verifies its signature, and refuses one carrying no
664
722
  `aud`. A token is the app's own access JWT plus `aud`/`iss`/`client_id`, so `AccountMiddleware` and the guards
665
- judge it unchanged; there is no scope. Per-app knobs live in `env.server.*` under `oauth` (`consentPath` and
666
- `signinPath` with the basePath, `clients`, `dynamicRegistration`, `allowedRedirectSchemes`, `clientIdMetadata`,
667
- `enabled`). `/mcp` reads the `Authorization` header only — a cookie is dropped at the door — and `JWT_SECRET` is
668
- mandatory outside `local`, because the derived fallback is forgeable from three public names. A grant is revoked
723
+ judge it unchanged. The tokens this built-in server mints carry no scope; `auth.scopes` / `AKAN_MCP_SCOPES` are for
724
+ tokens an external issuer mints, and declared beside this server they refuse every one of its tokens with 403
725
+ `insufficient_scope`. Per-app knobs live in `env.server.*` under `oauth` (`consentPath` and
726
+ `signinPath` with the basePath, `clients`, `dynamicRegistration`, `allowedRedirectSchemes`, `accessTokenSeconds`,
727
+ `clientIdMetadata`, `enabled`, plus `issuer` / `resource` — set those when a tunnel or an edge makes the derived
728
+ 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
729
+ `security.jwtSecret`) is mandatory whenever `operationMode` is not `local`, because the derived fallback is
730
+ forgeable from three public names; `AKAN_ALLOW_DERIVED_JWT_SECRET=1` accepts that risk explicitly. A grant is revoked
669
731
  whole — `POST /oauth/revoke` (RFC 7009) by the client, `fetch.revokeOAuthConnection(sessionId)` by the account's
670
732
  owner, `fetch.listOAuthConnections()` to see them — and a revoked access token is refused at its next call.
671
733
  - **A person may, a model may not.** An agent's token names its `client_id` and `aud`; a browser session has
@@ -714,9 +776,12 @@ what every developer must know even when not building one.
714
776
  every tool runs in the caller's own browser session, gated by guards and the approval card. Its guard is
715
777
  `AgentRelayAccess`, which refuses every call until the app names a guard of its own —
716
778
  `option.setAgentAccess(SignedIn)`. **The LLM is configured in `option.ts`, never through the environment**
717
- (`option.setLlm({ apiKey, model, host })`). Three providers ship — `DeepseekLlm` (the default, text only),
718
- `OpenaiLlm` and `AnthropicLlm` (both read images) — and swapping one in is
719
- `option.applyAdaptor(LlmAdaptorRole, AnthropicLlm)`.
779
+ (`option.setLlm({ apiKey, model, host })`). Two adaptors ship, one per wire: `OpenaiLlm` is the default and
780
+ speaks the chat-completions dialect to whatever `host` names — OpenAI, DeepSeek, Groq, OpenRouter, Ollama — and
781
+ `AnthropicLlm` speaks the Messages API and reads a PDF as well as a picture. Both require `model`. A provider
782
+ neither covers is an `adapt()` class in `srvkit/` implementing `LlmAdaptor`, applied with
783
+ `option.applyAdaptor(LlmAdaptorRole, TheClass)`; whatever settings it needs beyond `LlmOption` ride `setLlm`
784
+ too, read back with `use<MyLlmOption>()`.
720
785
  - **Declare the tool beside the control that already does it**, never as a separate surface:
721
786
  `st.tool("x").desc("…").arg("id", ID).exec(fn)` returns the callable to hand to `onClick` — one handler for the
722
787
  person and the agent. Publish it only where the screen already renders the control; a falsy name declares the
@@ -779,7 +844,8 @@ Full contract — the trigger-maintained mirror, tokenizer changes, `AKAN_SEARCH
779
844
  backstop, including for a `text` field *underneath* one of those. Do not work around either.
780
845
  - The role works on a relation (`image: field(File, { text: "thumb" })`) and on an array; an array of objects
781
846
  indexes by leaf key. A field inside a `Map` indexes nothing — there is no fixed path to extract it from.
782
- - Search runs on sqlite/libsql only. `q.search()` against Postgres throws, loudly, rather than returning every row.
847
+ - Search runs in every database mode — fts5 on SQLite/libSQL, a weighted `tsvector` (`pg_trgm` for `trigram`) on
848
+ Postgres. The same text matches the same documents; only the order may differ on Postgres.
783
849
 
784
850
  ### Image & File Fields
785
851
 
@@ -828,10 +894,17 @@ shape, so `cascade` never means "related" — it means one of exactly these:
828
894
  - Dynamic segments use `[id]`; route groups use directories like `(user)`, `(public)`, `(tab)`, or `(detail)`.
829
895
  - **A route file exports one thing**: `export default page()…render(…)` for a page, `layout()…render(…)` for a
830
896
  `_layout.tsx`, `rootLayout()…render(…)` for the app's (or a basePath's) root `_layout.tsx`. Every route setting
831
- is a stage of the chain — `.param()`, `.search()`, `.config()`, `.head()` / `.metadata()`, `.loading()`;
832
- `.notFound()` / `.error()` on a layout; `.fonts()` / `.theme()` / `.manifest()` / `.reconnect()` / `.wsConnect()`
833
- / `.layoutStyle()` / `.gaTrackingId()` on the root layout; `.prompt()` on a page. A named export beside the chain,
834
- or `page()` in a `_layout.tsx`, fails the build.
897
+ is a stage of the chain — `.param()`, `.search()`, `.config()`, `.head()`, `.loading()`; `.notFound()` /
898
+ `.error()` on a layout; `.fonts()` / `.theme()` / `.manifest()` / `.reconnect()` / `.wsConnect()` /
899
+ `.layoutStyle()` on the root layout; `.prompt()` on a page. A named export beside the chain, or `page()` in a
900
+ `_layout.tsx`, fails the build. `.head()` takes JSX — `<title>`, `<meta>`, `<link>` — or a function of the
901
+ route's args returning it; there is no metadata object, and analytics is the app's own script, not a stage.
902
+ - **A CSR bundle (a native app, `?csr=true`) never shows a root layout's own markup.** Pages render into the
903
+ frame, and what the root layout draws around `{children}` stays in a hidden container, so its wrappers cannot
904
+ stack a second screen beside the frame. Its components still mount and their effects run (`<Auth.User />` works).
905
+ Anything a person must see from a root layout — an overlay, a gate, a banner — renders through
906
+ `createPortal(…, document.body)`, as `<Agent.Chat />` and the dialogs already do. A non-root `_layout.tsx` has no
907
+ such limit.
835
908
  - **A page names every `[x]` segment of its path with `.param("x", Type)`** and reads a query key only through
836
909
  `.search("k", Type)`. Values arrive typed — `ID`/`String` → string, `Int`/`Float` → number, `Boolean`, `Date` →
837
910
  Dayjs, an `enumOf` class → its union, `[T]` → array — a path value the type refuses answers not-found, and a
@@ -874,10 +947,10 @@ export default page()
874
947
 
875
948
  ## Akan Sync Conventions (`apps/**`, `libs/**`)
876
949
 
877
- - `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`.
878
- - `apps/<appName>` root may only contain these folders: `.akan`, `android`, `common`, `env`, `ios`, `lib`, `mobile`, `page`, `plugin`, `private`, `public`, `script`, `secrets`, `srvkit`, `ui`, `webkit`.
950
+ - `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`.
951
+ - `apps/<appName>` root may only contain these folders: `.akan`, `common`, `env`, `lib`, `native`, `page`, `plugin`, `private`, `public`, `script`, `secrets`, `srvkit`, `ui`, `webkit`.
879
952
  - `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`.
880
- - `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.
953
+ - `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>`.
881
954
  - Both allowlists have one source — `pkgs/@akanjs/devkit/workspaceLayout.ts`. `akan sync` (error), `akan doctor`
882
955
  (diagnostic), and `akan quality scan` (warning) all read it, so add a new root entry there and mirror it into this
883
956
  list, never into one of the three call sites.
@@ -901,12 +974,18 @@ export default page()
901
974
  | Folder | Admission test | Naming |
902
975
  |---|---|---|
903
976
  | `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 |
904
- | `webkit/` | touches `window` / `navigator` / Capacitor, or is a React hook | `use<Thing>.tsx` — `.tsx` even with no JSX |
977
+ | `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 |
905
978
  | `srvkit/` | touches `node:*`, `Bun`, `process.env`, a secret, or a server SDK | camelCase file, PascalCase class |
906
- | `ui/` | renders JSX and is not bound to one model | PascalCase component, camelCase sidecar (`swipeCard.util.ts`) |
979
+ | `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` |
907
980
  | `plugin/` | build- or CLI-time `AkanPlugin` | `<name>.plugin.ts`, registered in `akan.config.ts` |
981
+ | `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 |
908
982
 
909
983
  - Hooks return a named object of async closures, never a tuple.
984
+ - A `native/` plugin's page API is imported by a `webkit/` hook (`../native/<id>/src`) and nothing else; pages and
985
+ components call the hook, the same way they reach `akanjs/client/native`.
986
+ - `ui/` is the presentation layer — markup and the looks it is built from — so a recipe lives there although it is
987
+ neither a component nor a hook. Not `webkit/`: its hooks are `"use client"`, the opposite signal for a function
988
+ server components call. Not `common/`: it cannot import `akanjs/ui`, where `recipe` / `tv` come from.
910
989
  - `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.
911
990
  - 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.
912
991
  - `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.
@@ -919,12 +998,15 @@ when two shapes disagree.
919
998
  - `children: any` — newer files use `children: ReactNode`.
920
999
  - Hard-coded API keys or secrets in source; they belong in `option.ts` or env.
921
1000
  - Large blocks of commented-out code left in place.
922
- - Cross-store writes through a `RootStore` cast — it collides with the Bun SSR-bundler segfault.
923
1001
  - Stale `// TODO: Implement …` comments above implemented methods.
924
1002
  - `{cond && <X/>}` in JSX, hard-coded Korean bypassing `l()`, and `window.alert(...)` for user feedback.
925
1003
  - Bare `/* eslint-disable */` blocks — use `// biome-ignore lint/<rule>: <why>`.
926
1004
  - Raw palette grays such as `text-gray-400` instead of the semantic tokens (`text-foreground/70`).
927
1005
 
1006
+ A store calling `(this as unknown as RootStore).<action>()` or `.set({ … })` over
1007
+ `import type { RootStore } from "../st"` is **not** one of these — it is the sanctioned way to reach another store's
1008
+ actions and state (see `<model>.store.ts` above).
1009
+
928
1010
  ## Secrets And Env Safety (`.env`, `infra/**`, `*secret*`, `*credential*`)
929
1011
 
930
1012
  - Never print, summarize, commit, or expose real secret values, credentials, tokens, private keys, or `.env` contents.
@@ -943,7 +1025,9 @@ when two shapes disagree.
943
1025
  selected one's log beside it. `Tab` walks every row and `1`-`9` jump to that app (the rail shows each
944
1026
  app's digit), `↑↓` scroll (shift for a page, `G` back to following),
945
1027
  `/` greps, `e` shows stderr only, `c` clears, `y` copies, `Y` copies the log path, `o` opens the browser,
946
- `r` restarts that app, `q` quits.
1028
+ `r` restarts that app, `q` quits. Under `--share`, `s` copies the public URL — the selected app's, or every
1029
+ app's from the merged row — and the header carries it ahead of the local one, because the line `--share`
1030
+ printed before the view took the terminal is gone by its first repaint.
947
1031
  **`--plain` prints prefixed interleaved lines instead**, and a pipe, a redirect or an unsized terminal
948
1032
  downgrades to that on its own.
949
1033
  - **A supervised session writes `local/apps/<app>/runtime/dev.log`, and that path is how a log gets handed over.**
@@ -954,11 +1038,13 @@ when two shapes disagree.
954
1038
  other file. The previous session is kept beside it as `dev.prev.log`. Hand somebody — or an agent — the
955
1039
  path rather than a paste: it costs one line instead of a transcript, and it can be re-read after a fix.
956
1040
  In the view, `y` copies the lines the filters have already narrowed and `Y` copies the path.
957
- - **How many apps boot at once is the machine's answer, not a constant.** A cold boot build is the builder's
958
- RSS peak (~900MB per app), so the default wave is what memory and cores allow — half the memory budget
959
- divided by that peak, and one app per four cores — which boots a laptop's apps together and still staggers
960
- them inside a small container. The session says which in one line. `--concurrency <n>` overrides it, and
961
- `AKAN_MEMORY_LIMIT` lowers the budget it derives from.
1041
+ - **How many apps boot at once is the machine's answer, not a constant.** A cold boot is an app's RSS peak
1042
+ (~1.8GB per app, most of it the build worker — the base build, then the SSR registry's), so the default wave
1043
+ is what memory and cores allow — half the memory budget divided by that peak, and one app per four cores —
1044
+ which boots a laptop's apps together and still staggers them inside a small container. The session says
1045
+ which in one line. The next wave starts once an app serves and its boot builds have settled, or 30 seconds
1046
+ after it serves, while the app shows as ready — and `--open` opens it — as soon as it serves.
1047
+ `--concurrency <n>` overrides the wave size, and `AKAN_MEMORY_LIMIT` lowers the budget it derives from.
962
1048
  - `--kill` frees the dev ports first — it resolves each port's listener, walks up to the top of that akan dev
963
1049
  tree and signals it, so another checkout's server or a stale orphan on the same port is reclaimed. A holder
964
1050
  that is not recognisably an akan process is reported and left alone.
@@ -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