@skyf0xx/hedgehog-core-full-stack-app 1.1.0 → 1.3.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/CLAUDE.core.md CHANGED
@@ -22,46 +22,25 @@ steps from memory:
22
22
  `hedgehog verify`, which commits it on a pass. Also holds the Correction
23
23
  Protocol for fixing a wrong upstream step. Invoke it at the start of any
24
24
  build session and for "what's next".
25
+ <!-- hedgehog:bootstrap-only start -->
25
26
  - **`hedgehog-bootstrap`** — run **once**, at project start, to scaffold
26
27
  the core stack, the enforcement config, and whichever add-ons (Auth,
27
28
  Queue, Mobile) planning intake turned on. Skip if `nx.json` already
28
29
  exists.
30
+ <!-- hedgehog:bootstrap-only end -->
29
31
  - **`conventional-commits`** — when a change spans several steps in one
30
32
  working-tree pass and needs splitting back into per-step commits (mainly
31
33
  Correction Protocol cleanups).
32
34
 
33
35
  ### The agents — delegate the judgment calls
34
36
 
35
- - **`planner`** — planning intake (which core applies, then
36
- `hedgehog-planning-intake`'s BMAD-METHOD brainstorming/brief/PRD/UX-spec
37
- shelf, mined into intent records, the Add-ons decision, and domain
38
- vocabulary) at project start. Writes intents via `hedgehog intent add`,
39
- `.hedgehog/addons.yaml`, and `.hedgehog/BMAD/`. On first run, hands off
40
- to the `bootstrap` agent once Confirm & Lock holds. Runs again whenever
41
- new scope enters play — including after the build is complete — taking
42
- `hedgehog-planning-intake`'s **Re-entry pass**: the BMAD shelf and
43
- `bootstrap` are both skipped, new modules are mined into additional
44
- intents, and `hedgehog plan` appends their tasks without touching
45
- anything already built.
46
- - **`bootstrap`** — runs `hedgehog-bootstrap`'s core steps (always) plus
47
- whichever add-on steps planning intake turned on. Triggered
48
- automatically by `planner` after its first run; skip if `nx.json`
49
- already exists.
50
- - **`backend-eng`** — builds each module's Phase A layers (schema →
51
- contract → repository → service → controller → queue?), one
52
- `hedgehog claim`ed packet at a time, gated by `hedgehog verify`.
53
- - **`ux-planner`** — once per module in Phase B, after the hook exists and
54
- before the screen: writes `docs/design/<module>.md`, reading
55
- `.hedgehog/BMAD/05-ux-spec/` directly (or
56
- `docs/design/<module>-notes.md` if a prior run already filed one). Where
57
- the archive holds no UX spec, it asks for visual input rather than
58
- inferring a direction.
59
- - **`front-end-eng`** — builds each module's Phase B layers (hook, screen)
60
- from the ux-planner rationale, one `hedgehog claim`ed packet at a time,
61
- gated by `hedgehog verify`.
62
- - **`reviewer`** — phase-transition and Correction Protocol checks the
63
- mechanical gate can't make (port discipline, FK-by-ID discipline,
64
- contract shape).
37
+ `backend-eng` builds each module's Phase A layers (schema → contract →
38
+ repository → service → controller → queue?), one `hedgehog claim`ed
39
+ packet at a time, gated by `hedgehog verify`. `front-end-eng` builds
40
+ each module's Phase B layers (hook, screen) from `ux-planner`'s
41
+ rationale, the same way. See `hedgehog-loop` for exactly which agent
42
+ owns which layer and the claim/verify sequencing — that skill is the
43
+ source, not restated here.
65
44
 
66
45
  ## The constants (do not deviate)
67
46
 
@@ -121,9 +100,6 @@ docs/
121
100
  design <module>.md (ux-planner, reading .hedgehog/BMAD/05-ux-spec/ directly)
