@skyf0xx/hedgehog 4.1.0 → 4.2.2

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 (63) hide show
  1. package/README.md +21 -1
  2. package/bin/cli.mjs +108 -8
  3. package/package.json +2 -2
  4. package/src/agents/backend-eng.md +62 -16
  5. package/src/agents/front-end-eng.md +48 -14
  6. package/src/agents/reviewer.md +9 -4
  7. package/src/agents/ux-planner.md +2 -2
  8. package/src/db/next.mjs +95 -8
  9. package/src/db/status.mjs +140 -15
  10. package/src/golden-cores/full-stack-app/.env.example +1 -0
  11. package/src/golden-cores/full-stack-app/apps/api/package.json +19 -1
  12. package/src/golden-cores/full-stack-app/apps/api/src/app/app.module.spec.ts +15 -0
  13. package/src/golden-cores/full-stack-app/apps/api/src/app/app.module.ts +6 -1
  14. package/src/golden-cores/full-stack-app/apps/api/src/app/feature-modules.ts +8 -0
  15. package/src/golden-cores/full-stack-app/apps/api/src/main.ts +10 -1
  16. package/src/golden-cores/full-stack-app/apps/api/tsconfig.app.json +3 -0
  17. package/src/golden-cores/full-stack-app/apps/api/tsconfig.json +3 -0
  18. package/src/golden-cores/full-stack-app/apps/api/tsconfig.spec.json +36 -0
  19. package/src/golden-cores/full-stack-app/apps/api/vitest.config.mts +18 -0
  20. package/src/golden-cores/full-stack-app/apps/web/.env.example +12 -0
  21. package/src/golden-cores/full-stack-app/apps/web/package.json +3 -0
  22. package/src/golden-cores/full-stack-app/apps/web/src/components/theme-toggle.spec.tsx +20 -0
  23. package/src/golden-cores/full-stack-app/apps/web/src/test-setup.ts +1 -0
  24. package/src/golden-cores/full-stack-app/apps/web/tsconfig.json +6 -0
  25. package/src/golden-cores/full-stack-app/apps/web/tsconfig.spec.json +37 -0
  26. package/src/golden-cores/full-stack-app/apps/web/vitest.config.mts +27 -0
  27. package/src/golden-cores/full-stack-app/core.yaml +11 -3
  28. package/src/golden-cores/full-stack-app/nx.json +4 -1
  29. package/src/golden-cores/full-stack-app/package.json +8 -0
  30. package/src/golden-cores/full-stack-app/packages/config/eslint-base.js +108 -13
  31. package/src/golden-cores/full-stack-app/packages/config/src/env.schema.spec.ts +14 -1
  32. package/src/golden-cores/full-stack-app/packages/config/src/env.schema.ts +1 -0
  33. package/src/golden-cores/full-stack-app/packages/config/src/index.ts +1 -1
  34. package/src/golden-cores/full-stack-app/packages/db/src/index.ts +2 -1
  35. package/src/golden-cores/full-stack-app/packages/db/src/lib/db.spec.ts +3 -3
  36. package/src/golden-cores/full-stack-app/packages/db/src/schema/index.ts +4 -0
  37. package/src/golden-cores/full-stack-app/pnpm-lock.yaml +8735 -2907
  38. package/src/golden-cores/full-stack-app/pnpm-workspace.yaml +4 -0
  39. package/src/golden-cores/full-stack-app/tools/generate-feature-modules.cjs +104 -0
  40. package/src/golden-cores/full-stack-app/tools/generators/contract/generator.ts +283 -0
  41. package/src/golden-cores/full-stack-app/tools/generators/contract/schema.json +20 -0
  42. package/src/golden-cores/full-stack-app/tools/generators/controller/generator.ts +323 -0
  43. package/src/golden-cores/full-stack-app/tools/generators/controller/schema.json +20 -0
  44. package/src/golden-cores/full-stack-app/tools/generators/fields.ts +126 -0
  45. package/src/golden-cores/full-stack-app/tools/generators/generators.json +42 -0
  46. package/src/golden-cores/full-stack-app/tools/generators/hook/generator.ts +274 -0
  47. package/src/golden-cores/full-stack-app/tools/generators/hook/schema.json +19 -0
  48. package/src/golden-cores/full-stack-app/tools/generators/lib-shell.ts +124 -0
  49. package/src/golden-cores/full-stack-app/tools/generators/naming.ts +84 -0
  50. package/src/golden-cores/full-stack-app/tools/generators/package.json +6 -0
  51. package/src/golden-cores/full-stack-app/tools/generators/repository/generator.ts +298 -0
  52. package/src/golden-cores/full-stack-app/tools/generators/repository/schema.json +15 -0
  53. package/src/golden-cores/full-stack-app/tools/generators/schema/generator.ts +169 -0
  54. package/src/golden-cores/full-stack-app/tools/generators/schema/schema.json +20 -0
  55. package/src/golden-cores/full-stack-app/tools/generators/screen/generator.ts +218 -0
  56. package/src/golden-cores/full-stack-app/tools/generators/screen/schema.json +15 -0
  57. package/src/golden-cores/full-stack-app/tools/generators/service/generator.ts +194 -0
  58. package/src/golden-cores/full-stack-app/tools/generators/service/schema.json +15 -0
  59. package/src/golden-cores/full-stack-app/tsconfig.base.json +2 -2
  60. package/src/skills/conventional-commits/SKILL.md +2 -3
  61. package/src/skills/hedgehog-bootstrap/SKILL.md +25 -7
  62. package/src/skills/hedgehog-bootstrap-full-stack-app-core/SKILL.md +135 -20
  63. package/src/skills/hedgehog-loop/SKILL.md +161 -8
