@akanjs/cli 3.1.0-rc.4 → 3.1.0-rc.6

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 (89) hide show
  1. package/.build-stamp +1 -1
  2. package/{CodeAgent-ymte1tv8.js → CodeAgent-rkjyxgec.js} +2 -2
  3. package/{CodeTui-4rhdm1qw.js → CodeTui-h7pyj6xa.js} +1 -1
  4. package/TunnelShare-661cxz5e.js +14 -0
  5. package/{agent.command-r5x1mch5.js → agent.command-3qxf4xcj.js} +5 -5
  6. package/akanCode.js +2 -2
  7. package/{application.command-3dkr7rdz.js → application.command-m1kcd7jf.js} +12 -11
  8. package/{applicationBuildRunner-h0j2swx1.js → applicationBuildRunner-dj0xkrep.js} +16 -9
  9. package/buildBatch.proc.js +7 -7
  10. package/{cloud.command-4kyxfw0y.js → cloud.command-3jzym8q7.js} +17 -16
  11. package/cloud.script-r4nskbc6.js +24 -0
  12. package/{code.command-sjvzt8fc.js → code.command-0ad6gw2a.js} +6 -6
  13. package/{context.command-j9frz0dz.js → context.command-0qx447mb.js} +17 -16
  14. package/context.runner-1vsjztjg.js +25 -0
  15. package/{dependencyScanner-06z2dmrr.js → dependencyScanner-3afj2vt1.js} +1 -1
  16. package/{guideline.command-1epw6hey.js → guideline.command-cv5bmrx2.js} +2 -2
  17. package/guidelines/conventions/conventions.instruction.md +450 -1084
  18. package/guidelines/cssRule/cssRule.instruction.md +16 -1
  19. package/guidelines/fieldRule/fieldRule.instruction.md +57 -0
  20. package/guidelines/framework/framework.instruction.md +4 -7
  21. package/guidelines/mcpRule/mcpRule.instruction.md +11 -8
  22. package/guidelines/modelConstant/modelConstant.instruction.md +2 -1
  23. package/guidelines/modelStore/modelStore.instruction.md +9 -0
  24. package/guidelines/pageRule/pageRule.instruction.md +80 -0
  25. package/guidelines/queryRule/queryRule.instruction.md +35 -8
  26. package/guidelines/runtimeRule/runtimeRule.instruction.md +89 -2
  27. package/guidelines/ssrRule/ssrRule.instruction.md +13 -0
  28. package/guidelines/transportRule/transportRule.instruction.md +76 -8
  29. package/guidelines/workspaceOnboarding/workspaceOnboarding.instruction.md +4 -4
  30. package/guidelines/workspaceRecipes/workspaceRecipes.instruction.md +49 -4
  31. package/incrementalBuilder.proc.js +4 -4
  32. package/{index-jw6st2rd.js → index-192ha4xh.js} +356 -307
  33. package/{index-1hkr7mrb.js → index-1p98exm2.js} +2 -2
  34. package/{index-8byzcvxp.js → index-1xj2t7cb.js} +7 -7
  35. package/{index-er3hekv9.js → index-20bxxyvn.js} +80 -80
  36. package/{index-xe3x2kw5.js → index-35hc95h5.js} +1 -1
  37. package/{index-p5nrc5he.js → index-4mhy6qme.js} +4 -4
  38. package/{index-0qc5nd61.js → index-5edzv8fn.js} +5 -5
  39. package/{index-k9rrten6.js → index-5nd1wtkv.js} +92 -94
  40. package/index-62md4dwy.js +260 -0
  41. package/{index-z3v7abrk.js → index-665zp9rs.js} +38 -15
  42. package/{index-1t2xw0p0.js → index-6xn2n442.js} +4 -4
  43. package/{index-jg3fj8h0.js → index-86jt2a0e.js} +1 -1
  44. package/{index-k33me5sp.js → index-8fp3b47h.js} +419 -418
  45. package/{index-shastb7m.js → index-9z426msb.js} +17 -12
  46. package/{index-gdybjw3x.js → index-awbdfcf2.js} +4 -4
  47. package/{index-3hx1z96c.js → index-bqbf654f.js} +1 -1
  48. package/{index-jppthsff.js → index-cpb3nvma.js} +57 -58
  49. package/{index-b60ccq2s.js → index-d47ypebv.js} +17 -15
  50. package/{index-gc05vqd2.js → index-d72hfmdm.js} +13 -13
  51. package/{index-akha197v.js → index-fmzcs8v6.js} +4 -4
  52. package/{index-28ftaz3d.js → index-fyq1z6kt.js} +29 -27
  53. package/{index-vwmv64ad.js → index-g1mbppa9.js} +238 -236
  54. package/{index-hedt951c.js → index-h47cf1gg.js} +7 -7
  55. package/{index-hfqs9jvc.js → index-j4mkhz93.js} +3 -3
  56. package/{index-08a5ga6n.js → index-jw1zm273.js} +28 -30
  57. package/{index-q958tqqg.js → index-m9jj544v.js} +1 -1
  58. package/{index-8935h3n1.js → index-mdjc93e1.js} +1 -1
  59. package/{index-2wyn17fa.js → index-pydbnfez.js} +2 -2
  60. package/{index-bjh4wbtk.js → index-qwa5k1j8.js} +1 -1
  61. package/{index-81a1ywp6.js → index-tamvyvj3.js} +3 -3
  62. package/{index-dez4t5sd.js → index-trpasv9q.js} +260 -259
  63. package/{index-2ahj896a.js → index-tvehn2sp.js} +1 -1
  64. package/{index-2k0t8p3p.js → index-va06nr10.js} +1086 -1075
  65. package/{index-9c3cd767.js → index-vk0pa0mn.js} +20 -21
  66. package/{index-qyaqzh8e.js → index-w44cy4vs.js} +2216 -2216
  67. package/index.js +22 -22
  68. package/{library.command-n58dgakb.js → library.command-erp2f56n.js} +3 -3
  69. package/{localRegistry.command-f2768sww.js → localRegistry.command-xxpqgxjw.js} +15 -14
  70. package/{module.command-xzjg5k7n.js → module.command-gs2r615e.js} +5 -5
  71. package/{package.command-24sh1j1q.js → package.command-7343f74r.js} +4 -4
  72. package/package.json +2 -2
  73. package/{page.command-fqnm88na.js → page.command-4h4a8pnh.js} +3 -3
  74. package/{pipeline.command-vn6taz75.js → pipeline.command-a2nrk26m.js} +3 -3
  75. package/{primitive.command-4j1g1a0d.js → primitive.command-w6t0asqz.js} +6 -6
  76. package/{projectTypecheck-3y7ayty0.js → projectTypecheck-f534enqj.js} +1 -1
  77. package/{quality.command-5hqmcdwt.js → quality.command-77x7vbbx.js} +4 -4
  78. package/{repair.command-rw5tk460.js → repair.command-y6sstev7.js} +5 -5
  79. package/{scalar.command-0jpap45m.js → scalar.command-20b8cy0k.js} +4 -4
  80. package/{subspace.command-y7n9sghk.js → subspace.command-92xkc0s0.js} +11 -10
  81. package/templates/lib/cnst.ts +0 -1
  82. package/templates/workspaceRoot/.gitignore.template +24 -53
  83. package/{tunnel.command-5gjwh1f8.js → tunnel.command-2kw8vt9g.js} +8 -7
  84. package/{workflow.command-3t22f7eb.js → workflow.command-2djwmpqf.js} +10 -10
  85. package/{workspace.command-cq5xg2jq.js → workspace.command-5se6spqb.js} +28 -27
  86. package/TunnelShare-p8ymkww8.js +0 -13
  87. package/cloud.script-jc3hc5d9.js +0 -23
  88. package/context.runner-v0def5my.js +0 -24
  89. package/index-n68jx79w.js +0 -120
