@akanjs/cli 2.4.1 → 2.4.2-rc.1

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 (66) hide show
  1. package/.build-stamp +1 -1
  2. package/{agent.command-sa12ccz3.js → agent.command-ash7wabs.js} +7 -7
  3. package/{application.command-jv63vv7z.js → application.command-zm47j93m.js} +5 -5
  4. package/{applicationBuildRunner-vzjdgv1g.js → applicationBuildRunner-554a2vnk.js} +6 -1
  5. package/{applicationReleasePackager-brvth6rs.js → applicationReleasePackager-pjbxc793.js} +1 -1
  6. package/buildBatch.proc.js +3 -3
  7. package/{capacitorApp-wnxzrppx.js → capacitorApp-wc75rvfm.js} +3 -3
  8. package/{cloud.command-8z2nc6b2.js → cloud.command-vwpzten0.js} +10 -10
  9. package/{context.command-0m854xme.js → context.command-j0kq87xa.js} +15 -15
  10. package/{dependencyScanner-m4x5maek.js → dependencyScanner-f6grrep3.js} +2 -2
  11. package/{guideline.command-dsdpae45.js → guideline.command-1m4jwhv0.js} +5 -5
  12. package/incrementalBuilder.proc.js +22 -14
  13. package/{index-eja0sbm6.js → index-0wgpfp7t.js} +6 -6
  14. package/{index-1b9f27pm.js → index-2nb022xv.js} +3 -3
  15. package/{index-n2mp0f48.js → index-31183bxt.js} +6 -6
  16. package/{index-p9f1rpbp.js → index-31e8cwjq.js} +5 -5
  17. package/{index-x53a5nya.js → index-46tjzh6s.js} +1 -1
  18. package/{index-61keag0s.js → index-67546d0j.js} +4 -1
  19. package/{index-q4sc46ec.js → index-79v9jwqe.js} +3 -3
  20. package/{index-m0vg9n8z.js → index-7k9sj7j5.js} +3 -2
  21. package/{index-e3b8ms64.js → index-8dsjmcbp.js} +11 -11
  22. package/{index-2v10yr8g.js → index-ckywhpvp.js} +1 -1
  23. package/{index-d2e2y85b.js → index-cp26p7qd.js} +4 -4
  24. package/{index-eqxmxan6.js → index-ehjhzg5y.js} +1 -1
  25. package/{index-phj3ewxv.js → index-en4zpzdm.js} +2 -2
  26. package/{index-qacv44mm.js → index-f1x6fe2e.js} +1 -1
  27. package/{index-m04as81s.js → index-mpj63mbw.js} +4 -4
  28. package/{index-3a4vntr0.js → index-n08vr4hh.js} +2 -2
  29. package/{index-5vvwc0cz.js → index-nsj2ftxj.js} +1 -1
  30. package/{index-0t7pwff2.js → index-s6nk9815.js} +182 -64
  31. package/{index-hk58kr1m.js → index-smt4re0s.js} +1 -1
  32. package/{index-zw4aqvwh.js → index-t5rn8q9f.js} +4 -4
  33. package/{index-h6pevatb.js → index-tnngmzgf.js} +7 -7
  34. package/{index-eay4t1te.js → index-xdf6rxyt.js} +3 -3
  35. package/{index-81epkybm.js → index-xjkjcbhd.js} +3 -3
  36. package/index.js +19 -19
  37. package/{library.command-fhx5ce8n.js → library.command-39eq3brk.js} +4 -4
  38. package/{localRegistry.command-18f61945.js → localRegistry.command-34zvgkys.js} +9 -9
  39. package/{module.command-29wvnb56.js → module.command-vwahdkzr.js} +8 -8
  40. package/{package.command-46r1dncm.js → package.command-wkwd5x5p.js} +5 -5
  41. package/package.json +2 -2
  42. package/{page.command-vxvqdzc0.js → page.command-9w85hrg0.js} +4 -4
  43. package/{primitive.command-yk622wmt.js → primitive.command-2hnr8h7f.js} +9 -9
  44. package/{quality.command-p0mb5xwt.js → quality.command-4kerz7mh.js} +3 -3
  45. package/{repair.command-nptckp6w.js → repair.command-zmwshmvr.js} +6 -6
  46. package/{routeSourceValidator-wbhmbwpj.js → routeSourceValidator-cncd92fv.js} +39 -0
  47. package/{scalar.command-bss14hez.js → scalar.command-9kanb73t.js} +7 -7
  48. package/templates/workspaceRoot/.cursor/rules/akan-page-routing.mdc.template +27 -1
  49. package/templates/workspaceRoot/.cursor/rules/akan-sync-conventions.mdc.template +14 -0
  50. package/templates/workspaceRoot/.cursor/rules/client-server-boundaries.mdc.template +1 -1
  51. package/templates/workspaceRoot/.cursor/rules/coding-comments.mdc.template +13 -7
  52. package/templates/workspaceRoot/.cursor/rules/coding-style.mdc.template +23 -3
  53. package/templates/workspaceRoot/.cursor/rules/lint-enforced-rules.mdc.template +18 -0
  54. package/templates/workspaceRoot/.cursor/rules/module-file-playbook.mdc.template +61 -0
  55. package/templates/workspaceRoot/.cursor/rules/naming-conventions.mdc.template +15 -0
  56. package/templates/workspaceRoot/.cursor/rules/react-component-style.mdc.template +21 -0
  57. package/templates/workspaceRoot/.cursor/rules/scalar-modeling.mdc.template +47 -3
  58. package/templates/workspaceRoot/.cursor/rules/service-signal-conventions.mdc.template +69 -0
  59. package/templates/workspaceRoot/.cursor/rules/srvkit-adapters-and-guards.mdc.template +50 -0
  60. package/templates/workspaceRoot/.cursor/rules/typescript-imports.mdc.template +3 -0
  61. package/templates/workspaceRoot/.gitignore.template +1 -0
  62. package/templates/workspaceRoot/AGENTS.md.template +140 -14
  63. package/templates/workspaceRoot/docs/AI-DEVELOPMENT.md.template +19 -0
  64. package/templates/workspaceRoot/docs/GENERATED.md.template +1 -1
  65. package/{workflow.command-7fhqb33d.js → workflow.command-4areq0ex.js} +12 -12
  66. package/{workspace.command-xkcec7ky.js → workspace.command-02x93b10.js} +22 -22
