@akanjs/cli 3.0.0-beta.9 → 3.0.1

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-zfab7pkn.js +9 -0
  3. package/CodeAgentRpcHost-p7h6n2hp.js +7 -0
  4. package/CodeAgentRpcListener-7p7z5dd7.js +97 -0
  5. package/CodeTui-0zhnr0pw.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-aja6z9mn.js} +8 -9
  10. package/akanCode.js +102 -0
  11. package/application.command-049ehgx3.js +246 -0
  12. package/{applicationBuildRunner-kxwbssgn.js → applicationBuildRunner-75n5rpwp.js} +121 -77
  13. package/buildBatch.proc.js +120 -32
  14. package/cloud.command-gw4kqxt8.js +95 -0
  15. package/cloud.script-c723cpcb.js +20 -0
  16. package/code.command-x9t6z5nw.js +154 -0
  17. package/commandManifest.json +1 -1
  18. package/consoleToStderr-h5bryj94.js +7 -0
  19. package/context.command-yfbe1rtd.js +48 -0
  20. package/context.runner-erfe68j5.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-kq9gp690.js +42 -0
  25. package/guidelines/agentRule/agentRule.instruction.md +154 -26
  26. package/guidelines/conventions/conventions.instruction.md +138 -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-2tv3fdx4.js → index-150s07n8.js} +1230 -1459
  36. package/{index-vyvre9en.js → index-25fvrxmy.js} +6 -2
  37. package/{index-kcxmv48c.js → index-2ahj896a.js} +45 -79
  38. package/index-2wc41k5w.js +49 -0
  39. package/index-3hx1z96c.js +87 -0
  40. package/{index-q0y2jq9j.js → index-52w0fvh8.js} +21 -4
  41. package/{index-04bk599g.js → index-5g4rkrvt.js} +10 -17
  42. package/index-5pvxf7z0.js +53 -0
  43. package/index-5q14pwzd.js +69 -0
  44. package/{index-vxtad5bd.js → index-66yspb67.js} +5 -8
  45. package/index-7md2g9vw.js +6 -0
  46. package/index-7p4c0yer.js +1058 -0
  47. package/{index-d43r08tf.js → index-8b639zes.js} +77 -150
  48. package/{index-ktb5njgy.js → index-8kvy8qb4.js} +49 -99
  49. package/index-94s080qm.js +266 -0
  50. package/index-9dp031by.js +12 -0
  51. package/{index-0g7r8xz3.js → index-a37by9hn.js} +114 -232
  52. package/{index-k2j1hc4r.js → index-b95yhqjr.js} +12 -12
  53. package/{index-3d9y066c.js → index-bjwxs904.js} +9 -8
  54. package/{index-2xp0kfkt.js → index-br8pkrk1.js} +37 -25
  55. package/index-c14nsvnv.js +700 -0
  56. package/index-c6eggbee.js +123 -0
  57. package/index-d2242ksy.js +99 -0
  58. package/{index-92zztqy1.js → index-dy0jx9ay.js} +2 -17
  59. package/{index-a64esmtb.js → index-fkvb79qe.js} +523 -1022
  60. package/index-h54yj9wr.js +1119 -0
  61. package/{index-by5gpp8x.js → index-hj6w4ka1.js} +2 -1
  62. package/{index-y6n8nmny.js → index-kn5d4jmg.js} +75 -203
  63. package/index-m3xsb1ms.js +4300 -0
  64. package/{index-gyd220p2.js → index-mb59b8x0.js} +554 -577
  65. package/{index-j9yhg404.js → index-mzjb6zp4.js} +4 -7
  66. package/index-p6pbz9n5.js +25 -0
  67. package/{index-sj5vxr8f.js → index-qe71g4ze.js} +983 -898
  68. package/index-rfcz04w9.js +187 -0
  69. package/{index-hwkygn9s.js → index-senscydg.js} +331 -36
  70. package/{index-ffvk10zz.js → index-vs8mefhy.js} +107 -241
  71. package/index-wa5jbxj1.js +760 -0
  72. package/index-y5ga5w9t.js +2729 -0
  73. package/{index-9077wzd2.js → index-y7hx1ygz.js} +43 -26
  74. package/{index-h7yw2z9n.js → index-zzvvc845.js} +8 -14
  75. package/index.js +23 -22
  76. package/{library.command-qzzbj6d8.js → library.command-keqjevpp.js} +4 -5
  77. package/{localRegistry.command-vyrgg4xw.js → localRegistry.command-pydeq5zf.js} +23 -43
  78. package/{module.command-rq5hq3ax.js → module.command-dkrnqx3v.js} +8 -12
  79. package/{package.command-d9hd6v9n.js → package.command-wmc0ys5y.js} +6 -7
  80. package/package.json +15 -7
  81. package/{page.command-126d56te.js → page.command-tf3erhrv.js} +4 -5
  82. package/{primitive.command-taxd1538.js → primitive.command-7hqpegj4.js} +9 -23
  83. package/prompter-76k56b37.js +8 -0
  84. package/{quality.command-6gsna0a3.js → quality.command-8122axq3.js} +205 -377
  85. package/{recipeScanner-fpagtgc1.js → recipeScanner-4b69ybe0.js} +1 -1
  86. package/{repair.command-tq2dgy72.js → repair.command-mcy12h0n.js} +11 -35
  87. package/{routeSourceValidator-apyqw7wk.js → routeSourceValidator-7ycnfz0e.js} +0 -6
  88. package/{scalar.command-3bcmrrkv.js → scalar.command-vvvbzttc.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-pzvqfy5h.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-82zz5xnq.js +129 -0
  155. package/{typeChecker-3xgrzc2v.js → typeChecker-px49mezc.js} +1 -1
  156. package/typecheck.proc.js +1 -1
  157. package/workflow.command-y8x3kgjv.js +79 -0
  158. package/{workspace.command-06wvstgr.js → workspace.command-jvsb7brc.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,10 +77,27 @@ 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,
82
96
  and a `static` helper are all still fine.
97
+ - **Never declare a `static` on an `XObject` or `LightX` class** (`no-static-in-object-light-model.grit`). The full
98
+ model `via(XObject, LightX, …)` mixes in their prototypes only, so a static there never reaches `X` — put it on
99
+ `XInput` (or a scalar), `X`, or `XInsight`. The Object class is recognised by its `*Object` name and the Light
100
+ class by its `Light*` name or its `[...] as const` field tuple.
83
101
  - **Never redeclare a generated CRUD endpoint name** in `*.signal.ts` (`no-redeclare-predefined-endpoint.grit`).
84
102
  - **Never type a `*.Util.tsx` / `*.Zone.tsx` prop as a `cnst` model** (`no-model-type-in-util-zone.grit`). Those two
85
103
  roles are always client components, so a `cnst.Banner` / `cnst.LightBanner` prop is a class instance the server
@@ -216,6 +234,8 @@ Do not narrate code. Do document the thing the code cannot say. Both halves are
216
234
  ## Client / Server Boundaries (`apps/**`, `libs/**`, `pkgs/akanjs/**`)
217
235
 
218
236
  - Use `"use client";` at the top of client component files.
237
+ - A `"use client"` file re-exports by name (`export { A, B } from "./x"`), never with `export *`: the server knows a
238
+ client module only by the export names it spells out, so `akan start` and `akan build` stop on a star re-export.
219
239
  - Be careful when importing client-only code from page or layout modules.
220
240
  - Keep page props serializable unless the existing route pattern clearly allows otherwise.
221
241
  - 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 +293,10 @@ are, because those are the ones the server could have done.
273
293
  SEO snapshots, prerendering and pre-hydration E2E read, so await what the page needs immediately and stream
274
294
  the rest. `xListInY` / `xInsightInY` hold hydrated model instances that React Flight refuses as client props:
275
295
  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.
296
+ 6. **Use named `ReactNode` slots, not just `children`** — a client shell that takes one slot per region lets a
297
+ server component supply each of them, instead of absorbing the whole subtree into the bundle. `Layout.Navbar`
298
+ declares `title` / `left` / `right` alongside `back` and `children`, but renders only the last two today, so
299
+ copy the shape rather than the component.
278
300
  7. **Let the server do the derived work.** Display and predicate logic belongs on `Light<Model>`; enum→class
279
301
  lookups belong in a module-scope `as const` map.
280
302
  8. **Gate auth on the server** — `getSelf({ unauthorize: "/signin" })` in `_layout.tsx`, before any HTML is sent.
@@ -291,11 +313,11 @@ What an app serves, how many processes it runs, where its logs go, and what ship
291
313
  declared in `akan.config.ts` and narrowed — never widened — by an env at boot. Domain code reaches none of it.
292
314
  Full contract: `get_guideline` with `runtimeRule`, or `akan guideline show runtimeRule`.
293
315
 
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.
316
+ - **Web surfaces are `web: true | false | { csr: boolean }`.** `{ csr: false }` drops the CSR bundle the native
317
+ apps ship, `false` is an API-only build that serves nothing under `page/`. There is no CSR-without-SSR option, by
318
+ type — the CSR bundle inlines the stylesheet the SSR base artifact compiles. `AKAN_SSR` / `AKAN_CSR` narrow
319
+ further at runtime and can never switch a surface the build left out back on. `akan start` ignores `web` and
320
+ keeps the whole dev surface.
299
321
  - **One traffic replica runs in the container's only process.** `AKAN_REPLICA=0,0,1` — the default everywhere —
300
322
  has nothing to balance, so there is no gateway and no unix-socket proxy hop. Two or more replicas, a batch-only
301
323
  replica, `AKAN_SOLO=false`, passing `replica` to `new AkanApp(...)`, or `akan start` all bring the gateway back.
@@ -304,7 +326,7 @@ Full contract: `get_guideline` with `runtimeRule`, or `akan guideline show runti
304
326
  rotating log file; nothing supervises it but the orchestrator's probes.
305
327
  - **Route prefixes move together or not at all.** `new AkanApp({ prefix, websocketPrefix })` in `main.ts` moves
306
328
  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
329
+ `fetchClient` in every tab the server renders. A prebuilt CSR shell or a native app bundle is never rendered by
308
330
  a server, so `api: { prefix, websocketPrefix }` in `akan.config.ts` is what those follow; declare it there too
309
331
  when the app ships either. Read the value with `getApiPrefix()` / `getWsPrefix()` from `akanjs/base` — never
310
332
  write `/api` or `/ws` as a literal, and never try to hand it down as a React prop: `FetchClient` fixes its
@@ -332,9 +354,42 @@ Full contract: `get_guideline` with `runtimeRule`, or `akan guideline show runti
332
354
  assembles one from; the string form takes no contributions. The generated image installs `ca-certificates` and
333
355
  `tzdata` and nothing else, so an app needing `ffmpeg` or Chromium declares it in `preRuns` / `postRuns`. A lib
334
356
  declares the steps its own runtime needs and every mounting app inherits them.
357
+ - **Native apps are the `native` section** — iOS, Android and a desktop app (macOS, Windows, Linux), each shipping
358
+ the CSR bundle on the native runtime. Shared fields (`appName`, `appId`, `version`, `indexPath`, `permissions`,
359
+ `plugins`, `deepLinks`, `updates`, …) sit beside one section per platform: `ios`, `android`, `desktop`, and a
360
+ platform section's own `indexPath` wins on that platform (`desktop: { indexPath: "/" }`). Without
361
+ `targets` the app has one target, `default`, and an app with no basePaths leaves `basePath` out; a target takes
362
+ the same shape and overrides the section field by field — objects merge key by key, a list or any other value
363
+ replaces the section's. `akan start-ios` / `start-android` / `start-desktop` run a dev build that loads its pages
364
+ from `akan start`; `build-ios` / `build-android` / `build-desktop` write
365
+ `apps/<app>/.akan/native/<target>/build/<platform>`, a desktop app only for the OS that builds it — `--arch` picks
366
+ a Windows or Linux app's CPU, and a macOS app is Apple silicon only. `--installer true` adds a Windows setup, a
367
+ macOS dmg or a Linux AppImage; `AKAN_NATIVE_MACOS_*` signs with a Developer ID and notarizes,
368
+ `AKAN_NATIVE_WINDOWS_*` signs with Authenticode. With `updates: { url, publicKey }` an installed app takes the signed
369
+ releases `akan publish-update` makes; `akan update-keygen` makes the key and prints its `publicKey`.
370
+ - **A desktop app that carries its server (`desktop: { server: true }` in `native`, or in one target) gets nothing
371
+ 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
372
+ program on the computer can call, so guard its endpoints as a network server's. An executable the app spawns
373
+ goes in `bin` — per platform, a download checked against its `sha256` or a file beside the config, put first on
374
+ the app's PATH so the server's `spawn("ffmpeg")` finds it, and in `ctx.binDir` for a native plugin — and a package
375
+ that builds itself at install goes in `trustedDependencies`, which the image honours too. Carry a static LGPL
376
+ ffmpeg: a `--enable-nonfree` build may not be redistributed. A file the user picks with
377
+ `filePicker.pickFiles({ forServer: true })` reaches it as a grant, never a copy or a path:
378
+ `NativeFile.resolve(grant, "read")` in `akanjs/server` asks the shell for it. What it carries is readable on the
379
+ user's computer — `private/` (each lib's too, under `private/libs/<lib>`), the one `env.server.<env>.ts` of its
380
+ `--env` and its libs' server env defaults (`env.server.testing.ts`) — and it has no `public/`: read runtime files
381
+ from `AKAN_APP_DIR`, never `process.cwd()`.
335
382
  - **`assets: { pruneFonts, keepFonts }`** trims from the `dist` copy of `public/` the fonts nothing reads; source
336
383
  trees are never touched. A font with `optimize` on is a build input, not a runtime asset. `keepFonts` belongs to
337
384
  the `akan.config.ts` that owns the font, written against that scope's own `public/`.
385
+ - **Database modes are `database: { modes: [...] }`** — `single` (SQLite + Solid), `multiple` (one SQLite file on
386
+ a host volume + Redis), `cluster` (Postgres + Redis). The build carries the drivers of every declared mode and a
387
+ deployment picks one with `AKAN_DATABASE_MODE` — a declared one, named whenever the build declares several.
388
+ Where the data lives (`POSTGRES_URL`, `REDIS_URI`, `SQLITE_DATABASE_PATH`) is the deployment's env, ahead of
389
+ `env.server.ts`. `q.raw` is written in the dialect's own SQL, so an app running in two modes avoids it.
390
+ - **Backups run through the image's own `main.js`**: `bun main.js ops snapshot` / `ops restore` (offline), and
391
+ `/_akan/ops/*` for a control plane, mounted only when `AKAN_OPS_PUBLIC_KEY` is set and authenticated by a short
392
+ EdDSA token that key verifies. `GET /_akan/app/info` is public. `operationMode` gates none of it.
338
393
 
339
394
  ## React Components And Styling (`**/*.tsx`)
340
395
 
@@ -357,7 +412,7 @@ Full contract: `get_guideline` with `runtimeRule`, or `akan guideline show runti
357
412
  nothing — they covered five control types, keyed on the translated label, and restored over server data.
358
413
  - 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
414
  - 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.
415
+ - 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
416
  - Use `<Link>` from `akanjs/ui` for internal navigation; `<a>` only for `mailto:` and external links.
362
417
 
363
418
  ## Naming And Language
@@ -421,8 +476,11 @@ stores need none, because state and CRUD actions are generated. The body is thre
421
476
  `this.setX(...)` → toast. The optimistic shape is mutate the client model, `void fetch.*`, then commit. Use
422
477
  `this.pick(...)` when the value must exist, `this.get()` when it may not, and `this.set({...})` to write. Mutate lists
423
478
  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.
479
+ nothing** (`no-return-in-store-action.grit`). **Reach another store through `this`:**
480
+ `import type { RootStore } from "../st"`, then `(this as unknown as RootStore).logout()` to call its action or
481
+ `(this as unknown as RootStore).set({ … })` / `.get()` to write or read its state. Every store is mixed into one root
482
+ at runtime, so the cast only tells the type what `this` already is. Keep it `import type` — `st.ts` imports every
483
+ store, so a value import is a cycle.
426
484
 
427
485
  **`<model>.dictionary.ts`** — fixed chain, with empty stages still written:
428
486
  `.of() → .model() → .insight() → .query() → .sort() → .enum() → .slice() → .endpoint() → .error() → .translate()`.
@@ -475,13 +533,14 @@ Full contract — credential handshake, room revalidation, socket cleanup scopin
475
533
  never bounded. **Losing the race does not cancel the work**: the handler runs to completion with nobody
476
534
  holding its result, so a deadline is an answer to the caller, not an undo.
477
535
  - **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.
536
+ one.** It is a step of the call, not a middleware, and stands aside for every endpoint that declares nothing.
537
+ Internal arguments are how a call learns who is asking (`.with(Self)`), so an endpoint that has them answers
538
+ per caller and one shared entry would be one caller's answer handed to the next — that endpoint and every
539
+ `mutation` are named in the log and left uncached. The lookup runs after the guards, so a hit reaches only a
540
+ caller they admitted; what is cached is the handler's result, so `resolveReturn` still masks fields and resolves
541
+ relations per call; a cache backend that is down is warned about and the call runs uncached. The default
542
+ middleware chain is `Logging → Timeout → <lib middlewares>`, then guards → internal arguments → cache →
543
+ handler, and there is no `Retry` middleware.
485
544
 
486
545
  ### Authorization Defaults
487
546
 
@@ -548,8 +607,11 @@ Full contract — filter-arg `ref` pickers, `getQueryMeta` summary counters, `la
548
607
  first "more". `isCumulativeOf<Model>` is the mode.
549
608
  - **"Is there more" is `hasMoreOf<Model>`, never `count > page * limit`.** It comes from the length of the batch
550
609
  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.
610
+ - **Hydrated vs raw:** server queries return hydrated `cnst.<Model>` instances (with `set`/`save`/`refresh`),
611
+ and so do client fetch results — `FetchClient` parses every return with `crystalize` defaulting to **true**
612
+ (`pkgs/akanjs/fetch/client/fetchClient.ts`). The raw `GetStateObject` shape is what the `<ref>Obj` /
613
+ `<ref>ObjList` / `<ref>ObjInsight` handles carry, because those pass `crystalize: false` on purpose so the
614
+ value can cross the RSC boundary as a client prop.
553
615
  - Every filter generates fourteen methods: `list` · `listIds` · `find` · `findId` · `pick` · `pickId` · `exists` ·
554
616
  `count` · `insight` · `query` · **`remove`** · **`removeOne`** · **`update`** · **`updateOne`**. The last four
555
617
  are query-level writes — one atomic UPDATE, **no hooks**, and therefore no `_postRemove` and no cascade. Use
@@ -587,7 +649,7 @@ Full contract — relevance ordering, `columns` / `weights`: `get_guideline` wit
587
649
  - 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
650
  - 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
651
  - 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.
652
+ - **`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
653
 
592
654
  ### Adapters — `adapt()` And `plug()`
593
655
 
@@ -662,10 +724,14 @@ it.
662
724
  (RFC 7591; Cursor's `cursor://` redirect admitted) plus the consent page `libs/shared/page/oauth/consent` — and
663
725
  names itself as the issuer, so `/mcp` demands a bearer token, verifies its signature, and refuses one carrying no
664
726
  `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
727
+ judge it unchanged. The tokens this built-in server mints carry no scope; `auth.scopes` / `AKAN_MCP_SCOPES` are for
728
+ tokens an external issuer mints, and declared beside this server they refuse every one of its tokens with 403
729
+ `insufficient_scope`. Per-app knobs live in `env.server.*` under `oauth` (`consentPath` and
730
+ `signinPath` with the basePath, `clients`, `dynamicRegistration`, `allowedRedirectSchemes`, `accessTokenSeconds`,
731
+ `clientIdMetadata`, `enabled`, plus `issuer` / `resource` — set those when a tunnel or an edge makes the derived
732
+ 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
733
+ `security.jwtSecret`) is mandatory whenever `operationMode` is not `local`, because the derived fallback is
734
+ forgeable from three public names; `AKAN_ALLOW_DERIVED_JWT_SECRET=1` accepts that risk explicitly. A grant is revoked
669
735
  whole — `POST /oauth/revoke` (RFC 7009) by the client, `fetch.revokeOAuthConnection(sessionId)` by the account's
670
736
  owner, `fetch.listOAuthConnections()` to see them — and a revoked access token is refused at its next call.
671
737
  - **A person may, a model may not.** An agent's token names its `client_id` and `aud`; a browser session has
@@ -714,9 +780,12 @@ what every developer must know even when not building one.
714
780
  every tool runs in the caller's own browser session, gated by guards and the approval card. Its guard is
715
781
  `AgentRelayAccess`, which refuses every call until the app names a guard of its own —
716
782
  `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)`.
783
+ (`option.setLlm({ apiKey, model, host })`). Two adaptors ship, one per wire: `OpenaiLlm` is the default and
784
+ speaks the chat-completions dialect to whatever `host` names — OpenAI, DeepSeek, Groq, OpenRouter, Ollama — and
785
+ `AnthropicLlm` speaks the Messages API and reads a PDF as well as a picture. Both require `model`. A provider
786
+ neither covers is an `adapt()` class in `srvkit/` implementing `LlmAdaptor`, applied with
787
+ `option.applyAdaptor(LlmAdaptorRole, TheClass)`; whatever settings it needs beyond `LlmOption` ride `setLlm`
788
+ too, read back with `use<MyLlmOption>()`.
720
789
  - **Declare the tool beside the control that already does it**, never as a separate surface:
721
790
  `st.tool("x").desc("…").arg("id", ID).exec(fn)` returns the callable to hand to `onClick` — one handler for the
722
791
  person and the agent. Publish it only where the screen already renders the control; a falsy name declares the
@@ -779,7 +848,8 @@ Full contract — the trigger-maintained mirror, tokenizer changes, `AKAN_SEARCH
779
848
  backstop, including for a `text` field *underneath* one of those. Do not work around either.
