@pavp/wavefront 1.1.1 → 1.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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).
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.1",
3
+ "version": "1.2.1",
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",
@@ -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,615 @@
1
+ ---
2
+ name: reference-note-module
3
+ description: Canonical end-to-end Clean-Arch module agents read to copy current scaffold idioms.
4
+ source: scaffold-nextjs-app/src/modules/todo (types, api, repository, gateways, stores, selectors, views/hooks, components)
5
+ source_version: 8edaa0b
6
+ ---
7
+
8
+ # Reference module — `note` (canonical Clean-Arch chain)
9
+
10
+ > **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).
11
+ >
12
+ > 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.
13
+ >
14
+ > File is `.md` (compile-inert) so it never gets type-checked/linted in a consuming app. Provenance: see `reference/README.md`.
15
+
16
+ Module tree this produces:
17
+ ```
18
+ src/modules/note/
19
+ note.types.ts
20
+ api/note-api.ts
21
+ repositories/note/
22
+ gateways/
23
+ note.gateway.types.ts
24
+ http-gateway/http-gateway.ts
25
+ index.ts # createNoteGateway(source) factory
26
+ note.repository.keys.ts
27
+ note.query-options.ts
28
+ note.repository.queries.ts
29
+ note.repository.mutations.ts
30
+ note.repository.types.ts
31
+ index.ts # combined noteRepository object
32
+ stores/
33
+ note.store.ts
34
+ note.store.types.ts
35
+ note.store.actions.ts
36
+ selectors/
37
+ use-filtered-notes-selector/use-filtered-notes-selector.hook.ts
38
+ views/note-management/
39
+ note-management.view.tsx
40
+ hooks/
41
+ use-note-management-business/use-note-management-business.hook.ts
42
+ use-note-management-controller/use-note-management-controller.hook.ts
43
+ index.ts
44
+ components/
45
+ note-form/note-form.component.tsx
46
+ note-item/note-item.component.tsx
47
+ note-list/note-list.component.tsx
48
+ error-boundary/error-boundary.component.tsx
49
+ index.ts
50
+ ```
51
+
52
+ ---
53
+
54
+ ## 1. Types + Zod schemas (`note.types.ts`) — Zod is the source of truth
55
+
56
+ ```tsx
57
+ import { z } from 'zod';
58
+
59
+ export const NoteSchema = z.object({
60
+ id: z.number(),
61
+ title: z.string().min(1, 'common.validation.requiredField'), // i18n key, not a literal
62
+ body: z.string().optional(),
63
+ completed: z.boolean().default(false),
64
+ createdAt: z.string(),
65
+ updatedAt: z.string(),
66
+ });
67
+
68
+ export const CreateNoteSchema = NoteSchema.omit({ id: true, createdAt: true, updatedAt: true, completed: true }).extend({
69
+ completed: z.boolean().optional(),
70
+ });
71
+
72
+ export const UpdateNoteSchema = CreateNoteSchema.partial();
73
+
74
+ export const NoteFiltersSchema = z
75
+ .object({
76
+ completed: z.union([z.boolean(), z.string().transform((v) => v === 'true')]).optional(),
77
+ search: z.string().optional(),
78
+ page: z.union([z.number().int().positive(), z.string().transform((v) => parseInt(v, 10))]).optional(),
79
+ limit: z.union([z.number().int().positive(), z.string().transform((v) => parseInt(v, 10))]).optional(),
80
+ })
81
+ .loose();
82
+
83
+ export const NoteArraySchema = z.array(NoteSchema);
84
+
85
+ // Types are INFERRED from schemas — never hand-written interfaces for the entity.
86
+ export type Note = z.infer<typeof NoteSchema>;
87
+ export type CreateNoteRequest = z.infer<typeof CreateNoteSchema>;
88
+ export type UpdateNoteRequest = z.infer<typeof UpdateNoteSchema>;
89
+ export type NoteFilters = z.infer<typeof NoteFiltersSchema>;
90
+ ```
91
+
92
+ ## 2. API service (`api/note-api.ts`) — contract + Zod-validated httpClient + singleton
93
+
94
+ ```tsx
95
+ import type { ApiOptions } from '@/api/api.types';
96
+ import { endpoints } from '@/api/endpoints'; // add NOTE.* to @/api/endpoints (Ask-first — shared file)
97
+ import { httpClient } from '@/api/http-client';
98
+ import type { CreateNoteRequest, Note, NoteFilters, UpdateNoteRequest } from '@/modules/note/note.types';
99
+ import { CreateNoteSchema, NoteArraySchema, NoteFiltersSchema, NoteSchema, UpdateNoteSchema } from '@/modules/note/note.types';
100
+
101
+ export interface NoteApiContract {
102
+ getAll(filters?: NoteFilters, options?: ApiOptions): Promise<Note[]>;
103
+ getById(id: string | number, options?: ApiOptions): Promise<Note>;
104
+ create(note: CreateNoteRequest, options?: ApiOptions): Promise<Note>;
105
+ update(id: string | number, note: UpdateNoteRequest, options?: ApiOptions): Promise<Note>;
106
+ delete(id: string | number, options?: ApiOptions): Promise<void>;
107
+ }
108
+
109
+ const createNoteApiService = (): NoteApiContract => ({
110
+ async getAll(filters = {}, options) {
111
+ const response = await httpClient.get<Note[]>(endpoints.NOTE.BASE, {
112
+ params: filters,
113
+ requestSchema: NoteFiltersSchema, // request validated
114
+ responseSchema: NoteArraySchema, // response validated
115
+ signal: options?.signal,
116
+ });
117
+
118
+ return response.data;
119
+ },
120
+ async getById(id, options) {
121
+ const response = await httpClient.get<Note>(endpoints.NOTE.BY_ID(id), {
122
+ responseSchema: NoteSchema,
123
+ signal: options?.signal,
124
+ });
125
+
126
+ return response.data;
127
+ },
128
+ async create(note, options) {
129
+ const response = await httpClient.post<Note>(endpoints.NOTE.BASE, note, {
130
+ requestSchema: CreateNoteSchema,
131
+ responseSchema: NoteSchema,
132
+ signal: options?.signal,
133
+ });
134
+
135
+ return response.data;
136
+ },
137
+ async update(id, note, options) {
138
+ const response = await httpClient.put<Note>(endpoints.NOTE.BY_ID(id), note, {
139
+ requestSchema: UpdateNoteSchema,
140
+ responseSchema: NoteSchema,
141
+ signal: options?.signal,
142
+ });
143
+
144
+ return response.data;
145
+ },
146
+ async delete(id, options) {
147
+ await httpClient.delete(endpoints.NOTE.BY_ID(id), { signal: options?.signal });
148
+ },
149
+ });
150
+
151
+ export const noteApi = createNoteApiService(); // singleton for the whole app
152
+ ```
153
+
154
+ ## 3. Gateway (`repositories/note/gateways/`) — interface + http impl + factory
155
+
156
+ `note.gateway.types.ts`:
157
+ ```tsx
158
+ import type { CreateNoteRequest, Note, NoteFilters, UpdateNoteRequest } from '@/modules/note/note.types';
159
+ import type { BaseGateway, GatewayOptions } from '@/types/gateway.types';
160
+
161
+ export interface NoteGateway extends BaseGateway {
162
+ findAll(filters?: NoteFilters, options?: GatewayOptions): Promise<Note[]>;
163
+ findById(id: string | number, options?: GatewayOptions): Promise<Note>;
164
+ create(note: CreateNoteRequest, options?: GatewayOptions): Promise<Note>;
165
+ update(id: string | number, note: UpdateNoteRequest, options?: GatewayOptions): Promise<Note>;
166
+ delete(id: string | number, options?: GatewayOptions): Promise<void>;
167
+ }
168
+ ```
169
+
170
+ `http-gateway/http-gateway.ts`:
171
+ ```tsx
172
+ import { noteApi } from '@/modules/note/api/note-api';
173
+ import type { NoteGateway } from '@/modules/note/repositories/note/gateways/note.gateway.types';
174
+
175
+ export const createHttpNoteGateway = (): NoteGateway => ({
176
+ async findAll(filters, options) {
177
+ return noteApi.getAll(filters, options);
178
+ },
179
+ async findById(id, options) {
180
+ return noteApi.getById(id, options);
181
+ },
182
+ async create(note, options) {
183
+ return noteApi.create(note, options);
184
+ },
185
+ async update(id, note, options) {
186
+ return noteApi.update(id, note, options);
187
+ },
188
+ async delete(id, options) {
189
+ await noteApi.delete(id, options);
190
+ },
191
+ getSourceInfo() {
192
+ return {
193
+ type: 'http',
194
+ name: 'HTTP API Gateway',
195
+ capabilities: { offline: false, realtime: true, persistence: true },
196
+ };
197
+ },
198
+ });
199
+ ```
200
+
201
+ `gateways/index.ts` — the factory (`DataSource` is `'http' | 'localStorage'` only, no `'mock'`):
202
+ ```tsx
203
+ import { DataSource } from '@/types/gateway.types';
204
+
205
+ import { createHttpNoteGateway } from './http-gateway/http-gateway';
206
+ import type { NoteGateway } from './note.gateway.types';
207
+
208
+ export const createNoteGateway = (source: DataSource = 'http'): NoteGateway => {
209
+ switch (source) {
210
+ case 'http':
211
+ return createHttpNoteGateway();
212
+ // add createLocalStorageNoteGateway() only if a localStorage source is requested
213
+ default:
214
+ return createHttpNoteGateway();
215
+ }
216
+ };
217
+
218
+ export { createHttpNoteGateway } from './http-gateway/http-gateway';
219
+ export type { NoteGateway } from './note.gateway.types';
220
+ ```
221
+
222
+ ## 4. Repository (`repositories/note/`) — keys → options → queries → mutations → types → combined
223
+
224
+ `note.repository.keys.ts` — query-key factory, namespaced by `dataSource`:
225
+ ```tsx
226
+ import type { NoteFilters } from '@/modules/note/note.types';
227
+ import type { DataSource } from '@/types/gateway.types';
228
+
229
+ export const noteQueryKeys = {
230
+ all: ['notes'] as const,
231
+ lists: (dataSource: DataSource = 'http') => [...noteQueryKeys.all, 'list', dataSource] as const,
232
+ list: (filters: NoteFilters = {}, dataSource: DataSource = 'http') => [...noteQueryKeys.lists(dataSource), filters] as const,
233
+ details: (dataSource: DataSource = 'http') => [...noteQueryKeys.all, 'detail', dataSource] as const,
234
+ detail: (id: string | number, dataSource: DataSource = 'http') => [...noteQueryKeys.details(dataSource), id] as const,
235
+ } as const;
236
+ ```
237
+
238
+ `note.query-options.ts` — `queryOptions`/`mutationOptions` factories (reused by hooks + prefetch):
239
+ ```tsx
240
+ import { mutationOptions, queryOptions } from '@tanstack/react-query';
241
+
242
+ import type { DataSource } from '@/types/gateway.types';
243
+
244
+ import { createNoteGateway } from './gateways';
245
+ import { noteQueryKeys } from './note.repository.keys';
246
+
247
+ const getNotesQueryOptions = (filters = {}, dataSource: DataSource = 'http') =>
248
+ queryOptions({
249
+ queryKey: noteQueryKeys.list(filters, dataSource),
250
+ queryFn: ({ signal }) => createNoteGateway(dataSource).findAll(filters, { signal }),
251
+ staleTime: 5 * 60 * 1000,
252
+ gcTime: 10 * 60 * 1000,
253
+ });
254
+
255
+ const getNoteQueryOptions = (id: string | number, dataSource: DataSource = 'http') =>
256
+ queryOptions({
257
+ queryKey: noteQueryKeys.detail(id, dataSource),
258
+ queryFn: ({ signal }) => createNoteGateway(dataSource).findById(id, { signal }),
259
+ staleTime: 5 * 60 * 1000,
260
+ gcTime: 10 * 60 * 1000,
261
+ enabled: !!id,
262
+ });
263
+
264
+ const getCreateNoteMutationOptions = (dataSource: DataSource = 'http') =>
265
+ mutationOptions({ mutationFn: (note: any) => createNoteGateway(dataSource).create(note), retry: 1 });
266
+
267
+ const getUpdateNoteMutationOptions = (dataSource: DataSource = 'http') =>
268
+ mutationOptions({
269
+ mutationFn: ({ id, data }: { id: string | number; data: any }) => createNoteGateway(dataSource).update(id, data),
270
+ retry: 1,
271
+ });
272
+
273
+ const getDeleteNoteMutationOptions = (dataSource: DataSource = 'http') =>
274
+ mutationOptions({ mutationFn: (id: string | number) => createNoteGateway(dataSource).delete(id), retry: 1 });
275
+
276
+ export const noteQueryOptions = { notes: getNotesQueryOptions, note: getNoteQueryOptions } as const;
277
+ export const noteMutationOptions = {
278
+ createNote: getCreateNoteMutationOptions,
279
+ updateNote: getUpdateNoteMutationOptions,
280
+ deleteNote: getDeleteNoteMutationOptions,
281
+ } as const;
282
+ ```
283
+
284
+ `note.repository.queries.ts` — query hooks compose the options:
285
+ ```tsx
286
+ import { useQuery } from '@tanstack/react-query';
287
+
288
+ import { noteQueryOptions } from './note.query-options';
289
+ import type { NoteQueriesRepository } from './note.repository.types';
290
+
291
+ export const noteQueriesRepository: NoteQueriesRepository = {
292
+ useNotes: (filters = {}, dataSource = 'http', options) =>
293
+ useQuery({ ...noteQueryOptions.notes(filters, dataSource), ...options }),
294
+ useNote: (id, dataSource = 'http', options) =>
295
+ useQuery({ ...noteQueryOptions.note(id, dataSource), ...options }),
296
+ };
297
+ ```
298
+
299
+ `note.repository.mutations.ts` — mutation hooks own cache writes in `onSuccess`; always forward the user's `options?.onSuccess`:
300
+ ```tsx
301
+ import { useMutation, useQueryClient } from '@tanstack/react-query';
302
+
303
+ import type { Note } from '@/modules/note/note.types';
304
+
305
+ import { noteMutationOptions } from './note.query-options';
306
+ import { noteQueryKeys } from './note.repository.keys';
307
+ import type { NoteMutationsRepository } from './note.repository.types';
308
+
309
+ export const noteMutationsRepository: NoteMutationsRepository = {
310
+ useCreateNote: (dataSource = 'http', options) => {
311
+ const queryClient = useQueryClient();
312
+ const baseOptions = noteMutationOptions.createNote(dataSource);
313
+
314
+ return useMutation({
315
+ ...baseOptions,
316
+ // eslint-disable-next-line max-params ← REQUIRED: onSuccess has 4 params, lint cap is 3
317
+ onSuccess: (newNote, vars, onMutateResult, ctx) => {
318
+ queryClient.invalidateQueries({ queryKey: noteQueryKeys.lists(dataSource) });
319
+ queryClient.setQueryData<Note[]>(noteQueryKeys.lists(dataSource), (old) => (old ? [...old, newNote] : [newNote]));
320
+ options?.onSuccess?.(newNote, vars, onMutateResult, ctx);
321
+ },
322
+ ...options,
323
+ });
324
+ },
325
+ useUpdateNote: (dataSource = 'http', options) => {
326
+ const queryClient = useQueryClient();
327
+
328
+ return useMutation({
329
+ ...noteMutationOptions.updateNote(dataSource),
330
+ // eslint-disable-next-line max-params
331
+ onSuccess: (updated: Note, vars, onMutateResult, ctx) => {
332
+ queryClient.setQueryData<Note[]>(noteQueryKeys.lists(), (old) => old?.map((n) => (n.id === updated.id ? updated : n)));
333
+ queryClient.invalidateQueries({ queryKey: noteQueryKeys.lists(dataSource) });
334
+ options?.onSuccess?.(updated, vars, onMutateResult, ctx);
335
+ },
336
+ ...options,
337
+ });
338
+ },
339
+ useDeleteNote: (dataSource = 'http', options) => {
340
+ const queryClient = useQueryClient();
341
+
342
+ return useMutation({
343
+ ...noteMutationOptions.deleteNote(dataSource),
344
+ // eslint-disable-next-line max-params
345
+ onSuccess: (result, deletedId, onMutateResult, ctx) => {
346
+ queryClient.setQueryData<Note[]>(noteQueryKeys.lists(), (old) => old?.filter((n) => n.id !== deletedId));
347
+ queryClient.invalidateQueries({ queryKey: noteQueryKeys.lists(dataSource) });
348
+ options?.onSuccess?.(result, deletedId, onMutateResult, ctx);
349
+ },
350
+ ...options,
351
+ });
352
+ },
353
+ };
354
+ ```
355
+
356
+ `note.repository.types.ts` — interfaces (queries extend `BaseRepository`; options from `@/core/lib/react-query`):
357
+ ```tsx
358
+ import { type UseMutationResult, type UseQueryResult } from '@tanstack/react-query';
359
+
360
+ import type { BaseRepository } from '@/core/lib/react-query';
361
+ import { MutationOptions, QueryOptions } from '@/core/lib/react-query';
362
+ import type { CreateNoteRequest, Note, NoteFilters, UpdateNoteRequest } from '@/modules/note/note.types';
363
+ import type { DataSource } from '@/types/gateway.types';
364
+
365
+ export interface NoteQueriesRepository extends BaseRepository {
366
+ useNotes: (filters?: NoteFilters, dataSource?: DataSource, options?: QueryOptions) => UseQueryResult<Note[], Error>;
367
+ useNote: (id: string | number, dataSource?: DataSource, options?: QueryOptions) => UseQueryResult<Note, Error>;
368
+ }
369
+ export interface NoteMutationsRepository {
370
+ useCreateNote: (dataSource?: DataSource, options?: MutationOptions) => UseMutationResult<Note, Error, CreateNoteRequest>;
371
+ useUpdateNote: (dataSource?: DataSource, options?: MutationOptions) => UseMutationResult<Note, Error, { id: string | number; data: UpdateNoteRequest }>;
372
+ useDeleteNote: (dataSource?: DataSource, options?: MutationOptions) => UseMutationResult<void, Error, string | number>;
373
+ }
374
+ export interface NoteRepository {
375
+ queries: NoteQueriesRepository;
376
+ mutations: NoteMutationsRepository;
377
+ queryKeys: typeof import('./note.repository.keys').noteQueryKeys;
378
+ }
379
+ ```
380
+
381
+ `repositories/note/index.ts` — the combined repository object the hooks import:
382
+ ```tsx
383
+ import { noteMutationsRepository } from './note.repository.mutations';
384
+ import { noteQueriesRepository } from './note.repository.queries';
385
+ import { noteQueryKeys } from './note.repository.keys';
386
+ import type { NoteRepository } from './note.repository.types';
387
+
388
+ export const noteRepository: NoteRepository = {
389
+ queries: noteQueriesRepository,
390
+ mutations: noteMutationsRepository,
391
+ queryKeys: noteQueryKeys,
392
+ };
393
+ ```
394
+
395
+ ## 5. Store (`stores/`) — Zustand via `createStoreWithMiddleware` (never raw `create`)
396
+
397
+ `note.store.types.ts`:
398
+ ```tsx
399
+ import type { NoteFilters } from '@/modules/note/note.types';
400
+
401
+ export interface NoteState {
402
+ selectedNoteId: number | null;
403
+ filters: NoteFilters;
404
+ setSelectedNoteId: (id: number | null) => void;
405
+ setFilters: (filters: NoteFilters) => void;
406
+ }
407
+ ```
408
+
409
+ `note.store.ts`:
410
+ ```tsx
411
+ import { createStoreWithMiddleware } from '@/core/lib/zustand';
412
+
413
+ import type { NoteState } from './note.store.types';
414
+
415
+ export const useNoteStore = createStoreWithMiddleware<NoteState>(
416
+ (set) => ({
417
+ selectedNoteId: null,
418
+ filters: {},
419
+ setSelectedNoteId: (id) => set((state) => { state.selectedNoteId = id; }), // immer middleware → mutate draft
420
+ setFilters: (filters) => set((state) => { state.filters = filters; }),
421
+ }),
422
+ { name: 'note-store' },
423
+ );
424
+ ```
425
+
426
+ `note.store.actions.ts` — selector hook for actions (stable references):
427
+ ```tsx
428
+ import { useNoteStore } from './note.store';
429
+
430
+ export const useNoteActions = () =>
431
+ useNoteStore((s) => ({ setSelectedNoteId: s.setSelectedNoteId, setFilters: s.setFilters }));
432
+ ```
433
+
434
+ ## 6. Selector (`selectors/`) — derived state via `useMemo` over a query
435
+
436
+ `use-filtered-notes-selector/use-filtered-notes-selector.hook.ts`:
437
+ ```tsx
438
+ import { useMemo } from 'react';
439
+
440
+ import { noteRepository } from '@/modules/note/repositories/note';
441
+ import { useNoteStore } from '@/modules/note/stores/note.store';
442
+ import type { DataSource } from '@/types/gateway.types';
443
+
444
+ export const useFilteredNotesSelector = (dataSource: DataSource = 'http') => {
445
+ const filters = useNoteStore((s) => s.filters);
446
+ const notesQuery = noteRepository.queries.useNotes(filters, dataSource);
447
+
448
+ const data = useMemo(() => {
449
+ const notes = notesQuery.data ?? [];
450
+ if (filters.completed === undefined) return notes;
451
+
452
+ return notes.filter((n) => n.completed === filters.completed);
453
+ }, [notesQuery.data, filters.completed]);
454
+
455
+ return { ...notesQuery, data };
456
+ };
457
+ ```
458
+
459
+ ## 7. Hooks (`views/note-management/hooks/`) — business vs controller split (mandatory)
460
+
461
+ **Business hook** = data + business rules + the four states. Exposes `error` + `refetch` for the error-boundary:
462
+ ```tsx
463
+ 'use client';
464
+
465
+ import { useCallback } from 'react';
466
+
467
+ import { noteRepository } from '@/modules/note/repositories/note';
468
+ import { useNoteActions } from '@/modules/note/stores/note.store.actions';
469
+ import type { CreateNoteRequest, UpdateNoteRequest } from '@/modules/note/note.types';
470
+ import type { DataSource } from '@/types/gateway.types';
471
+
472
+ export const useNoteManagementBusiness = (dataSource: DataSource = 'http') => {
473
+ const { setSelectedNoteId } = useNoteActions();
474
+
475
+ const notesQuery = noteRepository.queries.useNotes({}, dataSource);
476
+ const createMutation = noteRepository.mutations.useCreateNote(dataSource);
477
+ const deleteMutation = noteRepository.mutations.useDeleteNote(dataSource);
478
+
479
+ const createNote = useCallback(
480
+ (data: CreateNoteRequest) => {
481
+ if (!data.title?.trim()) throw new Error('common.validation.requiredField');
482
+
483
+ return createMutation.mutate({ ...data, title: data.title.trim() });
484
+ },
485
+ [createMutation],
486
+ );
487
+ const deleteNote = useCallback((id: number) => deleteMutation.mutate(id), [deleteMutation]);
488
+
489
+ return {
490
+ notes: notesQuery.data ?? [],
491
+ isLoading: notesQuery.isLoading,
492
+ isEmpty: !notesQuery.isLoading && (notesQuery.data?.length ?? 0) === 0,
493
+ error: notesQuery.error ?? createMutation.error ?? deleteMutation.error, // four states: error
494
+ isCreating: createMutation.isPending,
495
+ isDeleting: deleteMutation.isPending,
496
+ createNote,
497
+ deleteNote,
498
+ setSelectedNoteId,
499
+ refetch: notesQuery.refetch, // wired to the error-boundary's onRetry
500
+ };
501
+ };
502
+ ```
503
+
504
+ **Controller hook** = UI orchestration (event handlers that adapt UI events → business actions). No data fetching:
505
+ ```tsx
506
+ 'use client';
507
+
508
+ import { useCallback } from 'react';
509
+
510
+ import type { CreateNoteRequest } from '@/modules/note/note.types';
511
+
512
+ export const useNoteManagementController = () => {
513
+ const handleCreateSubmit = useCallback(
514
+ (createNote: (data: CreateNoteRequest) => void) => (data: CreateNoteRequest) => createNote(data),
515
+ [],
516
+ );
517
+ const handleDeleteClick = useCallback((deleteNote: (id: number) => void) => (id: number) => deleteNote(id), []);
518
+
519
+ return { handleCreateSubmit, handleDeleteClick };
520
+ };
521
+ ```
522
+
523
+ ## 8. View (`views/note-management/note-management.view.tsx`) — orchestrates; returns the error state; no inline markup blocks
524
+
525
+ ```tsx
526
+ 'use client';
527
+
528
+ import { ErrorBoundary, NoteForm, NoteList } from '@/modules/note/components';
529
+ import tokens from '@/styles/tokens';
530
+ import { Box, Typography } from '@/ui';
531
+
532
+ import { useNoteManagementBusiness, useNoteManagementController } from './hooks';
533
+
534
+ export const NoteManagementView = () => {
535
+ const { notes, isLoading, isCreating, isDeleting, error, createNote, deleteNote, refetch } = useNoteManagementBusiness('http');
536
+ const { handleCreateSubmit, handleDeleteClick } = useNoteManagementController();
537
+
538
+ // four states: ERROR — recoverable error+retry, never a blank screen (see error-boundary skill)
539
+ if (error) return <ErrorBoundary error={error} onRetry={refetch} />;
540
+
541
+ return (
542
+ <Box sx={{ p: tokens.spacing.scale6 }}>
543
+ <Typography gutterBottom variant="h4">
544
+ Notes
545
+ </Typography>
546
+ <NoteForm isCreating={isCreating} onSubmit={handleCreateSubmit(createNote)} />
547
+ <NoteList isDeleting={isDeleting} isLoading={isLoading} notes={notes} onDelete={handleDeleteClick(deleteNote)} />
548
+ </Box>
549
+ );
550
+ };
551
+ ```
552
+
553
+ ## 9. Components (`components/`) — composed, `@/ui` + tokens, mapped row is its own component
554
+
555
+ `note-list.component.tsx` — anything inside `.map()` is its own component (`NoteItem`), never inline JSX:
556
+ ```tsx
557
+ import tokens from '@/styles/tokens';
558
+ import { Box, Typography } from '@/ui';
559
+
560
+ import { NoteItem } from '../note-item/note-item.component';
561
+ import type { Note } from '@/modules/note/note.types';
562
+
563
+ interface NoteListProps {
564
+ notes: Note[];
565
+ isLoading: boolean;
566
+ isDeleting: boolean;
567
+ onDelete: (id: number) => void;
568
+ }
569
+
570
+ export const NoteList = ({ notes, isLoading, isDeleting, onDelete }: NoteListProps) => {
571
+ if (isLoading) return <Typography>Loading…</Typography>; // four states: loading
572
+ if (notes.length === 0) return <Typography>No notes yet</Typography>; // four states: empty
573
+
574
+ return (
575
+ <Box sx={{ display: 'flex', flexDirection: 'column', gap: tokens.spacing.scale4 }}>
576
+ {notes.map((note) => (
577
+ <NoteItem key={note.id} note={note} isDeleting={isDeleting} onDelete={onDelete} />
578
+ ))}
579
+ </Box>
580
+ );
581
+ };
582
+ ```
583
+
584
+ `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.
585
+
586
+ `components/index.ts` barrels every component; `views/index.ts` barrels the view; each layer dir has its `index.ts`.
587
+
588
+ ## 10. Tests (colocated, one per layer) — `@test/utils` + faker factories
589
+
590
+ ```tsx
591
+ // note-item.component.test.tsx
592
+ import { fireEvent, renderWithProviders, screen } from '@test/utils';
593
+ import { createMockNote } from '@test/entities/note.mock'; // faker factory; create if missing
594
+
595
+ import { NoteItem } from './note-item.component';
596
+
597
+ it('calls onDelete when delete is clicked', () => {
598
+ const onDelete = jest.fn();
599
+ renderWithProviders(<NoteItem note={createMockNote()} isDeleting={false} onDelete={onDelete} />);
600
+ fireEvent.click(screen.getByRole('button', { name: /delete/i }));
601
+ expect(onDelete).toHaveBeenCalled();
602
+ });
603
+ ```
604
+
605
+ 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.
606
+
607
+ ---
608
+
609
+ ## Layer dependency direction (never violate)
610
+
611
+ ```
612
+ View → controller hook + business hook → repository (queries/mutations) → gateway → api → httpClient
613
+ ↘ store / selectors (local + derived state)
614
+ ```
615
+ 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.
@@ -21,7 +21,7 @@ Drift = the scaffold's `source` files changed since `source_version`.
21
21
 
22
22
  Inputs: path to a scaffold checkout (or the generated app, which mirrors it). Without it, degrade to a frontmatter-only report (list pins, can't diff).
23
23
 
24
- 1. **Collect pins:** read `source_version` + `source` from all `.claude/skills/*/SKILL.md`. Confirm they agree on one version (they should).
24
+ 1. **Collect pins:** read `source_version` + `source` from all `.claude/skills/*/SKILL.md` **and the bundled reference `.claude/reference/module/note-module.md`** (it carries the same frontmatter pin). Confirm they agree on one version (they should).
25
25
  2. **Get scaffold HEAD:** in the scaffold repo, `git rev-parse --short HEAD`. If it equals the pin → CLEAN, stop.
26
26
  3. **Diff source files:** `git diff --name-only <pin> <HEAD> -- <union of all skills' source paths>`.
27
27
  - Map each changed file back to the skill(s) whose `source:` references it.
@@ -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