@akanjs/cli 2.3.1-rc.2 → 2.3.1-rc.4

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 (37) hide show
  1. package/index.js +9 -2
  2. package/package.json +2 -2
  3. package/templates/appSample/common/formatters.ts +32 -0
  4. package/templates/appSample/common/validators.ts +32 -0
  5. package/templates/appSample/lib/__scalar/workHistory/workHistory.abstract.ts +47 -0
  6. package/templates/appSample/lib/__scalar/workHistory/workHistory.constant.ts +38 -0
  7. package/templates/appSample/lib/__scalar/workHistory/workHistory.dictionary.ts +28 -0
  8. package/templates/appSample/lib/_noti/noti.abstract.ts +38 -0
  9. package/templates/appSample/lib/_noti/noti.dictionary.ts +25 -0
  10. package/templates/appSample/lib/_noti/noti.service.ts +31 -0
  11. package/templates/appSample/lib/_noti/noti.signal.ts +30 -0
  12. package/templates/appSample/lib/_noti/noti.store.ts +38 -0
  13. package/templates/appSample/lib/task/Task.Template.tsx +49 -0
  14. package/templates/appSample/lib/task/Task.Unit.tsx +70 -0
  15. package/templates/appSample/lib/task/Task.Util.tsx +103 -0
  16. package/templates/appSample/lib/task/Task.View.tsx +94 -0
  17. package/templates/appSample/lib/task/Task.Zone.tsx +66 -0
  18. package/templates/appSample/lib/task/task.abstract.ts +40 -0
  19. package/templates/appSample/lib/task/task.constant.ts +54 -0
  20. package/templates/appSample/lib/task/task.dictionary.ts +85 -0
  21. package/templates/appSample/lib/task/task.document.ts +54 -0
  22. package/templates/appSample/lib/task/task.service.ts +48 -0
  23. package/templates/appSample/lib/task/task.signal.ts +49 -0
  24. package/templates/appSample/lib/task/task.store.ts +45 -0
  25. package/templates/appSample/page/task/[taskId]/_index.tsx +39 -0
  26. package/templates/appSample/page/task/[taskId]/edit.tsx +34 -0
  27. package/templates/appSample/page/task/_index.tsx +38 -0
  28. package/templates/appSample/page/task/_layout.tsx +24 -0
  29. package/templates/appSample/page/task/new.tsx +34 -0
  30. package/templates/appSample/srvkit/AuthGuard.ts +35 -0
  31. package/templates/appSample/srvkit/SessionInternalArg.ts +32 -0
  32. package/templates/appSample/ui/GlobalLoading.tsx +30 -0
  33. package/templates/appSample/ui/QuantityControl.tsx +52 -0
  34. package/templates/appSample/webkit/useDebounce.ts +41 -0
  35. package/templates/workspaceRoot/AGENTS.md.template +470 -16
  36. package/templates/workspaceRoot/package.json.template +4 -1
  37. package/templates/workspaceRoot/patches/react-dom@19.2.7.patch.template +62 -0
@@ -7,7 +7,7 @@ the framework generate the repeated surfaces.
7
7
 
8
8
  - `apps/<app>` contains application pages, app UI, app domain modules, env files, and `akan.config.ts`.
9
9
  - `libs/<lib>` contains shared domain and utility code reused by apps.