780
849
  - The role works on a relation (`image: field(File, { text: "thumb" })`) and on an array; an array of objects
781
850
  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.
851
+ - Search runs in every database mode — fts5 on SQLite/libSQL, a weighted `tsvector` (`pg_trgm` for `trigram`) on
852
+ Postgres. The same text matches the same documents; only the order may differ on Postgres.
783
853
 
784
854
  ### Image & File Fields
785
855
 
@@ -828,10 +898,17 @@ shape, so `cascade` never means "related" — it means one of exactly these:
828
898
  - Dynamic segments use `[id]`; route groups use directories like `(user)`, `(public)`, `(tab)`, or `(detail)`.
829
899
  - **A route file exports one thing**: `export default page()…render(…)` for a page, `layout()…render(…)` for a
830
900
  `_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.
901
+ is a stage of the chain — `.param()`, `.search()`, `.config()`, `.head()`, `.loading()`; `.notFound()` /
902
+ `.error()` on a layout; `.fonts()` / `.theme()` / `.manifest()` / `.reconnect()` / `.wsConnect()` /
903
+ `.layoutStyle()` on the root layout; `.prompt()` on a page. A named export beside the chain, or `page()` in a
904
+ `_layout.tsx`, fails the build. `.head()` takes JSX — `<title>`, `<meta>`, `<link>` — or a function of the
905
+ route's args returning it; there is no metadata object, and analytics is the app's own script, not a stage.
906
+ - **A CSR bundle (a native app, `?csr=true`) never shows a root layout's own markup.** Pages render into the
907
+ frame, and what the root layout draws around `{children}` stays in a hidden container, so its wrappers cannot
908
+ stack a second screen beside the frame. Its components still mount and their effects run (`<Auth.User />` works).
909
+ Anything a person must see from a root layout — an overlay, a gate, a banner — renders through
910
+ `createPortal(…, document.body)`, as `<Agent.Chat />` and the dialogs already do. A non-root `_layout.tsx` has no
911
+ such limit.
835
912
  - **A page names every `[x]` segment of its path with `.param("x", Type)`** and reads a query key only through
836
913
  `.search("k", Type)`. Values arrive typed — `ID`/`String` → string, `Int`/`Float` → number, `Boolean`, `Date` →
837
914
  Dayjs, an `enumOf` class → its union, `[T]` → array — a path value the type refuses answers not-found, and a
@@ -874,10 +951,10 @@ export default page()
874
951
 
875
952
  ## Akan Sync Conventions (`apps/**`, `libs/**`)
876
953
 
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`.
954
+ - `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`.
955
+ - `apps/<appName>` root may only contain these folders: `.akan`, `common`, `env`, `lib`, `native`, `page`, `plugin`, `private`, `public`, `script`, `secrets`, `srvkit`, `ui`, `webkit`.
879
956
  - `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.
957
+ - `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
958
  - Both allowlists have one source — `pkgs/@akanjs/devkit/workspaceLayout.ts`. `akan sync` (error), `akan doctor`
882
959
  (diagnostic), and `akan quality scan` (warning) all read it, so add a new root entry there and mirror it into this
883
960
  list, never into one of the three call sites.
@@ -901,12 +978,18 @@ export default page()
901
978
  | Folder | Admission test | Naming |
902
979
  |---|---|---|
903
980
  | `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 |
981
+ | `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
982
  | `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`) |
983
+ | `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
984
  | `plugin/` | build- or CLI-time `AkanPlugin` | `<name>.plugin.ts`, registered in `akan.config.ts` |
985
+ | `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
986
 
909
987
  - Hooks return a named object of async closures, never a tuple.
988
+ - A `native/` plugin's page API is imported by a `webkit/` hook (`../native/<id>/src`) and nothing else; pages and
989
+ components call the hook, the same way they reach `akanjs/client/native`.
990
+ - `ui/` is the presentation layer — markup and the looks it is built from — so a recipe lives there although it is
991
+ neither a component nor a hook. Not `webkit/`: its hooks are `"use client"`, the opposite signal for a function
992
+ server components call. Not `common/`: it cannot import `akanjs/ui`, where `recipe` / `tv` come from.
910
993
  - `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
994
  - 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
995
  - `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 +1002,15 @@ when two shapes disagree.
919
1002
  - `children: any` — newer files use `children: ReactNode`.
920
1003
  - Hard-coded API keys or secrets in source; they belong in `option.ts` or env.
921
1004
  - 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
1005
  - Stale `// TODO: Implement …` comments above implemented methods.
924
1006
  - `{cond && <X/>}` in JSX, hard-coded Korean bypassing `l()`, and `window.alert(...)` for user feedback.
925
1007
  - Bare `/* eslint-disable */` blocks — use `// biome-ignore lint/<rule>: <why>`.