@@ -12,3 +12,72 @@ alwaysApply: false
12
12
  - Use `Slice` for typed data views that feed client stores and zones; keep each slice focused on one purpose.
13
13
  - Use `Endpoint` for query and mutation contracts exposed to callers.
14
14
  - Connect external APIs or infrastructure through adapters, usually under `srvkit/`, and inject them into services instead of importing vendor clients directly into domain logic.
15
+
16
+ ## Guards And Transports
17
+
18
+ - Guards run on both HTTP and websocket calls. Read the caller with `context.get<T>("account")` (`pkgs/akanjs/signal/signalContext.ts`) instead of branching on `getHttpContext()` / `getWebSocketContext()`.
19
+ - Slice-level `guards` only reach the generated query/mutation endpoints. A `pubsub`/`message` endpoint is unguarded unless it declares its own `guards` in its signal option.
20
+ - A pubsub room is authorized once, at subscribe. When a socket's credential changes the framework re-runs each room's guards and unsubscribes the ones that now fail (`SignalResolver.revalidateWsRooms`), so guards must stay side-effect free and safe to re-run.
21
+ - A websocket carries its credential in the handshake snapshot on `ws.data` (`AppWsData`); clients that hold the token in memory send it with `fetch.setJwt(...)`, which forwards an auth frame over the socket.
22
+
23
+ ## Authorization Defaults
24
+
25
+ - **Every `slice()` takes an explicit `{ guards: {…} }` second argument, and `root:` is always `Admin`.**
26
+ - **Every custom `mutation` / `query` / `message` names its own `guards: [...]` array.** Never rely on the slice default. `Public` belongs on a slice `get:`, never on a mutation.
27
+ - The acting user arrives via `.with(Self)` / `.with(CurrentUserId)` / `.with(Me)`. Never trust a client-supplied id.
28
+ - Services re-check ownership even when a guard already gated the call — two independent gates.
29
+ - Guard class shape, fail-closed rules, and the `static name = "User"` requirement live in the srvkit adapters and guards rule.
30
+
31
+ ## Service Method Shape
32
+
33
+ - Keep methods to a few lines: load → chain → `return await ….save()`. Write `return await` explicitly in tail position; do not "optimize" it away.
34
+ - Side effects belong in `override async _preUpdate` / `_postCreate`, not inline. Fire-and-forget is explicitly `void`-ed.
35
+ - Order deliberately: load every referenced document, then save, then notify.
36
+ - Return `null` / `false` for "not allowed" or "not found" and let the signal decide whether that is an error.
37
+ - `XInternal` → `XSlice` → `XEndpoint` are all declared even when empty, and `exec` is a one-liner delegating to the service.
38
+
39
+ ## Signal Body Types
40
+
41
+ - `.body(...)` / `.param(...)` args accept `ConstantFieldTypeInput` only: scalars, model refs, or `enumOf(...)`.
42
+ - Numbers must use `Int` or `Float` — `Number` is rejected (`pkgs/akanjs/signal/endpointInfo.ts`).
43
+ - `Upload` is valid only inside a mutation flagged for file upload: `mutation([cnst.File], { fileUpload: true }).body("files", [Upload])` (see `libs/shared/lib/file/file.signal.ts`). It is not a model field type.
44
+
45
+ ## Reserved Endpoint Names
46
+
47
+ - Auto-generated CRUD endpoints (e.g. `create<Model>`, `update<Model>`, `remove<Model>`) already exist for every model. Do not declare an `Endpoint`/`Slice` with a name that collides with them.
48
+ - The service layer surfaces such a collision as a typecheck error, but the signal layer can pass sync/typecheck/build and fail only at runtime — so treat name collisions as errors regardless of whether the build is green.
49
+
50
+ ## Slices, Queries, and Hydration
51
+
52
+ - A slice's `exec` returns a `QueryOf` (an opaque query descriptor, `pkgs/akanjs/constant/types.ts`); you **cannot** chain `.sort()`/`.limit()` on it.
53
+ - Apply ordering/paging via the store `init` fetch option instead: `initX(..., { sort, page, limit })` (`pkgs/akanjs/fetch/fetchType/sliceFetch.type.ts`).
54
+ - Generated list accessors like `listBy(...)` return `Promise<Doc[]>`. For a chainable builder (`.sort().skip().limit().select()`) use the model facade's `findMany`/`findOne` (`FindManyChain`, `pkgs/akanjs/document/into.ts`).
55
+ - **Hydrated vs raw:** server queries return hydrated `cnst.<Model>` instances (with `set`/`save`/`refresh`); client fetch results are raw `GetStateObject` plain data (functions stripped, `pkgs/akanjs/base/types.ts`).
56
+
57
+ ## Text Search In A Filter — `q.search()`
58
+
59
+ - Text search is a filter query node like any other: `bySearch: filter().arg("text", String).query((text, q) =>
60
+ q.search(text, { prefix: true }))`. The generated `listBySearch` / `countBySearch` / `queryBySearch` /
61
+ `insightBySearch` come for free — you do **not** need a slice to make search usable.
62
+ - **Only add a search slice when the model's data is safe to enumerate.** A filter is server-side; a slice is a
63
+ client-callable endpoint, so on a model whose slice `get:` is `Public` a search slice hands anyone a way to walk the
64
+ table. Leave that decision to the mounting app.
65
+ - `q.search()` compiles to a JOIN, not a WHERE fragment, so it **must sit at an AND position**. Nesting it under
66
+ `q.any()` or `q.not()` throws, and it is rejected in `updateOneByQuery` / `updateManyByQuery` — a query-level write
67
+ takes no join, so ignoring the node would silently widen the write to every other matching row.
68
+ - Blank or whitespace-only input matches **nothing**. Never "fix" that into a passthrough: a passthrough turns a
69
+ search endpoint into a full listing.
70
+ - Order by relevance with the built-in `relevance` sort key. It is an empty sort map, which the store reads as
71
+ "unspecified": score order when a search join is present, `createdAt` descending otherwise. That fallback is the
72
+ compiler's own, not a model-defined default — redefining `latest` on the model does not change it.
73
+ - **A slice endpoint never reaches "unspecified".** The resolver fills `latest` before the query is built, so a
74
+ client asking for the score order has to name `relevance`; leaving `sort` off gets `latest`, not relevance.
75
+ - Scope a search with `columns` (`q.search(text, { columns: ["title"] })`) and re-weight with `weights`, a tuple of
76
+ finite numbers positional over `["title", "desc", "tag", "filter"]`.
77
+
78
+ ## Service / Signal Injection
79
+
80
+ - Injected dependencies resolve by field-name convention: a field named `<refName>Service` resolves to the service registered under `<refName>`, and `<refName>Signal` likewise (`pkgs/akanjs/service/injectInfo.ts`).
81
+ - The `Service`/`Signal` suffix is required — the injector strips it to derive the registry lookup key. Name the field after the target refName plus the suffix, not arbitrarily.
82
+ - Preference order inside a service: `service<srv.XService>()` for another module's service · `plug(AdapterClass)` or `plug(StorageAdaptorRole)` for an adapter · `use<T>()` only to reach an `option.ts`-registered legacy singleton · `env(...)` for config.
83
+ - New adapters are `adapt("name" as const, ({ use, env, plug, memory }) => ({…}))` classes in `srvkit/` that self-register — do not add them to `lib/option.ts`. See the srvkit adapters and guards rule.
@@ -0,0 +1,50 @@
1
+ ---
2
+ description: srvkit adapter (adapt/plug), guard, and error-handling conventions
3
+ globs: apps/**/srvkit/**/*.ts,libs/**/srvkit/**/*.ts
4
+ alwaysApply: false
5
+ ---
6
+
7
+ # Adapters — `adapt()` And `plug()`
8
+
9
+ An injected singleton is an `adapt()` class in `srvkit/`. Write new adapters this way.
10
+
11
+ ```ts
12
+ export class AdminNoti extends adapt("adminNoti" as const, ({ use, env, plug, memory }) => ({
13
+ discordApi: use<DiscordApi>(),
14
+ workspaceRoot: env(() => `~/build/${getEnv().environment}`),
15
+ masterHost: plug(MasterHost),
16
+ tokenMap: memory(Map, { of: String }),
17
+ })) {}
18
+ ```
19
+
20
+ - Inject it with `plug(TheClass)` from a service or from another adapter. Destructure only the injectors you use, and write the registration key `as const`.
21
+ - **Do not register an `adapt()` class in `lib/option.ts`.** It self-registers, and `plug(Class)` uses the class itself as the token. `option.ts` is now only for legacy constructor-style adapters and for widening the options type.
22
+ - `this.logger` is provided; never construct a `Logger` inside an `adapt()` class. Lifecycle work goes in `override async onInit()`.
23
+ - `#private` is the house style here — the `#private` lint ban covers only `*.constant.ts`, `*.document.ts`, `*.service.ts`, and `*.store.ts`.
24
+ - **`adapt()` is for singletons only.** A per-use value object stays a plain class you `new` at the call site. Ask whether there is exactly one per process that a service needs injected; if not, it is a plain class.
25
+ - **Legacy shape — recognise it, do not copy it.** Plain classes with `constructor(options: XOptions)`, registered in `lib/option.ts` as `options.x ? new XApi(options.x) : null` and injected with `use<T>()`, still work. Migrate one to `adapt()` only when you are already changing it.
26
+ - Preference order inside a service: `service<srv.XService>()` for another module's service · `plug(AdapterClass)` or `plug(StorageAdaptorRole)` for an adapter · `use<T>()` only to reach an `option.ts`-registered legacy singleton · `env(...)` for config.
27
+
28
+ Conventions that hold for both shapes:
29
+
30
+ - Route every remote call through one private `#api<T>(path, init?)` with `signal: AbortSignal.timeout(20_000)`.
31
+ - Paginate with `for (let page = 1; ; page += 1)` broken by `if (pageItems.length < 100) break;`.
32
+ - Resolve secrets as `process.env.X ?? options.x ?? deterministicGenerator(...)` **inside a function**, never at module scope.
33
+ - Extend a function by appending an optional trailing parameter with a default, never by changing arity.
34
+ - Parameters: up to three required primitives positional; optional flags in a trailing `{ … } = {}`; four or more parameters, or any two same-typed strings, in one named destructured object.
35
+ - Release locks in `finally`. Load heavy optional dependencies through a module-level memoized promise (`sharpLoad ??= import("sharp")`).
36
+
37
+ # Guards (`srvkit/guards.ts`)
38
+
39
+ - Resource guards are `Can<Verb><Model>` classes that `implements Guard` with an `async canPass(context)`.
40
+ - They **fail closed**: no resource named ⇒ `false`; a load that throws ⇒ `logger.warn` then `false`. Admin bypass goes first.
41
+ - Keep `static name = "User";` on guard classes. `fetch` serializes guard names and the API explorer filters on them; it looks like dead code, and deleting it breaks the UI. Comment it so the next reader knows.
42
+ - Guards ship with the library that owns the model and are imported by its own signals through the package path, so a mounting app inherits authorization and cannot forget it.
43
+ - Services re-check ownership even when a guard already gated the call — two independent gates.
44
+ - This file earns real comments: explain what would leak without each guard.
45
+
46
+ # Error Placement
47
+
48
+ - State-machine preconditions throw in `document.ts`; cross-document rules throw in `service.ts`; request-level policy belongs in signal guards.
49
+ - Best-effort code returns a sentinel (`null`, `undefined`, `[0, 0]`, `{}`). There are no Result/Either wrappers.
50
+ - `try/catch` is rare and always converts an exception into a decision, never swallows one. Guards catch → `logger.warn` → `return false`; adapters catch → `logger.error` → `return null`; UI uses `try/finally` to reset a spinner. A bodyless `catch {}` is acceptable only with a one-line reason.
@@ -11,3 +11,6 @@ alwaysApply: false
11
11
  - Use `akanjs/*` for framework facets, `@apps/*` for apps, `@libs/*` for shared libs, and `@contract/*` for contract code.