122
101
  ```
123
102
 
124
- Check `.hedgehog/addons.yaml` before assuming any "only if" line above is
125
- actually present in this codebase.
126
-
127
103
  ### Core rules
128
104
 
129
105
  - **One table = one domain module.** Each carries the full step sequence.
@@ -51,12 +51,12 @@ catch.
51
51
  ## Core Responsibilities
52
52
 
53
53
  - **`schema`**: define the table in `packages/db` (Drizzle). One domain
54
- module = one table. Cross-module references are FK-by-ID columns
55
- only — never a foreign schema import. Add one re-export line for the
56
- module to `packages/db/src/schema/index.ts` (in scope for this
57
- layer) so the table is importable outside `packages/db` — the
58
- package's own `src/index.ts` re-exports that barrel and never
59
- changes after bootstrap.
54
+ module per table, cross-module references FK-by-ID only (root
55
+ CLAUDE.md's Core rules) — never a foreign schema import. Add one
56
+ re-export line for the module to `packages/db/src/schema/index.ts`
57
+ (in scope for this layer) so the table is importable outside
58
+ `packages/db` — the package's own `src/index.ts` re-exports that
59
+ barrel and never changes after bootstrap.
60
60
  - **`contract`**: derive the Zod schema from Drizzle (`drizzle-zod`) and
61
61
  wire the ts-rest contract in `packages/contracts`. A `date`-mode
62
62
  `timestamp` column reflected through `createSelectSchema` is overridden
@@ -101,8 +101,7 @@ catch.
101
101
  the packet's scope and rules don't account for; your own tests prove
102
102
  internal consistency, never coverage of what was asked. INHERITED DEBT
103
103
  is what the layers you depend on declared they left for you; declare
104
- your own with `hedgehog debt add <task-id> "<note>"` rather than a
105
- code comment nothing reads. Its WHY NOW section
104
+ your own with `hedgehog debt add <task-id> "<note>"`. Its WHY NOW section
106
105
  already confirms the module is in scope and every dependency is
107
106
  `complete` — no need to re-derive that by hand. Cross-module FK
108
107
  targets should already have their own schema landed (the packet's
@@ -122,14 +121,10 @@ catch.
122
121
  name the shared files that changed (typically `pnpm-lock.yaml`, root
123
122
  `tsconfig.json`) in your report — the orchestrating session commits
124
123
  them separately, since you report but never commit (next step).
125
- 3. **Report the work as done; do not commit it yourself.** Per the build
126
- graph's design, an agent reporting success never moves a task — only
127
- `hedgehog verify <task-id>`'s passing exit code does. It checks your
128
- changes against the packet's ALLOWED SCOPE, re-runs the real
129
- verification command, and on a pass writes the commit (the packet's
130
- exact Conventional Commit message) itself. Any shared workspace files
131
- you flagged in step 2 are a separate commit the orchestrating session
132
- makes before dispatching `hedgehog verify`, not something you commit.
124
+ 3. **Report the work as done; do not commit it yourself.** Any shared
125
+ workspace files you flagged in step 2 are a separate commit the
126
+ orchestrating session makes before dispatching `hedgehog verify`, not
127
+ something you commit.
133
128
  4. One layer at a time — never start the next layer before
134
129
  `hedgehog verify` reports the current one `complete`.
135
130
  5. Once `hedgehog verify` reports the `controller` layer (and any bundled
@@ -154,7 +149,7 @@ catch.
154
149
  reported rather than chosen here. `verify` cannot check any of this,
155
150
  which is exactly why it's on you.
156
151
  - Never import another module's repository, service, or schema directly
157
- — cross-module references are FK-by-ID, resolved at the
152
+ — FK-by-ID only (root CLAUDE.md's Core rules), resolved at the
158
153
  contract/controller layer (parallel calls) or via a same-repository
159
154
  Drizzle join against the other module's *schema*, never its adapter.
160
155
  - Never write queue infra when the Queue add-on is off (per
@@ -96,8 +96,7 @@ don't reach for a second one.
96
96
  and rules don't account for; your own tests prove internal
97
97
  consistency, never coverage of what was asked. INHERITED DEBT is what
98
98
  the layers you depend on declared they left for you; declare your own
99
- with `hedgehog debt add <task-id> "<note>"` rather than a code comment
100
- nothing reads. Its WHY NOW section already
99
+ with `hedgehog debt add <task-id> "<note>"`. Its WHY NOW section already
101
100
  confirms Phase A is closed for this module (the `hook`/`screen`
102
101
  layer's dependencies wouldn't be `complete` otherwise) — no need to
103
102
  re-derive that by hand. If you're handed a step outside a packet with
@@ -116,12 +115,10 @@ don't reach for a second one.
116
115
  the workspace, and name the shared files that changed (typically
117
116
  `pnpm-lock.yaml`, root `tsconfig.json`) in your report — the
118
117
  orchestrating session commits them separately (next step).
119
- 3. **Report the work as done; do not commit it yourself.** Only
120
- `hedgehog verify <task-id>`'s passing exit code moves the task to
121
- `complete` and writes the commit (the packet's exact Conventional
122
- Commit message). Any shared workspace files you flagged in step 2 are a
123
- separate commit the orchestrating session makes before dispatching
124
- `hedgehog verify`, not something you commit.
118
+ 3. **Report the work as done; do not commit it yourself.** Any shared
119
+ workspace files you flagged in step 2 are a separate commit the
120
+ orchestrating session makes before dispatching `hedgehog verify`, not
121
+ something you commit.
125
122
  4. Build the screen consuming the hook the same way — packet, build,
126
123
  report, `hedgehog verify`.
127
124
  5. One layer at a time — `hook` fully `complete` before the `screen`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@skyf0xx/hedgehog-core-full-stack-app",
3
- "version": "1.1.0",
3
+ "version": "1.3.0",
4
4
  "description": "Hedgehog's full-stack-app core: an Nx/pnpm/NestJS/Next.js workspace, backend-first domain module build discipline, and the agents and skills that drive it.",
5
5
  "type": "module",
6
6
  "scripts": {
@@ -9,9 +9,10 @@ The operating loop for a bootstrapped Hedgehog project: `hedgehog claim`
9
9
  reserves the packet(s) for ready layers, build them, `hedgehog verify`
10
10
  gates and commits each. The build graph (`.hedgehog/hedgehog.db`) is the
11
11
  live list — query it via `hedgehog status`/`hedgehog ready`, never
12
- re-derive state from prose. The step tables below mirror this core's
12
+ re-derive state from prose. The step table in the [scaffolding
13
+ reference](references/scaffolding.md) mirrors this core's
13
14
  `workspace/core.yaml`, the design source of truth
14
- for layer order, scope, and verify command per layer — read the tables
15
+ for layer order, scope, and verify command per layer — read the table
15
16
  for the human-readable shape, and the YAML when they seem to disagree.
16
17
 
17
18
  The packet, though, is what actually runs. `hedgehog plan` copies each
@@ -44,14 +45,14 @@ Phase A.
44
45
 
45
46
  ## The Domain Module Pattern
46
47
 
47
- A **domain module = one table.** `users`, `orders`, `order_items` are each
48
- their own module, carrying the full step sequence below. The schema is the
49
- source of truth for module boundaries.
48
+ Root CLAUDE.md's Core rules own "one table = one domain module" and
49
+ "FK-by-ID only" — this section is their mechanics. `users`, `orders`,
50
+ `order_items` are each their own module, carrying the full step sequence
51
+ below. The schema is the source of truth for module boundaries.
50
52
 
51
- **Cross-module references are FK-by-ID only.** If `orders.user_id`
52
- references `users`, the `orders` schema holds a plain FK column. The
53
- `orders` repository and service depend only on their own ports — a service
54
- knows related entities only as an ID.
53
+ If `orders.user_id` references `users`, the `orders` schema holds a plain
54
+ FK column. The `orders` repository and service depend only on their own
55
+ ports — a service knows related entities only as an ID.
55
56
 
56
57
  - Need the related row? Resolve it at the contract/controller layer
57
58
  (parallel calls to each module's own endpoint), or join against the
@@ -96,49 +97,30 @@ What's authored on top is the entity-specific delta: the field list and
96
97
  its types, the module's business rules, and the UX intent behind its
97
98
  screen.
98
99
 
99
- ## Domain Module — Backend Steps (Phase A, every module in scope)
100
-
101
- A horizontal pass across the whole backend — every module goes through
102
- these before any module gets a hook or screen. Each row is one compiled
103
- layer in `full-stack-app/core.yaml`; delegate each module's Phase A
104
- layers to the `backend-eng` agent, one claimed packet per dispatch — it
105
- builds the layer, `hedgehog verify` gates and commits it.
106
-
107
- | # | Layer | Lives in | Commit |
108
- |---|---|---|---|
109
- | 1 | `schema` | `packages/db` (Drizzle) | `feat(<module>): schema` |
110
- | 2 | `contract` | `packages/contracts` (Zod via `drizzle-zod` + ts-rest) | `feat(<module>): contract` |
111
- | 3 | `repository` | `libs/<module>/repository` (port + Drizzle adapter) | `feat(<module>): repository` |
112
- | 4 | `service` | `libs/<module>/service` (domain logic — imports only ports) | `feat(<module>): service` |
113
- | 5 | `controller` | `apps/api` (thin HTTP, wires contract → service; bundles Queue infra, see above, if that add-on is on and this module needs it) | `feat(<module>): api` |
114
-
115
- Repeat 1–5 per module in scope, via `hedgehog claim`/`hedgehog verify`.
116
- The API is complete, typed, and callable (Postman/curl/contract tests)
117
- before frontend work starts.
118
-
119
- ## Domain Module — Frontend Steps (Phase B, after Phase A closes for the module)
120
-
121
- | # | Layer | Lives in | Commit |
122
- |---|---|---|---|
123
- | 6 | `hook` | `packages/hooks` (TanStack Query) | `feat(<module>): hooks` |
124
- | 6a | UX rationale | `docs/design/<module>.md`, `ux-planner` agent | bundled into layer 7's commit |
125
- | 7 | `screen` | `apps/web`, plus `apps/mobile` when the Mobile add-on is on | `feat(<module>): screen-web`, or `feat(<module>): screen` when the Mobile add-on is on |
126
-
127
- Phase B starts once Phase A is done for the scope. The frontend is a pure
128
- consumer of an already-finished API. Delegate each module's Phase B
129
- layers to the `front-end-eng` agent, same reasoning as `backend-eng` for
130
- Phase A — one claimed packet per dispatch, in its own context. Step
131
- 6a is where "how it should feel" gets decided — once per module, after
132
- the `hook` layer's task is `complete` and before `front-end-eng` starts
133
- the `screen` layer — via `ux-planner`, starting from whatever `planner`
134
- filed in `docs/design/<module>-notes.md` at planning intake, or the raw
135
- UX spec directly if that file is absent, or — where the archive holds
136
- neither — from the contract and hook plus whatever the user supplies when
137
- it asks. Its first run for a module also
138
- signals to the user that Phase B has started, and is the point a mockup,
139
- screenshot, or export (Google Stitch, Figma) can be handed over. It
140
- writes `docs/design/<module>.md`, not its own compiled layer — the
141
- `screen` layer's `hedgehog verify` is what gates and commits it.
100
+ ## Domain Module — Backend and Frontend Steps
101
+
102
+ Each module goes through `schema` → `contract` → `repository` → `service`
103
+ → `controller` in Phase A, then `hook` → `screen` in Phase B — the same
104
+ sequence "The Domain Module Pattern" above states. Read the [scaffolding
105
+ reference](references/scaffolding.md) for the full step table (layer,
106
+ where it lives, its commit message) and the generator command each layer
107
+ starts from. Delegate each module's Phase A layers to `backend-eng` and
108
+ Phase B layers to `front-end-eng`, one claimed packet per dispatch — it
109
+ builds the layer, `hedgehog verify` gates and commits it. The API is
110
+ complete, typed, and callable (Postman/curl/contract tests) before
111
+ frontend work starts.
112
+
113
+ Between the `hook` layer's task going `complete` and `front-end-eng`
114
+ starting the `screen` layer, `ux-planner` runs once per module to decide
115
+ "how it should feel" — starting from whatever `planner` filed in
116
+ `docs/design/<module>-notes.md` at planning intake, or the raw UX spec
117
+ directly if that file is absent, or — where the archive holds neither —
118
+ from the contract and hook plus whatever the user supplies when it asks.
119
+ Its first run for a module also signals to the user that Phase B has
120
+ started, and is the point a mockup, screenshot, or export (Google Stitch,
121
+ Figma) can be handed over. It writes `docs/design/<module>.md`, not its
122
+ own compiled layer — the `screen` layer's `hedgehog verify` is what gates
123
+ and commits it.
142
124
 
143
125
  ## The Loop (every unit of work)
144
126
 
@@ -193,12 +175,8 @@ writes `docs/design/<module>.md`, not its own compiled layer — the
193
175
  `hedgehog verify <task-id> --owner <owner>` (the same owner that
194
176
  claimed it; verify requires the lease owner). Building happens in
195
177
  parallel; verifying does not — verify writes a commit, and commits go
196
- through one at a time. It checks the touched files against the
197
- packet's ALLOWED SCOPE, runs the layer's VERIFICATION command, and on
198
- a pass writes the commit (the exact Conventional Commit message from
199
- the tables above, plus the updated build graph) and unlocks the next
200
- layer. On a scope violation or a failing check, the task moves to
201
- `blocked` with a `blocked_reason` of `scope_violation` or
178
+ through one at a time. On a scope violation or a failing check, the
179
+ task moves to `blocked` with a `blocked_reason` of `scope_violation` or
202
180
  `verification_failed`, and nothing downstream unlocks. Fix the work,
203
181
  then run `hedgehog retry <task-id>` to return the task to `planned`,
204
182
  claim it again (by task id — see below), and verify again —
@@ -238,7 +216,7 @@ the built work against it there, because nothing else in the build does.
238
216
  A layer that hits a limitation the next layer must compensate for
239
217
  declares it with `hedgehog debt add <task-id> "<note>"`; the note lands
240
218
  in the **INHERITED DEBT** section of every packet that depends on that
241
- task. A comment in a source file is not a mechanism — nothing reads it.
219
+ task.
242
220
 
243
221
  Each `hedgehog verify` call commits exactly one layer, built right for
244
222
  what's known now; a wrong layer is fixed forward later via the
@@ -249,183 +227,21 @@ or `lease_expired`).
249
227
 
250
228
  ## Scaffolding a layer
251
229
 
252
- `tools/generators/` holds one Nx generator per layer, and every layer
253
- starts from its own:
254
-
255
- ```bash
256
- nx g ./tools/generators:schema --module=<module> --fields='<name:type,...>'
257
- nx g ./tools/generators:contract --module=<module> --fields='<name:type,...>' [--toggleField=<boolField>]
258
- nx g ./tools/generators:repository --module=<module>
259
- nx g ./tools/generators:service --module=<module> [--toggleField=<boolField>]
260
- nx g ./tools/generators:controller --module=<module> --fields='<name:type,...>' [--toggleField=<boolField>]
261
- nx g ./tools/generators:hook --module=<module> [--toggleField=<boolField>]
262
- nx g ./tools/generators:screen --module=<module>
263
- ```
264
-
265
- `--module` is the domain module's plural kebab-case name (`tasks`,
266
- `order-items`). `--fields` is a comma-separated list of `name:type` pairs
267
- over `string`, `text`, `boolean`, `integer`, and `timestamp`, with a
268
- trailing `?` marking the column nullable
269
- (`--fields='title:string,done:boolean,dueDate:timestamp?'`); `contract`
270
- and `controller` take the same list the module's `schema` was generated
271
- with. A `string` field takes an optional length in parentheses
272
- (`title:string(500)`), threaded to both the Drizzle `varchar` and the Zod
273
- `.max()` so the two cannot disagree; omitted, it is 255. Check every
274
- length against the intent's own rules in the packet's **RELEVANT RULES** —
275
- a rule like "at most 500 characters" is the field list's business, not
276
- the authored delta's, and a Zod bound that outruns its column surfaces as
277
- a driver error at the database rather than a 400 at the boundary.
278
-
279
- `--toggleField` names a boolean field from the schema to expose as a
280
- toggle, and is passed to `contract`, `service`, `controller`, and `hook`
281
- alike — one flag, four layers, so the route, the domain method, the
282
- handler, and the mutation are generated from one source. The toggle is
283
- server-side by construction: `POST /<module>/:id/toggle` carries no body,
284
- and the service reads the current value and flips it inside the same
285
- transaction its `update` uses. The client sends only the id, so a stale
286
- cached row cannot overwrite a newer state — which is exactly what
287
- computing the new value on the client would do, losing one of any two
288
- flips that raced. Note that `@ts-rest/core` generates no `body` parameter
289
- for a `c.noBody()` route: the call is `client.toggle({ params: { id } })`,
290
- and passing `body: undefined` is a compile error.
291
-
292
- Each generator lands the whole conventional shape of its layer in one
293
- deterministic step — the package shell (`package.json`, `tsconfig*.json`,
294
- `vitest.config.mts`, `src/index.ts`) where the layer creates one, the
295
- `nx.tags` pair `packages/config/eslint-base.js`'s `depConstraints` keys
296
- on, the port-discipline file suffixes lint checks for, the Nest module
297
- and controller pair with `@Controller()` left bare (the ts-rest contract
298
- already encodes full route paths), and every barrel export the new files
299
- need. Hand-copying a sibling module's files invites exactly the drift
300
- `hedgehog verify`'s lint step then has to catch: missing tags, missing
301
- project references, a doubled route prefix.
302
-
303
- What the generator lands is the layer's skeleton, not the layer. Author
304
- the entity-specific delta on top: the module's business rules in
305
- `service`, its domain-error mapping in `controller`, and — for `screen`,
306
- which is skeleton-only by design — the layout, information hierarchy, and
307
- interaction pattern from `ux-planner`'s rationale, over the placeholders
308
- the generator leaves for the list, filter shell, empty state, and form.
309
-
310
- Registration inside `apps/api` is automatic and stays that way:
311
- `apps/api/src/app/feature-modules.ts` globs
312
- `apps/api/src/app/*/*.module.ts` and is regenerated by the
313
- `generate-feature-modules` Nx target that `build`/`typecheck`/`test`
314
- depend on. Never register a module by editing `app.module.ts` — a shared
315
- file no module-scoped task can safely touch. Validation is ts-rest + Zod,
316
- so this core has no Nest DTOs and no class-validator.
317
-
318
- The root page is the same: `apps/web/src/app/page.tsx` renders whatever
319
- `module-routes.ts` holds, and that file is regenerated by the
320
- `generate-module-routes` Nx target from every
321
- `apps/web/src/app/*/page.tsx` on disk. A screen layer creates its own
322
- `page.tsx` in its own directory and the root page picks it up — never
323
- edit either the root page or the generated file to add a link.
324
-
325
- **A new package needs wiring into the workspace before `hedgehog verify`
326
- runs on it.** A package that exists on disk isn't yet part of the
327
- workspace:
328
-
329
- ```bash
330
- pnpm install # link the new workspace:* deps
331
- pnpm nx sync # regenerate TypeScript project references
332
- ```
333
-
334
- `pnpm-workspace.yaml` already globs `packages/*`, `apps/*` and `libs/*/*`,
335
- so a package under any of those needs no edit there. This is the common
336
- case, not an edge case: any layer that is the first arrival in a package
337
- (`contract`, `hook`, each module's `repository` and `service`) or that
338
- wires a new package into an existing one (`controller`, adding the
339
- module's `contracts`/`repository`/`service` packages to `apps/api`) needs
340
- it — on a module's first pass through Phase A/B that is most of the
341
- layers, not an occasional one.
342
-
343
- The building agent runs `pnpm install` / `pnpm nx sync` and reports back
344
- which shared files changed (typically `pnpm-lock.yaml`, root
345
- `tsconfig.json`, and — on a `controller` layer — `apps/api/package.json`,
346
- `apps/api/tsconfig.app.json`), because it has the shell access to run
347
- them, but it never commits: no agent reporting success moves a task or
348
- touches git, only `hedgehog verify`'s passing exit code does (see the
349
- building agents' own Workflow step on this). Committing those shared
350
- files is the orchestrating session's job, done between dispatch and
351
- `hedgehog verify` on every layer where the agent flagged a change: expect
352
- it, don't wait to be reminded.
353
-
354
- ```bash
355
- git add pnpm-lock.yaml tsconfig.json # plus apps/*/package.json,
356
- # apps/*/tsconfig.app.json on a
357
- # controller layer
358
- git commit -m "chore(workspace): sync project references"
359
- ```
360
-
361
- These files are mechanically derived by `pnpm install` and `pnpm nx
362
- sync`, not authored content, and sit outside every module-scoped layer's
363
- scope — they belong to no layer, and no override covers them. Committing
364
- them separately, before `hedgehog verify` runs, keeps the layer's own
365
- commit exactly the layer.
366
-
367
- This is the orchestrating session's step rather than a verify post-step
368
- on purpose: `hedgehog verify` gates the tree it's handed, and a gate that
369
- mutates that tree would manufacture the scope violation it then reports.
230
+ Every layer starts from its own generator in `tools/generators/`, which
231
+ lands the layer's package shell, tags, and conventional shape in one
232
+ deterministic step, and a new package needs `pnpm install`/`pnpm nx sync`
233
+ wired in before `hedgehog verify` can run on it. Read the [scaffolding
234
+ reference](references/scaffolding.md) for the generator commands, flag
235
+ contract, and workspace-wiring steps.
370
236
 
371
237
  ## First arrival in a package
372
238
 
373
- Every layer scope names a directory *inside* a package
374
- (`packages/contracts/src/{module}/**`, `libs/{module}/repository/**`), and
375
- on the first module through that layer the package itself doesn't exist
376
- yet. Its shell — `package.json`, `tsconfig*.json`, `vitest.config.mts`,
377
- `src/index.ts` — necessarily lands outside the layer's scope glob, because
378
- no `{module}`-bearing glob can cover a package root. Left alone those files
379
- sit on disk uncommitted until the `join` layer's `**` scope sweeps them in,
380
- so `git log -- packages/contracts/` shows source with no buildable package
381
- behind it for the whole middle of the build.
382
-
383
- A generator can also drop shared, package-wide source at the `src/` root
384
- alongside the module's own files on that same first pass — the `contract`
385
- generator's `timestamp.ts` is one (a shared Zod util every module in the
386
- package imports, written once, sibling to `src/index.ts`). That file needs
387
- the same widening as the shell itself.
388
-
389
- The packet says so: when the package a scope points into has no
390
- `package.json` on disk yet, `hedgehog next`/`show` prints a **FIRST
391
- ARRIVAL** section under ALLOWED SCOPE carrying the exact command for that
392
- task. Run it before building — the widening is only available while the
393
- task is still `ready`, and a verify that rejects the shell paths blocks
394
- the task and turns this into a five-command recovery.
395
-
396
- ```bash
397
- hedgehog override add TASKS-CONTRACT \
398
- --scope 'packages/contracts/*' \
399
- --scope 'packages/contracts/src/*' \
400
- --reason 'first module through the contract layer also creates the package shell'
401
- ```
402
-
403
- `packages/contracts/src/*` is non-recursive, so it covers `src/index.ts`
404
- and `src/timestamp.ts` without also granting the module subdirectory the
405
- layer's own `packages/contracts/src/{module}/**` scope already covers.
406
-
407
- `.hedgehog/overrides/*.json` is additive, per-task, committed, and replayed
408
- by `plan`, `--recompile` and `db rebuild` alike, so the exception survives a
409
- rebuild and stays reviewable in the diff — unlike a hand-edited task row,
410
- which the next rebuild silently drops. It widens exactly the one task that
411
- creates the package, not the layer, so module two's task keeps the narrow
412
- scope.
413
-
414
- Which tasks need it: the first module through `contract`
415
- (`packages/contracts`) and through `hook` (`packages/hooks`), and every
416
- module's `repository` and `service`, since `libs/{module}/repository` and
417
- `libs/{module}/service` are new libs per module — there, the layer's own
418
- `libs/{module}/repository/**` glob already covers the package root, so no
419
- override is needed. `packages/db` and `packages/config` ship with core, so
420
- `schema` never needs one. `controller` never needs one either — `apps/api`
421
- ships with core, and its `apps/api/src/app/{module}/**` scope already
422
- covers the module's generated directory.
423
-
424
- Never widen a scope to route around a violation the Correction Protocol
425
- should handle — this is for a package shell the layer genuinely creates,
426
- nothing else. The shell itself comes from the layer's generator
427
- ("Scaffolding a layer" above), which is also where the workspace wiring a
428
- new package needs lives.
239
+ The first module through a layer whose scope names a directory inside a
240
+ package that doesn't exist yet on disk needs its scope widened for that
241
+ one task, since the package shell lands outside the layer's own
242
+ `{module}`-bearing glob. Read the [first arrival
243
+ reference](references/first-arrival.md) for which layers this applies to
244
+ and the `hedgehog override add` command it calls for.
429
245
 
430
246
  ## Intra-step conventions
431
247
 
@@ -615,8 +431,8 @@ the graph doesn't have a task for.
615
431
  committed.
616
432
  - The screen step doesn't start blank — `ux-planner` runs once per module,
617
433
  after the hook is committed, before `front-end-eng` starts the screen.
618
- - `packages/config` is the single source for shared config; a per-app
619
- override request signals to fix the base config at the source.
434
+ - `packages/config` is the single source for shared config (root
435
+ CLAUDE.md's Core rules).
620
436
 
621
437
  ## Stop Condition
622
438
 
@@ -0,0 +1,60 @@
1
+ # First arrival in a package
2
+
3
+ Every layer scope names a directory *inside* a package
4
+ (`packages/contracts/src/{module}/**`, `libs/{module}/repository/**`), and
5
+ on the first module through that layer the package itself doesn't exist
6
+ yet. Its shell — `package.json`, `tsconfig*.json`, `vitest.config.mts`,
7
+ `src/index.ts` — necessarily lands outside the layer's scope glob, because
8
+ no `{module}`-bearing glob can cover a package root. Left alone those files
9
+ sit on disk uncommitted until the `join` layer's `**` scope sweeps them in,
10
+ so `git log -- packages/contracts/` shows source with no buildable package
11
+ behind it for the whole middle of the build.
12
+
13
+ A generator can also drop shared, package-wide source at the `src/` root
14
+ alongside the module's own files on that same first pass — the `contract`
15
+ generator's `timestamp.ts` is one (a shared Zod util every module in the
16
+ package imports, written once, sibling to `src/index.ts`). That file needs
17
+ the same widening as the shell itself. See [scaffolding a
18
+ layer](scaffolding.md) for what a layer's generator lands and the
19
+ `pnpm install` / `pnpm nx sync` workspace wiring a new package needs.
20
+
21
+ The packet says so: when the package a scope points into has no
22
+ `package.json` on disk yet, `hedgehog next`/`show` prints a **FIRST
23
+ ARRIVAL** section under ALLOWED SCOPE carrying the exact command for that
24
+ task. Run it before building — the widening is only available while the
25
+ task is still `ready`, and a verify that rejects the shell paths blocks
26
+ the task and turns this into a five-command recovery.
27
+
28
+ ```bash
29
+ hedgehog override add TASKS-CONTRACT \
30
+ --scope 'packages/contracts/*' \
31
+ --scope 'packages/contracts/src/*' \
32
+ --reason 'first module through the contract layer also creates the package shell'
33
+ ```
34
+
35
+ `packages/contracts/src/*` is non-recursive, so it covers `src/index.ts`
36
+ and `src/timestamp.ts` without also granting the module subdirectory the
37
+ layer's own `packages/contracts/src/{module}/**` scope already covers.
38
+
39
+ `.hedgehog/overrides/*.json` is additive, per-task, committed, and replayed
40
+ by `plan`, `--recompile` and `db rebuild` alike, so the exception survives a
41
+ rebuild and stays reviewable in the diff — unlike a hand-edited task row,
42
+ which the next rebuild silently drops. It widens exactly the one task that
43
+ creates the package, not the layer, so module two's task keeps the narrow
44
+ scope.
45
+
46
+ Which tasks need it: the first module through `contract`
47
+ (`packages/contracts`) and through `hook` (`packages/hooks`), and every
48
+ module's `repository` and `service`, since `libs/{module}/repository` and
49
+ `libs/{module}/service` are new libs per module — there, the layer's own
50
+ `libs/{module}/repository/**` glob already covers the package root, so no
51
+ override is needed. `packages/db` and `packages/config` ship with core, so
52
+ `schema` never needs one. `controller` never needs one either — `apps/api`
53
+ ships with core, and its `apps/api/src/app/{module}/**` scope already
54
+ covers the module's generated directory.
55
+
56
+ Never widen a scope to route around a violation the Correction Protocol
57
+ should handle — this is for a package shell the layer genuinely creates,
58
+ nothing else. The shell itself comes from the layer's generator, which is
59
+ also where the workspace wiring a new package needs lives (see
60
+ [scaffolding a layer](scaffolding.md)).
@@ -0,0 +1,151 @@
1
+ # Scaffolding a layer
2
+
3
+ Every module in this core goes through the same layer sequence, in order:
4
+
5
+ ```
6
+ schema (Drizzle) — types before data
7
+ contract (Zod / ts-rest) — the boundary
8
+ repository (port + Drizzle adapter)
9
+ service (domain logic) — imports only ports
10
+ controller (thin HTTP)
11
+ hook (TanStack Query) — Phase B only
12
+ screen (Next.js / Expo) — Phase B only
13
+ ```
14
+
15
+ `workspace/core.yaml` declares this as `pattern: vertical-slice` — every
16
+ layer's scope carries `{module}`, so the chain runs once per module,
17
+ independently, joined only at the exclusive `join` layer. Each row below
18
+ is one compiled layer; `backend-eng` builds the Phase A rows (`schema`
19
+ through `controller`), `front-end-eng` builds the Phase B rows (`hook`,
20
+ `screen`), one claimed packet per dispatch — it builds the layer,
21
+ `hedgehog verify` gates and commits it.
22
+
23
+ | # | Layer | Lives in | Commit |
24
+ |---|---|---|---|
25
+ | 1 | `schema` | `packages/db` (Drizzle) | `feat(<module>): schema` |
26
+ | 2 | `contract` | `packages/contracts` (Zod via `drizzle-zod` + ts-rest) | `feat(<module>): contract` |
27
+ | 3 | `repository` | `libs/<module>/repository` (port + Drizzle adapter) | `feat(<module>): repository` |
28
+ | 4 | `service` | `libs/<module>/service` (domain logic — imports only ports) | `feat(<module>): service` |
29
+ | 5 | `controller` | `apps/api` (thin HTTP, wires contract → service; bundles Queue infra if that add-on is on and this module needs it) | `feat(<module>): api` |
30
+ | 6 | `hook` | `packages/hooks` (TanStack Query) | `feat(<module>): hooks` |
31
+ | 7 | `screen` | `apps/web`, plus `apps/mobile` when the Mobile add-on is on | `feat(<module>): screen-web`, or `feat(<module>): screen` when the Mobile add-on is on |
32
+
33
+ `tools/generators/` holds one Nx generator per layer, and every layer
34
+ starts from its own:
35
+
36
+ ```bash
37
+ nx g ./tools/generators:schema --module=<module> --fields='<name:type,...>'
38
+ nx g ./tools/generators:contract --module=<module> --fields='<name:type,...>' [--toggleField=<boolField>]
39
+ nx g ./tools/generators:repository --module=<module>
40
+ nx g ./tools/generators:service --module=<module> [--toggleField=<boolField>]
41
+ nx g ./tools/generators:controller --module=<module> --fields='<name:type,...>' [--toggleField=<boolField>]
42
+ nx g ./tools/generators:hook --module=<module> [--toggleField=<boolField>]
43
+ nx g ./tools/generators:screen --module=<module>
44
+ ```
45
+
46
+ `--module` is the domain module's plural kebab-case name (`tasks`,
47
+ `order-items`). `--fields` is a comma-separated list of `name:type` pairs
48
+ over `string`, `text`, `boolean`, `integer`, and `timestamp`, with a
49
+ trailing `?` marking the column nullable
50
+ (`--fields='title:string,done:boolean,dueDate:timestamp?'`); `contract`
51
+ and `controller` take the same list the module's `schema` was generated
52
+ with. A `string` field takes an optional length in parentheses
53
+ (`title:string(500)`), threaded to both the Drizzle `varchar` and the Zod
54
+ `.max()` so the two cannot disagree; omitted, it is 255. Check every
55
+ length against the intent's own rules in the packet's **RELEVANT RULES** —
56
+ a rule like "at most 500 characters" is the field list's business, not
57
+ the authored delta's, and a Zod bound that outruns its column surfaces as
58
+ a driver error at the database rather than a 400 at the boundary.
59
+
60
+ `--toggleField` names a boolean field from the schema to expose as a
61
+ toggle, and is passed to `contract`, `service`, `controller`, and `hook`
62
+ alike — one flag, four layers, so the route, the domain method, the
63
+ handler, and the mutation are generated from one source. The toggle is
64
+ server-side by construction: `POST /<module>/:id/toggle` carries no body,
65
+ and the service reads the current value and flips it inside the same
66
+ transaction its `update` uses. The client sends only the id, so a stale
67
+ cached row cannot overwrite a newer state — which is exactly what
68
+ computing the new value on the client would do, losing one of any two
69
+ flips that raced. Note that `@ts-rest/core` generates no `body` parameter
70
+ for a `c.noBody()` route: the call is `client.toggle({ params: { id } })`,
71
+ and passing `body: undefined` is a compile error.
72
+
73
+ Each generator lands the whole conventional shape of its layer in one
74
+ deterministic step — the package shell (`package.json`, `tsconfig*.json`,
75
+ `vitest.config.mts`, `src/index.ts`) where the layer creates one, the
76
+ `nx.tags` pair `packages/config/eslint-base.js`'s `depConstraints` keys
77
+ on, the port-discipline file suffixes lint checks for, the Nest module
78
+ and controller pair with `@Controller()` left bare (the ts-rest contract
79
+ already encodes full route paths), and every barrel export the new files
80
+ need. Hand-copying a sibling module's files invites exactly the drift
81
+ `hedgehog verify`'s lint step then has to catch: missing tags, missing
82
+ project references, a doubled route prefix.
83
+
84
+ What the generator lands is the layer's skeleton, not the layer. Author
85
+ the entity-specific delta on top: the module's business rules in
86
+ `service`, its domain-error mapping in `controller`, and — for `screen`,
87
+ which is skeleton-only by design — the layout, information hierarchy, and
88
+ interaction pattern from `ux-planner`'s rationale, over the placeholders
89
+ the generator leaves for the list, filter shell, empty state, and form.
90
+
91
+ Registration inside `apps/api` is automatic and stays that way:
92
+ `apps/api/src/app/feature-modules.ts` globs
93
+ `apps/api/src/app/*/*.module.ts` and is regenerated by the
94
+ `generate-feature-modules` Nx target that `build`/`typecheck`/`test`
95
+ depend on. Never register a module by editing `app.module.ts` — a shared
96
+ file no module-scoped task can safely touch. Validation is ts-rest + Zod,
97
+ so this core has no Nest DTOs and no class-validator.
98
+
99
+ The root page is the same: `apps/web/src/app/page.tsx` renders whatever
100
+ `module-routes.ts` holds, and that file is regenerated by the
101
+ `generate-module-routes` Nx target from every
102
+ `apps/web/src/app/*/page.tsx` on disk. A screen layer creates its own
103
+ `page.tsx` in its own directory and the root page picks it up — never
104
+ edit either the root page or the generated file to add a link.
105
+
106
+ **A new package needs wiring into the workspace before `hedgehog verify`
107
+ runs on it.** A package that exists on disk isn't yet part of the
108
+ workspace:
109
+
110
+ ```bash
111
+ pnpm install # link the new workspace:* deps
112
+ pnpm nx sync # regenerate TypeScript project references
113
+ ```
114
+
115
+ `pnpm-workspace.yaml` already globs `packages/*`, `apps/*` and `libs/*/*`,
116
+ so a package under any of those needs no edit there. This is the common
117
+ case, not an edge case: any layer that is the first arrival in a package
118
+ (`contract`, `hook`, each module's `repository` and `service`) or that
119
+ wires a new package into an existing one (`controller`, adding the
120
+ module's `contracts`/`repository`/`service` packages to `apps/api`) needs
121
+ it — on a module's first pass through Phase A/B that is most of the
122
+ layers, not an occasional one. See [first arrival in a
123
+ package](first-arrival.md) for the scope-widening override this same
124
+ situation needs.
125
+
126
+ The building agent runs `pnpm install` / `pnpm nx sync` and reports back
127
+ which shared files changed (typically `pnpm-lock.yaml`, root
128
+ `tsconfig.json`, and — on a `controller` layer — `apps/api/package.json`,
129
+ `apps/api/tsconfig.app.json`), because it has the shell access to run
130
+ them, but it never commits: no agent reporting success moves a task or
131
+ touches git, only `hedgehog verify`'s passing exit code does. Committing
132
+ those shared files is the orchestrating session's job, done between
133
+ dispatch and `hedgehog verify` on every layer where the agent flagged a
134
+ change: expect it, don't wait to be reminded.
135
+
136
+ ```bash
137
+ git add pnpm-lock.yaml tsconfig.json # plus apps/*/package.json,
138
+ # apps/*/tsconfig.app.json on a
139
+ # controller layer
140
+ git commit -m "chore(workspace): sync project references"
141
+ ```
142
+
143
+ These files are mechanically derived by `pnpm install` and `pnpm nx
144
+ sync`, not authored content, and sit outside every module-scoped layer's
145
+ scope — they belong to no layer, and no override covers them. Committing
146
+ them separately, before `hedgehog verify` runs, keeps the layer's own
147
+ commit exactly the layer.
148
+
149
+ This is the orchestrating session's step rather than a verify post-step
150
+ on purpose: `hedgehog verify` gates the tree it's handed, and a gate that
151
+ mutates that tree would manufacture the scope violation it then reports.