10
- - `apps/<app>/page` contains file-routed pages. Index pages use `_index.tsx`; nested layouts use `_layout.tsx`.
10
+ - `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].
11
11
  - `apps/<app>/lib/<model>` contains database-backed domain modules.
12
12
  - `apps/<app>/lib/_<service>` contains service modules that are not database document models.
13
13
  - `apps/<app>/lib/__scalar/<scalar>` contains reusable scalar/value types.
@@ -20,10 +20,6 @@ Do not hand-edit generated Akan files. Regenerate them through Akan scan, lint,
20
20
  CLI command instead.
21
21
 
22
22
  Common generated files include:
23
-
24
- - `apps/*/akan.app.json`
25
- - `libs/*/akan.lib.json`
26
- - `pkgs/*/akan.pkg.json`
27
23
  - `apps/*/client.ts`
28
24
  - `apps/*/server.ts`
29
25
  - `apps/*/lib/cnst.ts`
@@ -34,11 +30,11 @@ Common generated files include:
34
30
  - `apps/*/lib/sig.ts`
35
31
  - `apps/*/lib/useClient.ts`
36
32
  - `apps/*/lib/useServer.ts`
33
+ - `apps/*/lib/*/index.ts`
37
34
  - `apps/*/ui/index.ts`
38
35
  - `apps/*/webkit/index.ts`
39
36
  - `apps/*/srvkit/index.ts`
40
37
  - `apps/*/common/index.ts`
41
- - module `index.ts` and `index.tsx` files generated by scan
42
38
 
43
39
  ## Domain Module Responsibilities
44
40
 
@@ -52,11 +48,11 @@ Use the local module shape before adding a new abstraction.
52
48
  - `<model>.document.ts` owns persistence and document queries.
53
49
  - `<model>.service.ts` owns business logic.
54
50
  - `<model>.store.ts` owns domain state and actions.
55
- - `<Model>.Template.tsx` owns form-oriented UI.
56
- - `<Model>.Unit.tsx` owns list/item UI.
57
- - `<Model>.View.tsx` owns detail UI.
58
- - `<Model>.Zone.tsx` owns page/container integration.
59
- - `<model>.Util.tsx` owns small module UI helpers.
51
+ - `<Model>.Template.tsx` owns form-oriented UI. Client components, with 'use client'.
52
+ - `<Model>.Unit.tsx` owns list/item UI. Server components, no 'use client'.
53
+ - `<Model>.View.tsx` owns detail UI. Server components, no 'use client'.
54
+ - `<Model>.Zone.tsx` owns page/container integration. Client components, with 'use client'.
55
+ - `<model>.Util.tsx` owns small module UI helpers. Client components, with 'use client'.
60
56
 
61
57
  ## Agent Workflow
62
58
 
@@ -73,12 +69,43 @@ Use the local module shape before adding a new abstraction.
73
69
 
74
70
  Run commands from the workspace root unless a task says otherwise.
75
71
 
72
+ ### Module Addition Workflow
73
+
74
+ When adding a new database-backed domain module (e.g., product, user):
75
+
76
+ ```bash
77
+ # 1. Scaffold the module with Akan CLI (creates constant, service, signal, store, document files)
78
+ akan create-module <module-name> --app <%= appName %>
79
+
80
+ # 2. Start dev server with HMR and type checking at http://localhost:8282
81
+ akan start <%= appName %>
82
+ ```
83
+
84
+ ### Change Verification Workflow
85
+
86
+ After any code change, run these in order:
87
+
76
88
  ```bash
77
- akan start <app-name>
78
- akan build <app-name>
79
- akan test <app-or-lib-or-pkg>
80
- akan lint <app-or-lib-or-pkg>
81
- akan lint-all
89
+ # 1. Fast lint check — Akan.js conventions and Biome rules
90
+ akan lint <%= appName %>
91
+
92
+ # 2. Type-only check — catches server/client boundary violations and import errors
93
+ akan typecheck <%= appName %>
94
+
95
+ # 3. Test — Run the test code (lib/*/*.signal.test.ts or others)
96
+ akan test <%= appName %>
97
+
98
+ # 4. Full production build — bundles the app, runs all type/lint checks combined
99
+ akan build <%= appName %>
100
+ ```
101
+
102
+ ### Other Frequently Used Commands
103
+
104
+ ```bash
105
+ akan create-scalar <scalar-name> --app <%= appName %> # Add a scalar module (lib/__scalar/<scalar-name>/)
106
+ akan create-service <service-name> --app <%= appName %> # Add a service module (lib/_<service-name>/)
107
+ akan test <%= appName %> # Run the test code (lib/*/*.signal.test.ts or others)
108
+ akan lint <%= appName %> # Lint only (no typecheck)
82
109
  ```
83
110
 
84
111
  For the default generated app, start with:
@@ -86,3 +113,430 @@ For the default generated app, start with:
86
113
  ```bash
87
114
  akan start <%= appName %>
