@akanjs/cli 3.0.0-alpha.4 → 3.0.0-alpha.41

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 (108) hide show
  1. package/.build-stamp +1 -1
  2. package/README.ko.md +1 -1
  3. package/README.md +1 -1
  4. package/{abstractCompactor-rk6twzvj.js → abstractCompactor-20e0cwbs.js} +3 -3
  5. package/{agent.command-jfz8yfwy.js → agent.command-e835fjem.js} +9 -8
  6. package/{application.command-mxyqtd6p.js → application.command-sjtbk6tr.js} +7 -6
  7. package/{applicationBuildRunner-cwk52sb7.js → applicationBuildRunner-7gnfz7ww.js} +7 -8
  8. package/{applicationReleasePackager-yrrhbkv3.js → applicationReleasePackager-93nzdfsy.js} +2 -2
  9. package/buildBatch.proc.js +6 -5
  10. package/{capacitorApp-j357ak1g.js → capacitorApp-qv7g3dp2.js} +29 -28
  11. package/{cloud.command-f87dwe52.js → cloud.command-y0e4eyqz.js} +11 -10
  12. package/{context.command-z5bf8bn7.js → context.command-vn87jb08.js} +17 -16
  13. package/{dependencyScanner-f6grrep3.js → dependencyScanner-rbs2407b.js} +1 -1
  14. package/getDirname-ka11hm7e.js +8 -0
  15. package/{guideline.command-1m1ctar3.js → guideline.command-s4hn2bsy.js} +7 -6
  16. package/guidelines/conventions/conventions.generate.json +25 -0
  17. package/guidelines/conventions/conventions.instruction.md +1193 -0
  18. package/guidelines/cssRule/cssRule.instruction.md +7 -0
  19. package/guidelines/framework/framework.instruction.md +1 -0
  20. package/guidelines/ssrRule/ssrRule.generate.json +31 -0
  21. package/guidelines/ssrRule/ssrRule.instruction.md +108 -0
  22. package/guidelines/workspaceOnboarding/workspaceOnboarding.generate.json +24 -0
  23. package/guidelines/workspaceOnboarding/workspaceOnboarding.instruction.md +972 -0
  24. package/incrementalBuilder.proc.js +7 -6
  25. package/{index-1s71fwrq.js → index-0h212yqe.js} +13 -13
  26. package/index-1577bej2.js +7 -0
  27. package/{index-e3qskyt2.js → index-33zce8gt.js} +8 -4
  28. package/{index-csqk9z5j.js → index-3dp7x6rc.js} +3 -3
  29. package/{index-cn24dcqn.js → index-73rnbwgg.js} +15 -7
  30. package/{index-tq1y56tp.js → index-8d2sv6pw.js} +39 -16
  31. package/{index-jtm86byd.js → index-8vkkm7v0.js} +125 -66
  32. package/{index-r4w8cry5.js → index-9jsnb4vw.js} +4 -4
  33. package/{index-ybybkec0.js → index-becx1t0k.js} +6 -6
  34. package/{index-4yp06pn1.js → index-g9e2fhw2.js} +49 -32
  35. package/{index-cap2k2ch.js → index-hg0qvhmx.js} +152 -241
  36. package/{index-xwgm94zr.js → index-j37qq1f2.js} +1 -1
  37. package/{index-n0z3f2xg.js → index-k6fvbcj1.js} +37 -6
  38. package/{index-6pz1j0zj.js → index-mxvakhsm.js} +6 -2
  39. package/{index-vhv3xnjz.js → index-mykmf6wd.js} +8 -8
  40. package/{index-y0k42k04.js → index-mz5jtaq7.js} +1 -1
  41. package/{index-jk9q6pse.js → index-n706txcv.js} +5 -3
  42. package/{index-hjzr8ja1.js → index-qc0tnhs2.js} +1 -1
  43. package/{index-rh16j3c7.js → index-qnv0aqmm.js} +12 -8
  44. package/{index-3pgtw35d.js → index-sh01r3pm.js} +2 -2
  45. package/{index-xj8bks3p.js → index-t71zvmt9.js} +9 -11
  46. package/{index-4zgs8g1w.js → index-tsmjxph2.js} +1 -1
  47. package/{index-fmgbwsr5.js → index-x33gnap3.js} +4 -4
  48. package/{index-b978vs2e.js → index-xyrb7pk5.js} +3 -3
  49. package/index.js +21 -20
  50. package/{library.command-pwfx8dx2.js → library.command-v5cyaw9t.js} +6 -5
  51. package/{localRegistry.command-r4zjx5py.js → localRegistry.command-53t758pt.js} +10 -9
  52. package/{module.command-0t3ewata.js → module.command-pq6w84xg.js} +10 -9
  53. package/{package.command-7hmkxvpa.js → package.command-qkcs60ts.js} +6 -5
  54. package/package.json +3 -4
  55. package/{page.command-st6r4s0q.js → page.command-qj5bfnh9.js} +6 -5
  56. package/{primitive.command-1qkxpzsn.js → primitive.command-bw0nes6x.js} +11 -10
  57. package/{quality.command-hy5vt1bh.js → quality.command-g1s2wt8b.js} +532 -87
  58. package/{recipeScanner-tghkzrrt.js → recipeScanner-zrzev25x.js} +3 -3
  59. package/{repair.command-nakkq2zp.js → repair.command-mzwtejpk.js} +8 -7
  60. package/{routeSourceValidator-cncd92fv.js → routeSourceValidator-7fycdqyv.js} +3 -39
  61. package/{scalar.command-fjq272gs.js → scalar.command-rm6k3c65.js} +9 -8
  62. package/templates/app/env/env.server.debug.ts.template +1 -5
  63. package/templates/app/env/env.server.develop.ts.template +1 -5
  64. package/templates/app/env/env.server.local.ts.template +1 -5
  65. package/templates/app/env/env.server.main.ts.template +1 -5
  66. package/templates/app/page/styles.css.template +3 -3
  67. package/templates/appSample/srvkit/AuthGuard.ts +9 -5
  68. package/templates/appSample/srvkit/SessionInternalArg.ts +4 -3
  69. package/templates/crudPages/[__model__Id]/edit/page.tsx +1 -1
  70. package/templates/crudPages/new/page.tsx +1 -1
  71. package/templates/crudSinglePage/page.tsx +1 -1
  72. package/templates/env/_env.server.type.ts +7 -5
  73. package/templates/lib/db.ts +4 -2
  74. package/templates/lib/dict.ts +1 -1
  75. package/templates/lib/sig.ts +1 -1
  76. package/templates/lib/srv.ts +6 -2
  77. package/templates/lib/useClient.ts +9 -3
  78. package/templates/module/__Model__.Util.tsx +1 -1
  79. package/templates/module/__Model__.Zone.tsx +3 -5
  80. package/templates/workspaceRoot/AGENTS.md.template +0 -831
  81. package/templates/workspaceRoot/CLAUDE.md.template +19 -0
  82. package/templates/workspaceRoot/biome.json.template +4 -217
  83. package/{typeChecker-kravn7ns.js → typeChecker-g6bh0xfv.js} +1 -1
  84. package/typecheck.proc.js +1 -1
  85. package/{workflow.command-jswbgk90.js → workflow.command-g2cgcgxn.js} +16 -15
  86. package/{workspace.command-h9wwcjpt.js → workspace.command-f7jerqb2.js} +70 -52
  87. package/templates/libRoot/base/baseLogic.ts +0 -14
  88. package/templates/libRoot/base/index.ts +0 -8
  89. package/templates/workspaceRoot/.cursor/rules/akan-page-routing.mdc.template +0 -43
  90. package/templates/workspaceRoot/.cursor/rules/akan-sync-conventions.mdc.template +0 -33
  91. package/templates/workspaceRoot/.cursor/rules/application-test-commands.mdc.template +0 -16
  92. package/templates/workspaceRoot/.cursor/rules/change-scope.mdc.template +0 -12
  93. package/templates/workspaceRoot/.cursor/rules/client-server-boundaries.mdc.template +0 -17
  94. package/templates/workspaceRoot/.cursor/rules/coding-comments.mdc.template +0 -25
  95. package/templates/workspaceRoot/.cursor/rules/coding-style.mdc.template +0 -44
  96. package/templates/workspaceRoot/.cursor/rules/domain-module-conventions.mdc.template +0 -17
  97. package/templates/workspaceRoot/.cursor/rules/format-lint.mdc.template +0 -12
  98. package/templates/workspaceRoot/.cursor/rules/lint-enforced-rules.mdc.template +0 -18
  99. package/templates/workspaceRoot/.cursor/rules/module-file-playbook.mdc.template +0 -61
  100. package/templates/workspaceRoot/.cursor/rules/naming-conventions.mdc.template +0 -15
  101. package/templates/workspaceRoot/.cursor/rules/react-component-style.mdc.template +0 -21
  102. package/templates/workspaceRoot/.cursor/rules/repo-overview.mdc.template +0 -17
  103. package/templates/workspaceRoot/.cursor/rules/scalar-modeling.mdc.template +0 -58
  104. package/templates/workspaceRoot/.cursor/rules/secrets-and-env.mdc.template +0 -13
  105. package/templates/workspaceRoot/.cursor/rules/service-signal-conventions.mdc.template +0 -83
  106. package/templates/workspaceRoot/.cursor/rules/srvkit-adapters-and-guards.mdc.template +0 -50
  107. package/templates/workspaceRoot/.cursor/rules/typescript-imports.mdc.template +0 -16
  108. /package/{index-r24hmh0q.js → index-yb2wsc7f.js} +0 -0
