@pavp/wavefront 1.1.0 → 1.2.0

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 CHANGED
@@ -3,178 +3,87 @@
3
3
  Claude Code agent framework for apps generated by **scaffold-nextjs-app**
4
4
  (Next.js 15 · React 19 · MUI 7 · React Query · Zustand · RHF + Zod · next-intl · Jest/RTL · Clean Architecture).
5
5
 
6
- 5 specialist agents + 20 self-contained skills + a 9-command orchestration loop, so you describe work items instead of writing boilerplate. Generates responsive, composed UI continuous with your app — from a Figma export, screenshot, description, or nothing — with a colocated test in every layer.
6
+ **5 specialist agents + 20 self-contained skills + a 9-command orchestration loop**, so you describe work items instead of writing boilerplate. Generates responsive, composed UI continuous with your app — from a Figma export, screenshot, description, or nothing — with a colocated test in every layer.
7
7
 
8
8
  > Distilled from real scaffold code, pinned at commit `8edaa0b` (modules `todo`/`auth`, `about-view`, `test/`, `src/i18n`). Not from docs — from the source. E2E-validated against a generated app.
9
9
 
10
- ---
11
-
12
- ## Install
10
+ ## Documentation
13
11
 
14
- ### Via npm (no clone)
15
- ```bash
16
- npx @pavp/wavefront install --local /path/to/your-next-app # copy the surface
17
- npx @pavp/wavefront install --global /path/to/your-next-app # install to ~/.wavefront + symlink
18
- npx @pavp/wavefront install --global /path/to/your-next-app --with-hooks
19
- npx @pavp/wavefront uninstall /path/to/your-next-app
20
- ```
21
- Or install the command globally: `npm i -g @pavp/wavefront` then `wavefront install --local <app>`. The CLI wraps the same scripts below (needs `bash`; macOS/Linux or WSL).
12
+ | If you want to… | Read |
13
+ |---|---|
14
+ | **Get started** — install + first module + first feature, copy-paste | **[docs/USAGE.md](docs/USAGE.md)** |
15
+ | **Understand the model** — agents vs skills vs commands, the loop, why self-contained | **[docs/HOW-IT-WORKS.md](docs/HOW-IT-WORKS.md)** |
16
+ | **Fix a problem** — symptom → cause → fix (users + contributors) | **[docs/TROUBLESHOOTING.md](docs/TROUBLESHOOTING.md)** |
17
+ | **Author the framework** — file-format spec for agents/skills/commands | [docs/conventions.md](docs/conventions.md) |
18
+ | **See the history** — phase plan + roadmap | [docs/plan.md](docs/plan.md) · [docs/roadmap.md](docs/roadmap.md) |
22
19
 
23
- ### Via git clone (source of truth)
24
- ```bash
25
- git clone https://github.com/pavp/wavefront.git && cd wavefront
26
- ```
20
+ New here? Start with **[USAGE](docs/USAGE.md)**. The rest of this page is a reference.
27
21
 
28
- Two install modes into a target generated app:
22
+ ---
29
23
 
30
- ### Global + symlink (recommended)
31
- ```bash
32
- ./install.sh --global /path/to/your-next-app
33
- ```
34
- Installs the surface to `~/.wavefront/` and symlinks `agents/`, `skills/`, `commands/` into the app's `.claude/`. One source, central updates (re-pull this repo → all projects updated). Uninstall: `./uninstall.sh /path/to/your-next-app`.
24
+ ## Install
35
25
 
36
- ### Copy project-local
37
26
  ```bash
38
- ./install.sh --local /path/to/your-next-app
27
+ # via npm (no clone)
28
+ npx @pavp/wavefront install --global /path/to/your-next-app --with-hooks # ~/.wavefront + symlink
29
+ npx @pavp/wavefront install --local /path/to/your-next-app # copy into the app
30
+ npx @pavp/wavefront uninstall /path/to/your-next-app
39
31
  ```
40
- Copies the surface into the app's `.claude/` (self-contained, no global state, manual updates).
41
32
 