@@ -11,13 +11,28 @@ Use TailwindCSS with Akan's semantic design-token layer and the `akanjs/ui` prim
11
11
  - Compose classes with `cn` from `akanjs/client` (token-aware tailwind-merge) — the only class-combining function. No object syntax: write `cond && "x"`.
12
12
  - Forward `className` last so callers can extend styles.
13
13
 
14
+ ## Color Vocabulary — What Lint Refuses
15
+ - Vocabulary closure strips the raw Tailwind palette, so these render as no CSS and fail lint: raw palette classes
16
+ (`bg-blue-500`, `text-gray-400`), arbitrary color values (`bg-[#3b82f6]`), an arbitrary class built by
17
+ interpolation, daisyUI legacy classes (`btn-primary`, `card-body`) and its dropped color slots (`bg-base-100`,
18
+ `text-base-content`, `text-primary-content`, `bg-error` — use `background`/`muted`/`border`, `foreground`,
19
+ `<color>-foreground`, `destructive`), and color literals in `style={{...}}` (`no-raw-palette-class`,
20
+ `no-arbitrary-color`, `no-daisyui-legacy-class`, `no-inline-color`, `no-interpolated-arbitrary-class`).
21
+ - A legitimate fixed color (OS-chrome mockups, data-viz) takes a `// biome-ignore lint/plugin: <reason>`.
22
+ - Color literals in SVG color attributes (`fill`, `stroke`, `stopColor`, `floodColor`, `lightingColor`, `color`) and
23
+ in `el.style.<prop> = …` / `el.style.setProperty(…)` writes are flagged as warnings for now; `currentColor`,
24
+ `none`, `transparent`, `var(--…)` and `url(#id)` stay legal.
25
+ - Class order is the sorter's: `nursery/useSortedClasses` is an error and also sorts the string arguments to `cn()`.
26
+ Output such as `font-bold text-2xl text-foreground` or `border-foreground/5 border-t` is correct — write the
27
+ classes in any order, run the formatter, leave the result.
28
+
14
29
  ## Recipe Layer
15
30
  - Reusable or variant-like styling belongs in a recipe (a cva-style factory), not repeated inline class stacks.
16
31
  - Framework primitives ship recipes: `buttonRecipe` and `badgeRecipe` from `akanjs/ui`. Call them as `buttonRecipe(variants, className?)` — the second argument is merged automatically, so you never wrap it in `cn()`:
17
32
  - `buttonRecipe({ variant: "primary", size: "lg" }, "w-full rounded-2xl")`
18
33
  - Prefer the `akanjs/ui` primitives (`Button`, `Badge`, `Input`, `Field`, `Table` …) over re-implementing a component's look with utility stacks. There is **no `Card`/`Box` primitive** — a card/box is a recipe surface (e.g. `appCard`), not an `akanjs/ui` component; do not `import { Card }`.
19
34
  - App-specific repeated surfaces go one-per-file in `apps/<app>/ui/Recipe/` (server-safe, no `"use client"`) via `recipe(tv({ base, variants }))`, imported from `@apps/<app>/ui`. App recipes **extend** (surfaces the lib lacks — chat bubble, tile); they never re-define a lib component in parallel.
20
- - Full recipe authoring/consumption policy: the `recipeRule` guideline. Never guess recipe names/imports — the authoritative list is `AGENTS.md` `## UI Recipes` plus the `list_recipes` tool.
35
+ - Full recipe authoring/consumption policy: the `recipeRule` guideline. Never guess recipe names/imports — the authoritative list is `AGENTS.md` `## Recipes` plus the `list_recipes` tool.
21
36
 
22
37
  ## Customization Decision (where a design delta goes)
23
38
  Screen code is invariant — a plain `<Button variant="primary">` never changes. Only these config files do:
@@ -10,8 +10,65 @@ Use this whenever generating constants or scalar constants. It defines the curre
10
10
  - `field.hidden(Type).optional()` is for internal state that should not be treated as a normal public field.
11
11
  - `field.secret(Type).optional()` is for sensitive values that should not be selected by default.
12
12
  - Neither survives an endpoint response: both are stripped on the way out and hydration writes `null` over the key, while the generated type still declares the field — so a client holds a deliberate `null` behind a type that promises a value, and only `??` / `== null` catch it. A field a browser has to read is neither.
13
+ - `field.visual(Type)` is a field the page renders and an agent never sees (below).
13
14
  - `field([Type])` defines arrays; provide empty array defaults when the app expects list operations.
14
15
 
