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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (129) hide show
  1. package/.build-stamp +1 -1
  2. package/README.ko.md +1 -1
  3. package/README.md +1 -1
  4. package/{abstractCompactor-e5yq60qz.js → abstractCompactor-20e0cwbs.js} +2 -2
  5. package/{agent.command-pggr92jj.js → agent.command-xdvw642m.js} +9 -7
  6. package/{application.command-692yap1d.js → application.command-g3f7xj25.js} +31 -6
  7. package/{applicationBuildRunner-ha3y8ew8.js → applicationBuildRunner-af0wtspv.js} +19 -14
  8. package/{applicationReleasePackager-gg8ccbeh.js → applicationReleasePackager-93nzdfsy.js} +1 -1
  9. package/buildBatch.proc.js +7 -5
  10. package/{capacitorApp-p0z5yab2.js → capacitorApp-b0ppke3m.js} +29 -27
  11. package/{cloud.command-wqfsyzv2.js → cloud.command-0hmsv44q.js} +12 -10
  12. package/commandManifest.json +1 -1
  13. package/{context.command-9bxtdn0w.js → context.command-x92afrnf.js} +17 -15
  14. package/{dependencyScanner-f6grrep3.js → dependencyScanner-rbs2407b.js} +1 -1
  15. package/getDirname-ka11hm7e.js +8 -0
  16. package/{guideline.command-nfqahxex.js → guideline.command-hpq08q66.js} +8 -6
  17. package/guidelines/agentRule/agentRule.generate.json +31 -0
  18. package/guidelines/agentRule/agentRule.instruction.md +525 -0
  19. package/guidelines/conventions/conventions.generate.json +25 -0
  20. package/guidelines/conventions/conventions.instruction.md +839 -0
  21. package/guidelines/cssRule/cssRule.instruction.md +7 -0
  22. package/guidelines/framework/framework.generate.json +3 -2
  23. package/guidelines/framework/framework.instruction.md +1 -9
  24. package/guidelines/mcpRule/mcpRule.generate.json +25 -0
  25. package/guidelines/mcpRule/mcpRule.instruction.md +217 -0
  26. package/guidelines/queryRule/queryRule.generate.json +31 -0
  27. package/guidelines/queryRule/queryRule.instruction.md +143 -0
  28. package/guidelines/runtimeRule/runtimeRule.generate.json +25 -0
  29. package/guidelines/runtimeRule/runtimeRule.instruction.md +176 -0
  30. package/guidelines/transportRule/transportRule.generate.json +31 -0
  31. package/guidelines/transportRule/transportRule.instruction.md +60 -0
  32. package/guidelines/workspaceOnboarding/workspaceOnboarding.generate.json +24 -0
  33. package/guidelines/workspaceOnboarding/workspaceOnboarding.instruction.md +263 -0
  34. package/guidelines/workspaceRecipes/workspaceRecipes.generate.json +33 -0
  35. package/guidelines/workspaceRecipes/workspaceRecipes.instruction.md +402 -0
  36. package/incrementalBuilder.proc.js +8 -6
  37. package/index-1577bej2.js +7 -0
  38. package/{index-vg0sj9nc.js → index-5f4gnt4c.js} +258 -97
  39. package/{index-bjpxzr6s.js → index-5vy2jzar.js} +1 -1
  40. package/{index-mssqn2sq.js → index-6d5r8q6k.js} +54 -15
  41. package/{index-p3f4q532.js → index-737aj4fm.js} +14 -13
  42. package/{index-2c3e2xh8.js → index-8besc14m.js} +1 -1
  43. package/{index-qb61h0k7.js → index-abyjt72y.js} +333 -540
  44. package/{index-xhgtkekh.js → index-cbfp5wyp.js} +1 -1
  45. package/{index-5dw57f0d.js → index-cp00yns2.js} +80 -14
  46. package/{index-ztpw544k.js → index-ety6takb.js} +3 -3
  47. package/index-f39512t6.js +255 -0
  48. package/{index-6rhabeev.js → index-gvq17wqn.js} +5 -3
  49. package/{index-js368gk9.js → index-hs70d8ed.js} +6 -6
  50. package/{index-7myvn0f0.js → index-j37qq1f2.js} +1 -1
  51. package/{index-azv0xjb5.js → index-jybbmwed.js} +8 -4
  52. package/{index-4hek879p.js → index-m5gfc86a.js} +1 -1
  53. package/{index-w6wbb612.js → index-nq3avsf6.js} +13 -13
  54. package/{index-vtj3zps1.js → index-p27h7535.js} +8 -8
  55. package/{index-5pmx7bkg.js → index-pehn44hr.js} +3 -3
  56. package/{index-j9cxndc0.js → index-qnv0aqmm.js} +9 -6
  57. package/{index-gmy45vz2.js → index-r691v756.js} +4 -4
  58. package/{index-0cj2zxbm.js → index-sh01r3pm.js} +1 -1
  59. package/{index-a1e0j63x.js → index-sqsfv8wg.js} +15 -7
  60. package/{index-gex7c8kd.js → index-wbbswwqt.js} +51 -24
  61. package/{index-bz2sfzzt.js → index-wybbcgxn.js} +4 -4
  62. package/index.js +21 -19
  63. package/{library.command-md9715jz.js → library.command-8vgrdtwy.js} +6 -4
  64. package/{localRegistry.command-exkbamyj.js → localRegistry.command-0gtqj0r0.js} +11 -9
  65. package/{module.command-w5aaddxk.js → module.command-rama4zya.js} +10 -8
  66. package/{package.command-yfkrcy0z.js → package.command-6jxapdsg.js} +6 -4
  67. package/package.json +4 -5
  68. package/{page.command-echbavp4.js → page.command-hxy7x525.js} +6 -4
  69. package/{primitive.command-9fbhspw2.js → primitive.command-4bf6xcc3.js} +11 -9
  70. package/{quality.command-3pnt9yep.js → quality.command-h5edwegz.js} +192 -97
  71. package/{recipeScanner-tghkzrrt.js → recipeScanner-zrzev25x.js} +3 -3
  72. package/{repair.command-carj4vax.js → repair.command-rgqf6px6.js} +8 -6
  73. package/{routeSourceValidator-e4jmbfcx.js → routeSourceValidator-7fycdqyv.js} +1 -1
  74. package/{scalar.command-51ryzypc.js → scalar.command-6krkz6tw.js} +9 -7
  75. package/templates/app/env/env.client.debug.ts.template +1 -5
  76. package/templates/app/env/env.client.develop.ts.template +1 -5
  77. package/templates/app/env/env.client.local.ts.template +1 -5
  78. package/templates/app/env/env.client.main.ts.template +1 -5
  79. package/templates/app/env/env.client.testing.ts.template +1 -5
  80. package/templates/app/env/env.client.type.ts +2 -6
  81. package/templates/app/env/env.server.debug.ts.template +1 -5
  82. package/templates/app/env/env.server.develop.ts.template +1 -5
  83. package/templates/app/env/env.server.local.ts.template +1 -5
  84. package/templates/app/env/env.server.main.ts.template +1 -5
  85. package/templates/app/main.ts +3 -1
  86. package/templates/app/page/styles.css.template +3 -3
  87. package/templates/appSample/srvkit/AuthGuard.ts +9 -5
  88. package/templates/appSample/srvkit/SessionInternalArg.ts +4 -3
  89. package/templates/crudPages/[__model__Id]/edit/page.tsx +1 -1
  90. package/templates/crudPages/new/page.tsx +1 -1
  91. package/templates/crudSinglePage/page.tsx +1 -1
  92. package/templates/env/_env.server.type.ts +7 -5
  93. package/templates/lib/db.ts +1 -1
  94. package/templates/lib/dict.ts +1 -1
  95. package/templates/lib/sig.ts +1 -1
  96. package/templates/lib/srv.ts +6 -2
  97. package/templates/module/__Model__.Util.tsx +1 -1
  98. package/templates/module/__Model__.Zone.tsx +3 -5
  99. package/templates/workspaceRoot/AGENTS.md.template +4 -972
  100. package/templates/workspaceRoot/CLAUDE.md.template +19 -0
  101. package/templates/workspaceRoot/biome.json.template +4 -244
  102. package/templates/workspaceRoot/docs/AI-DEVELOPMENT.md.template +10 -14
  103. package/templates/workspaceRoot/docs/GENERATED.md.template +12 -10
  104. package/templates/workspaceRoot/package.json.template +1 -1
  105. package/{typeChecker-kravn7ns.js → typeChecker-g6bh0xfv.js} +1 -1
  106. package/typecheck.proc.js +1 -1
  107. package/{workflow.command-91wmvm8k.js → workflow.command-dwhwrqhd.js} +16 -14
  108. package/{workspace.command-wa10mfd9.js → workspace.command-f5h11gjn.js} +71 -52
  109. package/templates/workspaceRoot/.cursor/rules/akan-page-routing.mdc.template +0 -43
  110. package/templates/workspaceRoot/.cursor/rules/akan-sync-conventions.mdc.template +0 -34
  111. package/templates/workspaceRoot/.cursor/rules/application-test-commands.mdc.template +0 -16
  112. package/templates/workspaceRoot/.cursor/rules/change-scope.mdc.template +0 -12
  113. package/templates/workspaceRoot/.cursor/rules/client-server-boundaries.mdc.template +0 -18
  114. package/templates/workspaceRoot/.cursor/rules/coding-comments.mdc.template +0 -25
  115. package/templates/workspaceRoot/.cursor/rules/coding-style.mdc.template +0 -44
  116. package/templates/workspaceRoot/.cursor/rules/domain-module-conventions.mdc.template +0 -17
  117. package/templates/workspaceRoot/.cursor/rules/format-lint.mdc.template +0 -12
  118. package/templates/workspaceRoot/.cursor/rules/lint-enforced-rules.mdc.template +0 -19
  119. package/templates/workspaceRoot/.cursor/rules/module-file-playbook.mdc.template +0 -62
  120. package/templates/workspaceRoot/.cursor/rules/naming-conventions.mdc.template +0 -15
  121. package/templates/workspaceRoot/.cursor/rules/react-component-style.mdc.template +0 -21
  122. package/templates/workspaceRoot/.cursor/rules/repo-overview.mdc.template +0 -17
  123. package/templates/workspaceRoot/.cursor/rules/scalar-modeling.mdc.template +0 -99
  124. package/templates/workspaceRoot/.cursor/rules/secrets-and-env.mdc.template +0 -13
  125. package/templates/workspaceRoot/.cursor/rules/service-signal-conventions.mdc.template +0 -83
  126. package/templates/workspaceRoot/.cursor/rules/srvkit-adapters-and-guards.mdc.template +0 -50
  127. package/templates/workspaceRoot/.cursor/rules/ssr-first.mdc.template +0 -59
  128. package/templates/workspaceRoot/.cursor/rules/typescript-imports.mdc.template +0 -16
  129. /package/{index-r24hmh0q.js → index-yb2wsc7f.js} +0 -0
