@akanjs/cli 3.0.0-alpha.8 → 3.0.0-alpha.9
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 +1 -1
- package/{agent.command-pggr92jj.js → agent.command-f0gqkfgz.js} +7 -6
- package/{application.command-692yap1d.js → application.command-x903m2qq.js} +5 -4
- package/buildBatch.proc.js +2 -1
- package/{capacitorApp-p0z5yab2.js → capacitorApp-9xswcgx7.js} +3 -2
- package/{cloud.command-wqfsyzv2.js → cloud.command-zs6kyf83.js} +8 -7
- package/{context.command-9bxtdn0w.js → context.command-fmepgjzg.js} +14 -13
- package/getDirname-m9s0cmhq.js +8 -0
- package/{guideline.command-nfqahxex.js → guideline.command-n6sw0a0c.js} +5 -4
- package/guidelines/conventions/conventions.generate.json +25 -0
- package/guidelines/conventions/conventions.instruction.md +668 -0
- package/guidelines/workspaceOnboarding/workspaceOnboarding.generate.json +24 -0
- package/guidelines/workspaceOnboarding/workspaceOnboarding.instruction.md +972 -0
- package/incrementalBuilder.proc.js +2 -1
- package/index-1577bej2.js +7 -0
- package/{index-bz2sfzzt.js → index-3vta1tqa.js} +4 -4
- package/{index-p3f4q532.js → index-4m0bphac.js} +3 -3
- package/{index-w6wbb612.js → index-5sn5w2mt.js} +13 -13
- package/{index-5dw57f0d.js → index-62955kn5.js} +6 -6
- package/{index-mssqn2sq.js → index-774gcx29.js} +43 -4
- package/{index-xhgtkekh.js → index-779ske1y.js} +1 -1
- package/{index-js368gk9.js → index-82jrh4hj.js} +5 -5
- package/{index-a1e0j63x.js → index-d43t8af2.js} +2 -2
- package/{index-gex7c8kd.js → index-eyyf2149.js} +34 -6
- package/{index-azv0xjb5.js → index-gcv8ncfh.js} +2 -2
- package/{index-7myvn0f0.js → index-j37qq1f2.js} +1 -1
- package/{index-ztpw544k.js → index-jf2ya7r2.js} +3 -3
- package/{index-vtj3zps1.js → index-jhpa13zj.js} +7 -7
- package/{index-6rhabeev.js → index-kdh76j26.js} +4 -2
- package/{index-5pmx7bkg.js → index-qzdjd4ct.js} +3 -3
- package/{index-2c3e2xh8.js → index-rxkpk7n4.js} +1 -1
- package/{index-4hek879p.js → index-xmmzf150.js} +1 -1
- package/{index-gmy45vz2.js → index-y4vkyhvv.js} +3 -3
- package/{index-vg0sj9nc.js → index-z8ptfkzv.js} +56 -18
- package/index.js +19 -18
- package/{library.command-md9715jz.js → library.command-cv9gdbmg.js} +4 -3
- package/{localRegistry.command-exkbamyj.js → localRegistry.command-d8fg36zc.js} +7 -6
- package/{module.command-w5aaddxk.js → module.command-qyr1s9cn.js} +7 -6
- package/{package.command-yfkrcy0z.js → package.command-wsjx0k47.js} +4 -3
- package/package.json +2 -2
- package/{page.command-echbavp4.js → page.command-rdzg65rm.js} +4 -3
- package/{primitive.command-9fbhspw2.js → primitive.command-89g38b53.js} +8 -7
- package/{quality.command-3pnt9yep.js → quality.command-0cs2z92d.js} +3 -2
- package/{repair.command-carj4vax.js → repair.command-6fphxfwj.js} +6 -5
- package/{scalar.command-51ryzypc.js → scalar.command-xfer69s6.js} +6 -5
- package/templates/crudPages/[__model__Id]/edit/page.tsx +1 -1
- package/templates/crudPages/new/page.tsx +1 -1
- package/templates/crudSinglePage/page.tsx +1 -1
- package/templates/module/__Model__.Util.tsx +1 -1
- package/templates/module/__Model__.Zone.tsx +3 -5
- package/templates/workspaceRoot/AGENTS.md.template +0 -972
- package/templates/workspaceRoot/biome.json.template +3 -244
- package/{workflow.command-91wmvm8k.js → workflow.command-2ygfqt4f.js} +13 -12
- package/{workspace.command-wa10mfd9.js → workspace.command-88m0zs64.js} +35 -33
- package/templates/workspaceRoot/.cursor/rules/akan-page-routing.mdc.template +0 -43
- package/templates/workspaceRoot/.cursor/rules/akan-sync-conventions.mdc.template +0 -34
- package/templates/workspaceRoot/.cursor/rules/application-test-commands.mdc.template +0 -16
- package/templates/workspaceRoot/.cursor/rules/change-scope.mdc.template +0 -12
- package/templates/workspaceRoot/.cursor/rules/client-server-boundaries.mdc.template +0 -18
- package/templates/workspaceRoot/.cursor/rules/coding-comments.mdc.template +0 -25
- package/templates/workspaceRoot/.cursor/rules/coding-style.mdc.template +0 -44
- package/templates/workspaceRoot/.cursor/rules/domain-module-conventions.mdc.template +0 -17
- package/templates/workspaceRoot/.cursor/rules/format-lint.mdc.template +0 -12
- package/templates/workspaceRoot/.cursor/rules/lint-enforced-rules.mdc.template +0 -19
- package/templates/workspaceRoot/.cursor/rules/module-file-playbook.mdc.template +0 -62
- package/templates/workspaceRoot/.cursor/rules/naming-conventions.mdc.template +0 -15
- package/templates/workspaceRoot/.cursor/rules/react-component-style.mdc.template +0 -21
- package/templates/workspaceRoot/.cursor/rules/repo-overview.mdc.template +0 -17
- package/templates/workspaceRoot/.cursor/rules/scalar-modeling.mdc.template +0 -99
- package/templates/workspaceRoot/.cursor/rules/secrets-and-env.mdc.template +0 -13
- package/templates/workspaceRoot/.cursor/rules/service-signal-conventions.mdc.template +0 -83
- package/templates/workspaceRoot/.cursor/rules/srvkit-adapters-and-guards.mdc.template +0 -50
- package/templates/workspaceRoot/.cursor/rules/ssr-first.mdc.template +0 -59
- package/templates/workspaceRoot/.cursor/rules/typescript-imports.mdc.template +0 -16
|
@@ -0,0 +1,668 @@
|
|
|
1
|
+
## Repo Overview
|
|
2
|
+
|
|
3
|
+
- Akan.js is a full-stack TypeScript framework for building all-stack applications at once.
|
|
4
|
+
- Write one line and deploy across web, app, server, database, and infrastructure.
|
|
5
|
+
- Akan prioritizes actual business code by abstracting technical implementation details as much as possible.
|
|
6
|
+
- The goal is minimal code, high performance, and type-safe services that can deploy to web, mobile, server, and DB infrastructure together.
|
|
7
|
+
- This is a Bun-first Akan.js monorepo.
|
|
8
|
+
- Main top-level areas are `apps/`, `libs/`, `pkgs/`, and `infra/`.
|
|
9
|
+
- `apps/<app>` contains app-level pages, domain code, UI, env files, and `akan.config.ts`.
|
|
10
|
+
- `libs/*` contains shared domain and utility libraries.
|
|
11
|
+
- `pkgs/akanjs/*` contains framework, runtime, and tooling facets published through the single `akanjs` package. Prefer extending existing Akan facets before adding new framework-level patterns.
|
|
12
|
+
- `infra/` contains Helm, deployment templates, edge configs, and Jenkins env/secret scripts.
|
|
13
|
+
|
|
14
|
+
## Change Scope
|
|
15
|
+
|
|
16
|
+
- Keep edits scoped to the requested task and the directly related files.
|
|
17
|
+
- Do not reformat unrelated files or rewrite nearby code just for style.
|
|
18
|
+
- Do not revert or overwrite unrelated user changes in a dirty working tree.
|
|
19
|
+
- Prefer established nearby patterns over introducing a new abstraction.
|
|
20
|
+
- Add new abstractions only when they remove real duplication or match an existing project pattern.
|
|
21
|
+
|
|
22
|
+
## Lint-Enforced Rules (These Break The Build)
|
|
23
|
+
|
|
24
|
+
Enforced by `biome.json` and the grit plugins in `pkgs/@akanjs/devkit/lint/`. Several of them produce output
|
|
25
|
+
that looks wrong; do not "fix" it back.
|
|
26
|
+
|
|
27
|
+
- **Never hand-order Tailwind classes.** `nursery/useSortedClasses` is an error and also sorts the string
|
|
28
|
+
arguments to `cn()`. Sorter output such as `font-bold text-2xl text-base-content` or
|
|
29
|
+
`border-base-content/5 border-t` is correct. Write the classes in any order, run the formatter, leave the result.
|
|
30
|
+
- **Stay inside the color vocabulary.** Vocabulary closure strips the raw Tailwind palette, so these render as
|
|
31
|
+
no CSS and fail lint (`no-raw-palette-class.grit`, `no-arbitrary-color.grit`, `no-daisyui-legacy-class.grit`,
|
|
32
|
+
`no-inline-color.grit`): raw palette classes (`bg-blue-500`), arbitrary color values (`bg-[#3b82f6]`), daisyUI
|
|
33
|
+
legacy classes (`btn-primary`, `card-body`), and color literals in `style={{...}}`. Use semantic tokens
|
|
34
|
+
(`bg-primary`, `text-foreground/70`). A legitimate fixed color (OS-chrome mockups, data-viz) takes a
|
|
35
|
+
`// biome-ignore lint/plugin: <reason>` with the reason spelled out. `apps/akan/page/v1/**` is excluded.
|
|
36
|
+
- **Never `throw new Error`.** Throw `new Err("<module>.error.<key>")` and register the key as `[en, ko]` in that
|
|
37
|
+
module's dictionary `.error({})`. Import `Err` from `"../dict"` on the server and from `"@libs/<lib>/client"` or
|
|
38
|
+
`"@apps/<app>/client"` in UI. `no-throw-raw-error.grit` exempts `*.test.ts`, `*.spec.ts`, `*.constant.ts`,
|
|
39
|
+
`common/**`, and `apps/akan/env/**` — `common/` and `env/` have no legal `Err` import path, so keep throwing code
|
|
40
|
+
out of them.
|
|
41
|
+
- **Never import a third-party package** from `page/**`, from any barrel, or from any
|
|
42
|
+
`*.{constant,dictionary,document,service,signal,store}.ts` / `*.{Template,Unit,Util,View,Zone}.tsx`
|
|
43
|
+
(`no-import-external-library.grit`). Re-export the symbol through a lib first. One-line re-export shims such as
|
|
44
|
+
`libs/<lib>/base/<pkg>.ts` and `libs/<lib>/webkit/<hook>.ts` exist for exactly this reason — they are
|
|
45
|
+
load-bearing, not cruft. Do not delete them.
|
|
46
|
+
- **`#private` is banned in exactly four file suffixes:** `*.constant.ts`, `*.document.ts`, `*.service.ts`, and
|
|
47
|
+
`*.store.ts` (`no-js-private-class-method.grit`). The rule is scoped by file path, not by class shape, so
|
|
48
|
+
`#private` remains the house style everywhere under `srvkit/`, including `adapt()` adapter classes.
|
|
49
|
+
- **No `console.log` / `console.debug`.** Only `assert`, `error`, `info`, and `warn` are allowed. Server code uses
|
|
50
|
+
the injected `this.logger.*` or `new Logger("ClassName")`.
|
|
51
|
+
- **Never write a `//!` marker in browser-reachable code** — `ui/`, `webkit/`, `common/`, `page/**/*.tsx`,
|
|
52
|
+
`*.constant.ts`, `*.store.ts`, and the five module component suffixes (`no-bang-comment-in-client.grit`). Bun
|
|
53
|
+
classifies `//!` and `/*!` as legal comments and keeps them through minification, so the note ships to every
|
|
54
|
+
visitor. Use `// FIXME:` there; `//!` stays legal in server, `srvkit/`, and CLI files.
|
|
55
|
+
- **Never return a value from a store action** (`no-return-in-store-action.grit`). Every method of a `store(...)`
|
|
56
|
+
class dispatches through `st.do.<action>()`, which is typed `void` / `Promise<void>`, so the value is
|
|
57
|
+
unreachable — write it into state with `this.set({ ... })`. A bare `return;` guard, a `return` inside a nested
|
|
58
|
+
callback, a getter, and a `static` helper are all still fine.
|
|
59
|
+
- **Never redeclare a generated CRUD endpoint name** in `*.signal.ts` (`no-redeclare-predefined-endpoint.grit`).
|
|
60
|
+
- **No deep imports past a barrel** (`no-deep-internal-import.grit`). Cross-module constant references such as
|
|
61
|
+
`../map/map.constant` are the sanctioned exception.
|
|
62
|
+
- **Never import across the client/server boundary.** Client files (`ui/`, `webkit/`, `page/`, `*.store.ts`, every
|
|
63
|
+
`.tsx`) may not import a `*.document.ts` / `*.dictionary.ts` / `*.service.ts` / `*.signal.ts`, `srvkit/`, a
|
|
64
|
+
package `server` entrypoint, or the `db` / `srv` / `sig` / `dict` / `option` / `useServer` barrels
|
|
65
|
+
(`no-import-server-in-client.grit`). Server files (those four suffixes plus `srvkit/`) may not import a
|
|
66
|
+
`*.store.ts`, a module component, `ui/`, `webkit/`, a package `client` entrypoint, or the `st` / `store` /
|
|
67
|
+
`useClient` barrels (`no-import-client-in-server.grit`). Shared files — `common/` and `*.constant.ts` — are held
|
|
68
|
+
to **both**, so they reach neither side. `import type` is erased before bundling and stays legal in every
|
|
69
|
+
direction; a mixed value-and-type import is not exempt. Scoped to `apps/**` and `libs/**`: `pkgs/akanjs/**`
|
|
70
|
+
implements the boundary and is where the two graphs legitimately meet.
|
|
71
|
+
- **Server-component discipline** is enforced on `page/**`, `*.Unit.tsx`, and `*.View.tsx`
|
|
72
|
+
(`no-import-client-functions.grit`, `no-use-client-in-server.grit`, `non-scalar-props-restricted.grit`).
|
|
73
|
+
- `noArrayIndexKey` and `useExhaustiveDependencies` are **off** on purpose: `key={idx}` for embedded scalars and
|
|
74
|
+
short dependency arrays are intentional, not oversights.
|
|
75
|
+
|
|
76
|
+
## Coding Style (`**/*.{ts,tsx}`)
|
|
77
|
+
|
|
78
|
+
- For large units of work, prefer declaring a class and running the flow through an instance instead of scattering many standalone functions.
|
|
79
|
+
- Prefer class methods or `static` methods over unrelated top-level helper functions when the logic belongs to a class-level workflow.
|
|
80
|
+
- 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.
|
|
81
|
+
- In files that declare a class, avoid top-level functions or variables when they can reasonably live inside the class.
|
|
82
|
+
- Prefer `const` function expressions over `function` declarations unless hoisting, overloads, generators, or framework conventions make `function` the better fit.
|
|
83
|
+
- Prefer declaring only one class per file; split the file when two or more class declarations are needed.
|
|
84
|
+
- For class-centered modules, prefer noun-style filenames that match the primary class name, such as `RouteClientBuilder.ts`, instead of verb-style wrapper filenames like `buildRouteClient.ts`.
|
|
85
|
+
- Avoid keeping exported functions that only instantiate a class and immediately call one method. Prefer migrating callers to instantiate the class directly.
|
|
86
|
+
- Except for React component files or convention files, TypeScript filenames should use camelCase.
|
|
87
|
+
- 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.
|
|
88
|
+
|
|
89
|
+
### File Size And Duplication
|
|
90
|
+
|
|
91
|
+
- Keep files small. The house median is well under 50 lines; split a component before it reaches ~150 lines rather than adding section comments.
|
|
92
|
+
- 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.
|
|
93
|
+
- Never add a sibling helper file inside `lib/<model>/`. Helpers go to `common/`, `webkit/`, `srvkit/`, or `ui/`.
|
|
94
|
+
- 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.
|
|
95
|
+
|
|
96
|
+
### TypeScript Shape
|
|
97
|
+
|
|
98
|
+
- Use `interface` for object shapes and `type` only for unions and aliases.
|
|
99
|
+
- 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.
|
|
100
|
+
- Never use a non-null assertion. Narrow with `?.`, an early return, or a type predicate such as `.filter((id): id is string => !!id)`.
|
|
101
|
+
- Escape with `as unknown as T`. Never `as any`.
|
|
102
|
+
- Never annotate a component's return type. Annotate a helper only when the return is a union, a tuple, or a type predicate.
|
|
103
|
+
- 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.
|
|
104
|
+
- Async functions carry no `Async` suffix.
|
|
105
|
+
|
|
106
|
+
### Test Code
|
|
107
|
+
|
|
108
|
+
- Write TypeScript tests with Bun's test runner and import `describe`, `expect`, and `test` from `bun:test`.
|
|
109
|
+
- Keep tests colocated with the source they cover using `*.test.ts` or `*.spec.ts`, following the existing nearby pattern.
|
|
110
|
+
- Prefer focused behavior tests for public contracts and edge cases over implementation-detail assertions.
|
|
111
|
+
- Run package suites with `bun run akan test <pkg>` from the repo root, or `cd <pkg> && bun test --isolate`. Plain `bun test` without `--isolate` shares one global object across test files and fails dozens of tests from cross-file state pollution (`bunfig.toml` `[test] isolate` is not honored as of Bun 1.3), and running `bun test` from the repo root breaks subprocess stdio pipes.
|
|
112
|
+
- 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()`.
|
|
113
|
+
- `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.
|
|
114
|
+
- A placeholder `it` with a descriptive title is an acceptable floor. Write a real suite when the behaviour is security-relevant.
|
|
115
|
+
|
|
116
|
+
## Comments
|
|
117
|
+
|
|
118
|
+
Do not narrate code. Do document the thing the code cannot say. Both halves are the rule.
|
|
119
|
+
|
|
120
|
+
- Never add a comment that restates the identifier, the signature, or the control flow.
|
|
121
|
+
- Prefer clear names and structure so ordinary logic needs no explanation.
|
|
122
|
+
- Do not add JSDoc, section banners, or "why/how" comments for ordinary logic.
|
|
123
|
+
- 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.
|
|
124
|
+
- 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.
|
|
125
|
+
- In-code markers:
|
|
126
|
+
1. `TODO` — unfinished work that must be tracked in-code
|
|
127
|
+
2. `FIXME` — known broken or incorrect behavior that must be fixed
|
|
128
|
+
3. `XXX` — dangerous / surprising hazard that a reader must not miss
|
|
129
|
+
4. `//!` — disabled or must-fix code. **Server, `srvkit/`, and CLI files only.** Bun's bundler treats `//!`
|
|
130
|
+
(and `/*!`) as a legal comment and keeps it through minification, so in browser-reachable code the note
|
|
131
|
+
ships verbatim to every visitor. Use `// FIXME:` there instead; `no-bang-comment-in-client.grit` enforces it.
|
|
132
|
+
5. `//?` — an explanatory aside
|
|
133
|
+
6. `//*` — a design note
|
|
134
|
+
7. Deletion caution — warn why removing a line or block would break something non-obvious
|
|
135
|
+
- Keep allowed comments one short line when possible.
|
|
136
|
+
- Every suppression carries a reason: `// biome-ignore lint/<rule>: <why>`. Never a bare disable block.
|
|
137
|
+
- Match nearby file style: if the surrounding code has few comments, keep it that way.
|
|
138
|
+
|
|
139
|
+
## TypeScript And Imports (`**/*.{ts,tsx}`)
|
|
140
|
+
|
|
141
|
+
- Use Bun and ESM assumptions from the root `tsconfig.json`.
|
|
142
|
+
- Prefer path aliases over deep relative imports when crossing package boundaries.
|
|
143
|
+
- Use `akanjs/*` for framework facets, `@apps/*` for apps, `@libs/*` for shared libs, and `@contract/*` for contract code.
|
|
144
|
+
- Respect existing client/server entrypoints such as `@libs/shared/client`, `@libs/shared/server`, `@apps/akasys/client`, and `@apps/akasys/server`.
|
|
145
|
+
- Let Biome organize imports instead of manually reshuffling unrelated imports.
|
|
146
|
+
- 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.
|
|
147
|
+
- In `.tsx` files use one flat named import from the package client path (`import { cnst, fetch, st, Ticket, usePage } from "@apps/<app>/client"`) — never a relative `../` import.
|
|
148
|
+
|
|
149
|
+
## Formatting And Linting
|
|
150
|
+
|
|
151
|
+
- Use Biome as the formatter and linter.
|
|
152
|
+
- Format with `bun run akan lint <appName>` from the repo root.
|
|
153
|
+
- Keep formatting consistent with `biome.json`: 2-space indentation, 120 line width, and double quotes for JS/TS.
|
|
154
|
+
- Avoid adding new `console` usage except accepted methods such as `console.error`, `console.info`, and `console.warn`.
|
|
155
|
+
- Do not make broad formatting-only changes in unrelated files.
|
|
156
|
+
|
|
157
|
+
## Client / Server Boundaries (`apps/**`, `libs/**`, `pkgs/akanjs/**`)
|
|
158
|
+
|
|
159
|
+
- Use `"use client";` at the top of client component files.
|
|
160
|
+
- Be careful when importing client-only code from page or layout modules.
|
|
161
|
+
- Keep page props serializable unless the existing route pattern clearly allows otherwise.
|
|
162
|
+
- 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.
|
|
163
|
+
- Preserve established domain file roles such as `.document.ts`, `.service.ts`, `.store.ts`, `.constant.ts`, and `.client.ts`.
|
|
164
|
+
- When unsure, inspect nearby files in the same app or package before introducing a new boundary pattern.
|
|
165
|
+
|
|
166
|
+
## SSR First — Server Rendering Is The Default
|
|
167
|
+
|
|
168
|
+
Akan is SSR-first. Every JSX element that renders on the server ships as HTML and costs nothing to hydrate;
|
|
169
|
+
every element behind `"use client"` ships twice — as markup and as bundled JS that must re-run in the browser.
|
|
170
|
+
The boundary is not about which file *may* be client, it is about **how little** ends up on the client side.
|
|
171
|
+
|
|
172
|
+
**The default is server. `"use client"` is a cost you justify per component, not a habit.** A component earns the
|
|
173
|
+
directive only by using a client-only capability: a React hook, a JSX event handler, the store (`st.use.*` /
|
|
174
|
+
`st.do.*`), a browser global, or a client-only third-party package. Rendering markup, reading a param, calling
|
|
175
|
+
`l()`, and mapping over data are all server work.
|
|
176
|
+
|
|
177
|
+
Measure before and after with `akan quality ssr` (`--format json` for tooling). It prints the server render share
|
|
178
|
+
per app and lib — server-rendered JSX elements over total — and the SSR warnings below. Treat **50% server share
|
|
179
|
+
as the floor** for an app or lib and a **falling share as a regression**: if a change moves markup to the client,
|
|
180
|
+
say why in the PR or move it back.
|
|
181
|
+
|
|
182
|
+
### What `akan quality ssr` Flags
|
|
183
|
+
|
|
184
|
+
| Rule | Means |
|
|
185
|
+
|---|---|
|
|
186
|
+
| `akan.ssr.unnecessary-use-client` | The directive is there but nothing in the file needs it. Delete it. |
|
|
187
|
+
| `akan.ssr.client-static-component` | A component in a client file renders real markup with zero client-only capability — pure server work sitting in the bundle. |
|
|
188
|
+
| `akan.ssr.client-static-markup` | A large subtree wraps one or two interactive touches. Split it: interaction stays client, markup goes server. |
|
|
189
|
+
| `akan.ssr.client-mount-load` | A `useEffect(…, [])` loads server data. The route can fetch it before the first byte. |
|
|
190
|
+
| `akan.ssr.module-missing-server-view` | A module renders only from `Template`/`Zone`/`Util` and has no `Unit`/`View` at all. |
|
|
191
|
+
| `akan.ssr.template-client-state` | A `Template` holds form state in `useState` instead of the store. |
|
|
192
|
+
|
|
193
|
+
A third-party client package or an `index_.tsx` `lazy()` boundary is a legitimate reason for the directive and is
|
|
194
|
+
not flagged. Interaction-driven `fetch.*` (a lookup inside `onClick`) is not flagged either — only mount-time loads
|
|
195
|
+
are, because those are the ones the server could have done.
|
|
196
|
+
|
|
197
|
+
### Server-Side Implementation Playbook
|
|
198
|
+
|
|
199
|
+
**① Wrap the interaction, not the UI.** The smallest useful client component is a shell that adds one behaviour and
|
|
200
|
+
renders `children` untouched. The children stay server components, so the markup inside them never reaches the
|
|
201
|
+
bundle. `libs/shared/ui/Only/User.tsx` is the shape: it reads auth state on the client and returns `{children}`.
|
|
202
|
+
|
|
203
|
+
```tsx
|
|
204
|
+
"use client";
|
|
205
|
+
export const ClickWrapper = ({ children, onPick }: ClickWrapperProps) => (
|
|
206
|
+
<div onClick={onPick}>{children}</div>
|
|
207
|
+
);
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
**② Split compound components so panels stay on the server.** A tab, accordion, or disclosure needs client state
|
|
211
|
+
only for *which* part is visible — never for what the parts contain. Split into a context provider plus menu and
|
|
212
|
+
panel pieces, and take panel content as `children`. `Tab` / `Tab.Menus` / `Tab.Menu` / `Tab.Panel` in `akanjs/ui`
|
|
213
|
+
is exactly this: only the provider and the menu hold state, and `<Tab.Panel>` renders its children as-is, so a
|
|
214
|
+
server `Unit`/`View` passed in stays server-rendered. Never reach for one `"use client"` file with a mode
|
|
215
|
+
`useState` and every panel body inlined.
|
|
216
|
+
|
|
217
|
+
**③ Sync state instead of fetching it.** A server component cannot hold state, so render the initial data on the
|
|
218
|
+
server and hand it across the boundary as a serializable object. That is what `init` / `view` props are: the route
|
|
219
|
+
calls `fetch.initXInY(...)` / `fetch.viewX(...)`, passes the result into a `Zone`, and `Load.Units` / `Load.View`
|
|
220
|
+
hydrate the store from it. Never replace that with a `useEffect(…, [])` that fetches on mount — it renders an empty
|
|
221
|
+
shell, hydrates, then round-trips for data the server already had.
|
|
222
|
+
|
|
223
|
+
**④ Push the boundary down to the leaf that needs it.** When a `Zone` reads the store, it should hold *zero*
|
|
224
|
+
markup and delegate to a server `View`. `User.Zone.Self` is one line — `st.use.self()` into
|
|
225
|
+
`<User.View.General user={self} />` — so the whole detail surface renders server-side wherever a route uses the
|
|
226
|
+
`View` directly.
|
|
227
|
+
|
|
228
|
+
**⑤ Hand the promise across, not the awaited value.** `ClientInit` / `ClientView` are `PromiseOrObject<T>`, so a
|
|
229
|
+
route may pass an unawaited `fetch.initX(...)`; `Load.*` renders a skeleton and resolves it. `await` in the route
|
|
230
|
+
blocks the shell for data the page needs immediately; passing the promise streams the rest. Independent fetches
|
|
231
|
+
still go through one `Promise.all`.
|
|
232
|
+
|
|
233
|
+
**⑥ Use named `ReactNode` slots, not just `children`.** A client shell can take several server-rendered subtrees:
|
|
234
|
+
`Layout.Navbar` accepts `title`, `back`, `left`, `right`, and `children`, so a client navbar composes server
|
|
235
|
+
content in five places instead of absorbing it.
|
|
236
|
+
|
|
237
|
+
**⑦ Let the server do the derived work.** Display and predicate logic belongs on `Light<Model>` (`isNew()`,
|
|
238
|
+
`canWrite(user?)`, `formatTimes()`), and enum→class lookups belong in a module-scope `as const` map. Both sides
|
|
239
|
+
call the same method, so the server can render the result — a client component that exists only to compute a label
|
|
240
|
+
is markup in the wrong place.
|
|
241
|
+
|
|
242
|
+
**⑧ Gate auth on the server.** `getSelf({ unauthorize: "/signin" })` in `_layout.tsx` redirects before any HTML is
|
|
243
|
+
sent. A client-side auth check costs a hydration round-trip and flashes the wrong UI first.
|
|
244
|
+
|
|
245
|
+
**⑨ Prefer CSS over client state for pure visibility.** Toggling with a `data-*` attribute plus `group-data-[…]`
|
|
246
|
+
variants (see `libs/util/ui/Grid/*`) or with `<details>`/`<summary>` keeps both branches server-rendered. Reach for
|
|
247
|
+
`useState` when the state is real, not when a variant would do.
|
|
248
|
+
|
|
249
|
+
**⑩ Keep the heavy island out of the first load.** A large client-only widget goes behind the
|
|
250
|
+
`ui/<Folder>/index_.tsx` + `lazy()` pair so the server renders the page around it. `usePage()` and `l()` work in
|
|
251
|
+
server components, so translation never forces a boundary.
|
|
252
|
+
|
|
253
|
+
Full version with code, the `Tab` composition example, and a review checklist: `get_guideline` with `ssrRule`, or
|
|
254
|
+
`akan guideline show ssrRule`.
|
|
255
|
+
|
|
256
|
+
## React Components And Styling (`**/*.tsx`)
|
|
257
|
+
|
|
258
|
+
- Components are `export const X = ({ … }: XProps) => { return (…); };` — arrow const with a block body. `export default` is reserved for pages, layouts, and `lazy()` targets.
|
|
259
|
+
- Never `React.FC`, never `defaultProps`, never `PropsWithChildren`. Defaults go in the destructuring (`prefix = ""`); children are typed `children: ReactNode`.
|
|
260
|
+
- `"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.
|
|
261
|
+
- 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.
|
|
262
|
+
- 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.
|
|
263
|
+
- 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`.
|
|
264
|
+
- 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`.
|
|
265
|
+
- Read with `st.use.*` and write with `st.do.*`. Client components do not call `fetch.*`.
|
|
266
|
+
- Static class strings stay plain strings. Reach for `cn` only for a conditional or to merge an incoming `className`, and merge the caller last: `cn("base classes", cond && "extra", className)`. `cn` comes from `akanjs/client` (token-aware tailwind-merge) and is the only class-combining function — no `clsx` (removed), no raw `twMerge` imports, no object syntax (`{ x: cond }` → `cond && "x"`).
|
|
267
|
+
- Multi-slot components take extra named props (`wrapperClassName`, `bodyClassName`), never a `classNames` object.
|
|
268
|
+
- 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.
|
|
269
|
+
- 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.
|
|
270
|
+
- Use `<Link>` from `akanjs/ui` for internal navigation; `<a>` only for `mailto:` and external links.
|
|
271
|
+
|
|
272
|
+
## Naming And Language
|
|
273
|
+
|
|
274
|
+
- 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`).
|
|
275
|
+
- 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`.
|
|
276
|
+
- Slice and filter names are prepositional: `inOrg`, `inProject`, `inPeriod`, `byStatuses`, `ofPortfolio`. Never `getXInY`, never `listX`.
|
|
277
|
+
- Handlers are `onX` props taking inline arrows. Do not extract a `handleX`.
|
|
278
|
+
- 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`.
|
|
279
|
+
- 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.
|
|
280
|
+
- 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`.
|
|
281
|
+
- 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 `다.`.
|
|
282
|
+
|
|
283
|
+
## Domain Module Conventions (`apps/**/lib/**`, `libs/**/lib/**`)
|
|
284
|
+
|
|
285
|
+
- Organize business concepts as domain folders under `lib/`; keep related schema, service, signal, store, and UI files together.
|
|
286
|
+
- Use lowercase logic files such as `<model>.constant.ts`, `<model>.document.ts`, `<model>.service.ts`, `<model>.signal.ts`, `<model>.dictionary.ts`, and `<model>.store.ts`.
|
|
287
|
+
- Use PascalCase React component files such as `<Model>.Template.tsx`, `<Model>.Unit.tsx`, `<Model>.View.tsx`, `<Model>.Zone.tsx`, and `<Model>.Util.tsx`.
|
|
288
|
+
- Treat `constant.ts`, `dictionary.ts`, and `signal.ts` as shared contract files that should avoid platform-specific dependencies.
|
|
289
|
+
- Keep backend persistence/query logic in `.document.ts` and domain business orchestration in `.service.ts`.
|
|
290
|
+
- Keep frontend state in `.store.ts`; use `Template` for forms, `Unit` for list/card items, `View` for details, `Zone` for composed page sections, and `Util` for domain-specific UI helpers.
|
|
291
|
+
|
|
292
|
+
### Module File Playbook
|
|
293
|
+
|
|
294
|
+
**`<model>.constant.ts`** — five classes in order with one blank line between them and `enumOf("camelName", [...] as const)`
|
|
295
|
+
classes above: `XInput → XObject → LightX → X → XInsight`. Write `XInsight` even when it is empty. Put display and
|
|
296
|
+
predicate logic on `LightX` (`isNew()`, `canWrite(user?)`, `formatTimes()`, `isCancellable()`) — the Light class is the
|
|
297
|
+
one both server and client hold, so shared logic belongs there instead of in a util module. This is the most commonly
|
|
298
|
+
missed rule in the codebase. Collection-level helpers go `static` on the full model. Give any field whose business
|
|
299
|
+
meaning is not obvious a short trailing comment.
|
|
300
|
+
|
|
301
|
+
**`<model>.document.ts`** — fixed order: `XFilter extends from(...)` → `X extends by(...)` → `XModel extends into(...)`,
|
|
302
|
+
with `sort: {}` always present. Chain methods validate → mutate → `return this`, and never `save()`; the caller saves,
|
|
303
|
+
so chains compose (`org.removeUser(id).removeInvite(id).save()`). Put a one-line comment above each stating the
|
|
304
|
+
transition. Atomic counters live on the Model class with the updater-callback form, returning `!!modifiedCount`.
|
|
305
|
+
Indexes and derived totals go in `static override _onSchema`, not in the service. **Removal is always soft** — the
|
|
306
|
+
model facade's `removeMany(query)` and the store's `removeManyByQuery` stamp `removedAt` like `remove(id)` does; the
|
|
307
|
+
framework has no hard delete for a model table, and `delete` is deliberately left unused so it can mean one later.
|
|
308
|
+
The facade keeps `Many`/`One` spelled out on its writes (`updateOne` / `updateMany` / `removeOne` / `removeMany`):
|
|
309
|
+
a bare `update`/`remove` would read like the document-path `update(id)` / `doc.remove()` while hitting every match.
|
|
310
|
+
Only the count was shortened — `count(query)`, with `countDocuments` kept as `@deprecated`. `updateById(id, update)`
|
|
311
|
+
and `removeById(id)` are those same query-level writes narrowed to one id, **not** the document path: they fire no
|
|
312
|
+
hooks either, so a model whose removal cascades or carries a `_postRemove` still goes through `remove<Model>(id)`.
|
|
313
|
+
|
|
314
|
+
**`<model>.service.ts`** — keep methods to a few lines: load → chain → `return await ….save()`. Write `return await`
|
|
315
|
+
explicitly in tail position; do not "optimize" it away. Side effects belong in `override async _preUpdate` /
|
|
316
|
+
`_postCreate`, not inline. Fire-and-forget is explicitly `void`-ed. Order deliberately: load every referenced document,
|
|
317
|
+
then save, then notify. Return `null` / `false` for "not allowed" or "not found" and let the signal decide whether that
|
|
318
|
+
is an error.
|
|
319
|
+
|
|
320
|
+
**`<model>.signal.ts`** — `XInternal` → `XSlice` → `XEndpoint`, all three declared even when empty. `exec` is a one-liner
|
|
321
|
+
delegating to the service.
|
|
322
|
+
|
|
323
|
+
**`<model>.store.ts`** — write a custom action only for a toast, an optimistic update, or a multi-field write; most
|
|
324
|
+
stores need none, because state and CRUD actions are generated. The body is three lines: `await fetch.X` →
|
|
325
|
+
`this.setX(...)` → toast. The optimistic shape is mutate the client model, `void fetch.*`, then commit. Use
|
|
326
|
+
`this.pick(...)` when the value must exist, `this.get()` when it may not, and `this.set({...})` to write. Mutate lists
|
|
327
|
+
through the collection API (`this.set({ xList: xList.set(x).save() })`), not array spread. **An action returns
|
|
328
|
+
nothing** — `st.do.<action>()` is typed `void` / `Promise<void>`, so hand the result to `this.set({...})` rather
|
|
329
|
+
than returning it (`no-return-in-store-action.grit`); a bare `return;` guard stays fine. **Never
|
|
330
|
+
`import type { RootStore } from "../st"`** — it crashes `akan build` with a Bun SSR segfault.
|
|
331
|
+
|
|
332
|
+
**`<model>.dictionary.ts`** — fixed chain, with empty stages still written:
|
|
333
|
+
`.of() → .model() → .insight() → .query() → .sort() → .enum() → .slice() → .endpoint() → .error() → .translate()`.
|
|
334
|
+
Name every argument in `.arg()`, including framework-supplied `skip` / `limit` / `sort`. Use `modelDictionary`,
|
|
335
|
+
`scalarDictionary`, or `serviceDictionary` to match the module kind.
|
|
336
|
+
|
|
337
|
+
**`<module>.abstract.md`** — a title line, one declarative sentence naming what the module owns, a `## Rules` list of
|
|
338
|
+
two to five invariants the code cannot show, and an optional workflow arrow chain
|
|
339
|
+
(`draft -> signed -> active -> completed`). Never restate field lists or types. Update it whenever an invariant or
|
|
340
|
+
workflow changes.
|
|
341
|
+
|
|
342
|
+
## Service And Signal Conventions (`*.{service,signal}.ts`, `server/**`)
|
|
343
|
+
|
|
344
|
+
- Keep domain business operations in `.service.ts` classes built with `serve(...)`.
|
|
345
|
+
- Keep execution contracts and triggers in `.signal.ts` classes built with `internal(...)`, `slice(...)`, and `endpoint(...)`.
|
|
346
|
+
- Use `Internal` for internal triggers such as init, interval, cron, or queue jobs.
|
|
347
|
+
- Use `Slice` for typed data views that feed client stores and zones; keep each slice focused on one purpose.
|
|
348
|
+
- Use `Endpoint` for query and mutation contracts exposed to callers.
|
|
349
|
+
- Connect external APIs or infrastructure through adapters, usually under `srvkit/`, and inject them into services instead of importing vendor clients directly into domain logic.
|
|
350
|
+
|
|
351
|
+
### Guards And Transports
|
|
352
|
+
|
|
353
|
+
- 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()`.
|
|
354
|
+
- 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.
|
|
355
|
+
- 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.
|
|
356
|
+
- 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.
|
|
357
|
+
|
|
358
|
+
### Authorization Defaults
|
|
359
|
+
|
|
360
|
+
- **Every `slice()` takes an explicit `{ guards: {…} }` second argument, and `root:` is always `Admin`.**
|
|
361
|
+
- **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.
|
|
362
|
+
- Resource guards are `Can<Verb><Model>` classes in `srvkit/guards.ts` that `implements Guard` with an `async canPass(context)`. They **fail closed**: no resource named ⇒ `false`; a load that throws ⇒ `logger.warn` then `false`. Admin bypass goes first.
|
|
363
|
+
- 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.
|
|
364
|
+
- The acting user arrives via `.with(Self)` / `.with(CurrentUserId)` / `.with(Me)`. Never trust a client-supplied id.
|
|
365
|
+
- 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.
|
|
366
|
+
- Services re-check ownership even when a guard already gated the call — two independent gates.
|
|
367
|
+
- `srvkit/guards.ts` earns real comments: explain what would leak without each guard.
|
|
368
|
+
|
|
369
|
+
### Signal Body Types
|
|
370
|
+
|
|
371
|
+
- `.body(...)` / `.param(...)` args accept `ConstantFieldTypeInput` only: scalars, model refs, or `enumOf(...)`.
|
|
372
|
+
- Numbers must use `Int` or `Float` — `Number` is rejected (`pkgs/akanjs/signal/endpointInfo.ts`).
|
|
373
|
+
- `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.
|
|
374
|
+
|
|
375
|
+
### Reserved Endpoint Names
|
|
376
|
+
|
|
377
|
+
- 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.
|
|
378
|
+
- 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.
|
|
379
|
+
|
|
380
|
+
### Slices, Queries, and Hydration
|
|
381
|
+
|
|
382
|
+
- A slice's `exec` returns a `QueryOf` (an opaque query descriptor, `pkgs/akanjs/constant/types.ts`); you **cannot** chain `.sort()`/`.limit()` on it.
|
|
383
|
+
- Apply ordering/paging via the store `init` fetch option instead: `initX(..., { sort, page, limit })` (`pkgs/akanjs/fetch/fetchType/sliceFetch.type.ts`).
|
|
384
|
+
- 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`).
|
|
385
|
+
- **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`).
|
|
386
|
+
- Every filter generates fourteen methods: `list` · `listIds` · `find` · `findId` · `pick` · `pickId` · `exists` ·
|
|
387
|
+
`count` · `insight` · `query` · **`remove`** · **`removeOne`** · **`update`** · **`updateOne`**. The last four are
|
|
388
|
+
query-level writes — one atomic UPDATE, **no hooks**, and therefore no `_postRemove` and no cascade. Use them on a
|
|
389
|
+
model that carries no removal side effect; otherwise remove documents one at a time.
|
|
390
|
+
- **`update<Filter>` / `updateOne<Filter>` are chains, not calls:** `await updateInRoot(rootId).set({ status:
|
|
391
|
+
"archived" })`. The patch cannot trail the filter args — a filter's own args may be optional and no tuple type
|
|
392
|
+
puts a required element after those — so it lands on a terminal `.set()`, mirroring the `UPDATE … SET …` it
|
|
393
|
+
compiles to. Building the chain touches nothing; only `.set()` runs a query.
|
|
394
|
+
- `removeOne` / `updateOne` hit the **newest** match — the subquery they compile to is ordered `createdAt` descending
|
|
395
|
+
and there is no way to change that. They also report only counts, never which row they touched, so they are for
|
|
396
|
+
"there is at most one of these", not for claiming the next item off a queue. Pass a query that matches one row.
|
|
397
|
+
- **A filter may not be keyed after its own model.** Filter methods are assigned after CRUD, so a filter `chat` on
|
|
398
|
+
model `chat` would silently swap the single-document `removeChat`/`updateChat` for a hookless query-level one. It
|
|
399
|
+
throws at boot instead (`assertFilterFitsCrud`).
|
|
400
|
+
|
|
401
|
+
### Text Search In A Filter — `q.search()`
|
|
402
|
+
|
|
403
|
+
- Text search is a filter query node like any other: `bySearch: filter().arg("text", String).query((text, q) =>
|
|
404
|
+
q.search(text, { prefix: true }))`. The generated `listBySearch` / `countBySearch` / `queryBySearch` /
|
|
405
|
+
`insightBySearch` come for free — you do **not** need a slice to make search usable.
|
|
406
|
+
- **Only add a search slice when the model's data is safe to enumerate.** A filter is server-side; a slice is a
|
|
407
|
+
client-callable endpoint, so on a model whose slice `get:` is `Public` a search slice hands anyone a way to walk the
|
|
408
|
+
table. Leave that decision to the mounting app.
|
|
409
|
+
- `q.search()` compiles to a JOIN, not a WHERE fragment, so it **must sit at an AND position**. Nesting it under
|
|
410
|
+
`q.any()` or `q.not()` throws, and it is rejected in `updateOneByQuery` / `updateManyByQuery` — a query-level write
|
|
411
|
+
takes no join, so ignoring the node would silently widen the write to every other matching row.
|
|
412
|
+
- Blank or whitespace-only input matches **nothing**. Never "fix" that into a passthrough: a passthrough turns a
|
|
413
|
+
search endpoint into a full listing.
|
|
414
|
+
- Order by relevance with the built-in `relevance` sort key. It is an empty sort map, which the store reads as
|
|
415
|
+
"unspecified": score order when a search join is present, `createdAt` descending otherwise. That fallback is the
|
|
416
|
+
compiler's own, not a model-defined default — redefining `latest` on the model does not change it.
|
|
417
|
+
- **A slice endpoint never reaches "unspecified".** The resolver fills `latest` before the query is built, so a
|
|
418
|
+
client asking for the score order has to name `relevance`; leaving `sort` off gets `latest`, not relevance.
|
|
419
|
+
- Scope a search with `columns` (`q.search(text, { columns: ["title"] })`) and re-weight with `weights`, a tuple of
|
|
420
|
+
finite numbers positional over `["title", "desc", "tag", "filter"]`.
|
|
421
|
+
|
|
422
|
+
### Service / Signal Injection
|
|
423
|
+
|
|
424
|
+
- 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`).
|
|
425
|
+
- 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.
|
|
426
|
+
- 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.
|
|
427
|
+
|
|
428
|
+
### Adapters — `adapt()` And `plug()`
|
|
429
|
+
|
|
430
|
+
An injected singleton is an `adapt()` class in `srvkit/`. Write new adapters this way.
|
|
431
|
+
|
|
432
|
+
```ts
|
|
433
|
+
export class AdminNoti extends adapt("adminNoti" as const, ({ use, env, plug, memory }) => ({
|
|
434
|
+
discordApi: use<DiscordApi>(),
|
|
435
|
+
workspaceRoot: env(() => `~/build/${getEnv().environment}`),
|
|
436
|
+
masterHost: plug(MasterHost),
|
|
437
|
+
tokenMap: memory(Map, { of: String }),
|
|
438
|
+
})) {}
|
|
439
|
+
```
|
|
440
|
+
|
|
441
|
+
- 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`.
|
|
442
|
+
- **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.
|
|
443
|
+
- `this.logger` is provided; never construct a `Logger` inside an `adapt()` class. Lifecycle work goes in `override async onInit()`.
|
|
444
|
+
- **`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.
|
|
445
|
+
- **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.
|
|
446
|
+
|
|
447
|
+
Conventions that hold for both shapes:
|
|
448
|
+
|
|
449
|
+
- Route every remote call through one private `#api<T>(path, init?)` with `signal: AbortSignal.timeout(20_000)`.
|
|
450
|
+
- Paginate with `for (let page = 1; ; page += 1)` broken by `if (pageItems.length < 100) break;`.
|
|
451
|
+
- Resolve secrets as `process.env.X ?? options.x ?? deterministicGenerator(...)` **inside a function**, never at module scope.
|
|
452
|
+
- Extend a function by appending an optional trailing parameter with a default, never by changing arity.
|
|
453
|
+
- 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.
|
|
454
|
+
- Release locks in `finally`. Load heavy optional dependencies through a module-level memoized promise (`sharpLoad ??= import("sharp")`).
|
|
455
|
+
|
|
456
|
+
### Error Placement
|
|
457
|
+
|
|
458
|
+
- State-machine preconditions throw in `document.ts`; cross-document rules throw in `service.ts`; request-level policy belongs in `signal.ts` guards.
|
|
459
|
+
- Best-effort code returns a sentinel (`null`, `undefined`, `[0, 0]`, `{}`). There are no Result/Either wrappers.
|
|
460
|
+
- `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.
|
|
461
|
+
- Store actions do not `try/catch` — let the framework toast the `Err`. Client-side validation failure is `msg.error("<key>")` plus an early return, never a throw.
|
|
462
|
+
|
|
463
|
+
## Scalar Modeling (`**/*.constant.ts`)
|
|
464
|
+
|
|
465
|
+
- Define Akan models in `.constant.ts` files with `via` from `akanjs/constant`.
|
|
466
|
+
- Use `Int` for whole-number counts and quantities; use `Float` only for values that need decimals.
|
|
467
|
+
- Use `ID` for document references and prefer explicit structured fields over `Any` unless the content is genuinely flexible.
|
|
468
|
+
- For date defaults, prefer a function such as `default: () => dayjs()` so the value is created at runtime.
|
|
469
|
+
- 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.
|
|
470
|
+
- Put display and predicate logic on the `Light<Model>` class rather than in a util module — see Module File Playbook.
|
|
471
|
+
- Defaults are a literal for scalars and a thunk for anything constructed. Arrays are `field([T])`; optional is the postfix `.optional()`.
|
|
472
|
+
|
|
473
|
+
### Scalar & Field Type Reference
|
|
474
|
+
|
|
475
|
+
- **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.
|
|
476
|
+
- **Use the JS globals directly (no import needed)**: `String`, `Boolean`, `Date`. They are monkey-patched to behave like scalars, so `field(String)` typechecks.
|
|
477
|
+
- **`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.
|
|
478
|
+
- Runtime resolution of every scalar (globals included) goes through `PrimitiveRegistry` by `refName` (`pkgs/akanjs/base/primitiveRegistry.ts`).
|
|
479
|
+
|
|
480
|
+
### Text Search Fields — the `text` role
|
|
481
|
+
|
|
482
|
+
- A field joins the full-text index by declaring one of five roles: `field(String, { text: "title" })`, and likewise
|
|
483
|
+
`"desc"`, `"tag"`, `"thumb"`, `"filter"`. Nothing else opts a field in, and there is no per-model switch.
|
|
484
|
+
- Pick the role by what the value *is*, because `bm25` weights them positionally (`title` 10, `tag` 3, `desc` 1,
|
|
485
|
+
`filter` 0): `title` is the one line a human scans for, `desc` is prose, `tag` is a keyword list, `filter` is a
|
|
486
|
+
scoping value (status, owner, role) that must be matchable but must never outrank a real title hit.
|
|
487
|
+
- `thumb` is mirrored for rendering a hit and is **not** indexed — never expect it to match.
|
|
488
|
+
- **A `secret`, `hidden`, or `resolve()` field with `text` throws at class-build time**, not at query time. That is
|
|
489
|
+
deliberate: the mirror is plaintext, so an indexed secret would leak through search. Do not work around it. The
|
|
490
|
+
same throw covers a `text` field *underneath* one of those — a scalar's own field is reachable through its parent,
|
|
491
|
+
so `f.secret(Noti)` where `Noti.label` carries a role is rejected at the parent, not silently indexed.
|
|
492
|
+
- The role works on a relation too (`image: field(File, { text: "thumb" })`) and on an array (`playing: field([String],
|
|
493
|
+
{ text: "tag" })`); an array of objects indexes by leaf key, including an array leaf (`works[*].tags`). A field
|
|
494
|
+
inside a `Map` indexes nothing: there is no fixed path to extract it from.
|
|
495
|
+
- Declaring roles is all the wiring there is. Mirror rows are maintained by SQL triggers — not document hooks —
|
|
496
|
+
because `updateOneByQuery` and friends fire no hooks, and most searchable-field mutations go through exactly that
|
|
497
|
+
path.
|
|
498
|
+
- Search runs on sqlite/libsql only. `q.search()` against Postgres throws, loudly, rather than returning every row.
|
|
499
|
+
- `AKAN_SEARCH_ENABLED=0` switches the index off process-wide; unset means on. It never deletes mirror data, and
|
|
500
|
+
re-enabling reconciles every ref. **Give every process the same value** — a process cannot drop triggers for models
|
|
501
|
+
it does not mount, so a mixed fleet leaves stale triggers behind.
|
|
502
|
+
- The tokenizer is `AKAN_SEARCH_TOKENIZER` (or `database.search.tokenizer`, which wins), defaulting to
|
|
503
|
+
`unicode61 remove_diacritics 2`. Changing it rebuilds the index from the mirror on the next boot — the model
|
|
504
|
+
tables are never re-read — so it is a safe knob, unlike a `text` role change, which re-reads every row. The
|
|
505
|
+
rebuild takes no cross-process claim, so a fleet restarted at once repeats it in every process; stagger the
|
|
506
|
+
restart when the mirror is large.
|
|
507
|
+
|
|
508
|
+
### Image & File Fields
|
|
509
|
+
|
|
510
|
+
- **Do not declare `Upload` as a model field.** `Upload` is a signal-body-only primitive (see Service And Signal Conventions). Models reference the `File` model instead.
|
|
511
|
+
- 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`).
|
|
512
|
+
- 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`).
|
|
513
|
+
- 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.
|
|
514
|
+
|
|
515
|
+
### Cascade Remove — the `cascade` option
|
|
516
|
+
|
|
517
|
+
**The value names the direction, and getting it wrong is a data loss.** The two actions can sit on the same field
|
|
518
|
+
shape, so `cascade` never means "related" — it means one of exactly these:
|
|
519
|
+
|
|
520
|
+
- `removeRef` — *when I am removed, remove what this field points at.* Declared on the relation the owner holds:
|
|
521
|
+
`image: field(File, { cascade: "removeRef" })`, arrays included. Only a relation accepts it; a primitive, a bare
|
|
522
|
+
`ID`, and a scalar each fail the class build, because none of them names a document to remove.
|
|
523
|
+
- `removeWith` — *when what this field points at is removed, remove me.* Declared on the child's own reference to
|
|
524
|
+
its owner, so the owner never learns about its children and a lib model can be extended by an app's. Three forms:
|
|
525
|
+
a relation (`agentSession: field(AgentSession, { cascade: "removeWith" })`), an id with `ref`
|
|
526
|
+
(`field(ID, { ref: "agentSession", cascade: "removeWith" })`), or a polymorphic id with `refPath`
|
|
527
|
+
(`field(ID, { refPath: "parentType", cascade: "removeWith" })`). An array, a Map, `ref` together with `refPath`,
|
|
528
|
+
and a field naming no owner each fail the class build.
|
|
529
|
+
- **A `refPath` must name an `enumOf` field.** A free-form owner type is unknowable at build time, so every model's
|
|
530
|
+
removal would have to sweep the polymorphic table on the chance it is the owner. The enum names the candidates and
|
|
531
|
+
the reverse index reaches only them.
|
|
532
|
+
- **A cascade goes through the target's service, never its model** — unless it provably makes no difference. The
|
|
533
|
+
service path is what runs the target's `_postRemove`, which is where a module puts the side effect the removal has
|
|
534
|
+
to carry (`FileService._postRemove` deletes the stored blob there).
|
|
535
|
+
- **Bulk is decided at boot, per target model, for both directions.** When the target has no `remove` schema hook, no
|
|
536
|
+
`_pre`/`_postRemove` (its own or a lib's), no cascade of its own, and no children, one `removeManyByQuery` leaves
|
|
537
|
+
exactly the rows the loop would, so the framework takes it. Adding a `_postRemove` to that model silently flips it
|
|
538
|
+
back to one document at a time — the boot log (`info` summary, `verbose` per edge) is the only place that shows.
|
|
539
|
+
- **The plan is sealed after every service is live**, so a `listenPost("remove")` registered in `onInit` still counts
|
|
540
|
+
and a `removeRef` target the app never mounted fails the boot rather than the first removal. An unmounted
|
|
541
|
+
`removeWith` owner fails the boot too; an unmounted `refPath` candidate only warns, since that list spans optional
|
|
542
|
+
modules by design.
|
|
543
|
+
- **Nothing checks whether another document still references the same target.** `File` in particular is deduped by
|
|
544
|
+
`origin`, so two parents can share one row; declaring `removeRef` says the field owns its target exclusively, and
|
|
545
|
+
that judgement is the declaring model's to make.
|
|
546
|
+
- Removal is soft (`removedAt`) but the storage delete a `_postRemove` performs is not — a cascade is not
|
|
547
|
+
restorable, and reviving the owner does not revive what went with it.
|
|
548
|
+
- A `removeWith` declaration **auto-creates its index** (`{ removedAt, fk }`, or `{ removedAt, typeKey, fk }` when
|
|
549
|
+
polymorphic). Every non-base field lives in the `_doc` JSON column, so the lookup would otherwise scan the table
|
|
550
|
+
on every owner removal.
|
|
551
|
+
- **Query-level removes fire no hooks and therefore no cascade.** `removeManyByQuery` / `updateManyByQuery`, the
|
|
552
|
+
generated `remove<Filter>` / `update<Filter>`, and the facade's `removeById` / `updateById` stamp
|
|
553
|
+
`removedAt` in one atomic UPDATE, so nothing downstream runs. Remove one document at a time when it cascades.
|
|
554
|
+
- Cascades are **idempotent**: `removedAt IS NULL` is ANDed into every query-level write, so a retry after a partial
|
|
555
|
+
failure re-stamps nothing. Cycles are cut by a visited set carried down the whole chain, with a depth cap of 16.
|
|
556
|
+
|
|
557
|
+
## Akan Page Routing (`apps/**/page/**`)
|
|
558
|
+
|
|
559
|
+
- `apps/<app>/page` may contain route modules only. Do not add helper logic or component-only files there.
|
|
560
|
+
- Route source files under `page/` must use `.tsx`. Do not add `logic.ts`, `.js`, or `.jsx` files under `page/`.
|
|
561
|
+
- Route pages use `_index.tsx`; layouts use `_layout.tsx`; per-route UI overrides use `_overrides.tsx`.
|
|
562
|
+
- Reserved `_*.tsx` route filenames are limited to `_index.tsx`, `_layout.tsx`, and `_overrides.tsx`; do not add files like `_Component.tsx` or `_helper.tsx`.
|
|
563
|
+
- Page filenames must not start with an uppercase letter. Move helper components like `Component.tsx` to app `ui`, `common`, or `lib` instead.
|
|
564
|
+
- Dynamic segments use `[id]`; route groups use directories like `(user)`, `(public)`, `(tab)`, or `(detail)`.
|
|
565
|
+
- Page modules should usually export `default`, `pageConfig`, `head`, `generateHead`, or `Loading`.
|
|
566
|
+
- `_overrides.tsx` is a logic-free UI-override manifest: imports plus a single `export default override({ Slot: AppComponent })` (from `akanjs/ui`), no `"use client"`. It re-skins framework `akanjs/ui` components for its route subtree; nested manifests merge over ancestors slot-by-slot (closest wins). See the UI Customization reference for the slot list.
|
|
567
|
+
- Prefer `export default function Page` or `export default async function Page` for page components.
|
|
568
|
+
- `libs/<lib>/page` follows the same rules and ships routes to apps that opt in with `syncPageLibs` in `akan.config.ts`: `true` takes every lib dep that has a `page` folder, an array takes the libs listed, `false` (the default) syncs nothing.
|
|
569
|
+
- `akan sync` links those routes into `apps/<app>/page/(libs)/(<lib>)` — once per basePath when the app declares subRoutes. The folder is generated and gitignored; edit the lib source, never the link.
|
|
570
|
+
- Both path segments are route groups, so a lib route mounts at its own path (`libs/shared/page/login/_index.tsx` serves `/login`). Two synced routes that resolve to the same pattern are a sync-time error.
|
|
571
|
+
- `export const pageConfig = { devOnly: true }` keeps a route out of `akan build` while it keeps serving under `akan start` and keeps being typechecked. On a `_layout.tsx` it excludes every route under that directory too. Write it as a literal `true`/`false` — the build reads it off the source without evaluating the module.
|
|
572
|
+
- Before changing route behavior, check `pkgs/akanjs/server/src/routeTree.tsx` and nearby routes for the expected pattern.
|
|
573
|
+
|
|
574
|
+
### Page Body Shape
|
|
575
|
+
|
|
576
|
+
```tsx
|
|
577
|
+
interface PageProps {
|
|
578
|
+
params: { orgId: string };
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
export default async function Page({ params }: PageProps) {
|
|
582
|
+
const { l } = usePage();
|
|
583
|
+
getSelf({ unauthorize: "/signin" });
|
|
584
|
+
const { orgId } = params;
|
|
585
|
+
const [{ org }, { taskInitInOrg }] = await Promise.all([fetch.viewOrg(orgId), fetch.initTaskInOrg(orgId)]);
|
|
586
|
+
return <Task.Zone.Card init={taskInitInOrg} prefix={`/org/${orgId}`} />;
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
export const pageConfig = { transition: "stack" } satisfies PageConfig;
|
|
590
|
+
```
|
|
591
|
+
|
|
592
|
+
- There is no `loader=` / `render=` page prop. Pages are `export default async function Page`.
|
|
593
|
+
- Declare `interface PageProps { params: {...}; searchParams?: {...} }` immediately above the default export.
|
|
594
|
+
- Body order: `usePage()`, auth, destructure params, fetch, return.
|
|
595
|
+
- Run independent fetches through `Promise.all`, even when there is only one.
|
|
596
|
+
- Gate auth at `_layout.tsx`; repeating `getSelf({ unauthorize: "/signin" })` in the page is fine and common.
|
|
597
|
+
- Keep `async` even when nothing is awaited — it marks a real server page.
|
|
598
|
+
- No `useState`, no `useEffect`, and no comments in page files.
|
|
599
|
+
|
|
600
|
+
## Akan Sync Conventions (`apps/**`, `libs/**`)
|
|
601
|
+
|
|
602
|
+
- `apps/<appName>` root may only contain these files: `AGENTS.md`, `CLAUDE.md`, `akan.app.json`, `akan.config.ts`, `capacitor.config.ts`, `client.ts`, `main.ts`, `package.json`, `server.ts`, `tsconfig.json`.
|
|
603
|
+
- `apps/<appName>` root may only contain these folders: `.akan`, `android`, `common`, `env`, `ios`, `lib`, `mobile`, `page`, `plugin`, `private`, `public`, `script`, `secrets`, `srvkit`, `ui`, `webkit`.
|
|
604
|
+
- That allowlist has one source — `pkgs/@akanjs/devkit/workspaceLayout.ts`. `akan sync` (error), `akan doctor`
|
|
605
|
+
(diagnostic), and `akan quality scan` (warning) all read it, so add a new root entry there and mirror it into this
|
|
606
|
+
list, never into one of the three call sites.
|
|
607
|
+
- `akan sync` maintains a scoped agent guide per app/lib: `apps/<app>/AGENTS.md` / `libs/<lib>/AGENTS.md`. The
|
|
608
|
+
section between the `akan:agent` markers (the `## Recipes In Scope` index) is generated — do not hand-edit it;
|
|
609
|
+
content outside the markers is yours. `akan lint` fails when the generated section is stale.
|
|
610
|
+
- The `plugin/` facet holds Akan plugin declarations; files use the `<name>.plugin.ts` convention (e.g. `pushNotification.plugin.ts`) and are re-exported from the generated `plugin/index.ts` barrel.
|
|
611
|
+
- Do not add `apps/*/base`; place shared app utilities under `apps/*/common`.
|
|
612
|
+
- `apps/*/lib` and `libs/*/lib` root files are limited to generated/support files: `cnst.ts`, `db.ts`, `dict.ts`, `option.ts`, `sig.ts`, `srv.ts`, `st.ts`, `useClient.ts`, `useServer.ts`.
|
|
613
|
+
- Domain module folders are `lib/<model>` for database modules, `lib/_<service>` for service modules, and `lib/__scalar/<scalar>` for scalar modules.
|
|
614
|
+
- Database module UI files are limited to `<Model>.Template.tsx`, `<Model>.Unit.tsx`, `<Model>.Util.tsx`, `<Model>.View.tsx`, and `<Model>.Zone.tsx`.
|
|
615
|
+
- Service module UI files are limited to `<Service>.Util.tsx` and `<Service>.Zone.tsx`.
|
|
616
|
+
- Scalar module UI files are limited to `<Scalar>.Template.tsx` and `<Scalar>.Unit.tsx`.
|
|
617
|
+
- Module `*.test.ts`, `*.test.tsx`, `*.spec.ts`, and `*.spec.tsx` files are allowed.
|
|
618
|
+
- `ui/index.ts`, `webkit/index.ts`, `srvkit/index.ts`, `common/index.ts`, `plugin/index.ts`, and module `lib/**/index.ts` files are generated by scanSync; do not hand-edit or track them.
|
|
619
|
+
- Generated facet indexes export only 1-depth files/folders with `export * from "./name";`.
|
|
620
|
+
- `libs/<libName>` may hold a `page` folder of route modules; scanSync links it into every app that opts in with `syncPageLibs`, so `apps/*/page/**/(libs)` is generated and gitignored like `public/libs`.
|
|
621
|
+
|
|
622
|
+
## Layer Placement (`common/`, `webkit/`, `srvkit/`, `ui/`)
|
|
623
|
+
|
|
624
|
+
| Folder | Admission test | Naming |
|
|
625
|
+
|---|---|---|
|
|
626
|
+
| `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 |
|
|
627
|
+
| `webkit/` | touches `window` / `navigator` / Capacitor, or is a React hook | `use<Thing>.tsx` — `.tsx` even with no JSX |
|
|
628
|
+
| `srvkit/` | touches `node:*`, `Bun`, `process.env`, a secret, or a server SDK | camelCase file, PascalCase class |
|
|
629
|
+
| `ui/` | renders JSX and is not bound to one model | PascalCase component, camelCase sidecar (`swipeCard.util.ts`) |
|
|
630
|
+
| `plugin/` | build- or CLI-time `AkanPlugin` | `<name>.plugin.ts`, registered in `akan.config.ts` |
|
|
631
|
+
|
|
632
|
+
- Hooks return a named object of async closures, never a tuple.
|
|
633
|
+
- A layer-root `index.ts` is generated, but 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.
|
|
634
|
+
- `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.
|
|
635
|
+
|
|
636
|
+
## Present In The Code — Do Not Imitate
|
|
637
|
+
|
|
638
|
+
Older files contain these. They are warts, not conventions: do not copy them forward, and prefer the newer neighbour
|
|
639
|
+
when two shapes disagree.
|
|
640
|
+
|
|
641
|
+
- `children: any` — newer files use `children: ReactNode`.
|
|
642
|
+
- Hard-coded API keys or secrets in source; they belong in `option.ts` or env.
|
|
643
|
+
- Large blocks of commented-out code left in place.
|
|
644
|
+
- Cross-store writes through a `RootStore` cast — it collides with the Bun SSR-bundler segfault.
|
|
645
|
+
- Stale `// TODO: Implement …` comments above implemented methods.
|
|
646
|
+
- `{cond && <X/>}` in JSX, hard-coded Korean bypassing `l()`, and `window.alert(...)` for user feedback.
|
|
647
|
+
- Bare `/* eslint-disable */` blocks — use `// biome-ignore lint/<rule>: <why>`.
|
|
648
|
+
- Raw palette grays such as `text-gray-400` instead of daisyUI semantic tokens.
|
|
649
|
+
|
|
650
|
+
## Secrets And Env Safety (`.env`, `infra/**`, `*secret*`, `*credential*`)
|
|
651
|
+
|
|
652
|
+
- Never print, summarize, commit, or expose real secret values, credentials, tokens, private keys, or `.env` contents.
|
|
653
|
+
- If env keys are needed for documentation, list only key names and example placeholders, not live values.
|
|
654
|
+
- Preserve the existing env/secret flow through root scripts such as `bun run downloadEnv`, `bun run uploadEnv`, `bun run downloadSecret`, and `bun run uploadSecret`.
|
|
655
|
+
- When editing infra env or secret scripts, keep Jenkins and deployment assumptions intact unless the task explicitly asks to change them.
|
|
656
|
+
- Treat generated env/secret artifacts as sensitive even when they are not named `.env`.
|
|
657
|
+
|
|
658
|
+
## Application Test Commands
|
|
659
|
+
|
|
660
|
+
- After changing application source code, test the app with `bun run akan start <appName>`.
|
|
661
|
+
- Test production build generation with `bun run akan build <appName>`.
|
|
662
|
+
- To test a built artifact locally, run it from the generated app directory with the required Akan runtime environment variables.
|
|
663
|
+
|
|
664
|
+
```bash
|
|
665
|
+
cd dist/apps/akan && USE_AKANJS_PKGS=true AKAN_PUBLIC_REPO_NAME=akanjs AKAN_PUBLIC_SERVE_DOMAIN="akanjs.com" AKAN_PUBLIC_APP_NAME=akan AKAN_PUBLIC_ENV=local AKAN_PUBLIC_OPERATION_MODE=local SERVER_MODE=federation AKAN_PUBLIC_BASE_PATHS=akanjs,soft,office bun main.js
|
|
666
|
+
```
|
|
667
|
+
|
|
668
|
+
- Adjust `<appName>`, `AKAN_PUBLIC_APP_NAME`, and `AKAN_PUBLIC_BASE_PATHS` to match the app being tested.
|