@skyf0xx/hedgehog-core-full-stack-app 1.0.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.
Files changed (137) hide show
  1. package/CLAUDE.core.md +147 -0
  2. package/LICENSE +21 -0
  3. package/README.md +41 -0
  4. package/agents/backend-eng.md +189 -0
  5. package/agents/front-end-eng.md +176 -0
  6. package/agents/ux-planner.md +174 -0
  7. package/hedgehog-core.yaml +20 -0
  8. package/package.json +41 -0
  9. package/skills/hedgehog-bootstrap/SKILL.md +324 -0
  10. package/skills/hedgehog-bootstrap-full-stack-app-core/SKILL.md +411 -0
  11. package/skills/hedgehog-loop/SKILL.md +643 -0
  12. package/skills/link-workspace-packages/SKILL.md +41 -0
  13. package/skills/nx-generate/SKILL.md +165 -0
  14. package/skills/nx-run-tasks/SKILL.md +52 -0
  15. package/skills/nx-workspace/SKILL.md +261 -0
  16. package/skills/nx-workspace/references/AFFECTED.md +27 -0
  17. package/vendor-skills/GSAP/ATTRIBUTION.md +30 -0
  18. package/vendor-skills/GSAP/LICENSE +21 -0
  19. package/vendor-skills/GSAP/gsap-core/SKILL.md +254 -0
  20. package/vendor-skills/GSAP/gsap-frameworks/SKILL.md +266 -0
  21. package/vendor-skills/GSAP/gsap-performance/SKILL.md +79 -0
  22. package/vendor-skills/GSAP/gsap-plugins/SKILL.md +433 -0
  23. package/vendor-skills/GSAP/gsap-react/SKILL.md +136 -0
  24. package/vendor-skills/GSAP/gsap-scrolltrigger/SKILL.md +296 -0
  25. package/vendor-skills/GSAP/gsap-timeline/SKILL.md +107 -0
  26. package/vendor-skills/GSAP/gsap-utils/SKILL.md +284 -0
  27. package/vendor-skills/GSAP/llms.txt +39 -0
  28. package/workspace/.env.example +5 -0
  29. package/workspace/.github/workflows/phase-gate.yml +17 -0
  30. package/workspace/.prettierignore +5 -0
  31. package/workspace/.prettierrc +3 -0
  32. package/workspace/.vscode/extensions.json +3 -0
  33. package/workspace/apps/api/eslint.config.mjs +3 -0
  34. package/workspace/apps/api/package.json +117 -0
  35. package/workspace/apps/api/src/app/app.module.spec.ts +15 -0
  36. package/workspace/apps/api/src/app/app.module.ts +15 -0
  37. package/workspace/apps/api/src/app/feature-modules.ts +8 -0
  38. package/workspace/apps/api/src/app/health.controller.ts +9 -0
  39. package/workspace/apps/api/src/assets/.gitkeep +0 -0
  40. package/workspace/apps/api/src/main.ts +32 -0
  41. package/workspace/apps/api/tsconfig.app.json +22 -0
  42. package/workspace/apps/api/tsconfig.json +13 -0
  43. package/workspace/apps/api/tsconfig.spec.json +36 -0
  44. package/workspace/apps/api/vitest.config.mts +18 -0
  45. package/workspace/apps/api/webpack.config.cjs +25 -0
  46. package/workspace/apps/api-e2e/eslint.config.mjs +3 -0
  47. package/workspace/apps/api-e2e/package.json +28 -0
  48. package/workspace/apps/api-e2e/src/api/api.spec.ts +11 -0
  49. package/workspace/apps/api-e2e/src/support/global-setup.ts +17 -0
  50. package/workspace/apps/api-e2e/src/support/test-setup.ts +6 -0
  51. package/workspace/apps/api-e2e/tsconfig.json +10 -0
  52. package/workspace/apps/api-e2e/tsconfig.spec.json +28 -0
  53. package/workspace/apps/api-e2e/vitest.config.mts +20 -0
  54. package/workspace/apps/web/.env.example +12 -0
  55. package/workspace/apps/web/.prettierrc.js +10 -0
  56. package/workspace/apps/web/.swcrc +30 -0
  57. package/workspace/apps/web/components.json +21 -0
  58. package/workspace/apps/web/eslint.config.mjs +12 -0
  59. package/workspace/apps/web/index.d.ts +7 -0
  60. package/workspace/apps/web/next-env.d.ts +7 -0
  61. package/workspace/apps/web/next.config.js +9 -0
  62. package/workspace/apps/web/package.json +38 -0
  63. package/workspace/apps/web/postcss.config.mjs +7 -0
  64. package/workspace/apps/web/public/.gitkeep +0 -0
  65. package/workspace/apps/web/public/favicon.ico +0 -0
  66. package/workspace/apps/web/src/app/global.css +83 -0
  67. package/workspace/apps/web/src/app/layout.tsx +34 -0
  68. package/workspace/apps/web/src/app/module-routes.ts +13 -0
  69. package/workspace/apps/web/src/app/page.tsx +34 -0
  70. package/workspace/apps/web/src/app/providers.tsx +12 -0
  71. package/workspace/apps/web/src/components/theme-toggle.spec.tsx +20 -0
  72. package/workspace/apps/web/src/components/theme-toggle.tsx +32 -0
  73. package/workspace/apps/web/src/components/ui/button.tsx +60 -0
  74. package/workspace/apps/web/src/lib/utils.ts +6 -0
  75. package/workspace/apps/web/src/test-setup.ts +1 -0
  76. package/workspace/apps/web/tsconfig.json +62 -0
  77. package/workspace/apps/web/tsconfig.spec.json +37 -0
  78. package/workspace/apps/web/vitest.config.mts +27 -0
  79. package/workspace/apps/web-e2e/eslint.config.mjs +12 -0
  80. package/workspace/apps/web-e2e/package.json +10 -0
  81. package/workspace/apps/web-e2e/playwright.config.mts +75 -0
  82. package/workspace/apps/web-e2e/src/example.spec.ts +7 -0
  83. package/workspace/apps/web-e2e/tsconfig.json +26 -0
  84. package/workspace/commitlint.config.cjs +3 -0
  85. package/workspace/core.yaml +68 -0
  86. package/workspace/docker-compose.yml +14 -0
  87. package/workspace/eslint.config.mjs +15 -0
  88. package/workspace/gitignore.template +24 -0
  89. package/workspace/lefthook.yml +52 -0
  90. package/workspace/nx.json +90 -0
  91. package/workspace/package.json +99 -0
  92. package/workspace/packages/config/eslint-base.js +205 -0
  93. package/workspace/packages/config/package.json +28 -0
  94. package/workspace/packages/config/prettier.js +13 -0
  95. package/workspace/packages/config/src/env.schema.spec.ts +38 -0
  96. package/workspace/packages/config/src/env.schema.ts +18 -0
  97. package/workspace/packages/config/src/index.ts +1 -0
  98. package/workspace/packages/config/tsconfig.json +13 -0
  99. package/workspace/packages/config/tsconfig.lib.json +27 -0
  100. package/workspace/packages/config/tsconfig.spec.json +34 -0
  101. package/workspace/packages/config/vitest.config.mts +18 -0
  102. package/workspace/packages/db/package.json +27 -0
  103. package/workspace/packages/db/src/index.ts +2 -0
  104. package/workspace/packages/db/src/lib/db.spec.ts +48 -0
  105. package/workspace/packages/db/src/lib/db.ts +54 -0
  106. package/workspace/packages/db/src/schema/index.ts +4 -0
  107. package/workspace/packages/db/tsconfig.json +13 -0
  108. package/workspace/packages/db/tsconfig.lib.json +27 -0
  109. package/workspace/packages/db/tsconfig.spec.json +34 -0
  110. package/workspace/packages/db/vitest.config.mts +18 -0
  111. package/workspace/pnpm-lock.yaml +23719 -0
  112. package/workspace/pnpm-workspace.yaml +7 -0
  113. package/workspace/tools/generate-feature-modules.cjs +104 -0
  114. package/workspace/tools/generate-module-routes.cjs +89 -0
  115. package/workspace/tools/generators/contract/generator.ts +336 -0
  116. package/workspace/tools/generators/contract/schema.json +24 -0
  117. package/workspace/tools/generators/controller/generator.ts +390 -0
  118. package/workspace/tools/generators/controller/schema.json +24 -0
  119. package/workspace/tools/generators/fields.ts +160 -0
  120. package/workspace/tools/generators/generators.json +42 -0
  121. package/workspace/tools/generators/hook/generator.ts +291 -0
  122. package/workspace/tools/generators/hook/schema.json +19 -0
  123. package/workspace/tools/generators/lib-shell.ts +124 -0
  124. package/workspace/tools/generators/naming.ts +84 -0
  125. package/workspace/tools/generators/package.json +6 -0
  126. package/workspace/tools/generators/repository/generator.ts +298 -0
  127. package/workspace/tools/generators/repository/schema.json +15 -0
  128. package/workspace/tools/generators/schema/generator.ts +169 -0
  129. package/workspace/tools/generators/schema/schema.json +20 -0
  130. package/workspace/tools/generators/screen/generator.ts +218 -0
  131. package/workspace/tools/generators/screen/schema.json +15 -0
  132. package/workspace/tools/generators/service/generator.ts +270 -0
  133. package/workspace/tools/generators/service/schema.json +19 -0
  134. package/workspace/tools/phase-gate.cjs +77 -0
  135. package/workspace/tsconfig.base.json +21 -0
  136. package/workspace/tsconfig.json +25 -0
  137. package/workspace/vitest.workspace.ts +4 -0