@@ -0,0 +1,176 @@
1
+ # Runtime Rule — Serving, Processes, Logging, Image, Assets
2
+
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
+ its image. Everything here is declared in `akan.config.ts` or narrowed by an env at boot; none of it is reached
5
+ from domain code. `conventions` carries the invariants — this is the full contract behind them.
6
+
7
+ ## Web Surfaces — Building And Serving Without SSR/CSR
8
+
9
+ An app serves three things: the API, the SSR/RSC web renderer, and the CSR single-file bundle. The API is always
10
+ on; the other two are declared in `akan.config.ts` as **`web: true | false | { csr: boolean }`** and can be
11
+ narrowed again per deployment.
12
+
13
+ ```ts
14
+ const config: AppConfig = { web: { csr: false } }; // web without the mobile bundle
15
+ const config: AppConfig = { web: false }; // api only
16
+ ```
17
+
18
+ - **`web: { csr: false }`** drops the CSR build phase and the `/__csr` + `?csr=true` routes. The CSR bundle is
19
+ what the Capacitor mobile build ships, so a web-only deployment never needs it — and an app that declares a
20
+ `mobile` section is refused, because `akan build-ios` copies `dist/apps/<app>/csr/<target>.html` into the
21
+ native project.
22
+ - **`web: false`** is an API-only build: no base artifact, no pages or client bundles, no RSC worker
23
+ entrypoint, and no `public/` in the image (the web router's catch-all is its only reader). Nothing under
24
+ `page/` is served, including routes a lib contributed through `syncPageLibs`.
25
+ - **There is no CSR-without-SSR option, by type.** The CSR bundle inlines the stylesheet the SSR base artifact
26
+ compiles, so it would ship an unstyled app — the object form therefore carries only `csr`, and SSR goes off
27
+ only through the whole-surface `false`.
28
+ - **At runtime, `AKAN_SSR` and `AKAN_CSR` narrow further and never widen** — `false` or `0` turns one off, and
29
+ `AKAN_SSR=false` takes CSR with it for the same reason the option has no such pair. A surface the build left
30
+ out cannot be switched back on, and the boot log names what the process ended up serving. The generated
31
+ Dockerfile writes the build's own answer as the image default.
32
+ - **A build with no web artifact boots the API instead of crashing.** `WebRouter.create` returns `null` when
33
+ `.akan/artifact/base-artifact.json` is absent — an api-only build, or a workspace with no `page/` at all.
34
+ - `akan start` ignores `web` and keeps the whole dev surface: the incremental builder is also the file watcher,
35
+ so switching it off would take server-code HMR with it. It warns once when the config and the dev server
36
+ disagree.
37
+ - The saving is mostly the RSC worker, which is a **separate process per web-serving replica**. Measured on
38
+ `apps/akan` at boot plus one render: 350MB across 3 processes with SSR on, 120MB across 2 with
39
+ `AKAN_SSR=false`, and the built image goes 86MB → 6.2MB when the artifacts are left out of it too.
40
+ - **The generated image installs `ca-certificates` and `tzdata` and nothing else.** It used to carry the whole
41
+ Chromium runtime, ffmpeg, `build-essential`, `python3` and `redis` in every app's image whether or not the app
42
+ reached for any of them. An app that needs one declares it in `docker.preRuns` / `docker.postRuns`, which are
43
+ emitted around the `bun install` — that is the migration for a `puppeteer` or `ffmpeg` app.
44
+
45
+ ## The Process Model — Gateway And Solo
46
+
47
+ A container runs one process per replica, a gateway in front of them when there is more than one, and one RSC
48
+ worker per web-serving replica.
49
+
50
+ - **One traffic replica runs in the container's only process.** `AKAN_REPLICA=0,0,1` — the default, and what
51
+ every environment in `infra/app/values` sets — means there is nothing to balance and nothing to fan pubsub out
52
+ to, so `AkanApp` starts that replica in-process rather than spawning it. Measured on `apps/akan`: 28MB less RSS
53
+ and twice the requests per second, because every request used to cross a unix-socket proxy hop. Declare two or
54
+ more and the gateway is back, spawning and proxying them.
55
+ - **`AKAN_SOLO=false` forces the gateway** for a single replica. Like `AKAN_SSR`, the env only narrows — it
56
+ cannot fold a real gateway's replicas into one process. Passing `replica` to `new AkanApp(...)` also keeps the
57
+ gateway: code that states a topology is asking for the thing that serves it.
58
+ - **`akan start` always runs the gateway**, whatever the replica count. It is also the dev host's builder relay,
59
+ its crash page, and what holds the port across a child restart.
60
+ - **A batch-only replica (`0,1,0`) keeps the gateway too**, because a batch server never listens and the gateway
61
+ is then the only thing bound to answer `/_akan/app/health`.
62
+ - **The RSC worker is never folded in.** It runs under `--conditions react-server`, which resolves the same
63
+ module graph differently, so it cannot share a process with the server that renders client components.
64
+ - **A solo process answers `/_akan/app/health`, `/_akan/app/metrics` and `/_akan/bench/ping` itself**, in the
65
+ gateway's own shape — a `children` array with one entry — so a probe reads one contract either way. It owns the
66
+ rotating log file the gateway would otherwise write, in the same `runtime/logs` directory.
67
+ - **Nothing supervises a solo process but the orchestrator**, since the gateway's crash-restart-with-backoff went
68
+ with it. `infra/app/templates/app.yaml` carries the liveness, readiness and startup probes that replace it.
69
+ - **`main.ts` imports `AkanApp` from `akanjs/server/akanApp`, not the barrel.** The barrel re-exports
70
+ `AkanServer`, whose graph the gateway never runs; through it the process evaluated 35MB of SSR renderer and
71
+ SQLite driver to spawn children and relay bytes. Keep entrypoint imports at the leaf.
72
+
73
+ ## Logging — Records, Request Context And Live Tail
74
+
75
+ Every `Logger` call builds a `LogRecord` before any text exists: `at`, `level`, OTel `sev`, `name`, `message`,
76
+ `pid`, `replicaIdx`, `role` (`gateway` / `all` / `batch` / `rsc-worker`) and — inside a call — `traceId`,
77
+ `endpoint` (`mutation:signScContract`, `internal:cleanupJob`, `page:/org/[orgId]`) and `origin` (`http`,
78
+ `websocket`, `mcp`, `internal`, `page`). The console line is rendered from the record and is byte-identical to
79
+ what it was; a sink reads `entry.record` and renders only if it touches `entry.message`.
80
+
81
+ The level ladder is `trace verbose debug info warn error`. `log` was a seventh tier *below* `info` that
82
+ `AKAN_PUBLIC_LOG_LEVEL=info` silently dropped; the method is kept and emits at `info`, so
83
+ `no-deprecated-log-level.grit` refuses a call to it — write `.info()`. `AKAN_PUBLIC_LOG_LEVEL=log` still boots,
84
+ normalized to `info` with one warning.
85
+
86
+ - **Request context is on by default, production included.** `SignalContext.try` / `SignalContext.run` own the
87
+ `AsyncLocalStorage` scope for the whole call, including the 500 log, so every line of one request shares a
88
+ `traceId`. Internal triggers get `internal:<key>`, the RSC worker gets `page:<route>`, MCP calls are the
89
+ endpoint's own type with `origin: "mcp"`. `AKAN_LOG_CONTEXT=0` is the escape hatch; `AKAN_TRACE=1` is a
90
+ different switch that adds span and query aggregation. Measured cost: ~25ns per call.
91
+ - **Not everything carries context, by design.** The primitive query fast path (an unauthenticated GET of a
92
+ primitive with no args, guards or middlewares) skips it; the schedule adaptor's own `started/finished/error`
93
+ lines wrap the traced handler from outside; gateway-internal lines have none. `akan logs --endpoint` says so.
94
+ - **`Logger.addSink(sink, { minLevel })` — give a sink its floor.** A sink with none follows `AKAN_LOG_FILE_LEVEL`
95
+ (default `trace`), which is why a registered sink used to make every `verbose` call render (1,294ns against an
96
+ 11ns reject). The IPC forwarder and any sink that never reads the text pass a floor and cost a record literal.
97
+ - **The hub lives with whoever owns the surface**: the gateway when there is one, the replica itself under solo
98
+ (`AKAN_REPLICA=0,0,1`), the same rule as the rotating log file and `/_akan/app/*`. It keeps a ring
99
+ (`AKAN_LOG_BUFFER` records / `AKAN_LOG_BUFFER_MB`, default 2000 / 4), suppresses a line repeating more than 20
100
+ times a second into one counted line, and serves `<runtimeDir>/akan-control.sock` (`0600`, NDJSON). A child
101
+ forwards records only while a subscriber wants that level (`log.level` IPC), so an unwatched process sends
102
+ nothing; `AKAN_LOG_STREAM=1` keeps it on. The RSC worker forwards to its replica the same way.
103
+ - **`akan logs <app>`** is the client: `--level --grep --endpoint --trace --child --role --origin --since`
104
+ AND together, comma lists OR, `*` is the only glob; `--replay N` first, `--follow false` for history only,
105
+ `--json` for NDJSON, `--runtime-dir` for a built app. Inside `akan console`, `.tail level=warn grep=payment`,
106
+ `.tail off`, `.trace <id>` — the console is its own `listen: false` process, so these attach to the running
107
+ server's socket rather than reading their own logs.
108
+ - **The generated Dockerfile sets `AKAN_LOG_TO_FILE=0`.** A container's writable layer is ephemeral and nothing
109
+ collects a file from it; stdout is the collection path. A deployment that wants the files back sets
110
+ `AKAN_LOG_TO_FILE=1`.
111
+ - **Never log per delivered record in anything that delivers records.** The control socket and the hub do not,
112
+ and a subscriber asking for everything is the test.
113
+
114
+ ## The Generated Image — `docker` In `akan.config.ts`
115
+
116
+ **`docker` is `string | { image, preRuns, postRuns, command }`** — a whole Dockerfile, or the parts Akan
117
+ assembles one from. There is no `content` field; the string *is* the content.
118
+
119
+ ```ts
120
+ const config: AppConfig = {
121
+ docker: { preRuns: ["apt-get update && apt-get install -y --no-install-recommends ffmpeg"] },
122
+ };
123
+ const config: AppConfig = { docker: "FROM oven/bun:1-slim\n…" }; // verbatim, nothing merged in
124
+ ```
125
+
126
+ - `image` and each run entry take `string | { amd64?, arm64? }`; the object form compiles to a
127
+ `RUN if [ "$TARGETARCH" = "<arch>" ]` guard, so a multi-arch build runs it on one leg only. `preRuns` land
128
+ before `bun install --production` (where a native dependency's build tools have to be), `postRuns` after it
129
+ and before the app files are copied.
130
+ - **A lib declares the steps its own runtime needs**, and every app that mounts it inherits them:
131
+ `libs/<lib>/akan.config.ts` takes `docker: { preRuns, postRuns }` and nothing else — the base image and the
132
+ command belong to the app. Lib steps are emitted before the app's own, and an identical step declared twice
133
+ becomes one layer.
134
+ - **The string form takes no contributions.** A Dockerfile handed over whole is used exactly as written, so a
135
+ lib's `preRuns` are dropped rather than spliced into a file Akan does not own. An app that needs both writes
136
+ the parts instead.
137
+ - Like `externalLibs`, lib steps are collected from **every lib in the workspace**, not just this app's
138
+ dependency closure — narrowing that set needs the dependency scan, and this config is re-read on every file
139
+ change in dev. Keep a lib's steps to what its runtime genuinely requires.
140
+ - `AkanAppConfig.docker` is the resolved declaration; `AkanAppConfig.dockerfile` is the text `akan build` writes
141
+ to `dist/apps/<app>/Dockerfile`.
142
+
143
+ ## Shipped Assets — `assets` In `akan.config.ts`
144
+
145
+ `akan build` copies the whole `public/` tree into `dist`, lib assets dereferenced, and that copy is the image.
146
+ **`assets: { pruneFonts, keepFonts }`** trims the fonts out of it that nothing reads. **Source trees are never
147
+ touched** — an app's and a lib's own `public/` keep every file, because one shared font folder is picked over
148
+ differently by every app that mounts it and by other repos.
149
+
150
+ ```ts
151
+ const config: LibConfig = { assets: { keepFonts: ["fonts/Assistant-*.woff2"] } }; // libs/<lib>/akan.config.ts
152
+ const config: AppConfig = { assets: { pruneFonts: false } }; // emergency valve, not the normal escape hatch
153
+ ```
154
+
155
+ - **A font with `optimize` on is a build input, not a runtime asset.** `FontOptimizer` subsets it into
156
+ `.akan/artifact/fonts` and the emitted CSS points at `/_akan/fonts/*`, which `WebRouter` serves from the
157
+ artifact — so the source in `public/` is never opened again. That is the bulk of what this drops, and it
158
+ applies to the fonts the app *does* use, not only the ones it does not.
159
+ - **The keep-set is derived per app, per build; there is no list of fonts to drop.** An exclude list would rot
160
+ the moment another app or repo started using one of them, and nobody would know to update it. A font survives
161
+ because ① its font declares `optimize: false`, so `FontCss.getRuntimeCss` emits the raw src; ② something
162
+ references its filename — anything under `public/`, the compiled stylesheet, a client/server bundle, the CSR
163
+ shell; or ③ a `keepFonts` glob names it.
164
+ - **`keepFonts` belongs to the `akan.config.ts` that owns the font**, written against that lib's or app's own
165
+ `public/` (`"fonts/X.woff2"`, resolved to `libs/<lib>/fonts/X.woff2`). A lib shared across repos carries the
166
+ reason its own CSS needs a font, instead of every mounting app rediscovering it in production.
167
+ - **A reference from a build bundle is ignored for an already-subset font.** Every route file's `fonts`
168
+ declaration is inlined into the pages bundle, the client chunks and the CSR shell, so a declared source is in
169
+ all three whether or not anything loads it. `public/` and the compiled CSS are read as authoritative.
170
+ - **A generated manifest that lists every public asset keeps every font it names** — a service-worker precache
171
+ file is the usual one. That is a real reference, not a false positive, so the build reports it at `info`:
172
+ read the `[font-prune] kept …` line before concluding the prune does nothing.
173
+ - Matching is by filename, including the percent-encoded spelling, so a `url()` survives however it is written.
174
+ - The phase is `assets`, between `csr` and `compress`. An api-only build (`web: false`) copies no `public/` and
175
+ the phase finds nothing to do.
176
+
@@ -0,0 +1,31 @@
1
+ {
2
+ "title": "Transport Rule",
3
+ "description": "How a call reaches an Akan signal — guards across HTTP and websocket, socket identity and cleanup, binary pubsub frames, and mutation HTTP verbs",
4
+ "group": "core",
5
+ "codegenPriority": 4,
6
+ "deprecated": false,
7
+ "scans": [
8
+ {
9
+ "type": "example",
10
+ "description": "Guard classes and their scope markers",
11
+ "path": "{apps,libs}/*/srvkit/guards.ts",
12
+ "sample": 2
13
+ },
14
+ {
15
+ "type": "example",
16
+ "description": "Signal endpoints declaring guards, pubsub, and message handlers",
17
+ "path": "{apps,libs}/*/lib/*/*.signal.ts",
18
+ "sample": 4
19
+ }
20
+ ],
21
+ "update": {
22
+ "filePath": "./transportRule.instruction.md",
23
+ "contents": ["guards across transports", "signal body types", "binary pubsub", "mutation HTTP verbs"],
24
+ "rules": [
25
+ "Use current Akan APIs only",
26
+ "State the rule and the reason it exists, never restate a type signature",
27
+ "Say what leaks or breaks when a rule is ignored",
28
+ "Keep it workspace-neutral — no app or repo names"
29
+ ]
30
+ }
31
+ }
@@ -0,0 +1,60 @@
1
+ # Transport Rule — Guards, Sockets, Binary Frames, HTTP Verbs
2
+
3
+ How a call reaches a signal: what a guard may read on either transport, how a websocket carries its credential
4
+ and its per-connection state, when a pubsub payload leaves JSON for a binary frame, and which HTTP verb a
5
+ mutation is mounted under. `conventions` carries the invariants — this is the full contract behind them.
6
+
7
+ ## Guards And Transports
8
+
9
+ - Guards run on both HTTP and websocket calls. Read the caller with `context.get<T>("account")` (`pkgs/akanjs/signal/signalContext.ts`) instead of branching on `getHttpContext()` / `getWebSocketContext()`.
10
+ - Slice-level `guards` only reach the generated query/mutation endpoints. A `pubsub`/`message` endpoint is unguarded unless it declares its own `guards` in its signal option.
11
+ - A pubsub room is authorized once, at subscribe. When a socket's credential changes the framework re-runs each room's guards and unsubscribes the ones that now fail (`SignalResolver.revalidateWsRooms`), so guards must stay side-effect free and safe to re-run.
12
+ - A websocket carries its credential in the handshake snapshot on `ws.data` (`AppWsData`); clients that hold the token in memory send it with `fetch.setJwt(...)`, which forwards an auth frame over the socket.
13
+ - **Never read the caller's IP off the socket or the request peer — take `.with(Ip)`.** Whenever a federation
14
+ gateway is in front, `ws.remoteAddress` and the child's own peer are the *gateway* (`127.0.0.1`) for every
15
+ caller and for the whole life of every socket. `Ip` reads what a proxy recorded (`x-real-ip`, then the first
16
+ `x-forwarded-for` entry) and falls back to the peer only when nothing proxied the call — which is the answer
17
+ for a solo process behind an ingress that sets neither; `context.getClientIp()` is the same answer inside a
18
+ guard or middleware. It arrives unwrapped from
19
+ its `::ffff:` IPv4-mapped form, so it can address a `udp4` socket as well as identify a caller, and it is
20
+ `null` rather than a placeholder when genuinely unknown — a loopback-looking address for an unknown caller
21
+ is the failure this replaced. The gateway also forwards the client port (`x-forwarded-port`, read with
22
+ `context.getClientPort()`) along with host and protocol.
23
+ - **Every socket carries a `socketId`, and only the framework mints one.** `AppWsData` assigns it at the handshake, and a `message` / `pubsub` handler reads it off the `Ws` internal arg — `.with(Ws)` hands `{ ws, socketId, subscribe, on, off }`. Never mint your own from `ws.data`: the room bookkeeping keys on the framework's id, and a second one fails to match it silently. It identifies a **connection**, not a caller — a reconnect gets a new id and the federation gateway's own socket is a different one — so per-user state keys on the account, never on this.
24
+
25
+ - **A cleanup registered with `ws.on("disconnect" | "unsubscribe", fn)` is scoped to the call that registered it.**
26
+ From a `pubsub` subscribe it belongs to that room — `unsubscribe` runs when the client leaves it or a credential
27
+ change revokes it, `disconnect` when the socket closes while still subscribed — so a room already left runs
28
+ neither again, and cleanup that must happen either way registers for both. From a `message` handler it belongs to
29
+ the socket and runs at close. A handler that throws is logged and never blocks the rest of the teardown.
30
+
31
+ ## Binary Pubsub
32
+
33
+ - **`pubsub(Binary)` sends its payload in a websocket binary frame**, skipping the JSON `{ type: "pub" }`
34
+ envelope and the base64 a JSON wire would need. The client's subscribe callback receives a `Uint8Array`.
35
+ Nothing else is declared: the return type is the whole switch, and text and binary frames coexist on one
36
+ socket, so every JSON endpoint is untouched. The optimization applies only when the **whole** return is
37
+ `Binary` — `[Binary]`, or a `Binary` inside a model, falls back to base64.
38
+ - **A declared `Binary` room coalesces under backpressure**: while a socket cannot keep up the room keeps only
39
+ its newest frame and drops the rest, which is what a telemetry or video stream wants. Declare
40
+ `pubsub(Binary, { backpressure: "queue" })` when the frames are a sequence a subscriber must see in full, such
41
+ as deltas against a base it already holds; the send buffer then grows with the slowest subscriber. Coalescing
42
+ is keyed by the endpoint, not carried with the frame, so this process, an IPC deliver and a Redis fan-out all
43
+ reach the same answer. Coalesced frames are counted in `pubsubCoalesceCount` on the replica's metrics report.
44
+ - **A `pubsub(Any)` that happens to carry bytes is framed too, and warns once.** `Any` passes a `Buffer` through
45
+ untouched, so the transport sees bytes and sends them whole rather than as a number array — an undeclared
46
+ publisher is fixed rather than left corrupt. It is still not the contract: it queues rather than coalescing
47
+ (nobody declared lossy delivery for that room), and a Redis fan-out still JSON-stringifies it. Declare
48
+ `Binary`.
49
+ - The federation gateway relays a binary frame unchanged, and Bun IPC carries bytes as bytes, so the only hop
50
+ that needed teaching was the socket itself. A cross-server (Redis) fan-out encodes it as protobuf `bytes`.
51
+
52
+ ## Mutation HTTP Verb
53
+
54
+ - A `mutation` is `POST`. `{ method: "PATCH" | "PUT" | "DELETE" }` moves it, and one path may carry several verbs
55
+ — a `query` GET and a `mutation` POST on the same custom `path` are mounted side by side. Two endpoints claiming
56
+ the same path **and** verb fail the boot rather than silently shadowing one another.
57
+ - Reach for it only when a foreign wire protocol forces the verb (a client you cannot change that sends
58
+ `PATCH /rest/v1/<table>`). Akan's own `fetch.*` client, the OpenAPI document, and the API explorer all follow
59
+ whatever is declared, so nothing needs restating per caller.
60
+
@@ -0,0 +1,24 @@
1
+ {
2
+ "title": "Workspace Onboarding",
3
+ "description": "Product-workspace orientation — layout, decision matrix, workflow recipes, and query gotchas",
4
+ "group": "core",
5
+ "codegenPriority": 2,
6
+ "deprecated": false,
7
+ "scans": [],
8
+ "update": {
9
+ "filePath": "./workspaceOnboarding.instruction.md",
10
+ "contents": [
11
+ "workspace layout and generated files",
12
+ "agent workflow and common commands",
13
+ "where-do-I-put-this decision matrix",
14
+ "workflow recipes",
15
+ "modeling and query gotchas"
16
+ ],
17
+ "rules": [
18
+ "Use current Akan APIs only",
19
+ "Write for someone building a product on Akan, not for someone changing Akan",
20
+ "Refer to the app as <%= appName %>; the renderer substitutes the workspace's first app",
21
+ "Do not repeat a rule that the conventions guideline already states"
22
+ ]
23
+ }
24
+ }
@@ -0,0 +1,263 @@
1
+ ## Workspace Layout
2
+
3
+ - `apps/<app>` contains application pages, app UI, app domain modules, env files, and `akan.config.ts`.
4
+ - `libs/<lib>` contains shared domain and utility code reused by apps.
5
+ - `apps/<app>/lib/<model>` holds database-backed domain modules, `lib/_<service>` service modules, and
6
+ `lib/__scalar/<scalar>` reusable value types. Module abstracts sit beside the code as `<name>.abstract.md`.
7
+ - `apps/<app>/page` holds server-side file-routed pages: `<routeName>.tsx` serves `/routeName`, a directory's
8
+ `_index.tsx` serves that directory, `_layout.tsx` nests a layout, and `[modelId]` is a dynamic segment.
9
+
10
+ The file roles inside a module — which file owns persistence, business logic, state, and each UI shape — are in
11
+ the convention set above under **Domain Module Conventions**.
12
+
13
+ ## Agent Workflow
14
+
15
+ 1. Read the nearby module and convention before creating files. If `*.abstract.md` exists, read it first.
16
+ 2. Put new files in the established Akan location instead of adding parallel architecture.
17
+ 3. Prefer Akan MCP workflows before direct source edits. Start with `akan mcp --mode plan` for `list_workflows`, `explain_workflow`, and `plan_workflow`.
18
+ 4. If `plan_workflow` returns `planPath` or `next.tool=apply_workflow`, call `apply_workflow({ planPath })` before direct source edits.
19
+ 5. Use `akan mcp --mode apply` only for allowlisted `apply_workflow`, `run_validation`, and repair tools.
20
+ 6. After `apply_workflow`, run `run_validation` with `validationTarget` when present; otherwise use `applyReportPath`.
21
+ 7. Direct source edits are denied when an allowlisted Akan workflow or repair tool can perform the change.
22
+ 8. If no workflow exists, or apply reports unsupported/no-op/failed diagnostics that require manual action, edit only the owning source files and never patch generated files directly.
23
+ 9. Treat `AKAN_PUBLIC_*` env vars as public. Never put secrets in them.
24
+ 10. Add or update tests when behavior, contracts, or CLI output changes.
25
+ 11. Update `*.abstract.md` when business invariants, workflows, or public behavior change.
26
+ 12. Run the smallest relevant verification command after changes. After touching any `.tsx`, that includes
27
+ `akan quality ssr`.
28
+
29
+ The rules for where code goes, the client/server boundary, and SSR discipline are in the convention set above;
30
+ this section covers only the order of operations.
31
+
32
+ ## Common Commands
33
+
34
+ Run commands from the workspace root unless a task says otherwise.
35
+
36
+ ### Module Addition Workflow
37
+
38
+ When adding a new database-backed domain module (e.g., product, user):
39
+
40
+ ```bash
41
+ # 1. Scaffold the module with Akan CLI (creates constant, service, signal, store, document files)
42
+ # The target app/lib is a POSITIONAL argument, not a --app flag.
43
+ akan create-module <module-name> <%= appName %>
44
+
45
+ # 2. Start dev server with HMR and type checking at http://localhost:8282
46
+ akan start <%= appName %>
47
+ ```
48
+
49
+ ### Change Verification Workflow
50
+
51
+ After any code change, run these in order:
52
+
53
+ ```bash
54
+ # 1. Fast lint check — Akan.js conventions and Biome rules
55
+ akan lint <%= appName %>
56
+
57
+ # 2. Type-only check — catches server/client boundary violations and import errors
58
+ akan typecheck <%= appName %>
59
+
60
+ # 3. Test — Run the test code (lib/*/*.signal.test.ts or others)
61
+ akan test <%= appName %>
62
+
63
+ # 4. SSR balance — only after touching .tsx files. Reports the server render share
64
+ # per app/lib and flags client code that should render on the server.
65
+ akan quality ssr
66
+
67
+ # 5. Full production build — bundles the app, runs all type/lint checks combined
68
+ akan build <%= appName %>
69
+ ```
70
+
71
+ **Verify endpoints with signal tests, not raw HTTP.** The canonical way to check a query/mutation/slice
72
+ contract is an in-memory signal test (`<model>.signal.test.ts`), using the test fetch harness
73
+ (`getOrSetupSignalTestFetch`) — it is fast, needs no running server, and exercises `fetch.*`, `view/edit/merge<Model>`,
74
+ and slice `init`/`list`/`insight` directly. Prefer it over `curl`: the dev gateway locale-prefixes routes (`/en/...`),
75
+ so hand-rolled HTTP calls against a raw path can redirect unexpectedly. See `akan test <%= appName %>`.
76
+
77
+ ### Other Frequently Used Commands
78
+
79
+ ```bash
80
+ akan create-scalar <scalar-name> <%= appName %> # Add a scalar module (lib/__scalar/<scalar-name>/)
81
+ akan create-service <service-name> <%= appName %> # Add a service module (lib/_<service-name>/)
82
+ akan test <%= appName %> # Run the test code (lib/*/*.signal.test.ts or others)
83
+ akan lint <%= appName %> # Lint only (no typecheck)
84
+ akan quality scan # All code-quality warnings + SSR balance
85
+ akan quality ssr # SSR balance and client-boundary warnings only
86
+ ```
87
+
88
+ **CLI argument conventions.** Two argument styles, and mixing them up is a common mistake:
89
+
90
+ - Scaffolding and whole-app commands take the target app/lib as a **positional** argument, not a flag:
91
+
92
+ ```bash
93
+ akan create-module photo <%= appName %>
94
+ akan create-scalar money <%= appName %>
95
+ akan create-service billing <%= appName %>
96
+ akan sync <%= appName %>
97
+ ```
98
+
99
+ - Only the source-limited field commands use `--app`/`--module` flags:
100
+
101
+ ```bash
102
+ akan add-field --app <%= appName %> --module photo --field width --type Int
103
+ akan add-enum-field --app <%= appName %> --module photo --field status --values draft,active
104
+ ```
105
+
106
+ Passing `--app` to `create-module` is not recognized, and the target app will not resolve.
107
+
108
+ For the default generated app, start with:
109
+
110
+ ```bash
111
+ akan start <%= appName %>
112
+ ```
113
+
114
+ ### The Essential Loop: Workflow -> Sync -> Check
115
+
116
+ Almost every Akan.js change follows this pattern. **Missing sync or repair is the #1 cause of agent confusion.**
117
+
118
+ > **If the Akan MCP tools are not connected in your agent, skip straight to the CLI-only fallback below.**
119
+ > `akan mcp --mode plan/apply` starts a stdio MCP server that only works when your agent is wired to it as an
120
+ > MCP client. When those `list_workflows` / `plan_workflow` / `apply_workflow` tools are not available, the CLI
121
+ > commands are a fully supported, first-class path — you are not losing any capability by using them.
122
+
123
+ 1. **Plan** — Ask the Akan MCP server for the workflow first.
124
+ ```
125
+ akan mcp --mode plan
126
+ # use list_workflows, explain_workflow, and plan_workflow
127
+ ```
128
+
129
+ 2. **Apply the plan** — If `plan_workflow` returns `planPath` or `next.tool=apply_workflow`, call
130
+ `apply_workflow({ planPath })`. Do not copy the workflow plan into direct source edits.
131
+ ```
132
+ akan mcp --mode apply
133
+ # use apply_workflow, run_validation, repair_generated, repair_imports, or repair_module_shape
134
+ ```
135
+
136
+ Direct edits are fallback only: use them after `list_workflows`/`explain_workflow` confirm no matching workflow, or
137
+ after apply reports unsupported/no-op/failed diagnostics that require manual action. Keep fallback edits to owning
138
+ source files such as `task.constant.ts`, `task.dictionary.ts`, `Task.Template.tsx`, or `Task.Unit.tsx`.
139
+
140
+ 3. **Validate the apply report** — Use the apply report artifact, not the original raw plan, when it is available.
141
+ ```
142
+ # run_validation with validationTarget first; otherwise use applyReportPath
143
+ ```
144
+
145
+ 4. **Sync or repair** — Regenerate barrel files so Akan discovers your change. This regenerates:
146
+ `cnst.ts`, `db.ts`, `srv.ts`, `sig.ts`, `st.ts`, `dict.ts`, `useClient.ts`, `useServer.ts`,
147
+ `ui/index.ts`, `webkit/index.ts`, `srvkit/index.ts`, `common/index.ts`, and all module `index.ts` files.
148
+ ```
149
+ akan sync <%= appName %>
150
+ # or: akan repair generated --app <%= appName %>
151
+ ```
152
+ **CRITICAL**: Sync after EVERY file add, delete, or rename. Without sync, other modules cannot
153
+ `import * as cnst from "../cnst"` and find your new model.
154
+
155
+ 5. **Check** — Verify your change compiles and lints.
156
+ ```
157
+ akan start <%= appName %> # dev server with live feedback (preferred)
158
+ akan lint <%= appName %> # quick lint-only check
159
+ akan doctor --strict # structured workspace diagnostics
160
+ ```
161
+
162
+ If `akan sync` gives errors, try:
163
+ - `akan build <%= appName %>` — full rebuild catches type errors sync may miss
164
+ - Re-run `akan create-module <name> <%= appName %>` if the scaffold is corrupted
165
+
166
+ For compound natural-language requests, split the request into workflows and apply each artifact in order. For example,
167
+ "create a project module and add a budget field" should run `create-module` plan/apply first, then `add-field`
168
+ plan/apply, then validation/doctor on the returned `validationTarget`.
169
+
170
+ ### CLI-Only Fallback (MCP Not Connected)
171
+
172
+ When the Akan MCP tools are not loaded, run the CLI commands directly. Each MCP tool maps 1:1 to a CLI command,
173
+ and the CLI emits the same structured report via `--format json`:
174
+
175
+ | MCP tool | CLI-only equivalent |
176
+ |----------|---------------------|
177
+ | `list_workflows` | `akan workflow list` |
178
+ | `explain_workflow <name>` | `akan workflow explain <name>` |
179
+ | `plan_workflow <name> ...` | `akan workflow plan <name> ... --format json --out <planPath>` |
180
+ | `apply_workflow { planPath }` | `akan workflow apply <planPath> --format json` (add `--dry-run` to preview) |
181
+ | `run_validation { validationTarget }` | `akan doctor --strict --format json` (or `akan typecheck <%= appName %>`) |
182
+ | `repair_generated` / `repair_imports` / `repair_module_shape` | `akan repair generated\|imports\|module-shape --app <%= appName %> --format json` |
183
+
184
+ The scaffolding primitives (`akan create-module`, `akan create-scalar`, `akan create-service`, `akan add-field`,
185
+ `akan add-enum-field`) are the same primitives the workflows call, so `create-module <name> <%= appName %>` followed
186
+ by `akan sync <%= appName %>` is equivalent to running the `create-module` workflow. Direct source edits remain the
187
+ final fallback when no CLI command covers the change.
188
+
189
+ ## Quick Decision Matrix — "Where do I put this code?"
190
+
191
+ | You want to... | Create in... | Run after... |
192
+ |----------------|-------------|--------------|
193
+ | Define a new database-backed noun (e.g., User, Product) | `lib/<model>/` → constant, document, service, signal, store, dictionary, abstract | `akan sync <name>` |
194
+ | Add a pure workflow / integration (e.g., Payment, Email) | `lib/_<service>/` → service, signal, store, dictionary, abstract | `akan sync <name>` |
195
+ | Add a reusable value type (e.g., Address, WorkHistory) | `lib/__scalar/<type>/` → constant, dictionary, abstract | `akan sync <name>` |
196
+ | Create a new URL-visitable page | `page/` → `_index.tsx`, `_layout.tsx`, `[param]/_index.tsx` | Rebuild (akan start auto-detects) |
197
+ | Change the app color theme / design tokens | `apps/<app>/page/styles.css` → override the semantic token values under `:root, [data-theme="dark"]` and `[data-theme="light"]` (`--primary`, `--background`, `--foreground`, …) | akan start hot-reloads |
198
+ | Add a form or reusable UI component | `ui/` → PascalCase `.tsx`, **no** `"use client"` unless it uses a hook, event handler, store, or browser API | `akan sync <name>` |
199
+ | Add a React hook or browser helper | `webkit/` → camelCase `.ts` with `"use client"` | `akan sync <name>` |
200
+ | Add a server-only guard, middleware, or adaptor | `srvkit/` → PascalCase `.ts` | `akan sync <name>` |
201
+ | Add a pure helper (no DOM, no server API) | `common/` → camelCase `.ts` | `akan sync <name>` |
202
+
203
+ ## Workflow Recipes
204
+
205
+ Six worked recipes carry the full code for the most frequent Akan changes, together with the reference of what
206
+ `akan sync` generates at each layer. They are **not** inlined here — fetch them when you are about to do one:
207
+ `get_guideline` with `workspaceRecipes`, or `akan guideline show workspaceRecipes`.
208
+
209
+ | Recipe | Covers |
210
+ |---|---|
211
+ | 1. Adding a field to a model | the five `constant.ts` classes, the dictionary entry, the store form, `akan sync` |
212
+ | 2. Injecting a dependency into a service | `service<srv.XService>()`, `plug(Adapter)`, `env(...)`, `use<T>()` |
213
+ | 3. Creating and using a slice | `slice()` guards, the generated `init<Model><Suffix>`, `Load.Units` hydration |
214
+ | 4. Creating a mutation endpoint | the document chain method, the service call, the signal `guards`, the store action |
215
+ | 5. Internal triggers | `internal()` with interval and cron |
216
+ | 6. Creating an insight | the `XInsight` class, the aggregation, the dashboard tile |
217
+ | Data Flow Summary | how a value travels page → store → signal → service → document → DB |
218
+ | Auto-Generated API Reference | what signal, service, store, and document each generate — never hand-write these |
219
+
220
+ The rules those recipes obey are in this file; the recipes only show the code. When you edit a file, read the
221
+ existing content first and change only the relevant sections — never rewrite a whole file.
222
+
223
+ ## Modeling & Query Gotchas
224
+
225
+ A short list of things the type system does not always catch. The full rules for text search, `cascade`, and the
226
+ query-level writes are in the convention set above; these are the shapes that build green and fail later.
227
+
228
+ - **Slices return a query, not a list.** A slice `exec` must return `this.<model>Service.query<Filter>(...)`, never
229
+ a `list<Filter>(...)` / `listBy...(...)` array. Returning an array type-checks but fails at runtime with
230
+ `Unknown document field path: 0`. (See Recipe 3.)
231
+ - **Custom endpoint names must not collide with generated CRUD.** `create/update/remove/view/edit/merge<Model>`
232
+ already exist. A collision can build green and fail only at runtime — pick a distinct verb.
233
+ - **Numbers are `Int` or `Float`, never `Number`.** `field(Number)` / `.body("x", Number)` fail to typecheck.
234
+ - **Array fields use `field([T])`** — `tags: field([String])`, `images: field([File])`.
235
+ - **Reading a secret field needs an explicit select.** `field(...).secret()` values are stripped from query results
236
+ by default: `this.userModel.pickById(id, { select: { passwordHash: true } })`.
237
+ - **`cascade` names a direction, and the wrong one is a data loss** — `removeRef` on the relation the owner holds,
238
+ `removeWith` on the child's reference to its owner. A query-level removal fires no hooks and therefore no cascade.
239
+ - **`q.search()` is a filter node, not a slice requirement.** `bySearch: filter().arg("text", String).query((text,
240
+ q) => q.search(text, { prefix: true }))` generates `listBySearch` / `countBySearch` / `queryBySearch` /
241
+ `insightBySearch` for free. Only add a search *slice* when the model's data is safe to enumerate.
242
+
243
+ ## Current User, Guards & Auth-Gated Pages
244
+
245
+ Built-in user authentication (session / JWT / password hashing) ships as a separate Akan auth library, not in the
246
+ core framework. The core framework gives you the composition points below; wire the auth library through them.
247
+
248
+ - **A `slice()` takes its guard map as a second argument**, `slice(srv.task, { guards: { root: Admin, get: SignedIn,
249
+ cru: SignedIn } }, (init) => ({...}))`. An `endpoint()` takes no guard argument at all —
250
+ `endpoint(srv.task, ({ mutation }) => ({...}))` — so **every custom `mutation` / `query` / `message` names its own
251
+ `guards: [...]` array**: `mutation(cnst.Task, { guards: [SignedIn] })`. `Public` always allows; other guards
252
+ implement the `Guard` interface in `srvkit/` (server-only).
253
+ - **Read the caller inside a guard** with `context.get<T>("account")`, which works on HTTP and websocket calls
254
+ alike — never branch on `getHttpContext()` / `getWebSocketContext()`. Every guard class also declares
255
+ `static scope: GuardScope`, `"account"` or `"resource"`, with no default.
256
+ - **Read the current user inside a custom endpoint** by injecting an `InternalArg` with `.with(...)`:
257
+ `mutation(cnst.Task, { guards: [SignedIn] }).with(CurrentUserId).exec(async function (currentUserId) { ... })`.
258
+ - **Auto-generated CRUD and `serve()` service methods / lifecycle hooks do not receive session context.** If an
259
+ operation needs the acting user, expose a custom endpoint that takes it via `.with(CurrentUserId)` — never trust a
260
+ client-supplied user id.
261
+ - **SSR auth-gated pages: guard at the layout.** Check the session in the `_layout.tsx` loader and redirect when it is
262
+ absent, so nested pages never render for signed-out users.
263
+
@@ -0,0 +1,33 @@
1
+ {
2
+ "title": "Workspace Recipes",
3
+ "description": "Worked step-by-step recipes for common Akan changes, plus the auto-generated signal/service/store/document API reference",
4
+ "group": "core",
5
+ "codegenPriority": 5,
6
+ "deprecated": false,
7
+ "scans": [
8
+ {
9
+ "type": "example",
10
+ "description": "Module files a recipe walks through",
11
+ "path": "{apps,libs}/*/lib/*/*.{constant,document,service,signal,store}.ts",
12
+ "sample": 5
13
+ }
14
+ ],
15
+ "update": {
16
+ "filePath": "./workspaceRecipes.instruction.md",
17
+ "contents": [
18
+ "add a field",
19
+ "inject a dependency",
20
+ "create a slice",
21
+ "create a mutation",
22
+ "internal triggers",
23
+ "insights",
24
+ "auto-generated API reference"
25
+ ],
26
+ "rules": [
27
+ "Use current Akan APIs only",
28
+ "Every recipe names the files to edit and the order to edit them in",
29
+ "Keep the <%= appName %> placeholder so the rendered guide names this workspace's app",
30
+ "Never duplicate a rule that workspaceOnboarding already states — link to it instead"
31
+ ]
32
+ }
33
+ }