@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.
- package/README.md +21 -1
- package/bin/cli.mjs +108 -8
- package/package.json +2 -2
- package/src/agents/backend-eng.md +62 -16
- package/src/agents/front-end-eng.md +48 -14
- package/src/agents/reviewer.md +9 -4
- package/src/agents/ux-planner.md +2 -2
- package/src/db/next.mjs +95 -8
- package/src/db/status.mjs +140 -15
- package/src/golden-cores/full-stack-app/.env.example +1 -0
- package/src/golden-cores/full-stack-app/apps/api/package.json +19 -1
- package/src/golden-cores/full-stack-app/apps/api/src/app/app.module.spec.ts +15 -0
- package/src/golden-cores/full-stack-app/apps/api/src/app/app.module.ts +6 -1
- package/src/golden-cores/full-stack-app/apps/api/src/app/feature-modules.ts +8 -0
- package/src/golden-cores/full-stack-app/apps/api/src/main.ts +10 -1
- package/src/golden-cores/full-stack-app/apps/api/tsconfig.app.json +3 -0
- package/src/golden-cores/full-stack-app/apps/api/tsconfig.json +3 -0
- package/src/golden-cores/full-stack-app/apps/api/tsconfig.spec.json +36 -0
- package/src/golden-cores/full-stack-app/apps/api/vitest.config.mts +18 -0
- package/src/golden-cores/full-stack-app/apps/web/.env.example +12 -0
- package/src/golden-cores/full-stack-app/apps/web/package.json +3 -0
- package/src/golden-cores/full-stack-app/apps/web/src/components/theme-toggle.spec.tsx +20 -0
- package/src/golden-cores/full-stack-app/apps/web/src/test-setup.ts +1 -0
- package/src/golden-cores/full-stack-app/apps/web/tsconfig.json +6 -0
- package/src/golden-cores/full-stack-app/apps/web/tsconfig.spec.json +37 -0
- package/src/golden-cores/full-stack-app/apps/web/vitest.config.mts +27 -0
- package/src/golden-cores/full-stack-app/core.yaml +11 -3
- package/src/golden-cores/full-stack-app/nx.json +4 -1
- package/src/golden-cores/full-stack-app/package.json +8 -0
- package/src/golden-cores/full-stack-app/packages/config/eslint-base.js +108 -13
- package/src/golden-cores/full-stack-app/packages/config/src/env.schema.spec.ts +14 -1
- package/src/golden-cores/full-stack-app/packages/config/src/env.schema.ts +1 -0
- package/src/golden-cores/full-stack-app/packages/config/src/index.ts +1 -1
- package/src/golden-cores/full-stack-app/packages/db/src/index.ts +2 -1
- package/src/golden-cores/full-stack-app/packages/db/src/lib/db.spec.ts +3 -3
- package/src/golden-cores/full-stack-app/packages/db/src/schema/index.ts +4 -0
- package/src/golden-cores/full-stack-app/pnpm-lock.yaml +8735 -2907
- package/src/golden-cores/full-stack-app/pnpm-workspace.yaml +4 -0
- package/src/golden-cores/full-stack-app/tools/generate-feature-modules.cjs +104 -0
- package/src/golden-cores/full-stack-app/tools/generators/contract/generator.ts +283 -0
- package/src/golden-cores/full-stack-app/tools/generators/contract/schema.json +20 -0
- package/src/golden-cores/full-stack-app/tools/generators/controller/generator.ts +323 -0
- package/src/golden-cores/full-stack-app/tools/generators/controller/schema.json +20 -0
- package/src/golden-cores/full-stack-app/tools/generators/fields.ts +126 -0
- package/src/golden-cores/full-stack-app/tools/generators/generators.json +42 -0
- package/src/golden-cores/full-stack-app/tools/generators/hook/generator.ts +274 -0
- package/src/golden-cores/full-stack-app/tools/generators/hook/schema.json +19 -0
- package/src/golden-cores/full-stack-app/tools/generators/lib-shell.ts +124 -0
- package/src/golden-cores/full-stack-app/tools/generators/naming.ts +84 -0
- package/src/golden-cores/full-stack-app/tools/generators/package.json +6 -0
- package/src/golden-cores/full-stack-app/tools/generators/repository/generator.ts +298 -0
- package/src/golden-cores/full-stack-app/tools/generators/repository/schema.json +15 -0
- package/src/golden-cores/full-stack-app/tools/generators/schema/generator.ts +169 -0
- package/src/golden-cores/full-stack-app/tools/generators/schema/schema.json +20 -0
- package/src/golden-cores/full-stack-app/tools/generators/screen/generator.ts +218 -0
- package/src/golden-cores/full-stack-app/tools/generators/screen/schema.json +15 -0
- package/src/golden-cores/full-stack-app/tools/generators/service/generator.ts +194 -0
- package/src/golden-cores/full-stack-app/tools/generators/service/schema.json +15 -0
- package/src/golden-cores/full-stack-app/tsconfig.base.json +2 -2
- package/src/skills/conventional-commits/SKILL.md +2 -3
- package/src/skills/hedgehog-bootstrap/SKILL.md +25 -7
- package/src/skills/hedgehog-bootstrap-full-stack-app-core/SKILL.md +135 -20
- 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
|
-
|
|
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
|
|
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: [
|
|
69
|
+
onlyDependOnLibsWithTags: [
|
|
70
|
+
'type:adapter',
|
|
71
|
+
'type:contract',
|
|
72
|
+
'type:util',
|
|
73
|
+
],
|
|
52
74
|
},
|
|
53
|
-
// adapters (repositories, db client, auth config)
|
|
54
|
-
// shared utils
|
|
55
|
-
// service
|
|
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: [
|
|
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
|
|
124
|
+
// worker reaches domain logic the same way api does
|
|
87
125
|
{
|
|
88
126
|
sourceTag: 'scope:worker',
|
|
89
127
|
onlyDependOnLibsWithTags: [
|
|
90
|
-
'type:
|
|
128
|
+
'type:adapter',
|
|
129
|
+
'type:service',
|
|
130
|
+
'type:contract',
|
|
91
131
|
'type:util',
|
|
92
132
|
'scope:shared',
|
|
93
133
|
],
|
|
94
134
|
},
|
|
95
|
-
// api
|
|
96
|
-
//
|
|
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:
|
|
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
|
|
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
|
|
1
|
+
export * from './env.schema';
|
|
@@ -1 +1,2 @@
|
|
|
1
|
-
export * from './lib/db
|
|
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
|
|
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
|
|
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
|
|
41
|
+
const { getPool } = await import('./db');
|
|
42
42
|
expect(() => getPool()).toThrow('process.exit called');
|
|
43
43
|
expect(exitSpy).toHaveBeenCalledWith(1);
|
|
44
44
|
|