@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 +9 -33
- package/agents/backend-eng.md +12 -17
- package/agents/front-end-eng.md +5 -8
- package/package.json +1 -1
- package/skills/hedgehog-loop/SKILL.md +51 -235
- package/skills/hedgehog-loop/references/first-arrival.md +60 -0
- package/skills/hedgehog-loop/references/scaffolding.md +151 -0
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
|
-
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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.
|
package/agents/backend-eng.md
CHANGED
|
@@ -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
|
|
55
|
-
|
|
56
|
-
module to `packages/db/src/schema/index.ts`
|
|
57
|
-
layer) so the table is importable outside
|
|
58
|
-
package's own `src/index.ts` re-exports that
|
|
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>"
|
|
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.**
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
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
|
-
—
|
|
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
|
package/agents/front-end-eng.md
CHANGED
|
@@ -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>"
|
|
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.**
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
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.
|
|
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
|
|
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
|
|
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
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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.
|
|
197
|
-
|
|
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.
|
|
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
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
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
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
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
|
|
619
|
-
|
|
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.
|