@@ -6,837 +6,6 @@ the framework generate the repeated surfaces.
6
6
  For agent-neutral documentation that Claude Code, Codex, Devin, GitHub Copilot Coding Agent, and other tools can
7
7
  also read, see `docs/AI-DEVELOPMENT.md` and `docs/GENERATED.md`.
8
8
 
9
- ## Workspace Layout
10
-
11
- - `apps/<app>` contains application pages, app UI, app domain modules, env files, and `akan.config.ts`.
12
- - `libs/<lib>` contains shared domain and utility code reused by apps.
13
- - `apps/<app>/page` contains server-side file-routed pages. Index pages use `_index.tsx`; nested layouts use `_layout.tsx`. routeName.tsx becomes /routeName and [modelId].tsx becomes /[modelId].
14
- - `apps/<app>/lib/<model>` contains database-backed domain modules.
15
- - `apps/<app>/lib/_<service>` contains service modules that are not database document models.
16
- - `apps/<app>/lib/__scalar/<scalar>` contains reusable scalar/value types.
17
- - Module abstracts live beside module code as `<model>.abstract.md`, `<service>.abstract.md`, or
18
- `<scalar>.abstract.md`.
19
-
20
- ## Generated Files
21
-
22
- Do not hand-edit generated Akan files. Regenerate them through Akan sync, lint, start, build, or the matching
23
- CLI command instead.
24
-
25
- Common generated files include:
26
- - `apps/*/client.ts`
27
- - `apps/*/server.ts`
28
- - `*/lib/cnst.ts`
29
- - `*/lib/db.ts`
30
- - `*/lib/dict.ts`
31
- - `*/lib/sig.ts`
32
- - `*/lib/srv.ts`
33
- - `*/lib/st.ts`
34
- - `*/lib/useClient.ts`
35
- - `*/lib/useServer.ts`
36
- - `*/lib/**/index.ts`
37
- - `*/ui/index.ts`
38
- - `*/webkit/index.ts`
39
- - `*/srvkit/index.ts`
40
- - `*/common/index.ts`
41
-
42
- Only **layer-root** barrels are generated. A nested `ui/<Folder>/index.tsx` that builds a namespace is
43
- hand-written source and must be edited by hand. The distinguishing test: a generated barrel contains nothing
44
- but `export * from "./X";` lines.
45
-
46
- ## Domain Module Responsibilities
47
-
48
- Use the local module shape before adding a new abstraction.
49
-
50
- - `<model>.constant.ts` defines model, enum, scalar, and schema intent.
51
- - `<model>.abstract.md` defines business intent, domain rules, workflows, and agent notes that are not obvious
52
- from code.
53
- - `<model>.dictionary.ts` defines labels, descriptions, messages, and i18n metadata.
54
- - `<model>.signal.ts` defines typed endpoint contracts.
55
- - `<model>.document.ts` owns persistence and document queries.
56
- - `<model>.service.ts` owns business logic.
57
- - `<model>.store.ts` owns reusable workflow state and actions that span loading, selections, messages, or multi-step client actions. Consume stores through generated `st.use.*` state and `st.do.*` actions.
58
- - `<Model>.Template.tsx` owns form-oriented UI, including local form inputs and submit/import interactions. Client components, with 'use client'.
59
- - `<Model>.Unit.tsx` owns list/item UI. Server components, no 'use client'.
60
- - `<Model>.View.tsx` owns detail UI. Server components, no 'use client'.
61
- - `<Model>.Zone.tsx` owns page/container integration and interactive loading or action flows that need client state. Client components, with 'use client'.
62
- - `<Model>.Util.tsx` owns small module UI helpers, including buttons, import actions, and client-side navigation such as `router.push`. Client components, with 'use client'.
63
-
64
- ## Agent Workflow
65
-
66
- 1. Read the nearby module and convention before creating files. If `*.abstract.md` exists, read it first.
67
- 2. Put new files in the established Akan location instead of adding parallel architecture.
68
- 3. Prefer Akan MCP workflows before direct source edits. Start with `akan mcp --mode plan` for `list_workflows`, `explain_workflow`, and `plan_workflow`.
69
- 4. If `plan_workflow` returns `planPath` or `next.tool=apply_workflow`, call `apply_workflow({ planPath })` before direct source edits.
70
- 5. Use `akan mcp --mode apply` only for allowlisted `apply_workflow`, `run_validation`, and repair tools.
71
- 6. After `apply_workflow`, run `run_validation` with `validationTarget` when present; otherwise use `applyReportPath`.
72
- 7. Direct source edits are denied when an allowlisted Akan workflow or repair tool can perform the change.
73
- 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.
74
- 9. Keep page and layout files server-oriented: route-level `usePage`, headers, static layout, and `akanjs/ui` `Tab` composition belong there.
75
- 10. Put interactive loading, submit/import actions, local form state, and `router.push` in client `Util`, `Template`, or `Zone` components.
76
- 11. Prefer `Tab` for static mode selection instead of extra `useState`; render one focused client component inside each `Tab.Panel`.
77
- 12. Keep server-only logic out of client surfaces and client-only code out of server imports.
78
- 13. Treat `AKAN_PUBLIC_*` env vars as public. Never put secrets in them.
79
- 14. Add or update tests when behavior, contracts, or CLI output changes.
80
- 15. Update `*.abstract.md` when business invariants, workflows, or public behavior change.
81
- 16. Run the smallest relevant verification command after changes.
82
-
83
- ## Common Commands
84
-
85
- Run commands from the workspace root unless a task says otherwise.
86
-
87
- ### Module Addition Workflow
88
-
89
- When adding a new database-backed domain module (e.g., product, user):
90
-
91
- ```bash
92
- # 1. Scaffold the module with Akan CLI (creates constant, service, signal, store, document files)
93
- # The target app/lib is a POSITIONAL argument, not a --app flag.
94
- akan create-module <module-name> <%= appName %>
95
-
96
- # 2. Start dev server with HMR and type checking at http://localhost:8282
97
- akan start <%= appName %>
98
- ```
99
-
100
- ### Change Verification Workflow
101
-
102
- After any code change, run these in order:
103
-
104
- ```bash
105
- # 1. Fast lint check — Akan.js conventions and Biome rules
106
- akan lint <%= appName %>
107
-
108
- # 2. Type-only check — catches server/client boundary violations and import errors
109
- akan typecheck <%= appName %>
110
-
111
- # 3. Test — Run the test code (lib/*/*.signal.test.ts or others)
112
- akan test <%= appName %>
113
-
114
- # 4. Full production build — bundles the app, runs all type/lint checks combined
115
- akan build <%= appName %>
116
- ```
117
-
118
- **Verify endpoints with signal tests, not raw HTTP.** The canonical way to check a query/mutation/slice
119
- contract is an in-memory signal test (`<model>.signal.test.ts`), using the test fetch harness
120
- (`getOrSetupSignalTestFetch`) — it is fast, needs no running server, and exercises `fetch.*`, `view/edit/merge<Model>`,
121
- and slice `init`/`list`/`insight` directly. Prefer it over `curl`: the dev gateway locale-prefixes routes (`/en/...`),
122
- so hand-rolled HTTP calls against a raw path can redirect unexpectedly. See `akan test <%= appName %>`.
123
-
124
- ### Other Frequently Used Commands
125
-
126
- ```bash
127
- akan create-scalar <scalar-name> <%= appName %> # Add a scalar module (lib/__scalar/<scalar-name>/)
128
- akan create-service <service-name> <%= appName %> # Add a service module (lib/_<service-name>/)
129
- akan test <%= appName %> # Run the test code (lib/*/*.signal.test.ts or others)
130
- akan lint <%= appName %> # Lint only (no typecheck)
131
- ```
132
-
133
- **CLI argument conventions.** Two argument styles, and mixing them up is a common mistake:
134
-
135
- - Scaffolding and whole-app commands take the target app/lib as a **positional** argument, not a flag:
136
-
137
- ```bash
138
- akan create-module photo <%= appName %>
139
- akan create-scalar money <%= appName %>
140
- akan create-service billing <%= appName %>
141
- akan sync <%= appName %>
142
- ```
143
-
144
- - Only the source-limited field commands use `--app`/`--module` flags:
145
-
146
- ```bash
147
- akan add-field --app <%= appName %> --module photo --field width --type Int
148
- akan add-enum-field --app <%= appName %> --module photo --field status --values draft,active
149
- ```
150
-
151
- Passing `--app` to `create-module` is not recognized, and the target app will not resolve.
152
-
153
- For the default generated app, start with:
154
-
155
- ```bash
156
- akan start <%= appName %>
157
- ```
158
-
159
- ### The Essential Loop: Workflow -> Sync -> Check
160
-
161
- Almost every Akan.js change follows this pattern. **Missing sync or repair is the #1 cause of agent confusion.**
162
-
163
- > **If the Akan MCP tools are not connected in your agent, skip straight to the CLI-only fallback below.**
164
- > `akan mcp --mode plan/apply` starts a stdio MCP server that only works when your agent is wired to it as an
165
- > MCP client. When those `list_workflows` / `plan_workflow` / `apply_workflow` tools are not available, the CLI
166
- > commands are a fully supported, first-class path — you are not losing any capability by using them.
167
-
168
- 1. **Plan** — Ask the Akan MCP server for the workflow first.
169
- ```
170
- akan mcp --mode plan
171
- # use list_workflows, explain_workflow, and plan_workflow
172
- ```
173
-
174
- 2. **Apply the plan** — If `plan_workflow` returns `planPath` or `next.tool=apply_workflow`, call
175
- `apply_workflow({ planPath })`. Do not copy the workflow plan into direct source edits.
176
- ```
177
- akan mcp --mode apply
178
- # use apply_workflow, run_validation, repair_generated, repair_imports, or repair_module_shape
179
- ```
180
-
181
- Direct edits are fallback only: use them after `list_workflows`/`explain_workflow` confirm no matching workflow, or
182
- after apply reports unsupported/no-op/failed diagnostics that require manual action. Keep fallback edits to owning
183
- source files such as `task.constant.ts`, `task.dictionary.ts`, `Task.Template.tsx`, or `Task.Unit.tsx`.
184
-
185
- 3. **Validate the apply report** — Use the apply report artifact, not the original raw plan, when it is available.
186
- ```
187
- # run_validation with validationTarget first; otherwise use applyReportPath
188
- ```
189
-
190
- 4. **Sync or repair** — Regenerate barrel files so Akan discovers your change. This regenerates:
191
- `cnst.ts`, `db.ts`, `srv.ts`, `sig.ts`, `st.ts`, `dict.ts`, `useClient.ts`, `useServer.ts`,
192
- `ui/index.ts`, `webkit/index.ts`, `srvkit/index.ts`, `common/index.ts`, and all module `index.ts` files.
193
- ```
194
- akan sync <%= appName %>
195
- # or: akan repair generated --app <%= appName %>
196
- ```
197
- **CRITICAL**: Sync after EVERY file add, delete, or rename. Without sync, other modules cannot
198
- `import * as cnst from "../cnst"` and find your new model.
199
-
200
- 5. **Check** — Verify your change compiles and lints.
201
- ```
202
- akan start <%= appName %> # dev server with live feedback (preferred)
203
- akan lint <%= appName %> # quick lint-only check
204
- akan doctor --strict # structured workspace diagnostics
205
- ```
206
-
207
- If `akan sync` gives errors, try:
208
- - `akan build <%= appName %>` — full rebuild catches type errors sync may miss
209
- - Re-run `akan create-module <name> <%= appName %>` if the scaffold is corrupted
210
-
211
- For compound natural-language requests, split the request into workflows and apply each artifact in order. For example,
212
- "create a project module and add a budget field" should run `create-module` plan/apply first, then `add-field`
213
- plan/apply, then validation/doctor on the returned `validationTarget`.
214
-
215
- ### CLI-Only Fallback (MCP Not Connected)
216
-
217
- When the Akan MCP tools are not loaded, run the CLI commands directly. Each MCP tool maps 1:1 to a CLI command,
218
- and the CLI emits the same structured report via `--format json`:
219
-
220
- | MCP tool | CLI-only equivalent |
221
- |----------|---------------------|
222
- | `list_workflows` | `akan workflow list` |
223
- | `explain_workflow <name>` | `akan workflow explain <name>` |
224
- | `plan_workflow <name> ...` | `akan workflow plan <name> ... --format json --out <planPath>` |
225
- | `apply_workflow { planPath }` | `akan workflow apply <planPath> --format json` (add `--dry-run` to preview) |
226
- | `run_validation { validationTarget }` | `akan doctor --strict --format json` (or `akan typecheck <%= appName %>`) |
227
- | `repair_generated` / `repair_imports` / `repair_module_shape` | `akan repair generated\|imports\|module-shape --app <%= appName %> --format json` |
228
-
229
- The scaffolding primitives (`akan create-module`, `akan create-scalar`, `akan create-service`, `akan add-field`,
230
- `akan add-enum-field`) are the same primitives the workflows call, so `create-module <name> <%= appName %>` followed
231
- by `akan sync <%= appName %>` is equivalent to running the `create-module` workflow. Direct source edits remain the
232
- final fallback when no CLI command covers the change.
233
-
234
- ## Quick Decision Matrix — "Where do I put this code?"
235
-
236
- | You want to... | Create in... | Run after... |
237
- |----------------|-------------|--------------|
238
- | Define a new database-backed noun (e.g., User, Product) | `lib/<model>/` → constant, document, service, signal, store, dictionary, abstract | `akan sync <name>` |
239
- | Add a pure workflow / integration (e.g., Payment, Email) | `lib/_<service>/` → service, signal, store, dictionary, abstract | `akan sync <name>` |
240
- | Add a reusable value type (e.g., Address, WorkHistory) | `lib/__scalar/<type>/` → constant, dictionary, abstract | `akan sync <name>` |
241
- | Create a new URL-visitable page | `page/` → `_index.tsx`, `_layout.tsx`, `[param]/_index.tsx` | Rebuild (akan start auto-detects) |
242
- | 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 |
243
- | Add a form or reusable UI component | `ui/` → PascalCase `.tsx` with `"use client"` if needed | `akan sync <name>` |
244
- | Add a React hook or browser helper | `webkit/` → camelCase `.ts` with `"use client"` | `akan sync <name>` |
245
- | Add a server-only guard, middleware, or adaptor | `srvkit/` → PascalCase `.ts` | `akan sync <name>` |
246
- | Add a pure helper (no DOM, no server API) | `common/` → camelCase `.ts` | `akan sync <name>` |
247
-
248
- ## Anti-patterns: Never Do These
249
-
250
- | Don't | Why | Do Instead |
251
- |-------|-----|------------|
252
- | Edit `cnst.ts`, `db.ts`, `srv.ts`, `sig.ts`, `st.ts`, `dict.ts`, `useClient.ts`, `useServer.ts`, or any `index.ts` | These are **generated by `akan sync`**. Your changes will be overwritten. | Edit the source files in `lib/<model>/` directories and run `akan sync <name>` |
253
- | Create a file without running sync | New files won't appear in barrel exports. Imports like `import * as cnst from "../cnst"` will fail. | Always run `akan sync <name>` after creating, renaming, or deleting any module file |
254
- | Use JS `#private` in `*.constant.ts`, `*.document.ts`, `*.service.ts`, or `*.store.ts` | `no-js-private-class-method.grit` bans `#private` in exactly those four file suffixes. The rule is scoped by file path, not class shape — `#private` stays the house style under `srvkit/`, including `adapt()` classes. | `private _methodName()` in those four files; `#methodName()` in `srvkit/` |
255
- | Use `console.log()` | Biome lint forbids `console.log`. Only `console.error`, `console.info`, `console.warn` are allowed. | Use one of the three allowed console methods, or `this.logger.*` / `new Logger("ClassName")` on the server |
256
- | `throw new Error("...")` | `no-throw-raw-error.grit` bans raw errors outside tests, `*.constant.ts`, and `common/`. Raw errors carry no dictionary key, so they cannot be localized or toasted. | `throw new Err("task.error.<key>")` plus an `[en, ko]` entry in the module dictionary's `.error({})` |
257
- | Hand-order Tailwind classes, or reorder them to "fix" a diff | `useSortedClasses` is an error and also sorts the string arguments to `cn()`. Sorter output like `font-bold text-2xl` looks wrong but is correct. | Write classes in any order and let `akan lint` sort them |
258
- | A colour outside the semantic vocabulary — `bg-red-500`, `bg-[#3b82f6]`, `btn-primary`, `text-base-content`, `style={{ color: "#fff" }}` | The theme closes the vocabulary, so these produce **no CSS at all** — the element renders unstyled, and without the lint rules there is no error and no warning either. Raw-palette, arbitrary-colour, daisyUI-legacy and inline-colour rules all catch this. | Semantic tokens: `bg-primary`, `text-foreground/70`, `border-border`. A genuinely fixed colour takes a `// biome-ignore lint/plugin: <reason>` |
259
- | `import` a third-party package inside a page, a barrel, or a module file | `no-import-external-library.grit` covers `page/**`, all barrels, and every `*.{constant,dictionary,document,service,signal,store}.ts` and `*.{Template,Unit,Util,View,Zone}.tsx`. | Re-export the symbol from a one-line shim in `base/`, `webkit/`, or `ui/` first, then import that |
260
- | Import server APIs (`fs`, `Bun`, `process.env`) in `ui/`, `webkit/`, or `common/` | Server-only imports in client code cause build failures. | Keep server dependencies in `lib/`, `srvkit/`, or `private/` only |
261
- | Skip running `akan sync` after deleting a file | Deleted files remain referenced in barrel exports, causing import errors everywhere. | Run `akan sync <name>` after every file add, remove, or rename |
262
- | Use "use client" or `useState`/`useEffect` in pages/*.tsx, *.Unit.tsx, and *.View.tsx files | Server code cannot use React hooks. Wrap in a separate `"use client"` component. | Move hook logic to `webkit/` or a `"use client"` UI component |
263
- | Use `<a>` tag for internal navigation between pages | Akan.js uses `<Link>` from `akanjs/ui` for client-side navigation — avoids full page reloads. | `import { Link } from "akanjs/ui"` and use `<Link href="/task">...</Link>` |
264
- | Name a custom `Endpoint`/`Slice` like a generated CRUD op — `create<Model>`, `update<Model>`, `remove<Model>`, `view<Model>`, `edit<Model>`, `merge<Model>` | These names are already auto-generated. A collision can pass sync/typecheck/build and only fail at runtime. | Pick a distinct verb, e.g. `startTask`/`archiveTask`, never `createTask` for a custom endpoint |
265
-
266
- ## Code Style
267
-
268
- House style for `apps/**` and `libs/**`. `akan lint` enforces the rules in the anti-pattern table above; the rest
269
- is convention that keeps hand-written code reading like generated code.
270
-
271
- ### Files And Types
272
-
273
- - Keep files small. Split a component before it reaches ~150 lines instead of adding section comments.
274
- - Ship every scaffold file even when it is empty — `export class TaskInternal extends internal(srv.task, () => ({})) {}`,
275
- empty dictionary stages, the `// state` / `// action` markers in an empty store. They mark where things go.
276
- - Never add a sibling helper file inside `lib/<model>/`. Helpers go to `common/`, `webkit/`, `srvkit/`, or `ui/`.
277
- - Prefer duplication to premature abstraction at the leaf: copy the near-identical file and change the literals.
278
- - `interface` for object shapes, `type` only for unions and aliases.
279
- - Never use a non-null assertion (`!`). Narrow with `?.`, an early return, or a type predicate.
280
- - Escape with `as unknown as T`, never `as any`.
281
- - `as const` on every `enumOf(...)` array, every Light field tuple, and every module-scope lookup map. Never the
282
- TypeScript `enum` keyword.
283
- - Never annotate a component's return type. Async functions carry no `Async` suffix.
284
-
285
- ### Components
286
-
287
- - `export const X = ({ … }: XProps) => { return (…); };` — arrow const with a block body. `export default` is only
288
- for pages, layouts, and `lazy()` targets.
289
- - Declare `interface <ComponentName>Props` immediately above the component with no blank line, `className?: string`
290
- first. Name it for the component (`CardProps`), never for the model.
291
- - Never `React.FC`, never `defaultProps`, never `PropsWithChildren`. Defaults go in the destructuring; children are
292
- typed `children: ReactNode`.
293
- - `"use client"` on line 1 is mechanical by file role: every `.Zone.tsx`, `.Template.tsx`, and `.Util.tsx` has it;
294
- no `.Unit.tsx` or `.View.tsx` ever does.
295
- - Conditional render is `cond ? <X/> : null`, never `{cond && <X/>}` — in a `className` context the latter renders
296
- the literal string `"false"`.
297
- - Never hand-roll loading, empty, or list states. Use `Load.Units` / `Load.View` / `Load.Edit` with `renderItem`,
298
- `renderList`, and `renderEmpty`, and `Model.New` / `Model.Edit` / `Model.SureToRemove` for CRUD modals.
299
- - Avoid hooks. `useState` is for modal-open, tab, draft-input, and drag state only — never for server data.
300
- `.Template.tsx` files contain zero `useState`: forms are store-driven with `Field.*`, `value={taskForm.x}`, and
301
- `onChange={st.do.setXOnTask}` passed by reference.
302
- - Read with `st.use.*` and write with `st.do.*`. Client components do not call `fetch.*`.
303
- - Reach for a recipe before writing a look by hand: `buttonRecipe`, `badgeRecipe` and `inputRecipe` from
304
- `akanjs/ui` carry the button, badge and field surfaces. Call them as `buttonRecipe(variants?, className?)` —
305
- the second argument merges internally and takes an array, so never wrap it in `cn()`.
306
- - Static class strings stay plain strings. Reach for `cn` only for a conditional or to merge an incoming
307
- `className`, and merge the caller last: `cn("base", conditional, className)`. `cn` comes from `akanjs/client`
308
- and is the only class-combining function — no `clsx`, no raw `twMerge`.
309
- - Use semantic tokens with opacity modifiers (`text-foreground/60`, `bg-background/70`, `border-border`). Never
310
- `dark:` — theming is the token block in `page/*/styles.css`, where each token has a `-foreground` pair.
311
- - Hoist enum→class lookups to a module-scope `as const` map typed `{ [key in cnst.TaskStatus["value"]]: string }`,
312
- not `Record<...>`.
313
-
314
- ### Naming
315
-
316
- - Component exports are role names (`Card`, `General`, `Preview`, `Remove`). The model comes from the namespace, so
317
- write `Card`, not `TaskCard`.
318
- - Layer the verbs: the document chain method drops the model (`start()`) and the signal, store, and dictionary
319
- re-add it (`startTask`). This keeps custom endpoints clear of generated CRUD and makes `st.do.X` read the same as
320
- `fetch.X`.
321
- - Slice and filter names are prepositional: `inTodo`, `byStatuses`, `ofProject`. Never `getXInY`, never `listX`.
322
- - Handlers are `onX` props with inline arrows. Do not extract a `handleX`.
323
- - Booleans are `is*` / `has*` / `can*` / `show*`. Counters are `*Num`, indices are `idx`, collections are `*List`.
324
- - Identifiers, type names, endpoint names, and log messages are English. Everything a user reads goes through
325
- `l("task.title")` or `l.trans({ … })` — never a hard-coded string in JSX, never `window.alert`.
326
-
327
- ### Backend
328
-
329
- - **`constant.ts`** — five classes in order, `TaskInput → TaskObject → LightTask → Task → TaskInsight`, and write
330
- `TaskInsight` even when empty. Put display and predicate logic on `LightTask` (`isNew()`, `canWrite(user?)`): it is
331
- the class both server and client hold, so shared logic belongs there rather than in a util module. Collection
332
- helpers go `static` on the full model.
333
- - **`document.ts`** — `TaskFilter extends from(...)` → `Task extends by(...)` → `TaskModel extends into(...)`, with
334
- `sort: {}` always present. Chain methods validate → mutate → `return this` and never `save()`; the caller saves, so
335
- chains compose. Indexes and derived totals go in `static override _onSchema`.
336
- - **`service.ts`** — keep methods to a few lines: load → chain → `return await ….save()`. Side effects go in
337
- `override async _preUpdate` / `_postCreate`, not inline. Fire-and-forget is explicitly `void`-ed. Return `null` or
338
- `false` for "not allowed" and let the signal decide whether that is an error.
339
- - **`signal.ts`** — `TaskInternal` → `TaskSlice` → `TaskEndpoint`, all three declared even when empty, and `exec` is a
340
- one-liner delegating to the service. Every `slice()` takes an explicit `{ guards: { root: Admin, … } }`, and every
341
- custom mutation, query, and message names its own `guards: [...]`.
342
- - **`store.ts`** — write a custom action only for a toast, an optimistic update, or a multi-field write; the rest is
343
- generated. Never `import type { RootStore } from "../st"` — it crashes `akan build` with a Bun SSR segfault.
344
- - **`dictionary.ts`** — fixed chain with empty stages still written:
345
- `.of() → .model() → .insight() → .query() → .sort() → .enum() → .slice() → .endpoint() → .error() → .translate()`.
346
- Every label is `t(["English", "한국어"])`, and nearly every one also carries `.desc([en, ko])`.
347
- - **`srvkit/` adapters** — an injected singleton is an `adapt("name" as const, ({ use, env, plug, memory }) => ({…}))`
348
- class, injected with `plug(TheClass)`. It self-registers, so do not add it to `lib/option.ts`. `this.logger` is
349
- provided; lifecycle work goes in `override async onInit()`. A per-use value object stays a plain class you `new` at
350
- the call site. Route remote calls through one private `#api<T>(path, init?)` with `AbortSignal.timeout(20_000)`, and
351
- resolve secrets inside a function, never at module scope.
352
- - **Errors** — state-machine preconditions throw in `document.ts`, cross-document rules in `service.ts`, and
353
- request-level policy lives in signal guards. `try/catch` always converts an exception into a decision, never
354
- swallows one. Store actions do not `try/catch`; let the framework toast the `Err`.
355
-
356
- ### Comments
357
-
358
- Do not narrate code. Do document the thing the code cannot say. Both halves are the rule.
359
-
360
- - Never add a comment that restates the identifier, the signature, or the control flow.
361
- - Do not add JSDoc, section banners, or "why/how" comments for ordinary logic.
362
- - Density tracks the layer: pages carry none, product code stays under 1 %, and `srvkit/` adapters and `guards.ts`
363
- carry as much as the external constraints require.
364
- - A comment is warranted for a vendor spec or protocol quirk, an infrastructure constraint, a third-party library
365
- gotcha, security reasoning, a math derivation, a domain field's business meaning, a state transition above a
366
- document chain method, or why an obvious alternative was rejected.
367
- - Markers: `TODO` unfinished work · `FIXME` known broken behavior · `XXX` hazard a reader must not miss · `//!`
368
- disabled or must-fix code · `//?` an explanatory aside · `//*` a design note · deletion caution, warning why
369
- removing a line would break something non-obvious.
370
- - Keep allowed comments to one short line, and give every suppression a reason:
371
- `// biome-ignore lint/<rule>: <why>`. Never a bare disable block.
372
- - Match nearby file style: if the surrounding code has few comments, keep it that way.
373
-
374
- ## Generated File Tracker (Quick Reference)
375
-
376
- These files are regenerated by `akan sync` and overwritten on every sync. **Do not hand-edit them.**
377
-
378
- | File | Generated From | Purpose |
379
- |------|---------------|---------|
380
- | `*/lib/cnst.ts` | All `*/lib/*/**.constant.ts` | Barrel for all constants |
381
- | `*/lib/db.ts` | All `*/lib/<model>/*.document.ts` | Barrel for all document models |
382
- | `*/lib/dict.ts` | All `*/lib/*/**.dictionary.ts` | Barrel for all dictionaries |
383
- | `*/lib/sig.ts` | All `*/lib/**/**.signal.ts` | Barrel for all signals |
384
- | `*/lib/srv.ts` | All `*/lib/**/**.service.ts` | Barrel for all services |
385
- | `*/lib/st.ts` | All `*/lib/**/**.store.ts` | Barrel for all stores |
386
- | `*/lib/useClient.ts` | Client-safe module re-exports | Client-side import entry |
387
- | `*/lib/useServer.ts` | Server-only module re-exports | Server-side import entry |
388
- | `apps/*/client.ts` | App-wide client barrel | The `fetch` and `st` instances |
389
- | `apps/*/server.ts` | App-wide server barrel | Server-side service resolution |
390
- | `*/lib/**/index.ts` | Per-module barrel | Module-level re-exports |
391
- | `*/ui/index.ts` | All 1-depth UI files/folders | UI layer-root barrel (nested `ui/<Folder>/index.tsx` is **not** generated) |
392
- | `*/webkit/index.ts` | All webkit files | Webkit barrel |
393
- | `*/srvkit/index.ts` | All srvkit files | Srvkit barrel |
394
- | `*/common/index.ts` | All common files | Common barrel |
395
-
396
- ## Workflow Recipes
397
-
398
- Concrete step-by-step recipes for the most frequent Akan.js changes. Each recipe shows which files to edit
399
- and in what order. The code examples reference the `task` module in `apps/<%= appName %>/lib/task/` as a
400
- template; replace `task` with your model name and `Task` with your PascalCase model name.
401
-
402
- When editing a file, always read the existing content first. Only change the relevant sections — do not
403
- rewrite the entire file.
404
-
405
- ---
406
-
407
- ### Recipe 1: Adding a New Field to a Model
408
-
409
- **Files to edit (in order):** `constant.ts` → `dictionary.ts` → `Template.tsx` → `Unit.tsx` → `akan sync`
410
-
411
- ```typescript
412
- // 1. apps/<app>/lib/<model>/<model>.constant.ts
413
- // Add field to the Input class. Use field() builder with optional defaults
414
- export class TaskInput extends via((field) => ({
415
- title: field(String),
416
- priority: field(TaskPriority, { default: "medium" }), // NEW FIELD
417
- })) {}
418
-
419
- // If the new field should appear in list views, also add it to LightTask:
420
- export class LightTask extends via(TaskObject, ["title", "priority", "status", "due"] as const, () => ({})) {}
421
-
422
- // 2. apps/<app>/lib/<model>/<model>.dictionary.ts
423
- // Add i18n labels for the new field (and its enum values if any).
424
- // Labels are [en, ko] pairs, and nearly every one also carries a .desc([en, ko]).
425
- .model<Task>((t) => ({
426
- priority: t(["Priority", "우선순위"]).desc(["How urgent the task is", "할 일의 긴급도"]),
427
- }))
428
- .enum<TaskPriority>("taskPriority", (t) => ({
429
- low: t(["Low", "낮음"]),
430
- medium: t(["Medium", "보통"]),
431
- high: t(["High", "높음"]),
432
- }))
433
-
434
- // 3. apps/<app>/lib/<model>/<Model>.Template.tsx
435
- // Add a form field using st.do.setXxxOnYyy (auto-generated setter)
436
- const form = st.use.taskForm();
437
- <Field.ToggleSelect
438
- label={l("task.priority")}
439
- items={cnst.TaskPriority}
440
- value={form.priority}
441
- onChange={st.do.setPriorityOnTask}
442
- />
443
-
444
- // 4. apps/<app>/lib/<model>/<Model>.Unit.tsx
445
- // Display the new field in card/list views
446
- <Badge variant={task.priority === "high" ? "error" : task.priority === "medium" ? "warning" : "default"}>
447
- {task.priority}
448
- </Badge>
449
-
450
- // 5. Regenerate barrels
451
- // akan sync <name>
452
- ```
453
-
454
- ---
455
-
456
- ### Recipe 2: Injecting a Dependency into a Service
457
-
458
- Three patterns: injecting an **external adapter** (`use<>()`), another **module's service** (`service<>()`),
459
- or a **predefined framework adapter** (`plug()`). A field named `<refName>Service` resolves to the service
460
- registered under `<refName>` — the `Service`/`Signal` suffix is required and stripped to derive the lookup key.
461
-
462
- > **For a new adapter you own, prefer the `adapt()` shape in pattern C over the `option.ts` registration in
463
- > pattern A.** An `adapt()` class self-registers and is injected with `plug(TheClass)`, so it never touches
464
- > `option.ts`. Pattern A is the legacy constructor-style shape: recognise it, keep it working, and migrate one
465
- > only when you are already changing it.
466
-
467
- > `apps/<app>/lib/option.ts` is a **user-owned** file scaffolded once — edit it to register adapters/DI. Unlike the
468
- > barrels (`cnst.ts`, `db.ts`, `srv.ts`, …) it is **not** overwritten by `akan sync`, so your `.use(...)` registrations
469
- > are safe.
470
-
471
- **A. Adapter injection via `use<>()` (for external clients / global singletons)**
472
-
473
- ```typescript
474
- // 1. Create the adapter class in apps/<app>/srvkit/
475
- // apps/<app>/srvkit/EmailClient.ts
476
- export class EmailClient {
477
- constructor(readonly apiKey: string) {}
478
- async send(opts: { to: string; subject: string; body: string }) { /* ... */ }
479
- }
480
-
481
- // 2. Register in apps/<app>/lib/option.ts
482
- export const option = new AkanOption()
483
- .use((options) => ({
484
- emailClient: new EmailClient(options.mailerApiKey),
485
- }));
486
-
487
- // 3. Inject via use<>() in <model>.service.ts
488
- export class TaskService extends serve(db.task, ({ use }) => ({
489
- emailClient: use<EmailClient>(),
490
- })) {
491
- async _postCreate(task: cnst.Task) {
492
- await this.emailClient.send({ to: "...", subject: "Task Created", body: `Task "${task.title}" created.` });
493
- }
494
- }
495
- ```
496
-
497
- **B. Cross-module service injection via `service<>()` (for other Akan services)**
498
-
499
- ```typescript
500
- // In <model>.service.ts — inject another module's service
501
- import * as srv from "../srv";
502
-
503
- export class TaskService extends serve(db.task, ({ service }) => ({
504
- notiService: service<srv.NotiService>(),
505
- })) {
506
- async _postCreate(task: cnst.Task) {
507
- await this.notiService.send("info", `Task "${task.title}" created`);
508
- }
509
- }
510
- ```
511
-
512
- **C. Predefined framework adapter injection via `plug()` (storage, cache, queue, schedule, …)**
513
-
514
- Akan ships predefined adapter roles from `akanjs/service`: `StorageAdaptorRole`, `CacheAdaptorRole`,
515
- `QueueAdaptorRole`, `ScheduleAdaptorRole`, `DatabaseAdaptorRole`, `WebsocketAdaptorRole`,
516
- `LoggingAdaptorRole`, `CompressAdaptorRole`. `plug()` injects the concrete adapter bound to that role (the
517
- default `StorageAdaptor` binding is `BlobStorage`). `plug()` also accepts a concrete adapter class directly.
518
-
519
- ```typescript
520
- // In <model>.service.ts — inject the framework storage adapter by role
521
- import { plug, serve, StorageAdaptorRole } from "akanjs/service";
522
-
523
- export class TaskService extends serve(db.task, ({ plug }) => ({
524
- storage: plug(StorageAdaptorRole),
525
- })) {
526
- async attach(taskId: string, path: string, localPath: string) {
527
- // BlobStorage returns a URL under blobStorage.urlPrefix (default "/api/localFile/getBlob").
528
- return await this.storage.uploadDataFromLocal({ path, localPath });
529
- }
530
- }
531
- ```
532
-
533
- For a custom adapter class (not a predefined role), pass the class itself, e.g. `ipfsApi: plug(IpfsApi)`
534
- (see `libs/shared/lib/file/file.service.ts`). Injecting a file/image field is usually simpler than calling
535
- storage directly: declare `image: field(File).optional()` (or `images: field([File])`) on the model and let the
536
- store's generated `upload<Field>On<Model>(fileList)` action handle the upload. Add `{ cascade: "remove" }` to that
537
- field when the file belongs to the model alone, and removing the model removes the file and its stored object.
538
-
539
- ---
540
-
541
- ### Recipe 3: Creating and Using a Slice
542
-
543
- A Slice is a named, filtered data view. Add file entries and connect from a page.
544
-
545
- > **Silent failure — a slice `exec` must return a query descriptor, never an executed list.**
546
- > Return `this.taskService.queryByStatuses(...)` (the `query<Filter>` builder), **not**
547
- > `this.taskService.listByStatuses(...)` / `listBy...(...)` (which returns a `Promise<Doc[]>`).
548
- > Returning an array type-checks but throws at runtime during insight aggregation with the opaque
549
- > `Error: Unknown document field path: 0`. If you see that error, your slice is returning a list, not a query.
550
-
551
- ```typescript
552
- // 1. apps/<app>/lib/<model>/<model>.signal.ts — Define the slice
553
- export class TaskSlice extends slice(srv.task, (init) => ({
554
- inTodo: init()
555
- .search("statuses", [cnst.TaskStatus])
556
- .exec(function (statuses?) {
557
- // ✅ query<Filter> — a query descriptor. ❌ listByStatuses(...) returns an array and fails at runtime.
558
- return this.taskService.queryByStatuses(statuses ?? ["todo", "inProgress"]);
559
- }),
560
- })) {}
561
-
562
- // 2. apps/<app>/lib/<model>/<model>.document.ts — Add query filter for slice
563
- export class TaskFilter extends from(cnst.Task, (filter) => ({
564
- query: {
565
- byStatuses: filter()
566
- .arg("statuses", [cnst.TaskStatus])
567
- .query((statuses) => ({ status: { $in: statuses } })),
568
- },
569
- })) {}
570
-
571
- // 3. apps/<app>/lib/<model>/<model>.dictionary.ts — Slice labels
572
- .slice<TaskSlice>((fn) => ({
573
- inTodo: fn(["Tasks In Todo", "할 일"]).arg((t) => ({
574
- statuses: t(["Statuses", "상태"]),
575
- })),
576
- }))
577
-
578
- // 4. In page — Init the slice in an async Page and hand the init to a Zone.
579
- export default async function Page() {
580
- const [{ taskInitInTodo }] = await Promise.all([fetch.initTaskInTodo()]);
581
- return <Task.Zone.Card init={taskInitInTodo} sliceName="taskInTodo" />;
582
- }
583
- ```
584
-
585
- The slice name in code uses camelCase (`inTodo`). In dictionary and components it becomes `"taskInTodo"`.
586
-
587
- ---
588
-
589
- ### Recipe 4: Creating a Mutation Endpoint (with Status Workflow)
590
-
591
- **Files to edit (in order):** `document.ts` → `service.ts` → `signal.ts` → `dictionary.ts` → `store.ts` → `Util.tsx`
592
-
593
- ```typescript
594
- // 1. <model>.document.ts — Document chain method with state validation
595
- export class TaskDocument extends by(cnst.Task) {
596
- start() {
597
- if (this.status !== "todo") throw new Err("task.error.cannotStartFromNonTodo");
598
- this.status = "inProgress";
599
- return this; // Return this for chaining: task.start().save()
600
- }
601
- }
602
-
603
- // 2. <model>.service.ts — Service method wrapping document
604
- async startTask(taskId: string) {
605
- const task = await this.getTask(taskId);
606
- return task.start().save();
607
- }
608
-
609
- // 3. <model>.signal.ts — Mutation endpoint
610
- export class TaskEndpoint extends endpoint(srv.task, ({ mutation }) => ({
611
- startTask: mutation(cnst.Task)
612
- .param("taskId", String)
613
- .exec(async function (taskId) {
614
- return await this.taskService.startTask(taskId);
615
- }),
616
- })) {}
617
-
618
- // 4. <model>.dictionary.ts — Endpoint + error labels
619
- .endpoint<TaskEndpoint>((fn) => ({
620
- startTask: fn(["Start Task", "작업시작"])
621
- .arg((t) => ({ taskId: t(["Task ID", "할 일 ID"]) })),
622
- }))
623
- .error({
624
- cannotStartFromNonTodo: ["Task can only start from todo status", "할 일 상태에서만 시작 가능"],
625
- })
626
-
627
- // 5. <model>.store.ts — Client-side action with toast feedback
628
- async startTask(taskId: string) {
629
- msg.loading("task.startTaskLoading", { key: "startTask" });
630
- const task = await fetch.startTask(taskId);
631
- this.setTask(task); // Auto-generated: updates task state in store
632
- msg.success("task.startTaskSuccess", { key: "startTask" });
633
- }
634
-
635
- // 6. <Model>.Util.tsx — Reusable button component
636
- export const Start = ({ taskId }: { taskId: string }) => (
637
- <button className={buttonRecipe({ variant: "primary", size: "xs" })} onClick={() => st.do.startTask(taskId)}>
638
- Start
639
- </button>
640
- );
641
- ```
642
-
643
- ---
644
-
645
- ### Recipe 5: Internal Triggers — Interval & Cron
646
-
647
- Server-side background jobs. Defined in `internal()` signal, implemented in service.
648
-
649
- ```typescript
650
- // 1. <model>.signal.ts — Define triggers
651
- export class TaskInternal extends internal(srv.task, ({ interval, cron }) => ({
652
- cleanupStaleTasks: interval(10000).exec(async function () {
653
- await this.taskService.cleanupStaleTasks();
654
- }),
655
-
656
- dailyDigest: cron("0 0 * * *").exec(async function () {
657
- await this.taskService.sendDailyDigest();
658
- }),
659
- })) {}
660
-
661
- // 2. <model>.service.ts — Implement the logic
662
- async cleanupStaleTasks() {
663
- const weekAgo = dayjs().subtract(7, "day").toDate();
664
- const stale = await this.taskModel.listDueBefore(weekAgo);
665
- for (const task of stale) {
666
- await task.remove();
667
- }
668
- }
669
- ```
670
-
671
- **Available trigger types:**
672
- - `interval(ms)` — runs repeatedly at the given interval
673
- - `cron("min hour dom month dow")` — runs on a schedule
674
- - `initialize()` — runs once on service startup
675
- - `process(Return).msg(Type)` — message queue consumer
676
-
677
- ---
678
-
679
- ### Recipe 6: Creating an Insight (Aggregation / Dashboard Stats)
680
-
681
- Insights display aggregated statistics across model data.
682
-
683
- ```typescript
684
- // 1. <model>.constant.ts — Insight class with accumulate rules
685
- export class TaskInsight extends via(Task, (field) => ({
686
- totalCount: field(Int, { default: 0, accumulate: {} }),
687
- completedCount: field(Int, { default: 0, accumulate: { status: "completed" } }),
688
- })) {}
689
-
690
- // 2. <model>.dictionary.ts — Insight field labels
691
- .insight<TaskInsight>((t) => ({
692
- totalCount: t(["Total Tasks", "전체 할 일"]),
693
- completedCount: t(["Completed", "완료됨"]),
694
- }))
695
-
696
- // 3. <Model>.View.tsx — Display component consuming an Insight model
697
- export const Stats = ({ taskInsight }: { taskInsight: cnst.TaskInsight }) => (
698
- <div className="grid grid-cols-2 gap-4">
699
- <div className="rounded-lg border border-border bg-card p-4">
700
- <div className="text-muted-foreground text-sm">{l("task.totalCount")}</div>
701
- <div className="font-bold text-2xl text-primary">{taskInsight.totalCount}</div>
702
- </div>
703
- <div className="rounded-lg border border-border bg-card p-4">
704
- <div className="text-muted-foreground text-sm">{l("task.completedCount")}</div>
705
- <div className="font-bold text-2xl text-success">{taskInsight.completedCount}</div>
706
- </div>
707
- </div>
708
- );
709
-
710
- // 4. <Model>.Zone.tsx — Mount in page via Load.Insight bound to a slice
711
- export const Insight = ({ sliceName }: { sliceName: string }) => {
712
- const insight = st.slice[sliceName].use.taskInsight();
713
- if (!insight) return null;
714
- return <Task.View.Stats taskInsight={insight} />;
715
- };
716
- ```
717
-
718
- ---
719
-
720
- ### Data Flow Summary
721
-
722
- For each business question, follow this chain:
723
-
724
- | Question | File Pattern |
725
- |----------|-------------|
726
- | What fields does it have? | `constant.ts` — `via()` layers: Input → Object → Light → Model → Insight |
727
- | How is it stored/searched? | `document.ts` — `from()` filters + `by()` document methods + `into()` model |
728
- | What business rule should run? | `service.ts` — `serve()` with DI (`use`, `service`, `plug`, `env`, `memory`) |
729
- | What should a page call? | `signal.ts` — `internal()` (jobs), `endpoint()` (APIs), `slice()` (data views) |
730
- | What client state is shared? | `store.ts` — `store()` with auto-generated form/insight state + custom actions |
731
- | What should users see? | `View.tsx` + `Zone.tsx` (detail/container), `Template.tsx` (forms), `Unit.tsx` (cards), `Util.tsx` (buttons) |
732
-
733
- ## Modeling & Query Gotchas
734
-
735
- A short list of things the type system does not always catch:
736
-
737
- - **Slices return a query, not a list.** A slice `exec` must return `this.<model>Service.query<Filter>(...)`, never
738
- a `list<Filter>(...)` / `listBy...(...)` array. Returning an array type-checks but fails at runtime with
739
- `Unknown document field path: 0`. (See Recipe 3.)
740
- - **Custom endpoint names must not collide with generated CRUD.** `create/update/remove/view/edit/merge<Model>`
741
- already exist. A collision can build green and fail only at runtime — pick a distinct verb.
742
- - **Numbers are `Int` or `Float`, never `Number`.** `field(Number)` / `.body("x", Number)` fail to typecheck. Use
743
- `Int` for counts, `Float` for decimals.
744
- - **Array fields use `field([T])`.** e.g. `tags: field([String])`, `images: field([File])` — not `field(String)` with
745
- a suffix.
746
- - **Reading a secret field needs an explicit select.** `field(...).secret()` values (e.g. `passwordHash`) are stripped
747
- from query results by default. Fetch them with `{ select: { <field>: true } }`, e.g.
748
- `this.userModel.pickById(id, { select: { passwordHash: true } })`.
749
- - **Text search fields use the `text` role.** Opt a field into the full-text index with
750
- `field(String, { text: "title" })` (or `"desc"` / `"tag"` / `"thumb"` / `"filter"`). Nothing else opts a field in.
751
- `secret` / `hidden` / `resolve()` fields with `text` throw at class-build time — the mirror is plaintext. Search
752
- runs on sqlite/libsql only; `q.search()` against Postgres throws. `thumb` is mirrored for rendering and is not
753
- indexed.
754
- - **`cascade: "remove"` takes a relation's target down with its owner.** Declare it on the relation itself —
755
- `image: field(File, { cascade: "remove" })`, arrays included. The removal runs through the **target's service**,
756
- so the target's own `_postRemove` runs too; that is how removing a model also deletes the file's stored object.
757
- Only a relation accepts it: a `String`, an `ID`, or a scalar throws while the class is being built. Nothing
758
- checks for other references to the same target, so declaring it asserts exclusive ownership. Document removal is
759
- soft but the storage delete is not, and query-level removal fires no hooks and therefore no cascade.
760
- - **`q.search()` is a filter node, not a slice requirement.** Prefer
761
- `bySearch: filter().arg("text", String).query((text, q) => q.search(text, { prefix: true }))` — the generated
762
- `listBySearch` / `countBySearch` / `queryBySearch` / `insightBySearch` come for free. Only add a search slice when
763
- the model's data is safe to enumerate. It must sit at an AND position (not under `q.any()` / `q.not()`), blank
764
- input matches nothing, and score order needs the built-in `relevance` sort key — a slice endpoint that leaves
765
- `sort` off gets `latest`, not relevance.
766
-
767
- ## Current User, Guards & Auth-Gated Pages
768
-
769
- Built-in user authentication (session / JWT / password hashing) ships as a separate Akan auth library, not in the
770
- core framework. The core framework gives you the composition points below; wire the auth library through them.
771
-
772
- - **Guards** attach at the signal declaration, not per-method:
773
- `endpoint(srv.task, { guards: { root: SignedIn } }, ({ mutation }) => ({...}))` or
774
- `slice(srv.task, { guards: { root: SignedIn, get: Public, cru: Public } }, ...)`. `Public` always allows; other
775
- guards implement the `Guard` interface in `srvkit/` (server-only) and read the request context.
776
- - **Read the current user inside a custom endpoint** by injecting an `InternalArg` with `.with(...)`:
777
- `mutation(cnst.Task).with(CurrentUserId).exec(async function (currentUserId) { ... })`. The `Guard` /
778
- `InternalArg` helpers live in `srvkit/` and read `context.getHttpContext().req.user`.
779
- - **Auto-generated CRUD and `serve()` service methods / lifecycle hooks do not receive session context.** If an
780
- operation needs the acting user, expose a custom endpoint that takes it via `.with(CurrentUserId)` — never trust a
781
- client-supplied user id.
782
- - **SSR auth-gated pages: guard at the layout.** Check the session in the `_layout.tsx` loader and redirect when it is
783
- absent, so nested pages never render for signed-out users.
784
-
785
- ## Auto-Generated API Reference
786
-
787
- akan sync automatically generates APIs across all layers. Only write custom logic — never hand-write what the framework generates.
788
-
789
- ### Signal — Endpoint Auto-Generation
790
-
791
- | Auto-Generated | Signature | Description |
792
- |---------------|-----------|-------------|
793
- | `view[Model](id)` | `fetch.viewTask(id)` | Fetch single model for detail view |
794
- | `edit[Model](id)` | `fetch.editTask(id)` | Fetch model for edit view |
795
- | `merge[Model](id, data)` | `fetch.mergeTask(data)` | Create (no id) or update (with id) model |
796
- | `[model]List[Suffix](args, skip, limit, sort)` | `fetch.taskListInTodo(args)` | Paginated list from slice filter |
797
- | `[model]Insight[Suffix](args)` | `fetch.taskInsightInTodo(args)` | Aggregated insight from slice query |
798
- | `init[Model][Suffix](args)` | `fetch.initTaskInTodo(args)` | Initialize slice with list + insight |
799
-
800
- **Rule**: Only define `query()`, `mutation()`, `message()`, `pubsub()` endpoints manually when the endpoint needs custom business logic. Standard CRUD is already auto-generated.
801
-
802
- ### Service — CRUD & Query Auto-Generation
803
-
804
- | Auto-Generated | Description |
805
- |---------------|-------------|
806
- | `this.<model>Model` | Auto-injected model adaptor |
807
- | `get<Model>(id)`, `load<Model>(id)` | Single document lookup |
808
- | `create<Model>(data)`, `update<Model>(id, data)`, `remove<Model>(id)` | CRUD operations — named after the model, e.g. `createTask`/`updateTask`/`removeTask` (there is no literal `createModel`) |
809
- | `list<Query>(args)`, `find<Query>(args)`, `pick<Query>(args)` | Filter-based queries |
810
- | `exists<Query>(args)`, `count<Query>(args)`, `insight<Query>(args)` | Filter-based helpers |
811
- | `_preCreate`, `_postCreate`, `_preUpdate`, `_postUpdate`, `_preRemove`, `_postRemove` | Lifecycle hooks (override to add logic) |
812
-
813
- **Rule**: Use `_preCreate`/`_postCreate` lifecycle hooks for side effects (e.g., push workHistory entries). Write custom service methods only for multi-model orchestration.
814
-
815
- ### Store — State & Action Auto-Generation
816
-
817
- | Auto-Generated | Description |
818
- |---------------|-------------|
819
- | `[model]` (cached full model), `[model]Loading`, `[model]Form`, `[model]Modal` | Base model states |
820
- | `create[Model](data)`, `update[Model](id, data)`, `remove[Model](id)` | CRUD actions |
821
- | `new[Model](partial)`, `edit[Model](model)`, `view[Model](model)` | Form/view state actions |
822
- | `[slice]List`, `[slice]InitList`, `[slice]Insight`, `[slice]Selection` | Slice states |
823
- | `init[Slice](args)`, `refresh[Slice]()`, `setPageOf[Slice](page)` | Slice actions |
824
- | `set[Field]On[Model](value)` | Auto-setters for each model field |
825
-
826
- **Rule**: Write custom store actions only for toast messages (`msg.loading`/`msg.success`) or multi-step workflows. State fields and CRUD actions are already auto-generated.
827
-
828
- ### Document — Filter Query Auto-Generation
829
-
830
- | Auto-Generated (from Filter definition) | Description |
831
- |----------------------------------------|-------------|
832
- | `list[Query](args)`, `listIds[Query](args)` | List documents matching filter |
833
- | `find[Query](args)`, `findId[Query](args)` | Find one (null if not found) |
834
- | `pick[Query](args)`, `pickId[Query](args)` | Find one (throw if not found) |
835
- | `exists[Query](args)`, `count[Query](args)` | Existence check and count |
836
- | `insight[Query](args)`, `query[Query](args)` | Insight and raw query |
837
-
838
- **Rule**: Define `Filter` with `.query()` conditions in `document.ts`. akan sync auto-generates all 10 query helper methods per filter. Write `Document` chain methods only for state transitions with validation.
839
-
840
9
  ## Generated Context
841
10
 
842
11
  The section below is regenerated by `akan agent install` (run `bun run setup:agent`). It lists this