@akanjs/devkit 3.0.0-beta.13 → 3.0.0-beta.15
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/README.md +1 -0
- package/cloud/cloudApi.ts +17 -4
- package/cloud/constants.ts +0 -3
- package/cloud/globalConfig.ts +1 -9
- package/codeAgent/agent/AkanAuthStorageBackend.ts +105 -0
- package/codeAgent/agent/AkanCodeServices.ts +104 -0
- package/codeAgent/agent/CodeAgent.ts +543 -0
- package/codeAgent/agent/CodeAgentAsks.ts +87 -0
- package/codeAgent/agent/CodeAgentEventMapper.ts +260 -0
- package/codeAgent/agent/CodeAgentGate.ts +119 -0
- package/codeAgent/agent/CodeAgentRpcHost.ts +96 -0
- package/codeAgent/agent/CodeAgentStreamPrinter.ts +93 -0
- package/codeAgent/agent/CodeAgentUi.ts +52 -0
- package/codeAgent/agent/CodeMailbox.ts +175 -0
- package/codeAgent/agent/CodeSessionFork.ts +71 -0
- package/codeAgent/agent/CodeSessionIndex.ts +104 -0
- package/codeAgent/agent/SubagentPool.ts +173 -0
- package/codeAgent/agent/akanCodeModel.ts +81 -0
- package/codeAgent/agent/akanCodePaths.ts +31 -0
- package/codeAgent/agent/akanSystemPrompt.ts +49 -0
- package/codeAgent/agent/codeAgentContract.test.ts +529 -0
- package/codeAgent/agent/codeMailbox.test.ts +80 -0
- package/codeAgent/agent/codeSessionFork.test.ts +60 -0
- package/codeAgent/agent/codeSessionIndex.test.ts +72 -0
- package/codeAgent/agent/consoleToStderr.ts +18 -0
- package/codeAgent/feedback/DevLogFeedback.ts +97 -0
- package/codeAgent/feedback/PreviewChrome.ts +106 -0
- package/codeAgent/feedback/PreviewFeedback.ts +53 -0
- package/codeAgent/feedback/PreviewView.ts +107 -0
- package/codeAgent/feedback/TurnFeedback.ts +113 -0
- package/codeAgent/feedback/VerifyFeedback.ts +46 -0
- package/codeAgent/feedback/previewChrome.test.ts +73 -0
- package/codeAgent/index.ts +43 -0
- package/codeAgent/skills/akan-debug/SKILL.md +42 -0
- package/codeAgent/skills/akan-deps/SKILL.md +50 -0
- package/codeAgent/skills/akan-err/SKILL.md +39 -0
- package/codeAgent/skills/akan-module/SKILL.md +45 -0
- package/codeAgent/skills/akan-scaffold/SKILL.md +76 -0
- package/codeAgent/skills/akan-scalar/SKILL.md +52 -0
- package/codeAgent/skills/akan-service/SKILL.md +34 -0
- package/codeAgent/skills/akan-store/SKILL.md +63 -0
- package/codeAgent/skills/akan-ui/SKILL.md +63 -0
- package/codeAgent/skills/akan-validate/SKILL.md +44 -0
- package/codeAgent/tools/AkanCli.ts +62 -0
- package/codeAgent/tools/AkanCodePlugins.ts +135 -0
- package/codeAgent/tools/AkanEditScope.ts +58 -0
- package/codeAgent/tools/AkanEnvKeys.ts +56 -0
- package/codeAgent/tools/AkanToolPack.ts +114 -0
- package/codeAgent/tools/AkanVerifier.ts +55 -0
- package/codeAgent/tools/MailToolPack.ts +85 -0
- package/codeAgent/tools/McpClient.ts +162 -0
- package/codeAgent/tools/McpToolPack.ts +134 -0
- package/codeAgent/tools/SessionToolPack.ts +101 -0
- package/codeAgent/tools/WebToolPack.ts +153 -0
- package/codeAgent/tools/akanCodePlugins.test.ts +476 -0
- package/commandDecorators/command.ts +1 -1
- package/incrementalBuilder/incrementalBuilder.proc.ts +1 -1
- package/index.ts +3 -2
- package/package.json +5 -5
- package/stripAnsi.ts +3 -0
- package/testHelpers.ts +179 -0
- package/abstractCompactor.ts +0 -112
- package/aiEditor.test.ts +0 -60
- package/aiEditor.ts +0 -387
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { AkanEditScope } from "../tools/AkanEditScope";
|
|
2
|
+
import { AkanVerifier } from "../tools/AkanVerifier";
|
|
3
|
+
import type { TurnFeedbackSource } from "./TurnFeedback";
|
|
4
|
+
|
|
5
|
+
export interface VerifyFeedbackOptions {
|
|
6
|
+
cwd: string;
|
|
7
|
+
/** A read-only profile reports what is stale instead of running a chain that would write generated files. */
|
|
8
|
+
readOnly?: boolean;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Runs the validation chain over whatever a turn changed and feeds the failures back.
|
|
13
|
+
*
|
|
14
|
+
* The model has a verification tool and the observed failure is that it finishes without calling it — so this
|
|
15
|
+
* is not a second tool but a hook. Detection reads git rather than tool events, because an edit made through
|
|
16
|
+
* `bash` produces no write tool call and is exactly the kind a model forgets to verify.
|
|
17
|
+
*/
|
|
18
|
+
export class VerifyFeedback implements TurnFeedbackSource {
|
|
19
|
+
readonly key = "verify";
|
|
20
|
+
readonly #options: VerifyFeedbackOptions;
|
|
21
|
+
readonly #verifier: AkanVerifier;
|
|
22
|
+
#baseline: ReadonlySet<string> = new Set();
|
|
23
|
+
|
|
24
|
+
constructor(options: VerifyFeedbackOptions) {
|
|
25
|
+
this.#options = options;
|
|
26
|
+
this.#verifier = new AkanVerifier(options.cwd);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async begin() {
|
|
30
|
+
this.#baseline = await AkanEditScope.baseline(this.#options.cwd);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async observe() {
|
|
34
|
+
const scope = await AkanEditScope.since(this.#options.cwd, this.#baseline);
|
|
35
|
+
if (!scope.apps.length && !scope.libs.length) return undefined;
|
|
36
|
+
if (this.#options.readOnly) return VerifyFeedback.#readOnlyNotice(scope.needsSync, [...scope.apps, ...scope.libs]);
|
|
37
|
+
const report = await this.#verifier.verify(scope);
|
|
38
|
+
if (report.ok) return undefined;
|
|
39
|
+
return AkanVerifier.summarize(report);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
static #readOnlyNotice(needsSync: boolean, targets: string[]) {
|
|
43
|
+
if (!needsSync) return undefined;
|
|
44
|
+
return `Files were added or removed under ${targets.join(", ")}, so the generated barrels are stale. This profile does not write, so run \`akan sync ${targets.join(" ")}\` yourself before trusting a typecheck.`;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { afterEach, describe, expect, test } from "bun:test";
|
|
2
|
+
import { PreviewChrome } from "./PreviewChrome";
|
|
3
|
+
|
|
4
|
+
const saved = {
|
|
5
|
+
backend: process.env[PreviewChrome.backendEnvKey],
|
|
6
|
+
path: process.env[PreviewChrome.pathEnvKey],
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
afterEach(() => {
|
|
10
|
+
if (saved.backend === undefined) delete process.env[PreviewChrome.backendEnvKey];
|
|
11
|
+
else process.env[PreviewChrome.backendEnvKey] = saved.backend;
|
|
12
|
+
if (saved.path === undefined) delete process.env[PreviewChrome.pathEnvKey];
|
|
13
|
+
else process.env[PreviewChrome.pathEnvKey] = saved.path;
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
describe("PreviewChrome", () => {
|
|
17
|
+
test("picks the system framework on macOS and Chrome everywhere else", () => {
|
|
18
|
+
expect(PreviewChrome.backend).toBe(process.platform === "darwin" ? "webkit" : "chrome");
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
test("an explicit backend wins over the platform default", () => {
|
|
22
|
+
process.env[PreviewChrome.backendEnvKey] = "chrome";
|
|
23
|
+
expect(PreviewChrome.backend).toBe("chrome");
|
|
24
|
+
process.env[PreviewChrome.backendEnvKey] = "webkit";
|
|
25
|
+
expect(PreviewChrome.backend).toBe("webkit");
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test("an unknown backend name falls back rather than reaching the launcher", () => {
|
|
29
|
+
process.env[PreviewChrome.backendEnvKey] = "firefox";
|
|
30
|
+
expect(PreviewChrome.backend).toBe(process.platform === "darwin" ? "webkit" : "chrome");
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test("the Chrome option carries the two flags a container needs and Bun does not add", () => {
|
|
34
|
+
process.env[PreviewChrome.backendEnvKey] = "chrome";
|
|
35
|
+
const option = PreviewChrome.backendOption();
|
|
36
|
+
expect(typeof option).toBe("object");
|
|
37
|
+
expect((option as { argv: string[] }).argv).toContain("--disable-dev-shm-usage");
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test("the webkit option is the bare string the constructor validates", () => {
|
|
41
|
+
process.env[PreviewChrome.backendEnvKey] = "webkit";
|
|
42
|
+
expect(PreviewChrome.backendOption()).toBe("webkit");
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test("BUN_CHROME_PATH is honoured, because Bun's own launcher reads it", () => {
|
|
46
|
+
process.env[PreviewChrome.pathEnvKey] = "/opt/custom/chrome";
|
|
47
|
+
expect(PreviewChrome.executable()).toBe("/opt/custom/chrome");
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test("availability tracks the resolved backend", () => {
|
|
51
|
+
process.env[PreviewChrome.backendEnvKey] = "chrome";
|
|
52
|
+
process.env[PreviewChrome.pathEnvKey] = "/opt/custom/chrome";
|
|
53
|
+
expect(PreviewChrome.available).toBe(true);
|
|
54
|
+
expect(PreviewChrome.unavailableReason()).toBeUndefined();
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test("the unavailable message names the install step rather than only the symptom", () => {
|
|
58
|
+
process.env[PreviewChrome.backendEnvKey] = "chrome";
|
|
59
|
+
delete process.env[PreviewChrome.pathEnvKey];
|
|
60
|
+
if (PreviewChrome.available) return;
|
|
61
|
+
const reason = PreviewChrome.unavailableReason() ?? "";
|
|
62
|
+
expect(reason).toContain("chromium");
|
|
63
|
+
expect(reason).toContain(PreviewChrome.pathEnvKey);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test("the image steps install fonts, or every rendered glyph is a box", () => {
|
|
67
|
+
const step = PreviewChrome.dockerRuns.join(" ");
|
|
68
|
+
expect(step).toContain("chromium");
|
|
69
|
+
expect(step).toContain("fonts-noto-cjk");
|
|
70
|
+
// Leaving the apt lists behind adds ~40MB to every layer of every image that takes this step.
|
|
71
|
+
expect(step).toContain("rm -rf /var/lib/apt/lists/*");
|
|
72
|
+
});
|
|
73
|
+
});
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The akan coding agent: the engine wrapper, the tools it is given, and the feedback that reopens a turn.
|
|
3
|
+
*
|
|
4
|
+
* Importing this barrel loads the engine, which costs ~122MiB resident. A host that only needs the cheap
|
|
5
|
+
* pieces — the model reference, the stream printer, the sub-agent kinds — reaches for the owning module
|
|
6
|
+
* instead (`@akanjs/devkit/codeAgent/agent/akanCodeModel`), the same way `@akanjs/devkit` itself asks to be
|
|
7
|
+
* used. `agent/consoleToStderr` is deliberately absent: it is a side-effect module that takes over
|
|
8
|
+
* `globalThis.console`, and it has to be the first import of an RPC entry rather than a re-export of this one.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
export * from "./agent/AkanAuthStorageBackend";
|
|
12
|
+
export * from "./agent/AkanCodeServices";
|
|
13
|
+
export * from "./agent/akanCodeModel";
|
|
14
|
+
export * from "./agent/akanCodePaths";
|
|
15
|
+
export * from "./agent/akanSystemPrompt";
|
|
16
|
+
export * from "./agent/CodeAgent";
|
|
17
|
+
export * from "./agent/CodeAgentAsks";
|
|
18
|
+
export * from "./agent/CodeAgentEventMapper";
|
|
19
|
+
export * from "./agent/CodeAgentGate";
|
|
20
|
+
export * from "./agent/CodeAgentRpcHost";
|
|
21
|
+
export * from "./agent/CodeAgentStreamPrinter";
|
|
22
|
+
export * from "./agent/CodeAgentUi";
|
|
23
|
+
export * from "./agent/CodeMailbox";
|
|
24
|
+
export * from "./agent/CodeSessionFork";
|
|
25
|
+
export * from "./agent/CodeSessionIndex";
|
|
26
|
+
export * from "./agent/SubagentPool";
|
|
27
|
+
export * from "./feedback/DevLogFeedback";
|
|
28
|
+
export * from "./feedback/PreviewChrome";
|
|
29
|
+
export * from "./feedback/PreviewFeedback";
|
|
30
|
+
export * from "./feedback/PreviewView";
|
|
31
|
+
export * from "./feedback/TurnFeedback";
|
|
32
|
+
export * from "./feedback/VerifyFeedback";
|
|
33
|
+
export * from "./tools/AkanCli";
|
|
34
|
+
export * from "./tools/AkanCodePlugins";
|
|
35
|
+
export * from "./tools/AkanEditScope";
|
|
36
|
+
export * from "./tools/AkanEnvKeys";
|
|
37
|
+
export * from "./tools/AkanToolPack";
|
|
38
|
+
export * from "./tools/AkanVerifier";
|
|
39
|
+
export * from "./tools/MailToolPack";
|
|
40
|
+
export * from "./tools/McpClient";
|
|
41
|
+
export * from "./tools/McpToolPack";
|
|
42
|
+
export * from "./tools/SessionToolPack";
|
|
43
|
+
export * from "./tools/WebToolPack";
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: akan-debug
|
|
3
|
+
description: "Debug a running Akan app: where the dev-server log is, how to read it, and how to tell a runtime failure from a build one."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Akan debug
|
|
7
|
+
|
|
8
|
+
## The log is a file, and that file is the whole session
|
|
9
|
+
|
|
10
|
+
A supervised `akan start` writes **`local/apps/<app>/runtime/dev.log`** — no ANSI, nothing truncated, every
|
|
11
|
+
process of the app in arrival order, including the dev host's own build output, which reaches no other
|
|
12
|
+
file. The previous session is kept beside it as `dev.prev.log`.
|
|
13
|
+
|
|
14
|
+
Read that file. Do not ask for a paste of the terminal: the full-screen view repaints one frame in place, so
|
|
15
|
+
what survives in the scrollback is bordered and truncated to the pane.
|
|
16
|
+
|
|
17
|
+
- `tail -n 200 local/apps/<app>/runtime/dev.log` after an edit, to see what the reload produced.
|
|
18
|
+
- `akan logs <app>` tails a running server with `--level --grep --endpoint --trace --child --role --origin
|
|
19
|
+
--since` and `--json`.
|
|
20
|
+
- The dev server's URL is printed in that log, and the port is allocated rather than fixed — the log is the
|
|
21
|
+
only source for it.
|
|
22
|
+
|
|
23
|
+
## Which loop you are in
|
|
24
|
+
|
|
25
|
+
- **A type or lint failure** is the `akan-validate` loop. `akan_verify`.
|
|
26
|
+
- **A runtime or SSR failure** is the dev log. The page renders blank, an overlay appears, or a request
|
|
27
|
+
500s — none of that shows up in a typecheck.
|
|
28
|
+
|
|
29
|
+
If a dev server is already running for the app, do **not** start another. If it looks wedged, surface the
|
|
30
|
+
log tail in your answer rather than killing it.
|
|
31
|
+
|
|
32
|
+
## Reading a log line
|
|
33
|
+
|
|
34
|
+
Every `Logger` call carries `traceId`, `endpoint` and `origin` alongside the message, so one request's lines
|
|
35
|
+
share a trace — `akan logs <app> --trace <id>` pulls just that request. The level ladder is
|
|
36
|
+
`trace verbose debug info warn error`; `.log()` is deprecated and emits at `info`, which is why a call that
|
|
37
|
+
reads like its own level is not one.
|
|
38
|
+
|
|
39
|
+
## Before you conclude
|
|
40
|
+
|
|
41
|
+
A change that renders is not a change that passed. Run `akan_verify`, and for a `.tsx` change check
|
|
42
|
+
`akan quality ssr` too — a UI edit that moves markup into a client bundle typechecks perfectly.
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: akan-deps
|
|
3
|
+
description: "Fix an import, dependency, or barrel problem in an Akan workspace — barrels are generated, and a third-party package may not be imported from most files."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Akan imports, deps and barrels
|
|
7
|
+
|
|
8
|
+
## Barrels are generated
|
|
9
|
+
|
|
10
|
+
Every `index.ts` under `ui/`, `webkit/`, `srvkit/`, `common/`, `plugin/` and `lib/**` is scanner output, as
|
|
11
|
+
are `cnst.ts`, `db.ts`, `dict.ts`, `sig.ts`, `srv.ts`, `st.ts`, `useClient.ts`, `useServer.ts`, `client.ts`
|
|
12
|
+
and `server.ts`. Do not hand-edit them and do not rely on their exact contents.
|
|
13
|
+
|
|
14
|
+
**If an import "cannot be found" but the file exists, the barrel is stale** — run `akan sync <app-or-lib>`,
|
|
15
|
+
or `repair_generated`. A `ui/<Folder>/index.tsx` that builds a namespace is hand-written source; the
|
|
16
|
+
distinguishing test is that a generated barrel contains nothing but `export * from "./X";` lines.
|
|
17
|
+
|
|
18
|
+
## Where a dependency is declared
|
|
19
|
+
|
|
20
|
+
Add the package to the `package.json` of the app or lib that imports it —
|
|
21
|
+
`apps/<app>/package.json` or `libs/<lib>/package.json` — then `bun install` from the repo root.
|
|
22
|
+
|
|
23
|
+
## A third-party package may not be imported from most files
|
|
24
|
+
|
|
25
|
+
`no-import-external-library.grit` refuses a third-party import from `page/**`, from any barrel, and from any
|
|
26
|
+
`*.{constant,dictionary,document,service,signal,store}.ts` or `*.{Template,Unit,Util,View,Zone}.tsx`.
|
|
27
|
+
Re-export the symbol through a lib first. The one-line re-export shims in a lib's `common/`, `webkit/` and
|
|
28
|
+
`ui/` exist for exactly this — they are load-bearing, not cruft.
|
|
29
|
+
|
|
30
|
+
## Two more import rules the build enforces
|
|
31
|
+
|
|
32
|
+
- **No deep import past a barrel** (`no-deep-internal-import.grit`). Cross-module constant references such as
|
|
33
|
+
`../map/map.constant` are the sanctioned exception.
|
|
34
|
+
- **No import across the client/server boundary.** A client file may not import a `*.document.ts` /
|
|
35
|
+
`*.dictionary.ts` / `*.service.ts` / `*.signal.ts`, `srvkit/`, a package `server` entrypoint, or the `db` /
|
|
36
|
+
`srv` / `sig` / `dict` / `option` / `useServer` barrels. A server file may not import a `*.store.ts`, a
|
|
37
|
+
module component, `ui/`, `webkit/`, a package `client` entrypoint, or `st` / `store` / `useClient`. Shared
|
|
38
|
+
files — `common/` and `*.constant.ts` — are held to **both**. `import type` is erased and stays legal in
|
|
39
|
+
every direction; a mixed value-and-type import is not exempt.
|
|
40
|
+
|
|
41
|
+
## Aliases
|
|
42
|
+
|
|
43
|
+
`akanjs/*` for framework facets, `@apps/*`, `@libs/*`, `@contract/*`. In `.tsx`, one flat named import from
|
|
44
|
+
the package client path — `import { cnst, fetch, st, usePage } from "@apps/<app>/client"` — never a relative
|
|
45
|
+
`../` import. In backend `.ts`, namespace the generated barrels: `import * as cnst from "../cnst"`.
|
|
46
|
+
|
|
47
|
+
## Validate
|
|
48
|
+
|
|
49
|
+
`akan_verify`, or `akan sync <app>` then `akan lint <app>` then `akan typecheck <app>`. Raw `tsc` misses the
|
|
50
|
+
generated barrels and reports false errors.
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: akan-err
|
|
3
|
+
description: "Throw errors the Akan way — a typed Err with a dictionary key, never a bare throw new Error. Lint-enforced."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# throw → Err
|
|
7
|
+
|
|
8
|
+
Domain code throws `new Err("<module>.error.<key>")` and registers the key as `[en, ko]` in that module's
|
|
9
|
+
dictionary `.error({})`. `throw new Error(...)` fails the build (`no-throw-raw-error.grit`).
|
|
10
|
+
|
|
11
|
+
## Getting the import side right
|
|
12
|
+
|
|
13
|
+
This is the common failure. Import `Err` from the source matching the **runtime side of the file**:
|
|
14
|
+
|
|
15
|
+
- server code: `import { Err } from "../dict"`
|
|
16
|
+
- UI: `from "@libs/<lib>/client"` or `from "@apps/<app>/client"`
|
|
17
|
+
|
|
18
|
+
## Where the rule does not apply
|
|
19
|
+
|
|
20
|
+
The lint rule exempts tests, `*.constant.ts`, `common/**`, and `env/**`. The last two have **no legal `Err`
|
|
21
|
+
import path at all**, so keep throwing code out of them entirely rather than reaching for a workaround.
|
|
22
|
+
|
|
23
|
+
## Where each error belongs
|
|
24
|
+
|
|
25
|
+
- a state-machine precondition → `document.ts`
|
|
26
|
+
- a cross-document rule → `service.ts`
|
|
27
|
+
- request-level policy → a guard in `signal.ts`
|
|
28
|
+
|
|
29
|
+
Best-effort code returns a sentinel (`null`, `undefined`, `{}`) instead of throwing. `try/catch` is rare and
|
|
30
|
+
always converts an exception into a decision: a guard catches, warns, and returns `false`; an adapter
|
|
31
|
+
catches, logs, and returns `null`. A store action never catches — let the framework toast the `Err`.
|
|
32
|
+
Client-side validation failure is `msg.error("<key>")` plus an early return, never a throw.
|
|
33
|
+
|
|
34
|
+
**A failure another process reported travels as itself.** A server-to-server `fetch.<endpoint>(…, { origin })`
|
|
35
|
+
restores the remote `Err` with its class, key and data. Rethrow it as-is — wrapping it in a `new Error`
|
|
36
|
+
discards what the endpoint chose to say, and a bare `Error` is generalized to "Internal Server Error" on the
|
|
37
|
+
way out.
|
|
38
|
+
|
|
39
|
+
Korean `.error()` messages end in `다.`.
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: akan-module
|
|
3
|
+
description: "Add or change an Akan database module: the constant→dictionary→document→service→signal→store→UI flow and which layer owns what."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Akan module flow
|
|
7
|
+
|
|
8
|
+
Creating a **new** module? Do not hand-write these files — scaffold with `create-module` first (the
|
|
9
|
+
`akan-scaffold` skill), then fill in the layers below.
|
|
10
|
+
|
|
11
|
+
A stored model flows through layers, one file each under `lib/<model>/`:
|
|
12
|
+
|
|
13
|
+
`constant → dictionary → document → service → signal → store → UI`
|
|
14
|
+
|
|
15
|
+
| file | owns |
|
|
16
|
+
| --- | --- |
|
|
17
|
+
| `<model>.constant.ts` | the five classes, in order: `XInput → XObject → LightX → X → XInsight`. Write `XInsight` even when empty. |
|
|
18
|
+
| `<model>.dictionary.ts` | `[en, ko]` labels and the `Err` keys, in the fixed chain with empty stages still written. |
|
|
19
|
+
| `<model>.document.ts` | `XFilter → X → XModel`. Chain methods validate, mutate, `return this`, and never `save()`. |
|
|
20
|
+
| `<model>.service.ts` | load → chain → `return await ….save()`. Side effects go in `_preUpdate` / `_postCreate`. |
|
|
21
|
+
| `<model>.signal.ts` | `XInternal → XSlice → XEndpoint`, all three declared even when empty. |
|
|
22
|
+
| `<model>.store.ts` | only what `store()` does not already generate — see `akan-store`. |
|
|
23
|
+
| `<Model>.*.tsx` | rendering. `Template` / `Zone` / `Util` are client, `Unit` / `View` are server. |
|
|
24
|
+
|
|
25
|
+
## The rule most often missed
|
|
26
|
+
|
|
27
|
+
**Display and predicate logic belongs on `Light<Model>`** — `isNew()`, `canWrite(user?)`, `formatTimes()`.
|
|
28
|
+
The Light class is the one both server and client hold, so shared logic lives there instead of in a util
|
|
29
|
+
module. Collection-level helpers go `static` on the full model.
|
|
30
|
+
|
|
31
|
+
## Rules
|
|
32
|
+
|
|
33
|
+
- New module ⇒ `create-module`. New field / action / list on an existing one ⇒ `add-field` /
|
|
34
|
+
`add-mutation` / `add-slice`. Hand-writing is the fallback, not the default.
|
|
35
|
+
- A module is for data that must be **stored**. Static page content is a page.
|
|
36
|
+
- Keep business decisions in constant, document and service; API exposure in signal; client coordination in
|
|
37
|
+
store; rendering in UI.
|
|
38
|
+
- Never hand-edit a generated registry file — `cnst.ts`, `db.ts`, `dict.ts`, `option.ts`, `sig.ts`, `srv.ts`,
|
|
39
|
+
`st.ts`, `useClient.ts`, `useServer.ts`, and every `index.ts` barrel. They are scanner output; run
|
|
40
|
+
`akan sync <app>` or `repair_generated`.
|
|
41
|
+
- Read the module's `<module>.abstract.md` before changing it, and update it when an invariant or workflow
|
|
42
|
+
changes — not for formatting.
|
|
43
|
+
|
|
44
|
+
Deeper detail per layer is a `get_guideline` call away: `moduleOverview`, `moduleCodegen`, `modelConstant`,
|
|
45
|
+
`modelDocument`, `modelService`, `modelSignal`, `modelStore`, `modelDictionary`.
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: akan-scaffold
|
|
3
|
+
description: "Create a new module, scalar, field, mutation, slice, or UI surface — scaffold it with the akan workflow tools (plan_workflow / apply_workflow) instead of writing the files by hand."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Akan scaffolding workflows (use these BEFORE writing files)
|
|
7
|
+
|
|
8
|
+
Akan generates the conventional file set for you. Creating those files by hand skips the conventions, the
|
|
9
|
+
generated-barrel sync, and the validation chain.
|
|
10
|
+
|
|
11
|
+
## Step 0 — decide WHAT gets a module, before any tool call
|
|
12
|
+
|
|
13
|
+
List the things the request says must be **stored in the database**. Those, and only those, become modules.
|
|
14
|
+
Everything else is pages, sections, and props.
|
|
15
|
+
|
|
16
|
+
- A page or section whose content you can write as static JSX or props is not an entity. "A self-intro page
|
|
17
|
+
with two detail pages" is pages only — no `profile` module.
|
|
18
|
+
- Never invent a supporting entity (profile, category, tag, author, like) to round out a design. If one looks
|
|
19
|
+
genuinely necessary, say so in your reply and let the user decide.
|
|
20
|
+
- "… plus a comment zone that saves to a database" is exactly one new module: `comment`.
|
|
21
|
+
- Check what exists first (`list_modules`) and extend it when it already fits — `add-field` / `add-slice` /
|
|
22
|
+
`create-ui` beat a whole new module.
|
|
23
|
+
- Build the entire request, at the smallest structure that satisfies it.
|
|
24
|
+
|
|
25
|
+
Then pick the workflow that matches the intent:
|
|
26
|
+
|
|
27
|
+
| The user wants… | workflow |
|
|
28
|
+
| --- | --- |
|
|
29
|
+
| a new database-backed entity (comment, post, todo, product …) | `create-module` |
|
|
30
|
+
| a reusable embedded value object | `create-scalar` |
|
|
31
|
+
| a new field on an existing module | `add-field` |
|
|
32
|
+
| a new field with a closed set of values | `add-enum-field` |
|
|
33
|
+
| a new state-changing action | `add-mutation` |
|
|
34
|
+
| a filtered list, tab, or reusable query | `add-slice` |
|
|
35
|
+
| a View / Unit / Template / Zone / Util for an existing module | `create-ui` |
|
|
36
|
+
|
|
37
|
+
## The chain
|
|
38
|
+
|
|
39
|
+
Each result names the next call in `next.tool`. Follow it; do not improvise.
|
|
40
|
+
|
|
41
|
+
1. `plan_workflow({ workflow: "create-module", inputs: { app: "<app>", module: "comment" } })` — read-only.
|
|
42
|
+
Returns `planPath` and `next.tool=apply_workflow`. (`list_workflows` / `explain_workflow` give the names
|
|
43
|
+
and each workflow's inputs.)
|
|
44
|
+
2. `apply_workflow({ planPath })` — writes the scaffold and syncs the generated barrels. Returns
|
|
45
|
+
`validationTarget`.
|
|
46
|
+
3. `run_validation({ runIdOrPlan: <validationTarget> })`, or just call `akan_verify`, which runs the whole
|
|
47
|
+
chain over everything the working tree changed.
|
|
48
|
+
4. **Read each scaffolded file before editing it.** They are a few lines each and they extend base classes
|
|
49
|
+
that already generated the CRUD surface. Read the `akan-store` skill before touching a store or adding
|
|
50
|
+
signal endpoints, or you will redeclare `create<Model>` and hit an "already exists" typecheck error.
|
|
51
|
+
5. Now fill in: real fields in the constant, invariants in document and service, non-CRUD API in the signal,
|
|
52
|
+
extra state in the store, rendering in the UI. Filling in a scaffold is the job; creating one by hand is
|
|
53
|
+
not.
|
|
54
|
+
|
|
55
|
+
## Rules
|
|
56
|
+
|
|
57
|
+
- One run per unit of structure: two new entities are two `create-module` runs.
|
|
58
|
+
- `requiresApproval: true` on a plan is a review signal for you, not a permission gate — read
|
|
59
|
+
`predictedChanges`, then apply.
|
|
60
|
+
- Hand-writing files is the fallback, used only when no workflow matches, or when apply reports
|
|
61
|
+
unsupported / no-op / failed diagnostics telling you to act manually. Say which case applied.
|
|
62
|
+
- Never hand-edit a generated file to "finish" a scaffold — use `repair_generated` or `akan sync <app>`.
|
|
63
|
+
- No workflow tools this turn? The same order through bash:
|
|
64
|
+
`akan workflow list` → `akan workflow plan create-module --app <app> --module comment --format json --out <path>`
|
|
65
|
+
→ `akan workflow apply <path> --format json`.
|
|
66
|
+
|
|
67
|
+
## Worked example — "an intro page, two detail pages, and a comment zone with a database"
|
|
68
|
+
|
|
69
|
+
0. What must be stored? Only the comments. One module, `comment`. The three pages are static content.
|
|
70
|
+
1. `plan_workflow({ workflow: "create-module", inputs: { app: "<app>", module: "comment" } })`
|
|
71
|
+
2. `apply_workflow({ planPath })` — `lib/comment/*` exists, barrels synced.
|
|
72
|
+
3. Read the scaffolded files. Edit `comment.constant.ts` for the real shape. The store's CRUD already exists;
|
|
73
|
+
leave the store body empty unless you need extra state.
|
|
74
|
+
4. Write the pages and the comment UI against the generated actions — `st.do.createComment`,
|
|
75
|
+
`st.do.initCommentInPublic`, `st.use.commentListInPublic`.
|
|
76
|
+
5. `akan_verify` until clean.
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: akan-scalar
|
|
3
|
+
description: "Create an embedded value object (scalar) under lib/__scalar/, or add a relation field to another model — including which cascade direction is which."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Akan scalars and relations
|
|
7
|
+
|
|
8
|
+
## Scalars
|
|
9
|
+
|
|
10
|
+
A scalar is an embedded value object with no collection of its own. It lives in `lib/__scalar/<name>/`, is
|
|
11
|
+
composed into models with `scalar(...)`, and its dictionary is a `scalarDictionary`. Create one with the
|
|
12
|
+
`create-scalar` workflow; its UI surface is limited to `<Scalar>.Template.tsx` and `<Scalar>.Unit.tsx`.
|
|
13
|
+
|
|
14
|
+
## Relations
|
|
15
|
+
|
|
16
|
+
A reference to another model is `field(Model)`, or `field([Model])` for many. Files and images are a
|
|
17
|
+
relation to the `File` model — `image: field(File).optional()` — never an `Upload` field; `Upload` is a
|
|
18
|
+
signal-body primitive only. The store then generates `upload<Field>On<Model>(fileList)` for you.
|
|
19
|
+
|
|
20
|
+
**Hydrated vs raw.** A server query returns a hydrated `cnst.<Model>` instance with `set` / `save` /
|
|
21
|
+
`refresh`. A client `fetch` result is raw plain data with the functions stripped. Code that works in a
|
|
22
|
+
service and fails in a store is usually this.
|
|
23
|
+
|
|
24
|
+
**A model's `Date` fields are prototype accessors, not own properties.** `user.createdAt` is a `Dayjs` built
|
|
25
|
+
on first read, so `Object.keys(user)` and `{ ...user }` do **not** include it, while `"createdAt" in user`,
|
|
26
|
+
`JSON.stringify(user)` and `plainFieldsOf(user)` do. To copy a model write `new cnst.X().set(user)`, never a
|
|
27
|
+
spread.
|
|
28
|
+
|
|
29
|
+
## Cascade — the value names the direction, and getting it wrong loses data
|
|
30
|
+
|
|
31
|
+
The two actions sit on the same field shape, so `cascade` never means "related". It means one of exactly
|
|
32
|
+
these:
|
|
33
|
+
|
|
34
|
+
- `removeRef` — *when I am removed, remove what this field points at.* Declared on the relation the owner
|
|
35
|
+
holds: `image: field(File, { cascade: "removeRef" })`.
|
|
36
|
+
- `removeWith` — *when what this field points at is removed, remove me.* Declared on the child's own
|
|
37
|
+
reference to its owner, so the owner never learns about its children.
|
|
38
|
+
|
|
39
|
+
Nothing checks whether another document still references the same target, and `File` in particular is
|
|
40
|
+
deduped by `origin`, so two parents can share one row. Removal is soft, but the storage delete a
|
|
41
|
+
`_postRemove` performs is not — a cascade is not restorable.
|
|
42
|
+
|
|
43
|
+
Query-level removes fire no hooks and therefore no cascade: `removeManyByQuery`, the generated
|
|
44
|
+
`remove<Filter>`, and the facade's `removeById`. Remove one document at a time when it cascades.
|
|
45
|
+
|
|
46
|
+
## Field types
|
|
47
|
+
|
|
48
|
+
Import `Int`, `Float`, `ID`, `Any`, `Binary`, `Upload`, `enumOf` and `dayjs` from `akanjs/base`. `String`,
|
|
49
|
+
`Boolean` and `Date` are the globals. **`Number` is not a field type** — use `Int` or `Float`. There is no
|
|
50
|
+
JSON scalar; use `Any`. `Binary` is wire bytes and is never a model field.
|
|
51
|
+
|
|
52
|
+
Full detail: `get_guideline` with `scalarModule`, `scalarConstant`, `fieldRule`, or `queryRule`.
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: akan-service
|
|
3
|
+
description: "Add a service module — behaviour not centred on one stored model — under lib/_<service>/, and keep its abstract current."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Akan service modules
|
|
7
|
+
|
|
8
|
+
Behaviour that is not centred on a single stored model lives in a **service module**: the folder is
|
|
9
|
+
`lib/_<service>/` and its abstract file is `<service>.abstract.md` — the folder carries the underscore, the
|
|
10
|
+
abstract drops it. Its UI surface is limited to `<Service>.Util.tsx` and `<Service>.Zone.tsx`, and its
|
|
11
|
+
dictionary is a `serviceDictionary`.
|
|
12
|
+
|
|
13
|
+
## Rules
|
|
14
|
+
|
|
15
|
+
- Read the module's `*.abstract.md` **before** changing it — it states the invariants the code cannot show.
|
|
16
|
+
Update it when an invariant, workflow, or public behaviour changes; not for formatting or imports.
|
|
17
|
+
- Keep the public surface in the signal layer and the decisions in the service.
|
|
18
|
+
- Service methods stay a few lines: load → chain → `return await ….save()`. Write `return await` explicitly
|
|
19
|
+
in tail position. Side effects belong in `_preUpdate` / `_postCreate`, not inline. Fire-and-forget is
|
|
20
|
+
explicitly `void`-ed.
|
|
21
|
+
- Return `null` / `false` for "not allowed" or "not found" and let the signal decide whether that is an
|
|
22
|
+
error.
|
|
23
|
+
|
|
24
|
+
## Reaching other things
|
|
25
|
+
|
|
26
|
+
Inside a service, in preference order: `service<srv.XService>()` for another module's service,
|
|
27
|
+
`plug(AdapterClass)` for an adapter, `use<T>()` only for an `option.ts`-registered legacy singleton, and
|
|
28
|
+
`env(...)` for config. Injection resolves by field name — a field named `<refName>Service` finds the service
|
|
29
|
+
registered under `<refName>`, and the `Service` suffix is required.
|
|
30
|
+
|
|
31
|
+
An external API or piece of infrastructure is an `adapt()` class in `srvkit/`, injected with `plug`. It
|
|
32
|
+
self-registers, so do **not** add it to `lib/option.ts`.
|
|
33
|
+
|
|
34
|
+
Full detail: `get_guideline` with `modelService`.
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: akan-store
|
|
3
|
+
description: "Fill in a module store, or call module data and actions from UI — what store() already generates (CRUD, form setters, slice actions) and what belongs in your subclass."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Akan store: what is already generated
|
|
7
|
+
|
|
8
|
+
`class CommentStore extends store(sig.comment, () => ({ /* extra state */ })) { /* extra actions */ }`
|
|
9
|
+
gives you the whole CRUD and form surface for free. **Redeclaring any of it is a typecheck error
|
|
10
|
+
("already exists"), and the fix is to delete your version and call the generated one** — never rename around
|
|
11
|
+
it, never cast, never reimplement.
|
|
12
|
+
|
|
13
|
+
For a model `comment` (`<Slice>` is a signal slice suffix, e.g. `InPublic`):
|
|
14
|
+
|
|
15
|
+
## Generated actions — `st.do.*`
|
|
16
|
+
|
|
17
|
+
- write: `createComment(input)`, `updateComment(id, input)`, `removeComment(id)`
|
|
18
|
+
- form: `newComment()`, `editComment(model)`, `writeOnComment(path, value)`, `set<Field>OnComment(value)`,
|
|
19
|
+
`add<Field>OnComment(value)` / `sub<Field>OnComment(idx)` for arrays, `upload<Field>OnComment(fileList)`
|
|
20
|
+
for file fields, `checkCommentSubmitable()`, `submitComment()`, `createCommentInForm()`,
|
|
21
|
+
`updateCommentInForm()`
|
|
22
|
+
- single: `viewComment(model)`, `mergeComment(model, data)`, `setComment(...models)`, `resetComment()`
|
|
23
|
+
- per slice: `initComment<Slice>(...args)`, `refreshComment<Slice>()`, `selectComment<Slice>(model)`,
|
|
24
|
+
`setPageOfComment<Slice>(page)`, `loadMoreOfComment<Slice>()`, `setLimitOfComment<Slice>(limit)`,
|
|
25
|
+
`setQueryArgsOfComment<Slice>(...)`, `setSortOfComment<Slice>(sort)`
|
|
26
|
+
|
|
27
|
+
## Generated state — `st.use.*`
|
|
28
|
+
|
|
29
|
+
`comment`, `commentLoading`, `commentForm`, `commentFormLoading`, `commentSubmit`, `commentModal`,
|
|
30
|
+
`commentViewAt`, `commentOperation`; per slice `commentList<Slice>`, `commentListLoading<Slice>`,
|
|
31
|
+
`commentInitList<Slice>`, `commentSelection<Slice>`, `commentInsight<Slice>`, `pageOfComment<Slice>`,
|
|
32
|
+
`lastPageOfComment<Slice>`, `hasMoreOfComment<Slice>`, `limitOfComment<Slice>`, `sortOfComment<Slice>`.
|
|
33
|
+
|
|
34
|
+
## What your subclass is for
|
|
35
|
+
|
|
36
|
+
Only what the generated set does not cover: extra client state, a multi-step or cross-model workflow, and
|
|
37
|
+
calls to the non-CRUD endpoints you added in the signal. Each is an `async` method using `this.set(...)` /
|
|
38
|
+
`this.get()` / `this.pick(...)`.
|
|
39
|
+
|
|
40
|
+
A store that needs none of this stays empty. `store(sig.comment, () => ({}))` with an empty body is the
|
|
41
|
+
correct, complete result — an empty store is not an unfinished one.
|
|
42
|
+
|
|
43
|
+
## Two rules the linter enforces
|
|
44
|
+
|
|
45
|
+
- **An action returns nothing.** Every method dispatches through `st.do.<action>()` and is typed `void`, so a
|
|
46
|
+
returned value is unreachable. Write it into state with `this.set({ … })`.
|
|
47
|
+
- **Never `import type { RootStore } from "../st"`** — it crashes `akan build` with a Bun SSR segfault.
|
|
48
|
+
|
|
49
|
+
## From UI
|
|
50
|
+
|
|
51
|
+
Read with `st.use.commentListInPublic()`, act with `st.do.createComment(input)` / `st.do.submitComment()`.
|
|
52
|
+
Do not wrap a generated action in a store method just to rename it.
|
|
53
|
+
|
|
54
|
+
**Pass a form setter by reference**: `onChange={st.do.setTypeOnComment}`, never
|
|
55
|
+
`onChange={(v) => st.do.setTypeOnComment(v)}`. The two run identically, but the arrow is an anonymous
|
|
56
|
+
closure, so the control emits no `data-akan-action` and publishes no agent tool for the field —
|
|
57
|
+
`no-unpublished-form-setter.grit` fails the build on it.
|
|
58
|
+
|
|
59
|
+
## Signal side
|
|
60
|
+
|
|
61
|
+
The scaffolded `slice(…, { guards: { root, get, cru } })` already exposes get / list / create / update /
|
|
62
|
+
remove. Add a query or mutation only for behaviour beyond CRUD, and give it its own `guards` array — a
|
|
63
|
+
custom endpoint never inherits the slice's.
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: akan-ui
|
|
3
|
+
description: "Build Akan pages and components: the server-by-default rule, the Unit/View vs Template/Zone/Util split, forms, and what never to hand-roll."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Akan UI
|
|
7
|
+
|
|
8
|
+
A new View / Unit / Template / Zone / Util for an existing module comes from the `create-ui` workflow (the
|
|
9
|
+
`akan-scaffold` skill). Edit the scaffolded component instead of authoring one from scratch.
|
|
10
|
+
|
|
11
|
+
## The boundary is mechanical, not a judgement call
|
|
12
|
+
|
|
13
|
+
| role | side | directive |
|
|
14
|
+
| --- | --- | --- |
|
|
15
|
+
| `<Model>.Unit.tsx`, `<Model>.View.tsx` | server | never `"use client"` |
|
|
16
|
+
| `<Model>.Template.tsx`, `<Model>.Zone.tsx`, `<Model>.Util.tsx` | client | `"use client"` on line 1 |
|
|
17
|
+
| `page/**` | server | never |
|
|
18
|
+
|
|
19
|
+
**Server is the default and `"use client"` is a cost you justify per component.** A component earns the
|
|
20
|
+
directive only by using a hook, a JSX event handler, the store, a browser global, or a client-only package.
|
|
21
|
+
Rendering markup, reading a param, calling `l()`, and mapping over data are all server work. Measure with
|
|
22
|
+
`akan quality ssr`; treat 50% server share as the floor and a falling share as a regression.
|
|
23
|
+
|
|
24
|
+
Four moves that keep markup on the server: wrap the interaction and render `children` untouched; split a
|
|
25
|
+
compound component so only the provider holds state; fetch in the route and pass the result into a `Zone` as
|
|
26
|
+
an `init` prop; push the boundary down to the leaf that needs it.
|
|
27
|
+
|
|
28
|
+
## Never hand-roll these
|
|
29
|
+
|
|
30
|
+
- loading / empty / list states → `Load.Units` / `Load.View` / `Load.Edit` with `renderItem`, `renderList`,
|
|
31
|
+
`renderView`, `renderEmpty`; `<Empty />` for a bare placeholder; `Model.New` / `Model.Edit` /
|
|
32
|
+
`Model.SureToRemove` for CRUD modals.
|
|
33
|
+
- a mount-time fetch → the route calls `fetch.init<Model><Slice>(…)` before the first byte. A
|
|
34
|
+
`useEffect(…, [])` that loads data is flagged by `akan quality ssr`, and `fetch.init*` from a client file
|
|
35
|
+
fails lint outright.
|
|
36
|
+
- a model field input → `Field.*`, never a bare `<input>`.
|
|
37
|
+
- a mode switch → `Tab`, not a `useState`.
|
|
38
|
+
|
|
39
|
+
## Component shape
|
|
40
|
+
|
|
41
|
+
`export const X = ({ … }: XProps) => { return (…); };` — arrow const, block body. Never `React.FC`, never
|
|
42
|
+
`defaultProps`, never `PropsWithChildren`; children are `children: ReactNode`. Declare
|
|
43
|
+
`interface XProps` directly above the component with `className?: string` first.
|
|
44
|
+
|
|
45
|
+
Conditional render is `cond ? <X/> : null`. **Never `{cond && <X/>}`** — in a `className` context it renders
|
|
46
|
+
the literal string `"false"`.
|
|
47
|
+
|
|
48
|
+
## Classes
|
|
49
|
+
|
|
50
|
+
Never hand-order Tailwind classes; the formatter sorts them and its output is correct. Stay inside the
|
|
51
|
+
semantic token vocabulary — `bg-primary`, `text-foreground/70` — because a raw palette class (`bg-blue-500`),
|
|
52
|
+
an arbitrary colour (`bg-[#3b82f6]`), a daisyUI legacy class, and a colour literal in `style={{…}}` all
|
|
53
|
+
render no CSS and fail lint. Reach for `cn` only for a conditional or to merge an incoming `className`, and
|
|
54
|
+
merge the caller last.
|
|
55
|
+
|
|
56
|
+
## Forms
|
|
57
|
+
|
|
58
|
+
Store-driven end to end: `value={xForm.field}` with `onChange={st.do.setFieldOnX}`, **the setter passed by
|
|
59
|
+
reference**. An inline arrow silently drops the control's agent annotation and fails
|
|
60
|
+
`no-unpublished-form-setter.grit`. Nested rows use `st.do.writeOnX("payments.3.name", v)`.
|
|
61
|
+
|
|
62
|
+
Full detail: `get_guideline` with `ssrRule`, `componentRule`, `cssRule`, `recipeRule`, or the per-role
|
|
63
|
+
`modelUnit` / `modelView` / `modelZone` / `modelTemplate` / `modelUtil`.
|