@@ -7,6 +7,15 @@ import nx from '@nx/eslint-plugin';
7
7
  * `.claude/skills/hedgehog-bootstrap` and `.claude/skills/hedgehog-loop`
8
8
  * for the discipline this enforces.
9
9
  *
10
+ * The constraints below describe the project shape `core.yaml`'s compiled
11
+ * layer sequence actually produces. One layer, `repository`, lands a
12
+ * module's port interface AND its Drizzle adapter in the same lib
13
+ * (`libs/<module>/repository`, tagged `type:adapter`) — there is no
14
+ * separate `type:port` project, so a tag constraint alone cannot say
15
+ * "import the interface, not the concrete class." The
16
+ * `no-restricted-imports` block below carries that half of port
17
+ * discipline; the tag graph carries the rest.
18
+ *
10
19
  * Tag reference:
11
20
  * apps/api -> scope:api
12
21
  * apps/worker -> scope:worker
@@ -18,7 +27,6 @@ import nx from '@nx/eslint-plugin';
18
27
  * packages/auth -> scope:auth, type:adapter
19
28
  * packages/config -> scope:config, type:util
20
29
  * packages/shared -> scope:shared, type:util
21
- * libs/<module>/port -> scope:<module>, type:port
22
30
  * libs/<module>/repository -> scope:<module>, type:adapter
23
31
  * libs/<module>/service -> scope:<module>, type:service
24
32
  */