88
115
  ```
116
+
117
+ ### The Essential Loop: Edit → Scan → Check
118
+
119
+ Almost every Akan.js change follows this pattern. **Missing step 2 is the #1 cause of agent confusion.**
120
+
121
+ 1. **Edit** — Create or modify source files in the correct location.
122
+ ```
123
+ vi apps/<%= appName %>/lib/task/task.constant.ts # e.g., add a priority field
124
+ ```
125
+
126
+ 2. **Scan** — Regenerate barrel files so Akan discovers your change. This regenerates:
127
+ `cnst.ts`, `db.ts`, `srv.ts`, `sig.ts`, `st.ts`, `dict.ts`, `useClient.ts`, `useServer.ts`,
128
+ `ui/index.ts`, `webkit/index.ts`, `srvkit/index.ts`, `common/index.ts`, and all module `index.ts` files.
129
+ ```
130
+ akan scan app <%= appName %>
131
+ ```
132
+ **CRITICAL**: Scan after EVERY file add, delete, or rename. Without scan, other modules cannot
133
+ `import * as cnst from "../cnst"` and find your new model.
134
+
135
+ 3. **Check** — Verify your change compiles and lints.
136
+ ```
137
+ akan start <%= appName %> # dev server with live feedback (preferred)
138
+ akan lint <%= appName %> # quick lint-only check
139
+ ```
140
+
141
+ If `akan scan` gives errors, try:
142
+ - `akan build <%= appName %>` — full rebuild catches type errors scan may miss
143
+ - Re-run `akan create-model <name> --app <%= appName %>` if the scaffold is corrupted
144
+
145
+ ## Quick Decision Matrix — "Where do I put this code?"
146
+
147
+ | You want to... | Create in... | Run after... |
148
+ |----------------|-------------|--------------|
149
+ | Define a new database-backed noun (e.g., User, Product) | `lib/<model>/` → constant, document, service, signal, store, dictionary, abstract | `akan scan app <name>` |
150
+ | Add a pure workflow / integration (e.g., Payment, Email) | `lib/_<service>/` → service, signal, store, dictionary, abstract | `akan scan app <name>` |
151
+ | Add a reusable value type (e.g., Address, WorkHistory) | `lib/__scalar/<type>/` → constant, dictionary, abstract | `akan scan app <name>` |
152
+ | Create a new URL-visitable page | `page/` → `_index.tsx`, `_layout.tsx`, `[param]/_index.tsx` | Rebuild (akan start auto-detects) |
153
+ | Add a form or reusable UI component | `ui/` → PascalCase `.tsx` with `"use client"` if needed | `akan scan app <name>` |
154
+ | Add a React hook or browser helper | `webkit/` → camelCase `.ts` with `"use client"` | `akan scan app <name>` |
155
+ | Add a server-only guard, middleware, or adaptor | `srvkit/` → PascalCase `.ts` | `akan scan app <name>` |
156
+ | Add a pure helper (no DOM, no server API) | `common/` → camelCase `.ts` | `akan scan app <name>` |
157
+
158
+ ## Anti-patterns: Never Do These
159
+
160
+ | Don't | Why | Do Instead |
161
+ |-------|-----|------------|
162
+ | 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 scan`**. Your changes will be overwritten. | Edit the source files in `lib/<model>/` directories and run `akan scan` |
163
+ | Create a file without running scan | New files won't appear in barrel exports. Imports like `import * as cnst from "../cnst"` will fail. | Always run `akan scan app <name>` after creating, renaming, or deleting any module file |
164
+ | Use JS `#private` methods in service classes | Akan's build system rejects `#private`. Use TypeScript `private` keyword instead. | `private _methodName()` — never `#_methodName()` |
165
+ | 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 |
166
+ | 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 |
167
+ | Skip running `akan scan` after deleting a file | Deleted files remain referenced in barrel exports, causing import errors everywhere. | Run `akan scan app <name>` after every file add, remove, or rename |
168
+ | 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 |
169
+ | 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>` |
170
+
171
+ ## Generated File Tracker (Quick Reference)
172
+
173
+ These files are regenerated by `akan scan` and overwritten on every scan. **Do not hand-edit them.**
174
+
175
+ | File | Generated From | Purpose |
176
+ |------|---------------|---------|
177
+ | `apps/*/lib/cnst.ts` | All `*/lib/*/**.constant.ts` | Barrel for all constants |
178
+ | `apps/*/lib/dict.ts` | All `*/lib/*/**.dictionary.ts` | Barrel for all dictionaries |
179
+ | `apps/*/lib/db.ts` | All `*/lib/<model>/*.document.ts` | Barrel for all document models |
180
+ | `apps/*/lib/srv.ts` | All `*/lib/**/**.service.ts` | Barrel for all services |
181
+ | `apps/*/lib/sig.ts` | All `*/lib/**/**.signal.ts` | Barrel for all signals |
182
+ | `apps/*/lib/st.ts` | All `*/lib/**/**.store.ts` | Barrel for all stores |
183
+ | `apps/*/lib/useClient.ts` | Client-safe module re-exports | Client-side import entry |
184
+ | `apps/*/lib/useServer.ts` | Server-only module re-exports | Server-side import entry |
185
+ | `apps/*/client.ts` | App-wide client barrel | The `fetch` and `st` instances |
186
+ | `apps/*/server.ts` | App-wide server barrel | Server-side service resolution |
187
+ | `*/lib/*/index.ts` | Per-module barrel | Module-level re-exports |
188
+ | `*/ui/index.ts` | All UI component files | UI barrel |
189
+ | `*/webkit/index.ts` | All webkit files | Webkit barrel |
190
+ | `*/srvkit/index.ts` | All srvkit files | Srvkit barrel |
191
+ | `*/common/index.ts` | All common files | Common barrel |
192
+
193
+ ## Workflow Recipes
194
+
195
+ Concrete step-by-step recipes for the most frequent Akan.js changes. Each recipe shows which files to edit
196
+ and in what order. The code examples reference the `task` module in `apps/<%= appName %>/lib/task/` as a
197
+ template; replace `task` with your model name and `Task` with your PascalCase model name.
198
+
199
+ When editing a file, always read the existing content first. Only change the relevant sections — do not
200
+ rewrite the entire file.
201
+
202
+ ---
203
+
204
+ ### Recipe 1: Adding a New Field to a Model
205
+
206
+ **Files to edit (in order):** `constant.ts` → `dictionary.ts` → `Template.tsx` → `Unit.tsx` → `akan scan`
207
+
208
+ ```typescript
209
+ // 1. apps/<app>/lib/<model>/<model>.constant.ts
210
+ // Add field to the Input class. Use field() builder with optional defaults
211
+ export class TaskInput extends via((field) => ({
212
+ title: field(String),
213
+ priority: field(TaskPriority, { default: "medium" }), // NEW FIELD
214
+ })) {}
215
+
216
+ // If the new field should appear in list views, also add it to LightTask:
217
+ export class LightTask extends via(TaskObject, ["title", "priority", "status", "due"] as const, () => ({})) {}
218
+
219
+ // 2. apps/<app>/lib/<model>/<model>.dictionary.ts
220
+ // Add i18n labels for the new field (and its enum values if any)
221
+ .model<Task>((t) => ({
222
+ priority: t(["Priority", "우선순위"]),
223
+ }))
224
+ .enum<TaskPriority>("taskPriority", (t) => ({
225
+ low: t(["Low", "낮음"]),
226
+ medium: t(["Medium", "보통"]),
227
+ high: t(["High", "높음"]),
228
+ }))
229
+
230
+ // 3. apps/<app>/lib/<model>/<Model>.Template.tsx
231
+ // Add a form field using st.do.setXxxOnYyy (auto-generated setter)
232
+ const form = st.use.taskForm();
233
+ <Field.ToggleSelect
234
+ label={l("task.priority")}
235
+ items={cnst.TaskPriority}
236
+ value={form.priority}
237
+ onChange={st.do.setPriorityOnTask}
238
+ />
239
+
240
+ // 4. apps/<app>/lib/<model>/<Model>.Unit.tsx
241
+ // Display the new field in card/list views
242
+ <span className={clsx("badge badge-sm", {
243
+ "badge-error": task.priority === "high",
244
+ "badge-warning": task.priority === "medium",
245
+ "badge-ghost": task.priority === "low",
246
+ })}>{task.priority}</span>
247
+
248
+ // 5. Regenerate barrels
249
+ // akan scan app <name>
250
+ ```
251
+
252
+ ---
253
+
254
+ ### Recipe 2: Injecting a Dependency into a Service
255
+
256
+ Two patterns: injecting an **adapter** (external client) or injecting another **module's service**.
257
+
258
+ **A. Adapter injection via `use<>()` (for external clients / global singletons)**
259
+
260
+ ```typescript
261
+ // 1. Create the adapter class in apps/<app>/srvkit/
262
+ // apps/<app>/srvkit/EmailClient.ts
263
+ export class EmailClient {
264
+ constructor(readonly apiKey: string) {}
265
+ async send(opts: { to: string; subject: string; body: string }) { /* ... */ }
266
+ }
267
+
268
+ // 2. Register in apps/<app>/lib/option.ts
269
+ export const option = new AkanOption()
270
+ .use((options) => ({
271
+ emailClient: new EmailClient(options.mailerApiKey),
272
+ }));
273
+
274
+ // 3. Inject via use<>() in <model>.service.ts
275
+ export class TaskService extends serve(db.task, ({ use }) => ({
276
+ emailClient: use<EmailClient>(),
277
+ })) {
278
+ async _postCreate(task: cnst.Task) {
279
+ await this.emailClient.send({ to: "...", subject: "Task Created", body: `Task "${task.title}" created.` });
280
+ }
281
+ }
282
+ ```
283
+
284
+ **B. Cross-module service injection via `service<>()` (for other Akan services)**
285
+
286
+ ```typescript
287
+ // In <model>.service.ts — inject another module's service
288
+ import * as srv from "../srv";
289
+
290
+ export class TaskService extends serve(db.task, ({ service }) => ({
291
+ notiService: service<srv.NotiService>(),
292
+ })) {
293
+ async _postCreate(task: cnst.Task) {
294
+ await this.notiService.send("info", `Task "${task.title}" created`);
295
+ }
296
+ }
297
+ ```
298
+
299
+ ---
300
+
301
+ ### Recipe 3: Creating and Using a Slice
302
+
303
+ A Slice is a named, filtered data view. Add file entries and connect from a page.
304
+
305
+ ```typescript
306
+ // 1. apps/<app>/lib/<model>/<model>.signal.ts — Define the slice
307
+ export class TaskSlice extends slice(srv.task, (init) => ({
308
+ inTodo: init()
309
+ .search("statuses", [cnst.TaskStatus])
310
+ .exec(function (statuses?) {
311
+ return this.taskService.queryByStatuses(statuses ?? ["todo", "inProgress"]);
312
+ }),
313
+ })) {}
314
+
315
+ // 2. apps/<app>/lib/<model>/<model>.document.ts — Add query filter for slice
316
+ export class TaskFilter extends from(cnst.Task, (filter) => ({
317
+ query: {
318
+ byStatuses: filter()
319
+ .arg("statuses", [cnst.TaskStatus])
320
+ .query((statuses) => ({ status: { $in: statuses } })),
321
+ },
322
+ })) {}
323
+
324
+ // 3. apps/<app>/lib/<model>/<model>.dictionary.ts — Slice labels
325
+ .slice<TaskSlice>((fn) => ({
326
+ inTodo: fn(["Tasks In Todo", "할 일"]).arg((t) => ({
327
+ statuses: t(["Statuses", "상태"]),
328
+ })),
329
+ }))
330
+
331
+ // 4. In page — Init slice from loader, render with Zone
332
+ loader={async () => {
333
+ const { taskInitInTodo } = await fetch.initTaskInTodo();
334
+ return { taskInitInTodo };
335
+ }}
336
+ render={({ data: { taskInitInTodo } }) => (
337
+ <Task.Zone.Card init={taskInitInTodo} sliceName="taskInTodo" />
338
+ )}
339
+ ```
340
+
341
+ The slice name in code uses camelCase (`inTodo`). In dictionary and components it becomes `"taskInTodo"`.
342
+
343
+ ---
344
+
345
+ ### Recipe 4: Creating a Mutation Endpoint (with Status Workflow)
346
+
347
+ **Files to edit (in order):** `document.ts` → `service.ts` → `signal.ts` → `dictionary.ts` → `store.ts` → `Util.tsx`
348
+
349
+ ```typescript
350
+ // 1. <model>.document.ts — Document chain method with state validation
351
+ export class TaskDocument extends by(cnst.Task) {
352
+ start() {
353
+ if (this.status !== "todo") throw new Err("task.error.cannotStartFromNonTodo");
354
+ this.status = "inProgress";
355
+ return this; // Return this for chaining: task.start().save()
356
+ }
357
+ }
358
+
359
+ // 2. <model>.service.ts — Service method wrapping document
360
+ async startTask(taskId: string) {
361
+ const task = await this.getTask(taskId);
362
+ return task.start().save();
363
+ }
364
+
365
+ // 3. <model>.signal.ts — Mutation endpoint
366
+ export class TaskEndpoint extends endpoint(srv.task, ({ mutation }) => ({
367
+ startTask: mutation(cnst.Task)
368
+ .param("taskId", String)
369
+ .exec(async function (taskId) {
370
+ return await this.taskService.startTask(taskId);
371
+ }),
372
+ })) {}
373
+
374
+ // 4. <model>.dictionary.ts — Endpoint + error labels
375
+ .endpoint<TaskEndpoint>((fn) => ({
376
+ startTask: fn(["Start Task", "작업시작"])
377
+ .arg((t) => ({ taskId: t(["Task ID", "할 일 ID"]) })),
378
+ }))
379
+ .error({
380
+ cannotStartFromNonTodo: ["Task can only start from todo status", "할 일 상태에서만 시작 가능"],
381
+ })
382
+
383
+ // 5. <model>.store.ts — Client-side action with toast feedback
384
+ async startTask(taskId: string) {
385
+ msg.loading("task.startTaskLoading", { key: "startTask" });
386
+ const task = await fetch.startTask(taskId);
387
+ this.setTask(task); // Auto-generated: updates task state in store
388
+ msg.success("task.startTaskSuccess", { key: "startTask" });
389
+ }
390
+
391
+ // 6. <Model>.Util.tsx — Reusable button component
392
+ export const Start = ({ taskId }: { taskId: string }) => (
393
+ <button className="btn btn-xs btn-primary" onClick={() => st.do.startTask(taskId)}>
394
+ Start
395
+ </button>
396
+ );
397
+ ```
398
+
399
+ ---
400
+
401
+ ### Recipe 5: Internal Triggers — Interval & Cron
402
+
403
+ Server-side background jobs. Defined in `internal()` signal, implemented in service.
404
+
405
+ ```typescript
406
+ // 1. <model>.signal.ts — Define triggers
407
+ export class TaskInternal extends internal(srv.task, ({ interval, cron }) => ({
408
+ cleanupStaleTasks: interval(10000).exec(async function () {
409
+ await this.taskService.cleanupStaleTasks();
410
+ }),
411
+
412
+ dailyDigest: cron("0 0 * * *").exec(async function () {
413
+ await this.taskService.sendDailyDigest();
414
+ }),
415
+ })) {}
416
+
417
+ // 2. <model>.service.ts — Implement the logic
418
+ async cleanupStaleTasks() {
419
+ const weekAgo = dayjs().subtract(7, "day").toDate();
420
+ const stale = await this.taskModel.listDueBefore(weekAgo);
421
+ for (const task of stale) {
422
+ await task.remove();
423
+ }
424
+ }
425
+ ```
426
+
427
+ **Available trigger types:**
428
+ - `interval(ms)` — runs repeatedly at the given interval
429
+ - `cron("min hour dom month dow")` — runs on a schedule
430
+ - `initialize()` — runs once on service startup
431
+ - `process(Return).msg(Type)` — message queue consumer
432
+
433
+ ---
434
+
435
+ ### Recipe 6: Creating an Insight (Aggregation / Dashboard Stats)
436
+
437
+ Insights display aggregated statistics across model data.
438
+
439
+ ```typescript
440
+ // 1. <model>.constant.ts — Insight class with accumulate rules
441
+ export class TaskInsight extends via(Task, (field) => ({
442
+ totalCount: field(Int, { default: 0, accumulate: {} }),
443
+ completedCount: field(Int, { default: 0, accumulate: { status: "completed" } }),
444
+ })) {}
445
+
446
+ // 2. <model>.dictionary.ts — Insight field labels
447
+ .insight<TaskInsight>((t) => ({
448
+ totalCount: t(["Total Tasks", "전체 할 일"]),
449
+ completedCount: t(["Completed", "완료됨"]),
450
+ }))
451
+
452
+ // 3. <Model>.View.tsx — Display component consuming an Insight model
453
+ export const Stats = ({ taskInsight }: { taskInsight: cnst.TaskInsight }) => (
454
+ <div className="grid grid-cols-2 gap-4">
455
+ <div className="stat rounded-lg border border-base-content/10 bg-base-100 p-4">
456
+ <div className="stat-title text-base-content/60">{l("task.totalCount")}</div>
457
+ <div className="stat-value text-primary">{taskInsight.totalCount}</div>
458
+ </div>
459
+ <div className="stat rounded-lg border border-base-content/10 bg-base-100 p-4">
460
+ <div className="stat-title text-base-content/60">{l("task.completedCount")}</div>
461
+ <div className="stat-value text-success">{taskInsight.completedCount}</div>
462
+ </div>
463
+ </div>
464
+ );
465
+
466
+ // 4. <Model>.Zone.tsx — Mount in page via Load.Insight bound to a slice
467
+ export const Insight = ({ sliceName }: { sliceName: string }) => {
468
+ const insight = st.slice[sliceName].use.taskInsight();
469
+ if (!insight) return null;
470
+ return <Task.View.Stats taskInsight={insight} />;
471
+ };
472
+ ```
473
+
474
+ ---
475
+
476
+ ### Data Flow Summary
477
+
478
+ For each business question, follow this chain:
479
+
480
+ | Question | File Pattern |
481
+ |----------|-------------|
482
+ | What fields does it have? | `constant.ts` — `via()` layers: Input → Object → Light → Model → Insight |
483
+ | How is it stored/searched? | `document.ts` — `from()` filters + `by()` document methods + `into()` model |
484
+ | What business rule should run? | `service.ts` — `serve()` with DI (`use`, `service`, `plug`, `env`, `memory`) |
485
+ | What should a page call? | `signal.ts` — `internal()` (jobs), `endpoint()` (APIs), `slice()` (data views) |
486
+ | What client state is shared? | `store.ts` — `store()` with auto-generated form/insight state + custom actions |
487
+ | What should users see? | `View.tsx` + `Zone.tsx` (detail/container), `Template.tsx` (forms), `Unit.tsx` (cards), `Util.tsx` (buttons) |
488
+
489
+ ## Auto-Generated API Reference
490
+
491
+ akan scan automatically generates APIs across all layers. Only write custom logic — never hand-write what the framework generates.
492
+
493
+ ### Signal — Endpoint Auto-Generation
494
+
495
+ | Auto-Generated | Signature | Description |
496
+ |---------------|-----------|-------------|
497
+ | `view[Model](id)` | `fetch.viewTask(id)` | Fetch single model for detail view |
498
+ | `edit[Model](id)` | `fetch.editTask(id)` | Fetch model for edit view |
499
+ | `merge[Model](id, data)` | `fetch.mergeTask(data)` | Create (no id) or update (with id) model |
500
+ | `[model]List[Suffix](args, skip, limit, sort)` | `fetch.taskListInTodo(args)` | Paginated list from slice filter |
501
+ | `[model]Insight[Suffix](args)` | `fetch.taskInsightInTodo(args)` | Aggregated insight from slice query |
502
+ | `init[Model][Suffix](args)` | `fetch.initTaskInTodo(args)` | Initialize slice with list + insight |
503
+
504
+ **Rule**: Only define `query()`, `mutation()`, `message()`, `pubsub()` endpoints manually when the endpoint needs custom business logic. Standard CRUD is already auto-generated.
505
+
506
+ ### Service — CRUD & Query Auto-Generation
507
+
508
+ | Auto-Generated | Description |
509
+ |---------------|-------------|
510
+ | `this.<model>Model` | Auto-injected model adaptor |
511
+ | `get<Model>(id)`, `load<Model>(id)` | Single document lookup |
512
+ | `createModel(data)`, `updateModel(id, data)`, `removeModel(id)` | CRUD operations |
513
+ | `list<Query>(args)`, `find<Query>(args)`, `pick<Query>(args)` | Filter-based queries |
514
+ | `exists<Query>(args)`, `count<Query>(args)`, `insight<Query>(args)` | Filter-based helpers |
515
+ | `_preCreate`, `_postCreate`, `_preUpdate`, `_postUpdate`, `_preRemove`, `_postRemove` | Lifecycle hooks (override to add logic) |
516
+
517
+ **Rule**: Use `_preCreate`/`_postCreate` lifecycle hooks for side effects (e.g., push workHistory entries). Write custom service methods only for multi-model orchestration.
518
+
519
+ ### Store — State & Action Auto-Generation
520
+
521
+ | Auto-Generated | Description |
522
+ |---------------|-------------|
523
+ | `[model]` (cached full model), `[model]Loading`, `[model]Form`, `[model]Modal` | Base model states |
524
+ | `create[Model](data)`, `update[Model](id, data)`, `remove[Model](id)` | CRUD actions |
525
+ | `new[Model](partial)`, `edit[Model](model)`, `view[Model](model)` | Form/view state actions |
526
+ | `[slice]List`, `[slice]InitList`, `[slice]Insight`, `[slice]Selection` | Slice states |
527
+ | `init[Slice](args)`, `refresh[Slice]()`, `setPageOf[Slice](page)` | Slice actions |
528
+ | `set[Field]On[Model](value)` | Auto-setters for each model field |
529
+
530
+ **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.
531
+
532
+ ### Document — Filter Query Auto-Generation
533
+
534
+ | Auto-Generated (from Filter definition) | Description |
535
+ |----------------------------------------|-------------|
536
+ | `list[Query](args)`, `listIds[Query](args)` | List documents matching filter |
537
+ | `find[Query](args)`, `findId[Query](args)` | Find one (null if not found) |
538
+ | `pick[Query](args)`, `pickId[Query](args)` | Find one (throw if not found) |
539
+ | `exists[Query](args)`, `count[Query](args)` | Existence check and count |
540
+ | `insight[Query](args)`, `query[Query](args)` | Insight and raw query |
541
+
542
+ **Rule**: Define `Filter` with `.query()` conditions in `document.ts`. akan scan auto-generates all 10 query helper methods per filter. Write `Document` chain methods only for state transitions with validation.
@@ -3,5 +3,8 @@
3
3
  "description": "<%= repoName %> workspace",
4
4
  "version": "0.0.1",
5
5
  "dependencies": {},
6
- "devDependencies": {}
6
+ "devDependencies": {},
7
+ "patchedDependencies": {
8
+ "react-dom@19.2.7": "patches/react-dom@19.2.7.patch"
9
+ }
7
10
  }
@@ -0,0 +1,62 @@
1
+ diff --git a/cjs/react-dom-client.development.js b/cjs/react-dom-client.development.js
2
+ index 8c860aca7db6a1ca74788209d100f10ae5f085fa..fa5f3344475e66bc04befe3cc631a1f4f710228b 100644
3
+ --- a/cjs/react-dom-client.development.js
4
+ +++ b/cjs/react-dom-client.development.js
5
+ @@ -18607,8 +18607,9 @@
6
+ (workInProgressRootRenderLanes & 62914560) ===
7
+ workInProgressRootRenderLanes &&
8
+ now$1() - globalMostRecentFallbackTime < FALLBACK_THROTTLE_MS)
9
+ - ? (executionContext & RenderContext) === NoContext &&
10
+ - prepareFreshStack(root, 0)
11
+ + ? (executionContext & RenderContext) === NoContext
12
+ + ? prepareFreshStack(root, 0)
13
+ + : (workInProgressRootPingedLanes |= pingedLanes)
14
+ : (workInProgressRootPingedLanes |= pingedLanes),
15
+ workInProgressSuspendedRetryLanes === workInProgressRootRenderLanes &&
16
+ (workInProgressSuspendedRetryLanes = 0));
17
+ diff --git a/cjs/react-dom-client.production.js b/cjs/react-dom-client.production.js
18
+ index bbeab5ad7211c6fa240c790988898a47967b2469..5ebddf28cafddadf02af2edf49b98c1327bc31af 100644
19
+ --- a/cjs/react-dom-client.production.js
20
+ +++ b/cjs/react-dom-client.production.js
21
+ @@ -11856,7 +11856,9 @@ function pingSuspendedRoot(root, wakeable, pingedLanes) {
22
+ (workInProgressRootRenderLanes & 62914560) ===
23
+ workInProgressRootRenderLanes &&
24
+ 300 > now() - globalMostRecentFallbackTime)
25
+ - ? 0 === (executionContext & 2) && prepareFreshStack(root, 0)
26
+ + ? 0 === (executionContext & 2)
27
+ + ? prepareFreshStack(root, 0)
28
+ + : (workInProgressRootPingedLanes |= pingedLanes)
29
+ : (workInProgressRootPingedLanes |= pingedLanes),
30
+ workInProgressSuspendedRetryLanes === workInProgressRootRenderLanes &&
31
+ (workInProgressSuspendedRetryLanes = 0));
32
+ diff --git a/cjs/react-dom-profiling.development.js b/cjs/react-dom-profiling.development.js
33
+ index 0df4ce8bc7ab7574e6c18fc0356d85303f4970aa..e2ebd50133473319eb3d191ff790700cda497309 100644
34
+ --- a/cjs/react-dom-profiling.development.js
35
+ +++ b/cjs/react-dom-profiling.development.js
36
+ @@ -18615,8 +18615,9 @@
37
+ (workInProgressRootRenderLanes & 62914560) ===
38
+ workInProgressRootRenderLanes &&
39
+ now$1() - globalMostRecentFallbackTime < FALLBACK_THROTTLE_MS)
40
+ - ? (executionContext & RenderContext) === NoContext &&
41
+ - prepareFreshStack(root, 0)
42
+ + ? (executionContext & RenderContext) === NoContext
43
+ + ? prepareFreshStack(root, 0)
44
+ + : (workInProgressRootPingedLanes |= pingedLanes)
45
+ : (workInProgressRootPingedLanes |= pingedLanes),
46
+ workInProgressSuspendedRetryLanes === workInProgressRootRenderLanes &&
47
+ (workInProgressSuspendedRetryLanes = 0));
48
+ diff --git a/cjs/react-dom-profiling.profiling.js b/cjs/react-dom-profiling.profiling.js
49
+ index 91621dbe568cf0f924805e7492ff7f487c8254b5..315c5e88d3e03f841ac31ccee4fc17e19e196363 100644
50
+ --- a/cjs/react-dom-profiling.profiling.js
51
+ +++ b/cjs/react-dom-profiling.profiling.js
52
+ @@ -13672,7 +13672,9 @@ function pingSuspendedRoot(root, wakeable, pingedLanes) {
53
+ (workInProgressRootRenderLanes & 62914560) ===
54
+ workInProgressRootRenderLanes &&
55
+ 300 > now$1() - globalMostRecentFallbackTime)
56
+ - ? 0 === (executionContext & 2) && prepareFreshStack(root, 0)
57
+ + ? 0 === (executionContext & 2)
58
+ + ? prepareFreshStack(root, 0)
59
+ + : (workInProgressRootPingedLanes |= pingedLanes)
60
+ : (workInProgressRootPingedLanes |= pingedLanes),
61
+ workInProgressSuspendedRetryLanes === workInProgressRootRenderLanes &&
62
+ (workInProgressSuspendedRetryLanes = 0));