12
12
  - Respect existing client/server entrypoints such as `@libs/shared/client`, `@libs/shared/server`, `@apps/myapp/client`, and `@apps/myapp/server`.
13
13
  - Let Biome organize imports instead of manually reshuffling unrelated imports.
14
+ - Namespace the generated barrels in backend `.ts` files: `import * as cnst from "../cnst"`, `* as db`, `* as srv`. Use `import type * as srv` in services so the runtime graph stays lazy, and a value import in signals.
15
+ - In `.tsx` files use one flat named import from the package client path (`import { cnst, fetch, st, Task, usePage } from "@apps/myapp/client"`) — never a relative `../` import.
16
+ - Never import a third-party package from a page, a barrel, or a module file; re-export it through a one-line shim in `base/`, `webkit/`, or `ui/` first.
@@ -67,6 +67,7 @@ local.properties
67
67
  **/public/fallback-*.js
68
68
  **/public/libs
69
69
  **/private/libs
70
+ **/page/**/(libs)
70
71
  **/public/firebase-messaging-sw.js
71
72
 
72
73
  **/android/keystore.properties
@@ -39,6 +39,10 @@ Common generated files include:
39
39
  - `*/srvkit/index.ts`
40
40
  - `*/common/index.ts`
41
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
+
42
46
  ## Domain Module Responsibilities
