@elevasis/core 0.42.1 → 0.44.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/dist/auth/index.d.ts +8 -3
- package/dist/auth/index.js +6 -0
- package/dist/business/entities-published.d.ts +1 -1
- package/dist/index.d.ts +12 -13
- package/dist/index.js +48 -29
- package/dist/knowledge/index.d.ts +94 -6
- package/dist/knowledge/index.js +172 -8
- package/dist/organization-model/index.d.ts +12 -13
- package/dist/organization-model/index.js +48 -29
- package/dist/test-utils/index.d.ts +5 -6
- package/dist/test-utils/index.js +21 -18
- package/package.json +3 -3
- package/src/auth/access-keys.ts +6 -0
- package/src/business/acquisition/api-schemas.ts +1 -1
- package/src/business/base-entities.ts +1 -1
- package/src/knowledge/cli-helpers.ts +211 -0
- package/src/knowledge/index.ts +13 -0
- package/src/knowledge/published.ts +18 -5
- package/src/knowledge/queries.ts +5 -5
- package/src/organization-model/__tests__/cross-ref.test.ts +11 -1
- package/src/organization-model/__tests__/domains/systems.test.ts +34 -8
- package/src/organization-model/__tests__/scaffolders.test.ts +30 -1
- package/src/organization-model/__tests__/schema-refinements.test.ts +178 -0
- package/src/organization-model/cross-ref.ts +43 -7
- package/src/organization-model/defaults.ts +2 -2
- package/src/organization-model/domains/actions.ts +1 -1
- package/src/organization-model/domains/resources.ts +1 -1
- package/src/organization-model/domains/systems.ts +0 -4
- package/src/organization-model/ontology.ts +13 -18
- package/src/organization-model/organization-graph.mdx +9 -8
- package/src/organization-model/published.ts +9 -3
- package/src/organization-model/resolve.ts +9 -7
- package/src/organization-model/scaffolders/helpers.ts +1 -1
- package/src/organization-model/scaffolders/scaffoldKnowledgeNode.ts +1 -0
- package/src/organization-model/scaffolders/scaffoldOntologyRecord.ts +28 -6
- package/src/organization-model/scaffolders/scaffoldResource.ts +1 -0
- package/src/organization-model/scaffolders/scaffoldSystem.ts +2 -1
- package/src/organization-model/schema-refinements.ts +3 -5
- package/src/platform/registry/__tests__/validation.test.ts +28 -0
- package/src/platform/registry/validation.ts +20 -2
- package/src/scaffold-registry/__tests__/index.test.ts +380 -206
- package/src/scaffold-registry/index.ts +392 -381
- package/src/test-utils/mocks/supabase.ts +1 -1
- package/src/test-utils/mocks/workos.ts +2 -2
|
@@ -1,392 +1,403 @@
|
|
|
1
|
-
import { createHash } from 'node:crypto'
|
|
2
|
-
import { existsSync, readdirSync, readFileSync, statSync, writeFileSync } from 'node:fs'
|
|
3
|
-
import path from 'node:path'
|
|
4
|
-
import { parse as parseYaml } from 'yaml'
|
|
5
|
-
import { type ScaffoldRegistry, type ScaffoldRegistryEntry, ScaffoldRegistrySchema } from './schema'
|
|
6
|
-
|
|
7
|
-
const MODULE_DIR = path.dirname(new URL(import.meta.url).pathname.replace(/^\/([A-Z]:)/, '$1'))
|
|
8
|
-
|
|
1
|
+
import { createHash } from 'node:crypto'
|
|
2
|
+
import { existsSync, readdirSync, readFileSync, statSync, writeFileSync } from 'node:fs'
|
|
3
|
+
import path from 'node:path'
|
|
4
|
+
import { parse as parseYaml } from 'yaml'
|
|
5
|
+
import { type ScaffoldRegistry, type ScaffoldRegistryEntry, ScaffoldRegistrySchema } from './schema'
|
|
6
|
+
|
|
7
|
+
const MODULE_DIR = path.dirname(new URL(import.meta.url).pathname.replace(/^\/([A-Z]:)/, '$1'))
|
|
8
|
+
|
|
9
9
|
export {
|
|
10
10
|
ExternalSyncCategorySchema,
|
|
11
11
|
ExternalSyncDeletePolicySchema,
|
|
12
12
|
ExternalSyncOwnerSchema,
|
|
13
13
|
ExternalSyncStrategySchema,
|
|
14
14
|
ScaffoldEntryKindSchema,
|
|
15
|
-
ScaffoldRegistrySchema
|
|
16
|
-
} from './schema'
|
|
15
|
+
ScaffoldRegistrySchema
|
|
16
|
+
} from './schema'
|
|
17
17
|
export type {
|
|
18
18
|
ExternalSyncCategory,
|
|
19
19
|
ExternalSyncDeletePolicy,
|
|
20
20
|
ExternalSyncOwner,
|
|
21
21
|
ExternalSyncStrategy,
|
|
22
|
-
ScaffoldEntryKind,
|
|
23
|
-
ScaffoldRef,
|
|
24
|
-
ScaffoldRegistry,
|
|
25
|
-
ScaffoldRegistryEntry
|
|
26
|
-
} from './schema'
|
|
27
|
-
|
|
28
|
-
// ---------------------------------------------------------------------------
|
|
29
|
-
// Paths (resolved relative to the monorepo root, not this file's location)
|
|
30
|
-
// ---------------------------------------------------------------------------
|
|
31
|
-
|
|
32
|
-
/**
|
|
33
|
-
* Resolve a path relative to the monorepo root.
|
|
34
|
-
* Works whether this module is running from packages/core/src or from dist/.
|
|
35
|
-
*/
|
|
36
|
-
function monorepoRoot(): string {
|
|
37
|
-
// Walk up from __dirname until we find the .claude directory (monorepo marker)
|
|
38
|
-
const { dirname } = path
|
|
39
|
-
let dir = MODULE_DIR
|
|
40
|
-
for (let i = 0; i < 8; i++) {
|
|
41
|
-
try {
|
|
42
|
-
readFileSync(path.join(dir, '.claude', 'settings.json'))
|
|
43
|
-
return dir
|
|
44
|
-
} catch {
|
|
45
|
-
dir = dirname(dir)
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
throw new Error(
|
|
49
|
-
'scaffold-registry: could not locate monorepo root (no .claude/settings.json found in ancestor directories)'
|
|
50
|
-
)
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
const YAML_FILENAME = '.claude/registries/scaffold-registry.yml'
|
|
54
|
-
const JSON_FILENAME = '.claude/registries/scaffold-registry.compiled.json'
|
|
55
|
-
|
|
56
|
-
// ---------------------------------------------------------------------------
|
|
57
|
-
// Load + validate
|
|
58
|
-
// ---------------------------------------------------------------------------
|
|
59
|
-
|
|
60
|
-
/**
|
|
61
|
-
* Load and Zod-validate the scaffold registry from `.claude/registries/scaffold-registry.yml`.
|
|
62
|
-
*
|
|
63
|
-
* Throws if:
|
|
64
|
-
* - The YAML file is missing or unreadable
|
|
65
|
-
* - The YAML fails Zod validation
|
|
66
|
-
* - The compiled JSON is present but its
|
|
67
|
-
* (drift detection — regenerate with `compileScaffoldRegistry()` to fix)
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
const
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
*
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
throw new Error(`scaffold-registry:
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
if (
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
22
|
+
ScaffoldEntryKind,
|
|
23
|
+
ScaffoldRef,
|
|
24
|
+
ScaffoldRegistry,
|
|
25
|
+
ScaffoldRegistryEntry
|
|
26
|
+
} from './schema'
|
|
27
|
+
|
|
28
|
+
// ---------------------------------------------------------------------------
|
|
29
|
+
// Paths (resolved relative to the monorepo root, not this file's location)
|
|
30
|
+
// ---------------------------------------------------------------------------
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Resolve a path relative to the monorepo root.
|
|
34
|
+
* Works whether this module is running from packages/core/src or from dist/.
|
|
35
|
+
*/
|
|
36
|
+
function monorepoRoot(): string {
|
|
37
|
+
// Walk up from __dirname until we find the .claude directory (monorepo marker)
|
|
38
|
+
const { dirname } = path
|
|
39
|
+
let dir = MODULE_DIR
|
|
40
|
+
for (let i = 0; i < 8; i++) {
|
|
41
|
+
try {
|
|
42
|
+
readFileSync(path.join(dir, '.claude', 'settings.json'))
|
|
43
|
+
return dir
|
|
44
|
+
} catch {
|
|
45
|
+
dir = dirname(dir)
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
throw new Error(
|
|
49
|
+
'scaffold-registry: could not locate monorepo root (no .claude/settings.json found in ancestor directories)'
|
|
50
|
+
)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const YAML_FILENAME = '.claude/registries/scaffold-registry.yml'
|
|
54
|
+
const JSON_FILENAME = '.claude/registries/scaffold-registry.compiled.json'
|
|
55
|
+
|
|
56
|
+
// ---------------------------------------------------------------------------
|
|
57
|
+
// Load + validate
|
|
58
|
+
// ---------------------------------------------------------------------------
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Load and Zod-validate the scaffold registry from `.claude/registries/scaffold-registry.yml`.
|
|
62
|
+
*
|
|
63
|
+
* Throws if:
|
|
64
|
+
* - The YAML file is missing or unreadable
|
|
65
|
+
* - The YAML fails Zod validation
|
|
66
|
+
* - The compiled JSON is present but its sha256 hash differs from the YAML
|
|
67
|
+
* (drift detection — regenerate with `compileScaffoldRegistry()` to fix)
|
|
68
|
+
*
|
|
69
|
+
* @param rootDir - Optional monorepo root override. Defaults to the auto-detected
|
|
70
|
+
* monorepo root. Pass a fixture directory to test against a local snapshot.
|
|
71
|
+
*/
|
|
72
|
+
export function loadScaffoldRegistry(rootDir?: string): ScaffoldRegistry {
|
|
73
|
+
const root = rootDir ?? monorepoRoot()
|
|
74
|
+
const yamlPath = path.join(root, YAML_FILENAME)
|
|
75
|
+
|
|
76
|
+
let raw: string
|
|
77
|
+
try {
|
|
78
|
+
raw = readFileSync(yamlPath, 'utf-8')
|
|
79
|
+
} catch (err) {
|
|
80
|
+
throw new Error(`scaffold-registry: could not read ${YAML_FILENAME} — ${String(err)}`)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const parsed = parseYaml(raw) as unknown
|
|
84
|
+
const result = ScaffoldRegistrySchema.safeParse(parsed)
|
|
85
|
+
|
|
86
|
+
if (!result.success) {
|
|
87
|
+
const issues = result.error.issues.map((i) => ` [${i.path.join('.')}] ${i.message}`).join('\n')
|
|
88
|
+
throw new Error(`scaffold-registry: YAML validation failed:\n${issues}`)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const registry = result.data
|
|
92
|
+
|
|
93
|
+
// Drift check: if compiled JSON exists, verify the full normalized content matches.
|
|
94
|
+
const jsonPath = path.join(root, JSON_FILENAME)
|
|
95
|
+
try {
|
|
96
|
+
const compiledRaw = readFileSync(jsonPath, 'utf-8')
|
|
97
|
+
const compiledParsed = JSON.parse(compiledRaw) as unknown
|
|
98
|
+
const compiledResult = ScaffoldRegistrySchema.safeParse(compiledParsed)
|
|
99
|
+
if (!compiledResult.success) {
|
|
100
|
+
const issues = compiledResult.error.issues.map((i) => ` [${i.path.join('.')}] ${i.message}`).join('\n')
|
|
101
|
+
throw new Error(`scaffold-registry: compiled JSON validation failed:\n${issues}`)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const yamlHash = stableRegistryHash(registry)
|
|
105
|
+
const compiledHash = stableRegistryHash(compiledResult.data)
|
|
106
|
+
if (compiledHash !== yamlHash) {
|
|
107
|
+
throw new Error(
|
|
108
|
+
`scaffold-registry: compiled JSON is out of sync with YAML ` +
|
|
109
|
+
`(YAML hash ${yamlHash.slice(0, 12)}, JSON hash ${compiledHash.slice(0, 12)}). ` +
|
|
110
|
+
`Run compileScaffoldRegistry() to regenerate.`
|
|
111
|
+
)
|
|
112
|
+
}
|
|
113
|
+
} catch (err) {
|
|
114
|
+
// If the file doesn't exist, skip drift check silently (first-run scenario)
|
|
115
|
+
if ((err as { code?: string }).code !== 'ENOENT') {
|
|
116
|
+
throw err
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return registry
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// ---------------------------------------------------------------------------
|
|
124
|
+
// Compile
|
|
125
|
+
// ---------------------------------------------------------------------------
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Load, validate, and write the pre-compiled JSON lookup file.
|
|
129
|
+
* Run this whenever `.claude/registries/scaffold-registry.yml` changes.
|
|
130
|
+
*
|
|
131
|
+
* Called by the `pnpm scaffold:compile-registry` script.
|
|
132
|
+
*
|
|
133
|
+
* @param rootDir - Optional monorepo root override. Defaults to the auto-detected
|
|
134
|
+
* monorepo root. Pass a fixture directory to test against a local snapshot.
|
|
135
|
+
*/
|
|
136
|
+
export function compileScaffoldRegistry(rootDir?: string): ScaffoldRegistry {
|
|
137
|
+
const root = rootDir ?? monorepoRoot()
|
|
138
|
+
const registry = loadScaffoldRegistryNoSyncCheck(root)
|
|
139
|
+
|
|
140
|
+
const missing = findMissingDependentPaths(registry, root)
|
|
141
|
+
if (missing.length > 0) {
|
|
142
|
+
const formatted = missing.map((m) => ` [${m.entryId}] ${m.path}`).join('\n')
|
|
143
|
+
throw new Error(
|
|
144
|
+
`scaffold-registry: ${missing.length} dependent path(s) do not exist on disk:\n${formatted}\n` +
|
|
145
|
+
`Fix the typo, create the file, or convert the path to a glob/symbolic target.`
|
|
146
|
+
)
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const emptySources = findEmptySourcePatterns(registry, root)
|
|
150
|
+
if (emptySources.length > 0) {
|
|
151
|
+
const formatted = emptySources.map((source) => ` [${source.entryId}] ${source.pattern}`).join('\n')
|
|
152
|
+
throw new Error(
|
|
153
|
+
`scaffold-registry: ${emptySources.length} source pattern(s) match no files or directories:\n${formatted}\n` +
|
|
154
|
+
`Fix the stale source glob, create the scaffold surface, or add explicit registry support for intentional empty patterns.`
|
|
155
|
+
)
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const jsonPath = path.join(root, JSON_FILENAME)
|
|
159
|
+
writeFileSync(jsonPath, JSON.stringify(registry, null, 2) + '\n', 'utf-8')
|
|
160
|
+
|
|
161
|
+
return registry
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Return dependent paths declared in the registry that don't exist on disk.
|
|
166
|
+
* Skips symbolic targets (`docs:`, `autogen-target:`) and glob patterns
|
|
167
|
+
* (those containing `*`, `?`, or `[`), which can't be resolved to a single file.
|
|
168
|
+
*
|
|
169
|
+
* Exported so external scripts (e.g. CI gates) can run the same check.
|
|
170
|
+
*/
|
|
171
|
+
export function findMissingDependentPaths(
|
|
172
|
+
registry: ScaffoldRegistry,
|
|
173
|
+
monorepoRootDir: string
|
|
174
|
+
): Array<{ entryId: string; path: string }> {
|
|
175
|
+
const missing: Array<{ entryId: string; path: string }> = []
|
|
176
|
+
for (const entry of registry.entries) {
|
|
177
|
+
// sync-preservation dependents describe paths inside derived external
|
|
178
|
+
// projects (not files that physically exist in this monorepo), so skip them.
|
|
179
|
+
if (entry.kind === 'sync-preservation') continue
|
|
180
|
+
for (const dependent of entry.dependents) {
|
|
181
|
+
if (isSymbolicTarget(dependent.path) || isGlobPattern(dependent.path) || dependent.path === '(self)') {
|
|
182
|
+
continue
|
|
183
|
+
}
|
|
184
|
+
const absolute = path.join(monorepoRootDir, dependent.path)
|
|
185
|
+
if (!existsSync(absolute)) {
|
|
186
|
+
missing.push({ entryId: entry.id, path: dependent.path })
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
return missing
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Return source patterns that do not currently match any file or directory.
|
|
195
|
+
* Symbolic sources are skipped because they intentionally do not resolve to
|
|
196
|
+
* monorepo paths.
|
|
197
|
+
*/
|
|
198
|
+
export function findEmptySourcePatterns(
|
|
199
|
+
registry: ScaffoldRegistry,
|
|
200
|
+
monorepoRootDir: string
|
|
201
|
+
): Array<{ entryId: string; pattern: string }> {
|
|
202
|
+
const empty: Array<{ entryId: string; pattern: string }> = []
|
|
203
|
+
|
|
204
|
+
for (const entry of registry.entries) {
|
|
205
|
+
for (const sourcePattern of entry.sources) {
|
|
206
|
+
if (isSymbolicTarget(sourcePattern)) continue
|
|
207
|
+
if (!sourcePatternMatchesAnyPath(sourcePattern, monorepoRootDir)) {
|
|
208
|
+
empty.push({ entryId: entry.id, pattern: sourcePattern })
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
return empty
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// ---------------------------------------------------------------------------
|
|
217
|
+
// Lookup helpers (used by hooks for fast path matching)
|
|
218
|
+
// ---------------------------------------------------------------------------
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Load from the pre-compiled JSON only (fastest path; used by PostToolUse hooks).
|
|
222
|
+
* Falls back to YAML if the JSON is missing.
|
|
223
|
+
*/
|
|
224
|
+
export function loadScaffoldRegistryFast(): ScaffoldRegistry {
|
|
225
|
+
const root = monorepoRoot()
|
|
226
|
+
const jsonPath = path.join(root, JSON_FILENAME)
|
|
227
|
+
|
|
228
|
+
try {
|
|
229
|
+
const raw = readFileSync(jsonPath, 'utf-8')
|
|
230
|
+
const parsed = JSON.parse(raw) as unknown
|
|
231
|
+
const result = ScaffoldRegistrySchema.safeParse(parsed)
|
|
232
|
+
if (result.success) return result.data
|
|
233
|
+
} catch {
|
|
234
|
+
// fall through to YAML
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
return loadScaffoldRegistryNoSyncCheck(root)
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Return all entries whose `sources` contain at least one pattern that matches
|
|
242
|
+
* the given file path AND whose `excludes` contain no pattern that matches.
|
|
243
|
+
* Pattern matching is a simple substring/glob-prefix check suitable for hook
|
|
244
|
+
* use; Step 3 will upgrade to full micromatch when the hook is implemented.
|
|
245
|
+
*/
|
|
246
|
+
export function findMatchingEntries(registry: ScaffoldRegistry, filePath: string): ScaffoldRegistryEntry[] {
|
|
247
|
+
return registry.entries.filter((entry) => {
|
|
248
|
+
const sourceMatch = entry.sources.some((pattern) => scaffoldPathMatchesPattern(filePath, pattern))
|
|
249
|
+
if (!sourceMatch) return false
|
|
250
|
+
const excluded = (entry.excludes ?? []).some((pattern) => scaffoldPathMatchesPattern(filePath, pattern))
|
|
251
|
+
return !excluded
|
|
252
|
+
})
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// ---------------------------------------------------------------------------
|
|
256
|
+
// Internal helpers
|
|
257
|
+
// ---------------------------------------------------------------------------
|
|
258
|
+
|
|
259
|
+
function loadScaffoldRegistryNoSyncCheck(root: string): ScaffoldRegistry {
|
|
260
|
+
const yamlPath = path.join(root, YAML_FILENAME)
|
|
261
|
+
let raw: string
|
|
262
|
+
try {
|
|
263
|
+
raw = readFileSync(yamlPath, 'utf-8')
|
|
264
|
+
} catch (err) {
|
|
265
|
+
throw new Error(`scaffold-registry: could not read ${YAML_FILENAME} — ${String(err)}`)
|
|
266
|
+
}
|
|
267
|
+
const parsed = parseYaml(raw) as unknown
|
|
268
|
+
const result = ScaffoldRegistrySchema.safeParse(parsed)
|
|
269
|
+
if (!result.success) {
|
|
270
|
+
const issues = result.error.issues.map((i) => ` [${i.path.join('.')}] ${i.message}`).join('\n')
|
|
271
|
+
throw new Error(`scaffold-registry: YAML validation failed:\n${issues}`)
|
|
272
|
+
}
|
|
273
|
+
return result.data
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
export function normalizeScaffoldPath(p: string): string {
|
|
277
|
+
return p.replace(/\\/g, '/').replace(/^\.\//, '').replace(/\/+$/, '')
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
export function scaffoldPathMatchesPattern(filePath: string, pattern: string): boolean {
|
|
281
|
+
const normalizedFile = normalizeScaffoldPath(filePath)
|
|
282
|
+
const normalizedPattern = normalizeScaffoldPath(pattern)
|
|
283
|
+
|
|
284
|
+
if (!normalizedFile || !normalizedPattern) return false
|
|
285
|
+
if (normalizedFile === normalizedPattern) return true
|
|
286
|
+
|
|
287
|
+
if (!isGlobPattern(normalizedPattern)) {
|
|
288
|
+
return normalizedFile.startsWith(normalizedPattern + '/')
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
return globToRegExp(normalizedPattern).test(normalizedFile)
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function isSymbolicTarget(p: string): boolean {
|
|
295
|
+
return p.startsWith('docs:') || p.startsWith('autogen-target:')
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function isGlobPattern(p: string): boolean {
|
|
299
|
+
return /[*?[]/.test(p)
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function stableRegistryHash(registry: ScaffoldRegistry): string {
|
|
303
|
+
return createHash('sha256').update(JSON.stringify(registry)).digest('hex')
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// Published-boundary note: scripts/lib/glob-matcher.mjs carries a behaviorally
|
|
307
|
+
// identical copy of globToRegExp. The duplication is INTENTIONAL and must NOT be
|
|
308
|
+
// collapsed: @repo/core is a published npm package that cannot import from scripts/,
|
|
309
|
+
// and scripts/ cannot import across the published boundary. If you change the
|
|
310
|
+
// algorithm here, mirror it in scripts/lib/glob-matcher.mjs.
|
|
311
|
+
function globToRegExp(pattern: string): RegExp {
|
|
312
|
+
const segments = pattern.split('/')
|
|
313
|
+
let regex = '^'
|
|
314
|
+
|
|
315
|
+
for (let index = 0; index < segments.length; index++) {
|
|
316
|
+
const segment = segments[index]
|
|
317
|
+
const isLast = index === segments.length - 1
|
|
318
|
+
|
|
319
|
+
if (segment === '**') {
|
|
320
|
+
regex += isLast ? '(?:.*)?' : '(?:[^/]+/)*'
|
|
321
|
+
continue
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
regex += segmentToRegExpSource(segment)
|
|
325
|
+
if (!isLast) regex += '/'
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
regex += '$'
|
|
329
|
+
return new RegExp(regex)
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
function segmentToRegExpSource(segment: string): string {
|
|
333
|
+
let source = ''
|
|
334
|
+
for (let index = 0; index < segment.length; index++) {
|
|
335
|
+
const char = segment[index]
|
|
336
|
+
if (char === '*') {
|
|
337
|
+
source += '[^/]*'
|
|
338
|
+
} else if (char === '?') {
|
|
339
|
+
source += '[^/]'
|
|
340
|
+
} else {
|
|
341
|
+
source += escapeRegExp(char)
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
return source
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
function escapeRegExp(value: string): string {
|
|
348
|
+
return value.replace(/[\\^$.*+?()[\]{}|]/g, '\\$&')
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
function sourcePatternMatchesAnyPath(sourcePattern: string, monorepoRootDir: string): boolean {
|
|
352
|
+
const normalizedPattern = normalizeScaffoldPath(sourcePattern)
|
|
353
|
+
|
|
354
|
+
if (!isGlobPattern(normalizedPattern)) {
|
|
355
|
+
return existsSync(path.join(monorepoRootDir, normalizedPattern))
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
const baseDir = findGlobBaseDirectory(normalizedPattern)
|
|
359
|
+
const absoluteBase = path.join(monorepoRootDir, baseDir)
|
|
360
|
+
if (!existsSync(absoluteBase)) return false
|
|
361
|
+
|
|
362
|
+
return walkUntilMatch(absoluteBase, monorepoRootDir, normalizedPattern)
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
function findGlobBaseDirectory(pattern: string): string {
|
|
366
|
+
const segments = pattern.split('/')
|
|
367
|
+
const baseSegments: string[] = []
|
|
368
|
+
for (const segment of segments) {
|
|
369
|
+
if (isGlobPattern(segment)) break
|
|
370
|
+
baseSegments.push(segment)
|
|
371
|
+
}
|
|
372
|
+
return baseSegments.join('/')
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
function walkUntilMatch(currentPath: string, monorepoRootDir: string, pattern: string): boolean {
|
|
376
|
+
const rel = normalizeScaffoldPath(path.relative(monorepoRootDir, currentPath))
|
|
377
|
+
if (rel && scaffoldPathMatchesPattern(rel, pattern)) return true
|
|
378
|
+
|
|
379
|
+
let entries
|
|
380
|
+
try {
|
|
381
|
+
entries = readdirSync(currentPath, { withFileTypes: true })
|
|
382
|
+
} catch {
|
|
383
|
+
return false
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
for (const entry of entries) {
|
|
387
|
+
const absolutePath = path.join(currentPath, entry.name)
|
|
388
|
+
const relativePath = normalizeScaffoldPath(path.relative(monorepoRootDir, absolutePath))
|
|
389
|
+
|
|
390
|
+
if (scaffoldPathMatchesPattern(relativePath, pattern)) return true
|
|
391
|
+
if (entry.isDirectory()) {
|
|
392
|
+
try {
|
|
393
|
+
if (statSync(absolutePath).isDirectory() && walkUntilMatch(absolutePath, monorepoRootDir, pattern)) {
|
|
394
|
+
return true
|
|
395
|
+
}
|
|
396
|
+
} catch {
|
|
397
|
+
continue
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
return false
|
|
403
|
+
}
|