@akanjs/cli 3.0.0-alpha.7 → 3.0.0-alpha.70

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 (119) hide show
  1. package/.build-stamp +1 -1
  2. package/README.ko.md +1 -1
  3. package/README.md +1 -1
  4. package/{abstractCompactor-e5yq60qz.js → abstractCompactor-20e0cwbs.js} +2 -2
  5. package/{agent.command-pggr92jj.js → agent.command-964bf8qg.js} +9 -7
  6. package/{application.command-692yap1d.js → application.command-n45n18sc.js} +8 -6
  7. package/{applicationBuildRunner-ha3y8ew8.js → applicationBuildRunner-af0wtspv.js} +19 -14
  8. package/{applicationReleasePackager-gg8ccbeh.js → applicationReleasePackager-93nzdfsy.js} +1 -1
  9. package/buildBatch.proc.js +7 -5
  10. package/{capacitorApp-p0z5yab2.js → capacitorApp-vnvnvdhf.js} +29 -27
  11. package/{cloud.command-wqfsyzv2.js → cloud.command-kx1bh5yq.js} +12 -10
  12. package/{context.command-9bxtdn0w.js → context.command-g1a65x6j.js} +17 -15
  13. package/{dependencyScanner-f6grrep3.js → dependencyScanner-rbs2407b.js} +1 -1
  14. package/getDirname-ka11hm7e.js +8 -0
  15. package/{guideline.command-nfqahxex.js → guideline.command-xphyhvwh.js} +8 -6
  16. package/guidelines/agentRule/agentRule.generate.json +31 -0
  17. package/guidelines/agentRule/agentRule.instruction.md +525 -0
  18. package/guidelines/conventions/conventions.generate.json +25 -0
  19. package/guidelines/conventions/conventions.instruction.md +1020 -0
  20. package/guidelines/cssRule/cssRule.instruction.md +7 -0
  21. package/guidelines/framework/framework.instruction.md +1 -0
  22. package/guidelines/mcpRule/mcpRule.generate.json +25 -0
  23. package/guidelines/mcpRule/mcpRule.instruction.md +220 -0
  24. package/guidelines/workspaceOnboarding/workspaceOnboarding.generate.json +24 -0
  25. package/guidelines/workspaceOnboarding/workspaceOnboarding.instruction.md +638 -0
  26. package/incrementalBuilder.proc.js +8 -6
  27. package/{index-azv0xjb5.js → index-0ngvvgry.js} +8 -4
  28. package/index-1577bej2.js +7 -0
  29. package/{index-p3f4q532.js → index-4rw4qhe6.js} +14 -13
  30. package/{index-bjpxzr6s.js → index-5vy2jzar.js} +1 -1
  31. package/{index-js368gk9.js → index-8gybxemv.js} +6 -6
  32. package/{index-qb61h0k7.js → index-abyjt72y.js} +333 -540
  33. package/{index-4hek879p.js → index-bqqts262.js} +1 -1
  34. package/{index-6rhabeev.js → index-e31ghfp5.js} +5 -3
  35. package/index-f39512t6.js +255 -0
  36. package/{index-5pmx7bkg.js → index-fahkq20v.js} +3 -3
  37. package/{index-xhgtkekh.js → index-hee7egtm.js} +1 -1
  38. package/{index-7myvn0f0.js → index-j37qq1f2.js} +1 -1
  39. package/{index-5dw57f0d.js → index-mkn5qagt.js} +14 -14
  40. package/{index-ztpw544k.js → index-nj3w0r34.js} +3 -3
  41. package/{index-vg0sj9nc.js → index-nkwmnqwn.js} +257 -97
  42. package/{index-a1e0j63x.js → index-p681w089.js} +15 -7
  43. package/{index-j9cxndc0.js → index-qnv0aqmm.js} +9 -6
  44. package/{index-w6wbb612.js → index-qt4nfr0n.js} +13 -13
  45. package/{index-bz2sfzzt.js → index-rb65xnw9.js} +4 -4
  46. package/{index-gex7c8kd.js → index-rbnpe8cp.js} +37 -6
  47. package/{index-0cj2zxbm.js → index-sh01r3pm.js} +1 -1
  48. package/{index-gmy45vz2.js → index-v5fdxdre.js} +4 -4
  49. package/{index-mssqn2sq.js → index-va15r8h5.js} +54 -15
  50. package/{index-vtj3zps1.js → index-yg3hxk2k.js} +8 -8
  51. package/{index-2c3e2xh8.js → index-ytaqtc04.js} +1 -1
  52. package/index.js +21 -19
  53. package/{library.command-md9715jz.js → library.command-rfpd27yv.js} +6 -4
  54. package/{localRegistry.command-exkbamyj.js → localRegistry.command-pyxfe0fs.js} +11 -9
  55. package/{module.command-w5aaddxk.js → module.command-qhaxad0p.js} +10 -8
  56. package/{package.command-yfkrcy0z.js → package.command-2k22q9nt.js} +6 -4
  57. package/package.json +4 -5
  58. package/{page.command-echbavp4.js → page.command-ymk9dqh4.js} +6 -4
  59. package/{primitive.command-9fbhspw2.js → primitive.command-9k45zpc1.js} +11 -9
  60. package/{quality.command-3pnt9yep.js → quality.command-bf6vkxe5.js} +192 -97
  61. package/{recipeScanner-tghkzrrt.js → recipeScanner-zrzev25x.js} +3 -3
  62. package/{repair.command-carj4vax.js → repair.command-egvvm6xf.js} +8 -6
  63. package/{routeSourceValidator-e4jmbfcx.js → routeSourceValidator-7fycdqyv.js} +1 -1
  64. package/{scalar.command-51ryzypc.js → scalar.command-bt1facyj.js} +9 -7
  65. package/templates/app/env/env.client.debug.ts.template +1 -5
  66. package/templates/app/env/env.client.develop.ts.template +1 -5
  67. package/templates/app/env/env.client.local.ts.template +1 -5
  68. package/templates/app/env/env.client.main.ts.template +1 -5
  69. package/templates/app/env/env.client.testing.ts.template +1 -5
  70. package/templates/app/env/env.client.type.ts +2 -6
  71. package/templates/app/env/env.server.debug.ts.template +1 -5
  72. package/templates/app/env/env.server.develop.ts.template +1 -5
  73. package/templates/app/env/env.server.local.ts.template +1 -5
  74. package/templates/app/env/env.server.main.ts.template +1 -5
  75. package/templates/app/main.ts +3 -1
  76. package/templates/app/page/styles.css.template +3 -3
  77. package/templates/appSample/srvkit/AuthGuard.ts +9 -5
  78. package/templates/appSample/srvkit/SessionInternalArg.ts +4 -3
  79. package/templates/crudPages/[__model__Id]/edit/page.tsx +1 -1
  80. package/templates/crudPages/new/page.tsx +1 -1
  81. package/templates/crudSinglePage/page.tsx +1 -1
  82. package/templates/env/_env.server.type.ts +7 -5
  83. package/templates/lib/db.ts +1 -1
  84. package/templates/lib/dict.ts +1 -1
  85. package/templates/lib/sig.ts +1 -1
  86. package/templates/lib/srv.ts +6 -2
  87. package/templates/module/__Model__.Util.tsx +1 -1
  88. package/templates/module/__Model__.Zone.tsx +3 -5
  89. package/templates/workspaceRoot/AGENTS.md.template +0 -972
  90. package/templates/workspaceRoot/CLAUDE.md.template +19 -0
  91. package/templates/workspaceRoot/biome.json.template +4 -244
  92. package/templates/workspaceRoot/docs/AI-DEVELOPMENT.md.template +10 -14
  93. package/templates/workspaceRoot/docs/GENERATED.md.template +12 -10
  94. package/templates/workspaceRoot/package.json.template +1 -1
  95. package/{typeChecker-kravn7ns.js → typeChecker-g6bh0xfv.js} +1 -1
  96. package/typecheck.proc.js +1 -1
  97. package/{workflow.command-91wmvm8k.js → workflow.command-v1n3wg7v.js} +16 -14
  98. package/{workspace.command-wa10mfd9.js → workspace.command-yvvqvhzz.js} +71 -52
  99. package/templates/workspaceRoot/.cursor/rules/akan-page-routing.mdc.template +0 -43
  100. package/templates/workspaceRoot/.cursor/rules/akan-sync-conventions.mdc.template +0 -34
  101. package/templates/workspaceRoot/.cursor/rules/application-test-commands.mdc.template +0 -16
  102. package/templates/workspaceRoot/.cursor/rules/change-scope.mdc.template +0 -12
  103. package/templates/workspaceRoot/.cursor/rules/client-server-boundaries.mdc.template +0 -18
  104. package/templates/workspaceRoot/.cursor/rules/coding-comments.mdc.template +0 -25
  105. package/templates/workspaceRoot/.cursor/rules/coding-style.mdc.template +0 -44
  106. package/templates/workspaceRoot/.cursor/rules/domain-module-conventions.mdc.template +0 -17
  107. package/templates/workspaceRoot/.cursor/rules/format-lint.mdc.template +0 -12
  108. package/templates/workspaceRoot/.cursor/rules/lint-enforced-rules.mdc.template +0 -19
  109. package/templates/workspaceRoot/.cursor/rules/module-file-playbook.mdc.template +0 -62
  110. package/templates/workspaceRoot/.cursor/rules/naming-conventions.mdc.template +0 -15
  111. package/templates/workspaceRoot/.cursor/rules/react-component-style.mdc.template +0 -21
  112. package/templates/workspaceRoot/.cursor/rules/repo-overview.mdc.template +0 -17
  113. package/templates/workspaceRoot/.cursor/rules/scalar-modeling.mdc.template +0 -99
  114. package/templates/workspaceRoot/.cursor/rules/secrets-and-env.mdc.template +0 -13
  115. package/templates/workspaceRoot/.cursor/rules/service-signal-conventions.mdc.template +0 -83
  116. package/templates/workspaceRoot/.cursor/rules/srvkit-adapters-and-guards.mdc.template +0 -50
  117. package/templates/workspaceRoot/.cursor/rules/ssr-first.mdc.template +0 -59
  118. package/templates/workspaceRoot/.cursor/rules/typescript-imports.mdc.template +0 -16
  119. /package/{index-r24hmh0q.js → index-yb2wsc7f.js} +0 -0
