@akanjs/cli 2.4.2-rc.0 → 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.
package/.build-stamp CHANGED
@@ -1 +1 @@
1
- 662b40618a5997cc258bbcdff378d024ba3e5861396f7d73dbfa2a2eaf09d772
1
+ 267b35e31ad3c4b46042123b1a70ac6abdc6a296be7c297a037753bb89b81135
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@akanjs/cli",
3
- "version": "2.4.2-rc.0",
3
+ "version": "2.4.2-rc.1",
4
4
  "sourceType": "module",
5
5
  "type": "module",
6
6
  "publishConfig": {
@@ -34,7 +34,7 @@
34
34
  "@langchain/openai": "^1.4.6",
35
35
  "@tailwindcss/node": "^4.3.0",
36
36
  "@trapezedev/project": "^7.1.4",
37
- "akanjs": "2.4.2-rc.0",
37
+ "akanjs": "2.4.2-rc.1",
38
38
  "chalk": "^5.6.2",
39
39
  "commander": "^14.0.3",
40
40
  "daisyui": "5.5.23",
@@ -14,4 +14,30 @@ alwaysApply: false
14
14
  - Dynamic segments use `[id]`; route groups use directories like `(user)`, `(public)`, `(tab)`, or `(detail)`.
15
15
  - Page modules should usually export `default`, `pageConfig`, `head`, `generateHead`, or `Loading`.
16
16
  - Prefer `export default function Page` or `export default async function Page` for page components.
17
- - Before changing route behavior, check `pkgs/akanjs/server/src/routeTree.tsx` and nearby routes for the expected pattern.
17
+ - Before changing route behavior, check nearby routes for the expected pattern.
18
+
19
+ ## Page Body Shape
20
+
21
+ ```tsx
22
+ interface PageProps {
23
+ params: { taskId: string };
24
+ }
25
+
26
+ export default async function Page({ params }: PageProps) {
27
+ const { l } = usePage();
28
+ getSelf({ unauthorize: "/signin" });
29
+ const { taskId } = params;
30
+ const [{ task }] = await Promise.all([fetch.viewTask(taskId)]);
31
+ return <Task.View.General task={task} />;
32
+ }
33
+
34
+ export const pageConfig = { transition: "stack" } satisfies PageConfig;
35
+ ```
36
+
37
+ - There is no `loader=` / `render=` page prop. Pages are `export default async function Page`.
38
+ - Declare `interface PageProps { params: {...}; searchParams?: {...} }` immediately above the default export.
39
+ - Body order: `usePage()`, auth, destructure params, fetch, return.
40
+ - Run independent fetches through `Promise.all`, even when there is only one.
41
+ - Gate auth at `_layout.tsx`; repeating `getSelf({ unauthorize: "/signin" })` in the page is fine and common.
42
+ - Keep `async` even when nothing is awaited — it marks a real server page.
43
+ - No `useState`, no `useEffect`, and no comments in page files.
@@ -17,3 +17,17 @@ alwaysApply: false
17
17
  - Module `*.test.ts`, `*.test.tsx`, `*.spec.ts`, and `*.spec.tsx` files are allowed.
18
18
  - `ui/index.ts`, `webkit/index.ts`, `srvkit/index.ts`, `common/index.ts`, and module `lib/**/index.ts` files are generated by `akan sync`; do not hand-edit or track them.
19
19
  - Generated facet indexes export only 1-depth files/folders with `export * from "./name";`.
20
+
21
+ ## Layer Placement
22
+
23
+ | Folder | Admission test | Naming |
24
+ |---|---|---|
25
+ | `common/` | pure, isomorphic, zero-dependency; may import only sibling `common/*` and `akanjs/base`. Cannot import `Err`, so keep throwing code out of it. | camelCase file, filename equals the single export |
26
+ | `webkit/` | touches `window` / `navigator` / Capacitor, or is a React hook | `use<Thing>.tsx` — `.tsx` even with no JSX |
27
+ | `srvkit/` | touches `node:*`, `Bun`, `process.env`, a secret, or a server SDK | camelCase file, PascalCase class |
28
+ | `ui/` | renders JSX and is not bound to one model | PascalCase component, camelCase sidecar (`swipeCard.util.ts`) |
29
+
30
+ - Hooks return a named object of async closures, never a tuple.
31
+ - Only layer-root `index.ts` barrels are generated. A `ui/<Folder>/index.tsx` that builds a namespace is hand-written source; the distinguishing test is that a generated barrel contains nothing but `export * from "./X";` lines.
32
+ - `ui/<Folder>/index_.tsx` (trailing underscore) is the `"use client"` + `lazy()` boundary, with a server-safe `index.tsx` beside it. Collapsing the pair into one file breaks RSC.
33
+ - Never add a sibling helper file inside `lib/<model>/`; helpers belong in one of the folders above.
@@ -12,6 +12,6 @@ alwaysApply: false
12
12
  - Let server page/layout files own route-level concerns: `usePage`, headers, static layout, and `akanjs/ui` `Tab` composition.
13
13
  - Keep interactive loading, submit/import actions, local form inputs, and `router.push` inside client `Util`, `Template`, or `Zone` components.
14
14
  - Prefer server-rendered `Tab` shells for static mode selection; put one client component inside each `Tab.Panel` instead of managing the selected mode with extra `useState`.
15
- - In domain UI, `Template`, `Zone`, and `Util` components are usually client components; `Unit` and `View` components are usually server components.
15
+ - In domain UI the boundary is mechanical, not a judgment call: `Template`, `Zone`, and `Util` are always client components with `"use client"` on line 1; `Unit` and `View` are always server components and never carry the directive. `usePage()` is still legal in server files.
16
16
  - Preserve established domain file roles such as `.document.ts`, `.service.ts`, `.store.ts`, `.constant.ts`, and `.client.ts`.
17
17
  - When unsure, inspect nearby files in the same app or package before introducing a new boundary pattern.
@@ -1,19 +1,25 @@
1
1
  ---
2
- description: Never comment in code except TODO/FIXME/XXX or deletion warnings
2
+ description: Do not narrate code; document only what the code cannot say
3
3
  alwaysApply: true
4
4
  ---
5
5
 
6
6
  # Comments
7
7
 
8
- - Never add comments in code by default.
9
- - Prefer clear names and structure so the code needs no explanation.
10
- - Do not narrate what the code already says.
8
+ Do not narrate code. Do document the thing the code cannot say. Both halves are the rule.
9
+
10
+ - Never add a comment that restates the identifier, the signature, or the control flow.
11
+ - Prefer clear names and structure so ordinary logic needs no explanation.
11
12
  - Do not add JSDoc, section banners, or "why/how" comments for ordinary logic.
12
- - Comments are allowed only for these purposes:
13
+ - Comment density tracks the layer, not the author: pages carry none, product `lib/` and `ui/` code stays under 1 %, and `srvkit/` adapters and `guards.ts` carry as much as the external constraints require.
14
+ - A comment is warranted for: a vendor spec or protocol quirk; an infrastructure constraint; a third-party library gotcha; security reasoning; why a rule that looks arbitrary is correct; a math derivation; a domain field's business meaning; a state transition above a document chain method; why an obvious alternative was rejected.
15
+ - In-code markers:
13
16
  1. `TODO` — unfinished work that must be tracked in-code
14
17
  2. `FIXME` — known broken or incorrect behavior that must be fixed
15
18
  3. `XXX` — dangerous / surprising hazard that a reader must not miss
16
- 4. Deletion caution warn why removing a line or block would break something non-obvious
19
+ 4. `//!`disabled or must-fix code
20
+ 5. `//?` — an explanatory aside
21
+ 6. `//*` — a design note
22
+ 7. Deletion caution — warn why removing a line or block would break something non-obvious
17
23
  - Keep allowed comments one short line when possible.
24
+ - Every suppression carries a reason: `// biome-ignore lint/<rule>: <why>`. Never a bare disable block.
18
25
  - Match nearby file style: if the surrounding code has few comments, keep it that way.
19
-
@@ -1,5 +1,5 @@
1
1
  ---
2
- description: Coding style conventions for classes and functions
2
+ description: Coding style conventions for classes, functions, files, and types
3
3
  globs: **/*.{ts,tsx}
4
4
  alwaysApply: false
5
5
  ---
@@ -8,7 +8,7 @@ alwaysApply: false
8
8
 
9
9
  - For large units of work, prefer declaring a class and running the flow through an instance instead of scattering many standalone functions.
10
10
  - Prefer class methods or `static` methods over unrelated top-level helper functions when the logic belongs to a class-level workflow.
11
- - Prefer ECMAScript `#private` fields and methods over TypeScript `private`; `*.service.ts` files are the main exception where `private` methods are encouraged.
11
+ - Prefer ECMAScript `#private` fields and methods over TypeScript `private`, except in the four suffixes where `#private` is lint-banned (`*.constant.ts`, `*.document.ts`, `*.service.ts`, `*.store.ts`) those use TypeScript `private`. `#private` is the house style under `srvkit/`, including `adapt()` classes.
12
12
  - In files that declare a class, avoid top-level functions or variables when they can reasonably live inside the class.
13
13
  - Prefer `const` function expressions over `function` declarations unless hoisting, overloads, generators, or framework conventions make `function` the better fit.
14
14
  - Prefer declaring only one class per file; split the file when two or more class declarations are needed.
@@ -17,8 +17,28 @@ alwaysApply: false
17
17
  - Except for React component files or convention files, TypeScript filenames should use camelCase.
18
18
  - In React components, keep one-off `className` strings inline. Only extract class name constants when the class is reused, conditionally composed, or too large to read comfortably in JSX.
19
19
 
20
+ # File Size And Duplication
21
+
22
+ - Keep files small. The house median is well under 50 lines; split a component before it reaches ~150 lines rather than adding section comments.
23
+ - Ship every scaffold file even when it is empty — `export class XInternal extends internal(srv.x, () => ({})) {}`, empty dictionary stages, the `// state` / `// action` markers in an empty store. They mark where things go.
24
+ - Never add a sibling helper file inside `lib/<model>/`. Helpers go to `common/`, `webkit/`, `srvkit/`, or `ui/`.
25
+ - Prefer duplication to premature abstraction at the leaf. Near-identical sibling modules and per-vendor pages are deliberately copied, not parameterised. Copy the file and change the literals; share enums by import only.
26
+
27
+ # TypeScript Shape
28
+
29
+ - Use `interface` for object shapes and `type` only for unions and aliases.
30
+ - Declare `interface <ComponentName>Props` immediately above the component with no blank line between, and put `className?: string` first. Name it for the component (`CardProps`, `WorldProps`), never for the model. Do not export it unless a sibling imports it.
31
+ - Never use a non-null assertion. Narrow with `?.`, an early return, or a type predicate such as `.filter((id): id is string => !!id)`.
32
+ - Escape with `as unknown as T`. Never `as any`.
33
+ - Never annotate a component's return type. Annotate a helper only when the return is a union, a tuple, or a type predicate.
34
+ - Use `as const` on every `enumOf(...)` array, every `via(Model, [...] as const, …)` Light tuple, and every module-scope lookup map. Never use the TypeScript `enum` keyword.
35
+ - Async functions carry no `Async` suffix.
36
+
20
37
  # Test Code
21
38
 
22
39
  - Write TypeScript tests with Bun's test runner and import `describe`, `expect`, and `test` from `bun:test`.
23
40
  - Keep tests colocated with the source they cover using `*.test.ts` or `*.spec.ts`, following the existing nearby pattern.
24
- - Prefer focused behavior tests for public contracts and edge cases over implementation-detail assertions.
41
+ - Prefer focused behavior tests for public contracts and edge cases over implementation-detail assertions.
42
+ - Split signal tests in two. `<model>.signal.spec.ts` holds reusable fixtures built on `sampleOf(cnst.XInput)` with explicit `Promise<cnst.X>` return types and **no assertions**. `<model>.signal.test.ts` holds the assertions: `describe("<Model> Signal")`, `let` fixtures at describe scope, one `beforeAll`, story-ordered `it`s, and negatives via `await expect(p).rejects.toThrow()`.
43
+ - `lib/user/user.signal.spec.ts` is the one place agent types are re-exported and re-typed; import `UserAgent` / `AdminAgent` from there rather than from the shared lib directly.
44
+ - A placeholder `it` with a descriptive title is an acceptable floor. Write a real suite when the behaviour is security-relevant.
@@ -0,0 +1,18 @@
1
+ ---
2
+ description: Lint rules that break the build, including several whose correct output looks wrong
3
+ alwaysApply: true
4
+ ---
5
+
6
+ # Lint-Enforced Rules (These Break The Build)
7
+
8
+ Enforced by `biome.json` and the Akan grit plugins registered in `biome.json`. Several of them produce output that looks wrong; do not "fix" it back.
9
+
10
+ - **Never hand-order Tailwind classes.** `nursery/useSortedClasses` is an error and also sorts the string arguments to `clsx()` and `cva()`. Sorter output such as `font-bold text-2xl text-base-content` or `border-base-content/5 border-t` is correct. Write the classes in any order, run the formatter, leave the result.
11
+ - **Never `throw new Error`.** Throw `new Err("<module>.error.<key>")` and register the key as `[en, ko]` in that module's dictionary `.error({})`. Import `Err` from `"../dict"` on the server and from `"@libs/<lib>/client"` or `"@apps/<app>/client"` in UI. `no-throw-raw-error.grit` exempts `*.test.ts`, `*.spec.ts`, `*.constant.ts`, and `common/**` — `common/` has no legal `Err` import path, so keep throwing code out of it.
12
+ - **Never import a third-party package** from `page/**`, from any barrel, or from any `*.{constant,dictionary,document,service,signal,store}.ts` / `*.{Template,Unit,Util,View,Zone}.tsx` (`no-import-external-library.grit`). Re-export the symbol through a lib first. One-line re-export shims such as `libs/<lib>/base/<pkg>.ts` and `libs/<lib>/webkit/<hook>.ts` exist for exactly this reason — they are load-bearing, not cruft. Do not delete them.
13
+ - **`#private` is banned in exactly four file suffixes:** `*.constant.ts`, `*.document.ts`, `*.service.ts`, and `*.store.ts` (`no-js-private-class-method.grit`). The rule is scoped by file path, not by class shape, so `#private` remains the house style everywhere under `srvkit/`, including `adapt()` adapter classes.
14
+ - **No `console.log` / `console.debug`.** Only `assert`, `error`, `info`, and `warn` are allowed. Server code uses the injected `this.logger.*` or `new Logger("ClassName")`.
15
+ - **Never redeclare a generated CRUD endpoint name** in `*.signal.ts` (`no-redeclare-predefined-endpoint.grit`).
16
+ - **No deep imports past a barrel** (`no-deep-internal-import.grit`). Cross-module constant references such as `../map/map.constant` are the sanctioned exception.
17
+ - **Server-component discipline** is enforced on `page/**`, `*.Unit.tsx`, and `*.View.tsx` (`no-import-client-functions.grit`, `no-use-client-in-server.grit`, `non-scalar-props-restricted.grit`).
18
+ - `noArrayIndexKey` and `useExhaustiveDependencies` are **off** on purpose: `key={idx}` for embedded scalars and short dependency arrays are intentional, not oversights.
@@ -0,0 +1,61 @@
1
+ ---
2
+ description: Per-file shape for constant, document, service, signal, store, dictionary, and abstract files
3
+ globs: apps/**/lib/**/*.{ts,tsx,md},libs/**/lib/**/*.{ts,tsx,md}
4
+ alwaysApply: false
5
+ ---
6
+
7
+ # Module File Playbook
8
+
9
+ ## `<model>.constant.ts`
10
+
11
+ Five classes in order with one blank line between them and `enumOf("camelName", [...] as const)` classes above: `XInput → XObject → LightX → X → XInsight`. Write `XInsight` even when it is empty.
12
+
13
+ - Put display and predicate logic on `LightX` (`isNew()`, `canWrite(user?)`, `formatTimes()`, `isCancellable()`) — the Light class is the one both server and client hold, so shared logic belongs there instead of in a util module. This is the most commonly missed rule in the codebase.
14
+ - Collection-level helpers go `static` on the full model.
15
+ - Defaults are a literal for scalars and a thunk for anything constructed. Arrays are `field([T])`; optional is the postfix `.optional()`.
16
+ - Give any field whose business meaning is not obvious a short trailing comment.
17
+
18
+ ## `<model>.document.ts`
19
+
20
+ Fixed order: `XFilter extends from(...)` → `X extends by(...)` → `XModel extends into(...)`, with `sort: {}` always present.
21
+
22
+ - Chain methods validate → mutate → `return this`, and never `save()`; the caller saves, so chains compose (`org.removeUser(id).removeInvite(id).save()`).
23
+ - Put a one-line comment above each chain method stating the transition.
24
+ - Atomic counters live on the Model class with the updater-callback form, returning `!!modifiedCount`.
25
+ - Indexes and derived totals go in `static override _onSchema`, not in the service.
26
+
27
+ ## `<model>.service.ts`
28
+
29
+ - Keep methods to a few lines: load → chain → `return await ….save()`. Write `return await` explicitly in tail position; do not "optimize" it away.
30
+ - Side effects belong in `override async _preUpdate` / `_postCreate`, not inline.
31
+ - Fire-and-forget is explicitly `void`-ed.
32
+ - Order deliberately: load every referenced document, then save, then notify.
33
+ - Return `null` / `false` for "not allowed" or "not found" and let the signal decide whether that is an error.
34
+
35
+ ## `<model>.signal.ts`
36
+
37
+ - `XInternal` → `XSlice` → `XEndpoint`, all three declared even when empty. `exec` is a one-liner delegating to the service.
38
+ - Every `slice()` takes an explicit `{ guards: {…} }` second argument, and `root:` is always `Admin`.
39
+ - Every custom `mutation` / `query` / `message` names its own `guards: [...]` array. `Public` belongs on a slice `get:`, never on a mutation.
40
+ - The acting user arrives via `.with(Self)` / `.with(CurrentUserId)` / `.with(Me)`. Never trust a client-supplied id.
41
+
42
+ ## `<model>.store.ts`
43
+
44
+ - Write a custom action only for a toast, an optimistic update, or a multi-field write; most stores need none, because state and CRUD actions are generated.
45
+ - The body is three lines: `await fetch.X` → `this.setX(...)` → toast. The optimistic shape is mutate the client model, `void fetch.*`, then commit.
46
+ - Use `this.pick(...)` when the value must exist, `this.get()` when it may not, and `this.set({...})` to write.
47
+ - Mutate lists through the collection API (`this.set({ xList: xList.set(x).save() })`), not array spread.
48
+ - **Never `import type { RootStore } from "../st"`** — it crashes `akan build` with a Bun SSR segfault.
49
+ - Store actions do not `try/catch`; let the framework toast the `Err`.
50
+
51
+ ## `<model>.dictionary.ts`
52
+
53
+ Fixed chain, with empty stages still written: `.of() → .model() → .insight() → .query() → .sort() → .enum() → .slice() → .endpoint() → .error() → .translate()`.
54
+
55
+ - Every label is `t(["English", "한국어"])`, and nearly every one also carries `.desc([en, ko])`.
56
+ - Name every argument in `.arg()`, including framework-supplied `skip` / `limit` / `sort`.
57
+ - Use `modelDictionary`, `scalarDictionary`, or `serviceDictionary` to match the module kind.
58
+
59
+ ## `<module>.abstract.md`
60
+
61
+ A title line, one declarative sentence naming what the module owns, a `## Rules` list of two to five invariants the code cannot show, and an optional workflow arrow chain (`draft -> signed -> active -> completed`). Never restate field lists or types. Update it whenever an invariant or workflow changes.
@@ -0,0 +1,15 @@
1
+ ---
2
+ description: Naming conventions for components, methods, slices, booleans, and language choice
3
+ alwaysApply: true
4
+ ---
5
+
6
+ # Naming And Language
7
+
8
+ - Component exports are role names — `Card`, `Sticker`, `General`, `Preview`, `Admin`, `World`, `Mesh`, `Remove`. The model comes from the namespace (`<Floor.Unit.Card>`), so never write `FloorCard`. `Util` exports are the endpoint verb minus the model noun (`Serve`, `Refund`, `Complete`, `Terminate`).
9
+ - Layer the verbs: the document chain method drops the model (`sign()`, `approve()`), the service keeps the bare verb, and the signal, store, and dictionary re-add it (`signScContract`). This keeps custom endpoints clear of generated CRUD and makes `st.do.X` read the same as `fetch.X`.
10
+ - Slice and filter names are prepositional: `inOrg`, `inProject`, `inPeriod`, `byStatuses`, `ofPortfolio`. Never `getXInY`, never `listX`.
11
+ - Handlers are `onX` props taking inline arrows. Do not extract a `handleX`.
12
+ - Booleans are `is*` / `has*` / `can*` / `show*` / `disable*`. Counters are `*Num`, indices are `idx`, collections are `*List` or plural. `SCREAMING_SNAKE` is unused; module-scope tables are camelCase + `as const`.
13
+ - Keep existing domain vocabulary and its typos. Transliterated domain terms and misspelled identifiers already in use are load-bearing — renaming them silently breaks callers that match on the name.
14
+ - Identifiers, type names, endpoint names, and log messages are **English, always**. Everything a user reads goes through `l("model.field")` or `l.trans({ en, ko })` — never a hard-coded string in JSX, never `window.alert`.
15
+ - Dictionary entries are `[en, ko]` pairs, and nearly every label also carries a `.desc([en, ko])` even when it repeats the label. English labels are Title Case, Korean is the plain domain term, and `.error()` Korean ends in `다.`.
@@ -0,0 +1,21 @@
1
+ ---
2
+ description: React component, form, and Tailwind/daisyUI styling conventions
3
+ globs: **/*.tsx
4
+ alwaysApply: false
5
+ ---
6
+
7
+ # React Components And Styling
8
+
9
+ - Components are `export const X = ({ … }: XProps) => { return (…); };` — arrow const with a block body. `export default` is reserved for pages, layouts, and `lazy()` targets.
10
+ - Never `React.FC`, never `defaultProps`, never `PropsWithChildren`. Defaults go in the destructuring (`prefix = ""`); children are typed `children: ReactNode`.
11
+ - `"use client"` on line 1 above the imports is mechanical by file role: every `.Zone.tsx`, `.Template.tsx`, and `.Util.tsx` has it; no `.Unit.tsx` or `.View.tsx` ever does. `usePage()` is legal in server files.
12
+ - Conditional render is `cond ? <X/> : null`. Never `{cond && <X/>}` — in a `className` context it renders the literal string `"false"`. Early `return null` is for guard clauses only.
13
+ - Never hand-roll loading, empty, or list states. Use `Load.Units` / `Load.View` / `Load.Edit` with `renderItem`, `renderList`, `renderView`, and `renderEmpty`; `<Empty />` for a bare placeholder; and `Model.New` / `Model.Edit` / `Model.SureToRemove` for CRUD modals.
14
+ - Avoid hooks. `useState` is for modal-open, tab, draft-input, and drag state only — never for server data. `useEffect` must be a genuine effect such as subscribe-with-cleanup or one-shot init. Prefer `Tab` over a `useState` mode switch. `.Template.tsx` files contain zero `useState`.
15
+ - Forms are entirely store-driven: `value={xForm.field}` with `onChange={st.do.setFieldOnX}`, the setter passed by reference. Always use `Field.*`, never a bare `<input>` for a model field. Nested rows use `st.do.writeOnX("payments.3.name", v)` plus the generated `add<Field>OnX` / `sub<Field>OnX`.
16
+ - Read with `st.use.*` and write with `st.do.*`. Client components do not call `fetch.*`.
17
+ - Static class strings stay plain strings. Reach for `clsx` only for a conditional or to merge an incoming `className`, and merge the caller last: `clsx("base classes", conditional, className)`. `clsx` comes from `akanjs/client`. No `twMerge`, no `cn()`.
18
+ - Multi-slot components take extra named props (`wrapperClassName`, `bodyClassName`), never a `classNames` object.
19
+ - Use daisyUI semantic tokens with opacity modifiers (`text-base-content/60`, `border-base-content/10`, `bg-base-100/70`, `bg-primary/10`). Never use `dark:` — theming is the daisyUI theme block in `page/*/styles.css`. Raw hex belongs only in marketing surfaces; match the neighbouring files.
20
+ - Hoist enum→class lookups to a module-scope `as const` map typed `{ [key in cnst.XStatus["value"]]: string }`. Do not use `Record<...>`. Escalate the map to `webkit/` when a second module needs it.
21
+ - Use `<Link>` from `akanjs/ui` for internal navigation; `<a>` only for `mailto:` and external links.
@@ -7,8 +7,52 @@ alwaysApply: false
7
7
  # Scalar Modeling
8
8
 
9
9
  - Define Akan models in `.constant.ts` files with `via` from `akanjs/constant`.
10
- - Import custom scalar helpers such as `ID`, `Int`, `Float`, `JSON`, `Upload`, `Any`, `enumOf`, or `dayjs` from `akanjs/base` when the model needs them.
11
10
  - Use `Int` for whole-number counts and quantities; use `Float` only for values that need decimals.
12
- - Use `ID` for document references and prefer explicit structured fields over `JSON` unless the content is genuinely flexible.
11
+ - Use `ID` for document references and prefer explicit structured fields over `Any` unless the content is genuinely flexible.
13
12
  - For date defaults, prefer a function such as `default: () => dayjs()` so the value is created at runtime.
14
- - Follow the established model layering pattern: `Input`, `Object`, `Light<Model>`, full `<Model>`, and `<Model>Insight` when the domain needs those views.
13
+ - Follow the established model layering pattern in this order: `Input`, `Object`, `Light<Model>`, full `<Model>`, and `<Model>Insight`. Write all five, and write `<Model>Insight` even when it is empty.
14
+ - Put display and predicate logic on the `Light<Model>` class (`isNew()`, `canWrite(user?)`, `formatTimes()`) rather than in a util module — the Light class is the one both server and client hold. Collection-level helpers go `static` on the full model.
15
+ - Defaults are a literal for scalars and a thunk for anything constructed. Arrays are `field([T])`; optional is the postfix `.optional()`.
16
+ - Give any field whose business meaning is not obvious a short trailing comment.
17
+
18
+ ## Scalar & Field Type Reference
19
+
20
+ - **Import from `akanjs/base`** (real classes/helpers, not globals): `Int`, `Float`, `ID`, `Any`, `Upload`, `enumOf`, and the `dayjs` factory. There is **no `JSON` scalar** — use `Any` for open/flexible payloads.
21
+ - **Use the JS globals directly (no import needed)**: `String`, `Boolean`, `Date`. They are monkey-patched to behave like scalars, so `field(String)` typechecks.
22
+ - **`Number` is not a valid field/body type.** `NumberConstructor` is intentionally not augmented, so `field(Number)` / `.body("x", Number)` fails to typecheck. Use `Int` or `Float` instead.
23
+ - Runtime resolution of every scalar (globals included) goes through `PrimitiveRegistry` by `refName` (`pkgs/akanjs/base/primitiveRegistry.ts`).
24
+
25
+ ## Text Search Fields — the `text` role
26
+
27
+ - A field joins the full-text index by declaring one of five roles: `field(String, { text: "title" })`, and likewise
28
+ `"desc"`, `"tag"`, `"thumb"`, `"filter"`. Nothing else opts a field in, and there is no per-model switch.
29
+ - Pick the role by what the value *is*, because `bm25` weights them positionally (`title` 10, `tag` 3, `desc` 1,
30
+ `filter` 0): `title` is the one line a human scans for, `desc` is prose, `tag` is a keyword list, `filter` is a
31
+ scoping value (status, owner, role) that must be matchable but must never outrank a real title hit.
32
+ - `thumb` is mirrored for rendering a hit and is **not** indexed — never expect it to match.
33
+ - **A `secret`, `hidden`, or `resolve()` field with `text` throws at class-build time**, not at query time. That is
34
+ deliberate: the mirror is plaintext, so an indexed secret would leak through search. Do not work around it. The
35
+ same throw covers a `text` field *underneath* one of those — a scalar's own field is reachable through its parent,
36
+ so `f.secret(Noti)` where `Noti.label` carries a role is rejected at the parent, not silently indexed.
37
+ - The role works on a relation too (`image: field(File, { text: "thumb" })`) and on an array (`playing: field([String],
38
+ { text: "tag" })`); an array of objects indexes by leaf key, including an array leaf (`works[*].tags`). A field
39
+ inside a `Map` indexes nothing: there is no fixed path to extract it from.
40
+ - Declaring roles is all the wiring there is. Mirror rows are maintained by SQL triggers — not document hooks —
41
+ because `updateOneByQuery` and friends fire no hooks, and most searchable-field mutations go through exactly that
42
+ path.
43
+ - Search runs on sqlite/libsql only. `q.search()` against Postgres throws, loudly, rather than returning every row.
44
+ - `AKAN_SEARCH_ENABLED=0` switches the index off process-wide; unset means on. It never deletes mirror data, and
45
+ re-enabling reconciles every ref. **Give every process the same value** — a process cannot drop triggers for models
46
+ it does not mount, so a mixed fleet leaves stale triggers behind.
47
+ - The tokenizer is `AKAN_SEARCH_TOKENIZER` (or `database.search.tokenizer`, which wins), defaulting to
48
+ `unicode61 remove_diacritics 2`. Changing it rebuilds the index from the mirror on the next boot — the model
49
+ tables are never re-read — so it is a safe knob, unlike a `text` role change, which re-reads every row. The
50
+ rebuild takes no cross-process claim, so a fleet restarted at once repeats it in every process; stagger the
51
+ restart when the mirror is large.
52
+
53
+ ## Image & File Fields
54
+
55
+ - **Do not declare `Upload` as a model field.** `Upload` is a signal-body-only primitive. Models reference the `File` model instead.
56
+ - Declare an image/file field as a relation to `File`: `image: field(File).optional()` for one, `images: field([File])` for many (see `libs/shared/lib/user/user.constant.ts`, `libs/shared/lib/banner/banner.constant.ts`).
57
+ - The store then auto-generates an `upload<Field>On<Model>(fileList)` action that calls the framework upload mutation and polls file status until it leaves `"uploading"` (`pkgs/akanjs/store/action.ts`).
58
+ - Storage is wired through the `StorageAdaptor` DI role (default `BlobStorage`, `pkgs/akanjs/service/predefinedAdaptor/storage.adaptor.ts`); the reference implementation is the `file` lib (`libs/shared/lib/file/*`). Do not hand-roll data-URL fallbacks.
@@ -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.
@@ -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,26 +251,120 @@ 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
 
258
- ## Comments
259
-
260
- - Never add comments in code by default.
261
- - Prefer clear names and structure so the code needs no explanation.
262
- - Do not narrate what the code already says.
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.
263
357
  - Do not add JSDoc, section banners, or "why/how" comments for ordinary logic.
264
- - Comments are allowed only for these purposes:
265
- 1. `TODO` unfinished work that must be tracked in-code
266
- 2. `FIXME` known broken or incorrect behavior that must be fixed
267
- 3. `XXX` dangerous / surprising hazard that a reader must not miss
268
- 4. Deletion caution warn why removing a line or block would break something non-obvious
269
- - Keep allowed comments one short line when possible.
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.
270
368
  - Match nearby file style: if the surrounding code has few comments, keep it that way.
271
369
 
272
370
  ## Generated File Tracker (Quick Reference)
@@ -286,7 +384,7 @@ These files are regenerated by `akan sync` and overwritten on every sync. **Do n
286
384
  | `apps/*/client.ts` | App-wide client barrel | The `fetch` and `st` instances |
287
385
  | `apps/*/server.ts` | App-wide server barrel | Server-side service resolution |
288
386
  | `*/lib/**/index.ts` | Per-module barrel | Module-level re-exports |
289
- | `*/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) |
290
388
  | `*/webkit/index.ts` | All webkit files | Webkit barrel |