43
47
 
44
48
  Use the local module shape before adding a new abstraction.
@@ -55,7 +59,7 @@ Use the local module shape before adding a new abstraction.
55
59
  - `<Model>.Unit.tsx` owns list/item UI. Server components, no 'use client'.
56
60
  - `<Model>.View.tsx` owns detail UI. Server components, no 'use client'.
57
61
  - `<Model>.Zone.tsx` owns page/container integration and interactive loading or action flows that need client state. Client components, with 'use client'.
58
- - `<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'.
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'.
59
63
 
60
64
  ## Agent Workflow
61
65
 
@@ -247,14 +251,122 @@ final fallback when no CLI command covers the change.
247
251
  |-------|-----|------------|
248
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>` |
249
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 |
250
- | Use JS `#private` methods in service classes | Akan's build system rejects `#private`. Use TypeScript `private` keyword instead. | `private _methodName()` never `#_methodName()` |
251
- | 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 |
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 `clsx()` and `cva()`. Sorter output like `font-bold text-2xl` looks wrong but is correct. | Write classes in any order and let `akan lint` sort them |
258
+ | `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 |
252
259
  | 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 |
253
260
  | 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 |
254
261
  | 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 |
255
262
  | 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>` |
256
263
  | 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 |
257
264
 
265
+ ## Code Style
266
+
267
+ House style for `apps/**` and `libs/**`. `akan lint` enforces the rules in the anti-pattern table above; the rest
268
+ is convention that keeps hand-written code reading like generated code.
269
+
270
+ ### Files And Types
271
+
272
+ - Keep files small. Split a component before it reaches ~150 lines instead of adding section comments.
273
+ - Ship every scaffold file even when it is empty — `export class TaskInternal extends internal(srv.task, () => ({})) {}`,
274
+ empty dictionary stages, the `// state` / `// action` markers in an empty store. They mark where things go.
275
+ - Never add a sibling helper file inside `lib/<model>/`. Helpers go to `common/`, `webkit/`, `srvkit/`, or `ui/`.
276
+ - Prefer duplication to premature abstraction at the leaf: copy the near-identical file and change the literals.
277
+ - `interface` for object shapes, `type` only for unions and aliases.
278
+ - Never use a non-null assertion (`!`). Narrow with `?.`, an early return, or a type predicate.
279
+ - Escape with `as unknown as T`, never `as any`.
280
+ - `as const` on every `enumOf(...)` array, every Light field tuple, and every module-scope lookup map. Never the
281
+ TypeScript `enum` keyword.
282
+ - Never annotate a component's return type. Async functions carry no `Async` suffix.
283
+
284
+ ### Components
285
+
286
+ - `export const X = ({ … }: XProps) => { return (…); };` — arrow const with a block body. `export default` is only
287
+ for pages, layouts, and `lazy()` targets.
288
+ - Declare `interface <ComponentName>Props` immediately above the component with no blank line, `className?: string`
289
+ first. Name it for the component (`CardProps`), never for the model.
290
+ - Never `React.FC`, never `defaultProps`, never `PropsWithChildren`. Defaults go in the destructuring; children are
291
+ typed `children: ReactNode`.
292
+ - `"use client"` on line 1 is mechanical by file role: every `.Zone.tsx`, `.Template.tsx`, and `.Util.tsx` has it;
293
+ no `.Unit.tsx` or `.View.tsx` ever does.
294
+ - Conditional render is `cond ? <X/> : null`, never `{cond && <X/>}` — in a `className` context the latter renders
295
+ the literal string `"false"`.
296
+ - Never hand-roll loading, empty, or list states. Use `Load.Units` / `Load.View` / `Load.Edit` with `renderItem`,
297
+ `renderList`, and `renderEmpty`, and `Model.New` / `Model.Edit` / `Model.SureToRemove` for CRUD modals.
298
+ - Avoid hooks. `useState` is for modal-open, tab, draft-input, and drag state only — never for server data.
299
+ `.Template.tsx` files contain zero `useState`: forms are store-driven with `Field.*`, `value={taskForm.x}`, and
300
+ `onChange={st.do.setXOnTask}` passed by reference.
301
+ - Read with `st.use.*` and write with `st.do.*`. Client components do not call `fetch.*`.
302
+ - Static class strings stay plain strings. Use `clsx` only for a conditional or to merge an incoming `className`,
303
+ and merge the caller last: `clsx("base", conditional, className)`. `clsx` comes from `akanjs/client`. No
304
+ `twMerge`, no `cn()`.
305
+ - Use daisyUI semantic tokens with opacity modifiers (`text-base-content/60`, `bg-base-100/70`). Never `dark:` —
306
+ theming is the daisyUI theme block in `page/*/styles.css`.
307
+ - Hoist enum→class lookups to a module-scope `as const` map typed `{ [key in cnst.TaskStatus["value"]]: string }`,
308
+ not `Record<...>`.
309
+
310
+ ### Naming
311
+
312
+ - Component exports are role names (`Card`, `General`, `Preview`, `Remove`). The model comes from the namespace, so
313
+ write `Card`, not `TaskCard`.
314
+ - Layer the verbs: the document chain method drops the model (`start()`) and the signal, store, and dictionary
315
+ re-add it (`startTask`). This keeps custom endpoints clear of generated CRUD and makes `st.do.X` read the same as
316
+ `fetch.X`.
317
+ - Slice and filter names are prepositional: `inTodo`, `byStatuses`, `ofProject`. Never `getXInY`, never `listX`.
318
+ - Handlers are `onX` props with inline arrows. Do not extract a `handleX`.
319
+ - Booleans are `is*` / `has*` / `can*` / `show*`. Counters are `*Num`, indices are `idx`, collections are `*List`.
320
+ - Identifiers, type names, endpoint names, and log messages are English. Everything a user reads goes through
321
+ `l("task.title")` or `l.trans({ … })` — never a hard-coded string in JSX, never `window.alert`.
322
+
323
+ ### Backend
324
+
325
+ - **`constant.ts`** — five classes in order, `TaskInput → TaskObject → LightTask → Task → TaskInsight`, and write
326
+ `TaskInsight` even when empty. Put display and predicate logic on `LightTask` (`isNew()`, `canWrite(user?)`): it is
327
+ the class both server and client hold, so shared logic belongs there rather than in a util module. Collection
328
+ helpers go `static` on the full model.
329
+ - **`document.ts`** — `TaskFilter extends from(...)` → `Task extends by(...)` → `TaskModel extends into(...)`, with
330
+ `sort: {}` always present. Chain methods validate → mutate → `return this` and never `save()`; the caller saves, so
331
+ chains compose. Indexes and derived totals go in `static override _onSchema`.
332
+ - **`service.ts`** — keep methods to a few lines: load → chain → `return await ….save()`. Side effects go in
333
+ `override async _preUpdate` / `_postCreate`, not inline. Fire-and-forget is explicitly `void`-ed. Return `null` or
334
+ `false` for "not allowed" and let the signal decide whether that is an error.
335
+ - **`signal.ts`** — `TaskInternal` → `TaskSlice` → `TaskEndpoint`, all three declared even when empty, and `exec` is a
336
+ one-liner delegating to the service. Every `slice()` takes an explicit `{ guards: { root: Admin, … } }`, and every
337
+ custom mutation, query, and message names its own `guards: [...]`.
338
+ - **`store.ts`** — write a custom action only for a toast, an optimistic update, or a multi-field write; the rest is
339
+ generated. Never `import type { RootStore } from "../st"` — it crashes `akan build` with a Bun SSR segfault.
340
+ - **`dictionary.ts`** — fixed chain with empty stages still written:
341
+ `.of() → .model() → .insight() → .query() → .sort() → .enum() → .slice() → .endpoint() → .error() → .translate()`.
342
+ Every label is `t(["English", "한국어"])`, and nearly every one also carries `.desc([en, ko])`.
343
+ - **`srvkit/` adapters** — an injected singleton is an `adapt("name" as const, ({ use, env, plug, memory }) => ({…}))`
344
+ class, injected with `plug(TheClass)`. It self-registers, so do not add it to `lib/option.ts`. `this.logger` is
345
+ provided; lifecycle work goes in `override async onInit()`. A per-use value object stays a plain class you `new` at
346
+ the call site. Route remote calls through one private `#api<T>(path, init?)` with `AbortSignal.timeout(20_000)`, and
347
+ resolve secrets inside a function, never at module scope.
348
+ - **Errors** — state-machine preconditions throw in `document.ts`, cross-document rules in `service.ts`, and
349
+ request-level policy lives in signal guards. `try/catch` always converts an exception into a decision, never
350
+ swallows one. Store actions do not `try/catch`; let the framework toast the `Err`.
351
+
352
+ ### Comments
353
+
354
+ Do not narrate code. Do document the thing the code cannot say. Both halves are the rule.
355
+
356
+ - Never add a comment that restates the identifier, the signature, or the control flow.
357
+ - Do not add JSDoc, section banners, or "why/how" comments for ordinary logic.
358
+ - Density tracks the layer: pages carry none, product code stays under 1 %, and `srvkit/` adapters and `guards.ts`
359
+ carry as much as the external constraints require.
360
+ - A comment is warranted for a vendor spec or protocol quirk, an infrastructure constraint, a third-party library
361
+ gotcha, security reasoning, a math derivation, a domain field's business meaning, a state transition above a
362
+ document chain method, or why an obvious alternative was rejected.
363
+ - Markers: `TODO` unfinished work · `FIXME` known broken behavior · `XXX` hazard a reader must not miss · `//!`
364
+ disabled or must-fix code · `//?` an explanatory aside · `//*` a design note · deletion caution, warning why
365
+ removing a line would break something non-obvious.
366
+ - Keep allowed comments to one short line, and give every suppression a reason:
367
+ `// biome-ignore lint/<rule>: <why>`. Never a bare disable block.
368
+ - Match nearby file style: if the surrounding code has few comments, keep it that way.
369
+
258
370
  ## Generated File Tracker (Quick Reference)