@@ -0,0 +1,1020 @@
1
+ ## Repo Overview
2
+
3
+ - Akan.js is a full-stack TypeScript framework for building all-stack applications at once.
4
+ - Write one line and deploy across web, app, server, database, and infrastructure.
5
+ - Akan prioritizes actual business code by abstracting technical implementation details as much as possible.
6
+ - The goal is minimal code, high performance, and type-safe services that can deploy to web, mobile, server, and DB infrastructure together.
7
+ - This is a Bun-first Akan.js monorepo.
8
+ - Main top-level areas are `apps/`, `libs/`, `pkgs/`, and `infra/`.
9
+ - `apps/<app>` contains app-level pages, domain code, UI, env files, and `akan.config.ts`.
10
+ - `libs/*` contains shared domain and utility libraries.
11
+ - `pkgs/akanjs/*` contains framework, runtime, and tooling facets published through the single `akanjs` package. Prefer extending existing Akan facets before adding new framework-level patterns.
12
+ - `infra/` contains Helm, deployment templates, edge configs, and Jenkins env/secret scripts.
13
+
14
+ ## Change Scope
15
+
16
+ - Keep edits scoped to the requested task and the directly related files.
17
+ - Do not reformat unrelated files or rewrite nearby code just for style.
18
+ - Do not revert or overwrite unrelated user changes in a dirty working tree.
19
+ - Prefer established nearby patterns over introducing a new abstraction.
20
+ - Add new abstractions only when they remove real duplication or match an existing project pattern.
21
+
22
+ ## Lint-Enforced Rules (These Break The Build)
23
+
24
+ Enforced by `biome.json`, which extends `@akanjs/devkit/biome.base.json` — that file scopes every grit plugin in
25
+ `@akanjs/devkit/lint/` to the paths it applies to. Several rules produce output that looks wrong; do not "fix" it
26
+ back.
27
+
28
+ - **Never hand-order Tailwind classes.** `nursery/useSortedClasses` is an error and also sorts the string
29
+ arguments to `cn()`. Sorter output such as `font-bold text-2xl text-foreground` or
30
+ `border-foreground/5 border-t` is correct. Write the classes in any order, run the formatter, leave the result.
31
+ - **Stay inside the color vocabulary.** Vocabulary closure strips the raw Tailwind palette, so these render as
32
+ no CSS and fail lint (`no-raw-palette-class.grit`, `no-arbitrary-color.grit`, `no-daisyui-legacy-class.grit`, `no-inline-color.grit`,
33
+ `no-interpolated-arbitrary-class.grit`): raw palette classes (`bg-blue-500`), arbitrary color values
34
+ (`bg-[#3b82f6]`), an arbitrary class built by interpolation, daisyUI legacy classes (`btn-primary`, `card-body`)
35
+ and its dropped color slots (`bg-base-100`, `text-base-content`, `text-primary-content`, `bg-error` — use
36
+ `background`/`muted`/`border`, `foreground`, `<color>-foreground`, `destructive`), and color literals in
37
+ `style={{...}}`. Use semantic tokens (`bg-primary`, `text-foreground/70`). A legitimate
38
+ fixed color (OS-chrome mockups, data-viz) takes a `// biome-ignore lint/plugin: <reason>` with the reason
39
+ spelled out.
40
+ - **Never `throw new Error`.** Throw `new Err("<module>.error.<key>")` and register the key as `[en, ko]` in that
41
+ module's dictionary `.error({})`. Import `Err` from `"../dict"` on the server and from `"@libs/<lib>/client"` or
42
+ `"@apps/<app>/client"` in UI. `no-throw-raw-error.grit` exempts tests, `*.constant.ts`, `common/**`, and
43
+ `env/**` — `common/` and `env/` have no legal `Err` import path, so keep throwing code out of them.
44
+ - **Never import a third-party package** from `page/**`, from any barrel, or from any
45
+ `*.{constant,dictionary,document,service,signal,store}.ts` / `*.{Template,Unit,Util,View,Zone}.tsx`
46
+ (`no-import-external-library.grit`). Re-export the symbol through a lib first. One-line re-export shims in a lib's
47
+ `common/`, `webkit/`, or `ui/` exist for exactly this reason — they are load-bearing, not cruft. Do not delete
48
+ them.
49
+ - **`#private` is banned in exactly four file suffixes:** `*.constant.ts`, `*.document.ts`, `*.service.ts`, and
50
+ `*.store.ts` (`no-js-private-class-method.grit`). The rule is scoped by file path, not by class shape, so
51
+ `#private` remains the house style everywhere under `srvkit/`, including `adapt()` adapter classes.
52
+ - **No `console.log` / `console.debug`.** Only `assert`, `error`, `info`, and `warn` are allowed. Server code uses
53
+ the injected `this.logger.*` or `new Logger("ClassName")`.
54
+ - **Never write a `//!` marker in browser-reachable code** — `ui/`, `webkit/`, `common/`, `page/**/*.tsx`,
55
+ `*.constant.ts`, `*.store.ts`, and the five module component suffixes (`no-bang-comment-in-client.grit`). Bun
56
+ classifies `//!` and `/*!` as legal comments and keeps them through minification, so the note ships to every
57
+ visitor. Use `// FIXME:` there; `//!` stays legal in server, `srvkit/`, and CLI files.
58
+ - **Never return a value from a store action** (`no-return-in-store-action.grit`). Every method of a `store(...)`
59
+ class dispatches through `st.do.<action>()`, which is typed `void` / `Promise<void>`, so the value is
60
+ unreachable — write it into state with `this.set({ ... })`. A bare `return;` guard, a `return` inside a nested
61
+ callback, a getter, and a `static` helper are all still fine.
62
+ - **Never redeclare a generated CRUD endpoint name** in `*.signal.ts` (`no-redeclare-predefined-endpoint.grit`).
63
+ - **Never type a `*.Util.tsx` / `*.Zone.tsx` prop as a `cnst` model** (`no-model-type-in-util-zone.grit`). Those two
64
+ roles are always client components, so a `cnst.Banner` / `cnst.LightBanner` prop is a class instance the server has
65
+ to hand across the boundary; take `bannerId: string` and read the model from the store instead. **Only prop
66
+ positions are read** — a `*Props` interface or type alias, and the inline object type on the component's own
67
+ parameter — and only shapes that are actually an instance are flagged. Exempt: an indexed enum access
68
+ (`cnst.<Enum>["value"]`, a string union), a `ClientInit` / `ClientView` / `ClientEdit` type argument (plain
69
+ `GetStateObject<…>` data), a `ModelsProps<…>` type argument, a function-typed prop, and any `cnst` type that
70
+ never leaves the file. `ModelProps<"setting", cnst.LightSetting>` and any other indexed access
71
+ (`cnst.Banner["image"]` is a `File`) stay flagged.
72
+ - **Never wrap a form setter in a pass-through arrow** (`no-unpublished-form-setter.grit`).
73
+ `onChange={(type) => st.do.setTypeOnTicket(type)}` runs identically to `onChange={st.do.setTypeOnTicket}`, but
74
+ the arrow is a fresh anonymous closure, so the control emits no `data-akan-action` and publishes no agent tool
75
+ for the field — a silent failure in two lines that read the same. A wrapper that transforms the value, adds a
76
+ statement, or writes a nested path with `writeOnX` stays legal; publish that one with an explicit `st.tool`.
77
+ Scoped to `{apps,libs}/**/*.tsx`, and a typed parameter is not matched, so it under-reports rather than
78
+ misfiring.
79
+ - **No deep imports past a barrel** (`no-deep-internal-import.grit`). Cross-module constant references such as
80
+ `../map/map.constant` are the sanctioned exception.
81
+ - **Never import across the client/server boundary.** Client files (`ui/`, `webkit/`, `page/`, `*.store.ts`, every
82
+ `.tsx`) may not import a `*.document.ts` / `*.dictionary.ts` / `*.service.ts` / `*.signal.ts`, `srvkit/`, a
83
+ package `server` entrypoint, or the `db` / `srv` / `sig` / `dict` / `option` / `useServer` barrels
84
+ (`no-import-server-in-client.grit`). Server files (those four suffixes plus `srvkit/`) may not import a
85
+ `*.store.ts`, a module component, `ui/`, `webkit/`, a package `client` entrypoint, or the `st` / `store` /
86
+ `useClient` barrels (`no-import-client-in-server.grit`). Shared files — `common/` and `*.constant.ts` — are held
87
+ to **both**, so they reach neither side. `import type` is erased before bundling and stays legal in every
88
+ direction; a mixed value-and-type import is not exempt. Scoped to `apps/**` and `libs/**`: `pkgs/akanjs/**`
89
+ implements the boundary and is where the two graphs legitimately meet.
90
+ - **Server-component discipline** is enforced on `page/**`, `*.Unit.tsx`, and `*.View.tsx`
91
+ (`no-import-client-functions.grit`, `no-use-client-in-server.grit`, `non-scalar-props-restricted.grit`).
92
+ - **Never write an async component outside `page/`** (`no-async-component-in-ui.grit`, scoped to
93
+ `{apps,libs}/**/ui/**/*.tsx`). React has no async client component, so a `ui/` component that awaits breaks as
94
+ soon as a client parent renders it, and the load drops below the route, which could have started it before the
95
+ first byte. Await in the page — or hand an unawaited `fetch.*` to a `Zone` as an `init` / `view` prop — and take
96
+ the resolved data as a prop. A component is a PascalCase binding whose own initializer is `async`, so an async
97
+ handler declared inside a synchronous component, an inline `onClick={async () => …}`, and
98
+ `lazy(async () => import(…))` are all untouched.
99
+ - **Never call `fetch.init*` from a client file** (`no-init-fetch-in-client.grit`). `fetch.init<Model><Suffix>` and
100
+ `fetch.get<Model>Init<Suffix>` compose the slice's list and insight queries into the hydration snapshot that
101
+ `Load.Units` / `Load.View` seed the store from. From a route it resolves before the first byte; after hydration it
102
+ is two extra round-trips for a shell the browser already painted, landing in a value nothing reads. Load it in the
103
+ page and pass the result down as an `init` prop — or hand the unawaited promise across — and reload from the client
104
+ through the generated `st.do.init<Model><Suffix>()`. The gate is the file: the real `"use client"` directive (the
105
+ same text as a string literal or inside sample code is not one) plus `*.store.ts`, which is client-only by role and
106
+ carries no directive. The name is matched by shape, since a lint rule cannot know which slices exist: the
107
+ generated one always carries two capital-led segments (`init` + `Capitalize<refName>` + `Capitalize<suffix>`), so
108
+ a hand-written `initPayment` is out and `initializeSomething` was never in. `view` / `edit` hydrate alike but are
109
+ left unmatched — `edit<X>` is a plausible custom endpoint name.
110
+ - `noArrayIndexKey` and `useExhaustiveDependencies` are **off** on purpose: `key={idx}` for embedded scalars and
111
+ short dependency arrays are intentional, not oversights.
112
+ - **A grit plugin diagnostic is suppressed as `lint/plugin`, not `plugin`** — `// biome-ignore lint/plugin: <reason>`
113
+ for one line, `// biome-ignore-all lint/plugin: <reason>` for a file. The bare `// biome-ignore plugin:` form Biome's
114
+ own category name suggests does nothing. Suppress a plugin only where the rule is genuinely wrong for the file, and
115
+ say why: the module-convention plugins (`no-import-external-library`, `no-deep-internal-import`, the store/signal
116
+ ones) apply to `apps/**` and `libs/**` only, so a plain package under `pkgs/` never needs the escape hatch.
117
+ - **`biome.json` is strict JSON — a comment in it breaks config resolution.** Biome does not report the parse
118
+ error; it falls back to discovery and aborts on whatever nested config the walk finds. Rename the file to
119
+ `biome.jsonc` to document a disabled rule; `akan lint` pins the config path either way, so it reports the parse
120
+ error on the offending line.
121
+ - **`akan lint` prints up to 200 diagnostics** (`--max-diagnostics <n>`, `0` for no limit). Biome's own default is 20
122
+ with no count, which reads as progress when the mix of findings merely changed.
123
+
124
+ ## Coding Style (`**/*.{ts,tsx}`)
125
+
126
+ - For large units of work, prefer declaring a class and running the flow through an instance instead of scattering many standalone functions.
127
+ - Prefer class methods or `static` methods over unrelated top-level helper functions when the logic belongs to a class-level workflow.
128
+ - Prefer ECMAScript `#private` fields and methods over TypeScript `private`, except in the four suffixes where `#private` is lint-banned (`*.constant.ts`, `*.document.ts`, `*.service.ts`, `*.store.ts`) — those use TypeScript `private`. `#private` is the house style under `srvkit/`, including `adapt()` classes.
129
+ - In files that declare a class, avoid top-level functions or variables when they can reasonably live inside the class.
130
+ - Prefer `const` function expressions over `function` declarations unless hoisting, overloads, generators, or framework conventions make `function` the better fit.
131
+ - Prefer declaring only one class per file; split the file when two or more class declarations are needed.
132
+ - For class-centered modules, prefer noun-style filenames that match the primary class name, such as `RouteClientBuilder.ts`, instead of verb-style wrapper filenames like `buildRouteClient.ts`.
133
+ - Avoid keeping exported functions that only instantiate a class and immediately call one method. Prefer migrating callers to instantiate the class directly.
134
+ - Except for React component files or convention files, TypeScript filenames should use camelCase.
135
+ - In React components, keep one-off `className` strings inline. Only extract class name constants when the class is reused, conditionally composed, or too large to read comfortably in JSX.
136
+
137
+ ### File Size And Duplication
138
+
139
+ - Keep files small. The house median is well under 50 lines; split a component before it reaches ~150 lines rather than adding section comments.
140
+ - Ship every scaffold file even when it is empty — `export class XInternal extends internal(srv.x, () => ({})) {}`, empty dictionary stages, the `// state` / `// action` markers in an empty store. They mark where things go.
141
+ - Never add a sibling helper file inside `lib/<model>/`. Helpers go to `common/`, `webkit/`, `srvkit/`, or `ui/`.
142
+ - Prefer duplication to premature abstraction at the leaf. Near-identical sibling modules and per-vendor pages are deliberately copied, not parameterised. Copy the file and change the literals; share enums by import only.
143
+
144
+ ### TypeScript Shape
145
+
146
+ - Use `interface` for object shapes and `type` only for unions and aliases.
147
+ - Declare `interface <ComponentName>Props` immediately above the component with no blank line between, and put `className?: string` first. Name it for the component (`CardProps`, `WorldProps`), never for the model. Do not export it unless a sibling imports it.
148
+ - Never use a non-null assertion. Narrow with `?.`, an early return, or a type predicate such as `.filter((id): id is string => !!id)`.
149
+ - Escape with `as unknown as T`. Never `as any`.
150
+ - Never annotate a component's return type. Annotate a helper only when the return is a union, a tuple, or a type predicate.
151
+ - Use `as const` on every `enumOf(...)` array, every `via(Model, [...] as const, …)` Light tuple, and every module-scope lookup map. Never use the TypeScript `enum` keyword.
152
+ - Async functions carry no `Async` suffix.
153
+
154
+ ### Test Code
155
+
156
+ - Write TypeScript tests with Bun's test runner and import `describe`, `expect`, and `test` from `bun:test`.
157
+ - Keep tests colocated with the source they cover using `*.test.ts` or `*.spec.ts`, following the existing nearby pattern.
158
+ - Prefer focused behavior tests for public contracts and edge cases over implementation-detail assertions.
159
+ - Run package suites with `bun run akan test <pkg>` from the repo root, or `cd <pkg> && bun test --isolate`. Plain `bun test` without `--isolate` shares one global object across test files and fails dozens of tests from cross-file state pollution (`bunfig.toml` `[test] isolate` is not honored as of Bun 1.3), and running `bun test` from the repo root breaks subprocess stdio pipes.
160
+ - Split signal tests in two. `<model>.signal.spec.ts` holds reusable fixtures built on `sampleOf(cnst.XInput)` with explicit `Promise<cnst.X>` return types and **no assertions**. `<model>.signal.test.ts` holds the assertions: `describe("<Model> Signal")`, `let` fixtures at describe scope, one `beforeAll`, story-ordered `it`s, and negatives via `await expect(p).rejects.toThrow()`.
161
+ - `lib/user/user.signal.spec.ts` is the one place agent types are re-exported and re-typed; import `UserAgent` / `AdminAgent` from there rather than from the owning lib directly.
162
+ - A placeholder `it` with a descriptive title is an acceptable floor. Write a real suite when the behaviour is security-relevant.
163
+
164
+ ## Comments
165
+
166
+ Do not narrate code. Do document the thing the code cannot say. Both halves are the rule.
167
+
168
+ - Never add a comment that restates the identifier, the signature, or the control flow.
169
+ - Prefer clear names and structure so ordinary logic needs no explanation.
170
+ - Do not add JSDoc, section banners, or "why/how" comments for ordinary logic.
171
+ - Comment density tracks the layer, not the author: pages carry none, product `lib/` and `ui/` code stays under 1 %, and `srvkit/` adapters and `guards.ts` carry as much as the external constraints require.
172
+ - A comment is warranted for: a vendor spec or protocol quirk; an infrastructure constraint; a third-party library gotcha; security reasoning; why a rule that looks arbitrary is correct; a math derivation; a domain field's business meaning; a state transition above a document chain method; why an obvious alternative was rejected.
173
+ - In-code markers:
174
+ 1. `TODO` — unfinished work that must be tracked in-code
175
+ 2. `FIXME` — known broken or incorrect behavior that must be fixed
176
+ 3. `XXX` — dangerous / surprising hazard that a reader must not miss
177
+ 4. `//!` — disabled or must-fix code. **Server, `srvkit/`, and CLI files only.** Bun's bundler treats `//!`
178
+ (and `/*!`) as a legal comment and keeps it through minification, so in browser-reachable code the note
179
+ ships verbatim to every visitor. Use `// FIXME:` there instead; `no-bang-comment-in-client.grit` enforces it.
180
+ 5. `//?` — an explanatory aside
181
+ 6. `//*` — a design note
182
+ 7. Deletion caution — warn why removing a line or block would break something non-obvious
183
+ - Keep allowed comments one short line when possible.
184
+ - Every suppression carries a reason: `// biome-ignore lint/<rule>: <why>`. Never a bare disable block.
185
+ - Match nearby file style: if the surrounding code has few comments, keep it that way.
186
+
187
+ ## TypeScript And Imports (`**/*.{ts,tsx}`)
188
+
189
+ - Use Bun and ESM assumptions from the root `tsconfig.json`.
190
+ - Prefer path aliases over deep relative imports when crossing package boundaries.
191
+ - Use `akanjs/*` for framework facets, `@apps/*` for apps, `@libs/*` for shared libs, and `@contract/*` for contract code.
192
+ - Respect existing client/server entrypoints — `@libs/<lib>/client` / `@libs/<lib>/server` and `@apps/<app>/client` / `@apps/<app>/server`.
193
+ - Let Biome organize imports instead of manually reshuffling unrelated imports.
194
+ - Namespace the generated barrels in backend `.ts` files: `import * as cnst from "../cnst"`, `* as db`, `* as srv`. Use `import type * as srv` in services so the runtime graph stays lazy, and a value import in signals.
195
+ - In `.tsx` files use one flat named import from the package client path (`import { cnst, fetch, st, Ticket, usePage } from "@apps/<app>/client"`) — never a relative `../` import.
196
+
197
+ ## Formatting And Linting
198
+
199
+ - Use Biome as the formatter and linter.
200
+ - Format with `bun run akan lint <appName>` from the repo root.
201
+ - Keep formatting consistent with `biome.json`: 2-space indentation, 120 line width, and double quotes for JS/TS.
202
+ - Avoid adding new `console` usage except accepted methods such as `console.error`, `console.info`, and `console.warn`.
203
+ - Do not make broad formatting-only changes in unrelated files.
204
+
205
+ ## Client / Server Boundaries (`apps/**`, `libs/**`, `pkgs/akanjs/**`)
206
+
207
+ - Use `"use client";` at the top of client component files.
208
+ - Be careful when importing client-only code from page or layout modules.
209
+ - Keep page props serializable unless the existing route pattern clearly allows otherwise.
210
+ - 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.
211
+ - Preserve established domain file roles such as `.document.ts`, `.service.ts`, `.store.ts`, `.constant.ts`, and `.client.ts`.
212
+ - When unsure, inspect nearby files in the same app or package before introducing a new boundary pattern.
213
+
214
+ ## SSR First — Server Rendering Is The Default
215
+
216
+ Akan is SSR-first. Every JSX element that renders on the server ships as HTML and costs nothing to hydrate;
217
+ every element behind `"use client"` ships twice — as markup and as bundled JS that must re-run in the browser.
218
+ The boundary is not about which file *may* be client, it is about **how little** ends up on the client side.
219
+
220
+ **The default is server. `"use client"` is a cost you justify per component, not a habit.** A component earns the
221
+ directive only by using a client-only capability: a React hook, a JSX event handler, the store (`st.use.*` /
222
+ `st.do.*`), a browser global, or a client-only third-party package. Rendering markup, reading a param, calling
223
+ `l()`, and mapping over data are all server work.
224
+
225
+ Measure before and after with `akan quality ssr` (`--format json` for tooling). It prints the server render share
226
+ per app and lib — server-rendered JSX elements over total — and the SSR warnings below. Treat **50% server share
227
+ as the floor** for an app or lib and a **falling share as a regression**: if a change moves markup to the client,
228
+ say why in the PR or move it back.
229
+
230
+ ### What `akan quality ssr` Flags
231
+
232
+ | Rule | Means |
233
+ |---|---|
234
+ | `akan.ssr.unnecessary-use-client` | The directive is there but nothing in the file needs it. Delete it. |
235
+ | `akan.ssr.client-static-component` | A component in a client file renders real markup with zero client-only capability — pure server work sitting in the bundle. |
236
+ | `akan.ssr.client-static-markup` | A large subtree wraps one or two interactive touches. Split it: interaction stays client, markup goes server. |
237
+ | `akan.ssr.client-mount-load` | A `useEffect(…, [])` loads server data. The route can fetch it before the first byte. |
238
+ | `akan.ssr.module-missing-server-view` | A module renders only from `Template`/`Zone`/`Util` and has no `Unit`/`View` at all. |
239
+ | `akan.ssr.template-client-state` | A `Template` holds form state in `useState` instead of the store. |
240
+
241
+ A third-party client package or an `index_.tsx` `lazy()` boundary is a legitimate reason for the directive and is
242
+ not flagged. Interaction-driven `fetch.*` (a lookup inside `onClick`) is not flagged either — only mount-time loads
243
+ are, because those are the ones the server could have done.
244
+
245
+ ### Server-Side Implementation Playbook
246
+
247
+ 1. **Wrap the interaction, not the UI.** The smallest useful client component adds one behaviour and renders
248
+ `children` untouched, so the markup inside never reaches the bundle.
249
+ 2. **Split compound components so panels stay on the server.** `Tab` / `Tab.Menus` / `Tab.Menu` / `Tab.Panel` in
250
+ `akanjs/ui` is the shape: only the provider and menu hold state, and `<Tab.Panel>` renders children as-is.
251
+ Never one `"use client"` file with a mode `useState` and every panel body inlined.
252
+ 3. **Sync state instead of fetching it.** The route calls `fetch.initXInY(...)` / `fetch.viewX(...)` and passes
253
+ the result into a `Zone` as an `init` / `view` prop; `Load.Units` / `Load.View` hydrate the store from it.
254
+ Never a `useEffect(…, [])` that fetches on mount.
255
+ 4. **Push the boundary down to the leaf that needs it.** A store-reading `Zone` should hold zero markup and
256
+ delegate to a server `View`.
257
+ 5. **Hand the promise across, not the awaited value.** `ClientInit` / `ClientView` are `PromiseOrObject<T>`, so a
258
+ route may pass an unawaited `fetch.initX(...)` and `Load.*` resolves it behind a skeleton.
259
+ 6. **Use named `ReactNode` slots, not just `children`.** `Layout.Navbar` takes `title`, `back`, `left`, `right`,
260
+ and `children`, so a client shell composes server content in five places instead of absorbing it.
261
+ 7. **Let the server do the derived work.** Display and predicate logic belongs on `Light<Model>`; enum→class
262
+ lookups belong in a module-scope `as const` map.
263
+ 8. **Gate auth on the server.** `getSelf({ unauthorize: "/signin" })` in `_layout.tsx` redirects before any HTML
264
+ is sent.
265
+ 9. **Prefer CSS over client state for pure visibility.** A `data-*` attribute plus `group-data-[…]` variants, or
266
+ `<details>`/`<summary>`, keeps both branches server-rendered.
267
+ 10. **Keep the heavy island out of the first load.** A large client-only widget goes behind the
268
+ `ui/<Folder>/index_.tsx` + `lazy()` pair. `usePage()` and `l()` work in server components, so translation
269
+ never forces a boundary.
270
+
271
+ Full version with code, the `Tab` composition example, and a review checklist: `get_guideline` with `ssrRule`, or
272
+ `akan guideline show ssrRule`.
273
+
274
+ ## Web Surfaces — Building And Serving Without SSR/CSR
275
+
276
+ An app serves three things: the API, the SSR/RSC web renderer, and the CSR single-file bundle. The API is always
277
+ on; the other two are declared in `akan.config.ts` as **`web: true | false | { csr: boolean }`** and can be
278
+ narrowed again per deployment.
279
+
280
+ ```ts
281
+ const config: AppConfig = { web: { csr: false } }; // web without the mobile bundle
282
+ const config: AppConfig = { web: false }; // api only
283
+ ```
284
+
285
+ - **`web: { csr: false }`** drops the CSR build phase and the `/__csr` + `?csr=true` routes. The CSR bundle is
286
+ what the Capacitor mobile build ships, so a web-only deployment never needs it — and an app that declares a
287
+ `mobile` section is refused, because `akan build-ios` copies `dist/apps/<app>/csr/<target>.html` into the
288
+ native project.
289
+ - **`web: false`** is an API-only build: no base artifact, no pages or client bundles, no RSC worker
290
+ entrypoint, and no `public/` in the image (the web router's catch-all is its only reader). Nothing under
291
+ `page/` is served, including routes a lib contributed through `syncPageLibs`.
292
+ - **There is no CSR-without-SSR option, by type.** The CSR bundle inlines the stylesheet the SSR base artifact
293
+ compiles, so it would ship an unstyled app — the object form therefore carries only `csr`, and SSR goes off
294
+ only through the whole-surface `false`.
295
+ - **At runtime, `AKAN_SSR` and `AKAN_CSR` narrow further and never widen** — `false` or `0` turns one off, and
296
+ `AKAN_SSR=false` takes CSR with it for the same reason the option has no such pair. A surface the build left
297
+ out cannot be switched back on, and the boot log names what the process ended up serving. The generated
298
+ Dockerfile writes the build's own answer as the image default.
299
+ - **A build with no web artifact boots the API instead of crashing.** `WebRouter.create` returns `null` when
300
+ `.akan/artifact/base-artifact.json` is absent — an api-only build, or a workspace with no `page/` at all.
301
+ - `akan start` ignores `web` and keeps the whole dev surface: the incremental builder is also the file watcher,
302
+ so switching it off would take server-code HMR with it. It warns once when the config and the dev server
303
+ disagree.
304
+ - The saving is mostly the RSC worker, which is a **separate process per web-serving replica**. Measured on
305
+ `apps/akan` at boot plus one render: 350MB across 3 processes with SSR on, 120MB across 2 with
306
+ `AKAN_SSR=false`, and the built image goes 86MB → 6.2MB when the artifacts are left out of it too.
307
+ - **The generated image installs `ca-certificates` and `tzdata` and nothing else.** It used to carry the whole
308
+ Chromium runtime, ffmpeg, `build-essential`, `python3` and `redis` in every app's image whether or not the app
309
+ reached for any of them. An app that needs one declares it in `docker.preRuns` / `docker.postRuns`, which are
310
+ emitted around the `bun install` — that is the migration for a `puppeteer` or `ffmpeg` app.
311
+
312
+ ## The Process Model — Gateway And Solo
313
+
314
+ A container runs one process per replica, a gateway in front of them when there is more than one, and one RSC
315
+ worker per web-serving replica.
316
+
317
+ - **One traffic replica runs in the container's only process.** `AKAN_REPLICA=0,0,1` — the default, and what
318
+ every environment in `infra/app/values` sets — means there is nothing to balance and nothing to fan pubsub out
319
+ to, so `AkanApp` starts that replica in-process rather than spawning it. Measured on `apps/akan`: 28MB less RSS
320
+ and twice the requests per second, because every request used to cross a unix-socket proxy hop. Declare two or
321
+ more and the gateway is back, spawning and proxying them.
322
+ - **`AKAN_SOLO=false` forces the gateway** for a single replica. Like `AKAN_SSR`, the env only narrows — it
323
+ cannot fold a real gateway's replicas into one process. Passing `replica` to `new AkanApp(...)` also keeps the
324
+ gateway: code that states a topology is asking for the thing that serves it.
325
+ - **`akan start` always runs the gateway**, whatever the replica count. It is also the dev host's builder relay,
326
+ its crash page, and what holds the port across a child restart.
327
+ - **A batch-only replica (`0,1,0`) keeps the gateway too**, because a batch server never listens and the gateway
328
+ is then the only thing bound to answer `/_akan/app/health`.
329
+ - **The RSC worker is never folded in.** It runs under `--conditions react-server`, which resolves the same
330
+ module graph differently, so it cannot share a process with the server that renders client components.
331
+ - **A solo process answers `/_akan/app/health`, `/_akan/app/metrics` and `/_akan/bench/ping` itself**, in the
332
+ gateway's own shape — a `children` array with one entry — so a probe reads one contract either way. It owns the
333
+ rotating log file the gateway would otherwise write, in the same `runtime/logs` directory.
334
+ - **Nothing supervises a solo process but the orchestrator**, since the gateway's crash-restart-with-backoff went
335
+ with it. `infra/app/templates/app.yaml` carries the liveness, readiness and startup probes that replace it.
336
+ - **`main.ts` imports `AkanApp` from `akanjs/server/akanApp`, not the barrel.** The barrel re-exports
337
+ `AkanServer`, whose graph the gateway never runs; through it the process evaluated 35MB of SSR renderer and
338
+ SQLite driver to spawn children and relay bytes. Keep entrypoint imports at the leaf.
339
+
340
+ ## The Generated Image — `docker` In `akan.config.ts`
341
+
342
+ **`docker` is `string | { image, preRuns, postRuns, command }`** — a whole Dockerfile, or the parts Akan
343
+ assembles one from. There is no `content` field; the string *is* the content.
344
+
345
+ ```ts
346
+ const config: AppConfig = {
347
+ docker: { preRuns: ["apt-get update && apt-get install -y --no-install-recommends ffmpeg"] },
348
+ };
349
+ const config: AppConfig = { docker: "FROM oven/bun:1-slim\n…" }; // verbatim, nothing merged in
350
+ ```
351
+
352
+ - `image` and each run entry take `string | { amd64?, arm64? }`; the object form compiles to a
353
+ `RUN if [ "$TARGETARCH" = "<arch>" ]` guard, so a multi-arch build runs it on one leg only. `preRuns` land
354
+ before `bun install --production` (where a native dependency's build tools have to be), `postRuns` after it
355
+ and before the app files are copied.
356
+ - **A lib declares the steps its own runtime needs**, and every app that mounts it inherits them:
357
+ `libs/<lib>/akan.config.ts` takes `docker: { preRuns, postRuns }` and nothing else — the base image and the
358
+ command belong to the app. Lib steps are emitted before the app's own, and an identical step declared twice
359
+ becomes one layer.
360
+ - **The string form takes no contributions.** A Dockerfile handed over whole is used exactly as written, so a
361
+ lib's `preRuns` are dropped rather than spliced into a file Akan does not own. An app that needs both writes
362
+ the parts instead.
363
+ - Like `externalLibs`, lib steps are collected from **every lib in the workspace**, not just this app's
364
+ dependency closure — narrowing that set needs the dependency scan, and this config is re-read on every file
365
+ change in dev. Keep a lib's steps to what its runtime genuinely requires.
366
+ - `AkanAppConfig.docker` is the resolved declaration; `AkanAppConfig.dockerfile` is the text `akan build` writes
367
+ to `dist/apps/<app>/Dockerfile`.
368
+
369
+ ## Shipped Assets — `assets` In `akan.config.ts`
370
+
371
+ `akan build` copies the whole `public/` tree into `dist`, lib assets dereferenced, and that copy is the image.
372
+ **`assets: { pruneFonts, keepFonts }`** trims the fonts out of it that nothing reads. **Source trees are never
373
+ touched** — an app's and a lib's own `public/` keep every file, because one shared font folder is picked over
374
+ differently by every app that mounts it and by other repos.
375
+
376
+ ```ts
377
+ const config: LibConfig = { assets: { keepFonts: ["fonts/Assistant-*.woff2"] } }; // libs/<lib>/akan.config.ts
378
+ const config: AppConfig = { assets: { pruneFonts: false } }; // emergency valve, not the normal escape hatch
379
+ ```
380
+
381
+ - **A font with `optimize` on is a build input, not a runtime asset.** `FontOptimizer` subsets it into
382
+ `.akan/artifact/fonts` and the emitted CSS points at `/_akan/fonts/*`, which `WebRouter` serves from the
383
+ artifact — so the source in `public/` is never opened again. That is the bulk of what this drops, and it
384
+ applies to the fonts the app *does* use, not only the ones it does not.
385
+ - **The keep-set is derived per app, per build; there is no list of fonts to drop.** An exclude list would rot
386
+ the moment another app or repo started using one of them, and nobody would know to update it. A font survives
387
+ because ① its font declares `optimize: false`, so `FontCss.getRuntimeCss` emits the raw src; ② something
388
+ references its filename — anything under `public/`, the compiled stylesheet, a client/server bundle, the CSR
389
+ shell; or ③ a `keepFonts` glob names it.
390
+ - **`keepFonts` belongs to the `akan.config.ts` that owns the font**, written against that lib's or app's own
391
+ `public/` (`"fonts/X.woff2"`, resolved to `libs/<lib>/fonts/X.woff2`). A lib shared across repos carries the
392
+ reason its own CSS needs a font, instead of every mounting app rediscovering it in production.
393
+ - **A reference from a build bundle is ignored for an already-subset font.** Every route file's `fonts`
394
+ declaration is inlined into the pages bundle, the client chunks and the CSR shell, so a declared source is in
395
+ all three whether or not anything loads it. `public/` and the compiled CSS are read as authoritative.
396
+ - **A generated manifest that lists every public asset keeps every font it names** — a service-worker precache
397
+ file is the usual one. That is a real reference, not a false positive, so the build reports it at `info`:
398
+ read the `[font-prune] kept …` line before concluding the prune does nothing.
399
+ - Matching is by filename, including the percent-encoded spelling, so a `url()` survives however it is written.
400
+ - The phase is `assets`, between `csr` and `compress`. An api-only build (`web: false`) copies no `public/` and
401
+ the phase finds nothing to do.
402
+
403
+ ## React Components And Styling (`**/*.tsx`)
404
+
405
+ - Components are `export const X = ({ … }: XProps) => { return (…); };` — arrow const with a block body. `export default` is reserved for pages, layouts, and `lazy()` targets.
406
+ - Never `React.FC`, never `defaultProps`, never `PropsWithChildren`. Defaults go in the destructuring (`prefix = ""`); children are typed `children: ReactNode`.
407
+ - `"use client"` on line 1 above the imports is mechanical by file role: every `.Zone.tsx`, `.Template.tsx`, and `.Util.tsx` has it; no `.Unit.tsx` or `.View.tsx` ever does. `usePage()` is legal in server files.
408
+ - Conditional render is `cond ? <X/> : null`. Never `{cond && <X/>}` — in a `className` context it renders the literal string `"false"`. Early `return null` is for guard clauses only.
409
+ - Never hand-roll loading, empty, or list states. Use `Load.Units` / `Load.View` / `Load.Edit` with `renderItem`, `renderList`, `renderView`, and `renderEmpty`; `<Empty />` for a bare placeholder; and `Model.New` / `Model.Edit` / `Model.SureToRemove` for CRUD modals.
410
+ - Avoid hooks. `useState` is for modal-open, tab, draft-input, and drag state only — never for server data. `useEffect` must be a genuine effect such as subscribe-with-cleanup or one-shot init. Prefer `Tab` over a `useState` mode switch. `.Template.tsx` files contain zero `useState`.
411
+ - Forms are entirely store-driven: `value={xForm.field}` with `onChange={st.do.setFieldOnX}`, the setter passed by reference. Always use `Field.*`, never a bare `<input>` for a model field. Nested rows use `st.do.writeOnX("payments.3.name", v)` plus the generated `add<Field>OnX` / `sub<Field>OnX`. **Passing the setter by reference is also what makes the framework emit `data-akan-action` / `data-akan-state`** on the control — the annotation an in-page agent, an E2E selector, and an external browser agent all read. Wrapping it in an inline arrow (`onChange={(v) => st.do.setFieldOnX(v)}`) silently drops that: a closure the caller wrote says nothing about what it does. Never hand-write a `data-akan-*` attribute.
412
+ - Read with `st.use.*` and write with `st.do.*`. Client components do not call `fetch.*`.
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"`).
414
+ - Multi-slot components take extra named props (`wrapperClassName`, `bodyClassName`), never a `classNames` object.
415
+ - 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.
416
+ - Use `<Link>` from `akanjs/ui` for internal navigation; `<a>` only for `mailto:` and external links.
417
+
418
+ ## Naming And Language
419
+
420
+ - Component exports are role names — `Card`, `Sticker`, `General`, `Preview`, `Admin`, `World`, `Mesh`, `Remove`. The model comes from the namespace (`<Floor.Unit.Card>`), so never write `FloorCard`. `Util` exports are the endpoint verb minus the model noun (`Serve`, `Refund`, `Complete`, `Terminate`).
421
+ - Layer the verbs: the document chain method drops the model (`sign()`, `approve()`), the service keeps the bare verb, and the signal, store, and dictionary re-add it (`signScContract`). This keeps custom endpoints clear of generated CRUD and makes `st.do.X` read the same as `fetch.X`.
422
+ - Slice and filter names are prepositional: `inOrg`, `inProject`, `inPeriod`, `byStatuses`, `ofPortfolio`. Never `getXInY`, never `listX`.
423
+ - Handlers are `onX` props taking inline arrows. Do not extract a `handleX`.
424
+ - Booleans are `is*` / `has*` / `can*` / `show*` / `disable*`. Counters are `*Num`, indices are `idx`, collections are `*List` or plural. `SCREAMING_SNAKE` is unused; module-scope tables are camelCase + `as const`.
425
+ - Keep existing domain vocabulary and its typos. Transliterated domain terms and misspelled identifiers already in use are load-bearing — renaming them silently breaks callers that match on the name.
426
+ - Identifiers, type names, endpoint names, and log messages are **English, always**. Everything a user reads goes through `l("model.field")` or `l.trans({ en, ko })` — never a hard-coded string in JSX, never `window.alert`.
427
+ - Dictionary entries are `[en, ko]` pairs, and nearly every label also carries a `.desc([en, ko])` even when it repeats the label. English labels are Title Case, Korean is the plain domain term, and `.error()` Korean ends in `다.`.
428
+
429
+ ## Domain Module Conventions (`apps/**/lib/**`, `libs/**/lib/**`)
430
+
431
+ - Organize business concepts as domain folders under `lib/`; keep related schema, service, signal, store, and UI files together.
432
+ - Use lowercase logic files such as `<model>.constant.ts`, `<model>.document.ts`, `<model>.service.ts`, `<model>.signal.ts`, `<model>.dictionary.ts`, and `<model>.store.ts`.
433
+ - Use PascalCase React component files such as `<Model>.Template.tsx`, `<Model>.Unit.tsx`, `<Model>.View.tsx`, `<Model>.Zone.tsx`, and `<Model>.Util.tsx`.
434
+ - Treat `constant.ts`, `dictionary.ts`, and `signal.ts` as shared contract files that should avoid platform-specific dependencies.
435
+ - Keep backend persistence/query logic in `.document.ts` and domain business orchestration in `.service.ts`.
436
+ - Keep frontend state in `.store.ts`; use `Template` for forms, `Unit` for list/card items, `View` for details, `Zone` for composed page sections, and `Util` for domain-specific UI helpers.
437
+
438
+ ### Module File Playbook
439
+
440
+ **`<model>.constant.ts`** — five classes in order with one blank line between them and `enumOf("camelName", [...] as const)`
441
+ classes above: `XInput → XObject → LightX → X → XInsight`. Write `XInsight` even when it is empty. Put display and
442
+ predicate logic on `LightX` (`isNew()`, `canWrite(user?)`, `formatTimes()`) — the Light class is the one both server
443
+ and client hold, so shared logic belongs there instead of in a util module. This is the most commonly missed rule in
444
+ the codebase. Collection-level helpers go `static` on the full model. Give any field whose business meaning is not
445
+ obvious a short trailing comment.
446
+
447
+ **`<model>.document.ts`** — fixed order: `XFilter extends from(...)` → `X extends by(...)` → `XModel extends into(...)`,
448
+ with `sort: {}` always present. Chain methods validate → mutate → `return this`, and never `save()`; the caller saves,
449
+ so chains compose (`org.removeUser(id).removeInvite(id).save()`). Put a one-line comment above each stating the
450
+ transition. Atomic counters live on the Model class with the updater-callback form, returning `!!modifiedCount`.
451
+ Indexes and derived totals go in `static override _onSchema`, not in the service. **Removal is always soft** — the
452
+ framework has no hard delete for a model table, and `delete` is deliberately left unused so it can mean one later.
453
+ The facade spells `Many`/`One` out on its writes (`updateOne` / `updateMany` / `removeOne` / `removeMany`), because a
454
+ bare `update`/`remove` would read like the document-path `update(id)` / `doc.remove()` while hitting every match; only
455
+ the count was shortened to `count(query)`, with `countDocuments` kept as `@deprecated`. `updateById(id, update)` and
456
+ `removeById(id)` are those same query-level writes narrowed to one id, **not** the document path: they fire no hooks,
457
+ so a model whose removal cascades or carries a `_postRemove` still goes through `remove<Model>(id)`.
458
+
459
+ **`<model>.service.ts`** — keep methods to a few lines: load → chain → `return await ….save()`. Write `return await`
460
+ explicitly in tail position; do not "optimize" it away. Side effects belong in `override async _preUpdate` /
461
+ `_postCreate`, not inline. Fire-and-forget is explicitly `void`-ed. Order deliberately: load every referenced document,
462
+ then save, then notify. Return `null` / `false` for "not allowed" or "not found" and let the signal decide whether that
463
+ is an error.
464
+
465
+ **`<model>.signal.ts`** — `XInternal` → `XSlice` → `XEndpoint`, all three declared even when empty. `exec` is a one-liner
466
+ delegating to the service.
467
+
468
+ **`<model>.store.ts`** — write a custom action only for a toast, an optimistic update, or a multi-field write; most
469
+ stores need none, because state and CRUD actions are generated. The body is three lines: `await fetch.X` →
470
+ `this.setX(...)` → toast. The optimistic shape is mutate the client model, `void fetch.*`, then commit. Use
471
+ `this.pick(...)` when the value must exist, `this.get()` when it may not, and `this.set({...})` to write. Mutate lists
472
+ through the collection API (`this.set({ xList: xList.set(x).save() })`), not array spread. **An action returns
473
+ nothing** (`no-return-in-store-action.grit`) — hand the result to `this.set({...})`; a bare `return;` guard stays
474
+ fine. **Never `import type { RootStore } from "../st"`** — it crashes `akan build` with a Bun SSR segfault.
475
+
476
+ **`<model>.dictionary.ts`** — fixed chain, with empty stages still written:
477
+ `.of() → .model() → .insight() → .query() → .sort() → .enum() → .slice() → .endpoint() → .error() → .translate()`.
478
+ Name every argument in `.arg()`, including framework-supplied `skip` / `limit` / `sort`. Use `modelDictionary`,
479
+ `scalarDictionary`, or `serviceDictionary` to match the module kind.
480
+
481
+ **`<module>.abstract.md`** — a title line, one declarative sentence naming what the module owns, a `## Rules` list of
482
+ two to five invariants the code cannot show, and an optional workflow arrow chain
483
+ (`draft -> signed -> active -> completed`). Never restate field lists or types. Update it whenever an invariant or
484
+ workflow changes.
485
+
486
+ ## Service And Signal Conventions (`*.{service,signal}.ts`, `server/**`)
487
+
488
+ - Keep domain business operations in `.service.ts` classes built with `serve(...)`.
489
+ - Keep execution contracts and triggers in `.signal.ts` classes built with `internal(...)`, `slice(...)`, and `endpoint(...)`.
490
+ - Use `Internal` for internal triggers such as init, interval, cron, or queue jobs.
491
+ - Use `Slice` for typed data views that feed client stores and zones; keep each slice focused on one purpose.
492
+ - Use `Endpoint` for query and mutation contracts exposed to callers.
493
+ - Connect external APIs or infrastructure through adapters, usually under `srvkit/`, and inject them into services instead of importing vendor clients directly into domain logic.
494
+
495
+ ### Guards And Transports
496
+
497
+ - Guards run on both HTTP and websocket calls. Read the caller with `context.get<T>("account")` (`pkgs/akanjs/signal/signalContext.ts`) instead of branching on `getHttpContext()` / `getWebSocketContext()`.
498
+ - Slice-level `guards` only reach the generated query/mutation endpoints. A `pubsub`/`message` endpoint is unguarded unless it declares its own `guards` in its signal option.
499
+ - A pubsub room is authorized once, at subscribe. When a socket's credential changes the framework re-runs each room's guards and unsubscribes the ones that now fail (`SignalResolver.revalidateWsRooms`), so guards must stay side-effect free and safe to re-run.
500
+ - A websocket carries its credential in the handshake snapshot on `ws.data` (`AppWsData`); clients that hold the token in memory send it with `fetch.setJwt(...)`, which forwards an auth frame over the socket.
501
+ - **Never read the caller's IP off the socket or the request peer — take `.with(Ip)`.** Whenever a federation
502
+ gateway is in front, `ws.remoteAddress` and the child's own peer are the *gateway* (`127.0.0.1`) for every
503
+ caller and for the whole life of every socket. `Ip` reads what a proxy recorded (`x-real-ip`, then the first
504
+ `x-forwarded-for` entry) and falls back to the peer only when nothing proxied the call — which is the answer
505
+ for a solo process behind an ingress that sets neither; `context.getClientIp()` is the same answer inside a
506
+ guard or middleware. It arrives unwrapped from
507
+ its `::ffff:` IPv4-mapped form, so it can address a `udp4` socket as well as identify a caller, and it is
508
+ `null` rather than a placeholder when genuinely unknown — a loopback-looking address for an unknown caller
509
+ is the failure this replaced. The gateway also forwards the client port (`x-forwarded-port`, read with
510
+ `context.getClientPort()`) along with host and protocol.
511
+ - **Every socket carries a `socketId`, and only the framework mints one.** `AppWsData` assigns it at the handshake, and a `message` / `pubsub` handler reads it off the `Ws` internal arg — `.with(Ws)` hands `{ ws, socketId, subscribe, on, off }`. Never mint your own from `ws.data`: the room bookkeeping keys on the framework's id, and a second one fails to match it silently. It identifies a **connection**, not a caller — a reconnect gets a new id and the federation gateway's own socket is a different one — so per-user state keys on the account, never on this.
512
+
513
+ - **A cleanup registered with `ws.on("disconnect" | "unsubscribe", fn)` is scoped to the call that registered it.**
514
+ From a `pubsub` subscribe it belongs to that room — `unsubscribe` runs when the client leaves it or a credential
515
+ change revokes it, `disconnect` when the socket closes while still subscribed — so a room already left runs
516
+ neither again, and cleanup that must happen either way registers for both. From a `message` handler it belongs to
517
+ the socket and runs at close. A handler that throws is logged and never blocks the rest of the teardown.
518
+
519
+ ### Authorization Defaults
520
+
521
+ - **Every `slice()` takes an explicit `{ guards: {…} }` second argument, and `root:` is always `Admin`.**
522
+ - **Every custom `mutation` / `query` / `message` names its own `guards: [...]` array.** Never rely on the slice default. `Public` belongs on a slice `get:`, never on a mutation.
523
+ - **The guards are also the MCP exposure decision** — see MCP Exposure. An endpoint that names none is not published to agents at all, and a mutation whose only guard is `Public` is refused, so a missing `guards` array now costs visibility as well as authorization.
524
+ - Resource guards are `Can<Verb><Model>` classes in `srvkit/guards.ts` that `implements Guard` with an `async canPass(context)`. They **fail closed**: no resource named ⇒ `false`; a load that throws ⇒ `logger.warn` then `false`. Admin bypass goes first.
525
+ - Keep `static name = "User";` on guard classes. `fetch` serializes guard names and the API explorer filters on them; it looks like dead code, and deleting it breaks the UI. Comment it so the next reader knows.
526
+ - **Every guard class also declares `static scope: GuardScope`, and it is required with no default.** `"account"` means the verdict reads the caller and nothing about the call, so it can be evaluated with no arguments — which is what lets an MCP listing hide what this caller certainly cannot use. `"resource"` means it needs the call's arguments (`context.getArg()`) and fails closed without them, so it is never evaluated for a listing: the entry stays visible and is stopped at call time. Getting it wrong is not a type error, so the marker is mandatory rather than defaulted — `SignedIn` / `Admin` / role checks are `"account"`, and every `Can<Verb><Model>` is `"resource"`.
527
+ - The acting user arrives via `.with(Self)` / `.with(CurrentUserId)` / `.with(Me)`. Never trust a client-supplied id.
528
+ - Guards ship with the library that owns the model and are imported by its own signals through the package path, so a mounting app inherits authorization and cannot forget it.
529
+ - Services re-check ownership even when a guard already gated the call — two independent gates.
530
+ - `srvkit/guards.ts` earns real comments: explain what would leak without each guard.
531
+
532
+ ### Signal Body Types
533
+
534
+ - `.body(...)` / `.param(...)` args accept `ConstantFieldTypeInput` only: scalars, model refs, or `enumOf(...)`.
535
+ - Numbers must use `Int` or `Float` — `Number` is rejected (`pkgs/akanjs/signal/endpointInfo.ts`).
536
+ - `Upload` is valid only inside a mutation flagged for file upload: `mutation([cnst.File], { fileUpload: true }).body("files", [Upload])`, as the `file` module does. It is not a model field type.
537
+ - Bytes are `Binary`, never `Any` — see Scalar & Field Type Reference.
538
+
539
+ ### Binary Pubsub
540
+
541
+ - **`pubsub(Binary)` sends its payload in a websocket binary frame**, skipping the JSON `{ type: "pub" }`
542
+ envelope and the base64 a JSON wire would need. The client's subscribe callback receives a `Uint8Array`.
543
+ Nothing else is declared: the return type is the whole switch, and text and binary frames coexist on one
544
+ socket, so every JSON endpoint is untouched. The optimization applies only when the **whole** return is
545
+ `Binary` — `[Binary]`, or a `Binary` inside a model, falls back to base64.
546
+ - **A declared `Binary` room coalesces under backpressure**: while a socket cannot keep up the room keeps only
547
+ its newest frame and drops the rest, which is what a telemetry or video stream wants. Declare
548
+ `pubsub(Binary, { backpressure: "queue" })` when the frames are a sequence a subscriber must see in full, such
549
+ as deltas against a base it already holds; the send buffer then grows with the slowest subscriber. Coalescing
550
+ is keyed by the endpoint, not carried with the frame, so this process, an IPC deliver and a Redis fan-out all
551
+ reach the same answer. Coalesced frames are counted in `pubsubCoalesceCount` on the replica's metrics report.
552
+ - **A `pubsub(Any)` that happens to carry bytes is framed too, and warns once.** `Any` passes a `Buffer` through
553
+ untouched, so the transport sees bytes and sends them whole rather than as a number array — an undeclared
554
+ publisher is fixed rather than left corrupt. It is still not the contract: it queues rather than coalescing
555
+ (nobody declared lossy delivery for that room), and a Redis fan-out still JSON-stringifies it. Declare
556
+ `Binary`.
557
+ - The federation gateway relays a binary frame unchanged, and Bun IPC carries bytes as bytes, so the only hop
558
+ that needed teaching was the socket itself. A cross-server (Redis) fan-out encodes it as protobuf `bytes`.
559
+
560
+ ### Mutation HTTP Verb
561
+
562
+ - A `mutation` is `POST`. `{ method: "PATCH" | "PUT" | "DELETE" }` moves it, and one path may carry several verbs
563
+ — a `query` GET and a `mutation` POST on the same custom `path` are mounted side by side. Two endpoints claiming
564
+ the same path **and** verb fail the boot rather than silently shadowing one another.
565
+ - Reach for it only when a foreign wire protocol forces the verb (a client you cannot change that sends
566
+ `PATCH /rest/v1/<table>`). Akan's own `fetch.*` client, the OpenAPI document, and the API explorer all follow
567
+ whatever is declared, so nothing needs restating per caller.
568
+
569
+ ### Reserved Endpoint Names
570
+
571
+ - Auto-generated CRUD endpoints (e.g. `create<Model>`, `update<Model>`, `remove<Model>`) already exist for every model. Do not declare an `Endpoint`/`Slice` with a name that collides with them.
572
+ - The service layer surfaces such a collision as a typecheck error, but the signal layer can pass sync/typecheck/build and fail only at runtime — so treat name collisions as errors regardless of whether the build is green.
573
+
574
+ ### Slices, Queries, and Hydration
575
+
576
+ - **The root slice (`""`, generated by `slice()`) takes `queryKey` and `args`, not a query.** `initX("byOwner",
577
+ [ownerId])` names one of the model's own filters and passes that filter's args; no key at all is the `any`
578
+ filter every model carries. A raw query descriptor let a caller compose a query the model never declared, and
579
+ could not survive the query string at all — `String(value)` sends `[object Object]`. Args are parsed by the type
580
+ the filter declared, a missing required one is refused, and args past the declared ones are dropped
581
+ (`resolveFilterQuery`, `pkgs/akanjs/document/filterMeta.ts`). It is an admin API: `root:` is always `Admin`.
582
+ - **Give an id filter arg a `ref` and the admin panel stops asking for hex.** `filter().arg("ownerId", ID, { ref:
583
+ "user" })` names the model the id points at; the arg then renders a picker that runs the same query maker against
584
+ that model's own filters, so a ref of a ref keeps nesting. The picker holds its rows in local state rather than the
585
+ ref model's store — that store is a singleton, and loading into it would overwrite whatever listing of the same
586
+ model is already on screen. `ref` is the only UI hint a filter arg carries; there is no render callback, because a
587
+ function cannot cross to the client.
588
+ - **A summary counter names the listing it counts, on the field itself.** `field(Int).meta(getQueryMeta("user")
589
+ .query("byStatuses").args([["prepare"]]))` is what makes the dashboard tile clickable: clicking it applies that
590
+ filter to the listing in place — one request, no navigation — and fills the arg controls under the toolbar, so
591
+ the filter is visible and editable rather than opaque. `args` may be a thunk (`() => [dayjs().subtract(1,
592
+ "hour")]`), read at the click so a relative time is current. A meta naming a different model counts a different
593
+ listing, so its tile stays inert here. `AdminPanel`'s `queryMap` still overrides per column, and is now only for
594
+ a column whose field cannot declare one.
595
+ - **A picker row is labelled by `labelOf`, which reads `Light<Model>.label()` first.** Then the field carrying
596
+ `text: "title"`, then `title` / `name`, with the id as the floor. Write the method on the Light class — that is
597
+ where display logic belongs, and the same label is what `Load.Units` and the in-page agent's screen scope show.
598
+ A model with none renders ids and says so in the picker.
599
+ - A slice's `exec` returns a `QueryOf` (an opaque query descriptor, `pkgs/akanjs/constant/types.ts`); you **cannot** chain `.sort()`/`.limit()` on it.
600
+ - Apply ordering/paging via the store `init` fetch option instead: `initX(..., { sort, page, limit })` (`pkgs/akanjs/fetch/fetchType/sliceFetch.type.ts`).
601
+ - Generated list accessors like `listBy(...)` return `Promise<Doc[]>`. For a chainable builder (`.sort().skip().limit().select()`) use the model facade's `findMany`/`findOne` (`FindManyChain`, `pkgs/akanjs/document/into.ts`).
602
+ - **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`).
603
+ - Every filter generates fourteen methods: `list` · `listIds` · `find` · `findId` · `pick` · `pickId` · `exists` ·
604
+ `count` · `insight` · `query` · **`remove`** · **`removeOne`** · **`update`** · **`updateOne`**. The last four are
605
+ query-level writes — one atomic UPDATE, **no hooks**, and therefore no `_postRemove` and no cascade. Use them on a
606
+ model that carries no removal side effect; otherwise remove documents one at a time.
607
+ - **`update<Filter>` / `updateOne<Filter>` are chains, not calls:** `await updateInRoot(rootId).set({ status:
608
+ "archived" })`. The patch cannot trail the filter args — a filter's own args may be optional and no tuple type
609
+ puts a required element after those — so it lands on a terminal `.set()`, mirroring the `UPDATE … SET …` it
610
+ compiles to. Building the chain touches nothing; only `.set()` runs a query.
611
+ - `removeOne` / `updateOne` hit the **newest** match — the subquery they compile to is ordered `createdAt` descending
612
+ and there is no way to change that. They also report only counts, never which row they touched, so they are for
613
+ "there is at most one of these", not for claiming the next item off a queue. Pass a query that matches one row.
614
+ - **A filter may not be keyed after its own model.** Filter methods are assigned after CRUD, so a filter `chat` on
615
+ model `chat` would silently swap the single-document `removeChat`/`updateChat` for a hookless query-level one. It
616
+ throws at boot instead (`assertFilterFitsCrud`).
617
+ - **"Has no value" is `q.empty`, never `q.missing`.** The three absence operators are distinct: `q.exists` is the
618
+ key being present, `q.missing` is the key being *absent from the stored JSON*, and `q.empty` is either — absent,
619
+ or present and null. An optional field is written both ways over its life: an insert that omits it leaves the key
620
+ out, while a document read back and saved carries the explicit `null` that the read materialized. So `q.missing`
621
+ matches a freshly inserted row and silently stops matching that same row after its first round-trip save. Reach
622
+ for it only to find rows written before the field was declared.
623
+
624
+ ### Text Search In A Filter — `q.search()`
625
+
626
+ - Text search is a filter query node like any other: `bySearch: filter().arg("text", String).query((text, q) =>
627
+ q.search(text, { prefix: true }))`. The generated `listBySearch` / `countBySearch` / `queryBySearch` /
628
+ `insightBySearch` come for free — you do **not** need a slice to make search usable.
629
+ - **Only add a search slice when the model's data is safe to enumerate.** A filter is server-side; a slice is a
630
+ client-callable endpoint, so on a model whose slice `get:` is `Public` a search slice hands anyone a way to walk the
631
+ table. Leave that decision to the mounting app.
632
+ - `q.search()` compiles to a JOIN, not a WHERE fragment, so it **must sit at an AND position**. Nesting it under
633
+ `q.any()` or `q.not()` throws, and it is rejected in `updateOneByQuery` / `updateManyByQuery` — a query-level write
634
+ takes no join, so ignoring the node would silently widen the write to every other matching row.
635
+ - Blank or whitespace-only input matches **nothing**. Never "fix" that into a passthrough: a passthrough turns a
636
+ search endpoint into a full listing.
637
+ - Order by relevance with the built-in `relevance` sort key. It is an empty sort map, which the store reads as
638
+ "unspecified": score order when a search join is present, `createdAt` descending otherwise. That fallback is the
639
+ compiler's own, not a model-defined default — redefining `latest` on the model does not change it.
640
+ - **A slice endpoint never reaches "unspecified".** The resolver fills `latest` before the query is built, so a
641
+ client asking for the score order has to name `relevance`; leaving `sort` off gets `latest`, not relevance.
642
+ - Scope a search with `columns` (`q.search(text, { columns: ["title"] })`) and re-weight with `weights`, a tuple of
643
+ finite numbers positional over `["title", "desc", "tag", "filter"]`.
644
+
645
+ ### Service / Signal Injection
646
+
647
+ - 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`).
648
+ - 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.
649
+ - 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.
650
+
651
+ ### Adapters — `adapt()` And `plug()`
652
+
653
+ An injected singleton is an `adapt()` class in `srvkit/`. Write new adapters this way.
654
+
655
+ ```ts
656
+ export class AdminNoti extends adapt("adminNoti" as const, ({ use, env, plug, memory }) => ({
657
+ discordApi: use<DiscordApi>(),
658
+ workspaceRoot: env(() => `~/build/${getEnv().environment}`),
659
+ masterHost: plug(MasterHost),
660
+ tokenMap: memory(Map, { of: String }),
661
+ })) {}
662
+ ```
663
+
664
+ - Inject it with `plug(TheClass)` from a service or from another adapter. Destructure only the injectors you use, and write the registration key `as const`.
665
+ - **Do not register an `adapt()` class in `lib/option.ts`.** It self-registers, and `plug(Class)` uses the class itself as the token. `option.ts` is now only for legacy constructor-style adapters and for widening the options type.
666
+ - `this.logger` is provided; never construct a `Logger` inside an `adapt()` class. Lifecycle work goes in `override async onInit()`.
667
+ - **`adapt()` is for singletons only.** A per-use value object stays a plain class you `new` at the call site. Ask whether there is exactly one per process that a service needs injected; if not, it is a plain class.
668
+ - **Legacy shape — recognise it, do not copy it.** Plain classes with `constructor(options: XOptions)`, registered in `lib/option.ts` as `options.x ? new XApi(options.x) : null` and injected with `use<T>()`, still work. Migrate one to `adapt()` only when you are already changing it.
669
+
670
+ Conventions that hold for both shapes:
671
+
672
+ - Route every remote call through one private `#api<T>(path, init?)` with `signal: AbortSignal.timeout(20_000)`.
673
+ - Paginate with `for (let page = 1; ; page += 1)` broken by `if (pageItems.length < 100) break;`.
674
+ - Resolve secrets as `process.env.X ?? options.x ?? deterministicGenerator(...)` **inside a function**, never at module scope.
675
+ - Extend a function by appending an optional trailing parameter with a default, never by changing arity.
676
+ - Parameters: up to three required primitives positional; optional flags in a trailing `{ … } = {}`; four or more parameters, or any two same-typed strings, in one named destructured object.
677
+ - Release locks in `finally`. Load heavy optional dependencies through a module-level memoized promise (`puppeteerLoad ??= import("puppeteer")`).
678
+
679
+ ### Error Placement
680
+
681
+ - State-machine preconditions throw in `document.ts`; cross-document rules throw in `service.ts`; request-level policy belongs in `signal.ts` guards.
682
+ - Best-effort code returns a sentinel (`null`, `undefined`, `[0, 0]`, `{}`). There are no Result/Either wrappers.
683
+ - `try/catch` is rare and always converts an exception into a decision, never swallows one. Guards catch → `logger.warn` → `return false`; adapters catch → `logger.error` → `return null`; UI uses `try/finally` to reset a spinner. A bodyless `catch {}` is acceptable only with a one-line reason.
684
+ - Store actions do not `try/catch` — let the framework toast the `Err`. Client-side validation failure is `msg.error("<key>")` plus an early return, never a throw.
685
+
686
+ ### MCP Exposure
687
+
688
+ Every signal is served to AI agents as an MCP server on `POST /mcp`. **`/mcp` is mounted by default and exposure
689
+ follows an endpoint's guards — there is no per-endpoint opt-in, and nothing to write in a signal file.** An endpoint
690
+ that declares a real guard is published; one that declares none is refused, and so is a mutation whose only guard is
691
+ `Public`. The guards are already the authorization decision, so a second switch would say nothing they do not —
692
+ while guaranteeing that every endpoint added later is invisible to agents until somebody remembers it.
693
+
694
+ - **`mcp: false` keeps an endpoint off the shelf without touching its guards.** Guards answer "may an agent call
695
+ this"; they cannot answer "does this belong on a shelf", and a step of a UI-driven state machine
696
+ (`requestPhoneCodeForSignin`) is perfectly guarded and still a mistake for a model to reach. Write it in the
697
+ signal option (`mutation(Boolean, { guards: [Every], mcp: false })`, `init({ guards: [SignedIn], mcp: false })`
698
+ — which covers that slice's list and insight together), or as an `mcp` map on `slice()` that **mirrors the
699
+ `guards` map key for key** (`mcp: { cru: false }`), with the same `cru` fallbacks and the same scope: the root
700
+ slice and generated CRUD only, never a named slice or a custom endpoint. It is an opt-**out**, so everything
701
+ added later still publishes. **It is curation, not authorization — HTTP serves the endpoint exactly as before.**
702
+ - **Narrow by cost, and read the boot log first.** MCP has no shared component section and forbids a `$ref` across
703
+ entries, so every entry inlines the full schema of every model it mentions, and the listing is re-sent whole to
704
+ every agent that connects. `MCP catalogue: … · listing 214KB` plus a per-signal `MCP catalogue cost:` line says
705
+ where it went. A plain 21-field model costs 12.6KB over eight entries; `mcp: { cru: false }` takes it to 4.7KB,
706
+ because the generated writes carry the input *and* the full model.
707
+ - Settings live in the app's `lib/option.ts` — `option.setMcp({ enabled, readOnly, path, version, instructions,
708
+ allowedOrigins, pageSize, language, auth })`, **not `main.ts`**. Every lib's option is read in mount order with
709
+ the app's last, and each field has an `AKAN_MCP_*` env spelling the option overrides.
710
+ - **RFC 9728 metadata is served only once an authorization server is named.** Advertising `bearer_methods_supported`
711
+ without one makes Cursor (and similar clients) force OAuth. Set `AKAN_MCP_AUTH_SERVERS` when the surface is an
712
+ OAuth resource; otherwise the well-known document is absent.
713
+ - **The refusals are fail-closed**: a declared `mcp: false`, an endpoint with no `guards`, a mutation with no real guard (`[Public]` is
714
+ having none), `pubsub` and `message`, an `Any` or `Upload` return, a file upload, a required `Any` argument, and
715
+ the generated `light<Model>` read — it is `<Model>` in a smaller shape, so one read never costs two entries of
716
+ every listing. A `prompt` also refuses a list argument and any `Any` argument.
717
+ - **Every refusal is named in the boot log**, with a `MCP catalogue: tools=… prompts=…` count. Read that line
718
+ first when a tool you expected is missing — with no opt-in to notice, it is the only place the answer exists.
719
+ The boot log also names every published entry with no dictionary `.desc()`: an agent picks a tool by its
720
+ description, so write the model `.desc()` the generated entries borrow.
721
+ - A refused endpoint answers the *same* "unknown tool" as one that does not exist, and a guard's refusal is
722
+ generalized to `You are not permitted to perform this action.` Never make either message more helpful — the
723
+ difference is what enumerates your private surface.
724
+ - **A `field.visual` field is stripped from every MCP result**, and from the readable schema so the two agree — the
725
+ model-level answer to a field the page needs and an agent does not. `resolveReturn` leaves it alone, so the
726
+ browser still receives it.
727
+ - **A structured result ships twice by default** — once as `structuredContent`, once as the same JSON in the text
728
+ block, which is what the spec asks for clients predating the structured field and is also a flat doubling of what
729
+ every model-returning tool costs. `option.setMcp({ legacyTextBlock: false })` leaves a pointer there instead.
730
+ - `AKAN_MCP_READONLY=true` is the read-only-deployment valve, not the exposure switch; `AKAN_MCP=false` takes the
731
+ whole surface off.
732
+ - **`prompt()`** is invoked by the *user* as a slash command, never chosen by the model. `exec` returns
733
+ `PromptMessage[]` built with `Msg.user` / `Msg.assistant` / `Msg.resource` / `Msg.image`; an embedded payload is
734
+ masked by the model you name (`Msg.resource(uri, task, { model: cnst.LightTask })`), and one whose
735
+ `hidden`/`secret` fields are populated with no model named is refused. It is also mounted as a plain HTTP `GET`
736
+ whether or not MCP is enabled, so guard it like any other read.
737
+
738
+ Full contract — configuration, wire behaviour, resource URIs, OAuth metadata, protocol revisions, and
739
+ `McpProgress.report`: `get_guideline` with `mcpRule`, or `akan guideline show mcpRule`.
740
+
741
+ ## In-Page Agent
742
+
743
+ Every akan app can host a component-level agent that reads the rendered screen and drives it. **A component
744
+ declaration is the surface, exactly**: `st.tool` publishes one action, and `st.use` / `st.sel` / `st.ref` make one
745
+ store key readable while the reading component is mounted. Nothing is derived from a store class — a lever the
746
+ screen does not offer the user is not one an agent may pull in their place. The React core is the `use-agentic`
747
+ package; apps and libs never import it directly (`no-import-external-library`) — everything reaches them through
748
+ `st.*` and `akanjs/ui`.
749
+
750
+ - **Mount `<Agent.Chat />` once in a layout.** The endpoint is a stateless relay that **never executes tools**:
751
+ every tool runs in the caller's own browser session, gated by guards and the approval card. Its guard is
752
+ `AgentRelayAccess`, which refuses every call until the app names a guard of its own —
753
+ `option.setAgentAccess(SignedIn)`.
754
+ - **The LLM is configured in `option.ts`, never through the environment.** `option.setLlm({ apiKey, model, host })`
755
+ fills whichever adaptor holds `LlmAdaptorRole`; swap providers with `option.applyAdaptor(LlmAdaptorRole, X)`.
756
+ - **Declare the tool beside the control that already does it.**
757
+ `st.tool("x").desc("…").arg("id", ID).opt("force", Boolean).exec(fn)` publishes one action and returns the
758
+ callable to hand to `onClick` — one handler for the person and the agent. `.desc()` is required and comes first,
759
+ `.arg()` is an argument the caller must pass and `.opt()` one it may, and `.exec()` is the only hook. Reach a
760
+ store action from the body (`.exec((id) => st.do.removeX(id))`); `st.do` on its own reaches nobody.
761
+ - **Publish a tool only where the screen already renders the control.** A falsy name declares the tool without
762
+ publishing it, which is how a conditional surface stays legal — `.exec()` is a hook, so withhold the name, not
763
+ the call. A `disabled` control publishes nothing.
764
+ - **A component that renders once per row takes the row id as an argument**, never a closure over it — fifty
765
+ registrations of one name leave forty-nine shadowed. The description is what makes the repeats one declaration:
766
+ same name and same `.desc()` is the same tool registered fifty times, and only a second description under that
767
+ name warns.
768
+ - **`{ settle: false }` is the read that returns what is already there.** Every other tool is waited out before
769
+ what it did to the screen is reported, because a write may still be landing when `exec` resolves. `{ confirm }`
770
+ and `{ guard }` ride the same object; a `remove*` name confirms by default.
771
+ - **A component that can render twice on one screen takes a `namespace` prop** (`Tab`, `Dialog`, `Dropdown`,
772
+ `ScreenNavigator`) and publishes nothing without one.
773
+ - **Forms publish themselves**: a `Field.*` / `Input.*` / `Select` / `Switch` handed `onChange={st.do.setXOnY}`
774
+ **by reference** publishes that setter, and `st.use.yForm()` adds `fillYForm(patch)`. An inline arrow publishes
775
+ nothing — normalize with the control's `transform` prop, and multi-write with a `_postSet<Field>` store method.
776
+ - **Reading is per key, not per store.** `st.use.x({ agent: false })` keeps a subscribed key off the surface;
777
+ a key no component reads is unreadable. A value with populated `hidden`/`secret` fields is refused at read.
778
+ - **A component's own value is published by its declared type.** `st.expose("taskId", ID).desc("…").value(v)` for
779
+ a derived value and `st.useState("tab", String, { set: true }).desc("…").init("all")` for local state — the type
780
+ typechecks what is handed over and decides how it is read, so a model class masks by that model. `Any` is the
781
+ escape hatch: no typecheck, and the value passes untouched. `.value()` also takes a thunk, read when the agent
782
+ reads.
783
+ - **Return what answers the question, not the record.** A tool's value is capped at 20,000 characters before it
784
+ enters the transcript and clipped with a note the model reads (`ToolOutput.limit`), because a store key is sized
785
+ for a screen and not for a model's window: one `readState` of a list whose rows carry inlined bytes is megabytes,
786
+ and from there it rides *every* later turn. Each tool row in the chat carries its own estimated token cost and
787
+ opens onto the value the model was handed — that row is where a conversation that filled up in four messages
788
+ explains itself. A field that is bulky and useless to a model is fixed once at the model instead:
789
+ `field.visual(String)` keeps it on the page and out of every agent-facing read and MCP result.
790
+ - **`<Agent.Skip label="site footer">` leaves a region out of the default `readScreen`** — a footer, a cookie
791
+ banner, a nav that repeats on every route. `[skipped: site footer]` stands in its place rather than nothing, since
792
+ a deleted region reads as an absent one, and the marker's name is a `section` that reads the region on request.
793
+ Put `data-agent-skip="<name>"` on the element the page already renders where a wrapper div would move the layout.
794
+ It hides **text, not behaviour**: an `st.tool` inside is published as before. Narrowing from the other side —
795
+ `Agent.Zone`, `readScreen({ section })` — is the better move on a screen that is mostly chrome.
796
+ - **Route guidance is `<Agent.Guide instructions="..." />`** rendered from a `_layout.tsx` or page — the render
797
+ tree is the cascade. It is a component, not a `pageConfig` field, and `*.abstract.md` is never served to agents.
798
+ - The framework publishes five built-ins on every store surface — `navigate`, `goBack`, `readScreen`,
799
+ `readState(key)`, `highlight(target)` — plus `askUser`, which the session owns. **There is no general-purpose
800
+ wait**: publish an `st.tool` beside the control that starts the work and let it await the work.
801
+ - **Model-facing text is English, always** — every `.desc()`, `instructions`, Guide text. The `l()` rule covers
802
+ strings a *user* reads.
803
+
804
+ Full contract — the chat's own options, attachments and speech, zones, what `akanjs/ui` publishes for you, slash
805
+ commands, transcript compaction, and the built-in tool semantics: `get_guideline` with `agentRule`, or
806
+ `akan guideline show agentRule`.
807
+
808
+ ## Scalar Modeling (`**/*.constant.ts`)
809
+
810
+ - Define Akan models in `.constant.ts` files with `via` from `akanjs/constant`.
811
+ - Use `Int` for whole-number counts and quantities; use `Float` only for values that need decimals.
812
+ - Use `ID` for document references and prefer explicit structured fields over `Any` unless the content is genuinely flexible.
813
+ - For date defaults, prefer a function such as `default: () => dayjs()` so the value is created at runtime.
814
+ - Follow the established model layering pattern in this order: `Input`, `Object`, `Light<Model>`, full `<Model>`, and `<Model>Insight`. Write all five, and write `<Model>Insight` even when it is empty.
815
+ - Put display and predicate logic on the `Light<Model>` class rather than in a util module — see Module File Playbook.
816
+ - Defaults are a literal for scalars and a thunk for anything constructed. Arrays are `field([T])`; optional is the postfix `.optional()`.
817
+ - **`field.visual(T)` is a field the page renders and an agent never sees** — a blur placeholder, a rendered HTML body, a serialized geometry. It stays an ordinary stored `property` (persistence, search, forms and the page response untouched) and is stripped wherever a value is masked for an AI caller: every in-page-agent read and every MCP result, along with the MCP readable schema. Unlike `hidden`/`secret` it is cost, not secrecy — nothing is refused over one. Reach for it whenever a field is bulky and useless to a model; that is cheaper than every tool learning to avoid it.
818
+
819
+ ### Scalar & Field Type Reference
820
+
821
+ - **Import from `akanjs/base`** (real classes/helpers, not globals): `Int`, `Float`, `ID`, `Any`, `Binary`, `Upload`, `enumOf`, and the `dayjs` factory. There is **no `JSON` scalar** — use `Any` for open/flexible payloads.
822
+ - **Use the JS globals directly (no import needed)**: `String`, `Boolean`, `Date`. They are monkey-patched to behave like scalars, so `field(String)` typechecks.
823
+ - **`Number` is not a valid field/body type.** `NumberConstructor` is intentionally not augmented, so `field(Number)` / `.body("x", Number)` fails to typecheck. Use `Int` or `Float` instead.
824
+ - Runtime resolution of every scalar (globals included) goes through `PrimitiveRegistry` by `refName` (`pkgs/akanjs/base/primitiveRegistry.ts`).
825
+ - **`Binary` is raw bytes on the wire, and never a model field.** It is `Uint8Array` on both sides — a Node
826
+ `Buffer` is one, so a server handler may pass one straight in — and it accepts base64 in either direction, so
827
+ the same declaration serves a JSON body and a binary frame. It is **not storable**: the class build refuses
828
+ `field(Binary)` naming the `File` model instead, because every non-base field lives in the `_doc` JSON column
829
+ and bytes would sit there as base64 and ride every read of the row. Never send bytes as `Any` — `Any` passes a
830
+ `Buffer` through untouched and `JSON.stringify` then spells it `{ type: "Buffer", data: number[] }`, which
831
+ `JSON.parse` never restores; the result is 3.6x the wire and a shape that only breaks at the first byte-offset
832
+ read. MCP refuses a `Binary` return for the same reason it refuses `Any`.
833
+
834
+ ### Text Search Fields — the `text` role
835
+
836
+ - A field joins the full-text index by declaring one of five roles: `field(String, { text: "title" })`, and likewise
837
+ `"desc"`, `"tag"`, `"thumb"`, `"filter"`. Nothing else opts a field in, and there is no per-model switch.
838
+ - Pick the role by what the value *is*, because `bm25` weights them positionally (`title` 10, `tag` 3, `desc` 1,
839
+ `filter` 0): `title` is the one line a human scans for, `desc` is prose, `tag` is a keyword list, `filter` is a
840
+ scoping value (status, owner, role) that must be matchable but must never outrank a real title hit.
841
+ - `thumb` is mirrored for rendering a hit and is **not** indexed — never expect it to match.
842
+ - **`field.secret`, `field.hidden` and `resolve()` take no `text` role — it is a compile error**, because the
843
+ mirror is plaintext and an indexed secret would leak through search. The class build throws the same refusal as
844
+ a backstop, for an option object the excess-property check cannot see through, and names the way out: drop the
845
+ role, or leave the field unmasked. Do not work around either. The throw also covers a `text` field *underneath*
846
+ one of those — a scalar's own field is reachable through its parent, so `f.secret(Noti)` where `Noti.label`
847
+ carries a role is rejected at the parent, not silently indexed; that one is only reachable at runtime, since the
848
+ pairing spans two files.
849
+ - The role works on a relation too (`image: field(File, { text: "thumb" })`) and on an array (`playing: field([String],
850
+ { text: "tag" })`); an array of objects indexes by leaf key, including an array leaf (`works[*].tags`). A field
851
+ inside a `Map` indexes nothing: there is no fixed path to extract it from.
852
+ - Declaring roles is all the wiring there is. Mirror rows are maintained by SQL triggers — not document hooks —
853
+ because `updateOneByQuery` and friends fire no hooks, and most searchable-field mutations go through exactly that
854
+ path.
855
+ - Search runs on sqlite/libsql only. `q.search()` against Postgres throws, loudly, rather than returning every row.
856
+ - `AKAN_SEARCH_ENABLED=0` switches the index off process-wide; unset means on. It never deletes mirror data, and
857
+ re-enabling reconciles every ref. **Give every process the same value** — a process cannot drop triggers for models
858
+ it does not mount, so a mixed fleet leaves stale triggers behind.
859
+ - The tokenizer is `AKAN_SEARCH_TOKENIZER` (or `database.search.tokenizer`, which wins), defaulting to
860
+ `unicode61 remove_diacritics 2`. Changing it rebuilds the index from the mirror on the next boot — the model
861
+ tables are never re-read — so it is a safe knob, unlike a `text` role change, which re-reads every row. The
862
+ rebuild takes no cross-process claim, so a fleet restarted at once repeats it in every process; stagger the
863
+ restart when the mirror is large.
864
+
865
+ ### Image & File Fields
866
+
867
+ - **Do not declare `Upload` as a model field.** `Upload` is a signal-body-only primitive (see Service And Signal Conventions). Models reference the `File` model instead.
868
+ - Declare an image/file field as a relation to `File`: `image: field(File).optional()` for one, `images: field([File])` for many.
869
+ - The store then auto-generates an `upload<Field>On<Model>(fileList)` action that calls the framework upload mutation and polls file status until it leaves `"uploading"` (`pkgs/akanjs/store/action.ts`).
870
+ - Storage is wired through the `StorageAdaptor` DI role (default `BlobStorage`, `pkgs/akanjs/service/predefinedAdaptor/storage.adaptor.ts`); the reference implementation is the `file` module. Do not hand-roll data-URL fallbacks.
871
+
872
+ ### Cascade Remove — the `cascade` option
873
+
874
+ **The value names the direction, and getting it wrong is a data loss.** The two actions can sit on the same field
875
+ shape, so `cascade` never means "related" — it means one of exactly these:
876
+
877
+ - `removeRef` — *when I am removed, remove what this field points at.* Declared on the relation the owner holds:
878
+ `image: field(File, { cascade: "removeRef" })`, arrays included. Only a relation accepts it; a primitive, a bare
879
+ `ID`, and a scalar each fail the class build.
880
+ - `removeWith` — *when what this field points at is removed, remove me.* Declared on the child's own reference to
881
+ its owner, so the owner never learns about its children and a lib model can be extended by an app's. Three forms:
882
+ a relation (`field(AgentSession, { cascade: "removeWith" })`), an id with `ref`, or a polymorphic id with
883
+ `refPath`. An array, a Map, `ref` together with `refPath`, and a field naming no owner each fail the class build.
884
+ - **A `refPath` must name an `enumOf` field** — a free-form owner type is unknowable at build time, so every
885
+ model's removal would have to sweep the polymorphic table on the chance it is the owner.
886
+ - **A cascade goes through the target's service, never its model** — that path is what runs the target's
887
+ `_postRemove`, where a module puts the side effect the removal has to carry.
888
+ - **Bulk is decided at boot, per target model, for both directions.** A target with no `remove` schema hook, no
889
+ `_pre`/`_postRemove`, no cascade of its own, and no children takes one `removeManyByQuery`. Adding a
890
+ `_postRemove` silently flips it back to one document at a time — the boot log is the only place that shows.
891
+ - **The plan is sealed after every service is live**, so a `listenPost("remove")` registered in `onInit` counts,
892
+ and an unmounted `removeRef` target or `removeWith` owner fails the boot rather than the first removal. An
893
+ unmounted `refPath` candidate only warns, since that list spans optional modules by design.
894
+ - **Nothing checks whether another document still references the same target.** `File` in particular is deduped
895
+ by `origin`, so two parents can share one row; `removeRef` claims the field owns its target exclusively.
896
+ - Removal is soft (`removedAt`), but the storage delete a `_postRemove` performs is not — a cascade is not
897
+ restorable, and reviving the owner does not revive what went with it.
898
+ - A `removeWith` declaration **auto-creates its index** (`{ removedAt, fk }`, or `{ removedAt, typeKey, fk }` when
899
+ polymorphic); every non-base field lives in the `_doc` JSON column, so the lookup would otherwise scan the table.
900
+ - **Query-level removes fire no hooks and therefore no cascade** — `removeManyByQuery` / `updateManyByQuery`, the
901
+ generated `remove<Filter>` / `update<Filter>`, and the facade's `removeById` / `updateById`. Remove one document
902
+ at a time when it cascades.
903
+ - Cascades are **idempotent**: `removedAt IS NULL` is ANDed into every query-level write. Cycles are cut by a
904
+ visited set carried down the whole chain, with a depth cap of 16.
905
+
906
+ ## Akan Page Routing (`apps/**/page/**`)
907
+
908
+ - `apps/<app>/page` may contain route modules only. Do not add helper logic or component-only files there.
909
+ - Route source files under `page/` must use `.tsx`. Do not add `logic.ts`, `.js`, or `.jsx` files under `page/`.
910
+ - A route page is either `<routeName>.tsx` (serving `/routeName`) or a directory's `_index.tsx`; layouts use `_layout.tsx`; per-route UI overrides use `_overrides.tsx`.
911
+ - Reserved `_*.tsx` route filenames are limited to `_index.tsx`, `_layout.tsx`, and `_overrides.tsx`; do not add files like `_Component.tsx` or `_helper.tsx`.
912
+ - Page filenames must not start with an uppercase letter. Move helper components like `Component.tsx` to app `ui`, `common`, or `lib` instead.
913
+ - Dynamic segments use `[id]`; route groups use directories like `(user)`, `(public)`, `(tab)`, or `(detail)`.
914
+ - Page modules should usually export `default`, `pageConfig`, `head`, `generateHead`, or `Loading`.
915
+ - `_overrides.tsx` is a logic-free UI-override manifest: imports plus a single `export default override({ Slot: AppComponent })` (from `akanjs/ui`), no `"use client"`. It re-skins framework `akanjs/ui` components for its route subtree; nested manifests merge over ancestors slot-by-slot (closest wins). See the UI Customization reference for the slot list.
916
+ - Prefer `export default function Page` or `export default async function Page` for page components.
917
+ - `libs/<lib>/page` follows the same rules and ships routes to apps that opt in with `syncPageLibs` in `akan.config.ts`: `true` takes every lib dep that has a `page` folder, an array takes the libs listed, `false` (the default) syncs nothing.
918
+ - `akan sync` links those routes into `apps/<app>/page/(libs)/(<lib>)` — once per basePath when the app declares subRoutes. The folder is generated and gitignored; edit the lib source, never the link.
919
+ - Both path segments are route groups, so a lib route mounts at its own path (`libs/<lib>/page/login/_index.tsx` serves `/login`). Two synced routes that resolve to the same pattern are a sync-time error.
920
+ - `export const pageConfig = { devOnly: true }` keeps a route out of `akan build` while it keeps serving under `akan start` and keeps being typechecked. On a `_layout.tsx` it excludes every route under that directory too. Write it as a literal `true`/`false` — the build reads it off the source without evaluating the module.
921
+ - Before changing route behavior, check `pkgs/akanjs/server/routeTreeBuilder.ts` and nearby routes for the expected pattern.
922
+
923
+ ### Page Body Shape
924
+
925
+ ```tsx
926
+ interface PageProps {
927
+ params: { orgId: string };
928
+ }
929
+
930
+ export default async function Page({ params }: PageProps) {
931
+ const { l } = usePage();
932
+ getSelf({ unauthorize: "/signin" });
933
+ const { orgId } = params;
934
+ const [{ org }, { taskInitInOrg }] = await Promise.all([fetch.viewOrg(orgId), fetch.initTaskInOrg(orgId)]);
935
+ return <Task.Zone.Card init={taskInitInOrg} prefix={`/org/${orgId}`} />;
936
+ }
937
+
938
+ export const pageConfig = { transition: "stack" } satisfies PageConfig;
939
+ ```
940
+
941
+ - There is no `loader=` / `render=` page prop. Pages are `export default async function Page`.
942
+ - Declare `interface PageProps { params: {...}; searchParams?: {...} }` immediately above the default export.
943
+ - Body order: `usePage()`, auth, destructure params, fetch, return.
944
+ - Run independent fetches through `Promise.all`, even when there is only one.
945
+ - Gate auth at `_layout.tsx`; repeating `getSelf({ unauthorize: "/signin" })` in the page is fine and common.
946
+ - Keep `async` even when nothing is awaited — it marks a real server page.
947
+ - No `useState`, no `useEffect`, and no comments in page files.
948
+
949
+ ## Akan Sync Conventions (`apps/**`, `libs/**`)
950
+
951
+ - `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`.
952
+ - `apps/<appName>` root may only contain these folders: `.akan`, `android`, `common`, `env`, `ios`, `lib`, `mobile`, `page`, `plugin`, `private`, `public`, `script`, `secrets`, `srvkit`, `ui`, `webkit`.
953
+ - `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`.
954
+ - `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.
955
+ - Both allowlists have one source — `pkgs/@akanjs/devkit/workspaceLayout.ts`. `akan sync` (error), `akan doctor`
956
+ (diagnostic), and `akan quality scan` (warning) all read it, so add a new root entry there and mirror it into this
957
+ list, never into one of the three call sites.
958
+ - `akan sync` maintains a scoped agent guide per app/lib: `apps/<app>/AGENTS.md` / `libs/<lib>/AGENTS.md`. The
959
+ section between the `akan:agent` markers (the `## Recipes In Scope` index) is generated — do not hand-edit it;
960
+ content outside the markers is yours. `akan lint` fails when the generated section is stale.
961
+ - The `plugin/` facet holds Akan plugin declarations; files use the `<name>.plugin.ts` convention (e.g. `pushNotification.plugin.ts`) and are re-exported from the generated `plugin/index.ts` barrel.
962
+ - Do not add `apps/*/base` or `libs/*/base`; place shared utilities under that app or lib's own `common/`.
963
+ - `apps/*/lib` and `libs/*/lib` root files are limited to generated/support files: `cnst.ts`, `db.ts`, `dict.ts`, `option.ts`, `sig.ts`, `srv.ts`, `st.ts`, `useClient.ts`, `useServer.ts` — plus a `<model>.signal.test.ts` / `.spec.ts`, the one hand-written file that belongs there because the suite boots the whole barrel.
964
+ - Domain module folders are `lib/<model>` for database modules, `lib/_<service>` for service modules, and `lib/__scalar/<scalar>` for scalar modules.
965
+ - Database module UI files are limited to `<Model>.Template.tsx`, `<Model>.Unit.tsx`, `<Model>.Util.tsx`, `<Model>.View.tsx`, and `<Model>.Zone.tsx`.
966
+ - Service module UI files are limited to `<Service>.Util.tsx` and `<Service>.Zone.tsx`.
967
+ - Scalar module UI files are limited to `<Scalar>.Template.tsx` and `<Scalar>.Unit.tsx`.
968
+ - Module `*.test.ts`, `*.test.tsx`, `*.spec.ts`, and `*.spec.tsx` files are allowed.
969
+ - `ui/index.ts`, `webkit/index.ts`, `srvkit/index.ts`, `common/index.ts`, `plugin/index.ts`, and module `lib/**/index.ts` files are generated by scanSync; do not hand-edit or track them.
970
+ - Generated facet indexes export only 1-depth files/folders with `export * from "./name";`.
971
+ - `libs/<libName>` may hold a `page` folder of route modules; scanSync links it into every app that opts in with `syncPageLibs`, so `apps/*/page/**/(libs)` is generated and gitignored like `public/libs`.
972
+
973
+ ## Layer Placement (`common/`, `webkit/`, `srvkit/`, `ui/`)
974
+
975
+ | Folder | Admission test | Naming |
976
+ |---|---|---|
977
+ | `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 |
978
+ | `webkit/` | touches `window` / `navigator` / Capacitor, or is a React hook | `use<Thing>.tsx` — `.tsx` even with no JSX |
979
+ | `srvkit/` | touches `node:*`, `Bun`, `process.env`, a secret, or a server SDK | camelCase file, PascalCase class |
980
+ | `ui/` | renders JSX and is not bound to one model | PascalCase component, camelCase sidecar (`swipeCard.util.ts`) |
981
+ | `plugin/` | build- or CLI-time `AkanPlugin` | `<name>.plugin.ts`, registered in `akan.config.ts` |
982
+
983
+ - Hooks return a named object of async closures, never a tuple.
984
+ - `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.
985
+ - 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.
986
+ - `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.
987
+
988
+ ## Present In The Code — Do Not Imitate
989
+
990
+ Older files contain these. They are warts, not conventions: do not copy them forward, and prefer the newer neighbour
991
+ when two shapes disagree.
992
+
993
+ - `children: any` — newer files use `children: ReactNode`.
994
+ - Hard-coded API keys or secrets in source; they belong in `option.ts` or env.
995
+ - Large blocks of commented-out code left in place.
996
+ - Cross-store writes through a `RootStore` cast — it collides with the Bun SSR-bundler segfault.
997
+ - Stale `// TODO: Implement …` comments above implemented methods.
998
+ - `{cond && <X/>}` in JSX, hard-coded Korean bypassing `l()`, and `window.alert(...)` for user feedback.
999
+ - Bare `/* eslint-disable */` blocks — use `// biome-ignore lint/<rule>: <why>`.
1000
+ - Raw palette grays such as `text-gray-400` instead of the semantic tokens (`text-foreground/70`).
1001
+
1002
+ ## Secrets And Env Safety (`.env`, `infra/**`, `*secret*`, `*credential*`)
1003
+
1004
+ - Never print, summarize, commit, or expose real secret values, credentials, tokens, private keys, or `.env` contents.
1005
+ - If env keys are needed for documentation, list only key names and example placeholders, not live values.
1006
+ - Preserve the existing env/secret flow through root scripts such as `bun run downloadEnv`, `bun run uploadEnv`, `bun run downloadSecret`, and `bun run uploadSecret`.
1007
+ - When editing infra env or secret scripts, keep Jenkins and deployment assumptions intact unless the task explicitly asks to change them.
1008
+ - Treat generated env/secret artifacts as sensitive even when they are not named `.env`.
1009
+
1010
+ ## Application Test Commands
1011
+
1012
+ - After changing application source code, test the app with `bun run akan start <appName>`.
1013
+ - Test production build generation with `bun run akan build <appName>`.
1014
+ - To test a built artifact locally, run it from the generated app directory with the required Akan runtime environment variables.
1015
+
1016
+ ```bash
1017
+ cd dist/apps/<appName> && USE_AKANJS_PKGS=true AKAN_PUBLIC_REPO_NAME=<repo> AKAN_PUBLIC_SERVE_DOMAIN="<domain>" \
1018
+ AKAN_PUBLIC_APP_NAME=<appName> AKAN_PUBLIC_ENV=local AKAN_PUBLIC_OPERATION_MODE=local SERVER_MODE=federation \
1019
+ AKAN_PUBLIC_BASE_PATHS=<basePaths> bun main.js
1020
+ ```