@@ -42,20 +50,49 @@ export default [
42
50
  '@nx/enforce-module-boundaries': [
43
51
  'error',
44
52
  {
45
- enforceBuildableLibDependency: true,
53
+ // `false`, not `true`: `packages/contracts` and `packages/hooks`
54
+ // are source-only by this core's design — they have no `build`
55
+ // target and are consumed as TypeScript sources. With this on,
56
+ // `apps/web` (a real Next build target) importing either one
57
+ // fails lint purely for that structural choice, not for any
58
+ // dependency that's actually wrong.
59
+ enforceBuildableLibDependency: false,
46
60
  allow: ['^.*/eslint(\\.base)?\\.config\\.[cm]?[jt]s$'],
47
61
  depConstraints: [
48
- // domain services never import adapters directly — only ports
62
+ // domain services reach storage through their own module's
63
+ // repository lib, which holds the port interface alongside the
64
+ // Drizzle adapter (`type:adapter`). Which symbol a service may
65
+ // import out of that lib is the no-restricted-imports rule
66
+ // below, not something a tag can express.
49
67
  {
50
68
  sourceTag: 'type:service',
51
- onlyDependOnLibsWithTags: ['type:port', 'type:util'],
69
+ onlyDependOnLibsWithTags: [
70
+ 'type:adapter',
71
+ 'type:contract',
72
+ 'type:util',
73
+ ],
52
74
  },
53
- // adapters (repositories, db client, auth config) may depend on
54
- // shared utils only — never reach across into another module's
55
- // service/port
75
+ // adapters (repositories, db client, auth config) depend on
76
+ // packages/db and shared utils — never reach across into
77
+ // another module's service
56
78
  {
57
79
  sourceTag: 'type:adapter',
58
- onlyDependOnLibsWithTags: ['type:util'],
80
+ onlyDependOnLibsWithTags: [
81
+ 'type:adapter',
82
+ 'type:contract',
83
+ 'type:util',
84
+ ],
85
+ },
86
+ // the ts-rest/Zod contract is derived from the Drizzle schema
87
+ // (`drizzle-zod`), so it reaches packages/db and nothing else
88
+ {
89
+ sourceTag: 'type:contract',
90
+ onlyDependOnLibsWithTags: ['type:adapter', 'type:util'],
91
+ },
92
+ // TanStack Query hooks are typed by the contract they call
93
+ {
94
+ sourceTag: 'type:hook',
95
+ onlyDependOnLibsWithTags: ['type:contract', 'type:util'],
59
96
  },
60
97
  // shared utils (packages/config, packages/shared) depend on
61
98
  // nothing else in the workspace — they're the floor
@@ -81,24 +118,32 @@ export default [
81
118
  'scope:contracts',
82
119
  'scope:hooks',
83
120
  'scope:shared',
121
+ 'type:util',
84
122
  ],
85
123
  },
86
- // worker only reaches domain logic through ports, same as api
124
+ // worker reaches domain logic the same way api does
87
125
  {
88
126
  sourceTag: 'scope:worker',
89
127
  onlyDependOnLibsWithTags: [
90
- 'type:port',
128
+ 'type:adapter',
129
+ 'type:service',
130
+ 'type:contract',
91
131
  'type:util',
92
132
  'scope:shared',
93
133
  ],
94
134
  },
95
- // api reaches storage/domain logic through a module's port/service
96
- // only — never packages/db directly
135
+ // api is the composition root: its NestJS module is the only
136
+ // place that constructs a module's concrete adapter and binds
137
+ // it to the port's DI token, so it reaches `type:adapter` by
138
+ // necessity. It also serves the ts-rest contract, hence
139
+ // `type:contract`. What it may NOT do is inject an adapter
140
+ // into a controller — that's the no-restricted-imports rule.
97
141
  {
98
142
  sourceTag: 'scope:api',
99
143
  onlyDependOnLibsWithTags: [
100
- 'type:port',
144
+ 'type:adapter',
101
145
  'type:service',
146
+ 'type:contract',
102
147
  'type:util',
103
148
  ],
104
149
  },
@@ -107,4 +152,54 @@ export default [
107
152
  ],
108
153
  },
109
154
  },
155
+ {
156
+ // Port discipline, the half tags can't express. A module's port
157
+ // interface and its Drizzle adapter live in the same lib, so
158
+ // `type:service -> type:adapter` has to be allowed above; this is what
159
+ // stops a service from importing the concrete adapter through it.
160
+ // Services see the port only. Deep paths are named rather than the
161
+ // package entry point, because the entry point is also how the port
162
+ // itself is imported.
163
+ files: ['libs/*/service/**/*.ts'],
164
+ rules: {
165
+ 'no-restricted-imports': [
166
+ 'error',
167
+ {
168
+ patterns: [
169
+ {
170
+ group: ['**/*.adapter', '**/*.adapter.js', '**/*.adapter.ts'],
171
+ message:
172
+ 'A service depends on its module\'s port interface, never the concrete adapter. Import the port from the repository lib\'s entry point and let apps/api bind the adapter to its DI token.',
173
+ },
174
+ {
175
+ group: ['@app/db', '@app/db/*', 'db', 'db/*', 'drizzle-orm', 'drizzle-orm/*'],
176
+ message:
177
+ 'A service never talks to the database directly — that is the repository adapter\'s job, reached through the port.',
178
+ },
179
+ ],
180
+ },
181
+ ],
182
+ },
183
+ },
184
+ {
185
+ // The same rule one layer up, minus the composition root. A NestJS
186
+ // module file (`*.module.ts`) is where the adapter is constructed and
187
+ // bound, so it is exempt; a controller injecting an adapter is not.
188
+ files: ['apps/api/**/*.ts'],
189
+ ignores: ['apps/api/**/*.module.ts'],
190
+ rules: {
191
+ 'no-restricted-imports': [
192
+ 'error',
193
+ {
194
+ patterns: [
195
+ {
196
+ group: ['**/*.adapter', '**/*.adapter.js', '**/*.adapter.ts'],
197
+ message:
198
+ 'Only a NestJS module (*.module.ts) constructs a concrete adapter. A controller depends on the service, or on the port token the module bound.',
199
+ },
200
+ ],
201
+ },
202
+ ],
203
+ },
204
+ },
110
205
  ];
