@akanjs/cli 3.0.0-alpha.5 → 3.0.0-alpha.51

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 (110) 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-b0fcx3r2.js → agent.command-nqs3bw0g.js} +8 -7
  6. package/{application.command-cgmy00gd.js → application.command-k2v46pxf.js} +7 -6
  7. package/{applicationBuildRunner-n1svrwgt.js → applicationBuildRunner-t5v6tcrm.js} +6 -7
  8. package/{applicationReleasePackager-gg8ccbeh.js → applicationReleasePackager-93nzdfsy.js} +1 -1
  9. package/buildBatch.proc.js +6 -5
  10. package/{capacitorApp-yyw35vg4.js → capacitorApp-927ahhz2.js} +29 -26
  11. package/{cloud.command-1m7hfdpk.js → cloud.command-7jwmxtez.js} +11 -10
  12. package/{context.command-b5heat2a.js → context.command-6kn3tady.js} +16 -15
  13. package/{dependencyScanner-f6grrep3.js → dependencyScanner-rbs2407b.js} +1 -1
  14. package/getDirname-ka11hm7e.js +8 -0
  15. package/{guideline.command-9ew6aqhm.js → guideline.command-pjdrndqk.js} +7 -6
  16. package/guidelines/agentRule/agentRule.generate.json +31 -0
  17. package/guidelines/agentRule/agentRule.instruction.md +457 -0
  18. package/guidelines/conventions/conventions.generate.json +25 -0
  19. package/guidelines/conventions/conventions.instruction.md +773 -0
  20. package/guidelines/cssRule/cssRule.instruction.md +7 -0
  21. package/guidelines/framework/framework.instruction.md +1 -0
  22. package/guidelines/mcpRule/mcpRule.generate.json +25 -0
  23. package/guidelines/mcpRule/mcpRule.instruction.md +176 -0
  24. package/guidelines/workspaceOnboarding/workspaceOnboarding.generate.json +24 -0
  25. package/guidelines/workspaceOnboarding/workspaceOnboarding.instruction.md +638 -0
  26. package/incrementalBuilder.proc.js +7 -6
  27. package/index-1577bej2.js +7 -0
  28. package/{index-jn9vfpwz.js → index-24h5bqpj.js} +54 -15
  29. package/{index-hrwzz2wf.js → index-4z05haak.js} +1 -1
  30. package/{index-m4kpmmtj.js → index-5em412h5.js} +1 -1
  31. package/{index-6npsfpxr.js → index-5fe016p2.js} +3 -3
  32. package/{index-bjpxzr6s.js → index-5vy2jzar.js} +1 -1
  33. package/{index-exhpp31y.js → index-6bs24rb4.js} +15 -7
  34. package/{index-nx1vbtsc.js → index-7etegaht.js} +14 -14
  35. package/{index-wrrb8rxk.js → index-7w5vv8jk.js} +4 -4
  36. package/{index-meh7mmz9.js → index-7zd96bng.js} +8 -4
  37. package/{index-p69fcwe1.js → index-866yqz3y.js} +13 -13
  38. package/{index-jkynkyfk.js → index-8rr621r2.js} +5 -3
  39. package/{index-47qxw1tz.js → index-90zyjrby.js} +170 -65
  40. package/{index-pfrt7qwh.js → index-97cw8sg1.js} +4 -4
  41. package/{index-p6txr1ss.js → index-bwt525ne.js} +9 -11
  42. package/{index-qp089jp0.js → index-j37qq1f2.js} +1 -1
  43. package/{index-c94nq3c1.js → index-k7sm38v6.js} +3 -3
  44. package/{index-j9cxndc0.js → index-qnv0aqmm.js} +9 -6
  45. package/{index-0cj2zxbm.js → index-sh01r3pm.js} +1 -1
  46. package/{index-0fj4yg7k.js → index-wr999y03.js} +105 -227
  47. package/{index-nfxq39kj.js → index-xe3ychw5.js} +37 -6
  48. package/{index-60a3jfcz.js → index-xrdrttjb.js} +8 -8
  49. package/{index-mn96y1nk.js → index-y59x51f1.js} +6 -6
  50. package/{index-mterpj70.js → index-zgkey4d0.js} +86 -17
  51. package/index.js +20 -19
  52. package/{library.command-aa1g8nsa.js → library.command-7h3yezwc.js} +5 -4
  53. package/{localRegistry.command-kshva1ck.js → localRegistry.command-vnkc41gj.js} +10 -9
  54. package/{module.command-b1zzgqde.js → module.command-4g11k3xd.js} +9 -8
  55. package/{package.command-9qv6vr9s.js → package.command-z7dd6vy2.js} +5 -4
  56. package/package.json +3 -4
  57. package/{page.command-k16pw244.js → page.command-y996708m.js} +5 -4
  58. package/{primitive.command-s60dn1ae.js → primitive.command-4fcn8k83.js} +10 -9
  59. package/{quality.command-p3vaze52.js → quality.command-hx5af2yn.js} +193 -112
  60. package/{recipeScanner-tghkzrrt.js → recipeScanner-zrzev25x.js} +3 -3
  61. package/{repair.command-mfv749ha.js → repair.command-ysxd8pef.js} +7 -6
  62. package/{routeSourceValidator-cncd92fv.js → routeSourceValidator-7fycdqyv.js} +3 -39
  63. package/{scalar.command-hs3nwzv1.js → scalar.command-3chgxybs.js} +8 -7
  64. package/templates/app/env/env.server.debug.ts.template +1 -5
  65. package/templates/app/env/env.server.develop.ts.template +1 -5
  66. package/templates/app/env/env.server.local.ts.template +1 -5
  67. package/templates/app/env/env.server.main.ts.template +1 -5
  68. package/templates/app/page/styles.css.template +3 -3
  69. package/templates/appSample/srvkit/AuthGuard.ts +9 -5
  70. package/templates/appSample/srvkit/SessionInternalArg.ts +4 -3
  71. package/templates/crudPages/[__model__Id]/edit/page.tsx +1 -1
  72. package/templates/crudPages/new/page.tsx +1 -1
  73. package/templates/crudSinglePage/page.tsx +1 -1
  74. package/templates/env/_env.server.type.ts +7 -5
  75. package/templates/lib/db.ts +1 -1
  76. package/templates/lib/dict.ts +1 -1
  77. package/templates/lib/sig.ts +1 -1
  78. package/templates/lib/srv.ts +6 -2
  79. package/templates/module/__Model__.Util.tsx +1 -1
  80. package/templates/module/__Model__.Zone.tsx +3 -5
  81. package/templates/workspaceRoot/AGENTS.md.template +0 -967
  82. package/templates/workspaceRoot/CLAUDE.md.template +19 -0
  83. package/templates/workspaceRoot/biome.json.template +4 -220
  84. package/templates/workspaceRoot/docs/AI-DEVELOPMENT.md.template +15 -16
  85. package/templates/workspaceRoot/docs/GENERATED.md.template +12 -10
  86. package/{typeChecker-kravn7ns.js → typeChecker-g6bh0xfv.js} +1 -1
  87. package/typecheck.proc.js +1 -1
  88. package/{workflow.command-z0e0fk0z.js → workflow.command-endxfzv5.js} +15 -14
  89. package/{workspace.command-3q1d6kk4.js → workspace.command-5f2nmmd5.js} +70 -52
  90. package/templates/workspaceRoot/.cursor/rules/akan-page-routing.mdc.template +0 -43
  91. package/templates/workspaceRoot/.cursor/rules/akan-sync-conventions.mdc.template +0 -34
  92. package/templates/workspaceRoot/.cursor/rules/application-test-commands.mdc.template +0 -16
  93. package/templates/workspaceRoot/.cursor/rules/change-scope.mdc.template +0 -12
  94. package/templates/workspaceRoot/.cursor/rules/client-server-boundaries.mdc.template +0 -18
  95. package/templates/workspaceRoot/.cursor/rules/coding-comments.mdc.template +0 -25
  96. package/templates/workspaceRoot/.cursor/rules/coding-style.mdc.template +0 -44
  97. package/templates/workspaceRoot/.cursor/rules/domain-module-conventions.mdc.template +0 -17
  98. package/templates/workspaceRoot/.cursor/rules/format-lint.mdc.template +0 -12
  99. package/templates/workspaceRoot/.cursor/rules/lint-enforced-rules.mdc.template +0 -18
  100. package/templates/workspaceRoot/.cursor/rules/module-file-playbook.mdc.template +0 -61
  101. package/templates/workspaceRoot/.cursor/rules/naming-conventions.mdc.template +0 -15
  102. package/templates/workspaceRoot/.cursor/rules/react-component-style.mdc.template +0 -21
  103. package/templates/workspaceRoot/.cursor/rules/repo-overview.mdc.template +0 -17
  104. package/templates/workspaceRoot/.cursor/rules/scalar-modeling.mdc.template +0 -58
  105. package/templates/workspaceRoot/.cursor/rules/secrets-and-env.mdc.template +0 -13
  106. package/templates/workspaceRoot/.cursor/rules/service-signal-conventions.mdc.template +0 -83
  107. package/templates/workspaceRoot/.cursor/rules/srvkit-adapters-and-guards.mdc.template +0 -50
  108. package/templates/workspaceRoot/.cursor/rules/ssr-first.mdc.template +0 -59
  109. package/templates/workspaceRoot/.cursor/rules/typescript-imports.mdc.template +0 -16
  110. /package/{index-r24hmh0q.js → index-yb2wsc7f.js} +0 -0