926
1008
  - Raw palette grays such as `text-gray-400` instead of the semantic tokens (`text-foreground/70`).
927
1009
 
1010
+ A store calling `(this as unknown as RootStore).<action>()` or `.set({ … })` over
1011
+ `import type { RootStore } from "../st"` is **not** one of these — it is the sanctioned way to reach another store's
1012
+ actions and state (see `<model>.store.ts` above).
1013
+
928
1014
  ## Secrets And Env Safety (`.env`, `infra/**`, `*secret*`, `*credential*`)
929
1015
 
930
1016
  - Never print, summarize, commit, or expose real secret values, credentials, tokens, private keys, or `.env` contents.
@@ -943,7 +1029,9 @@ when two shapes disagree.
943
1029
  selected one's log beside it. `Tab` walks every row and `1`-`9` jump to that app (the rail shows each
944
1030
  app's digit), `↑↓` scroll (shift for a page, `G` back to following),
945
1031
  `/` 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.
1032
+ `r` restarts that app, `q` quits. Under `--share`, `s` copies the public URL — the selected app's, or every
1033
+ app's from the merged row — and the header carries it ahead of the local one, because the line `--share`
1034
+ printed before the view took the terminal is gone by its first repaint.
947
1035
  **`--plain` prints prefixed interleaved lines instead**, and a pipe, a redirect or an unsized terminal
