@akanjs/cli 3.1.0-rc.4 → 3.1.0-rc.5
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.
- package/.build-stamp +1 -1
- package/{CodeAgent-ymte1tv8.js → CodeAgent-rkjyxgec.js} +2 -2
- package/{CodeTui-4rhdm1qw.js → CodeTui-h7pyj6xa.js} +1 -1
- package/TunnelShare-661cxz5e.js +14 -0
- package/{agent.command-r5x1mch5.js → agent.command-3qxf4xcj.js} +5 -5
- package/akanCode.js +2 -2
- package/{application.command-3dkr7rdz.js → application.command-m1kcd7jf.js} +12 -11
- package/{applicationBuildRunner-h0j2swx1.js → applicationBuildRunner-dj0xkrep.js} +16 -9
- package/buildBatch.proc.js +7 -7
- package/{cloud.command-4kyxfw0y.js → cloud.command-3jzym8q7.js} +17 -16
- package/cloud.script-r4nskbc6.js +24 -0
- package/{code.command-sjvzt8fc.js → code.command-0ad6gw2a.js} +6 -6
- package/{context.command-j9frz0dz.js → context.command-0qx447mb.js} +17 -16
- package/context.runner-1vsjztjg.js +25 -0
- package/{dependencyScanner-06z2dmrr.js → dependencyScanner-3afj2vt1.js} +1 -1
- package/{guideline.command-1epw6hey.js → guideline.command-cv5bmrx2.js} +2 -2
- package/guidelines/conventions/conventions.instruction.md +450 -1084
- package/guidelines/cssRule/cssRule.instruction.md +16 -1
- package/guidelines/fieldRule/fieldRule.instruction.md +57 -0
- package/guidelines/framework/framework.instruction.md +4 -7
- package/guidelines/mcpRule/mcpRule.instruction.md +11 -8
- package/guidelines/modelConstant/modelConstant.instruction.md +2 -1
- package/guidelines/modelStore/modelStore.instruction.md +9 -0
- package/guidelines/pageRule/pageRule.instruction.md +80 -0
- package/guidelines/queryRule/queryRule.instruction.md +35 -8
- package/guidelines/runtimeRule/runtimeRule.instruction.md +89 -2
- package/guidelines/ssrRule/ssrRule.instruction.md +13 -0
- package/guidelines/transportRule/transportRule.instruction.md +76 -8
- package/guidelines/workspaceOnboarding/workspaceOnboarding.instruction.md +4 -4
- package/guidelines/workspaceRecipes/workspaceRecipes.instruction.md +49 -4
- package/incrementalBuilder.proc.js +4 -4
- package/{index-jw6st2rd.js → index-192ha4xh.js} +356 -307
- package/{index-1hkr7mrb.js → index-1p98exm2.js} +2 -2
- package/{index-8byzcvxp.js → index-1xj2t7cb.js} +7 -7
- package/{index-er3hekv9.js → index-20bxxyvn.js} +80 -80
- package/{index-xe3x2kw5.js → index-35hc95h5.js} +1 -1
- package/{index-p5nrc5he.js → index-4mhy6qme.js} +4 -4
- package/{index-0qc5nd61.js → index-5edzv8fn.js} +5 -5
- package/{index-k9rrten6.js → index-5nd1wtkv.js} +92 -94
- package/index-62md4dwy.js +260 -0
- package/{index-z3v7abrk.js → index-665zp9rs.js} +38 -15
- package/{index-1t2xw0p0.js → index-6xn2n442.js} +4 -4
- package/{index-jg3fj8h0.js → index-86jt2a0e.js} +1 -1
- package/{index-k33me5sp.js → index-8fp3b47h.js} +419 -418
- package/{index-shastb7m.js → index-9z426msb.js} +17 -12
- package/{index-gdybjw3x.js → index-awbdfcf2.js} +4 -4
- package/{index-3hx1z96c.js → index-bqbf654f.js} +1 -1
- package/{index-jppthsff.js → index-cpb3nvma.js} +57 -58
- package/{index-b60ccq2s.js → index-d47ypebv.js} +17 -15
- package/{index-gc05vqd2.js → index-d72hfmdm.js} +13 -13
- package/{index-akha197v.js → index-fmzcs8v6.js} +4 -4
- package/{index-28ftaz3d.js → index-fyq1z6kt.js} +29 -27
- package/{index-vwmv64ad.js → index-g1mbppa9.js} +238 -236
- package/{index-hedt951c.js → index-h47cf1gg.js} +7 -7
- package/{index-hfqs9jvc.js → index-j4mkhz93.js} +3 -3
- package/{index-08a5ga6n.js → index-jw1zm273.js} +28 -30
- package/{index-q958tqqg.js → index-m9jj544v.js} +1 -1
- package/{index-8935h3n1.js → index-mdjc93e1.js} +1 -1
- package/{index-2wyn17fa.js → index-pydbnfez.js} +2 -2
- package/{index-bjh4wbtk.js → index-qwa5k1j8.js} +1 -1
- package/{index-81a1ywp6.js → index-tamvyvj3.js} +3 -3
- package/{index-dez4t5sd.js → index-trpasv9q.js} +260 -259
- package/{index-2ahj896a.js → index-tvehn2sp.js} +1 -1
- package/{index-2k0t8p3p.js → index-va06nr10.js} +1086 -1075
- package/{index-9c3cd767.js → index-vk0pa0mn.js} +20 -21
- package/{index-qyaqzh8e.js → index-w44cy4vs.js} +2216 -2216
- package/index.js +22 -22
- package/{library.command-n58dgakb.js → library.command-erp2f56n.js} +3 -3
- package/{localRegistry.command-f2768sww.js → localRegistry.command-xxpqgxjw.js} +15 -14
- package/{module.command-xzjg5k7n.js → module.command-gs2r615e.js} +5 -5
- package/{package.command-24sh1j1q.js → package.command-7343f74r.js} +4 -4
- package/package.json +2 -2
- package/{page.command-fqnm88na.js → page.command-4h4a8pnh.js} +3 -3
- package/{pipeline.command-vn6taz75.js → pipeline.command-a2nrk26m.js} +3 -3
- package/{primitive.command-4j1g1a0d.js → primitive.command-w6t0asqz.js} +6 -6
- package/{projectTypecheck-3y7ayty0.js → projectTypecheck-f534enqj.js} +1 -1
- package/{quality.command-5hqmcdwt.js → quality.command-77x7vbbx.js} +4 -4
- package/{repair.command-rw5tk460.js → repair.command-y6sstev7.js} +5 -5
- package/{scalar.command-0jpap45m.js → scalar.command-20b8cy0k.js} +4 -4
- package/{subspace.command-y7n9sghk.js → subspace.command-92xkc0s0.js} +11 -10
- package/templates/lib/cnst.ts +0 -1
- package/templates/workspaceRoot/.gitignore.template +24 -53
- package/{tunnel.command-5gjwh1f8.js → tunnel.command-2kw8vt9g.js} +8 -7
- package/{workflow.command-3t22f7eb.js → workflow.command-2djwmpqf.js} +10 -10
- package/{workspace.command-cq5xg2jq.js → workspace.command-5se6spqb.js} +28 -27
- package/TunnelShare-p8ymkww8.js +0 -13
- package/cloud.script-jc3hc5d9.js +0 -23
- package/context.runner-v0def5my.js +0 -24
- 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` `##
|
|
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
|
-
- `
|
|
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
|
|
12
|
-
|
|
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`).**
|
|
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
|
|
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
|
|
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
|
|
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
|
-
-
|
|
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
|
|
4
|
-
query surfaces; `q.search()` and the `text` field
|
|
5
|
-
with it. `conventions` carries the invariants —
|
|
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
|
|
55
|
-
|
|
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,
|
|
112
|
-
`filter` 0): `title` is the one line a human scans for, `desc` is prose, `tag` is a keyword
|
|
113
|
-
scoping value (status, owner, role) that must be matchable but must never outrank a real title
|
|
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
|
|
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
|