@skyf0xx/hedgehog 0.1.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.
@@ -0,0 +1,403 @@
1
+ ---
2
+ name: hedgehog-bootstrap
3
+ description: Use once, at the start of a new Hedgehog project, to scaffold Project Bootstrap and wire in the enforcement config that makes the stack and build order mechanically true (Nx boundaries, lefthook, commitlint, env validation, phase gate). Triggers on "bootstrap this project", "set up the hedgehog stack", "scaffold the workspace". Not for per-module work — that's the `hedgehog-loop` skill, one step at a time.
4
+ ---
5
+
6
+ # Hedgehog Bootstrap
7
+
8
+ Scaffolds Project Bootstrap once per project, plus the enforcement config
9
+ that makes the stack and build order mechanically true rather than merely
10
+ documented. After this runs, `hedgehog-loop` takes over per module, one
11
+ step at a time. This skill touches no domain modules — no schema, no
12
+ contract, nothing under `libs/<module>/`. That's Phase A, started fresh
13
+ after Bootstrap closes.
14
+
15
+ Run the `nx g` commands below via nrwl's [nx-generate](https://github.com/nrwl/nx-ai-agents-config/tree/main/skills/nx-generate) skill — it dry-runs
16
+ and verifies generator flags against the installed Nx version. Run the
17
+ `nx run` / `nx affected` commands in Enforcement wiring via
18
+ [nx-run-tasks](https://github.com/nrwl/nx-ai-agents-config/tree/main/skills/nx-run-tasks) the same way. The commands below are the spec; those
19
+ skills execute it correctly.
20
+
21
+ ## The Stack (locked)
22
+
23
+ One opinionated stack, applied the same way on every project:
24
+
25
+ | Layer | Choice |
26
+ |---|---|
27
+ | Monorepo | Nx |
28
+ | Package manager | pnpm |
29
+ | Backend framework | NestJS |
30
+ | ORM | Drizzle (+ `drizzle-zod`) |
31
+ | Database | PostgreSQL |
32
+ | Platform | Railway |
33
+ | API contract | ts-rest |
34
+ | Validation | Zod |
35
+ | Auth | Better Auth (+ `@thallesp/nestjs-better-auth`, Drizzle adapter) |
36
+ | Data fetching / hooks | TanStack Query |
37
+ | Web UI | Next.js (frontend only) + ShadCN + Tailwind |
38
+ | Mobile UI (optional) | Expo + React Native Reusables + NativeWind |
39
+ | Queues / jobs | BullMQ + Redis |
40
+ | Logging | Pino (`nestjs-pino`) |
41
+ | Lint / format | ESLint (flat config) + Prettier (+ `prettier-plugin-tailwindcss`) |
42
+ | Testing | Vitest (unit/integration) + Playwright (web e2e) |
43
+ | Commits | Conventional Commits + commitlint + lefthook |
44
+ | Observability | Sentry |
45
+
46
+ Constraint-contingent substitutions: Prisma for Drizzle when the team
47
+ isn't SQL-comfortable; cloud + Pulumi/SST for Railway when full
48
+ declarative IaC is a hard requirement; tRPC for ts-rest when the client is
49
+ committed TypeScript-only.
50
+
51
+ ### Monorepo layout
52
+
53
+ ```
54
+ apps/
55
+ web (Next.js — UI only)
56
+ mobile (Expo — optional)
57
+ api (NestJS — owns all domain logic + DB access)
58
+ worker (BullMQ consumers)
59
+
60
+ packages/
61
+ db (Drizzle schema + client)
62
+ contracts (ts-rest + Zod contracts)
63
+ hooks (TanStack Query — shared web + mobile)
64
+ jobs (typed job registry / queue definitions)
65
+ auth (Better Auth config)
66
+ config (locked ESLint/Prettier/tsconfig/env schema)
67
+ shared (cross-cutting types + utils)
68
+
69
+ docs/
70
+ design (<module>.md per module — `ux-planner` agent output)
71
+ ```
72
+
73
+ `packages/auth` and `packages/jobs` are infra, built once, here — not
74
+ touched again per module. `docs/design` fills in per module during
75
+ Phase B; nothing to scaffold here beyond the empty directory.
76
+
77
+ ### Queues: seam in, usage deferred
78
+
79
+ The queue seam is a day-one standing default: Redis provisioned on
80
+ Railway, a `worker` app in the monorepo, a `Queue` port with a BullMQ
81
+ adapter (same pattern as repositories). Usage stays last-responsible-
82
+ moment: an operation goes async only when it genuinely needs to
83
+ (long-running work, retries, fan-out). Services don't know how their
84
+ results are returned — the enqueue-vs-await decision lives at the
85
+ application/controller layer. Workers are idempotent (at-least-once
86
+ delivery).
87
+
88
+ ## Before running
89
+
90
+ Confirm Intake already happened — a scope boundary and domain vocabulary
91
+ should exist (`planner` produces these). Bootstrap doesn't need the
92
+ vocabulary to scaffold infra, but starting before Intake signals work
93
+ getting ahead of itself. No scope boundary yet: stop and point to
94
+ `planner`.
95
+
96
+ Confirm this hasn't already run: check for an existing Nx workspace
97
+ (`nx.json` at repo root) or a prior Bootstrap commit
98
+ (`git log --grep="^feat(config)"`). Re-running Bootstrap against an
99
+ existing workspace is a Correction Protocol case (patch the specific
100
+ config step at its source, per `hedgehog-loop`), not a re-scaffold.
101
+
102
+ ## Steps (run in sequence, one commit per step)
103
+
104
+ ### 1. Nx workspace + `packages/config`
105
+
106
+ ```bash
107
+ npx create-nx-workspace@latest . --preset=ts --pm=pnpm --nxCloud=skip
108
+ ```
109
+
110
+ Scaffold `packages/config` as a plain `@nx/js` lib holding the locked,
111
+ shared files:
112
+
113
+ - `packages/config/eslint-base.js` — flat config, extended by every
114
+ app/lib. Include `@nx/enforce-module-boundaries` and `depConstraints`
115
+ from Enforcement wiring below, verbatim.
116
+ - `packages/config/prettier.js` — includes `prettier-plugin-tailwindcss`.
117
+ - `packages/config/env.schema.ts` — the Zod env schema from Enforcement
118
+ wiring below (`DATABASE_URL`, `BETTER_AUTH_SECRET`, `REDIS_URL`,
119
+ `NODE_ENV`; extend per project as new infra is added later).
120
+ - Root `eslint.config.js` extends `packages/config/eslint-base.js` and
121
+ declares the project tags table below (`scope:*`, `type:*`) as comments
122
+ or a lookup, so every later generator step tags its project correctly.
123
+
124
+ Commit: `feat(config): workspace + shared config`
125
+
126
+ ### 2. `packages/db` — Drizzle client + connection
127
+
128
+ ```bash
129
+ npx nx g @nx/js:lib packages/db --bundler=none --unitTestRunner=vitest
130
+ pnpm add drizzle-orm pg && pnpm add -D drizzle-kit drizzle-zod
131
+ ```
132
+
133
+ Wire a Postgres connection reading `DATABASE_URL` via `loadEnv()`
134
+ (step 1's env schema). No domain schema files yet — that's Phase A, per
135
+ module. Tag: `scope:db`, `type:adapter`.
136
+
137
+ Commit: `feat(db): drizzle client + connection`
138
+
139
+ ### 3. `packages/auth` — Better Auth config
140
+
141
+ ```bash
142
+ npx nx g @nx/js:lib packages/auth --bundler=none --unitTestRunner=vitest
143
+ pnpm add better-auth @thallesp/nestjs-better-auth
144
+ ```
145
+
146
+ Configure the Drizzle adapter against `packages/db`. Tag: `scope:auth`,
147
+ `type:adapter`.
148
+
149
+ Commit: `feat(auth): better auth config`
150
+
151
+ ### 4. `apps/api` — Nest app shell, global guard, Pino
152
+
153
+ ```bash
154
+ npx nx g @nx/nest:app apps/api
155
+ pnpm add nestjs-pino pino-http && pnpm add @thallesp/nestjs-better-auth
156
+ ```
157
+
158
+ Wire the global auth guard (secure-by-default) and `nestjs-pino` for
159
+ structured logging. Call `loadEnv()` at the top of `apps/api/src/main.ts`.
160
+ Tag: `scope:api`. No controllers beyond a health check — domain
161
+ controllers arrive per module in Phase A.
162
+
163
+ Commit: `feat(api): nest shell + global guard + pino`
164
+
165
+ ### 5. `apps/worker` — BullMQ seam (Redis, no consumers yet)
166
+
167
+ ```bash
168
+ npx nx g @nx/node:app apps/worker
169
+ pnpm add bullmq ioredis
170
+ ```
171
+
172
+ Provision the Redis connection and a `Queue` port shape (port + BullMQ
173
+ adapter, same pattern repositories use later) with no consumers — usage
174
+ is deferred (see Queues, above). Call `loadEnv()` at the top of
175
+ `apps/worker/src/main.ts`. Tag: `scope:worker`.
176
+
177
+ Commit: `feat(worker): bullmq seam, no consumers`
178
+
179
+ ### 6. `apps/web` — Next shell, TanStack Query provider, base theme
180
+
181
+ ```bash
182
+ npx nx g @nx/next:app apps/web
183
+ pnpm add @tanstack/react-query
184
+ pnpm dlx shadcn@latest init
185
+ ```
186
+
187
+ Wire the TanStack Query provider at the root layout. `shadcn init` writes
188
+ `apps/web`'s base theme (CSS variables for color, radius, light/dark
189
+ mode) — set the actual palette here, once, rather than leaving ShadCN's
190
+ placeholder values for `ui-builder` to inherit unnoticed on the first
191
+ screen. Light/dark mode toggle wiring belongs here too, not as a
192
+ per-screen decision later. No screens or hooks yet — Phase B doesn't
193
+ start until Phase A closes for at least one module. Tag: `scope:web`.
194
+
195
+ Commit: `feat(web): next shell + query provider + base theme`
196
+
197
+ ### 7. `apps/mobile` — Expo shell (only if mobile is in scope)
198
+
199
+ Skip entirely if mobile isn't in the scope boundary from Intake — don't
200
+ scaffold speculative infra.
201
+
202
+ ```bash
203
+ npx nx g @nx/expo:app apps/mobile
204
+ pnpm add react-native-reusables nativewind
205
+ ```
206
+
207
+ Configure NativeWind's theme (`tailwind.config.js` colors, light/dark) to
208
+ match `apps/web`'s base theme from step 6 — one visual identity across
209
+ platforms, set once here rather than drifting per-screen. Tag:
210
+ `scope:mobile`.
211
+
212
+ Commit: `feat(mobile): expo shell + base theme`
213
+
214
+ ## Enforcement wiring (within step 1, not a separate pass)
215
+
216
+ These are config *files*, not extra steps — write them as part of step 1
217
+ so the commit gate is live before step 2 starts. Every rule below is a
218
+ compiler error, lint failure, or blocked commit — this is what makes the
219
+ stack and build order mechanically true.
220
+
221
+ ### Nx module boundaries
222
+
223
+ Encodes "service imports only ports" as a build-time failure.
224
+ `@nx/enforce-module-boundaries` reasons at Nx-project granularity, so each
225
+ domain module's repository and service are their own Nx lib; `apps/api`
226
+ itself is wiring (controllers + module registration) importing those
227
+ libs. This makes cross-module isolation (FK-by-ID only, per
228
+ `hedgehog-loop`) mechanically true.
229
+
230
+ **Tags:**
231
+
232
+ ```
233
+ apps/api → scope:api
234
+ apps/worker → scope:worker
235
+ apps/web → scope:web
236
+ apps/mobile → scope:mobile
237
+ packages/db → scope:db, type:adapter
238
+ packages/contracts → scope:contracts, type:contract
239
+ packages/hooks → scope:hooks, type:hook
240
+ packages/auth → scope:auth, type:adapter
241
+ packages/shared → scope:shared, type:util
242
+ libs/<module>/port → scope:<module>, type:port
243
+ libs/<module>/repository → scope:<module>, type:adapter
244
+ libs/<module>/service → scope:<module>, type:service
245
+ ```
246
+
247
+ One `libs/<module>/` triplet per domain module (one table = one module) —
248
+ e.g. `libs/orders/port`, `libs/orders/repository`, `libs/orders/service`.
249
+
250
+ **Root `eslint.config.js` rule:**
251
+
252
+ ```js
253
+ '@nx/enforce-module-boundaries': ['error', {
254
+ depConstraints: [
255
+ // domain services never import adapters directly — only ports
256
+ { sourceTag: 'type:service', onlyDependOnLibsWithTags: ['type:port', 'type:util'] },
257
+ // web/mobile never import db or api internals — only contracts + hooks
258
+ { sourceTag: 'scope:web', onlyDependOnLibsWithTags: ['scope:contracts', 'scope:hooks', 'scope:shared'] },
259
+ { sourceTag: 'scope:mobile', onlyDependOnLibsWithTags: ['scope:contracts', 'scope:hooks', 'scope:shared'] },
260
+ // worker only reaches domain logic through ports, same as api
261
+ { sourceTag: 'scope:worker', onlyDependOnLibsWithTags: ['type:port', 'type:util', 'scope:shared'] },
262
+ ],
263
+ }],
264
+ ```
265
+
266
+ Building out of order (a controller before a service exists, a hook
267
+ reaching into `apps/api` directly) fails `nx lint`.
268
+
269
+ ### Commit gate (lefthook + commitlint)
270
+
271
+ Enforces one coherent change, tested, conventionally committed. Runs on
272
+ staged files only — fast regardless of repo size.
273
+
274
+ **`lefthook.yml`:**
275
+
276
+ ```yaml
277
+ pre-commit:
278
+ parallel: true
279
+ commands:
280
+ typecheck:
281
+ glob: "*.{ts,tsx}"
282
+ run: npx nx affected -t typecheck --files={staged_files}
283
+ lint:
284
+ glob: "*.{ts,tsx}"
285
+ run: npx nx affected -t lint --files={staged_files}
286
+ test:
287
+ glob: "*.{ts,tsx}"
288
+ run: npx nx affected -t test --files={staged_files}
289
+
290
+ commit-msg:
291
+ commands:
292
+ commitlint:
293
+ run: npx commitlint --edit {1}
294
+ ```
295
+
296
+ **`commitlint.config.js`:**
297
+
298
+ ```js
299
+ module.exports = {
300
+ extends: ['@commitlint/config-conventional'],
301
+ };
302
+ ```
303
+
304
+ Scope is open — a domain module (`orders`, `users`, ...) or an infra area
305
+ (`db`, `contracts`, `auth`, `hooks`, `api`, `worker`, `web`, `mobile`,
306
+ `config`). `@commitlint/config-conventional` validates type and subject
307
+ case; scope isn't restricted to a fixed list since new modules enter play
308
+ throughout a project's life.
309
+
310
+ A commit that fails typecheck, lint, or test does not happen. A commit
311
+ exists once it compiles and passes.
312
+
313
+ Run `pnpm dlx lefthook install` once `lefthook.yml` exists so the gate is
314
+ active from step 2 onward.
315
+
316
+ ### Env validation (fail fast)
317
+
318
+ Types-first extended to config. Boot fails immediately on a missing or
319
+ malformed env var.
320
+
321
+ **`packages/config/env.schema.ts`:**
322
+
323
+ ```ts
324
+ import { z } from 'zod';
325
+
326
+ export const envSchema = z.object({
327
+ DATABASE_URL: z.string().url(),
328
+ BETTER_AUTH_SECRET: z.string().min(32),
329
+ REDIS_URL: z.string().url(),
330
+ NODE_ENV: z.enum(['development', 'test', 'production']),
331
+ });
332
+
333
+ export type Env = z.infer<typeof envSchema>;
334
+
335
+ export function loadEnv(): Env {
336
+ const parsed = envSchema.safeParse(process.env);
337
+ if (!parsed.success) {
338
+ console.error(parsed.error.format());
339
+ process.exit(1);
340
+ }
341
+ return parsed.data;
342
+ }
343
+ ```
344
+
345
+ Called once, at the top of `apps/api/src/main.ts` and
346
+ `apps/worker/src/main.ts`.
347
+
348
+ ### Phase gate (CI)
349
+
350
+ Encodes "Phase A closes before Phase B opens" as a CI check. Blocks a PR
351
+ introducing a `feat(<module>): hooks` or `feat(<module>): screen-*` commit
352
+ for a module with no prior `feat(<module>): api` commit on the branch. Can
353
+ land in step 1's commit or as its own `feat(config): phase gate` commit.
354
+
355
+ **`.github/workflows/phase-gate.yml` (logic sketch):**
356
+
357
+ ```yaml
358
+ - name: Enforce Phase A before Phase B
359
+ run: |
360
+ node tools/phase-gate.js
361
+ ```
362
+
363
+ **`tools/phase-gate.js` (logic):**
364
+
365
+ ```
366
+ for each commit in PR:
367
+ if commit matches /^feat\(([a-z-]+)\): (hooks|screen-\w+)/:
368
+ module = capture group 1
369
+ fail unless a commit matching /^feat\(<module>\): api/ (module
370
+ substituted in) already exists on main or earlier in this branch
371
+ ```
372
+
373
+ ### Locked format/lint config
374
+
375
+ One shared config, extended everywhere:
376
+
377
+ - `packages/config/eslint-base.js` — flat config, extended by every
378
+ app/lib.
379
+ - `packages/config/prettier.js` — includes `prettier-plugin-tailwindcss`.
380
+
381
+ A per-app override request signals to fix the base config at the source.
382
+
383
+ ## After Bootstrap
384
+
385
+ Update `TODO.md`: check off every Bootstrap line now built, leave Phase
386
+ A/B sections as-is (per-module, filled in by `planner` during Intake or
387
+ when new scope enters play). Hand off to `hedgehog-loop` — from here,
388
+ every domain module goes through Phase A steps 1–5(a) one at a time,
389
+ gated by lefthook, each its own commit.
390
+
391
+ ## Constraints
392
+
393
+ - Run once per project. Not a per-module or per-feature tool.
394
+ - Don't scaffold `apps/mobile` unless mobile is explicitly in scope.
395
+ - Don't add domain schema, contracts, or any `libs/<module>/*` content —
396
+ that's Phase A, started after Bootstrap, one module at a time.
397
+ - Don't deviate from the package/library choices above. If a generator or
398
+ package name changed upstream since this was written, verify against
399
+ current docs before running the command — don't substitute a different
400
+ library.
401
+ - Each of the 7 steps is its own commit, in order — same unit-of-work
402
+ discipline as every other step in the discipline, even though this is
403
+ infra rather than a domain module.
@@ -0,0 +1,197 @@
1
+ ---
2
+ name: hedgehog-loop
3
+ description: Use for every unit of work once a Hedgehog project is bootstrapped — building one Order step (schema, contract, repository, service, controller, hook, screen), gating it, committing it, and checking it off TODO.md. Triggers on "next step", "build this module", "what's next", or the start of any work session on a bootstrapped project. Also covers the Correction Protocol for fixing a wrong upstream step.
4
+ ---
5
+
6
+ # Hedgehog Loop
7
+
8
+ The operating loop for a bootstrapped Hedgehog project: pick the next step,
9
+ build it, gate it, commit it, check it off. `TODO.md` at repo root is the
10
+ live list — read it before starting. It's thin: a context blurb plus a
11
+ checklist mirroring the phase/step structure below. Checked/unchecked is
12
+ its only state.
13
+
14
+ ## Determine phase
15
+
16
+ Before touching code, know which phase applies to the module in scope:
17
+
18
+ - **Phase A** — building/extending the backend. Every module in scope
19
+ needs schema → contract → repository → service → controller (→ queue)
20
+ before Phase B starts for any of them.
21
+ - **Phase B** — Phase A is closed for the module. Build hooks and screens.
22
+
23
+ Check `TODO.md`, or the commit log for `feat(<module>): api` commits. No
24
+ such commit means the module is in Phase A.
25
+
26
+ ## The Domain Module Pattern
27
+
28
+ A **domain module = one table.** `users`, `orders`, `order_items` are each
29
+ their own module, carrying the full step sequence below. The schema is the
30
+ source of truth for module boundaries.
31
+
32
+ **Cross-module references are FK-by-ID only.** If `orders.user_id`
33
+ references `users`, the `orders` schema holds a plain FK column. The
34
+ `orders` repository and service depend only on their own ports — a service
35
+ knows related entities only as an ID.
36
+
37
+ - Need the related row? Resolve it at the contract/controller layer
38
+ (parallel calls to each module's own endpoint), or join against the
39
+ other module's *schema* directly inside the repository (Drizzle query).
40
+ - This keeps every service importing only its own ports, so the Nx rule
41
+ `type:service → onlyDependOnLibsWithTags: ['type:port', 'type:util']`
42
+ holds uniformly (wired at bootstrap).
43
+
44
+ A junction table (e.g. `order_items`) is one table, one module, with two
45
+ FK-by-ID columns instead of one, each resolved the same way.
46
+
47
+ Every module goes through the same shape, in order:
48
+
49
+ ```
50
+ schema (Drizzle) — types before data
51
+ contract (Zod / ts-rest) — the boundary
52
+ repository (port + Drizzle adapter)
53
+ service (domain logic) — imports only ports
54
+ controller (thin HTTP)
55
+ hook (TanStack Query) — Phase B only
56
+ ```
57
+
58
+ Plus, when an operation needs async: **queue = port + BullMQ adapter**,
59
+ same port/adapter shape as the repository. The service imports only ports.
60
+
61
+ Standard Nx generators (`@nx/nest`, `@nx/next`, `@nx/expo`, `@nx/js`)
62
+ scaffold the app/lib shell. Each step's actual content (schema, contract,
63
+ repository, service, controller, hook) is hand-built, following this
64
+ sequence.
65
+
66
+ ## Domain Module — Backend Steps (Phase A, every module in scope)
67
+
68
+ A horizontal pass across the whole backend — every module goes through
69
+ these before any module gets a hook or screen.
70
+
71
+ | # | Step | Lives in | Commit |
72
+ |---|---|---|---|
73
+ | 1 | Schema | `packages/db` (Drizzle) | `feat(<module>): schema` |
74
+ | 2 | Contract | `packages/contracts` (Zod via `drizzle-zod` + ts-rest) | `feat(<module>): contract` |
75
+ | 3 | Repository | `libs/<module>/repository` (port + Drizzle adapter) | `feat(<module>): repository` |
76
+ | 4 | Service | `libs/<module>/service` (domain logic — imports only ports) | `feat(<module>): service` |
77
+ | 5 | Controller | `apps/api` (thin HTTP, wires contract → service) | `feat(<module>): api` |
78
+ | 5a | Queue *(if needed)* | `apps/worker` (port + BullMQ adapter) | `feat(<module>): queue` |
79
+
80
+ Repeat 1–5(a) per module in scope. The API is complete, typed, and
81
+ callable (Postman/curl/contract tests) before frontend work starts.
82
+
83
+ ## Domain Module — Frontend Steps (Phase B, after Phase A closes for the module)
84
+
85
+ | # | Step | Lives in | Commit |
86
+ |---|---|---|---|
87
+ | 6 | Hook | `packages/hooks` (TanStack Query) | `feat(<module>): hooks` |
88
+ | 6a | UX rationale | `docs/design/<module>.md`, `ux-planner` agent | bundled into step 7's commit |
89
+ | 7 | Screen | `apps/web` and/or `apps/mobile` | `feat(<module>): screen-web` / `feat(<module>): screen-mobile` |
90
+
91
+ Phase B starts once Phase A is done for the scope. The frontend is a pure
92
+ consumer of an already-finished API. Step 6a is where "how it should feel"
93
+ gets decided — once per module, after the hook exists and before
94
+ `ui-builder` starts the screen — via `ux-planner`, starting from whatever
95
+ `planner` filed in `docs/design/<module>-notes.md` at Intake. Its first run
96
+ for a module also signals to the user that Phase B has started, and is the
97
+ point a mockup, screenshot, or export (Google Stitch, Figma) can be handed
98
+ over. It writes `docs/design/<module>.md`, not its own step commit;
99
+ `TODO.md` tracks only hooks/screen-web/screen-mobile per module.
100
+
101
+ ## The Loop (every unit of work)
102
+
103
+ 1. **Pick the next step** per the tables above, from `TODO.md`. One step
104
+ at a time, in order.
105
+ 2. **Check the gate.** The prior step compiles and passes tests first.
106
+ 3. **Build exactly one step.** One schema, one contract, one repository.
107
+ 4. **Run the gate on your own work**: typecheck, lint, test (mirrors
108
+ lefthook, wired at bootstrap).
109
+ 5. **Commit** using the exact Conventional Commit format above.
110
+ 6. **Check off the line in `TODO.md`.**
111
+ 7. **Repeat.**
112
+
113
+ Each commit batches exactly one step, built right for what's known now; a
114
+ wrong step is fixed forward later via the Correction Protocol.
115
+
116
+ ## Intra-step conventions
117
+
118
+ The Nx boundaries, phase gate, and lint own the *structural* rules
119
+ (what imports what, what gets built when). These are the conventions
120
+ *inside* a step that those gates can't see — apply them uniformly so a
121
+ fresh-context session builds module N the same way it built module 1. The
122
+ `reviewer` agent checks these at a phase boundary.
123
+
124
+ - **Errors are thrown, typed, and domain-named.** A service throws a
125
+ domain error (`OrderNotFoundError`, not a bare `Error` or an HTTP
126
+ exception) — services don't know they're behind HTTP. The controller is
127
+ the only layer that maps domain errors to status codes. Never return
128
+ `null`/`undefined` to signal a failure a caller must branch on.
129
+ - **Repository not-found returns `undefined`; the service decides.** A
130
+ `findById` that misses returns `undefined` (a plain absence, not an
131
+ error); the service turns that into a thrown domain error when the
132
+ operation requires the row. Adapters don't throw domain errors — they
133
+ report absence, the service interprets it.
134
+ - **Validation lives at the contract boundary, once.** Input is
135
+ Zod-validated at the controller via the ts-rest contract. Past that
136
+ boundary, types are trusted — services and repositories don't re-parse.
137
+ A service-level invariant that isn't expressible in the Zod schema
138
+ (e.g. "can't cancel after payment") is enforced in the service as a
139
+ thrown domain error, not a second validation pass.
140
+ - **Multi-write operations are transactional.** A service method that
141
+ writes more than once wraps the writes in one Drizzle transaction,
142
+ passed through the port — partial writes never escape a failed
143
+ operation.
144
+ - **Services are pure domain logic.** No logging, no HTTP, no queue
145
+ mechanics inside a service method — those live at the controller /
146
+ adapter edge. A service reads as the business rule and nothing else.
147
+
148
+ ## Correction Protocol
149
+
150
+ When a downstream step reveals an upstream step was wrong:
151
+
152
+ 1. Stop.
153
+ 2. Patch the upstream step directly, in place.
154
+ 3. Fast-forward every dependent step that breaks, each its own small
155
+ commit.
156
+ 4. The commit messages are the explanation.
157
+ 5. Resume the loop.
158
+
159
+ Use `conventional-commits` when a correction touches several steps in one
160
+ working-tree pass and needs splitting back into per-step commits.
161
+
162
+ ## Phase Transition Checks
163
+
164
+ Before starting Phase B for a module, confirm:
165
+
166
+ - A `feat(<module>): api` commit exists for that module.
167
+ - The contract is callable and typed (contract tests pass).
168
+
169
+ Use the `reviewer` agent for this — it checks what the mechanical gate
170
+ can't (port discipline, FK-by-ID discipline, contract shape).
171
+
172
+ Before starting Phase A for a module, confirm it's inside the stated scope
173
+ boundary from Intake (`planner`). If not, stop and ask.
174
+
175
+ ## Rules
176
+
177
+ - **Phase A closes before Phase B opens.** Every module in scope has a
178
+ working, tested API before any hook or screen starts.
179
+ - **Sequential within a phase.** A step starts once the one before it
180
+ compiles and passes tests.
181
+ - **Step 5a is conditional** — only when an operation genuinely needs
182
+ async (long-running, retries, fan-out); the normal case has no queue.
183
+ - **A wrong step gets fixed at its source** — the Correction Protocol, not
184
+ a downstream workaround.
185
+ - **Tests gate every commit** in the sequence.
186
+ - A module's frontend code (hook, screen) is built after its API is
187
+ committed.
188
+ - The screen step doesn't start blank — `ux-planner` runs once per module,
189
+ after the hook is committed, before `ui-builder` starts the screen.
190
+ - `packages/config` is the single source for shared config; a per-app
191
+ override request signals to fix the base config at the source.
192
+
193
+ ## Stop Condition
194
+
195
+ A build session ends when every module in scope has completed both Phase
196
+ A and Phase B, or when scope is ambiguous enough that continuing means
197
+ guessing — ask one question and wait.