@akanjs/cli 3.0.0-beta.8 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.build-stamp +1 -1
- package/CodeAgent-2vmp9qe0.js +9 -0
- package/CodeAgentRpcHost-p7h6n2hp.js +7 -0
- package/CodeAgentRpcListener-7p7z5dd7.js +97 -0
- package/CodeTui-5ed6bg2a.js +1457 -0
- package/README.ko.md +83 -28
- package/README.md +70 -27
- package/TunnelShare-cwh9ezsm.js +9 -0
- package/{agent.command-0q17pnrs.js → agent.command-gn73rhe7.js} +8 -9
- package/akanCode.js +102 -0
- package/application.command-e968t0hw.js +246 -0
- package/{applicationBuildRunner-kxwbssgn.js → applicationBuildRunner-64taxseg.js} +121 -77
- package/buildBatch.proc.js +120 -32
- package/cloud.command-0jm301rr.js +95 -0
- package/cloud.script-nemnzjpv.js +20 -0
- package/code.command-18kcm7hq.js +154 -0
- package/commandManifest.json +1 -1
- package/consoleToStderr-h5bryj94.js +7 -0
- package/context.command-trd9bn18.js +48 -0
- package/context.runner-y5hwh673.js +21 -0
- package/{dependencyScanner-shs82ynp.js → dependencyScanner-06z2dmrr.js} +2 -2
- package/{devTui-m1atgkc9.js → devTui-q7k7bakd.js} +122 -40
- package/fileSys-wzzrntkw.js +9 -0
- package/guideline.command-bdmdn2de.js +42 -0
- package/guidelines/agentRule/agentRule.instruction.md +154 -26
- package/guidelines/conventions/conventions.instruction.md +134 -48
- package/guidelines/mcpRule/mcpRule.instruction.md +7 -5
- package/guidelines/queryRule/queryRule.instruction.md +21 -7
- package/guidelines/runtimeRule/runtimeRule.instruction.md +291 -6
- package/guidelines/ssrRule/ssrRule.instruction.md +4 -2
- package/guidelines/transportRule/transportRule.instruction.md +10 -8
- package/guidelines/workspaceOnboarding/workspaceOnboarding.instruction.md +20 -0
- package/guidelines/workspaceRecipes/workspaceRecipes.instruction.md +66 -3
- package/incrementalBuilder.proc.js +564 -109
- package/index-0wpybxtr.js +2729 -0
- package/{index-kcxmv48c.js → index-2ahj896a.js} +45 -79
- package/index-2wc41k5w.js +49 -0
- package/index-3hx1z96c.js +87 -0
- package/{index-2tv3fdx4.js → index-3w94e7n3.js} +1228 -1458
- package/{index-hwkygn9s.js → index-48k96p55.js} +325 -36
- package/index-4npz05nm.js +187 -0
- package/{index-q0y2jq9j.js → index-52w0fvh8.js} +21 -4
- package/index-6he6gvfg.js +4300 -0
- package/index-7md2g9vw.js +6 -0
- package/{index-04bk599g.js → index-93j60em0.js} +10 -17
- package/index-94s080qm.js +266 -0
- package/index-9dp031by.js +12 -0
- package/{index-ktb5njgy.js → index-9z771ptz.js} +49 -99
- package/{index-y6n8nmny.js → index-abxpehf8.js} +75 -203
- package/{index-3d9y066c.js → index-bjwxs904.js} +9 -8
- package/{index-9077wzd2.js → index-by9pxkr0.js} +43 -26
- package/index-c6eggbee.js +123 -0
- package/index-d2242ksy.js +99 -0
- package/index-drehwpdt.js +1119 -0
- package/{index-92zztqy1.js → index-dy0jx9ay.js} +2 -17
- package/{index-ffvk10zz.js → index-f3c78nd6.js} +107 -241
- package/index-gbggbqcp.js +1058 -0
- package/{index-by5gpp8x.js → index-hj6w4ka1.js} +2 -1
- package/index-hsmtysz4.js +53 -0
- package/{index-j9yhg404.js → index-jmrn54tt.js} +4 -7
- package/{index-a64esmtb.js → index-kcdj1z5v.js} +523 -1022
- package/{index-d43r08tf.js → index-kqabytj7.js} +77 -150
- package/{index-vyvre9en.js → index-mbxbhay5.js} +6 -2
- package/{index-vxtad5bd.js → index-mgxpx3f2.js} +5 -8
- package/index-p6pbz9n5.js +25 -0
- package/{index-sj5vxr8f.js → index-p993efkq.js} +981 -897
- package/{index-2xp0kfkt.js → index-pmhqttv3.js} +37 -25
- package/index-tqm7jjcn.js +700 -0
- package/{index-0g7r8xz3.js → index-vpj42xwt.js} +114 -232
- package/index-wa5jbxj1.js +760 -0
- package/{index-k2j1hc4r.js → index-wprnsjms.js} +12 -12
- package/{index-gyd220p2.js → index-y3mphr1m.js} +554 -577
- package/index-yg7jhz8x.js +69 -0
- package/{index-h7yw2z9n.js → index-zzvvc845.js} +8 -14
- package/index.js +23 -22
- package/{library.command-qzzbj6d8.js → library.command-9h749a1x.js} +4 -5
- package/{localRegistry.command-vyrgg4xw.js → localRegistry.command-2bqqhxyz.js} +23 -43
- package/{module.command-rq5hq3ax.js → module.command-nv8nzgft.js} +8 -12
- package/{package.command-d9hd6v9n.js → package.command-mq4bggp6.js} +6 -7
- package/package.json +15 -7
- package/{page.command-126d56te.js → page.command-j639n707.js} +4 -5
- package/{primitive.command-taxd1538.js → primitive.command-kz6849th.js} +9 -23
- package/prompter-76k56b37.js +8 -0
- package/{quality.command-6gsna0a3.js → quality.command-4hyabwkb.js} +205 -377
- package/{recipeScanner-fpagtgc1.js → recipeScanner-4b69ybe0.js} +1 -1
- package/{repair.command-tq2dgy72.js → repair.command-a9awpbqr.js} +11 -35
- package/{routeSourceValidator-apyqw7wk.js → routeSourceValidator-7ycnfz0e.js} +0 -6
- package/{scalar.command-3bcmrrkv.js → scalar.command-ma4fpm9m.js} +7 -11
- package/skills/akan-code/SKILL.md +130 -0
- package/skills/akan-debug/SKILL.md +42 -0
- package/skills/akan-deps/SKILL.md +50 -0
- package/skills/akan-err/SKILL.md +39 -0
- package/skills/akan-module/SKILL.md +45 -0
- package/skills/akan-scaffold/SKILL.md +76 -0
- package/skills/akan-scalar/SKILL.md +52 -0
- package/skills/akan-service/SKILL.md +34 -0
- package/skills/akan-store/SKILL.md +67 -0
- package/skills/akan-ui/SKILL.md +63 -0
- package/skills/akan-validate/SKILL.md +44 -0
- package/{subspace.command-eddzqpx3.js → subspace.command-3k2pmrs4.js} +53 -62
- package/templates/__scalar/__model__/__model__.abstract.md.template +8 -0
- package/templates/app/page/_index.tsx +257 -258
- package/templates/app/page/_layout.tsx +5 -1
- package/templates/app/page/_overrides.tsx +12 -0
- package/templates/app/page/styles.css.template +316 -14
- package/templates/app/public/apple-touch-icon.png +0 -0
- package/templates/app/public/favicon.ico +0 -0
- package/templates/app/public/jelly/cloud.webp +0 -0
- package/templates/app/public/jelly/comet.webp +0 -0
- package/templates/app/public/jelly/moon.webp +0 -0
- package/templates/app/public/jelly/planet.webp +0 -0
- package/templates/app/public/jelly/rocket.webp +0 -0
- package/templates/app/public/jelly/star.webp +0 -0
- package/templates/app/public/jelly/sugar.webp +0 -0
- package/templates/app/public/logo.png +0 -0
- package/templates/app/ui/Recipe/index.ts +9 -0
- package/templates/app/ui/Recipe/jellyButton.ts +63 -0
- package/templates/appSample/lib/__scalar/workHistory/workHistory.abstract.ts +8 -39
- package/templates/appSample/lib/_noti/noti.abstract.ts +6 -29
- package/templates/appSample/lib/task/Task.Unit.tsx +1 -1
- package/templates/appSample/lib/task/Task.Util.tsx +10 -13
- package/templates/appSample/lib/task/Task.View.tsx +2 -2
- package/templates/appSample/lib/task/Task.Zone.tsx +5 -2
- package/templates/appSample/lib/task/task.abstract.ts +12 -32
- package/templates/appSample/lib/task/task.signal.spec.ts +1 -15
- package/templates/appSample/lib/task/task.signal.test.ts +15 -1
- package/templates/appSample/lib/task/task.signal.ts +10 -8
- package/templates/appSample/page/task/[taskId]/_index.tsx +6 -6
- package/templates/appSample/page/task/[taskId]/edit.tsx +5 -4
- package/templates/appSample/page/task/_index.tsx +4 -3
- package/templates/appSample/page/task/_layout.tsx +1 -1
- package/templates/appSample/page/task/new.tsx +5 -4
- package/templates/appSample/srvkit/{AuthGuard.ts → guards.ts} +7 -4
- package/templates/appSample/srvkit/{SessionInternalArg.ts → internalArgs.ts} +6 -4
- package/templates/appSample/ui/GlobalLoading.tsx +2 -1
- package/templates/appSample/ui/QuantityControl.tsx +3 -3
- package/templates/client.ts +7 -30
- package/templates/crudPages/[__model__Id]/edit/page.tsx +2 -1
- package/templates/crudPages/[__model__Id]/page.tsx +2 -1
- package/templates/crudPages/new/page.tsx +2 -1
- package/templates/crudPages/page.tsx +2 -1
- package/templates/crudSinglePage/page.tsx +2 -1
- package/templates/lib/__lib/lib.dictionary.ts +0 -2
- package/templates/lib/__lib/lib.document.ts +0 -2
- package/templates/lib/sig.ts +8 -30
- package/templates/localDev/docker-compose.yaml.template +3 -14
- package/templates/module/__model__.signal.ts +12 -4
- package/templates/pkgRoot/tsconfig.json.template +2 -7
- package/templates/server.ts +1 -3
- package/templates/service/__model__.abstract.md.template +8 -0
- package/templates/workspaceRoot/.gitignore.template +0 -1
- package/templates/workspaceRoot/AGENTS.md.template +6 -0
- package/templates/workspaceRoot/package.json.template +1 -3
- package/tunnel.command-xwr46b66.js +129 -0
- package/{typeChecker-3xgrzc2v.js → typeChecker-px49mezc.js} +1 -1
- package/typecheck.proc.js +1 -1
- package/workflow.command-t7wqe4h7.js +79 -0
- package/{workspace.command-06wvstgr.js → workspace.command-4derhzrh.js} +79 -109
- package/abstractCompactor-gmkc0z8j.js +0 -102
- package/application.command-18y0s485.js +0 -204
- package/applicationReleasePackager-4yasxwv9.js +0 -244
- package/capacitorApp-36rbr858.js +0 -12
- package/cloud.command-av97ndh6.js +0 -94
- package/cloud.script-cm11xjdz.js +0 -20
- package/context.command-5v37ef3n.js +0 -81
- package/devTuiView-0hc2emhe.js +0 -9
- package/getDirname-dr3jz1xg.js +0 -7
- package/guideline.command-4pz9g1rt.js +0 -360
- package/guidelines/agentRule/agentRule.generate.json +0 -31
- package/guidelines/componentRule/componentRule.generate.json +0 -31
- package/guidelines/conventions/conventions.generate.json +0 -25
- package/guidelines/cssRule/cssRule.generate.json +0 -31
- package/guidelines/docPageRule/docPageRule.generate.json +0 -31
- package/guidelines/docPageRule/docPageRule.instruction.md +0 -25
- package/guidelines/docSyncRule/docSyncRule.generate.json +0 -31
- package/guidelines/docSyncRule/docSyncRule.instruction.md +0 -25
- package/guidelines/enumConstant/enumConstant.generate.json +0 -31
- package/guidelines/enumConstant/enumConstant.instruction.md +0 -28
- package/guidelines/fieldRule/fieldRule.generate.json +0 -31
- package/guidelines/framework/framework.generate.json +0 -32
- package/guidelines/mcpRule/mcpRule.generate.json +0 -35
- package/guidelines/modelConstant/modelConstant.generate.json +0 -32
- package/guidelines/modelDictionary/modelDictionary.generate.json +0 -32
- package/guidelines/modelDocument/modelDocument.generate.json +0 -31
- package/guidelines/modelService/modelService.generate.json +0 -31
- package/guidelines/modelSignal/modelSignal.generate.json +0 -31
- package/guidelines/modelStore/modelStore.generate.json +0 -31
- package/guidelines/modelTemplate/modelTemplate.generate.json +0 -31
- package/guidelines/modelUnit/modelUnit.generate.json +0 -31
- package/guidelines/modelUtil/modelUtil.generate.json +0 -31
- package/guidelines/modelView/modelView.generate.json +0 -31
- package/guidelines/modelZone/modelZone.generate.json +0 -31
- package/guidelines/moduleCodegen/moduleCodegen.generate.json +0 -31
- package/guidelines/moduleCodegen/moduleCodegen.instruction.md +0 -28
- package/guidelines/moduleOverview/moduleOverview.generate.json +0 -31
- package/guidelines/queryRule/queryRule.generate.json +0 -31
- package/guidelines/recipeRule/recipeRule.generate.json +0 -31
- package/guidelines/runtimeRule/runtimeRule.generate.json +0 -25
- package/guidelines/scalarConstant/scalarConstant.generate.json +0 -32
- package/guidelines/scalarDictionary/scalarDictionary.generate.json +0 -32
- package/guidelines/scalarModule/scalarModule.generate.json +0 -31
- package/guidelines/sharedUiUsage/sharedUiUsage.generate.json +0 -31
- package/guidelines/sharedUiUsage/sharedUiUsage.instruction.md +0 -25
- package/guidelines/ssrRule/ssrRule.generate.json +0 -31
- package/guidelines/transportRule/transportRule.generate.json +0 -31
- package/guidelines/utilUiUsage/utilUiUsage.generate.json +0 -31
- package/guidelines/utilUiUsage/utilUiUsage.instruction.md +0 -25
- package/guidelines/workspaceOnboarding/workspaceOnboarding.generate.json +0 -24
- package/guidelines/workspaceRecipes/workspaceRecipes.generate.json +0 -33
- package/index-47pj3aee.js +0 -76
- package/index-5ystnsd5.js +0 -841
- package/index-6wwzs1bc.js +0 -7
- package/index-a17x49vg.js +0 -229
- package/index-m0xccmmm.js +0 -228
- package/index-meh558jy.js +0 -65
- package/index-rtxxan88.js +0 -471
- package/index-te54x9zm.js +0 -338
- package/index-w8kmx1b6.js +0 -2176
- package/index-wt7qrtd4.js +0 -1516
- package/templates/__scalar/__model__/__model__.abstract.md +0 -28
- package/templates/module/__model__.abstract.md +0 -28
- package/templates/service/__model__.abstract.md +0 -28
- package/templates/workspaceRoot/patches/react-dom@19.2.7.patch.template +0 -62
- package/workflow.command-gtdnzvbz.js +0 -108
|
@@ -28,7 +28,7 @@ you fetch on demand — `get_guideline` with the name, or `akan guideline show <
|
|
|
28
28
|
| name | covers |
|
|
29
29
|
|---|---|
|
|
30
30
|
| `ssrRule` | server-share targets, the `akan.ssr.*` warnings, the client-boundary playbook |
|
|
31
|
-
| `runtimeRule` | `web` / `csr` surfaces, gateway vs solo processes, logging, the generated image, shipped assets |
|
|
31
|
+
| `runtimeRule` | `web` / `csr` surfaces, gateway vs solo processes, logging, the generated image, native apps and a desktop app's server, shipped assets, database modes |
|
|
32
32
|
| `queryRule` | slices and hydration, the generated filter methods, full-text search, cascade removal |
|
|
33
33
|
| `transportRule` | guards across HTTP and websocket, socket identity and cleanup, binary pubsub, mutation verbs |
|
|
34
34
|
| `mcpRule` | MCP configuration, wire behaviour, resource URIs, OAuth metadata, protocol revisions |
|
|
@@ -57,8 +57,9 @@ back.
|
|
|
57
57
|
`no-inline-color`, `no-interpolated-arbitrary-class`)
|
|
58
58
|
- **Never `throw new Error`.** Throw `new Err("<module>.error.<key>")` and register the key as `[en, ko]` in that
|
|
59
59
|
module's dictionary `.error({})`. Import `Err` from `"../dict"` on the server and from `"@libs/<lib>/client"` or
|
|
60
|
-
`"@apps/<app>/client"` in UI. `no-throw-raw-error.grit` exempts tests, `*.constant.ts`, `common/**`,
|
|
61
|
-
—
|
|
60
|
+
`"@apps/<app>/client"` in UI. `no-throw-raw-error.grit` exempts tests, `*.constant.ts`, `common/**`, `env/**`, and
|
|
61
|
+
a root `native/` folder — `common/` and `env/` have no legal `Err` import path, so keep throwing code out of them,
|
|
62
|
+
and a native plugin throws `AkanNativeError`, the error its bridge carries back to the page.
|
|
62
63
|
- **Never import a third-party package** from `page/**`, from any barrel, or from any
|
|
63
64
|
`*.{constant,dictionary,document,service,signal,store}.ts` / `*.{Template,Unit,Util,View,Zone}.tsx`
|
|
64
65
|
(`no-import-external-library.grit`). Re-export the symbol through a lib first. One-line re-export shims in a lib's
|
|
@@ -76,6 +77,19 @@ back.
|
|
|
76
77
|
`*.constant.ts`, `*.store.ts`, and the five module component suffixes (`no-bang-comment-in-client.grit`). Bun
|
|
77
78
|
classifies `//!` and `/*!` as legal comments and keeps them through minification, so the note ships to every
|
|
78
79
|
visitor. Use `// FIXME:` there; `//!` stays legal in server, `srvkit/`, and CLI files.
|
|
80
|
+
- **Never touch `document.cookie`, `localStorage` or `sessionStorage` in app or lib code** (`no-document-cookie.grit`,
|
|
81
|
+
`no-web-storage.grit`). A native shell serves the page from `app://localhost` on iOS, macOS and Linux, which keeps
|
|
82
|
+
no cookies: `document.cookie` reads `""` and a write is dropped, so the value exists in the browser and vanishes in
|
|
83
|
+
the app. Web Storage does work in the shells, but it bypasses the store akanjs picks per platform (the shell's
|
|
84
|
+
Preferences in an app) and throws during SSR. Use `getCookie` / `setCookie` / `removeCookie`, `storage`, and
|
|
85
|
+
`secretStorage` for tokens and credentials, all from `akanjs/client`.
|
|
86
|
+
- **Browser APIs an app WebView lacks live in `webkit/`** (`no-web-only-api-outside-webkit.grit`, a warning):
|
|
87
|
+
`navigator.share` / `canShare` (Android's WebView has none), `navigator.serviceWorker` and
|
|
88
|
+
`Notification.requestPermission` / `.permission` (no app page has them), `navigator.geolocation` (use
|
|
89
|
+
`useGeoLocation` from `akanjs/webkit`) and `navigator.vibrate` (iOS has none; use `haptics`). They are legal on the
|
|
90
|
+
web, so the rule does not ban them: it keeps them in a `webkit/` hook that branches on `isNativeApp()` from
|
|
91
|
+
`akanjs/client/native`, which is where the app side goes too. A root `native/` folder is out of its scope: a
|
|
92
|
+
plugin's web part is where those APIs belong.
|
|
79
93
|
- **Never return a value from a store action** (`no-return-in-store-action.grit`). Every method of a `store(...)`
|
|
80
94
|
class dispatches through `st.do.<action>()`, typed `void` / `Promise<void>`, so the value is unreachable — write
|
|
81
95
|
it into state with `this.set({ ... })`. A bare `return;` guard, a `return` inside a nested callback, a getter,
|
|
@@ -216,6 +230,8 @@ Do not narrate code. Do document the thing the code cannot say. Both halves are
|
|
|
216
230
|
## Client / Server Boundaries (`apps/**`, `libs/**`, `pkgs/akanjs/**`)
|
|
217
231
|
|
|
218
232
|
- Use `"use client";` at the top of client component files.
|
|
233
|
+
- A `"use client"` file re-exports by name (`export { A, B } from "./x"`), never with `export *`: the server knows a
|
|
234
|
+
client module only by the export names it spells out, so `akan start` and `akan build` stop on a star re-export.
|
|
219
235
|
- Be careful when importing client-only code from page or layout modules.
|
|
220
236
|
- Keep page props serializable unless the existing route pattern clearly allows otherwise.
|
|
221
237
|
- In domain UI the boundary is mechanical, not a judgment call: `Template`, `Zone`, and `Util` are always client components with `"use client"` on line 1; `Unit` and `View` are always server components and never carry the directive.
|
|
@@ -273,8 +289,10 @@ are, because those are the ones the server could have done.
|
|
|
273
289
|
SEO snapshots, prerendering and pre-hydration E2E read, so await what the page needs immediately and stream
|
|
274
290
|
the rest. `xListInY` / `xInsightInY` hold hydrated model instances that React Flight refuses as client props:
|
|
275
291
|
consume them in a server component, never as a `Zone` prop.
|
|
276
|
-
6. **Use named `ReactNode` slots, not just `children`** —
|
|
277
|
-
|
|
292
|
+
6. **Use named `ReactNode` slots, not just `children`** — a client shell that takes one slot per region lets a
|
|
293
|
+
server component supply each of them, instead of absorbing the whole subtree into the bundle. `Layout.Navbar`
|
|
294
|
+
declares `title` / `left` / `right` alongside `back` and `children`, but renders only the last two today, so
|
|
295
|
+
copy the shape rather than the component.
|
|
278
296
|
7. **Let the server do the derived work.** Display and predicate logic belongs on `Light<Model>`; enum→class
|
|
279
297
|
lookups belong in a module-scope `as const` map.
|
|
280
298
|
8. **Gate auth on the server** — `getSelf({ unauthorize: "/signin" })` in `_layout.tsx`, before any HTML is sent.
|
|
@@ -291,11 +309,11 @@ What an app serves, how many processes it runs, where its logs go, and what ship
|
|
|
291
309
|
declared in `akan.config.ts` and narrowed — never widened — by an env at boot. Domain code reaches none of it.
|
|
292
310
|
Full contract: `get_guideline` with `runtimeRule`, or `akan guideline show runtimeRule`.
|
|
293
311
|
|
|
294
|
-
- **Web surfaces are `web: true | false | { csr: boolean }`.** `{ csr: false }` drops the
|
|
295
|
-
`false` is an API-only build that serves nothing under `page/`. There is no CSR-without-SSR option, by
|
|
296
|
-
the CSR bundle inlines the stylesheet the SSR base artifact compiles. `AKAN_SSR` / `AKAN_CSR` narrow
|
|
297
|
-
runtime and can never switch a surface the build left out back on. `akan start` ignores `web` and
|
|
298
|
-
whole dev surface.
|
|
312
|
+
- **Web surfaces are `web: true | false | { csr: boolean }`.** `{ csr: false }` drops the CSR bundle the native
|
|
313
|
+
apps ship, `false` is an API-only build that serves nothing under `page/`. There is no CSR-without-SSR option, by
|
|
314
|
+
type — the CSR bundle inlines the stylesheet the SSR base artifact compiles. `AKAN_SSR` / `AKAN_CSR` narrow
|
|
315
|
+
further at runtime and can never switch a surface the build left out back on. `akan start` ignores `web` and
|
|
316
|
+
keeps the whole dev surface.
|
|
299
317
|
- **One traffic replica runs in the container's only process.** `AKAN_REPLICA=0,0,1` — the default everywhere —
|
|
300
318
|
has nothing to balance, so there is no gateway and no unix-socket proxy hop. Two or more replicas, a batch-only
|
|
301
319
|
replica, `AKAN_SOLO=false`, passing `replica` to `new AkanApp(...)`, or `akan start` all bring the gateway back.
|
|
@@ -304,7 +322,7 @@ Full contract: `get_guideline` with `runtimeRule`, or `akan guideline show runti
|
|
|
304
322
|
rotating log file; nothing supervises it but the orchestrator's probes.
|
|
305
323
|
- **Route prefixes move together or not at all.** `new AkanApp({ prefix, websocketPrefix })` in `main.ts` moves
|
|
306
324
|
the server's routes, the gateway's websocket upgrade and — through the SSR bootstrap script — the
|
|
307
|
-
`fetchClient` in every tab the server renders. A prebuilt CSR shell or a
|
|
325
|
+
`fetchClient` in every tab the server renders. A prebuilt CSR shell or a native app bundle is never rendered by
|
|
308
326
|
a server, so `api: { prefix, websocketPrefix }` in `akan.config.ts` is what those follow; declare it there too
|
|
309
327
|
when the app ships either. Read the value with `getApiPrefix()` / `getWsPrefix()` from `akanjs/base` — never
|
|
310
328
|
write `/api` or `/ws` as a literal, and never try to hand it down as a React prop: `FetchClient` fixes its
|
|
@@ -332,9 +350,42 @@ Full contract: `get_guideline` with `runtimeRule`, or `akan guideline show runti
|
|
|
332
350
|
assembles one from; the string form takes no contributions. The generated image installs `ca-certificates` and
|
|
333
351
|
`tzdata` and nothing else, so an app needing `ffmpeg` or Chromium declares it in `preRuns` / `postRuns`. A lib
|
|
334
352
|
declares the steps its own runtime needs and every mounting app inherits them.
|
|
353
|
+
- **Native apps are the `native` section** — iOS, Android and a desktop app (macOS, Windows, Linux), each shipping
|
|
354
|
+
the CSR bundle on the native runtime. Shared fields (`appName`, `appId`, `version`, `indexPath`, `permissions`,
|
|
355
|
+
`plugins`, `deepLinks`, `updates`, …) sit beside one section per platform: `ios`, `android`, `desktop`, and a
|
|
356
|
+
platform section's own `indexPath` wins on that platform (`desktop: { indexPath: "/" }`). Without
|
|
357
|
+
`targets` the app has one target, `default`, and an app with no basePaths leaves `basePath` out; a target takes
|
|
358
|
+
the same shape and overrides the section field by field — objects merge key by key, a list or any other value
|
|
359
|
+
replaces the section's. `akan start-ios` / `start-android` / `start-desktop` run a dev build that loads its pages
|
|
360
|
+
from `akan start`; `build-ios` / `build-android` / `build-desktop` write
|
|
361
|
+
`apps/<app>/.akan/native/<target>/build/<platform>`, a desktop app only for the OS that builds it — `--arch` picks
|
|
362
|
+
a Windows or Linux app's CPU, and a macOS app is Apple silicon only. `--installer true` adds a Windows setup, a
|
|
363
|
+
macOS dmg or a Linux AppImage; `AKAN_NATIVE_MACOS_*` signs with a Developer ID and notarizes,
|
|
364
|
+
`AKAN_NATIVE_WINDOWS_*` signs with Authenticode. With `updates: { url, publicKey }` an installed app takes the signed
|
|
365
|
+
releases `akan publish-update` makes; `akan update-keygen` makes the key and prints its `publicKey`.
|
|
366
|
+
- **A desktop app that carries its server (`desktop: { server: true }` in `native`, or in one target) gets nothing
|
|
367
|
+
from `docker`** — and `server: { omit: [...] }` leaves out packages only the image needs. That server is API-only, runs in database mode `single`, and listens on a loopback port any
|
|
368
|
+
program on the computer can call, so guard its endpoints as a network server's. An executable the app spawns
|
|
369
|
+
goes in `bin` — per platform, a download checked against its `sha256` or a file beside the config, put first on
|
|
370
|
+
the app's PATH so the server's `spawn("ffmpeg")` finds it, and in `ctx.binDir` for a native plugin — and a package
|
|
371
|
+
that builds itself at install goes in `trustedDependencies`, which the image honours too. Carry a static LGPL
|
|
372
|
+
ffmpeg: a `--enable-nonfree` build may not be redistributed. A file the user picks with
|
|
373
|
+
`filePicker.pickFiles({ forServer: true })` reaches it as a grant, never a copy or a path:
|
|
374
|
+
`NativeFile.resolve(grant, "read")` in `akanjs/server` asks the shell for it. What it carries is readable on the
|
|
375
|
+
user's computer — `private/` (each lib's too, under `private/libs/<lib>`), the one `env.server.<env>.ts` of its
|
|
376
|
+
`--env` and its libs' server env defaults (`env.server.testing.ts`) — and it has no `public/`: read runtime files
|
|
377
|
+
from `AKAN_APP_DIR`, never `process.cwd()`.
|
|
335
378
|
- **`assets: { pruneFonts, keepFonts }`** trims from the `dist` copy of `public/` the fonts nothing reads; source
|
|
336
379
|
trees are never touched. A font with `optimize` on is a build input, not a runtime asset. `keepFonts` belongs to
|
|
337
380
|
the `akan.config.ts` that owns the font, written against that scope's own `public/`.
|
|
381
|
+
- **Database modes are `database: { modes: [...] }`** — `single` (SQLite + Solid), `multiple` (one SQLite file on
|
|
382
|
+
a host volume + Redis), `cluster` (Postgres + Redis). The build carries the drivers of every declared mode and a
|
|
383
|
+
deployment picks one with `AKAN_DATABASE_MODE` — a declared one, named whenever the build declares several.
|
|
384
|
+
Where the data lives (`POSTGRES_URL`, `REDIS_URI`, `SQLITE_DATABASE_PATH`) is the deployment's env, ahead of
|
|
385
|
+
`env.server.ts`. `q.raw` is written in the dialect's own SQL, so an app running in two modes avoids it.
|
|
386
|
+
- **Backups run through the image's own `main.js`**: `bun main.js ops snapshot` / `ops restore` (offline), and
|
|
387
|
+
`/_akan/ops/*` for a control plane, mounted only when `AKAN_OPS_PUBLIC_KEY` is set and authenticated by a short
|
|
388
|
+
EdDSA token that key verifies. `GET /_akan/app/info` is public. `operationMode` gates none of it.
|
|
338
389
|
|
|
339
390
|
## React Components And Styling (`**/*.tsx`)
|
|
340
391
|
|
|
@@ -357,7 +408,7 @@ Full contract: `get_guideline` with `runtimeRule`, or `akan guideline show runti
|
|
|
357
408
|
nothing — they covered five control types, keyed on the translated label, and restored over server data.
|
|
358
409
|
- Static class strings stay plain strings. Reach for `cn` only for a conditional or to merge an incoming `className`, and merge the caller last: `cn("base classes", cond && "extra", className)`. `cn` comes from `akanjs/client` (token-aware tailwind-merge) and is the only class-combining function — no `clsx` (removed), no raw `twMerge` imports, no object syntax (`{ x: cond }` → `cond && "x"`).
|
|
359
410
|
- Multi-slot components take extra named props (`wrapperClassName`, `bodyClassName`), never a `classNames` object.
|
|
360
|
-
- Hoist enum→class lookups to a module-scope `as const` map typed `{ [key in cnst.XStatus["value"]]: string }`. Do not use `Record<...>`.
|
|
411
|
+
- Hoist enum→class lookups to a module-scope `as const` map typed `{ [key in cnst.XStatus["value"]]: string }`. Do not use `Record<...>`. When a second module needs it, the classes become a variant axis of a recipe in `ui/Recipe/`; only a map from the enum to another recipe's variant name (`BadgeVariants["variant"]`) moves to `webkit/`.
|
|
361
412
|
- Use `<Link>` from `akanjs/ui` for internal navigation; `<a>` only for `mailto:` and external links.
|
|
362
413
|
|
|
363
414
|
## Naming And Language
|
|
@@ -421,8 +472,11 @@ stores need none, because state and CRUD actions are generated. The body is thre
|
|
|
421
472
|
`this.setX(...)` → toast. The optimistic shape is mutate the client model, `void fetch.*`, then commit. Use
|
|
422
473
|
`this.pick(...)` when the value must exist, `this.get()` when it may not, and `this.set({...})` to write. Mutate lists
|
|
423
474
|
through the collection API (`this.set({ xList: xList.set(x).save() })`), not array spread. **An action returns
|
|
424
|
-
nothing** (`no-return-in-store-action.grit`). **
|
|
425
|
-
`
|
|
475
|
+
nothing** (`no-return-in-store-action.grit`). **Reach another store through `this`:**
|
|
476
|
+
`import type { RootStore } from "../st"`, then `(this as unknown as RootStore).logout()` to call its action or
|
|
477
|
+
`(this as unknown as RootStore).set({ … })` / `.get()` to write or read its state. Every store is mixed into one root
|
|
478
|
+
at runtime, so the cast only tells the type what `this` already is. Keep it `import type` — `st.ts` imports every
|
|
479
|
+
store, so a value import is a cycle.
|
|
426
480
|
|
|
427
481
|
**`<model>.dictionary.ts`** — fixed chain, with empty stages still written:
|
|
428
482
|
`.of() → .model() → .insight() → .query() → .sort() → .enum() → .slice() → .endpoint() → .error() → .translate()`.
|
|
@@ -475,13 +529,14 @@ Full contract — credential handshake, room revalidation, socket cleanup scopin
|
|
|
475
529
|
never bounded. **Losing the race does not cancel the work**: the handler runs to completion with nobody
|
|
476
530
|
holding its result, so a deadline is an answer to the caller, not an undo.
|
|
477
531
|
- **A cached answer is the endpoint's `{ cache: <ms> }`, and only a `query` taking no internal argument may have
|
|
478
|
-
one.**
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
handler's result, so `resolveReturn` still masks fields and resolves
|
|
483
|
-
is down is warned about and the call runs uncached. The default
|
|
484
|
-
|
|
532
|
+
one.** It is a step of the call, not a middleware, and stands aside for every endpoint that declares nothing.
|
|
533
|
+
Internal arguments are how a call learns who is asking (`.with(Self)`), so an endpoint that has them answers
|
|
534
|
+
per caller and one shared entry would be one caller's answer handed to the next — that endpoint and every
|
|
535
|
+
`mutation` are named in the log and left uncached. The lookup runs after the guards, so a hit reaches only a
|
|
536
|
+
caller they admitted; what is cached is the handler's result, so `resolveReturn` still masks fields and resolves
|
|
537
|
+
relations per call; a cache backend that is down is warned about and the call runs uncached. The default
|
|
538
|
+
middleware chain is `Logging → Timeout → <lib middlewares>`, then guards → internal arguments → cache →
|
|
539
|
+
handler, and there is no `Retry` middleware.
|
|
485
540
|
|
|
486
541
|
### Authorization Defaults
|
|
487
542
|
|
|
@@ -548,8 +603,11 @@ Full contract — filter-arg `ref` pickers, `getQueryMeta` summary counters, `la
|
|
|
548
603
|
first "more". `isCumulativeOf<Model>` is the mode.
|
|
549
604
|
- **"Is there more" is `hasMoreOf<Model>`, never `count > page * limit`.** It comes from the length of the batch
|
|
550
605
|
the server returned, so it survives `{ insight: false }` and cannot drift from a count a live event moved.
|
|
551
|
-
- **Hydrated vs raw:** server queries return hydrated `cnst.<Model>` instances (with `set`/`save`/`refresh`)
|
|
552
|
-
client fetch results
|
|
606
|
+
- **Hydrated vs raw:** server queries return hydrated `cnst.<Model>` instances (with `set`/`save`/`refresh`),
|
|
607
|
+
and so do client fetch results — `FetchClient` parses every return with `crystalize` defaulting to **true**
|
|
608
|
+
(`pkgs/akanjs/fetch/client/fetchClient.ts`). The raw `GetStateObject` shape is what the `<ref>Obj` /
|
|
609
|
+
`<ref>ObjList` / `<ref>ObjInsight` handles carry, because those pass `crystalize: false` on purpose so the
|
|
610
|
+
value can cross the RSC boundary as a client prop.
|
|
553
611
|
- Every filter generates fourteen methods: `list` · `listIds` · `find` · `findId` · `pick` · `pickId` · `exists` ·
|
|
554
612
|
`count` · `insight` · `query` · **`remove`** · **`removeOne`** · **`update`** · **`updateOne`**. The last four
|
|
555
613
|
are query-level writes — one atomic UPDATE, **no hooks**, and therefore no `_postRemove` and no cascade. Use
|
|
@@ -587,7 +645,7 @@ Full contract — relevance ordering, `columns` / `weights`: `get_guideline` wit
|
|
|
587
645
|
- Injected dependencies resolve by field-name convention: a field named `<refName>Service` resolves to the service registered under `<refName>`, and `<refName>Signal` likewise (`pkgs/akanjs/service/injectInfo.ts`).
|
|
588
646
|
- The `Service`/`Signal` suffix is required — the injector strips it to derive the registry lookup key. Name the field after the target refName plus the suffix, not arbitrarily.
|
|
589
647
|
- Preference order inside a service: `service<srv.XService>()` for another module's service · `plug(AdapterClass)` or `plug(StorageAdaptorRole)` for an adapter · `use<T>()` only to reach an `option.ts`-registered legacy singleton · `env(...)` for config.
|
|
590
|
-
- **`memory(...)` takes a scalar or model class, not only a primitive** — `memory(Map, { of: CallStateInput })` serializes through the constant and travels as JSON text, so the Redis and sqlite-backed caches round-trip the same declaration. Never hand-encode JSON into a `String` memory.
|
|
648
|
+
- **`memory(...)` takes a scalar or model class, not only a primitive** — `memory(Map, { of: CallStateInput })` serializes through the constant and travels as JSON text, so the Redis and sqlite-backed caches round-trip the same declaration. Never hand-encode JSON into a `String` memory. A memory is scoped to the service or adaptor that declares it; `ttl` (milliseconds) sets how long each write lives unless the `set` call names its own `expireAt`, and a `Map` entry expires on its own.
|
|
591
649
|
|
|
592
650
|
### Adapters — `adapt()` And `plug()`
|
|
593
651
|
|
|
@@ -662,10 +720,14 @@ it.
|
|
|
662
720
|
(RFC 7591; Cursor's `cursor://` redirect admitted) plus the consent page `libs/shared/page/oauth/consent` — and
|
|
663
721
|
names itself as the issuer, so `/mcp` demands a bearer token, verifies its signature, and refuses one carrying no
|
|
664
722
|
`aud`. A token is the app's own access JWT plus `aud`/`iss`/`client_id`, so `AccountMiddleware` and the guards
|
|
665
|
-
judge it unchanged
|
|
666
|
-
|
|
667
|
-
`
|
|
668
|
-
|
|
723
|
+
judge it unchanged. The tokens this built-in server mints carry no scope; `auth.scopes` / `AKAN_MCP_SCOPES` are for
|
|
724
|
+
tokens an external issuer mints, and declared beside this server they refuse every one of its tokens with 403
|
|
725
|
+
`insufficient_scope`. Per-app knobs live in `env.server.*` under `oauth` (`consentPath` and
|
|
726
|
+
`signinPath` with the basePath, `clients`, `dynamicRegistration`, `allowedRedirectSchemes`, `accessTokenSeconds`,
|
|
727
|
+
`clientIdMetadata`, `enabled`, plus `issuer` / `resource` — set those when a tunnel or an edge makes the derived
|
|
728
|
+
origin wrong, because an MCP client compares the issuer byte for byte). `/mcp` reads the `Authorization` header only — a cookie is dropped at the door — and `JWT_SECRET` (or
|
|
729
|
+
`security.jwtSecret`) is mandatory whenever `operationMode` is not `local`, because the derived fallback is
|
|
730
|
+
forgeable from three public names; `AKAN_ALLOW_DERIVED_JWT_SECRET=1` accepts that risk explicitly. A grant is revoked
|
|
669
731
|
whole — `POST /oauth/revoke` (RFC 7009) by the client, `fetch.revokeOAuthConnection(sessionId)` by the account's
|
|
670
732
|
owner, `fetch.listOAuthConnections()` to see them — and a revoked access token is refused at its next call.
|
|
671
733
|
- **A person may, a model may not.** An agent's token names its `client_id` and `aud`; a browser session has
|
|
@@ -714,9 +776,12 @@ what every developer must know even when not building one.
|
|
|
714
776
|
every tool runs in the caller's own browser session, gated by guards and the approval card. Its guard is
|
|
715
777
|
`AgentRelayAccess`, which refuses every call until the app names a guard of its own —
|
|
716
778
|
`option.setAgentAccess(SignedIn)`. **The LLM is configured in `option.ts`, never through the environment**
|
|
717
|
-
(`option.setLlm({ apiKey, model, host })`).
|
|
718
|
-
|
|
719
|
-
`
|
|
779
|
+
(`option.setLlm({ apiKey, model, host })`). Two adaptors ship, one per wire: `OpenaiLlm` is the default and
|
|
780
|
+
speaks the chat-completions dialect to whatever `host` names — OpenAI, DeepSeek, Groq, OpenRouter, Ollama — and
|
|
781
|
+
`AnthropicLlm` speaks the Messages API and reads a PDF as well as a picture. Both require `model`. A provider
|
|
782
|
+
neither covers is an `adapt()` class in `srvkit/` implementing `LlmAdaptor`, applied with
|
|
783
|
+
`option.applyAdaptor(LlmAdaptorRole, TheClass)`; whatever settings it needs beyond `LlmOption` ride `setLlm`
|
|
784
|
+
too, read back with `use<MyLlmOption>()`.
|
|
720
785
|
- **Declare the tool beside the control that already does it**, never as a separate surface:
|
|
721
786
|
`st.tool("x").desc("…").arg("id", ID).exec(fn)` returns the callable to hand to `onClick` — one handler for the
|
|
722
787
|
person and the agent. Publish it only where the screen already renders the control; a falsy name declares the
|
|
@@ -779,7 +844,8 @@ Full contract — the trigger-maintained mirror, tokenizer changes, `AKAN_SEARCH
|
|
|
779
844
|
backstop, including for a `text` field *underneath* one of those. Do not work around either.
|
|
780
845
|
- The role works on a relation (`image: field(File, { text: "thumb" })`) and on an array; an array of objects
|
|
781
846
|
indexes by leaf key. A field inside a `Map` indexes nothing — there is no fixed path to extract it from.
|
|
782
|
-
- Search runs
|
|
847
|
+
- Search runs in every database mode — fts5 on SQLite/libSQL, a weighted `tsvector` (`pg_trgm` for `trigram`) on
|
|
848
|
+
Postgres. The same text matches the same documents; only the order may differ on Postgres.
|
|
783
849
|
|
|
784
850
|
### Image & File Fields
|
|
785
851
|
|
|
@@ -828,10 +894,17 @@ shape, so `cascade` never means "related" — it means one of exactly these:
|
|
|
828
894
|
- Dynamic segments use `[id]`; route groups use directories like `(user)`, `(public)`, `(tab)`, or `(detail)`.
|
|
829
895
|
- **A route file exports one thing**: `export default page()…render(…)` for a page, `layout()…render(…)` for a
|
|
830
896
|
`_layout.tsx`, `rootLayout()…render(…)` for the app's (or a basePath's) root `_layout.tsx`. Every route setting
|
|
831
|
-
is a stage of the chain — `.param()`, `.search()`, `.config()`, `.head()
|
|
832
|
-
`.
|
|
833
|
-
|
|
834
|
-
|
|
897
|
+
is a stage of the chain — `.param()`, `.search()`, `.config()`, `.head()`, `.loading()`; `.notFound()` /
|
|
898
|
+
`.error()` on a layout; `.fonts()` / `.theme()` / `.manifest()` / `.reconnect()` / `.wsConnect()` /
|
|
899
|
+
`.layoutStyle()` on the root layout; `.prompt()` on a page. A named export beside the chain, or `page()` in a
|
|
900
|
+
`_layout.tsx`, fails the build. `.head()` takes JSX — `<title>`, `<meta>`, `<link>` — or a function of the
|
|
901
|
+
route's args returning it; there is no metadata object, and analytics is the app's own script, not a stage.
|
|
902
|
+
- **A CSR bundle (a native app, `?csr=true`) never shows a root layout's own markup.** Pages render into the
|
|
903
|
+
frame, and what the root layout draws around `{children}` stays in a hidden container, so its wrappers cannot
|
|
904
|
+
stack a second screen beside the frame. Its components still mount and their effects run (`<Auth.User />` works).
|
|
905
|
+
Anything a person must see from a root layout — an overlay, a gate, a banner — renders through
|
|
906
|
+
`createPortal(…, document.body)`, as `<Agent.Chat />` and the dialogs already do. A non-root `_layout.tsx` has no
|
|
907
|
+
such limit.
|
|
835
908
|
- **A page names every `[x]` segment of its path with `.param("x", Type)`** and reads a query key only through
|
|
836
909
|
`.search("k", Type)`. Values arrive typed — `ID`/`String` → string, `Int`/`Float` → number, `Boolean`, `Date` →
|
|
837
910
|
Dayjs, an `enumOf` class → its union, `[T]` → array — a path value the type refuses answers not-found, and a
|
|
@@ -874,10 +947,10 @@ export default page()
|
|
|
874
947
|
|
|
875
948
|
## Akan Sync Conventions (`apps/**`, `libs/**`)
|
|
876
949
|
|
|
877
|
-
- `apps/<appName>` root may only contain these files: `AGENTS.md`, `CLAUDE.md`, `akan.app.json`, `akan.config.ts`, `
|
|
878
|
-
- `apps/<appName>` root may only contain these folders: `.akan`, `
|
|
950
|
+
- `apps/<appName>` root may only contain these files: `AGENTS.md`, `CLAUDE.md`, `akan.app.json`, `akan.config.ts`, `client.ts`, `main.ts`, `package.json`, `server.ts`, `tsconfig.json`, `tsconfig.tsbuildinfo`.
|
|
951
|
+
- `apps/<appName>` root may only contain these folders: `.akan`, `common`, `env`, `lib`, `native`, `page`, `plugin`, `private`, `public`, `script`, `secrets`, `srvkit`, `ui`, `webkit`.
|
|
879
952
|
- `libs/<libName>` root may only contain these files: `AGENTS.md`, `CLAUDE.md`, `README.md`, `akan.config.ts`, `akan.lib.json`, `client.ts`, `index.ts`, `package.json`, `server.ts`, `tsconfig.json`, `tsconfig.spec.json`, `tsconfig.tsbuildinfo`.
|
|
880
|
-
- `libs/<libName>` root may only contain these folders: `common`, `env`, `lib`, `page`, `plugin`, `private`, `public`, `srvkit`, `ui`, `webkit`. A library is never booted or packaged as an app, so the run and mobile entries an app carries (`main.ts`,
|
|
953
|
+
- `libs/<libName>` root may only contain these folders: `common`, `env`, `lib`, `native`, `page`, `plugin`, `private`, `public`, `srvkit`, `ui`, `webkit`. A library is never booted or packaged as an app, so the run and mobile entries an app carries (`main.ts`, `.akan`, `script`, `secrets`) are rejected there. A Capacitor-era `ios` / `android` / `mobile` folder or `capacitor.config.*` is refused in an app root too, named as a leftover: the native runtime generates its projects under `.akan/native/<target>`.
|
|
881
954
|
- Both allowlists have one source — `pkgs/@akanjs/devkit/workspaceLayout.ts`. `akan sync` (error), `akan doctor`
|
|
882
955
|
(diagnostic), and `akan quality scan` (warning) all read it, so add a new root entry there and mirror it into this
|
|
883
956
|
list, never into one of the three call sites.
|
|
@@ -901,12 +974,18 @@ export default page()
|
|
|
901
974
|
| Folder | Admission test | Naming |
|
|
902
975
|
|---|---|---|
|
|
903
976
|
| `common/` | pure, isomorphic, zero-dependency; may import only sibling `common/*` and `akanjs/base`. Cannot import `Err`, so keep throwing code out of it. | camelCase file, filename equals the single export |
|
|
904
|
-
| `webkit/` | touches `window` / `navigator` /
|
|
977
|
+
| `webkit/` | touches `window` / `navigator` / the native bridge (`akanjs/client/native`, or `akanjs/client/native/<plugin id>` for any builtin plugin), or is a React hook | `use<Thing>.tsx` — `.tsx` even with no JSX |
|
|
905
978
|
| `srvkit/` | touches `node:*`, `Bun`, `process.env`, a secret, or a server SDK | camelCase file, PascalCase class |
|
|
906
|
-
| `ui/` | renders JSX and is not bound to one model | PascalCase component, camelCase sidecar (`swipeCard.util.ts`) |
|
|
979
|
+
| `ui/` | renders JSX, or defines a look (a recipe in `Recipe/`, a lib's `tokens.css`), and is not bound to one model | PascalCase component, camelCase sidecar (`swipeCard.util.ts`), `Recipe/<name>.ts` |
|
|
907
980
|
| `plugin/` | build- or CLI-time `AkanPlugin` | `<name>.plugin.ts`, registered in `akan.config.ts` |
|
|
981
|
+
| `native/` | a native plugin the app or lib owns: `native-plugin.json` beside its page API (`src/index.ts`, `definePlugin` from `akanjs/client/native`), its desktop part (`src/desktop.ts`, `defineDesktopPlugin` from `akanjs/native/desktop`), Kotlin and Swift. Every mobile and desktop target ships it; a lib's reach only the apps that depend on it, and the app's own wins an id | `native/<id>/`, the folder named after the plugin id |
|
|
908
982
|
|
|
909
983
|
- Hooks return a named object of async closures, never a tuple.
|
|
984
|
+
- A `native/` plugin's page API is imported by a `webkit/` hook (`../native/<id>/src`) and nothing else; pages and
|
|
985
|
+
components call the hook, the same way they reach `akanjs/client/native`.
|
|
986
|
+
- `ui/` is the presentation layer — markup and the looks it is built from — so a recipe lives there although it is
|
|
987
|
+
neither a component nor a hook. Not `webkit/`: its hooks are `"use client"`, the opposite signal for a function
|
|
988
|
+
server components call. Not `common/`: it cannot import `akanjs/ui`, where `recipe` / `tv` come from.
|
|
910
989
|
- `libs/<lib>/ui/tokens.css` is the one CSS file a lib owns: plain `:root` custom properties for colors that must **not** follow the theme (a vendor brand color, a fixed surface). Every app whose pages reach that lib compiles it automatically, ahead of the app's own stylesheets, so nothing is imported by hand and no app can forget it. Reference them as `bg-[var(--kakao)]`; `@theme` extensions stay in the app stylesheet, because the color vocabulary is closed per stylesheet. Theme-following colors are the app's, not the lib's.
|
|
911
990
|
- A layer-root `index.ts` is generated, but a `ui/<Folder>/index.tsx` that builds a namespace is hand-written source. The distinguishing test is that a generated barrel contains nothing but `export * from "./X";` lines.
|
|
912
991
|
- `ui/<Folder>/index_.tsx` (trailing underscore) is the `"use client"` + `lazy()` boundary, with a server-safe `index.tsx` beside it. Collapsing the pair into one file breaks RSC.
|
|
@@ -919,12 +998,15 @@ when two shapes disagree.
|
|
|
919
998
|
- `children: any` — newer files use `children: ReactNode`.
|
|
920
999
|
- Hard-coded API keys or secrets in source; they belong in `option.ts` or env.
|
|
921
1000
|
- Large blocks of commented-out code left in place.
|
|
922
|
-
- Cross-store writes through a `RootStore` cast — it collides with the Bun SSR-bundler segfault.
|
|
923
1001
|
- Stale `// TODO: Implement …` comments above implemented methods.
|
|
924
1002
|
- `{cond && <X/>}` in JSX, hard-coded Korean bypassing `l()`, and `window.alert(...)` for user feedback.
|
|
925
1003
|
- Bare `/* eslint-disable */` blocks — use `// biome-ignore lint/<rule>: <why>`.
|
|
926
1004
|
- Raw palette grays such as `text-gray-400` instead of the semantic tokens (`text-foreground/70`).
|
|
927
1005
|
|
|
1006
|
+
A store calling `(this as unknown as RootStore).<action>()` or `.set({ … })` over
|
|
1007
|
+
`import type { RootStore } from "../st"` is **not** one of these — it is the sanctioned way to reach another store's
|
|
1008
|
+
actions and state (see `<model>.store.ts` above).
|
|
1009
|
+
|
|
928
1010
|
## Secrets And Env Safety (`.env`, `infra/**`, `*secret*`, `*credential*`)
|
|
929
1011
|
|
|
930
1012
|
- Never print, summarize, commit, or expose real secret values, credentials, tokens, private keys, or `.env` contents.
|
|
@@ -943,7 +1025,9 @@ when two shapes disagree.
|
|
|
943
1025
|
selected one's log beside it. `Tab` walks every row and `1`-`9` jump to that app (the rail shows each
|
|
944
1026
|
app's digit), `↑↓` scroll (shift for a page, `G` back to following),
|
|
945
1027
|
`/` greps, `e` shows stderr only, `c` clears, `y` copies, `Y` copies the log path, `o` opens the browser,
|
|
946
|
-
`r` restarts that app, `q` quits.
|
|
1028
|
+
`r` restarts that app, `q` quits. Under `--share`, `s` copies the public URL — the selected app's, or every
|
|
1029
|
+
app's from the merged row — and the header carries it ahead of the local one, because the line `--share`
|
|
1030
|
+
printed before the view took the terminal is gone by its first repaint.
|
|
947
1031
|
**`--plain` prints prefixed interleaved lines instead**, and a pipe, a redirect or an unsized terminal
|
|
948
1032
|
downgrades to that on its own.
|
|
949
1033
|
- **A supervised session writes `local/apps/<app>/runtime/dev.log`, and that path is how a log gets handed over.**
|
|
@@ -954,11 +1038,13 @@ when two shapes disagree.
|
|
|
954
1038
|
other file. The previous session is kept beside it as `dev.prev.log`. Hand somebody — or an agent — the
|
|
955
1039
|
path rather than a paste: it costs one line instead of a transcript, and it can be re-read after a fix.
|
|
956
1040
|
In the view, `y` copies the lines the filters have already narrowed and `Y` copies the path.
|
|
957
|
-
- **How many apps boot at once is the machine's answer, not a constant.** A cold boot
|
|
958
|
-
|
|
959
|
-
divided by that peak, and one app per four cores —
|
|
960
|
-
|
|
961
|
-
|
|
1041
|
+
- **How many apps boot at once is the machine's answer, not a constant.** A cold boot is an app's RSS peak
|
|
1042
|
+
(~1.8GB per app, most of it the build worker — the base build, then the SSR registry's), so the default wave
|
|
1043
|
+
is what memory and cores allow — half the memory budget divided by that peak, and one app per four cores —
|
|
1044
|
+
which boots a laptop's apps together and still staggers them inside a small container. The session says
|
|
1045
|
+
which in one line. The next wave starts once an app serves and its boot builds have settled, or 30 seconds
|
|
1046
|
+
after it serves, while the app shows as ready — and `--open` opens it — as soon as it serves.
|
|
1047
|
+
`--concurrency <n>` overrides the wave size, and `AKAN_MEMORY_LIMIT` lowers the budget it derives from.
|
|
962
1048
|
- `--kill` frees the dev ports first — it resolves each port's listener, walks up to the top of that akan dev
|
|
963
1049
|
tree and signals it, so another checkout's server or a stale orphan on the same port is reclaimed. A holder
|
|
964
1050
|
that is not recognisably an akan process is reported and left alone.
|
|
@@ -189,11 +189,13 @@ export default page()
|
|
|
189
189
|
- OAuth resource metadata is published at `/.well-known/oauth-protected-resource` (and at that path plus the mount
|
|
190
190
|
path, the spelling most clients try first). `auth.authorizationServers` / `AKAN_MCP_AUTH_SERVERS`,
|
|
191
191
|
`auth.scopes` / `AKAN_MCP_SCOPES`, `auth.resource` / `AKAN_MCP_RESOURCE` and `auth.verify` configure it;
|
|
192
|
-
`insufficient_scope` is enforced only once scopes are declared.
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
issuer
|
|
192
|
+
`insufficient_scope` is enforced only once scopes are declared. Declare them only for tokens an external issuer
|
|
193
|
+
mints: the built-in authorization server's tokens carry no scope, so beside it every one of them would be refused
|
|
194
|
+
with 403. Naming an issuer changes three things at once: a request with no credential is answered 401 from
|
|
195
|
+
`initialize` on (an MCP client starts its OAuth flow on nothing else), a token carrying no `aud` is refused (that
|
|
196
|
+
issuer mints for its other resources too), and the challenge carries no `error` code (RFC 6750 §3.1 reserves it
|
|
197
|
+
for a credential that was presented). A server naming no issuer keeps anonymous access and accepts a first-party
|
|
198
|
+
token, which is bound by app and environment instead.
|
|
197
199
|
- **The boot log names every published entry with no dictionary `.desc()`.** An agent picks a tool by its
|
|
198
200
|
description, so a missing one is a broken tool. What the framework generates has no text of its own and borrows
|
|
199
201
|
the model's: the generated list reads the `.of()` label, and the base CRUD tools append the model's `.desc()` to
|
|
@@ -31,6 +31,10 @@ with it. `conventions` carries the invariants — this is the full contract behi
|
|
|
31
31
|
A model with none renders ids and says so in the picker.
|
|
32
32
|
- A slice's `exec` returns a `QueryOf` (an opaque query descriptor, `pkgs/akanjs/constant/types.ts`); you **cannot** chain `.sort()`/`.limit()` on it.
|
|
33
33
|
- Apply ordering/paging via the store `init` fetch option instead: `initX(..., { sort, page, limit })` (`pkgs/akanjs/fetch/fetchType/sliceFetch.type.ts`).
|
|
34
|
+
- **Every sort ends with `id`, in the direction of its last key, and a missing value sorts below every other** —
|
|
35
|
+
first ascending, last descending, in every database mode and in the client placing a live row. Rows the declared
|
|
36
|
+
keys tie on (a midpoint priority dragged into one gap until the float runs out, two rows saved in one millisecond)
|
|
37
|
+
therefore keep one order across `loadMoreOf<Model>()` pages and live insertions.
|
|
34
38
|
- **A slice list is either one window or an accumulated one, and the store says which.** `setPageOf<Model>` swaps
|
|
35
39
|
the window and `pageOf<Model>` names it; `loadMoreOf<Model>()` appends the rows *after* the ones already loaded
|
|
36
40
|
and takes no page number, because in an accumulated list no page number names anything. It skips by
|
|
@@ -49,7 +53,7 @@ with it. `conventions` carries the invariants — this is the full contract behi
|
|
|
49
53
|
`findBy<Filter>(...args, { select: { secretField: true } })` takes it inside the option object. The shapes do not
|
|
50
54
|
swap: `{ select: … }` handed to the facade projects a field named `select`, which no model has, and the read comes
|
|
51
55
|
back empty with no error. This is the only way to read a `field.secret(...)` value, which is otherwise stripped.
|
|
52
|
-
- **Hydrated vs raw:** server queries return hydrated `cnst.<Model>` instances (with `set`/`save`/`refresh`)
|
|
56
|
+
- **Hydrated vs raw:** server queries return hydrated `cnst.<Model>` instances (with `set`/`save`/`refresh`), and so do client fetch results — `FetchClient` parses every return with `crystalize` defaulting to **true** (`pkgs/akanjs/fetch/client/fetchClient.ts`). The raw `GetStateObject` shape (functions stripped, `pkgs/akanjs/base/types.ts`) is what the `<ref>Obj` / `<ref>ObjList` / `<ref>ObjInsight` handles carry, because those pass `crystalize: false` on purpose so the value can cross the RSC boundary as a client prop.
|
|
53
57
|
- Every filter generates fourteen methods: `list` · `listIds` · `find` · `findId` · `pick` · `pickId` · `exists` ·
|
|
54
58
|
`count` · `insight` · `query` · **`remove`** · **`removeOne`** · **`update`** · **`updateOne`**. The last four are
|
|
55
59
|
query-level writes — one atomic UPDATE, **no hooks**, and therefore no `_postRemove` and no cascade. Use them on a
|
|
@@ -90,14 +94,15 @@ with it. `conventions` carries the invariants — this is the full contract behi
|
|
|
90
94
|
- **A slice endpoint never reaches "unspecified".** The resolver fills `latest` before the query is built, so a
|
|
91
95
|
client asking for the score order has to name `relevance`; leaving `sort` off gets `latest`, not relevance.
|
|
92
96
|
- Scope a search with `columns` (`q.search(text, { columns: ["title"] })`) and re-weight with `weights`, a tuple of
|
|
93
|
-
finite numbers positional over `["title", "desc", "tag", "filter"]
|
|
97
|
+
finite, non-negative numbers positional over `["title", "desc", "tag", "filter"]` — Postgres ranks by weight
|
|
98
|
+
class and has no reading for a negative one.
|
|
94
99
|
|
|
95
100
|
|
|
96
101
|
## Text Search Fields — the `text` role
|
|
97
102
|
|
|
98
103
|
- A field joins the full-text index by declaring one of five roles: `field(String, { text: "title" })`, and likewise
|
|
99
104
|
`"desc"`, `"tag"`, `"thumb"`, `"filter"`. Nothing else opts a field in, and there is no per-model switch.
|
|
100
|
-
- Pick the role by what the value *is*, because
|
|
105
|
+
- Pick the role by what the value *is*, because the rank weights them positionally (`title` 10, `tag` 3, `desc` 1,
|
|
101
106
|
`filter` 0): `title` is the one line a human scans for, `desc` is prose, `tag` is a keyword list, `filter` is a
|
|
102
107
|
scoping value (status, owner, role) that must be matchable but must never outrank a real title hit.
|
|
103
108
|
- `thumb` is mirrored for rendering a hit and is **not** indexed — never expect it to match.
|
|
@@ -114,15 +119,24 @@ with it. `conventions` carries the invariants — this is the full contract behi
|
|
|
114
119
|
- Declaring roles is all the wiring there is. Mirror rows are maintained by SQL triggers — not document hooks —
|
|
115
120
|
because `updateOneByQuery` and friends fire no hooks, and most searchable-field mutations go through exactly that
|
|
116
121
|
path.
|
|
117
|
-
- Search runs on
|
|
122
|
+
- Search runs in every database mode: fts5 on SQLite/libSQL, a weighted `tsvector` under a GIN index on Postgres.
|
|
123
|
+
For the same text the same documents match — punctuation splits words the way unicode61 does, and a phrase never
|
|
124
|
+
runs from one column into the next — but the order may differ, since Postgres ranks without document frequency.
|
|
125
|
+
Assert on the match set, never on a Postgres order the weights alone do not decide.
|
|
126
|
+
- Postgres reads `AKAN_SEARCH_TOKENIZER` as `unicode61 [remove_diacritics 0|1|2]` (`simple`, with `unaccent` unless
|
|
127
|
+
`0`) or `trigram [case_sensitive 0|1]` (`pg_trgm`); any other spelling is refused. The extension is created when
|
|
128
|
+
the role may, otherwise a role that may runs `CREATE EXTENSION`. It indexes the first 20,000 characters of
|
|
129
|
+
`title`, `tag` and `filter` and 200,000 of `desc` — a tsvector refuses more than 1MB, and the refusal would fail
|
|
130
|
+
the model write. A database whose `LC_CTYPE` is `C` keeps non-ASCII case apart; use a UTF-8 ctype.
|
|
118
131
|
- `AKAN_SEARCH_ENABLED=0` switches the index off process-wide; unset means on. It never deletes mirror data, and
|
|
119
132
|
re-enabling reconciles every ref. **Give every process the same value** — a process cannot drop triggers for models
|
|
120
133
|
it does not mount, so a mixed fleet leaves stale triggers behind.
|
|
121
134
|
- The tokenizer is `AKAN_SEARCH_TOKENIZER` (or `database.search.tokenizer`, which wins), defaulting to
|
|
122
135
|
`unicode61 remove_diacritics 2`. Changing it rebuilds the index from the mirror on the next boot — the model
|
|
123
|
-
tables are never re-read — so it is a safe knob, unlike a `text` role change, which re-reads every row. The
|
|
124
|
-
|
|
125
|
-
|
|
136
|
+
tables are never re-read — so it is a safe knob, unlike a `text` role change, which re-reads every row. The schema
|
|
137
|
+
is ensured in one write turn — a SQLite write transaction, a Postgres schema lock — so of a fleet restarted at
|
|
138
|
+
once the first rebuilds and the rest find it current. A SQLite process waits for that turn up to its
|
|
139
|
+
`busy_timeout`, so stagger the restart when the mirror is large.
|
|
126
140
|
|
|
127
141
|
## Cascade Remove — the `cascade` option
|
|
128
142
|
|