@@ -1,4 +1,4 @@
1
- import { envSchema } from './env.schema.js';
1
+ import { envSchema } from './env.schema';
2
2
 
3
3
  describe('envSchema', () => {
4
4
  const valid = {
@@ -22,4 +22,17 @@ describe('envSchema', () => {
22
22
  const result = envSchema.safeParse({ ...valid, NODE_ENV: 'staging' });
23
23
  expect(result.success).toBe(false);
24
24
  });
25
+
26
+ it('defaults WEB_ORIGIN when absent', () => {
27
+ const result = envSchema.safeParse(valid);
28
+ expect(result.success).toBe(true);
29
+ if (result.success) {
30
+ expect(result.data.WEB_ORIGIN).toBe('http://localhost:3000');
31
+ }
32
+ });
33
+
34
+ it('rejects a non-URL WEB_ORIGIN', () => {
35
+ const result = envSchema.safeParse({ ...valid, WEB_ORIGIN: 'not-a-url' });
36
+ expect(result.success).toBe(false);
37
+ });
25
38
  });
@@ -3,6 +3,7 @@ import { z } from 'zod';
3
3
  export const envSchema = z.object({
4
4
  DATABASE_URL: z.string().url(),
5
5
  NODE_ENV: z.enum(['development', 'test', 'production']),
6
+ WEB_ORIGIN: z.string().url().default('http://localhost:3000'),
6
7
  });
7
8
 
8
9
  export type Env = z.infer<typeof envSchema>;
@@ -1 +1 @@
1
- export * from './env.schema.js';
1
+ export * from './env.schema';
@@ -1 +1,2 @@
1
- export * from './lib/db.js';
1
+ export * from './lib/db';
2
+ export * from './schema/index';
@@ -15,7 +15,7 @@ describe('db', () => {
15
15
  });
16
16
 
17
17
  it('lazily constructs a single shared pool', async () => {
18
- const { getPool, closeDb } = await import('./db.js');
18
+ const { getPool, closeDb } = await import('./db');
19
19
  const first = getPool();
20
20
  const second = getPool();
21
21
  expect(first).toBe(second);
@@ -23,7 +23,7 @@ describe('db', () => {
23
23
  });
24
24
 
25
25
  it('returns a drizzle client bound to the shared pool', async () => {
26
- const { getDb, closeDb } = await import('./db.js');
26
+ const { getDb, closeDb } = await import('./db');
27
27
  const db = getDb();
28
28
  expect(db).toBeDefined();
29
29
  await closeDb();
@@ -38,7 +38,7 @@ describe('db', () => {
38
38
  .spyOn(console, 'error')
39
39
  .mockImplementation(() => undefined);
40
40
 
41
- const { getPool } = await import('./db.js');
41
+ const { getPool } = await import('./db');
42
42
  expect(() => getPool()).toThrow('process.exit called');
43
43
  expect(exitSpy).toHaveBeenCalledWith(1);
44
44
 
@@ -0,0 +1,4 @@
1
+ // Barrel-of-barrels: one re-export line per module, added by that module's
2
+ // own schema layer (in scope — see core.yaml's schema layer). Keeps
3
+ // packages/db/src/index.ts itself static after bootstrap.
4
+ export {};