291
389
  | `*/srvkit/index.ts` | All srvkit files | Srvkit barrel |
292
390
  | `*/common/index.ts` | All common files | Common barrel |
@@ -318,9 +416,10 @@ export class TaskInput extends via((field) => ({
318
416
  export class LightTask extends via(TaskObject, ["title", "priority", "status", "due"] as const, () => ({})) {}
319
417
 
320
418
  // 2. apps/<app>/lib/<model>/<model>.dictionary.ts
321
- // 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]).
322
421
  .model<Task>((t) => ({
323
- priority: t(["Priority", "우선순위"]),
422
+ priority: t(["Priority", "우선순위"]).desc(["How urgent the task is", "할 일의 긴급도"]),
324
423
  }))
325
424
  .enum<TaskPriority>("taskPriority", (t) => ({
326
425
  low: t(["Low", "낮음"]),
@@ -358,6 +457,11 @@ Three patterns: injecting an **external adapter** (`use<>()`), another **module'
358
457
  or a **predefined framework adapter** (`plug()`). A field named `<refName>Service` resolves to the service
359
458
  registered under `<refName>` — the `Service`/`Signal` suffix is required and stripped to derive the lookup key.
360
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
+
361
465
  > `apps/<app>/lib/option.ts` is a **user-owned** file scaffolded once — edit it to register adapters/DI. Unlike the
362
466
  > barrels (`cnst.ts`, `db.ts`, `srv.ts`, …) it is **not** overwritten by `akan sync`, so your `.use(...)` registrations
363
467
  > are safe.
@@ -468,14 +572,11 @@ export class TaskFilter extends from(cnst.Task, (filter) => ({
468
572
  })),
469
573
  }))
470
574
 
471
- // 4. In page — Init slice from loader, render with Zone
472
- loader={async () => {
473
- const { taskInitInTodo } = await fetch.initTaskInTodo();
474
- return { taskInitInTodo };
475
- }}
476
- render={({ data: { taskInitInTodo } }) => (
477
- <Task.Zone.Card init={taskInitInTodo} sliceName="taskInTodo" />
478
- )}
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
+ }
479
580
  ```
480
581
 
481
582
  The slice name in code uses camelCase (`inTodo`). In dictionary and components it becomes `"taskInTodo"`.
@@ -642,6 +743,17 @@ A short list of things the type system does not always catch:
642
743
  - **Reading a secret field needs an explicit select.** `field(...).secret()` values (e.g. `passwordHash`) are stripped
643
744
  from query results by default. Fetch them with `{ select: { <field>: true } }`, e.g.
644
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.
645
757
 
646
758
  ## Current User, Guards & Auth-Gated Pages
647
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. |