@@ -0,0 +1,638 @@
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
+ Concrete step-by-step recipes for the most frequent Akan.js changes. Each recipe shows which files to edit
206
+ and in what order. The code examples reference the `task` module in `apps/<%= appName %>/lib/task/` as a
207
+ template; replace `task` with your model name and `Task` with your PascalCase model name.
208
+
209
+ When editing a file, always read the existing content first. Only change the relevant sections — do not
210
+ rewrite the entire file.
211
+
212
+ ---
213
+
214
+ ### Recipe 1: Adding a New Field to a Model
215
+
216
+ **Files to edit (in order):** `constant.ts` → `dictionary.ts` → `Template.tsx` → `Unit.tsx` → `akan sync`
217
+
218
+ ```typescript
219
+ // 1. apps/<app>/lib/<model>/<model>.constant.ts
220
+ // Add field to the Input class. Use field() builder with optional defaults
221
+ export class TaskInput extends via((field) => ({
222
+ title: field(String),
223
+ priority: field(TaskPriority, { default: "medium" }), // NEW FIELD
224
+ })) {}
225
+
226
+ // If the new field should appear in list views, also add it to LightTask:
227
+ export class LightTask extends via(TaskObject, ["title", "priority", "status", "due"] as const, () => ({})) {}
228
+
229
+ // 2. apps/<app>/lib/<model>/<model>.dictionary.ts
230
+ // Add i18n labels for the new field (and its enum values if any).
231
+ // Labels are [en, ko] pairs, and nearly every one also carries a .desc([en, ko]).
232
+ .model<Task>((t) => ({
233
+ priority: t(["Priority", "우선순위"]).desc(["How urgent the task is", "할 일의 긴급도"]),
234
+ }))
235
+ .enum<TaskPriority>("taskPriority", (t) => ({
236
+ low: t(["Low", "낮음"]),
237
+ medium: t(["Medium", "보통"]),
238
+ high: t(["High", "높음"]),
239
+ }))
240
+
241
+ // 3. apps/<app>/lib/<model>/<Model>.Template.tsx
242
+ // Add a form field using st.do.setXxxOnYyy (auto-generated setter)
243
+ const form = st.use.taskForm();
244
+ <Field.ToggleSelect
245
+ label={l("task.priority")}
246
+ items={cnst.TaskPriority}
247
+ value={form.priority}
248
+ onChange={st.do.setPriorityOnTask}
249
+ />
250
+
251
+ // 4. apps/<app>/lib/<model>/<Model>.Unit.tsx
252
+ // Display the new field in card/list views
253
+ <Badge variant={task.priority === "high" ? "error" : task.priority === "medium" ? "warning" : "default"}>
254
+ {task.priority}
255
+ </Badge>
256
+
257
+ // 5. Regenerate barrels
258
+ // akan sync <name>
259
+ ```
260
+
261
+ ---
262
+
263
+ ### Recipe 2: Injecting a Dependency into a Service
264
+
265
+ Three patterns: injecting an **external adapter** (`use<>()`), another **module's service** (`service<>()`),
266
+ or a **predefined framework adapter** (`plug()`). A field named `<refName>Service` resolves to the service
267
+ registered under `<refName>` — the `Service`/`Signal` suffix is required and stripped to derive the lookup key.
268
+
269
+ > **For a new adapter you own, prefer the `adapt()` shape in pattern C over the `option.ts` registration in
270
+ > pattern A.** An `adapt()` class self-registers and is injected with `plug(TheClass)`, so it never touches
271
+ > `option.ts`. Pattern A is the legacy constructor-style shape: recognise it, keep it working, and migrate one
272
+ > only when you are already changing it.
273
+
274
+ > `apps/<app>/lib/option.ts` is a **user-owned** file scaffolded once — edit it to register adapters/DI. Unlike the
275
+ > barrels (`cnst.ts`, `db.ts`, `srv.ts`, …) it is **not** overwritten by `akan sync`, so your `.use(...)` registrations
276
+ > are safe.
277
+
278
+ **A. Adapter injection via `use<>()` (for external clients / global singletons)**
279
+
280
+ ```typescript
281
+ // 1. Create the adapter class in apps/<app>/srvkit/
282
+ // apps/<app>/srvkit/EmailClient.ts
283
+ export class EmailClient {
284
+ constructor(readonly apiKey: string) {}
285
+ async send(opts: { to: string; subject: string; body: string }) { /* ... */ }
286
+ }
287
+
288
+ // 2. Register in apps/<app>/lib/option.ts
289
+ export const option = new AkanOption()
290
+ .use((options) => ({
291
+ emailClient: new EmailClient(options.mailerApiKey),
292
+ }));
293
+
294
+ // 3. Inject via use<>() in <model>.service.ts
295
+ export class TaskService extends serve(db.task, ({ use }) => ({
296
+ emailClient: use<EmailClient>(),
297
+ })) {
298
+ async _postCreate(task: cnst.Task) {
299
+ await this.emailClient.send({ to: "...", subject: "Task Created", body: `Task "${task.title}" created.` });
300
+ }
301
+ }
302
+ ```
303
+
304
+ **B. Cross-module service injection via `service<>()` (for other Akan services)**
305
+
306
+ ```typescript
307
+ // In <model>.service.ts — inject another module's service
308
+ import * as srv from "../srv";
309
+
310
+ export class TaskService extends serve(db.task, ({ service }) => ({
311
+ notiService: service<srv.NotiService>(),
312
+ })) {
313
+ async _postCreate(task: cnst.Task) {
314
+ await this.notiService.send("info", `Task "${task.title}" created`);
315
+ }
316
+ }
317
+ ```
318
+
319
+ **C. Predefined framework adapter injection via `plug()` (storage, cache, queue, schedule, …)**
320
+
321
+ Akan ships predefined adapter roles from `akanjs/service`: `StorageAdaptorRole`, `CacheAdaptorRole`,
322
+ `QueueAdaptorRole`, `ScheduleAdaptorRole`, `DatabaseAdaptorRole`, `WebsocketAdaptorRole`,
323
+ `LoggingAdaptorRole`, `CompressAdaptorRole`. `plug()` injects the concrete adapter bound to that role (the
324
+ default `StorageAdaptor` binding is `BlobStorage`). `plug()` also accepts a concrete adapter class directly.
325
+
326
+ ```typescript
327
+ // In <model>.service.ts — inject the framework storage adapter by role
328
+ import { plug, serve, StorageAdaptorRole } from "akanjs/service";
329
+
330
+ export class TaskService extends serve(db.task, ({ plug }) => ({
331
+ storage: plug(StorageAdaptorRole),
332
+ })) {
333
+ async attach(taskId: string, path: string, localPath: string) {
334
+ // BlobStorage returns a URL under blobStorage.urlPrefix (default "/api/localFile/getBlob").
335
+ return await this.storage.uploadDataFromLocal({ path, localPath });
336
+ }
337
+ }
338
+ ```
339
+
340
+ For a custom adapter class (not a predefined role), pass the class itself, e.g. `ipfsApi: plug(IpfsApi)`.
341
+ Injecting a file/image field is usually simpler than calling
342
+ storage directly: declare `image: field(File).optional()` (or `images: field([File])`) on the model and let the
343
+ store's generated `upload<Field>On<Model>(fileList)` action handle the upload. Add `{ cascade: "removeRef" }` to that
344
+ field when the file belongs to the model alone, and removing the model removes the file and its stored object.
345
+
346
+ ---
347
+
348
+ ### Recipe 3: Creating and Using a Slice
349
+
350
+ A Slice is a named, filtered data view. Add file entries and connect from a page.
351
+
352
+ > **Silent failure — a slice `exec` must return a query descriptor, never an executed list.**
353
+ > Return `this.taskService.queryByStatuses(...)` (the `query<Filter>` builder), **not**
354
+ > `this.taskService.listByStatuses(...)` / `listBy...(...)` (which returns a `Promise<Doc[]>`).
355
+ > Returning an array type-checks but throws at runtime during insight aggregation with the opaque
356
+ > `Error: Unknown document field path: 0`. If you see that error, your slice is returning a list, not a query.
357
+
358
+ ```typescript
359
+ // 1. apps/<app>/lib/<model>/<model>.signal.ts — Define the slice
360
+ export class TaskSlice extends slice(srv.task, { guards: { root: Admin, get: SignedIn, cru: SignedIn } }, (init) => ({
361
+ inTodo: init({ guards: [SignedIn] })
362
+ .search("statuses", [cnst.TaskStatus])
363
+ .exec(function (statuses?) {
364
+ // ✅ query<Filter> — a query descriptor. ❌ listByStatuses(...) returns an array and fails at runtime.
365
+ return this.taskService.queryByStatuses(statuses ?? ["todo", "inProgress"]);
366
+ }),
367
+ })) {}
368
+
369
+ // 2. apps/<app>/lib/<model>/<model>.document.ts — Add query filter for slice
370
+ export class TaskFilter extends from(cnst.Task, (filter) => ({
371
+ query: {
372
+ byStatuses: filter()
373
+ .arg("statuses", [cnst.TaskStatus])
374
+ .query((statuses) => ({ status: { $in: statuses } })),
375
+ },
376
+ })) {}
377
+
378
+ // 3. apps/<app>/lib/<model>/<model>.dictionary.ts — Slice labels
379
+ .slice<TaskSlice>((fn) => ({
380
+ inTodo: fn(["Tasks In Todo", "할 일"]).arg((t) => ({
381
+ statuses: t(["Statuses", "상태"]),
382
+ })),
383
+ }))
384
+
385
+ // 4. In page — Init the slice in an async Page and hand the init to a Zone.
386
+ export default async function Page() {
387
+ const [{ taskInitInTodo }] = await Promise.all([fetch.initTaskInTodo()]);
388
+ return <Task.Zone.Card init={taskInitInTodo} sliceName="taskInTodo" />;
389
+ }
390
+ ```
391
+
392
+ The slice name in code uses camelCase (`inTodo`). In dictionary and components it becomes `"taskInTodo"`.
393
+
394
+ ---
395
+
396
+ ### Recipe 4: Creating a Mutation Endpoint (with Status Workflow)
397
+
398
+ **Files to edit (in order):** `document.ts` → `service.ts` → `signal.ts` → `dictionary.ts` → `store.ts` → `Util.tsx`
399
+
400
+ ```typescript
401
+ // 1. <model>.document.ts — Document chain method with state validation
402
+ export class TaskDocument extends by(cnst.Task) {
403
+ start() {
404
+ if (this.status !== "todo") throw new Err("task.error.cannotStartFromNonTodo");
405
+ this.status = "inProgress";
406
+ return this; // Return this for chaining: task.start().save()
407
+ }
408
+ }
409
+
410
+ // 2. <model>.service.ts — Service method wrapping document
411
+ async startTask(taskId: string) {
412
+ const task = await this.getTask(taskId);
413
+ return task.start().save();
414
+ }
415
+
416
+ // 3. <model>.signal.ts — Mutation endpoint
417
+ export class TaskEndpoint extends endpoint(srv.task, ({ mutation }) => ({
418
+ startTask: mutation(cnst.Task, { guards: [SignedIn] })
419
+ .param("taskId", ID)
420
+ .exec(async function (taskId) {
421
+ return await this.taskService.startTask(taskId);
422
+ }),
423
+ })) {}
424
+
425
+ // 4. <model>.dictionary.ts — Endpoint + error labels
426
+ .endpoint<TaskEndpoint>((fn) => ({
427
+ startTask: fn(["Start Task", "작업시작"])
428
+ .arg((t) => ({ taskId: t(["Task ID", "할 일 ID"]) })),
429
+ }))
430
+ .error({
431
+ cannotStartFromNonTodo: ["Task can only start from todo status", "할 일 상태에서만 시작 가능"],
432
+ })
433
+
434
+ // 5. <model>.store.ts — Client-side action with toast feedback
435
+ async startTask(taskId: string) {
436
+ msg.loading("task.startTaskLoading", { key: "startTask" });
437
+ const task = await fetch.startTask(taskId);
438
+ this.setTask(task); // Auto-generated: updates task state in store
439
+ msg.success("task.startTaskSuccess", { key: "startTask" });
440
+ }
441
+
442
+ // 6. <Model>.Util.tsx — Reusable button component
443
+ export const Start = ({ taskId }: { taskId: string }) => (
444
+ <button className={buttonRecipe({ variant: "primary", size: "xs" })} onClick={() => st.do.startTask(taskId)}>
445
+ Start
446
+ </button>
447
+ );
448
+ ```
449
+
450
+ ---
451
+
452
+ ### Recipe 5: Internal Triggers — Interval & Cron
453
+
454
+ Server-side background jobs. Defined in `internal()` signal, implemented in service.
455
+
456
+ ```typescript
457
+ // 1. <model>.signal.ts — Define triggers
458
+ export class TaskInternal extends internal(srv.task, ({ interval, cron }) => ({
459
+ cleanupStaleTasks: interval(10000).exec(async function () {
460
+ await this.taskService.cleanupStaleTasks();
461
+ }),
462
+
463
+ dailyDigest: cron("0 0 * * *").exec(async function () {
464
+ await this.taskService.sendDailyDigest();
465
+ }),
466
+ })) {}
467
+
468
+ // 2. <model>.service.ts — Implement the logic
469
+ async cleanupStaleTasks() {
470
+ const weekAgo = dayjs().subtract(7, "day").toDate();
471
+ const stale = await this.taskModel.listDueBefore(weekAgo);
472
+ for (const task of stale) {
473
+ await task.remove();
474
+ }
475
+ }
476
+ ```
477
+
478
+ **Available trigger types:**
479
+ - `interval(ms)` — runs repeatedly at the given interval
480
+ - `cron("min hour dom month dow")` — runs on a schedule
481
+ - `initialize()` — runs once on service startup
482
+ - `process(Return).msg(Type)` — message queue consumer
483
+
484
+ ---
485
+
486
+ ### Recipe 6: Creating an Insight (Aggregation / Dashboard Stats)
487
+
488
+ Insights display aggregated statistics across model data.
489
+
490
+ ```typescript
491
+ // 1. <model>.constant.ts — Insight class with accumulate rules
492
+ export class TaskInsight extends via(Task, (field) => ({
493
+ totalCount: field(Int, { default: 0, accumulate: {} }),
494
+ completedCount: field(Int, { default: 0, accumulate: { status: "completed" } }),
495
+ })) {}
496
+
497
+ // 2. <model>.dictionary.ts — Insight field labels
498
+ .insight<TaskInsight>((t) => ({
499
+ totalCount: t(["Total Tasks", "전체 할 일"]),
500
+ completedCount: t(["Completed", "완료됨"]),
501
+ }))
502
+
503
+ // 3. <Model>.View.tsx — Display component consuming an Insight model
504
+ export const Stats = ({ taskInsight }: { taskInsight: cnst.TaskInsight }) => (
505
+ <div className="grid grid-cols-2 gap-4">
506
+ <div className="rounded-lg border border-border bg-card p-4">
507
+ <div className="text-muted-foreground text-sm">{l("task.totalCount")}</div>
508
+ <div className="font-bold text-2xl text-primary">{taskInsight.totalCount}</div>
509
+ </div>
510
+ <div className="rounded-lg border border-border bg-card p-4">
511
+ <div className="text-muted-foreground text-sm">{l("task.completedCount")}</div>
512
+ <div className="font-bold text-2xl text-success">{taskInsight.completedCount}</div>
513
+ </div>
514
+ </div>
515
+ );
516
+
517
+ // 4. <Model>.Zone.tsx — Mount in page via Load.Insight bound to a slice
518
+ export const Insight = ({ sliceName }: { sliceName: string }) => {
519
+ const insight = st.slice[sliceName].use.taskInsight();
520
+ if (!insight) return null;
521
+ return <Task.View.Stats taskInsight={insight} />;
522
+ };
523
+ ```
524
+
525
+ ---
526
+
527
+ ### Data Flow Summary
528
+
529
+ For each business question, follow this chain:
530
+
531
+ | Question | File Pattern |
532
+ |----------|-------------|
533
+ | What fields does it have? | `constant.ts` — `via()` layers: Input → Object → Light → Model → Insight |
534
+ | How is it stored/searched? | `document.ts` — `from()` filters + `by()` document methods + `into()` model |
535
+ | What business rule should run? | `service.ts` — `serve()` with DI (`use`, `service`, `plug`, `env`, `memory`) |
536
+ | What should a page call? | `signal.ts` — `internal()` (jobs), `endpoint()` (APIs), `slice()` (data views) |
537
+ | What client state is shared? | `store.ts` — `store()` with auto-generated form/insight state + custom actions |
538
+ | What should users see? | `View.tsx` + `Zone.tsx` (detail/container), `Template.tsx` (forms), `Unit.tsx` (cards), `Util.tsx` (buttons) |
539
+
540
+ ## Modeling & Query Gotchas
541
+
542
+ A short list of things the type system does not always catch. The full rules for text search, `cascade`, and the
543
+ query-level writes are in the convention set above; these are the shapes that build green and fail later.
544
+
545
+ - **Slices return a query, not a list.** A slice `exec` must return `this.<model>Service.query<Filter>(...)`, never
546
+ a `list<Filter>(...)` / `listBy...(...)` array. Returning an array type-checks but fails at runtime with
547
+ `Unknown document field path: 0`. (See Recipe 3.)
548
+ - **Custom endpoint names must not collide with generated CRUD.** `create/update/remove/view/edit/merge<Model>`
549
+ already exist. A collision can build green and fail only at runtime — pick a distinct verb.
550
+ - **Numbers are `Int` or `Float`, never `Number`.** `field(Number)` / `.body("x", Number)` fail to typecheck.
551
+ - **Array fields use `field([T])`** — `tags: field([String])`, `images: field([File])`.
552
+ - **Reading a secret field needs an explicit select.** `field(...).secret()` values are stripped from query results
553
+ by default: `this.userModel.pickById(id, { select: { passwordHash: true } })`.
554
+ - **`cascade` names a direction, and the wrong one is a data loss** — `removeRef` on the relation the owner holds,
555
+ `removeWith` on the child's reference to its owner. A query-level removal fires no hooks and therefore no cascade.
556
+ - **`q.search()` is a filter node, not a slice requirement.** `bySearch: filter().arg("text", String).query((text,
557
+ q) => q.search(text, { prefix: true }))` generates `listBySearch` / `countBySearch` / `queryBySearch` /
558
+ `insightBySearch` for free. Only add a search *slice* when the model's data is safe to enumerate.
559
+
560
+ ## Current User, Guards & Auth-Gated Pages
561
+
562
+ Built-in user authentication (session / JWT / password hashing) ships as a separate Akan auth library, not in the
563
+ core framework. The core framework gives you the composition points below; wire the auth library through them.
564
+
565
+ - **A `slice()` takes its guard map as a second argument**, `slice(srv.task, { guards: { root: Admin, get: SignedIn,
566
+ cru: SignedIn } }, (init) => ({...}))`. An `endpoint()` takes no guard argument at all —
567
+ `endpoint(srv.task, ({ mutation }) => ({...}))` — so **every custom `mutation` / `query` / `message` names its own
568
+ `guards: [...]` array**: `mutation(cnst.Task, { guards: [SignedIn] })`. `Public` always allows; other guards
569
+ implement the `Guard` interface in `srvkit/` (server-only).
570
+ - **Read the caller inside a guard** with `context.get<T>("account")`, which works on HTTP and websocket calls
571
+ alike — never branch on `getHttpContext()` / `getWebSocketContext()`. Every guard class also declares
572
+ `static scope: GuardScope`, `"account"` or `"resource"`, with no default.
573
+ - **Read the current user inside a custom endpoint** by injecting an `InternalArg` with `.with(...)`:
574
+ `mutation(cnst.Task, { guards: [SignedIn] }).with(CurrentUserId).exec(async function (currentUserId) { ... })`.
575
+ - **Auto-generated CRUD and `serve()` service methods / lifecycle hooks do not receive session context.** If an
576
+ operation needs the acting user, expose a custom endpoint that takes it via `.with(CurrentUserId)` — never trust a
577
+ client-supplied user id.
578
+ - **SSR auth-gated pages: guard at the layout.** Check the session in the `_layout.tsx` loader and redirect when it is
579
+ absent, so nested pages never render for signed-out users.
580
+
581
+ ## Auto-Generated API Reference
582
+
583
+ akan sync automatically generates APIs across all layers. Only write custom logic — never hand-write what the framework generates.
584
+
585
+ ### Signal — Endpoint Auto-Generation
586
+
587
+ | Auto-Generated | Signature | Description |
588
+ |---------------|-----------|-------------|
589
+ | `view[Model](id)` | `fetch.viewTask(id)` | Fetch single model for detail view |
590
+ | `edit[Model](id)` | `fetch.editTask(id)` | Fetch model for edit view |
591
+ | `merge[Model](id, data)` | `fetch.mergeTask(data)` | Create (no id) or update (with id) model |
592
+ | `[model]List[Suffix](args, skip, limit, sort)` | `fetch.taskListInTodo(args)` | Paginated list from slice filter |
593
+ | `[model]Insight[Suffix](args)` | `fetch.taskInsightInTodo(args)` | Aggregated insight from slice query |
594
+ | `init[Model][Suffix](args)` | `fetch.initTaskInTodo(args)` | Initialize slice with list + insight |
595
+
596
+ **Rule**: Only define `query()`, `mutation()`, `message()`, `pubsub()` endpoints manually when the endpoint needs custom business logic. Standard CRUD is already auto-generated.
597
+
598
+ ### Service — CRUD & Query Auto-Generation
599
+
600
+ | Auto-Generated | Description |
601
+ |---------------|-------------|
602
+ | `this.<model>Model` | Auto-injected model adaptor |
603
+ | `get<Model>(id)`, `load<Model>(id)` | Single document lookup |
604
+ | `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`) |
605
+ | `list<Query>(args)`, `find<Query>(args)`, `pick<Query>(args)` | Filter-based queries |
606
+ | `exists<Query>(args)`, `count<Query>(args)`, `insight<Query>(args)` | Filter-based helpers |
607
+ | `_preCreate`, `_postCreate`, `_preUpdate`, `_postUpdate`, `_preRemove`, `_postRemove` | Lifecycle hooks (override to add logic) |
608
+
609
+ **Rule**: Use `_preCreate`/`_postCreate` lifecycle hooks for side effects (e.g., push workHistory entries). Write custom service methods only for multi-model orchestration.
610
+
611
+ ### Store — State & Action Auto-Generation
612
+
613
+ | Auto-Generated | Description |
614
+ |---------------|-------------|
615
+ | `[model]` (cached full model), `[model]Loading`, `[model]Form`, `[model]Modal` | Base model states |
616
+ | `create[Model](data)`, `update[Model](id, data)`, `remove[Model](id)` | CRUD actions |
617
+ | `new[Model](partial)`, `edit[Model](model)`, `view[Model](model)` | Form/view state actions |
618
+ | `[slice]List`, `[slice]InitList`, `[slice]Insight`, `[slice]Selection` | Slice states |
619
+ | `init[Slice](args)`, `refresh[Slice]()`, `setPageOf[Slice](page)` | Slice actions |
620
+ | `set[Field]On[Model](value)` | Auto-setters for each model field |
621
+
622
+ **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.
623
+
624
+ ### Document — Filter Query Auto-Generation
625
+
626
+ | Auto-Generated (from Filter definition) | Description |
627
+ |----------------------------------------|-------------|
628
+ | `list[Query](args)`, `listIds[Query](args)` | List documents matching filter |
629
+ | `find[Query](args)`, `findId[Query](args)` | Find one (null if not found) |
630
+ | `pick[Query](args)`, `pickId[Query](args)` | Find one (throw if not found) |
631
+ | `exists[Query](args)`, `count[Query](args)` | Existence check and count |
632
+ | `insight[Query](args)`, `query[Query](args)` | Insight and raw query |
633
+ | `remove[Query](args)`, `removeOne[Query](args)` | Query-level soft remove — all matches, or the newest one (`createdAt` desc, not caller-chosen) |
634
+ | `update[Query](args).set(patch)`, `updateOne[Query](args).set(patch)` | Query-level update — the patch lands on a terminal `.set()`, because a filter's trailing args may be optional |
635
+
636
+ **Rule**: Define `Filter` with `.query()` conditions in `document.ts`. akan sync auto-generates all 14 query helper methods per filter. Write `Document` chain methods only for state transitions with validation.
637
+
638
+ **The four query-level writes fire no hooks**, so no `_pre`/`_postRemove` and no cascade run — same as `updateManyByQuery`. Reach for them when the model carries no removal side effect; otherwise remove documents one at a time. A filter keyed after its own model (filter `chat` on model `chat`) is rejected at boot, because `removeChat`/`updateChat` would otherwise shadow the generated single-document CRUD.