@@ -0,0 +1,411 @@
1
+ ---
2
+ name: hedgehog-bootstrap-full-stack-app-core
3
+ description: Use once, at the start of a new Hedgehog project on the full-stack-app core, before hedgehog-bootstrap's add-on steps, to land the full-stack-app core workspace (Nx, packages/config, packages/db, apps/api, apps/web, and every enforcement file) and verify it's green. Runs as the first move of the `bootstrap` agent, which `planner` invokes automatically after Confirm & Lock. No per-project decisions, no add-on awareness.
4
+ ---
5
+
6
+ # Hedgehog Bootstrap — full-stack-app Core
7
+
8
+ Lands the always-on core of a Hedgehog project — the same four pieces on
9
+ every project, regardless of size or which add-ons are on — by copying a
10
+ pre-built, pre-verified workspace (this package's `workspace/`) rather
11
+ than generating it live: Nx workspace + enforcement
12
+ config, `packages/db`, `apps/api`, `apps/web`. These pieces are
13
+ deterministic — the same commands produce the same output on every
14
+ project — so the output is committed once, upstream, and copied here
15
+ instead of re-derived by an agent on every run. `hedgehog-bootstrap`
16
+ calls this skill first, unconditionally, then continues with its own
17
+ add-on steps (Auth, Queue, Mobile) — those genuinely vary per project
18
+ and stay live.
19
+
20
+ This skill has no per-project decisions to make: no Add-ons dependency,
21
+ no Add-ons check, nothing to ask. Core is identical on every Hedgehog
22
+ project.
23
+
24
+ ## What lands
25
+
26
+ Everything under this package's `workspace/`, copied to the repo root:
27
+
28
+ - Root: `nx.json`, `pnpm-workspace.yaml`, `package.json` (with the
29
+ `pnpm.overrides.esbuild` pin, `packageManager` field, and a `dev`
30
+ script — `docker compose up -d && nx run-many --target=serve,dev
31
+ --projects=api,web --parallel` — so a fresh clone has one command that
32
+ brings up Postgres and both apps together), `eslint.config.mjs`,
33
+ `docker-compose.yml` (Postgres only — Redis joins later, only if the
34
+ Queue add-on turns on), `.env.example`
35
+ (`DATABASE_URL`/`NODE_ENV`/`WEB_ORIGIN`, copied to `.env` in step 4),
36
+ `lefthook.yml`, `commitlint.config.cjs`,
37
+ `tools/phase-gate.cjs`, `.github/workflows/phase-gate.yml`,
38
+ `tsconfig.base.json`, `pnpm-lock.yaml`, and `core.yaml` — the shipped
39
+ layer sequence `hedgehog plan`/`verify`/`next` read for this project.
40
+ This root `core.yaml` is a different file from `.hedgehog/core.yaml`,
41
+ which only exists on an authored core (see `hedgehog-core-design`) —
42
+ the two never coexist on the same project.
43
+ - `packages/config/` — `eslint-base.js`, `prettier.js` (no
44
+ `prettier-plugin-tailwindcss` — that's `apps/web`'s own config, already
45
+ wired), `env.schema.ts` (core fields only: `DATABASE_URL`, `NODE_ENV`,
46
+ and `WEB_ORIGIN` — the origin `apps/api` enables CORS for, defaulted to
47
+ `apps/web`'s dev origin). Tagged `type:util`.
48
+ - `packages/db/` — Drizzle client/connection wired to `loadEnv()`, no
49
+ domain schema. `src/index.ts` re-exports `src/schema/index.ts`, an
50
+ empty barrel-of-barrels a module's `schema` layer adds one re-export
51
+ line to (in that layer's scope — see `core.yaml`). Tagged `scope:db`,
52
+ `type:adapter`.
53
+ - `apps/api/` — Nest shell, `nestjs-pino` wired, CORS enabled for
54
+ `WEB_ORIGIN`, health check only, no domain controllers. Vitest wired
55
+ (`vitest.config.mts`, `tsconfig.spec.json` with
56
+ `experimentalDecorators`/`emitDecoratorMetadata` for `Test.createTestingModule`),
57
+ with a smoke test (`app.module.spec.ts`) instantiating `AppModule`.
58
+ `apps/api/src/app/feature-modules.ts` is a generated barrel — see
59
+ **The controller barrel** below — that `AppModule` imports and spreads
60
+ into its `imports`, so no domain module's controller layer ever edits
61
+ `app.module.ts`. Depends on `packages/db` (the controller layer imports
62
+ it by construction). `apps/api-e2e` already converted to Vitest with an
63
+ explicit `e2e` target. Tagged `scope:api`.
64
+ - `apps/web/` — `.env.example` (`NEXT_PUBLIC_API_BASE_URL`, copied to
65
+ `apps/web/.env.local` in step 4 — Next loads env files from the app
66
+ directory, so the root `.env` never reaches it; the value carries
67
+ `apps/api`'s `/api` global prefix), Next shell, Tailwind v4 (PostCSS
68
+ plugin, no `tailwind.config.js`), hand-built ShadCN base (`components.json`,
69
+ `cn()` util, CSS variable theme, light/dark toggle via an inline
70
+ pre-hydration script + a client-side `ThemeToggle`), TanStack Query
71
+ provider at the root layout, `prettier-plugin-tailwindcss` scoped to
72
+ its own `.prettierrc.js`. Vitest wired for jsdom (`vitest.config.mts`
73
+ with the `@vitejs/plugin-react` plugin and the `@/*` -> `./src/*` alias
74
+ apps/web/tsconfig.json already declares, `tsconfig.spec.json`,
75
+ `src/test-setup.ts` loading `@testing-library/jest-dom`'s matchers),
76
+ with a smoke test (`theme-toggle.spec.tsx`) rendering and clicking
77
+ `ThemeToggle` via Testing Library. Tagged `scope:web`. `apps/web-e2e`
78
+ (Playwright, scaffolded automatically by `@nx/next:app`) gets its own
79
+ `e2e` target by default — no rename needed, unlike `apps/api-e2e`.
80
+ - The full `@nx/enforce-module-boundaries` `depConstraints` list for
81
+ exactly these tags, matching the project shape `core.yaml`'s layer
82
+ sequence actually produces — plus the `no-restricted-imports` rules that
83
+ carry port discipline, which a tag constraint can't express because a
84
+ module's port interface and its Drizzle adapter share one lib
85
+ (`libs/<module>/repository`). `enforceBuildableLibDependency` is off:
86
+ `packages/contracts` and `packages/hooks` are source-only by design, and
87
+ it fails `apps/web` for importing either. `scope:auth`/`scope:worker`/
88
+ `scope:mobile` entries are added live by the matching add-on step in
89
+ `hedgehog-bootstrap`, only if that add-on turns on.
90
+ - The `@nx/js/typescript` plugin registration (`typecheck` target) with
91
+ `composite`/`declaration` already set on every `tsconfig.lib.json` /
92
+ `tsconfig.app.json` / `tsconfig.spec.json` this core touches.
93
+
94
+ `node_modules` is not part of the copy — `pnpm install` regenerates it
95
+ from the committed `pnpm-lock.yaml`, which is a fast resolve against a
96
+ locked graph, not a fresh solve.
97
+
98
+ ## The controller barrel
99
+
100
+ `apps/api/src/app/app.module.ts` never takes a per-module edit.
101
+ `core.yaml`'s controller layer scope is `apps/api/src/app/{module}/**` —
102
+ module-disjoint by construction, so two modules' controller tasks never
103
+ touch the same file — but `app.module.ts` itself sits outside every
104
+ module's scope, and `src/db/core.mjs`'s `validateCore` rejects a
105
+ non-exclusive layer whose scope omits `{module}` on a module-axis core,
106
+ so the controller layer's scope can't be widened to include it either. A
107
+ file two concurrent module builds both hand-edited would also be
108
+ invisible to the scheduler's conflict check (`src/db/conflict.mjs` only
109
+ compares each task's own declared scope globs), so even a single shared
110
+ line to append to would race undetected.
111
+
112
+ `apps/api/src/app/feature-modules.ts` solves this by never being
113
+ hand-edited at all: `tools/generate-feature-modules.cjs` globs
114
+ `apps/api/src/app/*/*.module.ts` and writes it as a generated barrel — a
115
+ literal `import { XModule } from './x/x.module'` per domain module found,
116
+ plus an exported `featureModules` array — every time the
117
+ `generate-feature-modules` Nx target runs. `AppModule` imports that one
118
+ generated file and spreads `featureModules` into its own `imports`. A
119
+ module's controller layer only ever creates its own `{module}.module.ts`
120
+ inside its own `apps/api/src/app/{module}/` directory — always in scope,
121
+ never colliding with any other module's controller task. In the shipped
122
+ core, no domain module exists yet, so the glob finds nothing and
123
+ `feature-modules.ts` exports an empty array.
124
+
125
+ `generate-feature-modules` is wired as an explicit Nx target on `apps/api`
126
+ (`apps/api/package.json`'s `nx.targets`), cached, with `build` declaring
127
+ it as a `dependsOn`; `test` and `typecheck` pick it up the same way
128
+ through `nx.json`'s `targetDefaults` (a project with no matching target,
129
+ such as `db` or `web`, silently skips a missing `dependsOn` entry rather
130
+ than failing). Static imports rather than a runtime directory scan:
131
+ `apps/api` builds through `NxAppWebpackPlugin`, which bundles by
132
+ statically walking `main.ts`'s import graph — a file never reached by a
133
+ static `import`/`require` is dropped from the bundle entirely, so a
134
+ runtime `fs.readdirSync` scan for sibling files would find nothing in the
135
+ built output even though the same scan works when Vitest runs the same
136
+ source directly. Generating literal imports keeps both paths identical.
137
+
138
+ ## The route index
139
+
140
+ `apps/web/src/app/page.tsx` is the same category of file on the web side,
141
+ and gets the same answer. The screen layer's scope is
142
+ `apps/web/src/app/{module}/**`, module-disjoint and — by the same
143
+ `validateCore` rule — unable to be widened to reach one level up, so the
144
+ root page is outside every layer's scope. Left to itself it can never
145
+ learn about the modules built beneath it, and a finished build ships a
146
+ landing page with no route into the app it just built.
147
+
148
+ `tools/generate-module-routes.cjs` globs `apps/web/src/app/*/page.tsx`
149
+ and writes `apps/web/src/app/module-routes.ts` — an exported
150
+ `moduleRoutes` array of `{ href, label }`, one per route directory found,
151
+ skipping Next.js route groups (`(group)`) and `_`/`.`-prefixed
152
+ directories, which are not routes. The root page imports that one
153
+ generated file and renders a link per entry, or a short "no modules yet"
154
+ line when the array is empty, which is what the shipped core has. A
155
+ module's screen layer only ever creates its own `page.tsx` inside its own
156
+ `apps/web/src/app/{module}/` directory — always in scope — and nothing
157
+ ever hand-edits the root page or the generated file.
158
+
159
+ `generate-module-routes` is wired as an explicit Nx target on `apps/web`
160
+ (`apps/web/package.json`'s `nx.targets`), cached, with `build`, `dev`,
161
+ `test` and `typecheck` picking it up through `nx.json`'s `targetDefaults`
162
+ the same way its API sibling does.
163
+
164
+ ## Steps
165
+
166
+ ### 1. Confirm this hasn't already run
167
+
168
+ Check for `nx.json` at the repo root, or a prior
169
+ `feat(config): workspace + shared config` commit
170
+ (`git log --grep="^feat(config): workspace"`). Either means core already
171
+ landed — stop, don't re-copy. If something about the landed core seems
172
+ wrong, that's a Correction Protocol case (`hedgehog-loop`), not a
173
+ re-copy: patch the specific file at its source.
174
+
175
+ ### 2. Confirm Docker is available
176
+
177
+ `docker --version`. Core's `docker-compose.yml` (Postgres) needs it
178
+ immediately after landing, on every host OS — macOS, Windows, Linux
179
+ alike. No Docker: stop and point to installing Docker Desktop
180
+ (macOS/Windows) or Docker Engine (Linux) rather than falling back to a
181
+ natively-installed Postgres. See **Local infra: Docker, always** below.
182
+
183
+ ### 3. Land this package's `workspace/`
184
+
185
+ `hedgehog init --ts-full-stack-app` copies this package's `workspace/`
186
+ to the repo root at install time, the same way it copies the engine's
187
+ `src/agents` to this host's own agents directory — check whether the
188
+ core files are already present (same check as step 1) before copying
189
+ again. On a project that ran plain `init` (no core flag) and only
190
+ reaches `full-stack-app` because `planner` picked it at Phase 0, this
191
+ hasn't happened yet: copy `workspace/`'s contents to the repo root now.
192
+ Also merge this core's `CLAUDE.md` section into root `CLAUDE.md`
193
+ (this package's `CLAUDE.core.md` fills the shell's `{{CORE_SECTION}}`
194
+ placeholder left unfilled by a deferred install) — skip this if the
195
+ section is already filled. Either way, by the end of this step every
196
+ file listed in "What lands" above should be on disk.
197
+
198
+ ### 4. Install and start local infra
199
+
200
+ ```bash
201
+ pnpm install
202
+ cp .env.example .env
203
+ cp apps/web/.env.example apps/web/.env.local
204
+ docker compose up -d
205
+ ```
206
+
207
+ Two env files, because the two apps load env from different places. The
208
+ root `.env` is `apps/api`'s: gitignored and never shipped, with
209
+ `.env.example` as the committed template (`DATABASE_URL` matching
210
+ `docker-compose.yml`'s Postgres credentials, `NODE_ENV=development`,
211
+ `WEB_ORIGIN`). Skipping that copy means `packages/config`'s `loadEnv()`
212
+ fails its Zod check the moment `apps/api` boots (`DATABASE_URL` missing) —
213
+ a confusing crash to debug live instead of one line here.
214
+
215
+ `apps/web/.env.local` is Next's, loaded from the app directory rather than
216
+ the workspace root, so nothing in the root file reaches the browser.
217
+ Skipping that copy is worse than a crash: `NEXT_PUBLIC_API_BASE_URL` comes
218
+ back `undefined`, the ts-rest client builds a relative URL, and every hook
219
+ 404s against Next's own dev server rather than the api — a routing bug in
220
+ appearance, an env bug in fact.
221
+
222
+ `pnpm install` resolves against the committed `pnpm-lock.yaml` — this
223
+ should be fast and produce no lockfile changes. A lockfile diff here
224
+ means the shipped `pnpm-lock.yaml` doesn't match `package.json` — that's
225
+ a packaging bug in this package's `workspace/`, not something to patch
226
+ locally (see **If verification fails**, below).
227
+
228
+ ### 5. Verify
229
+
230
+ ```bash
231
+ npx nx run-many -t typecheck,lint,test
232
+ npx nx format:write
233
+ ```
234
+
235
+ Both must be clean: 0 errors, 0 warnings from the first command; no
236
+ diff produced by the second. This is the one live check that replaces
237
+ four separate live generate-and-verify passes — core isn't proven
238
+ correct by trusting the copy, it's proven correct by actually running
239
+ the same gate every other step in this discipline runs.
240
+
241
+ The commit gate is already active by this point: `lefthook.yml` came
242
+ with the copy, and lefthook's own postinstall ran `lefthook install -f`
243
+ during step 4's `pnpm install`. Run `pnpm dlx lefthook install` only if
244
+ `.git/hooks/pre-commit` is somehow absent.
245
+
246
+ Confirm the gate is real rather than assuming it, with:
247
+
248
+ ```bash
249
+ hedgehog status
250
+ ```
251
+
252
+ Its `COMMIT GATE` line reads `active` only when the hooks exist, were
253
+ generated with `lefthook.yml`'s `assert_lefthook_installed: true`, and
254
+ the lefthook binary actually resolves. Anything else is a gate that
255
+ isn't enforcing, and the line names the repair. Two failure modes it
256
+ catches that nothing else does: hooks generated before that flag
257
+ existed (they fail open — regenerate with `pnpm dlx lefthook install`),
258
+ and a `node_modules` that has since been cleared (`pnpm install`).
259
+
260
+ Also check the hook resolves to the project's pinned local lefthook
261
+ (`lefthook version` during a commit, or that `.git/hooks/` resolves
262
+ into `node_modules/`) rather than a global Homebrew-installed shadow.
263
+
264
+ ### 6. Commit
265
+
266
+ ```
267
+ feat(config): workspace + shared config
268
+ ```
269
+
270
+ One commit for all of core, landed as a verified copy. That commit
271
+ existing is the record that core landed — `bootstrap` checks for it via
272
+ the commit log, not a checklist line.
273
+
274
+ ## Known issues baked into the full-stack-app core
275
+
276
+ These are already fixed in the committed tree — listed here so anyone
277
+ regenerating `workspace/` (see `scripts/regenerate-full-stack-app-core.sh`
278
+ in this package) knows why the tree looks the way it does, and doesn't
279
+ reintroduce the bug by "cleaning up" what looks like an unnecessary pin
280
+ or directive.
281
+
282
+ - **`tsconfig.base.json` uses `esnext`/`bundler` module resolution, not
283
+ `nodenext`.** Turbopack (Next 16's dev bundler) can't resolve a
284
+ `nodenext`-style `.js`-extension relative import specifier (`export *
285
+ from './tasks/index.js'` where only `index.ts` exists) across a pnpm
286
+ workspace-package boundary — `apps/web` importing a runtime value
287
+ (not just a type) from `packages/hooks`, `packages/contracts`, or
288
+ `packages/db` 500s with `Module not found`. `bundler` is TypeScript's
289
+ mode for exactly this case and is Next's own tsconfig default; nothing
290
+ in this stack runs raw `tsc` output directly with Node (`apps/api`
291
+ builds via `webpack-cli`, `apps/web` via Next/Turbopack, tests via
292
+ Vitest), so `nodenext`'s stricter runtime-accurate extension
293
+ enforcement isn't load-bearing anywhere here. Every relative import
294
+ specifier across the template is extensionless accordingly — a
295
+ `schema`/`contract`/`hook` layer adding a new barrel re-export must
296
+ match (`export * from './tasks/index'`, not `'./tasks/index.js'`).
297
+ - **`packages/db/src/schema/index.ts` needs `export {};`.** It's an
298
+ empty barrel-of-barrels (see the schema layer's scope in `core.yaml`)
299
+ until the first module's schema layer adds a line to it, but a
300
+ comment-only file with zero import/export statements compiles as an
301
+ ambient script, not a module, under `isolatedModules` +
302
+ `declaration`/`composite`. `packages/db/src/index.ts`'s `export *
303
+ from './schema/index'` fails `db:typecheck` (`TS2306: File ... is not
304
+ a module`) without it.
305
+ - **`apps/web/package.json` needs `"nx": { "tags": ["scope:web"] }`.**
306
+ `packages/config/eslint-base.js`'s `depConstraints` already defines a
307
+ `sourceTag: 'scope:web'` rule, but `@nx/next:app` doesn't tag the
308
+ project it generates — an untagged project is barred from depending on
309
+ any tagged library at all under `@nx/enforce-module-boundaries`, so
310
+ `apps/web` fails lint on its first import from any workspace package
311
+ without this.
312
+ - **`apps/web-e2e/tsconfig.json` needs an explicit `"types": ["node"]`.**
313
+ `@nx/next:app`'s generated Playwright config (`playwright.config.mts`)
314
+ uses `process.env` and `import.meta.dirname`, but the generator's
315
+ `tsconfig.json` for `apps/web-e2e` has no `types` array — unlike
316
+ `apps/api-e2e`'s Vitest config, which explicitly lists `node` among
317
+ its types. Without it, `nx run web-e2e:typecheck` fails with
318
+ `TS2591`/`TS2339` on both. Add `"types": ["node"]` to
319
+ `compilerOptions` there.
320
+ - **`prettier-plugin-tailwindcss` must stay pinned to `^0.7.4`, not
321
+ `^0.8.x`.** `0.8.0+` made `parser.preprocess` async, but Prettier core
322
+ calls it synchronously — crashes with `TypeError: e.charAt is not a
323
+ function` on every `.ts`/`.tsx` file, independent of Tailwind config,
324
+ module format, or Nx/pnpm. `0.7.4` fully supports Tailwind v4's
325
+ `tailwindStylesheet` option, so nothing is lost by staying on it.
326
+ Re-check this pin when bumping the plugin.
327
+ - **Tailwind v4 needs `tailwindStylesheet` set explicitly in
328
+ `apps/web/.prettierrc.js`.** There's no `tailwind.config.js` for the
329
+ plugin to autodetect under v4 — point it at
330
+ `./src/app/global.css` (the file with `@import 'tailwindcss'`),
331
+ resolved relative to the `.prettierrc.js` file itself.
332
+ - **Any component that imports Radix's `Slot` (the `asChild` pattern —
333
+ e.g. `components/ui/button.tsx`) needs `'use client'` as its first
334
+ line, even if a given render path never actually sets `asChild`.**
335
+ `Slot` calls `React.createContext` at module scope, which crashes
336
+ Server Component builds (`TypeError: e.createContext is not a
337
+ function`) purely on import. Verify with a clean `.next`/Nx cache
338
+ (`rm -rf apps/web/.next .nx/cache`) — a warm cache can mask this.
339
+ - **`apps/web/package.json` needs `"type": "module"`, same reasoning as
340
+ the root `package.json`** — `apps/web/.prettierrc.js`
341
+ is ESM (`export default`), and without a matching `"type": "module"`
342
+ Node emits a `MODULE_TYPELESS_PACKAGE_JSON` warning on every prettier
343
+ invocation touching that directory.
344
+ - **`apps/api`'s dev port defaults to 3333, not 3000.** `apps/web`'s
345
+ `next dev` also defaults to port 3000, and the root `dev` script runs
346
+ both side by side (`nx run-many --target=serve,dev
347
+ --projects=api,web`). If `apps/api/src/main.ts` ever falls back to
348
+ `process.env.PORT || 3000`, whichever process binds the port second
349
+ either crashes or is silently unreachable, and every API call from the
350
+ web client 404s against Next's own dev server instead (Next has no
351
+ matching route, so it serves its catch-all 404 page rather than a
352
+ connection error — easy to mistake for a routing bug in the api
353
+ itself). Keep `apps/api`'s fallback at `3333` and don't let it drift
354
+ back to matching Next's default.
355
+ - **`apps/web/vitest.config.mts` needs `environment: 'jsdom'`, the
356
+ `@vitejs/plugin-react` plugin, and the `@/*` -> `./src/*` alias resolved
357
+ explicitly.** `@nx/next:app --unitTestRunner=vitest` generates a
358
+ Node-flavored config with no JSX plugin and no alias resolution — wrong
359
+ for a React app, and Vitest doesn't read `tsconfig.json`'s `paths` on
360
+ its own. `apps/web/src/test-setup.ts` (loading
361
+ `@testing-library/jest-dom/vitest`) is wired as the config's
362
+ `setupFiles` entry so `toBeInTheDocument()` and friends resolve in every
363
+ spec without a per-file import.
364
+ - **No `NODE_ENV=production` build-target override needed** (Nx
365
+ 23.1.0, Next 16.1.7). Targets are inferred from
366
+ `package.json`/`next.config.js` via the `@nx/next` plugin, with no
367
+ per-app `project.json` to override. If a future Nx/Next bump ever
368
+ produces a dev-mode production build crash
369
+ (`web:build` succeeds but crashes prerendering `/_global-error` with a
370
+ React-internals error), set `NODE_ENV=production` explicitly on
371
+ `apps/web`'s `build` target as the fix.
372
+
373
+ ## If verification fails
374
+
375
+ A clean copy of `workspace/` that fails typecheck/lint/test or needs a
376
+ lockfile change means the shipped template itself is broken — not a
377
+ per-project problem to hand-patch around. Stop and report exactly what
378
+ failed (which target, which error). Fixing this means updating
379
+ `workspace/` at its source (via `scripts/regenerate-full-stack-app-core.sh`
380
+ in this package) and shipping a new package version — never patch a
381
+ consuming project's copy to route around a broken template and call core
382
+ done.
383
+
384
+ ## Local infra: Docker, always
385
+
386
+ Postgres runs through the `docker-compose.yml` this step lands, on
387
+ every host OS, regardless of which add-ons a project turns on later.
388
+ This isn't a convenience default — it's what makes "clone the repo, run
389
+ the stack" mechanically true regardless of who's building. Don't offer
390
+ a native-Postgres alternative even if a contributor already has one
391
+ running locally for another project. Redis joins the same file only if
392
+ the Queue add-on turns on later, in `hedgehog-bootstrap`'s own step —
393
+ not here.
394
+
395
+ ## Constraints
396
+
397
+ - Run once per project, always as `hedgehog-bootstrap`'s first move —
398
+ never invoked on its own by a user.
399
+ - No add-on awareness. If a check here ever seems to need
400
+ `.hedgehog/addons.yaml`, that check belongs in `hedgehog-bootstrap`
401
+ instead — this skill's whole point is being identical across every
402
+ project.
403
+ - Don't hand-edit any file this step lands to work around a verification
404
+ failure. Fix `workspace/` at the source instead (see **If
405
+ verification fails**).
406
+ - Don't add domain schema, contracts, or any `libs/<module>/*` content —
407
+ that's Phase A, after every Bootstrap box (core and whichever add-ons
408
+ are on) is checked.
409
+ - Local Postgres always runs through the copied `docker-compose.yml`, on
410
+ every host OS. Never substitute a natively-installed Postgres, even to
411
+ match a contributor's existing local setup.