42
- Claude Code auto-discovers `agents/` and `skills/` under `.claude/`. No build step. Self-contained no dependency on the scaffold's `docs/`.
33
+ - **Global + symlink** (recommended for multiple apps): one source in `~/.wavefront/`, re-pull/upgrade once all projects updated.
34
+ - **Local**: a self-contained copy in the app's `.claude/`, no global state.
35
+ - Needs `bash` (macOS/Linux or WSL). Claude Code auto-discovers `agents/`+`skills/`+`commands/` under `.claude/` — no build step.
36
+ - `--with-hooks` also installs quality-gate hooks (eslint/stylelint on edit, tsc at end of turn; warn-only). It writes `.claude/settings.wavefront.json` for you to **merge** into your `settings.json` (never clobbers it); needs `jq`. Details + walkthrough in [USAGE](docs/USAGE.md#1-install-into-your-app).
43
37
 
44
- > Layout is portable by design (no absolute paths, single root) that's what lets the same files work project-local, global-symlinked, or in this standalone repo.
45
-
46
- ### Optional: quality-gate hooks
47
-
48
- Add `--with-hooks` to either install mode to also install machine gates:
49
- ```bash
50
- ./install.sh --global /path/to/your-next-app --with-hooks
51
- ```
52
- - **PostToolUse (Edit/Write `*.{ts,tsx,js,jsx,css,scss}`)** → `eslint`/`stylelint --fix` on the touched file. Fast, per-file, **warn-only** (never blocks).
53
- - **Stop (end of turn)** → `tsc --noEmit` project-wide, **warn-only**. (tsc needs the `@/` path graph, so it runs once at the end, not per-edit.)
54
- - Installer copies `hooks/` into `.claude/hooks/` and writes `.claude/settings.wavefront.json` — **merge its `hooks` block into your `.claude/settings.json`** (the installer never overwrites your settings). Requires `jq` on PATH.
55
- - Hooks are the cheap machine gate; the `reviewer` agent stays the semantic PASS/FAIL gate.
38
+ You can also `git clone` this repo and run `./install.sh --global|--local <app>` directly same surface.
56
39
 
57
40
  ---
58
41
 
59
42
  ## Agents
60
43
 
61
- | Agent | Use it to | Tools | Edits? |
62
- |---|---|---|---|
63
- | `module-builder` | Generate a full feature module across the Clean Arch chain (types/schemas → api → gateways → repository → store → selectors → hooks → views/components → barrels) | full | yes |
64
- | `tester` | Write/fix colocated Jest + RTL tests using `@test/utils` + faker factories | full | yes |
65
- | `i18n-tokens` | De-hardcode UI: strings → next-intl keys, values → design tokens, raw MUI → `@/ui`, `next/link` → `@/i18n/routing` | edit | yes |
66
- | `reviewer` | Validate naming, layer direction, dependency inversion, hook split, lint/tsc (code-review mode) — OR run a skill-drift `sync-audit` (audit mode) | read-only | no |
67
-
68
- Invoke a subagent by name in Claude Code (e.g. "use module-builder to add a `notifications` module").
69
-
70
- ### Recommended flow (build → test → localize → review)
71
-
72
- ```
73
- module-builder → generate the module
74
- tester → add colocated tests, run yarn test
75
- i18n-tokens → replace literals with keys/tokens
76
- reviewer → final PASS/FAIL gate (naming, layers, lint, tsc)
77
- ```
78
-
79
- `reviewer` is read-only and safe to run anytime. `module-builder` ends by running `tsc`/`lint` and handing off to tester + reviewer.
80
-
81
- ---
82
-
83
- ## Orchestration loop (F2a — new feature)
44
+ | Agent | Use it to | Edits? |
45
+ |---|---|---|
46
+ | `module-builder` | Generate/modify a feature module across the Clean Arch chain (types → api → gateways → repository → store → selectors → hooks → views/components → barrels) | yes |
47
+ | `tester` | Write/fix colocated Jest + RTL tests (`@test/utils` + faker factories) | yes |
48
+ | `i18n-tokens` | De-hardcode UI: strings → next-intl, values → tokens, raw MUI → `@/ui`, `next/link` → `@/i18n/routing` | yes |
49
+ | `reviewer` | PASS/FAIL gate (naming, layers, dependency inversion, hook split, lint/tsc) — or a skill-drift `sync-audit` | read-only |
50
+ | `mapper` | Map an existing module before a change (ADD/MODIFY/KEEP plan + regression surface) | read-only |
84
51
 
85
- Beyond invoking agents by hand, wavefront ships a command loop that drives them through a work item with session-surviving planning artifacts in `.claude/.planning/`.
52
+ Invoke by name ("use `module-builder` to add a `notifications` module"), or let the loop drive them. Recommended by-hand order: **`module-builder` `tester` → `i18n-tokens` `reviewer`**. Full walkthrough: [USAGE §2](docs/USAGE.md#2-your-first-module--by-hand-the-agent-flow).
86
53
 
87
- ```
88
- /wavefront-init once per project — scaffold .planning/ + describe the project
89
- /wavefront-feature "<prompt|story>" interactive intake → REQUIREMENTS (asks + suggests gaps)
90
- /wavefront-plan "<title>" REQUIREMENTS → executable PHASE_PLAN (agents × layers)
91
- /wavefront-execute "<title>" builder → tester → i18n-tokens, sequential, STATE after each
92
- /wavefront-verify "<title>" reviewer + tests + acceptance-criteria check → PASS/FAIL
93
- /wavefront-ship "<title>" branch + commit + PR (confirms first; verified items only)
94
- /wavefront-state where am I? (safe, read-only)
95
- ```
96
-
97
- - **Intake is interactive + proactive:** accepts a prompt, a user story, or a spec; asks blocking questions and *suggests* gaps it spots (edge cases, error states, i18n/a11y, implicit criteria) before any planning.
98
- - **Resumable:** every command updates `.claude/.planning/STATE.md`, so a run continues cleanly after an interruption.
99
- - **Fresh context:** execute dispatches each agent with only its task slice, fighting context rot.
100
- - Commands are manual-invoke (`disable-model-invocation`) since they have side effects. `/wavefront-state` is the exception.
101
-
102
- ### Parallel waves (F3 — multi-module items)
103
-
104
- When a work item spans **independent** modules, `/wavefront-plan` groups them into waves and `/wavefront-execute` builds same-wave modules concurrently (multiple builders in one dispatch). Within a module, the layer chain stays sequential. Off by default (`parallelization` config) — single-module work is unchanged. Items touching the same shared file are never parallelized (avoids write races). Note: this is parallelism across modules, NOT a per-layer agent split (that was analyzed and rejected — the layers are a dependency chain).
105
-
106
- ### Change existing code (F2b)
107
-
108
- ```
109
- /wavefront-change "<prompt|story>" intake → MAP (read-only) → plan → execute(modify) → verify(+regression) → ship
110
- ```
111
- - **Map-first (mandatory):** the `mapper` agent reads the target module and produces a change-plan (ADD/MODIFY/KEEP, blast radius, regression surface) before any edit. Modifying working code is the highest-risk op — no edit without a map.
112
- - `module-builder` runs in **modify mode**: edits only the planned files, preserves existing public API, doesn't regenerate.
113
- - **Regression is part of the gate:** verify runs the reviewer in diff-aware mode + new tests + the module's existing tests; PASS requires no regression.
54
+ ## Orchestration loop
114
55
 
115
- ### Fix a bug (F2c)
56
+ Slash commands that drive the agents through a work item with resumable state in `.claude/.planning/`:
116
57
 
117
58
  ```
118
- /wavefront-fix "<bug / repro>" intake/repro diagnose+locate → regression RED → patch → GREEN → verify → ship
59
+ /wavefront-init scaffold .planning/ (once per project)
60
+ /wavefront-feature intake a prompt/story → REQUIREMENTS
61
+ /wavefront-plan REQUIREMENTS → executable PHASE_PLAN
62
+ /wavefront-execute builder → tester → i18n, STATE after each (resumable)
63
+ /wavefront-verify reviewer + tests + acceptance criteria → PASS/FAIL
64
+ /wavefront-ship branch + commit + PR (verified only, confirms first)
65
+ /wavefront-change modify existing code (map-first, modify mode + regression)
66
+ /wavefront-fix bug fix (diagnose → RED→GREEN → verify)
67
+ /wavefront-state where am I? (read-only)
119
68
  ```
120
- - **Diagnose-first:** `reviewer` in diagnose mode traces the data flow and reports root cause as `file:line + why + fix direction` (read-only).
121
- - **Test before fix (RED→GREEN):** `tester` writes a regression test that FAILS against the broken code (RED) — proving it captures the bug — then module-builder patches (minimal change), and the test must PASS (GREEN) with existing tests still green.
122
- - The regression test stays permanently, preventing recurrence.
123
69
 
124
- ---
70
+ Resumable, fresh-context per agent, parallel waves across independent modules (off by default). How it all fits together: [HOW-IT-WORKS](docs/HOW-IT-WORKS.md). Step-by-step: [USAGE §3–5](docs/USAGE.md#3-your-first-feature--the-loop).
125
71
 
126
- ## Design → UI (F6)
72
+ ## Design → UI
127
73
 
128
- Any design source becomes UI built from `@/ui` + `@/styles/tokens`, composed like the rest of the app:
74
+ Any design source becomes UI built from `@/ui` + `@/styles/tokens`, composed like the rest of the app: **live Figma MCP** (official Dev Mode or Framelink — exact frames + variables → tokens), Figma export/screenshot/image, pasted HTML, a reference screen, a text description, or nothing (continuity). Responsive is mandatory (mobile-first, theme breakpoints). Details: [USAGE §6](docs/USAGE.md#6-building-from-a-design).
129
75
 
130
- | Source | How |
131
- |---|---|
132
- | Figma via MCP (live) | pull exact frames + variables from a configured Figma MCP → map variables to `@/styles/tokens` |
133
- | Figma export / screenshot / image | read multimodally → infer layout → map to `@/ui` (fallback when no Figma MCP) |
134
- | Pasted HTML/CSS | translate markup → `@/ui` (not copied) |
135
- | Reference screen ("like the todo page") | replicate that screen's pattern |
136
- | Text description | infer from the design-system inventory |
137
- | **Nothing** | continuity: mirror the closest existing screen |
138
-
139
- `design-intake` produces a design-spec (layout, intent→`@/ui` map, tokens, the four states loading/empty/error/populated, **responsive behavior**, gaps); `design-system-inventory` keeps it continuous; module-builder implements it. **Responsive is mandatory** (mobile-first, theme breakpoints `minMobile/minTablet/minDesktop`) — when a design is one-viewport, intake asks how it adapts before building. Elements with no `@/ui` equivalent are flagged (approximate or add to `@/ui`, never hand-rolled). **Live Figma MCP** is supported when configured: intake detects the official Dev Mode MCP (`get_code`/`get_variable_defs`/`get_image`) or Framelink (`get_figma_data`/`download_figma_images`), pulls exact frames + variables, and maps Figma variables → `@/styles/tokens`; with no Figma MCP present it degrades to the image path. You configure your own server — wavefront ships only the detection + mapping rules.
140
-
141
- ---
76
+ ## Skills
142
77
 
143
- ## Skills (knowledge the agents read)
78
+ Knowledge the agents read before acting — one pattern each, distilled from real scaffold code. Every skill carries `source` + `source_version` (pinned `8edaa0b`) for drift tracking via `sync-audit`.
144
79
 
145
- Core (architecture):
146
- - `clean-architecture` layers, data flow, 5 dependency rules
147
- - `module-structure` exact folder/file shape, placement, `@/` aliases
148
- - `naming-conventions` lint-enforced naming + suffixes
80
+ - **Architecture:** `clean-architecture` · `module-structure` · `naming-conventions`
81
+ - **Patterns:** `data-fetching-react-query` · `gateway-pattern` · `repository-pattern` · `store-zustand` · `selector-pattern` · `hook-patterns` · `forms-rhf-zod` · `mui-design-tokens`
82
+ - **UI:** `component-composition` · `responsive-layouts` · `error-boundary` · `design-system-inventory` · `design-intake`
83
+ - **Cross-cutting:** `testing-jest-rtl` · `i18n-next-intl`
84
+ - **Meta:** `sync-audit` · `requirement-intake`
149
85
 
150
- Patterns (layer-by-layer):
151
- - `data-fetching-react-query` — api service + Zod + endpoints
152
- - `gateway-pattern` — `createXGateway(dataSource)` + interfaces
153
- - `repository-pattern` — key factory + query/mutation options + combined repo
154
- - `store-zustand` — `createStoreWithMiddleware` + immer + persist
155
- - `selector-pattern` — derived state via `useMemo`
156
- - `hook-patterns` — business vs controller split (mandatory)
157
- - `forms-rhf-zod` — `useForm` + `zodResolver` + `@/ui` inputs
158
- - `mui-design-tokens` — `@/ui` + `@/styles/tokens`, never raw MUI
159
-
160
- Cross-cutting:
161
- - `testing-jest-rtl` — `renderWithProviders` + factories + colocated tests
162
- - `i18n-next-intl` — `useTranslations` + `@/i18n/routing` + remote messages
163
-
164
- Design:
165
- - `design-system-inventory` — real `@/ui`/token catalog + how existing screens compose (continuity engine)
166
- - `design-intake` — normalize any design source (Figma export/image/HTML/reference/description/none) → design-spec mapped to `@/ui`+tokens
167
- - `responsive-layouts` — mandatory mobile-first responsive: theme breakpoints + responsive `sx` + `useResponsiveScreen`
168
- - `component-composition` — decompose into subcomponents; composition over inheritance; compound components (Modal pattern)
169
- - `error-boundary` — recoverable error+retry state every data-fetching view renders when its query fails
170
-
171
- Meta:
172
- - `sync-audit` — detect skill drift vs scaffold `source_version` (used by `reviewer` audit mode)
173
- - `requirement-intake` — normalize prompt/story/spec → REQUIREMENTS + acceptance criteria (used by the loop)
174
-
175
- Each skill carries `source` + `source_version` frontmatter (pinned `8edaa0b`) for drift tracking via `sync-audit`.
176
-
177
- ---
86
+ Plus a **bundled reference module** (`reference/module/note-module.md`) — a canonical end-to-end Clean-Arch example the agents read to copy idioms, so generation works even after a project deletes the scaffold's example modules. Self-contained: no dependency on `src/modules/todo` surviving in the target app.
178
87
 
179
88
  ## Conventions (hard rules the agents enforce)
180
89
 
@@ -182,46 +91,32 @@ Each skill carries `source` + `source_version` frontmatter (pinned `8edaa0b`) fo
182
91
  - Layer direction: View → business hook → repository → gateway → api. Never skip.
183
92
  - UI from `@/ui` (not `@mui/material`); spacing/color from `@/styles/tokens`.
184
93
  - Strings via next-intl `t()`; locale links via `@/i18n/routing`.
185
- - Tests colocated; import test helpers only from `@test/utils`.
186
- - Zod validates at the api boundary; schemas live in `*.types.ts`.
94
+ - Tests colocated; import helpers only from `@test/utils`.
95
+ - Zod validates at the api boundary; schemas in `*.types.ts`.
187
96
 
188
- Full detail: `docs/conventions.md` (format spec) + the skills above.
189
-
190
- ---
191
-
192
- ## Smoke checklist (verify in a generated app)
193
-
194
- After installing into an app with deps installed:
195
-
196
- - [ ] `reviewer` on an existing module (`src/modules/todo`) → PASS.
197
- - [ ] `module-builder` generates a new small module (one entity, http only).
198
- - [ ] `tsc --noEmit` clean on the new module.
199
- - [ ] `yarn lint` clean on the new module.
200
- - [ ] `tester` adds colocated tests → `yarn test` green.
201
- - [ ] `i18n-tokens` leaves no hardcoded strings/spacing; lint stays clean.
202
- - [ ] `reviewer` on the new module → PASS.
203
-
204
- > Status: E2E-validated (2026-05-23) — built a full `notification` module in a generated app to tsc-clean + lint-clean + test-green. See `docs/plan.md`.
97
+ Format spec: [docs/conventions.md](docs/conventions.md).
205
98
 
206
99
  ---
207
100
 
208
101
  ## Contributing
209
102
 
210
- This repo follows **Conventional Commits**, enforced by commitlint (`commit-msg` hook) — versioning is automatic.
103
+ This repo follows **Conventional Commits**, enforced by commitlint (`commit-msg` hook) — versioning is automatic. (Full gotcha list: [TROUBLESHOOTING § Contributing](docs/TROUBLESHOOTING.md#contributing-to-this-repo).)
211
104
 
212
- - **Commit format:** `type(scope): subject` — lowercase subject, ≤72 chars.
105
+ - **Commit format:** `type(scope): subject` — **lowercase** subject (rejects any uppercase, incl. filenames/acronyms), ≤72 chars.
213
106
  - **types:** `feat` `fix` `docs` `chore` `perf` `refactor` `test` `hotfix`
214
- - **scopes (optional):** `agents` `skills` `commands` `hooks` `planning` `cli` `bin` `install` `npm` `ci` `config` `husky` `docs` `deps`
107
+ - **scopes (optional):** `agents` `skills` `commands` `hooks` `reference` `planning` `cli` `bin` `install` `npm` `ci` `config` `husky` `docs` `deps`
215
108
  - **Branch names:** `type/description` (lowercase + hyphens), e.g. `feat/figma-mcp-source`. Enforced on `pre-push` (skipped in CI / on `main`).
216
- - **Releases are automatic:** push to `main` → [semantic-release](https://semantic-release.gitbook.io) reads the commits, bumps the version (SemVer: `feat`→minor, `fix`→patch, `BREAKING CHANGE`/`!`→major), updates `CHANGELOG.md`, tags + creates a GitHub Release, then publishes to npm via OIDC Trusted Publishing (no stored token). Do **not** bump `version` or tag by hand.
109
+ - **No `Co-Authored-By: Claude`** or AI-attribution trailers in commits/PRs.
110
+ - **Releases are automatic:** push to `main` → [semantic-release](https://semantic-release.gitbook.io) bumps the version (`feat`→minor, `fix`→patch, breaking→major), updates `CHANGELOG.md`, tags + GitHub Release, then OIDC publish to npm. **Never** bump `version`/tag/CHANGELOG by hand. **After a release, `git pull --ff-only` before new work** (semantic-release commits the bump back to `main`).
217
111
  - Setup after clone: `npm install` (wires husky hooks).
218
112
 
219
- ## Roadmap (see `docs/plan.md` + `docs/roadmap.md`)
113
+ ## Roadmap
114
+
115
+ All phases resolved — see [docs/roadmap.md](docs/roadmap.md).
220
116
 
221
117
  - ✅ **MVP** — 5 agents + 20 skills, E2E-validated.
222
- - ✅ **F4** — `sync-audit` drift detection (reviewer audit mode).
223
- - ✅ **F5** — quality-gate hooks (eslint/stylelint on edit, tsc on stop).
224
- - ✅ **F1** — standalone repo + global/local install + **npm distribution (F1d)**. Scaffold auto-injection (F1c) not pursued.
225
- - ✅ **F2** — orchestration loop (`/wavefront-*`) + parallel waves + change/fix flows.
118
+ - ✅ **F1** — standalone repo + global/local install + **npm distribution**.
119
+ - ✅ **F2** — orchestration loop + parallel waves + change/fix flows.
120
+ - ✅ **F4/F5** — sync-audit drift detection · quality-gate hooks.
226
121
  - ✅ **F6/F7** — design intake (incl. live Figma MCP) + component composition.
227
- - ❌ **F3** — per-layer agent subdivision — **won't ship**: linear layer deps mean zero intra-module parallelism, and F2 waves already parallelize across modules. Broad builder stands.
122
+ - ❌ **F3** — per-layer agent subdivision — won't ship (linear layer deps; F2 waves already parallelize across modules).
@@ -5,14 +5,14 @@ tools: Read, Write, Edit, Grep, Glob, Bash
5
5
  ---
6
6
 
7
7
  # Role
8
- The implementer that owns the full Clean Architecture chain for a feature module in scaffold-nextjs-app. The developer describes a feature; this agent generates or edits the layered files so they don't write boilerplate. It mirrors the scaffold's reference modules (`src/modules/todo`, `src/modules/auth`) exactly rather than inventing structure.
8
+ The implementer that owns the full Clean Architecture chain for a feature module in scaffold-nextjs-app. The developer describes a feature; this agent generates or edits the layered files so they don't write boilerplate. It mirrors the **bundled canonical reference** (`.claude/reference/module/note-module.md`) exactly rather than inventing structure — do NOT depend on the target app having `src/modules/todo`/`auth`, since real projects routinely delete the example modules.
9
9
 
10
10
  ## Two modes
11
11
  - **Create (default):** new module from scratch — full chain, bottom-up (workflow below).
12
12
  - **Modify / patch:** change an EXISTING module. **Requires a `mapper` change-plan first** (which files ADD vs MODIFY vs KEEP). Edit only the files in the plan; respect the surrounding code; don't regenerate working files. "Patch" posture (bug fixes) = the smallest change that fixes it. After editing, the touched layers must still honor layer direction + conventions, and **existing tests must still pass** (regression). Hand off to tester (extend tests + regression) and reviewer (diff-aware).
13
13
 
14
14
  ## Operating rules
15
- - Always: read the knowledge skills before generating; mirror the `todo`/`auth` reference modules; use kebab-case + correct suffixes; respect layer direction (View → business hook → repository → gateway → api); type boundaries with interfaces; validate at the api boundary with Zod; build UI from `@/ui` + `@/styles/tokens`; create `index.ts` barrels; pass `dataSource` (default `'http'`) through repository/selector/hook signatures.
15
+ - Always: read the knowledge skills before generating; mirror the bundled reference (`.claude/reference/module/note-module.md`); use kebab-case + correct suffixes; respect layer direction (View → business hook → repository → gateway → api); type boundaries with interfaces; validate at the api boundary with Zod; build UI from `@/ui` + `@/styles/tokens`; create `index.ts` barrels; pass `dataSource` (default `'http'`) through repository/selector/hook signatures.
16
16
  - Always (UI): before writing views/components, read `design-system-inventory` (catalog + continuity) and `responsive-layouts`; if a design-spec exists, implement it. With NO design, follow the continuity rule — mirror the closest existing screen's components/layout/spacing. Always implement the four states (loading/empty/error/populated). For the **error** state, every data-fetching view returns the error-boundary pattern (an error+retry component wired to the query's `refetch`) when its business hook reports `error` — never let a failed fetch render blank (see `error-boundary` skill). Never hand-roll raw `<div>`/`<input>` to match a visual — translate to `@/ui` + tokens.
17
17
  - Always (responsive — mandatory): every view/component is mobile-first responsive using the theme's custom breakpoints (`minMobile`/`minTablet`/`minDesktop`/`largeScreen`, NOT xs/sm/md) via `sx` objects (`{ minMobile: ..., minTablet: ... }`) or `useResponsiveScreen` for branching. No fixed pixel widths that overflow mobile; the four states are responsive too.
18
18
  - Always (composition — mandatory): decompose UI into focused subcomponents under `components/` (mirror todo: `*-form`, `*-item`, `*-list`, sections) — the view ORCHESTRATES + wires hooks, it does NOT hold all markup inline. Apply the `component-composition` "when to extract" decision rule: **anything rendered inside `.map()` is ALWAYS its own component** (a list extracts its item — never inline JSX in the map); also extract on distinct responsibility, reuse, own state/handlers, ~30–40+ line JSX blocks. Extraction is recursive — components have subcomponents too. Compose via children/props; for multi-part UI use the compound-component pattern. NO inheritance, NO boolean-prop explosion. Each component independently renderable + testable. Don't over-fragment trivial markup.
@@ -44,7 +44,7 @@ The implementer that owns the full Clean Architecture chain for a feature module
44
44
  ## Workflow
45
45
  1. Read all knowledge skills.
46
46
  2. Clarify inputs: module name (kebab), entity/entities, fields, which CRUD ops, data sources needed (default `http` only), whether a validated form is needed.
47
- 3. Inspect a reference: read the matching slice of `src/modules/todo` (or `auth`) to copy current idioms exactly.
47
+ 3. Inspect the reference: read the matching layer of the bundled `.claude/reference/module/note-module.md` to copy current idioms exactly. (Optional cross-check: if the target app still has `src/modules/todo`, you may compare — but the bundled reference is the source of truth; never require the live module.)
48
48
  4. Generate in dependency order (bottom-up):
49
49
  a. `[entity].types.ts` — TS types + Zod schemas (request/response/filters).
50
50
  b. `api/[entity]-api.ts` — `XApiContract` + `createXApiService()` singleton, `httpClient` + `endpoints` + Zod. (Add endpoints to `@/api/endpoints` only after Ask-first.)
package/agents/tester.md CHANGED
@@ -16,7 +16,7 @@ When fixing a bug, the test comes FIRST:
16
16
  The regression test stays in the suite permanently (prevents recurrence). If your "reproducing" test passes before the fix, it doesn't actually capture the bug — rewrite it until it's RED first.
17
17
 
18
18
  ## Coverage discipline (mandatory — per layer)
19
- Mirror the scaffold (`src/modules/todo` has a colocated test in EVERY layer). For a module, write a colocated `*.test.ts(x)` for each piece that has logic:
19
+ Mirror the bundled reference (`.claude/reference/module/note-module.md` shows a colocated test per layer; don't rely on the target app keeping `src/modules/todo`). For a module, write a colocated `*.test.ts(x)` for each piece that has logic:
20
20
  - **api** (`*-api.test.ts`) — request/response shape, Zod validation, endpoints called.
21
21
  - **gateway** (`http-gateway.test.ts`, + localStorage if present) — delegates correctly, source info.
22
22
  - **repository** (`*.repository.queries.test.ts` + `*.repository.mutations.test.ts`) — hooks fetch/mutate, cache effects (invalidate/setQueryData).
@@ -13,7 +13,7 @@ Change request: $ARGUMENTS
13
13
 
14
14
  ## Steps
15
15
  1. **Intake** — read `requirement-intake` skill; classify as a CHANGE; produce/append the work item in `.claude/.planning/REQUIREMENTS.md` (Type: change). Interactive + proactive: ask blocking questions, suggest gaps (esp. what existing behavior must be preserved).
16
- 2. **Map (mandatory, read-only)** — dispatch the `mapper` agent over the target module(s). It reports current state + a change-plan (ADD/MODIFY/KEEP, layers, blast radius, regression surface). Do NOT skip this. Write the change-plan into `PHASE_PLAN.md`.
16
+ 2. **Map (mandatory, read-only)** — dispatch the `mapper` agent over the target module(s). It reports current state + a change-plan (ADD/MODIFY/KEEP, layers, blast radius, regression surface). Do NOT skip this. Write the change-plan into `PHASE_PLAN.md` and set its `type: change` (so `/wavefront-execute` runs modify mode and refuses to overwrite the module).
17
17
  3. **Plan** — turn the change-plan into ordered tasks (each tagged module-builder modify / tester / i18n-tokens / reviewer). Map new acceptance criteria → tests; note the regression set (existing tests that must stay green).
18
18
  4. **Execute (modify mode)** — dispatch `module-builder` in MODIFY mode with the mapper change-plan; it edits only the planned files, preserves existing code/public API. Then `tester`: extend tests for new criteria AND confirm the regression set still passes. Then `i18n-tokens` if UI strings/tokens changed. Update `STATE.md` after each task.
19
19
  5. **Verify (mandatory regression)** — `/wavefront-verify` style: reviewer in **change-review (diff-aware)** mode + run new tests AND the full module's existing tests (`yarn test --testPathPatterns=<module>`) + tsc + lint. PASS requires: reviewer PASS, new criteria covered, AND no regression (existing tests green).
@@ -10,19 +10,24 @@ allowed-tools: Read, Write, Edit, Bash, Grep, Glob, Agent
10
10
  Execute the plan for: $ARGUMENTS
11
11
 
12
12
  ## Steps
13
- 1. Read `.claude/.planning/PHASE_PLAN.md` (item $ARGUMENTS) and `STATE.md`. Resume at the first wave/task not marked done. Check the `parallelization` config flag (default off).
13
+ 1. Read `.claude/.planning/PHASE_PLAN.md` (item $ARGUMENTS) and `STATE.md`. Resume at the first wave/task not marked done (and read STATE's `Resume context` if present — the interrupted task + last error). Check the `parallelization` config flag (default off).
14
+ 1b. **Determine build mode from the plan's `type:` field** (PHASE_PLAN § Work item):
15
+ - `type: feature` → **create mode** (build from scratch). If the target module dir already exists, STOP — a create plan must not overwrite working code; tell the user to run `/wavefront-change` instead.
16
+ - `type: change` or `type: fix` → **modify mode** — the plan MUST carry a `mapper` change-plan (ADD/MODIFY/KEEP file list). If it doesn't (e.g. the plan came from `/wavefront-plan`, not `/wavefront-change`/`/wavefront-fix`), STOP and ask the user to re-plan via the change/fix flow. Pass the change-plan to module-builder; it edits ONLY listed files.
14
17
  2. Execute **wave by wave** (a single-module item is just one wave with one item):
15
- - **If `parallelization` is on AND the wave has multiple independent items:** dispatch their `module-builder` subagents **in parallel** multiple Agent calls in ONE message. Each builder gets ONLY its module slice (target files + relevant skills + acceptance criteria). Wait for the whole wave to finish before the next wave.
18
+ - **Before any parallel dispatch collision check:** confirm the wave's items declare NO shared-file overlap (compare each item's target files + the plan's Risks/shared-file notes). If two items in the wave touch the same file (`@/api/endpoints`, global types, etc.), DO NOT parallelize them run those two serially even when `parallelization` is on. Only genuinely disjoint items go parallel.
19
+ - **If `parallelization` is on AND the wave has multiple independent items (collision check passed):** dispatch their `module-builder` subagents **in parallel** — multiple Agent calls in ONE message. Each builder gets ONLY its module slice (target files + relevant skills + acceptance criteria). Wait for the whole wave to finish before the next wave.
16
20
  - **Otherwise (default):** run items sequentially.
17
21
  - Within any single module, tasks are always sequential (layer chain): types→api→gateway→repo→store→selector→hook→view→barrels.
18
22
  3. Per item, after build: dispatch `tester`, then `i18n-tokens` if UI changed. (These can also run per-item within the wave.)
19
- 4. After EACH task/item: mark done in `PHASE_PLAN.md`, update `STATE.md` (current wave, last step, next step). Resume point.
23
+ 4. After EACH task/item: mark done in `PHASE_PLAN.md`, update `STATE.md` (current wave, last step, next step). Resume point. **On a task FAILURE (agent error, tsc/lint/test red that can't be auto-fixed):** stop, and write STATE's `Resume context` — the failed task, the owning agent, and the exact error — so the next run resumes WITH that context instead of blindly re-dispatching.
20
24
  5. After a wave: run `tsc --noEmit` + `yarn lint`; fix failures (route to owning agent) before the next wave.
21
25
  6. Do NOT run the reviewer here — that's `/wavefront-verify`. Stop after the build/test/i18n tasks and report status + next command (`/wavefront-verify "<title>"`).
22
26
 
23
27
  ## Rules
24
- - Persist STATE after each task AND each wave (interruption-safe; resume mid-plan).
28
+ - Persist STATE after each task AND each wave (interruption-safe; resume mid-plan). On failure, persist `Resume context` (failed task + agent + error).
25
29
  - Fresh-context dispatch: each subagent gets only its slice.
26
- - Parallel only across INDEPENDENT items in the same wave; never parallelize items that touch the same shared file.
30
+ - Parallel only across INDEPENDENT items in the same wave; **run the collision check first** — never parallelize items that touch the same shared file, even with the flag on.
31
+ - Build mode follows the plan's `type:`: feature→create (refuse to overwrite an existing module), change/fix→modify (requires a mapper change-plan; edit only listed files).
27
32
  - Within a module, always sequential (dependency chain).
28
33
  - Stop and ask on Ask-first (new dep, shared-file edit, breaking change).
@@ -13,7 +13,7 @@ Bug: $ARGUMENTS
13
13
 
14
14
  ## Steps
15
15
  1. **Intake / repro** — read `requirement-intake` skill; classify as a FIX. Capture the bug as a work item in `.claude/.planning/REQUIREMENTS.md` (Type: fix): expected vs actual behavior, reproduction steps, affected area. Ask for repro details if missing (a bug you can't reproduce, you can't verify fixed).
16
- 2. **Diagnose + locate (read-only)** — dispatch the `reviewer` agent in **diagnose mode**. It traces the data flow through the layers and reports root cause as `file:line + why wrong + fix direction`. Write the diagnosis into `PHASE_PLAN.md`. No edits yet.
16
+ 2. **Diagnose + locate (read-only)** — dispatch the `reviewer` agent in **diagnose mode**. It traces the data flow through the layers and reports root cause as `file:line + why wrong + fix direction`. Write the diagnosis into `PHASE_PLAN.md` and set its `type: fix` (so `/wavefront-execute`, if used, runs modify/patch mode — never create). No edits yet.
17
17
  3. **Regression test RED** — dispatch the `tester` agent (bug regression flow): write a colocated test that reproduces the bug and **fails against current code** (RED). Confirm it's actually red (run it). This captures the bug.
18
18
  4. **Patch** — dispatch `module-builder` in **patch mode** (smallest focused change at the diagnosed location; respects surrounding code; touches only what's needed).
19
19
  5. **GREEN + regression** — re-run the new test → must PASS. Run the module's existing tests + tsc + lint → no regression.
@@ -12,11 +12,15 @@ Verify the work item: $ARGUMENTS
12
12
  ## Steps
13
13
  1. Read the item's `REQUIREMENTS` (acceptance criteria) + `PHASE_PLAN` (what was built).
14
14
  2. Dispatch the `reviewer` subagent (code-review mode) over the changed module/files → expect PASS/FAIL with BLOCKER/WARNING/NIT.
15
- 3. Run the tests for the work item (`yarn test --testPathPatterns=<module>`) and `yarn typecheck` + `yarn lint`. Note: a single-module test run trips the 85% global coverage gate (exit 1) even when tests pass — judge by `Tests: N passed`, not the exit code.
15
+ 3. Run the tests for the work item (`yarn test --testPathPatterns=<module>`) and `yarn typecheck` + `yarn lint`.
16
+ - **Coverage-gate discriminator (do NOT judge by exit code alone):** a single-module run trips the 85% GLOBAL coverage gate → `exit 1` even when every test passed. Distinguish by parsing the Jest summary:
17
+ - Read the `Tests:` line. **Real failure** = it shows `N failed` (N≥1), OR a `FAIL <path>` test-suite line, OR a tsc/lint error. → tests NOT green.
18
+ - **Coverage-only failure** = `Tests: M passed` with `0 failed`, exit 1, and the only error is the coverage threshold (`Jest: "global" coverage threshold ... not met`). → tests ARE green; the gate trip is expected for a single-module run, not a verdict failure.
19
+ - tsc/lint are judged normally (any reported error = fail).
16
20
  4. Check each **acceptance criterion** is actually covered by a passing test. List any uncovered.
17
21
  5. Verdict:
18
- - **PASS** (reviewer PASS + tests green + criteria covered) → update `STATE.md` (status: verified, next = `/wavefront-ship`). Report.
19
- - **FAIL** → write a concise fix plan (what failed, which file, which agent should fix) into `STATE.md` blockers + report. Do not ship.
22
+ - **PASS** (reviewer PASS + tests green per the discriminator + criteria covered) → update `STATE.md` (status: verified, next = `/wavefront-ship`). Report.
23
+ - **FAIL** (real test failures, tsc/lint errors, reviewer FAIL, or uncovered criteria) → write a concise fix plan (what failed, which file, which agent should fix) into `STATE.md` blockers + `Resume context` + report. Do not ship. A coverage-only gate trip with all tests passing is NOT a fail.
20
24
  6. Do NOT edit code here — verify is read-only/dispatch. Fixes go back through `/wavefront-execute` or the owning agent.
21
25
 
22
26
  ## Rules
package/install.sh CHANGED
@@ -8,7 +8,7 @@ set -euo pipefail
8
8
 
9
9
  REPO_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
10
10
  HOME_DIR="${WAVEFRONT_HOME:-$HOME/.wavefront}"
11
- SURFACE=(agents skills commands planning-templates)
11
+ SURFACE=(agents skills commands reference planning-templates)
12
12
 
13
13
  die() { echo "error: $*" >&2; exit 1; }
14
14
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pavp/wavefront",
3
- "version": "1.1.0",
3
+ "version": "1.2.0",
4
4
  "description": "Claude Code frontend-engineering agent framework for scaffold-nextjs-app apps (Next.js/React/MUI/Clean Architecture). Installs agents, skills, and an orchestration loop into a project's .claude/.",
5
5
  "bin": {
6
6
  "wavefront": "bin/wavefront.js"
@@ -10,13 +10,15 @@
10
10
  "release": "semantic-release",
11
11
  "release:dry": "semantic-release --dry-run",
12
12
  "validate:commits": "commitlint --from origin/main --to HEAD --verbose",
13
- "validate:branch": "node scripts/validate-branch-name.js"
13
+ "validate:branch": "node scripts/validate-branch-name.js",
14
+ "validate:links": "node scripts/validate-skill-links.js"
14
15
  },
15
16
  "files": [
16
17
  "bin/",
17
18
  "agents/",
18
19
  "skills/",
19
20
  "commands/",
21
+ "reference/",
20
22
  "planning-templates/",
21
23
  "hooks/",
22
24
  "install.sh",
@@ -14,6 +14,13 @@
14
14
  ## Execution progress
15
15
  <which PHASE_PLAN tasks are done; which is next>
16
16
 
17
+ ## Resume context
18
+ > Set by `/wavefront-execute` (or `/wavefront-verify`) ONLY when a task fails or is interrupted. Empty on a clean run. Lets the next run resume WITH context instead of blindly re-dispatching the same task.
19
+ - Interrupted/failed task: <task # + name from PHASE_PLAN, or empty>
20
+ - Owning agent: <module-builder|tester|i18n-tokens|reviewer>
21
+ - Last error: <the exact tsc/lint/test/agent error>
22
+ - What a resume should do: <re-run as-is | needs a fix first | needs user input>
23
+
17
24
  ## Decisions log
18
25
  - <date> <decision> — <why>
19
26
 
@@ -0,0 +1,14 @@
1
+ # reference/ — the framework's own idiom anchor
2
+
3
+ `module/note-module.md` is a canonical, end-to-end Clean-Arch module that agents read to copy current scaffold idioms.
4
+
5
+ **Why it exists:** apps built from scaffold-nextjs-app routinely **delete the example modules** (`src/modules/todo`, `src/modules/auth`). If `module-builder` relied on reading those live, it would lose its idiom anchor the moment a real project removes them. So the framework ships its own reference instead — no dependency on the scaffold OR on the target app keeping example modules.
6
+
7
+ **What it is:** one minimal CRUD entity (`note`) showing every layer once, in dependency order, with the exact idioms (`createXGateway` factory, key factory, `queryOptions`/`mutationOptions`, `createStoreWithMiddleware`, business/controller hook split, `@/ui` + tokens, the four states incl. error-boundary, colocated tests). The per-pattern detail lives in `skills/*`; this shows how the layers fit together.
8
+
9
+ **Compile-inert:** stored as `.md` so a consuming app never type-checks/lints it (same trick as `skills/component-composition/examples/compound-component.md`).
10
+
11
+ **Provenance / drift:** distilled from the scaffold's real `todo` module at `source_version: 8edaa0b` — the same pin the skills carry. Drift is tracked the same way: `sync-audit` (via the `reviewer` audit mode) diffs this pin against the live scaffold. When the scaffold's idioms change, re-distill this file and bump the pin.
12
+
13
+ - `source:` scaffold-nextjs-app `src/modules/todo` (types, api, repository, gateways, stores, selectors, views/hooks, components)
14
+ - `source_version:` 8edaa0b
@@ -0,0 +1,608 @@
1
+ # Reference module — `note` (canonical Clean-Arch chain)
2
+
3
+ > **This is the framework's own idiom anchor.** Agents read THIS file to copy current scaffold idioms — they do NOT depend on the target app keeping `src/modules/todo`/`auth` (apps routinely delete the example modules).
4
+ >
5
+ > It is a single, minimal CRUD module for one entity (`note`: `id`, `title`, `body`, `completed`, timestamps), distilled from the scaffold's real `todo` module — same idioms, none of `todo`'s app-specific noise (test-error harness, bulk ops, prefetch/cancel surface, 7 selectors). One example per layer, in dependency order. The detailed per-pattern rules live in the individual `skills/*` — this shows how the layers fit together end to end.
6
+ >
7
+ > File is `.md` (compile-inert) so it never gets type-checked/linted in a consuming app. Provenance: see `reference/README.md`.
8
+
9
+ Module tree this produces:
10
+ ```
11
+ src/modules/note/
12
+ note.types.ts
13
+ api/note-api.ts
14
+ repositories/note/
15
+ gateways/
16
+ note.gateway.types.ts
17
+ http-gateway/http-gateway.ts
18
+ index.ts # createNoteGateway(source) factory
19
+ note.repository.keys.ts
20
+ note.query-options.ts
21
+ note.repository.queries.ts
22
+ note.repository.mutations.ts
23
+ note.repository.types.ts
24
+ index.ts # combined noteRepository object
25
+ stores/
26
+ note.store.ts
27
+ note.store.types.ts
28
+ note.store.actions.ts
29
+ selectors/
30
+ use-filtered-notes-selector/use-filtered-notes-selector.hook.ts
31
+ views/note-management/
32
+ note-management.view.tsx
33
+ hooks/
34
+ use-note-management-business/use-note-management-business.hook.ts
35
+ use-note-management-controller/use-note-management-controller.hook.ts
36
+ index.ts
37
+ components/
38
+ note-form/note-form.component.tsx
39
+ note-item/note-item.component.tsx
40
+ note-list/note-list.component.tsx
41
+ error-boundary/error-boundary.component.tsx
42
+ index.ts
43
+ ```
44
+
45
+ ---
46
+
47
+ ## 1. Types + Zod schemas (`note.types.ts`) — Zod is the source of truth
48
+
49
+ ```tsx
50
+ import { z } from 'zod';
51
+
52
+ export const NoteSchema = z.object({
53
+ id: z.number(),
54
+ title: z.string().min(1, 'common.validation.requiredField'), // i18n key, not a literal
55
+ body: z.string().optional(),
56
+ completed: z.boolean().default(false),
57
+ createdAt: z.string(),
58
+ updatedAt: z.string(),
59
+ });
60
+
61
+ export const CreateNoteSchema = NoteSchema.omit({ id: true, createdAt: true, updatedAt: true, completed: true }).extend({
62
+ completed: z.boolean().optional(),
63
+ });
64
+
65
+ export const UpdateNoteSchema = CreateNoteSchema.partial();
66
+
67
+ export const NoteFiltersSchema = z
68
+ .object({
69
+ completed: z.union([z.boolean(), z.string().transform((v) => v === 'true')]).optional(),
70
+ search: z.string().optional(),
71
+ page: z.union([z.number().int().positive(), z.string().transform((v) => parseInt(v, 10))]).optional(),
72
+ limit: z.union([z.number().int().positive(), z.string().transform((v) => parseInt(v, 10))]).optional(),
73
+ })
74
+ .loose();
75
+
76
+ export const NoteArraySchema = z.array(NoteSchema);
77
+
78
+ // Types are INFERRED from schemas — never hand-written interfaces for the entity.
79
+ export type Note = z.infer<typeof NoteSchema>;
80
+ export type CreateNoteRequest = z.infer<typeof CreateNoteSchema>;
81
+ export type UpdateNoteRequest = z.infer<typeof UpdateNoteSchema>;
82
+ export type NoteFilters = z.infer<typeof NoteFiltersSchema>;
83
+ ```
84
+
85
+ ## 2. API service (`api/note-api.ts`) — contract + Zod-validated httpClient + singleton
86
+
87
+ ```tsx
88
+ import type { ApiOptions } from '@/api/api.types';
89
+ import { endpoints } from '@/api/endpoints'; // add NOTE.* to @/api/endpoints (Ask-first — shared file)
90
+ import { httpClient } from '@/api/http-client';
91
+ import type { CreateNoteRequest, Note, NoteFilters, UpdateNoteRequest } from '@/modules/note/note.types';
92
+ import { CreateNoteSchema, NoteArraySchema, NoteFiltersSchema, NoteSchema, UpdateNoteSchema } from '@/modules/note/note.types';
93
+
94
+ export interface NoteApiContract {
95
+ getAll(filters?: NoteFilters, options?: ApiOptions): Promise<Note[]>;
96
+ getById(id: string | number, options?: ApiOptions): Promise<Note>;
97
+ create(note: CreateNoteRequest, options?: ApiOptions): Promise<Note>;
98
+ update(id: string | number, note: UpdateNoteRequest, options?: ApiOptions): Promise<Note>;
99
+ delete(id: string | number, options?: ApiOptions): Promise<void>;
100
+ }
101
+
102
+ const createNoteApiService = (): NoteApiContract => ({
103
+ async getAll(filters = {}, options) {
104
+ const response = await httpClient.get<Note[]>(endpoints.NOTE.BASE, {
105
+ params: filters,
106
+ requestSchema: NoteFiltersSchema, // request validated
107
+ responseSchema: NoteArraySchema, // response validated
108
+ signal: options?.signal,
109
+ });
110
+
111
+ return response.data;
112
+ },
113
+ async getById(id, options) {
114
+ const response = await httpClient.get<Note>(endpoints.NOTE.BY_ID(id), {
115
+ responseSchema: NoteSchema,
116
+ signal: options?.signal,
117
+ });
118
+
119
+ return response.data;
120
+ },
121
+ async create(note, options) {
122
+ const response = await httpClient.post<Note>(endpoints.NOTE.BASE, note, {
123
+ requestSchema: CreateNoteSchema,
124
+ responseSchema: NoteSchema,
125
+ signal: options?.signal,
126
+ });
127
+
128
+ return response.data;
129
+ },
130
+ async update(id, note, options) {
131
+ const response = await httpClient.put<Note>(endpoints.NOTE.BY_ID(id), note, {
132
+ requestSchema: UpdateNoteSchema,
133
+ responseSchema: NoteSchema,
134
+ signal: options?.signal,
135
+ });
136
+
137
+ return response.data;
138
+ },
139
+ async delete(id, options) {
140
+ await httpClient.delete(endpoints.NOTE.BY_ID(id), { signal: options?.signal });
141
+ },
142
+ });
143
+
144
+ export const noteApi = createNoteApiService(); // singleton for the whole app
145
+ ```
146
+
147
+ ## 3. Gateway (`repositories/note/gateways/`) — interface + http impl + factory
148
+
149
+ `note.gateway.types.ts`:
150
+ ```tsx
151
+ import type { CreateNoteRequest, Note, NoteFilters, UpdateNoteRequest } from '@/modules/note/note.types';
152
+ import type { BaseGateway, GatewayOptions } from '@/types/gateway.types';
153
+
154
+ export interface NoteGateway extends BaseGateway {
155
+ findAll(filters?: NoteFilters, options?: GatewayOptions): Promise<Note[]>;
156
+ findById(id: string | number, options?: GatewayOptions): Promise<Note>;
157
+ create(note: CreateNoteRequest, options?: GatewayOptions): Promise<Note>;
158
+ update(id: string | number, note: UpdateNoteRequest, options?: GatewayOptions): Promise<Note>;
159
+ delete(id: string | number, options?: GatewayOptions): Promise<void>;
160
+ }
161
+ ```
162
+
163
+ `http-gateway/http-gateway.ts`:
164
+ ```tsx
165
+ import { noteApi } from '@/modules/note/api/note-api';
166
+ import type { NoteGateway } from '@/modules/note/repositories/note/gateways/note.gateway.types';
167
+
168
+ export const createHttpNoteGateway = (): NoteGateway => ({
169
+ async findAll(filters, options) {
170
+ return noteApi.getAll(filters, options);
171
+ },
172
+ async findById(id, options) {
173
+ return noteApi.getById(id, options);
174
+ },
175
+ async create(note, options) {
176
+ return noteApi.create(note, options);
177
+ },
178
+ async update(id, note, options) {
179
+ return noteApi.update(id, note, options);
180
+ },
181
+ async delete(id, options) {
182
+ await noteApi.delete(id, options);
183
+ },
184
+ getSourceInfo() {
185
+ return {
186
+ type: 'http',
187
+ name: 'HTTP API Gateway',
188
+ capabilities: { offline: false, realtime: true, persistence: true },
189
+ };
190
+ },
191
+ });
192
+ ```
193
+
194
+ `gateways/index.ts` — the factory (`DataSource` is `'http' | 'localStorage'` only, no `'mock'`):
195
+ ```tsx
196
+ import { DataSource } from '@/types/gateway.types';
197
+
198
+ import { createHttpNoteGateway } from './http-gateway/http-gateway';
199
+ import type { NoteGateway } from './note.gateway.types';
200
+
201
+ export const createNoteGateway = (source: DataSource = 'http'): NoteGateway => {
202
+ switch (source) {
203
+ case 'http':
204
+ return createHttpNoteGateway();
205
+ // add createLocalStorageNoteGateway() only if a localStorage source is requested
206
+ default:
207
+ return createHttpNoteGateway();
208
+ }
209
+ };
210
+
211
+ export { createHttpNoteGateway } from './http-gateway/http-gateway';
212
+ export type { NoteGateway } from './note.gateway.types';
213
+ ```
214
+
215
+ ## 4. Repository (`repositories/note/`) — keys → options → queries → mutations → types → combined
216
+
217
+ `note.repository.keys.ts` — query-key factory, namespaced by `dataSource`:
218
+ ```tsx
219
+ import type { NoteFilters } from '@/modules/note/note.types';
220
+ import type { DataSource } from '@/types/gateway.types';
221
+
222
+ export const noteQueryKeys = {
223
+ all: ['notes'] as const,
224
+ lists: (dataSource: DataSource = 'http') => [...noteQueryKeys.all, 'list', dataSource] as const,
225
+ list: (filters: NoteFilters = {}, dataSource: DataSource = 'http') => [...noteQueryKeys.lists(dataSource), filters] as const,
226
+ details: (dataSource: DataSource = 'http') => [...noteQueryKeys.all, 'detail', dataSource] as const,
227
+ detail: (id: string | number, dataSource: DataSource = 'http') => [...noteQueryKeys.details(dataSource), id] as const,
228
+ } as const;
229
+ ```
230
+
231
+ `note.query-options.ts` — `queryOptions`/`mutationOptions` factories (reused by hooks + prefetch):
232
+ ```tsx
233
+ import { mutationOptions, queryOptions } from '@tanstack/react-query';
234
+
235
+ import type { DataSource } from '@/types/gateway.types';
236
+
237
+ import { createNoteGateway } from './gateways';
238
+ import { noteQueryKeys } from './note.repository.keys';
239
+
240
+ const getNotesQueryOptions = (filters = {}, dataSource: DataSource = 'http') =>
241
+ queryOptions({
242
+ queryKey: noteQueryKeys.list(filters, dataSource),
243
+ queryFn: ({ signal }) => createNoteGateway(dataSource).findAll(filters, { signal }),
244
+ staleTime: 5 * 60 * 1000,
245
+ gcTime: 10 * 60 * 1000,
246
+ });
247
+
248
+ const getNoteQueryOptions = (id: string | number, dataSource: DataSource = 'http') =>
249
+ queryOptions({
250
+ queryKey: noteQueryKeys.detail(id, dataSource),
251
+ queryFn: ({ signal }) => createNoteGateway(dataSource).findById(id, { signal }),
252
+ staleTime: 5 * 60 * 1000,
253
+ gcTime: 10 * 60 * 1000,
254
+ enabled: !!id,
255
+ });
256
+
257
+ const getCreateNoteMutationOptions = (dataSource: DataSource = 'http') =>
258
+ mutationOptions({ mutationFn: (note: any) => createNoteGateway(dataSource).create(note), retry: 1 });
259
+
260
+ const getUpdateNoteMutationOptions = (dataSource: DataSource = 'http') =>
261
+ mutationOptions({
262
+ mutationFn: ({ id, data }: { id: string | number; data: any }) => createNoteGateway(dataSource).update(id, data),
263
+ retry: 1,
264
+ });
265
+
266
+ const getDeleteNoteMutationOptions = (dataSource: DataSource = 'http') =>
267
+ mutationOptions({ mutationFn: (id: string | number) => createNoteGateway(dataSource).delete(id), retry: 1 });
268
+
269
+ export const noteQueryOptions = { notes: getNotesQueryOptions, note: getNoteQueryOptions } as const;
270
+ export const noteMutationOptions = {
271
+ createNote: getCreateNoteMutationOptions,
272
+ updateNote: getUpdateNoteMutationOptions,
273
+ deleteNote: getDeleteNoteMutationOptions,
274
+ } as const;
275
+ ```
276
+
277
+ `note.repository.queries.ts` — query hooks compose the options:
278
+ ```tsx
279
+ import { useQuery } from '@tanstack/react-query';
280
+
281
+ import { noteQueryOptions } from './note.query-options';
282
+ import type { NoteQueriesRepository } from './note.repository.types';
283
+
284
+ export const noteQueriesRepository: NoteQueriesRepository = {
285
+ useNotes: (filters = {}, dataSource = 'http', options) =>
286
+ useQuery({ ...noteQueryOptions.notes(filters, dataSource), ...options }),
287
+ useNote: (id, dataSource = 'http', options) =>
288
+ useQuery({ ...noteQueryOptions.note(id, dataSource), ...options }),
289
+ };
290
+ ```
291
+
292
+ `note.repository.mutations.ts` — mutation hooks own cache writes in `onSuccess`; always forward the user's `options?.onSuccess`:
293
+ ```tsx
294
+ import { useMutation, useQueryClient } from '@tanstack/react-query';
295
+
296
+ import type { Note } from '@/modules/note/note.types';
297
+
298
+ import { noteMutationOptions } from './note.query-options';
299
+ import { noteQueryKeys } from './note.repository.keys';
300
+ import type { NoteMutationsRepository } from './note.repository.types';
301
+
302
+ export const noteMutationsRepository: NoteMutationsRepository = {
303
+ useCreateNote: (dataSource = 'http', options) => {
304
+ const queryClient = useQueryClient();
305
+ const baseOptions = noteMutationOptions.createNote(dataSource);
306
+
307
+ return useMutation({
308
+ ...baseOptions,
309
+ // eslint-disable-next-line max-params ← REQUIRED: onSuccess has 4 params, lint cap is 3
310
+ onSuccess: (newNote, vars, onMutateResult, ctx) => {
311
+ queryClient.invalidateQueries({ queryKey: noteQueryKeys.lists(dataSource) });
312
+ queryClient.setQueryData<Note[]>(noteQueryKeys.lists(dataSource), (old) => (old ? [...old, newNote] : [newNote]));
313
+ options?.onSuccess?.(newNote, vars, onMutateResult, ctx);
314
+ },
315
+ ...options,
316
+ });
317
+ },
318
+ useUpdateNote: (dataSource = 'http', options) => {
319
+ const queryClient = useQueryClient();
320
+
321
+ return useMutation({
322
+ ...noteMutationOptions.updateNote(dataSource),
323
+ // eslint-disable-next-line max-params
324
+ onSuccess: (updated: Note, vars, onMutateResult, ctx) => {
325
+ queryClient.setQueryData<Note[]>(noteQueryKeys.lists(), (old) => old?.map((n) => (n.id === updated.id ? updated : n)));
326
+ queryClient.invalidateQueries({ queryKey: noteQueryKeys.lists(dataSource) });
327
+ options?.onSuccess?.(updated, vars, onMutateResult, ctx);
328
+ },
329
+ ...options,
330
+ });
331
+ },
332
+ useDeleteNote: (dataSource = 'http', options) => {
333
+ const queryClient = useQueryClient();
334
+
335
+ return useMutation({
336
+ ...noteMutationOptions.deleteNote(dataSource),
337
+ // eslint-disable-next-line max-params
338
+ onSuccess: (result, deletedId, onMutateResult, ctx) => {
339
+ queryClient.setQueryData<Note[]>(noteQueryKeys.lists(), (old) => old?.filter((n) => n.id !== deletedId));
340
+ queryClient.invalidateQueries({ queryKey: noteQueryKeys.lists(dataSource) });
341
+ options?.onSuccess?.(result, deletedId, onMutateResult, ctx);
342
+ },
343
+ ...options,
344
+ });
345
+ },
346
+ };
347
+ ```
348
+
349
+ `note.repository.types.ts` — interfaces (queries extend `BaseRepository`; options from `@/core/lib/react-query`):
350
+ ```tsx
351
+ import { type UseMutationResult, type UseQueryResult } from '@tanstack/react-query';
352
+
353
+ import type { BaseRepository } from '@/core/lib/react-query';
354
+ import { MutationOptions, QueryOptions } from '@/core/lib/react-query';
355
+ import type { CreateNoteRequest, Note, NoteFilters, UpdateNoteRequest } from '@/modules/note/note.types';
356
+ import type { DataSource } from '@/types/gateway.types';
357
+
358
+ export interface NoteQueriesRepository extends BaseRepository {
359
+ useNotes: (filters?: NoteFilters, dataSource?: DataSource, options?: QueryOptions) => UseQueryResult<Note[], Error>;
360
+ useNote: (id: string | number, dataSource?: DataSource, options?: QueryOptions) => UseQueryResult<Note, Error>;
361
+ }
362
+ export interface NoteMutationsRepository {
363
+ useCreateNote: (dataSource?: DataSource, options?: MutationOptions) => UseMutationResult<Note, Error, CreateNoteRequest>;
364
+ useUpdateNote: (dataSource?: DataSource, options?: MutationOptions) => UseMutationResult<Note, Error, { id: string | number; data: UpdateNoteRequest }>;
365
+ useDeleteNote: (dataSource?: DataSource, options?: MutationOptions) => UseMutationResult<void, Error, string | number>;
366
+ }
367
+ export interface NoteRepository {
368
+ queries: NoteQueriesRepository;
369
+ mutations: NoteMutationsRepository;
370
+ queryKeys: typeof import('./note.repository.keys').noteQueryKeys;
371
+ }
372
+ ```
373
+
374
+ `repositories/note/index.ts` — the combined repository object the hooks import:
375
+ ```tsx
376
+ import { noteMutationsRepository } from './note.repository.mutations';
377
+ import { noteQueriesRepository } from './note.repository.queries';
378
+ import { noteQueryKeys } from './note.repository.keys';
379
+ import type { NoteRepository } from './note.repository.types';
380
+
381
+ export const noteRepository: NoteRepository = {
382
+ queries: noteQueriesRepository,
383
+ mutations: noteMutationsRepository,
384
+ queryKeys: noteQueryKeys,
385
+ };
386
+ ```
387
+
388
+ ## 5. Store (`stores/`) — Zustand via `createStoreWithMiddleware` (never raw `create`)
389
+
390
+ `note.store.types.ts`:
391
+ ```tsx
392
+ import type { NoteFilters } from '@/modules/note/note.types';
393
+
394
+ export interface NoteState {
395
+ selectedNoteId: number | null;
396
+ filters: NoteFilters;
397
+ setSelectedNoteId: (id: number | null) => void;
398
+ setFilters: (filters: NoteFilters) => void;
399
+ }
400
+ ```
401
+
402
+ `note.store.ts`:
403
+ ```tsx
404
+ import { createStoreWithMiddleware } from '@/core/lib/zustand';
405
+
406
+ import type { NoteState } from './note.store.types';
407
+
408
+ export const useNoteStore = createStoreWithMiddleware<NoteState>(
409
+ (set) => ({
410
+ selectedNoteId: null,
411
+ filters: {},
412
+ setSelectedNoteId: (id) => set((state) => { state.selectedNoteId = id; }), // immer middleware → mutate draft
413
+ setFilters: (filters) => set((state) => { state.filters = filters; }),
414
+ }),
415
+ { name: 'note-store' },
416
+ );
417
+ ```
418
+
419
+ `note.store.actions.ts` — selector hook for actions (stable references):
420
+ ```tsx
421
+ import { useNoteStore } from './note.store';
422
+
423
+ export const useNoteActions = () =>
424
+ useNoteStore((s) => ({ setSelectedNoteId: s.setSelectedNoteId, setFilters: s.setFilters }));
425
+ ```
426
+
427
+ ## 6. Selector (`selectors/`) — derived state via `useMemo` over a query
428
+
429
+ `use-filtered-notes-selector/use-filtered-notes-selector.hook.ts`:
430
+ ```tsx
431
+ import { useMemo } from 'react';
432
+
433
+ import { noteRepository } from '@/modules/note/repositories/note';
434
+ import { useNoteStore } from '@/modules/note/stores/note.store';
435
+ import type { DataSource } from '@/types/gateway.types';
436
+
437
+ export const useFilteredNotesSelector = (dataSource: DataSource = 'http') => {
438
+ const filters = useNoteStore((s) => s.filters);
439
+ const notesQuery = noteRepository.queries.useNotes(filters, dataSource);
440
+
441
+ const data = useMemo(() => {
442
+ const notes = notesQuery.data ?? [];
443
+ if (filters.completed === undefined) return notes;
444
+
445
+ return notes.filter((n) => n.completed === filters.completed);
446
+ }, [notesQuery.data, filters.completed]);
447
+
448
+ return { ...notesQuery, data };
449
+ };
450
+ ```
451
+
452
+ ## 7. Hooks (`views/note-management/hooks/`) — business vs controller split (mandatory)
453
+
454
+ **Business hook** = data + business rules + the four states. Exposes `error` + `refetch` for the error-boundary:
455
+ ```tsx
456
+ 'use client';
457
+
458
+ import { useCallback } from 'react';
459
+
460
+ import { noteRepository } from '@/modules/note/repositories/note';
461
+ import { useNoteActions } from '@/modules/note/stores/note.store.actions';
462
+ import type { CreateNoteRequest, UpdateNoteRequest } from '@/modules/note/note.types';
463
+ import type { DataSource } from '@/types/gateway.types';
464
+
465
+ export const useNoteManagementBusiness = (dataSource: DataSource = 'http') => {
466
+ const { setSelectedNoteId } = useNoteActions();
467
+
468
+ const notesQuery = noteRepository.queries.useNotes({}, dataSource);
469
+ const createMutation = noteRepository.mutations.useCreateNote(dataSource);
470
+ const deleteMutation = noteRepository.mutations.useDeleteNote(dataSource);
471
+
472
+ const createNote = useCallback(
473
+ (data: CreateNoteRequest) => {
474
+ if (!data.title?.trim()) throw new Error('common.validation.requiredField');
475
+
476
+ return createMutation.mutate({ ...data, title: data.title.trim() });
477
+ },
478
+ [createMutation],
479
+ );
480
+ const deleteNote = useCallback((id: number) => deleteMutation.mutate(id), [deleteMutation]);
481
+
482
+ return {
483
+ notes: notesQuery.data ?? [],
484
+ isLoading: notesQuery.isLoading,
485
+ isEmpty: !notesQuery.isLoading && (notesQuery.data?.length ?? 0) === 0,
486
+ error: notesQuery.error ?? createMutation.error ?? deleteMutation.error, // four states: error
487
+ isCreating: createMutation.isPending,
488
+ isDeleting: deleteMutation.isPending,
489
+ createNote,
490
+ deleteNote,
491
+ setSelectedNoteId,
492
+ refetch: notesQuery.refetch, // wired to the error-boundary's onRetry
493
+ };
494
+ };
495
+ ```
496
+
497
+ **Controller hook** = UI orchestration (event handlers that adapt UI events → business actions). No data fetching:
498
+ ```tsx
499
+ 'use client';
500
+
501
+ import { useCallback } from 'react';
502
+
503
+ import type { CreateNoteRequest } from '@/modules/note/note.types';
504
+
505
+ export const useNoteManagementController = () => {
506
+ const handleCreateSubmit = useCallback(
507
+ (createNote: (data: CreateNoteRequest) => void) => (data: CreateNoteRequest) => createNote(data),
508
+ [],
509
+ );
510
+ const handleDeleteClick = useCallback((deleteNote: (id: number) => void) => (id: number) => deleteNote(id), []);
511
+
512
+ return { handleCreateSubmit, handleDeleteClick };
513
+ };
514
+ ```
515
+
516
+ ## 8. View (`views/note-management/note-management.view.tsx`) — orchestrates; returns the error state; no inline markup blocks
517
+
518
+ ```tsx
519
+ 'use client';
520
+
521
+ import { ErrorBoundary, NoteForm, NoteList } from '@/modules/note/components';
522
+ import tokens from '@/styles/tokens';
523
+ import { Box, Typography } from '@/ui';
524
+
525
+ import { useNoteManagementBusiness, useNoteManagementController } from './hooks';
526
+
527
+ export const NoteManagementView = () => {
528
+ const { notes, isLoading, isCreating, isDeleting, error, createNote, deleteNote, refetch } = useNoteManagementBusiness('http');
529
+ const { handleCreateSubmit, handleDeleteClick } = useNoteManagementController();
530
+
531
+ // four states: ERROR — recoverable error+retry, never a blank screen (see error-boundary skill)
532
+ if (error) return <ErrorBoundary error={error} onRetry={refetch} />;
533
+
534
+ return (
535
+ <Box sx={{ p: tokens.spacing.scale6 }}>
536
+ <Typography gutterBottom variant="h4">
537
+ Notes
538
+ </Typography>
539
+ <NoteForm isCreating={isCreating} onSubmit={handleCreateSubmit(createNote)} />
540
+ <NoteList isDeleting={isDeleting} isLoading={isLoading} notes={notes} onDelete={handleDeleteClick(deleteNote)} />
541
+ </Box>
542
+ );
543
+ };
544
+ ```
545
+
546
+ ## 9. Components (`components/`) — composed, `@/ui` + tokens, mapped row is its own component
547
+
548
+ `note-list.component.tsx` — anything inside `.map()` is its own component (`NoteItem`), never inline JSX:
549
+ ```tsx
550
+ import tokens from '@/styles/tokens';
551
+ import { Box, Typography } from '@/ui';
552
+
553
+ import { NoteItem } from '../note-item/note-item.component';
554
+ import type { Note } from '@/modules/note/note.types';
555
+
556
+ interface NoteListProps {
557
+ notes: Note[];
558
+ isLoading: boolean;
559
+ isDeleting: boolean;
560
+ onDelete: (id: number) => void;
561
+ }
562
+
563
+ export const NoteList = ({ notes, isLoading, isDeleting, onDelete }: NoteListProps) => {
564
+ if (isLoading) return <Typography>Loading…</Typography>; // four states: loading
565
+ if (notes.length === 0) return <Typography>No notes yet</Typography>; // four states: empty
566
+
567
+ return (
568
+ <Box sx={{ display: 'flex', flexDirection: 'column', gap: tokens.spacing.scale4 }}>
569
+ {notes.map((note) => (
570
+ <NoteItem key={note.id} note={note} isDeleting={isDeleting} onDelete={onDelete} />
571
+ ))}
572
+ </Box>
573
+ );
574
+ };
575
+ ```
576
+
577
+ `error-boundary/error-boundary.component.tsx` — recoverable error+retry (see the `error-boundary` skill for the full version + test). `NoteForm` uses RHF + `zodResolver(CreateNoteSchema)` (see `forms-rhf-zod`), `NoteItem` is `memo`'d with role-based interactions.
578
+
579
+ `components/index.ts` barrels every component; `views/index.ts` barrels the view; each layer dir has its `index.ts`.
580
+
581
+ ## 10. Tests (colocated, one per layer) — `@test/utils` + faker factories
582
+
583
+ ```tsx
584
+ // note-item.component.test.tsx
585
+ import { fireEvent, renderWithProviders, screen } from '@test/utils';
586
+ import { createMockNote } from '@test/entities/note.mock'; // faker factory; create if missing
587
+
588
+ import { NoteItem } from './note-item.component';
589
+
590
+ it('calls onDelete when delete is clicked', () => {
591
+ const onDelete = jest.fn();
592
+ renderWithProviders(<NoteItem note={createMockNote()} isDeleting={false} onDelete={onDelete} />);
593
+ fireEvent.click(screen.getByRole('button', { name: /delete/i }));
594
+ expect(onDelete).toHaveBeenCalled();
595
+ });
596
+ ```
597
+
598
+ Every logic layer gets a colocated `*.test.ts(x)` (api/gateway/repository queries+mutations/store/selector/business+controller hooks/each component/view). Import test helpers ONLY from `@test/utils`; query by role/label first.
599
+
600
+ ---
601
+
602
+ ## Layer dependency direction (never violate)
603
+
604
+ ```
605
+ View → controller hook + business hook → repository (queries/mutations) → gateway → api → httpClient
606
+ ↘ store / selectors (local + derived state)
607
+ ```
608
+ A View/component never calls the api or gateway directly — always through the repository. Types flow from `*.types.ts` (Zod-inferred). UI is `@/ui` + `@/styles/tokens` only. Strings are i18n keys, not literals.
@@ -9,8 +9,8 @@ source_version: 8edaa0b
9
9
 
10
10
  Tests colocate next to source as `*.test.ts` / `*.test.tsx`. Everything is imported from `@test/utils` (lint forbids importing RTL / `react-dom` directly in feature tests).
11
11
 
12
- ## Coverage is per-layer (mirror todo)
13
- The scaffold tests EVERY layer — `src/modules/todo` ships a colocated test for api, gateway, repository (queries + mutations), store, each selector, business + controller hooks, each helper, each component, and the view. Match that: one colocated test per piece with logic, not just the happy-path hook.
12
+ ## Coverage is per-layer
13
+ Test EVERY layer — a colocated test for api, gateway, repository (queries + mutations), store, each selector, business + controller hooks, each helper, each component, and the view (the bundled `.claude/reference/module/note-module.md` shows this per-layer test layout; don't rely on the target app keeping `src/modules/todo`). Match that: one colocated test per piece with logic, not just the happy-path hook.
14
14
  - **Unit:** pure pieces alone — helpers, store, a presentational component with mock props.
15
15
  - **Integration:** pieces wired through providers — `renderHookWithProviders` for a business hook (hook→repository→gateway→api), a view with real hooks.
16
16
  Cover both. A module with one test is undertested.
package/uninstall.sh CHANGED
@@ -5,7 +5,7 @@
5
5
  set -euo pipefail
6
6
 
7
7
  HOME_DIR="${WAVEFRONT_HOME:-$HOME/.wavefront}"
8
- SURFACE=(agents skills commands planning-templates)
8
+ SURFACE=(agents skills commands reference planning-templates)
9
9
 
10
10
  die() { echo "error: $*" >&2; exit 1; }
11
11