16
+ ## Field Types
17
+ - **Import from `akanjs/base`** (real classes/helpers, not globals): `Int`, `Float`, `ID`, `Any`, `Binary`, `Upload`,
18
+ `ImageResult`, `enumOf`, and the `dayjs` factory. There is **no `JSON` scalar** — use `Any` for open/flexible
19
+ payloads, and only when the content is genuinely flexible.
20
+ - **Use the JS globals directly (no import needed)**: `String`, `Boolean`, `Date`. They are monkey-patched to
21
+ behave like scalars, so `field(String)` typechecks.
22
+ - **`Number` is not a valid field/body type.** `NumberConstructor` is intentionally not augmented, so
23
+ `field(Number)` / `.body("x", Number)` fails to typecheck. Use `Int` for whole-number counts and quantities and
24
+ `Float` only for values that need decimals.
25
+ - Runtime resolution of every scalar (globals included) goes through `PrimitiveRegistry` by `refName`
26
+ (`pkgs/akanjs/base/primitiveRegistry.ts`).
27
+ - **`Binary`, `Upload` and `ImageResult` are never model fields.** The class build refuses `field(Binary)` naming
28
+ the `File` model instead, because every non-base field lives in the `_doc` JSON column and bytes would sit there
29
+ as base64 and ride every read of the row. How each travels on the wire: `transportRule` and `mcpRule`.
30
+ - Defaults are a literal for scalars and a thunk for anything constructed (`default: () => dayjs()`, so the value
31
+ is created at runtime). Optional is the postfix `.optional()`.
32
+
33
+ ## Masked And Visual Fields
34
+ - **A `field.hidden` / `field.secret` value reads `null` on the client, and the type still declares it.**
35
+ `SignalContext.resolveReturn` skips both field types while it builds an endpoint's response, and hydration
36
+ writes `null` over the key rather than leaving it absent — the first branch of the loop in
37
+ `constant/getDefault.ts`, ahead of the field's own default. Every use of it typechecks, and the failure lands
38
+ wherever it is finally dereferenced instead of where it was read. **Guard with `??` or `== null`**:
39
+ `=== undefined`, a destructuring default and an optional parameter default all catch only a missing key and sail
40
+ straight past this one. A projection (`pickById(id, { secret: true })`) widens the *server's* read, never the
41
+ response. If a screen needs the value, the field is neither hidden nor secret; if it only needs to be cheap for a
42
+ model rather than unseen, that is `field.visual`.
43
+ - **`field.visual(T)` is a field the page renders and an agent never sees** — a blur placeholder, a rendered HTML
44
+ body, a serialized geometry. It stays an ordinary stored `property` (persistence, search, forms and the page
45
+ response untouched) and is stripped wherever a value is masked for an AI caller: every in-page-agent read and
46
+ every MCP result, along with the MCP readable schema. Unlike `hidden`/`secret` it is cost, not secrecy — nothing
47
+ is refused over one. Reach for it whenever a field is bulky and useless to a model; that is cheaper than every
48
+ tool learning to avoid it.
49
+ - `field.secret`, `field.hidden` and `resolve()` take no `text` search role (`queryRule`).
50
+
51
+ ## Immutable Fields
52
+ - A document-path change to an `{ immutable: true }` field is refused with `base.error.immutableField` (400,
53
+ `data.field`), which the client toasts in the user's language; query-level writes are not checked.
54
+
55
+ ## Date Fields On An Instance
56
+ - **A model instance's `Date` fields are prototype accessors, not own properties.** `user.createdAt` is still a
57
+ `Dayjs`, built on first read from a native `Date` the instance holds under a symbol. So `Object.keys(user)` and
58
+ `{ ...user }` do **not** include them, while `"createdAt" in user`, `for...in`, `JSON.stringify(user)`,
59
+ `plainFieldsOf(user)` (`akanjs/common`), `immerify`, and `deepObjectify` all do. To copy a model,
60
+ `new cnst.X().set(user)`, never a spread.
61
+
62
+ ## Image And File Fields
63
+ - **Do not declare `Upload` as a model field.** `Upload` is a signal-body-only primitive. Declare an image/file
64
+ field as a relation to `File`: `image: field(File).optional()` for one, `images: field([File])` for many.
65
+ - The store then auto-generates an `upload<Field>On<Model>(fileList)` action that calls the framework upload
66
+ mutation and polls file status until it leaves `"uploading"` (`pkgs/akanjs/store/action.ts`).
67
+ - Storage is wired through the `StorageAdaptor` DI role (default `BlobStorage`,
68
+ `pkgs/akanjs/service/predefinedAdaptor/storage.adaptor.ts`); the reference implementation is the `file` module.
69
+ Do not hand-roll data-URL fallbacks. `{ cascade: "removeRef" }` on the field removes the file with its owner
70
+ (`queryRule`).
71
+
15
72
  ## Current Akan Patterns
16
73
  - Use `String`, `Boolean`, `Date`, `Int`, `Float`, `ID`, scalar classes, model light classes, or enum classes as field types.
17
74
  - Use `enumOf()` classes for categorical values that need type-safe values and dictionary entries.
@@ -1,16 +1,13 @@
1
1
  # Akan.js Framework Guide
2
2
 
3
3
  ## Ownership
4
- - `apps/<app>` contains app code, pages, env files, module folders, UI, webkit, srvkit, and common utilities.
5
- - `libs/<lib>` contains reusable domain and utility libraries consumed by apps.
6
- - `pkgs/akanjs` contains framework facets such as base, constant, document, service, signal, store, client, ui, and CLI tooling.
4
+ - `pkgs/akanjs` facets include base, constant, document, service, signal, store, client, ui, and CLI tooling.
7
5
  - Domain behavior lives near the model folder instead of being split by technical layer first.
8
6
 
9
7
  ## Current Akan Patterns
10
8
  - Database module flow is `constant -> dictionary -> document -> service -> signal -> store -> UI`.
11
- - Scalars live under `lib/__scalar/<scalarName>` and represent embedded value objects.
12
- - Service modules live under `lib/_<serviceName>` when behavior is not centered on one stored model.
13
- - Generated registry files such as `cnst.ts`, `db.ts`, `dict.ts`, `sig.ts`, `srv.ts`, and `st.ts` are scanner outputs and should not be hand-authored.
9
+ - Scalars (`lib/__scalar/<scalarName>`) represent embedded value objects; service modules (`lib/_<serviceName>`) hold
10
+ behavior not centered on one stored model.
14
11
  - Pages and components should consume generated client/server helpers rather than duplicating model shapes.
15
12
 
16
13
  ## Codegen Rules
@@ -23,5 +20,5 @@
23
20
  When a request implies a distinct look and feel, do not stop at colors — customize both the theme and, when needed, the components.
24
21
 
25
22
  - **Theme (`apps/<app>/page/styles.css`).** The app imports Tailwind and `akanjs/ui/styles.css`, then overrides semantic token *values* per theme under `:root, [data-theme="dark"]` and `[data-theme="light"]` (`--background`, `--foreground`, `--primary`, `--muted`, `--border`, … each with a `-foreground` pair for text). The framework maps them to Tailwind color names, so `bg-primary` / `text-foreground` follow the `data-theme` attribute; corner rounding uses `--radius-box` / `--radius-field`. Fetch `get_guideline` with `cssRule` for the full token set before a deep theme pass.
26
- - **Lib tokens (`libs/<lib>/ui/tokens.css`).** Colors a lib's own components pin — a vendor brand color, a fixed surface — are declared once there as plain `:root` custom properties and compiled into every app that reaches the lib, ahead of the app's stylesheets. Reference them as `bg-[var(--kakao)]`; never copy the block into each app.
23
+ - **Lib tokens (`libs/<lib>/ui/tokens.css`).** The one CSS file a lib owns: colors its own components pin that must **not** follow the theme — a vendor brand color, a fixed surface — declared once as plain `:root` custom properties and compiled automatically into every app whose pages reach the lib, ahead of the app's stylesheets, so nothing is imported by hand. Reference them as `bg-[var(--kakao)]`; never copy the block into each app. `@theme` extensions stay in the app stylesheet, because the color vocabulary is closed per stylesheet, and theme-following colors are the app's.
27
24
  - **Components (`page/**/_overrides.tsx`).** When a default `akanjs/ui` component (Button, Modal, Table, Input, Select, …) is too restrictive for the design, re-skin it per route instead of forking, wrapping, or fighting it with utility classes. Write a drop-in replacement in `apps/<app>/ui/` typed against the slot contract (`AkanModalComponent`, or `AkanUiOverrides["<Slot>"]`), composing the framework's headless parts, then bind it in a `page/**/_overrides.tsx` manifest with a single `export default override({ Slot: BrandComponent })`. Overrides cascade down the route tree like layouts (closest ancestor wins). Fetch `get_guideline` with `componentRule` and read the `references/ui/customize` docs page for the slot list and patterns.