259
371
 
260
372
  These files are regenerated by `akan sync` and overwritten on every sync. **Do not hand-edit them.**
@@ -272,7 +384,7 @@ These files are regenerated by `akan sync` and overwritten on every sync. **Do n
272
384
  | `apps/*/client.ts` | App-wide client barrel | The `fetch` and `st` instances |
273
385
  | `apps/*/server.ts` | App-wide server barrel | Server-side service resolution |
274
386
  | `*/lib/**/index.ts` | Per-module barrel | Module-level re-exports |
275
- | `*/ui/index.ts` | All UI component files | UI barrel |
387
+ | `*/ui/index.ts` | All 1-depth UI files/folders | UI layer-root barrel (nested `ui/<Folder>/index.tsx` is **not** generated) |
276
388
  | `*/webkit/index.ts` | All webkit files | Webkit barrel |
277
389
  | `*/srvkit/index.ts` | All srvkit files | Srvkit barrel |
278
390
  | `*/common/index.ts` | All common files | Common barrel |
@@ -304,9 +416,10 @@ export class TaskInput extends via((field) => ({
304
416
  export class LightTask extends via(TaskObject, ["title", "priority", "status", "due"] as const, () => ({})) {}
305
417
 
306
418
  // 2. apps/<app>/lib/<model>/<model>.dictionary.ts
307
- // Add i18n labels for the new field (and its enum values if any)
419
+ // Add i18n labels for the new field (and its enum values if any).
420
+ // Labels are [en, ko] pairs, and nearly every one also carries a .desc([en, ko]).
308
421
  .model<Task>((t) => ({
309
- priority: t(["Priority", "우선순위"]),
422
+ priority: t(["Priority", "우선순위"]).desc(["How urgent the task is", "할 일의 긴급도"]),
310
423
  }))
311
424
  .enum<TaskPriority>("taskPriority", (t) => ({
312
425
  low: t(["Low", "낮음"]),
@@ -344,6 +457,11 @@ Three patterns: injecting an **external adapter** (`use<>()`), another **module'
344
457
  or a **predefined framework adapter** (`plug()`). A field named `<refName>Service` resolves to the service
345
458
  registered under `<refName>` — the `Service`/`Signal` suffix is required and stripped to derive the lookup key.
346
459
 
460
+ > **For a new adapter you own, prefer the `adapt()` shape in pattern C over the `option.ts` registration in
461
+ > pattern A.** An `adapt()` class self-registers and is injected with `plug(TheClass)`, so it never touches
462
+ > `option.ts`. Pattern A is the legacy constructor-style shape: recognise it, keep it working, and migrate one
463
+ > only when you are already changing it.
464
+
347
465
  > `apps/<app>/lib/option.ts` is a **user-owned** file scaffolded once — edit it to register adapters/DI. Unlike the
348
466
  > barrels (`cnst.ts`, `db.ts`, `srv.ts`, …) it is **not** overwritten by `akan sync`, so your `.use(...)` registrations
349
467
  > are safe.
@@ -454,14 +572,11 @@ export class TaskFilter extends from(cnst.Task, (filter) => ({
454
572
  })),
455
573
  }))
456
574
 
457
- // 4. In page — Init slice from loader, render with Zone
458
- loader={async () => {
459
- const { taskInitInTodo } = await fetch.initTaskInTodo();
460
- return { taskInitInTodo };
461
- }}
462
- render={({ data: { taskInitInTodo } }) => (
463
- <Task.Zone.Card init={taskInitInTodo} sliceName="taskInTodo" />
464
- )}
575
+ // 4. In page — Init the slice in an async Page and hand the init to a Zone.
576
+ export default async function Page() {
577
+ const [{ taskInitInTodo }] = await Promise.all([fetch.initTaskInTodo()]);
578
+ return <Task.Zone.Card init={taskInitInTodo} sliceName="taskInTodo" />;
579
+ }
465
580
  ```
466
581
 
467
582
  The slice name in code uses camelCase (`inTodo`). In dictionary and components it becomes `"taskInTodo"`.
@@ -628,6 +743,17 @@ A short list of things the type system does not always catch:
628
743
  - **Reading a secret field needs an explicit select.** `field(...).secret()` values (e.g. `passwordHash`) are stripped
629
744
  from query results by default. Fetch them with `{ select: { <field>: true } }`, e.g.
630
745
  `this.userModel.pickById(id, { select: { passwordHash: true } })`.
746
+ - **Text search fields use the `text` role.** Opt a field into the full-text index with
747
+ `field(String, { text: "title" })` (or `"desc"` / `"tag"` / `"thumb"` / `"filter"`). Nothing else opts a field in.
748
+ `secret` / `hidden` / `resolve()` fields with `text` throw at class-build time — the mirror is plaintext. Search
749
+ runs on sqlite/libsql only; `q.search()` against Postgres throws. `thumb` is mirrored for rendering and is not
750
+ indexed.
751
+ - **`q.search()` is a filter node, not a slice requirement.** Prefer
752
+ `bySearch: filter().arg("text", String).query((text, q) => q.search(text, { prefix: true }))` — the generated
753
+ `listBySearch` / `countBySearch` / `queryBySearch` / `insightBySearch` come for free. Only add a search slice when
754
+ the model's data is safe to enumerate. It must sit at an AND position (not under `q.any()` / `q.not()`), blank
755
+ input matches nothing, and score order needs the built-in `relevance` sort key — a slice endpoint that leaves
756
+ `sort` off gets `latest`, not relevance.
631
757
 
632
758
  ## Current User, Guards & Auth-Gated Pages
633
759
 
@@ -45,6 +45,25 @@ See `docs/GENERATED.md` for the generated file list.
45
45
  - Server-oriented surfaces include pages, `*.Unit.tsx`, `*.View.tsx`, `lib/`, `srvkit/`, and server entrypoints.
46
46
  - Treat `AKAN_PUBLIC_*` values as public.
47
47
 
48
+ ## House Style
49
+
50
+ `AGENTS.md` holds the full style guide. The rules agents break most often:
51
+
52
+ - **Never hand-order Tailwind classes** — the linter sorts them, including inside `clsx()`, and its output looks
53
+ unnatural on purpose.
54
+ - **Never `throw new Error`** — throw `new Err("<module>.error.<key>")` with an `[en, ko]` entry in the module
55
+ dictionary. `common/` cannot import `Err`, so keep throwing code out of it.
56
+ - **Never import a third-party package** from a page, a barrel, or a module file. Re-export it through a one-line
57
+ shim in `base/`, `webkit/`, or `ui/` first.
58
+ - `#private` is lint-banned in `*.constant.ts`, `*.document.ts`, `*.service.ts`, and `*.store.ts` only; it stays
59
+ the house style under `srvkit/`.
60
+ - Components are `export const X = ({ … }: XProps) => { … }` with `interface XProps` directly above and
61
+ `className?: string` first. No `React.FC`, no non-null assertions, no `as any`.
62
+ - `cond ? <X/> : null`, never `{cond && <X/>}` — the latter renders the string `"false"` in a className context.
63
+ - Put display and predicate logic on the `Light<Model>` constant class; keep document chain methods
64
+ validate → mutate → `return this` without `save()`; keep services to load → chain → save.
65
+ - Do not narrate code in comments. Do document vendor quirks, infrastructure constraints, and security reasoning.
66
+
48
67
  ## Abstract Documents
49
68
 
50
69
  Update `*.abstract.md` when business invariants, workflows, user-visible behavior, cross-module relationships, or
@@ -28,7 +28,7 @@ sync or build can overwrite local changes.
28
28
 
29
29
  | File | Purpose |
30
30
  | --- | --- |
31
- | `*/ui/index.ts` | Re-exports UI component files. |
31
+ | `*/ui/index.ts` | Re-exports 1-depth UI component files and folders. Only this layer-root barrel is generated — a nested `ui/<Folder>/index.tsx` that builds a namespace is hand-written source. |
32
32
  | `*/webkit/index.ts` | Re-exports webkit/browser helper files. |
33
33
  | `*/srvkit/index.ts` | Re-exports server-kit helper files. |
34
34
  | `*/common/index.ts` | Re-exports common helper files. |
@@ -1,31 +1,31 @@
1
1
  // @bun
2
2
  import {
3
3
  WorkflowRunner
4
- } from "./index-1b9f27pm.js";
4
+ } from "./index-2nb022xv.js";
5
5
  import {
6
6
  ScalarScript
7
- } from "./index-d2e2y85b.js";
7
+ } from "./index-cp26p7qd.js";
8
8
  import {
9
9
  PrimitiveScript
10
- } from "./index-m04as81s.js";
10
+ } from "./index-mpj63mbw.js";
11
11
  import {
12
12
  ModuleScript
13
- } from "./index-h6pevatb.js";
14
- import"./index-qacv44mm.js";
13
+ } from "./index-tnngmzgf.js";
14
+ import"./index-f1x6fe2e.js";
15
15
  import"./index-ss469dec.js";