948
1036
  downgrades to that on its own.
949
1037
  - **A supervised session writes `local/apps/<app>/runtime/dev.log`, and that path is how a log gets handed over.**
@@ -954,11 +1042,13 @@ when two shapes disagree.
954
1042
  other file. The previous session is kept beside it as `dev.prev.log`. Hand somebody — or an agent — the
955
1043
  path rather than a paste: it costs one line instead of a transcript, and it can be re-read after a fix.
956
1044
  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.
1045
+ - **How many apps boot at once is the machine's answer, not a constant.** A cold boot is an app's RSS peak
1046
+ (~1.8GB per app, most of it the build worker — the base build, then the SSR registry's), so the default wave
1047
+ is what memory and cores allow — half the memory budget divided by that peak, and one app per four cores —
1048
+ which boots a laptop's apps together and still staggers them inside a small container. The session says
1049
+ which in one line. The next wave starts once an app serves and its boot builds have settled, or 30 seconds
1050
+ after it serves, while the app shows as ready — and `--open` opens it — as soon as it serves.
1051
+ `--concurrency <n>` overrides the wave size, and `AKAN_MEMORY_LIMIT` lowers the budget it derives from.
962
1052
  - `--kill` frees the dev ports first — it resolves each port's listener, walks up to the top of that akan dev
963
1053
  tree and signals it, so another checkout's server or a stale orphan on the same port is reclaimed. A holder
964
1054
  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