@@ -34,7 +34,8 @@ export class FileSlice extends slice(srv.file, {
34
34
  ```
35
35
 
36
36
  - **It is curation, not authorization.** HTTP serves the endpoint exactly as before and the guards are still the
37
- only thing deciding who may call it. Never write `mcp: false` where you meant a guard.
37
+ only thing deciding who may call it. Never write `mcp: false` where you meant a guard. To take generated CRUD off
38
+ HTTP too, write `guards: { cru: false }`.
38
39
  - **The `mcp` map on `slice()` mirrors the `guards` map above it** — same keys (`root`, `get`, `cru`, `create`,
39
40
  `update`, `remove`), same fallbacks (`create`/`update`/`remove` inherit `cru`), and the same scope: it narrows
40
41
  the root slice and the generated CRUD, and never reaches a named slice or a custom endpoint. Those declare
@@ -55,7 +56,8 @@ export class FileSlice extends slice(srv.file, {
55
56
  ## Configuration
56
57
  Settings live in the app's `lib/option.ts` — `option.setMcp({ … })`, taking `enabled`, `readOnly`, `path`,
57
58
  `version`, `instructions`, `allowedOrigins`, `pageSize`, `language`, `legacyTextBlock`, `outputSchema`, `rateLimit`,
58
- `promptBudget`, `apps`, `modern`, and `auth`. **Not `main.ts`**: the gateway
59
+ `promptBudget`, `apps`, `modern`, and `auth` — or `setMcp((env) => …)` for settings derived from the server env.
60
+ **Not `main.ts`**: the gateway
59
61
  there only spawns children, and `option.ts` is the app-authored file `server.ts` already hands to the process that
60
62
  mounts `/mcp`. Every lib's option is read in mount order with the app's last, so an app tightens what a library
61
63
  declared without restating it. Each field also has an env spelling (`AKAN_MCP`, `AKAN_MCP_READONLY`,
@@ -170,8 +172,8 @@ export default page()
170
172
  relation field as its id and the server never converts an object back, so publishing the related model's shape
171
173
  asked an agent for what the document layer cannot take. On the response side every entry inlines the transitive
172
174
  closure of every model it mentions (a `$ref` may not cross entries), and on an open catalogue 83% of the bytes
173
- were the same schemas repeated — so a nested database model is published as `{ type: "object", description:
174
- "<ModelName>" }` and the returned model keeps every field of its own; an embedded scalar stays inline. Measured,
175
+ were the same schemas repeated — so by default (`outputSchema: "shallow"`) a nested database model is published
176
+ as `{ type: "object", description: "<ModelName>" }` and the returned model keeps every field of its own; an embedded scalar stays inline. Measured,
175
177
  that took a 347-tool listing from 1194KB to 532KB, and one user's paged `tools/list` to 320KB.
176
178
  `option.setMcp({ outputSchema: "full" })` (`AKAN_MCP_OUTPUT_SCHEMA`) restores the closure; `"none"` publishes no
177
179
  `outputSchema` at all — results still ship as `structuredContent`, and the text block stays on whatever
@@ -323,9 +325,9 @@ delegates to lives in `akanjs/server` (`OAuthAuthorize`, `OAuthToken`, `OAuthReg
323
325
  (`["cursor"]` by default, for Cursor's desktop callback), `accessTokenSeconds` (3600, also how long a revoked
324
326
  access token can outlive its grant), `clientIdMetadata: { enabled, refusePrivateAddresses }` (both on; `enabled:
325
327
  false` turns Client ID Metadata Documents off and the metadata stops advertising them — Claude Code then registers
326
- dynamically instead), `issuer` and `resource` (only when a tunnel or an edge makes the derived origin wrong),
327
- `enabled: false` to take the server and the `/mcp` credential requirement away together. The signin page must
328
- honour `?redirect=` to return the user to the consent page.
328
+ dynamically instead), `issuer` and `resource` (only when a tunnel or an edge makes the derived origin wrong, since
329
+ an MCP client compares the issuer byte for byte), `enabled: false` to take the server and the `/mcp` credential
330
+ requirement away together. The signin page must honour `?redirect=` to return the user to the consent page.
329
331
  - **Clients:** Claude Code — `claude mcp add --transport http <name> <origin>/mcp` and nothing else; it registers,
330
332
  opens the browser to `http://localhost:<port>/callback`, refreshes on 401. claude.ai — a custom connector by URL
331
333
  (needs public HTTPS; DCR or the Advanced-settings client id/secret; callback
@@ -485,7 +487,8 @@ the same guards.** What stays closed is closed because no agent has a reason to
485
487
  - **Scope is what makes hiding possible.** `static scope: "account"` (a role check) is evaluated with no arguments
486
488
  and filters the listing per caller; `"resource"` (`Can<Verb><Model>`, ownership) needs `context.getArg()`, stays
487
489
  listed, and stops the call. Marking a resource guard `"account"` would make the listing lie — the marker has no
488
- default for that reason.
490
+ default for that reason. An `"account"` guard that throws at listing time reached for arguments it does not
491
+ have: the entry is hidden from every caller and the guard is named once in the log as mismarked.
489
492
  - **Identity comes from the internal args** (`.with(Self)`, `.with(Account)`, `.with(Me)`), never from an id the
490
493
  client sent, and the service re-checks ownership behind the guard.
491
494
  - **An admin who signs in on the consent page mints an admin token.** A connector created under an admin account
@@ -9,7 +9,7 @@ Generate the business data shape for a collection-backed Akan module. This file
9
9
  - Define `ModelObject` by extending `via(ModelInput, (field) => ({ ... }))` for stored fields controlled by system or service behavior.
10
10
  - Define `LightModel` by selecting compact list/reference fields from the object layer.
11
11
  - Define full `Model` by combining object and light layers and adding only small static or type helpers when needed.
12
- - Define `ModelInsight` only when aggregation or dashboard values are useful.
12
+ - Always define `ModelInsight`, empty when the model has no aggregation or dashboard values yet.
13
13
 
14
14
  ## Current Akan Patterns
15
15
  - Use `via()` and the `field` helper for all model layers.
@@ -21,6 +21,7 @@ Generate the business data shape for a collection-backed Akan module. This file
21
21
  - Keep the light model small enough for lists, cards, and relation previews.
22
22
  - Do not put persistence queries, service workflows, fetch calls, or React UI in constant files.
23
23
  - Use stable defaults for arrays and frequently read values.
24
+ - Never declare a `static` on the Object or Light class: `via(XObject, LightX, …)` mixes in their prototypes only, so a static there never reaches `X` — put it on `XInput` (or a scalar), `X`, or `XInsight`. `no-static-in-object-light-model` recognises the Object class by its `*Object` name (its `via()` shape is the Insight's) and the Light class by its `Light*` name or its `[...] as const` field tuple.
24
25
  - Add dictionary coverage for public fields, enums, insights, and query-facing names.
25
26
 
26
27
  ## Review Checklist
@@ -22,6 +22,15 @@ Generate client state coordination for a module. Store connects generated fetch
22
22
  - Do not call private or internal signal operations from UI state.
23
23
 
24
24
  ## Form Draft Recovery
25
+ - `Load.Edit`, `Model.EditModal` and `Model.New` save the whole `<model>Form` as the user types and offer it back on
26
+ the next open, so a modal closed by accident, a route change, or a killed app costs nothing. The scope is the
27
+ record id for an edit and the seed plus the route for a new form, both keyed to the signed-in user, so another
28
+ record or another parent never loads someone else's half-finished form.
29
+ - A new form's draft is applied on open with a "start over" chip; an edit whose record moved in the meantime is
30
+ *not* applied, and asks. Turn it off with `draft={false}`, or name the scope yourself with `draft="<scope>"` when
31
+ the context is in neither the id nor the seed.
32
+ - The old per-field `cache` / `cacheKey` props are deprecated and store nothing — they covered five control types,
33
+ keyed on the translated label, and restored over server data.
25
34
  - `<model>FormDraft`, `restore<Model>FormDraft`, `discard<Model>FormDraft` and `load<Model>FormDraft` are
26
35
  generated. Do not hand-write a draft action, and do not write `<model>Form` to browser storage yourself.
27
36
  - The draft key carries the signed-in user, derived by hashing the auth token's payload with `iat`, `exp`, `nbf`
@@ -0,0 +1,80 @@
1
+ # Page Rule — Route Files, The Route Chain, Heads, Lib Pages
2
+
3
+ What a file under `page/` may be, how a route declares its arguments and settings, how heads merge, what a CSR
4
+ bundle shows of a root layout, and how a lib's routes reach an app. `conventions` carries the invariants and the page
5
+ body shape — this is the full contract behind them. Before changing route behavior, check
6
+ `pkgs/akanjs/server/routeTreeBuilder.ts` and nearby routes for the expected pattern.
7
+
8
+ ## Route Files
9
+
10
+ - `apps/<app>/page` may contain route modules only. Do not add helper logic or component-only files there.
11
+ - Route source files under `page/` must use `.tsx`. Do not add `logic.ts`, `.js`, or `.jsx` files under `page/`.
12
+ - A route page is either `<routeName>.tsx` (serving `/routeName`) or a directory's `_index.tsx`; layouts use
13
+ `_layout.tsx`; per-route UI overrides use `_overrides.tsx`.
14
+ - Reserved `_*.tsx` route filenames are limited to `_index.tsx`, `_layout.tsx`, and `_overrides.tsx`; do not add
15
+ files like `_Component.tsx` or `_helper.tsx`.
16
+ - Page filenames must not start with an uppercase letter. Move helper components like `Component.tsx` to app `ui`,
17
+ `common`, or `lib` instead.
18
+ - Dynamic segments use `[id]`; route groups use directories like `(user)`, `(public)`, `(tab)`, or `(detail)`.
19
+ - `_overrides.tsx` is a logic-free UI-override manifest: imports plus a single
20
+ `export default override({ Slot: AppComponent })` (from `akanjs/ui`), no `"use client"`. It re-skins framework
21
+ `akanjs/ui` components for its route subtree; nested manifests merge over ancestors slot-by-slot (closest wins).
22
+ The slot list is in `componentRule`.
23
+
24
+ ## The Route Chain
25
+
26
+ - **A route file exports one thing**: `export default page()…render(…)` for a page, `layout()…render(…)` for a
27
+ `_layout.tsx`, `rootLayout()…render(…)` for the app's (or a basePath's) root `_layout.tsx`. Every route setting is
28
+ a stage of the chain — `.param()`, `.search()`, `.config()`, `.head()`, `.loading()`; `.notFound()` / `.error()` on
29
+ a layout; `.fonts()` / `.theme()` / `.manifest()` / `.reconnect()` / `.wsConnect()` / `.layoutStyle()` /
30
+ `.viewport()` on the root layout; `.prompt()` on a page. A named export beside the chain, or `page()` in a
31
+ `_layout.tsx`, fails the build.
32
+ - **A page names every `[x]` segment of its path with `.param("x", Type)`** and reads a query key only through
33
+ `.search("k", Type)`. Values arrive typed — `ID`/`String` → string, `Int`/`Float` → number, `Boolean`, `Date` →
34
+ Dayjs, an `enumOf` class → its union, `[T]` → array — a path value the type refuses answers not-found, and a
35
+ search value that fails is dropped. A layout may declare a subset. Names are string literals: `akan sync` reads
36
+ them off the source and refuses a `[projectId]` folder whose page declares no `.param("projectId")`.
37
+ - **`lang` is never declared**: every route sits under `/:lang`, and the locale segment reaches every stage as
38
+ `lang`.
39
+ - **On a web page's first render `st.use.searchParams()` is still the server's empty snapshot**, synced right after
40
+ hydration, so an effect that needs the query on its first run takes the page's `.search()` value as a prop.
41
+ - There is no `loader=` / `render=` page prop and no `PageProps` interface: the chain declares the arguments and
42
+ `.render()` receives them flat, typed, with `lang` on every route and `children` added for a layout.
43
+
44
+ ## Heads
45
+
46
+ - **`.head()` takes JSX — `<title>`, `<meta>`, `<link>` — or a function of the route's args returning it.** One that
47
+ throws renders no head (warned about), so a layout's sign-in redirect still answers. There is no metadata object,
48
+ and analytics is the app's own script, not a stage.
49
+ - **Heads merge root first**: a deeper `.head()` replaces the `<title>`, a `<meta>` with the same `name` /
50
+ `property`, and a canonical / manifest / icon link, and keeps the rest — so a page states only what it changes.
51
+ - A canonical link and `og:url` each name one URL, so a page carries them only from its own `.head()`, never from a
52
+ layout's.
53
+
54
+ ## A Root Layout Inside A CSR Bundle
55
+
56
+ - **A CSR bundle (a native app, `?csr=true`) never shows a root layout's own markup.** Pages render into the frame,
57
+ and what the root layout draws around `{children}` stays in a hidden container, so its wrappers cannot stack a
58
+ second screen beside the frame. Its components still mount and their effects run (`<Auth.User />` works).
59
+ - Anything a person must see from a root layout — an overlay, a gate, a banner — renders through
60
+ `createPortal(…, document.body)`, as `<Agent.Chat />` and the dialogs already do. A non-root `_layout.tsx` has no
61
+ such limit.
62
+
63
+ ## Lib Pages — `syncPageLibs`
64
+
65
+ - `libs/<lib>/page` follows the same rules and ships routes to apps that opt in with `syncPageLibs` in
66
+ `akan.config.ts`: `true` takes every lib dep that has a `page` folder, an array takes the libs listed, `false` (the
67
+ default) syncs nothing.
68
+ - `akan sync` links those routes into `apps/<app>/page/(libs)/(<lib>)` — once per basePath when the app declares
69
+ subRoutes. The folder is generated and gitignored like `public/libs`; edit the lib source, never the link.
70
+ - Both path segments are route groups, so a lib route mounts at its own path (`libs/<lib>/page/login/_index.tsx`
71
+ serves `/login`). Two synced routes that resolve to the same pattern are a sync-time error.
72
+
73
+ ## Build Scope And The Legacy Shape
74
+
75
+ - `.config({ devOnly: true })` keeps a route out of `akan build` while it keeps serving under `akan start` and keeps
76
+ being typechecked. On a `_layout.tsx` it excludes every route under that directory too. Write it as a literal
77
+ `true`/`false` — the build reads it off the source without evaluating the module.
78
+ - The legacy shape — `export default async function Page` beside `pageConfig` / `head` / `Loading` exports — still
79
+ loads and logs one deprecation warning per file at boot. Migrate with the `workspaceRecipes` recipe "Migrating
80
+ `page/` to the route chain"; new files are never written in it.
@@ -1,8 +1,9 @@
1
1
  # Query Rule — Filters, Slices, Hydration, Search, Cascade
2
2
 
3
- How a model's data is queried, exposed to a client store, searched, and removed. Filters and slices are the two
4
- query surfaces; `q.search()` and the `text` field roles are the search surface; `cascade` is what a removal takes
5
- with it. `conventions` carries the invariants — this is the full contract behind them.
3
+ How a model's data is queried, exposed to a client store, searched, written, and removed. Filters and slices are
4
+ the two query surfaces; the model facade carries the scoped reads and writes; `q.search()` and the `text` field
5
+ roles are the search surface; `cascade` is what a removal takes with it. `conventions` carries the invariants —
6
+ this is the full contract behind them.
6
7
 
7
8
  ## Slices, Queries, and Hydration
8
9
 
@@ -31,6 +32,9 @@ with it. `conventions` carries the invariants — this is the full contract behi
31
32
  A model with none renders ids and says so in the picker.
32
33
  - A slice's `exec` returns a `QueryOf` (an opaque query descriptor, `pkgs/akanjs/constant/types.ts`); you **cannot** chain `.sort()`/`.limit()` on it.
33
34
  - Apply ordering/paging via the store `init` fetch option instead: `initX(..., { sort, page, limit })` (`pkgs/akanjs/fetch/fetchType/sliceFetch.type.ts`).
35
+ - **`limit: 0` means every row on the server, never on the client.** A server `list<Filter>(…, { limit: 0 })`
36
+ reads without a ceiling, like an omitted limit, and `null` pages at 20. A client's `0` (`initX(…, { limit: 0 })`)
37
+ is served `MAX_PAGE_SIZE` (500) with a one-time warn. Paginate (`loadMoreOf<Model>`) or name a page size.
34
38
  - **Every sort ends with `id`, in the direction of its last key, and a missing value sorts below every other** —
35
39
  first ascending, last descending, in every database mode and in the client placing a live row. Rows the declared
36
40
  keys tie on (a midpoint priority dragged into one gap until the float runs out, two rows saved in one millisecond)
@@ -51,8 +55,9 @@ with it. `conventions` carries the invariants — this is the full contract behi
51
55
  - **A projection is bare on the facade and nested under `select` on a filter accessor.** `pickById(id, { secretField:
52
56
  true })` / `find(query, { secretField: true })` take the projection as their second argument; a generated
53
57
  `findBy<Filter>(...args, { select: { secretField: true } })` takes it inside the option object. The shapes do not
54
- swap: `{ select: … }` handed to the facade projects a field named `select`, which no model has, and the read comes
55
- back empty with no error. This is the only way to read a `field.secret(...)` value, which is otherwise stripped.
58
+ swap: `{ select: … }` handed to the facade projects a field named `select`, which no model has, and throws
59
+ `Unknown document field path`. The facade's third argument is the scope (`{ withRemoved: true }`), never a field.
60
+ This is the only way to read a `field.secret(...)` value, which is otherwise stripped.
56
61
  - **Hydrated vs raw:** server queries return hydrated `cnst.<Model>` instances (with `set`/`save`/`refresh`), and so do client fetch results — `FetchClient` parses every return with `crystalize` defaulting to **true** (`pkgs/akanjs/fetch/client/fetchClient.ts`). The raw `GetStateObject` shape (functions stripped, `pkgs/akanjs/base/types.ts`) is what the `<ref>Obj` / `<ref>ObjList` / `<ref>ObjInsight` handles carry, because those pass `crystalize: false` on purpose so the value can cross the RSC boundary as a client prop.
57
62
  - Every filter generates fourteen methods: `list` · `listIds` · `find` · `findId` · `pick` · `pickId` · `exists` ·
58
63
  `count` · `insight` · `query` · **`remove`** · **`removeOne`** · **`update`** · **`updateOne`**. The last four are
@@ -81,6 +86,26 @@ with it. `conventions` carries the invariants — this is the full contract behi
81
86
  matches a freshly inserted row and silently stops matching that same row after its first round-trip save. Reach
82
87
  for it only to find rows written before the field was declared.
83
88
 
89
+ ## Soft Removal And The Facade's Writes
90
+
91
+ - **Removal is always soft** — the framework has no hard delete for a model table, and `delete` is deliberately
92
+ left unused so it can mean one later. Every read and query-level write sees live rows only (`removedAt IS NULL`).
93
+ - **`{ withRemoved: true }` reaches removed rows**: the third argument of `find` / `findOne` / `findById`
94
+ (projection second, `null` when none), the last of `count` / `exists` / `updateMany`, or inside `updateOne` /
95
+ `updateById`'s options. Without it, a `removedAt` condition only a removed row can meet (`q.exists("removedAt")`,
96
+ a comparison) throws `can never match`.
97
+ - Removes, slices, filter methods, loaders and `pickById` stay live-only, a removed document is read-only
98
+ (`save()` throws), and `updateById(id, { removedAt: null }, { withRemoved: true })` revives one without hooks.
99
+ - The facade spells `Many`/`One` out on its writes (`updateOne` / `updateMany` / `removeOne` / `removeMany`); only
100
+ the count was shortened to `count(query)`, with `countDocuments` kept as `@deprecated`.
101
+ - **`updateById(id, update)` and `removeById(id)` are those same query-level writes narrowed to one id, not the
102
+ document path**: they fire no hooks, so a model whose removal cascades or carries a `_postRemove` still goes
103
+ through `remove<Model>(id)`.
104
+ - The write-through pair is `pickAndWrite(id, data)` / `pickOneAndWrite(query, data)` — pick the document, write
105
+ into it and save it, so the save hooks do run; reach for those when the write has to be seen by them.
106
+ - `upsertById(data, { preserveTimestamps })` writes a record whole under its own id, new or in place, with no
107
+ hook: the write a sync or a backfill repeats safely, keeping the record's `createdAt`/`updatedAt` when asked.
108
+
84
109
  ## Text Search In A Filter — `q.search()`
85
110
 
86
111
  - Text search is a filter query node like any other: `bySearch: filter().arg("text", String).query((text, q) =>
@@ -108,10 +133,12 @@ with it. `conventions` carries the invariants — this is the full contract behi
108
133
 
109
134
  - A field joins the full-text index by declaring one of five roles: `field(String, { text: "title" })`, and likewise
110
135
  `"desc"`, `"tag"`, `"thumb"`, `"filter"`. Nothing else opts a field in, and there is no per-model switch.
111
- - Pick the role by what the value *is*, because the rank weights them positionally (`title` 10, `tag` 3, `desc` 1,
112
- `filter` 0): `title` is the one line a human scans for, `desc` is prose, `tag` is a keyword list, `filter` is a
113
- scoping value (status, owner, role) that must be matchable but must never outrank a real title hit.
136
+ - Pick the role by what the value *is*, because the rank (`bm25` on SQLite) weights them positionally (`title` 10,
137
+ `tag` 3, `desc` 1, `filter` 0): `title` is the one line a human scans for, `desc` is prose, `tag` is a keyword
138
+ list, `filter` is a scoping value (status, owner, role) that must be matchable but must never outrank a real title
139
+ hit.
114
140
  - `thumb` is mirrored for rendering a hit and is **not** indexed — never expect it to match.
141
+ - `schema.index({ x: "text" })` (a Mongo text spec) builds nothing and warns at boot: full-text search is this role.
115
142
  - **`field.secret`, `field.hidden` and `resolve()` take no `text` role — it is a compile error**, because the
116
143
  mirror is plaintext and an indexed secret would leak through search. The class build throws the same refusal as
117
144
  a backstop, for an option object the excess-property check cannot see through, and names the way out: drop the
@@ -1,4 +1,4 @@
1
- # Runtime Rule — Serving, Processes, Logging, Image, Native Apps, Desktop Server, Assets, Build Pipeline
1
+ # Runtime Rule — Serving, Processes, Dev Server, Logging, Image, Native Apps, Desktop Server, Assets, Build Pipeline
2
2
 
3
3
  How an Akan app is built, what it serves, how many processes it runs, where its logs go, and what ends up in
4
4
  its image. Everything here is declared in `akan.config.ts` or narrowed by an env at boot; none of it is reached
@@ -114,6 +114,58 @@ worker per web-serving replica.
114
114
  `AkanServer`, whose graph the gateway never runs; through it the process evaluated 35MB of SSR renderer and
115
115
  SQLite driver to spawn children and relay bytes. Keep entrypoint imports at the leaf.
116
116
 
117
+ ## The Dev Server And Local Runs — `akan start`
118
+
119
+ - After changing application source code, test the app with `bun run akan start <appName>`; test production build
120
+ generation with `bun run akan build <appName>`.
121
+ - **`akan start` takes several apps** — `akan start a,b`, `akan start a b`, `akan start all`, or no argument at
122
+ all for a checkbox that remembers the last pick.
123
+ - **It opens a full-screen view by default, at any app count.** A rail of apps with the processes inside each
124
+ one — `host` for the dev host, gateway and RSC worker, plus a row per replica the gateway forwards — and the
125
+ selected one's log beside it. `Tab` walks every row and `1`-`9` jump to that app (the rail shows each app's
126
+ digit), `↑↓` scroll (shift for a page, `G` back to following), `/` greps, `e` shows stderr only, `c` clears,
127
+ `y` copies, `Y` copies the log path, `o` opens the browser, `r` restarts that app, `q` quits. Under `--share`,
128
+ `s` copies the public URL — the selected app's, or every app's from the merged row — and the header carries it
129
+ ahead of the local one, because the line `--share` printed before the view took the terminal is gone by its
130
+ first repaint. **`--plain` prints prefixed interleaved lines instead**, and a pipe, a redirect or an unsized
131
+ terminal downgrades to that on its own.
132
+ - **A supervised session writes `local/apps/<app>/runtime/dev.log`, and that path is how a log gets handed over.**
133
+ Ink repaints one frame in place, so the full-screen view leaves a bordered screenshot in the scrollback and takes
134
+ the rest of the session with it — a drag-selection is cleared by the next repaint, and what survives carries
135
+ borders and lines truncated to the pane. The file is the same output with no ANSI, nothing truncated, and every
136
+ process of the app in arrival order, including the dev host's own build output, which reaches no other file. The
137
+ previous session is kept beside it as `dev.prev.log`. Hand somebody — or an agent — the path rather than a paste:
138
+ it costs one line instead of a transcript, and it can be re-read after a fix. In the view, `y` copies the lines
139
+ the filters have already narrowed and `Y` copies the path.
140
+ - **How many apps boot at once is the machine's answer, not a constant.** A cold boot is an app's RSS peak (~1.8GB
141
+ per app, most of it the build worker — the base build, then the SSR registry's), so the default wave is what
142
+ memory and cores allow — half the memory budget divided by that peak, and one app per four cores — which boots a
143
+ laptop's apps together and still staggers them inside a small container. The session says which in one line.
144
+ The next wave starts once an app serves and its boot builds have settled, or 30 seconds after it serves, while
145
+ the app shows as ready — and `--open` opens it — as soon as it serves. `--concurrency <n>` overrides the wave
146
+ size, and `AKAN_MEMORY_LIMIT` lowers the budget it derives from.
147
+ - **`--kill` frees the dev ports first** — it resolves each port's listener, walks up to the top of that akan dev
148
+ tree and signals it, so another checkout's server or a stale orphan on the same port is reclaimed. A holder that
149
+ is not recognisably an akan process is reported and left alone.
150
+ - **A dev server watches its own app directory and the libs it actually depends on**, transitively, resolved once
151
+ at boot from the synced manifests — so a save under a lib the app never imports rebuilds and reloads nothing,
152
+ and neither does a save in a sibling app. An import that makes a new lib a dependency needs a fresh
153
+ `akan start`, because it needs a `sync` first anyway. Before the first `sync` the manifests do not exist yet and
154
+ the whole `libs/` container is watched, which is the safe direction.
155
+ - **A built artifact runs from the generated app directory** with the Akan runtime environment variables:
156
+
157
+ ```bash
158
+ cd dist/apps/<appName> && USE_AKANJS_PKGS=true AKAN_PUBLIC_REPO_NAME=<repo> AKAN_PUBLIC_SERVE_DOMAIN="<domain>" \
159
+ AKAN_PUBLIC_APP_NAME=<appName> AKAN_PUBLIC_ENV=local AKAN_PUBLIC_OPERATION_MODE=local SERVER_MODE=federation \
160
+ AKAN_PUBLIC_BASE_PATHS=<basePaths> bun main.js
161
+ ```
162
+
163
+ - **A server a test boots in-process takes `start({ listen: false, schedules: false })`** to run no internal
164
+ (init, interval, cron, queue worker); `listen: false` alone still runs them, as a batch replica does. A server
165
+ process a test spawns from source resolves the workspace from its cwd and gets the root `.env` from Bun, so
166
+ spawn it inside the repo with `bun --no-env-file`, or spawn the built `dist/apps/<app>` output, which needs
167
+ neither.
168
+
117
169
  ## Logging — Records, Request Context And Live Tail
118
170
 
119
171
  Every `Logger` call builds a `LogRecord` before any text exists: `at`, `level`, OTel `sev`, `name`, `message`,
@@ -152,6 +204,18 @@ normalized to `info` with one warning.
152
204
  - **The generated Dockerfile sets `AKAN_LOG_TO_FILE=0`.** A container's writable layer is ephemeral and nothing
153
205
  collects a file from it; stdout is the collection path. A deployment that wants the files back sets
154
206
  `AKAN_LOG_TO_FILE=1`.
207
+ - **`AKAN_LOG_FORMAT=ndjson` makes the container's stdout one JSON record per line, and the hub owner (gateway or
208
+ solo replica) its only writer** — every other server process turns its console off and forwards from
209
+ `AKAN_LOG_STDOUT_LEVEL` (default: the console level); a child's or the RSC worker's crash stack is wrapped as a
210
+ `raw=true` record instead of breaking the stream. Text stays the default; turn it on in the deployment.
211
+ - **`AKAN_LOG_STREAM_TOKEN` mounts `/_akan/app/logs`** (SSE, `Last-Event-ID` resume, explicit `gap` events);
212
+ unset, the route does not exist.
213
+ - **`AKAN_LOG_CANONICAL=1|slow`** writes one `ok|error <endpoint> ms= status=` line per call; **`AKAN_LOG_FLIGHT=1`**
214
+ keeps each call's last 64 sub-level records and promotes them (`flight=true`) only when it failed or ran past
215
+ `AKAN_LOG_FLIGHT_MS`; an `x-akan-debug` header lowers one request to `trace` — in `local`, or with the
216
+ `AKAN_LOG_DEBUG_HEADER` secret.
217
+ - **Structured values go in `LogRecord.attrs` via `Logger.emit(...)`**; a key naming a secret is `[redacted]`
218
+ before the record exists.
155
219
  - **Never log per delivered record in anything that delivers records.** The control socket and the hub do not,
156
220
  and a subscriber asking for everything is the test.
157
221
 
@@ -331,7 +395,7 @@ has none, or none when it has one, is refused, and switching means a reinstall.
331
395
  tied to the image's system, code only a process the app never starts loads — and whatever only they pull in; the
332
396
  image keeps them, and the server's own bundle must not import them. A package another dependency still installs
333
397
  stops the build, naming who needs it. So does a native addon that would not load on a user's computer: no binary
334
- for the app's OS and CPU, a link outside the system, or a `binding.gyp` its install never compiled. It runs as an API-only edge server on SQLite, on a loopback port the launcher picks — the last session's when
398
+ for the app's OS and CPU, a link outside the system, or a `binding.gyp` its install never compiled. It runs as an API-only edge server in database mode `single` (SQLite), on a loopback port the launcher picks — the last session's when
335
399
  it is free, which is not a guarantee, so a provider that needs an exact redirect URI signs in through a cloud
336
400
  server's adapter. It refuses a `Host` other than its own, but any program on the computer can still call that port:
337
401
  guard its endpoints as a network server's. It runs in one process, so a `main.ts` or an env asking for replicas (`replica`,
@@ -453,6 +517,29 @@ page API is
453
517
  hook; `volume` and `filePicker` also come from `akanjs/client/native` itself. A capability the shell
454
518
  lacks is added there: an app's own plugin runs as Bun code in the plugin host and cannot add a native shell op.
455
519
 
520
+ **A plugin the app or lib owns lives in `native/<id>/`**, the folder named after the plugin id: `native-plugin.json`
521
+ beside its page API (`src/index.ts`, `definePlugin` from `akanjs/client/native`), its desktop part (`src/desktop.ts`,
522
+ `defineDesktopPlugin` from `akanjs/native/desktop`), Kotlin and Swift. Every mobile and desktop target ships it; a
523
+ lib's reach only the apps that depend on it, and the app's own wins an id. Its page API is imported by a `webkit/`
524
+ hook (`../native/<id>/src`) and nothing else; pages and components call the hook, the same way they reach
525
+ `akanjs/client/native`. A plugin throws `AkanNativeError`, the error its bridge carries back to the page, which is
526
+ why `no-throw-raw-error` exempts a root `native/` folder.
527
+
528
+ **A page's storage and browser APIs follow the shell, not the browser.** A native shell serves the page from
529
+ `app://localhost` on iOS, macOS and Linux, which keeps no cookies: `document.cookie` reads `""` and a write is
530
+ dropped, so the value exists in the browser and vanishes in the app. Web Storage does work in the shells, but it
531
+ bypasses the store akanjs picks per platform (the shell's Preferences in an app) and throws during SSR. So app and lib
532
+ code never touches `document.cookie`, `localStorage` or `sessionStorage` (`no-document-cookie`, `no-web-storage`): it
533
+ uses `getCookie` / `setCookie` / `removeCookie`, `storage`, and `secretStorage` for tokens and credentials, all from
534
+ `akanjs/client`. `setCookie` writes `path=/; SameSite=Lax; Secure`, and an options object overrides only the keys it
535
+ names — pass `sameSite: "none"` only for a cookie a cross-site POST must carry back. Browser APIs an app WebView lacks
536
+ stay in a `webkit/` hook that branches on `isNativeApp()` from `akanjs/client/native`, which is where the app side goes
537
+ too (`no-web-only-api-outside-webkit`, a warning): `navigator.share` / `canShare` (Android's WebView has none),
538
+ `navigator.serviceWorker` and `Notification.requestPermission` / `.permission` (no app page has them),
539
+ `navigator.geolocation` (use `useGeoLocation` from `akanjs/webkit`) and `navigator.vibrate` (iOS has none; use
540
+ `haptics`). They are legal on the web, so the rule does not ban them; a root `native/` folder is out of its scope,
541
+ since a plugin's web part is where those APIs belong.
542
+
456
543
  ## Database Modes — `database` In `akan.config.ts`
457
544
 
458
545
  | mode | database | cache · queue · pubsub | runs as |
@@ -38,6 +38,19 @@ A third-party client package and the `ui/<Folder>/index_.tsx` + `lazy()` boundar
38
38
  not flagged. Interaction-driven `fetch.*` inside an `onClick` is not flagged either — only mount-time loads are,
39
39
  because those are the ones the server could have performed.
40
40
 
41
+ ## Lint Rules Behind The Boundary
42
+ - `no-async-component-in-ui` (`{apps,libs}/**/ui/**/*.tsx`) — React has no async client component, so a `ui/`
43
+ component that awaits breaks as soon as a client parent renders it, and the load drops below the route, which
44
+ could have started it before the first byte. Only a PascalCase binding whose own initializer is `async` is matched,
45
+ so an async handler inside a synchronous component and `lazy(async () => import(…))` are untouched.
46
+ - `no-init-fetch-in-client` — `fetch.init<Model><Suffix>` and `fetch.get<Model>Init<Suffix>` compose the slice's list
47
+ and insight queries into the hydration snapshot `Load.Units` / `Load.View` seed the store from. From a route it
48
+ resolves before the first byte; after hydration it is two extra round-trips for a shell the browser already
49
+ painted. Load it in the page and pass the result (or the unawaited `x<Model>Init<Suffix>` promise) down as `init`,
50
+ and reload from the client through the generated `st.do.init<Model><Suffix>()`. The rule is gated on the real
51
+ `"use client"` directive plus `*.store.ts` and matches by shape (`init` + `Capitalize<refName>` +
52
+ `Capitalize<suffix>`), so a hand-written `initPayment` is out.
53
+
41
54
  ## Implementation Playbook
42
55
 
43
56
  **① Wrap the interaction, not the UI.** The smallest useful client component adds one behaviour and renders