16
- import"./index-eay4t1te.js";
16
+ import"./index-xdf6rxyt.js";
17
17
  import {
18
18
  createWorkflowStepRegistry
19
- } from "./index-2v10yr8g.js";
20
- import"./index-eqxmxan6.js";
21
- import"./index-5vvwc0cz.js";
19
+ } from "./index-ckywhpvp.js";
20
+ import"./index-ehjhzg5y.js";
21
+ import"./index-nsj2ftxj.js";
22
22
  import {
23
23
  Workspace,
24
24
  command,
25
25
  script
26
- } from "./index-phj3ewxv.js";
27
- import"./index-0t7pwff2.js";
28
- import"./index-61keag0s.js";
26
+ } from "./index-en4zpzdm.js";
27
+ import"./index-s6nk9815.js";
28
+ import"./index-67546d0j.js";
29
29
  import"./index-6pz1j0zj.js";
30
30
  import"./index-r24hmh0q.js";
31
31
 
@@ -1,59 +1,59 @@
1
1
  // @bun
2
2
  import {
3
3
  ContextScript
4
- } from "./index-e3b8ms64.js";
5
- import"./index-81epkybm.js";
6
- import"./index-1b9f27pm.js";
4
+ } from "./index-8dsjmcbp.js";
5
+ import"./index-xjkjcbhd.js";
6
+ import"./index-2nb022xv.js";
7
7
  import {
8
8
  CloudScript
9
- } from "./index-eja0sbm6.js";
10
- import"./index-d2e2y85b.js";
11
- import"./index-m04as81s.js";
12
- import"./index-h6pevatb.js";
13
- import"./index-qacv44mm.js";
9
+ } from "./index-0wgpfp7t.js";
10
+ import"./index-cp26p7qd.js";
11
+ import"./index-mpj63mbw.js";
12
+ import"./index-tnngmzgf.js";
13
+ import"./index-f1x6fe2e.js";
14
14
  import"./index-ss469dec.js";
15
- import"./index-hk58kr1m.js";
15
+ import"./index-smt4re0s.js";
16
16
  import {
17
17
  AgentScript
18
- } from "./index-q4sc46ec.js";
19
- import"./index-eay4t1te.js";
20
- import"./index-2v10yr8g.js";
21
- import"./index-eqxmxan6.js";
18
+ } from "./index-79v9jwqe.js";
19
+ import"./index-xdf6rxyt.js";
20
+ import"./index-ckywhpvp.js";
21
+ import"./index-ehjhzg5y.js";
22
22
  import {
23
23
  getLatestPackageVersion,
24
24
  getNpmRegistryUrl
25
- } from "./index-zw4aqvwh.js";
25
+ } from "./index-t5rn8q9f.js";
26
26
  import {
27
27
  GlobalConfig
28
- } from "./index-5vvwc0cz.js";
28
+ } from "./index-nsj2ftxj.js";
29
29
  import {
30
30
  ApplicationScript
31
- } from "./index-n2mp0f48.js";
31
+ } from "./index-31183bxt.js";
32
32
  import"./index-76rn3g2c.js";
33
33
  import {
34
34
  LibraryScript
35
- } from "./index-3a4vntr0.js";
35
+ } from "./index-n08vr4hh.js";
36
36
  import {
37
37
  PackageScript
38
- } from "./index-p9f1rpbp.js";
38
+ } from "./index-31e8cwjq.js";
39
39
  import {
40
40
  Exec,
41
41
  Workspace,
42
42
  command,
43
43
  runner,
44
44
  script
45
- } from "./index-phj3ewxv.js";
45
+ } from "./index-en4zpzdm.js";
46
46
  import"./index-fgc8r6dj.js";
47
47
  import {
48
48
  AppExecutor,
49
49
  LibExecutor,
50
50
  PkgExecutor,
51
51
  WorkspaceExecutor
52
- } from "./index-0t7pwff2.js";
53
- import"./index-x53a5nya.js";
52
+ } from "./index-s6nk9815.js";
53
+ import"./index-46tjzh6s.js";
54
54
  import {
55
55
  FileSys
56
- } from "./index-61keag0s.js";
56
+ } from "./index-67546d0j.js";
57
57
  import"./index-bjpxzr6s.js";
58
58
  import"./index-6pz1j0zj.js";
59
59
  import"./index-r24hmh0q.js";