@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,206 +1,380 @@
|
|
|
1
|
-
import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from 'node:fs'
|
|
2
|
-
import { tmpdir } from 'node:os'
|
|
3
|
-
import path from 'node:path'
|
|
4
|
-
import { afterEach, describe, expect, it } from 'vitest'
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
})
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
{ path: '
|
|
88
|
-
{ path: '
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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
|
-
expect(matchedIds(reg, 'packages/core/
|
|
132
|
-
expect(matchedIds(reg, 'packages/core/src')).toEqual([])
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
expect(matchedIds(reg, 'packages/core/
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
expect(matchedIds(reg, 'apps/api/src/foo.
|
|
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
|
-
makeEntry('
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
1
|
+
import { mkdtempSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
|
|
2
|
+
import { tmpdir } from 'node:os'
|
|
3
|
+
import path from 'node:path'
|
|
4
|
+
import { afterEach, describe, expect, it } from 'vitest'
|
|
5
|
+
import {
|
|
6
|
+
compileScaffoldRegistry,
|
|
7
|
+
findEmptySourcePatterns,
|
|
8
|
+
findMatchingEntries,
|
|
9
|
+
findMissingDependentPaths,
|
|
10
|
+
loadScaffoldRegistry,
|
|
11
|
+
loadScaffoldRegistryFast,
|
|
12
|
+
normalizeScaffoldPath,
|
|
13
|
+
scaffoldPathMatchesPattern
|
|
14
|
+
} from '../index'
|
|
15
|
+
import type { ScaffoldRegistry, ScaffoldRegistryEntry } from '../schema'
|
|
16
|
+
|
|
17
|
+
function makeEntry(id: string, sources: string[]): ScaffoldRegistryEntry {
|
|
18
|
+
return {
|
|
19
|
+
id,
|
|
20
|
+
kind: 'manual-scaffold',
|
|
21
|
+
owner: 'packages/core',
|
|
22
|
+
sources,
|
|
23
|
+
dependents: [{ path: 'packages/core/package.json', regen: 'manual', hint: 'h' }]
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function registryFromEntries(entries: ScaffoldRegistryEntry[]): ScaffoldRegistry {
|
|
28
|
+
return { version: '1', entries }
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function matchedIds(registry: ScaffoldRegistry, filePath: string): string[] {
|
|
32
|
+
return findMatchingEntries(registry, filePath).map((e) => e.id)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
let tempRoots: string[] = []
|
|
36
|
+
|
|
37
|
+
function makeTempRoot(): string {
|
|
38
|
+
const root = mkdtempSync(path.join(tmpdir(), 'scaffold-registry-'))
|
|
39
|
+
tempRoots.push(root)
|
|
40
|
+
return root
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
afterEach(() => {
|
|
44
|
+
for (const root of tempRoots) {
|
|
45
|
+
rmSync(root, { recursive: true, force: true })
|
|
46
|
+
}
|
|
47
|
+
tempRoots = []
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
describe('loadScaffoldRegistryFast', () => {
|
|
51
|
+
it('preserves external sync metadata from the compiled registry', () => {
|
|
52
|
+
const registry = loadScaffoldRegistryFast()
|
|
53
|
+
const entry = registry.entries.find((candidate) => candidate.id === 'external-sync-managed-claude')
|
|
54
|
+
|
|
55
|
+
expect(entry).toBeDefined()
|
|
56
|
+
expect(entry).toMatchObject({
|
|
57
|
+
owner: 'template',
|
|
58
|
+
category: 'replace',
|
|
59
|
+
strategy: 'replace-all',
|
|
60
|
+
delete_policy: 'manifest-only'
|
|
61
|
+
})
|
|
62
|
+
})
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
describe('findMissingDependentPaths', () => {
|
|
66
|
+
const baseEntry = {
|
|
67
|
+
id: 'fake-entry',
|
|
68
|
+
kind: 'manual-scaffold' as const,
|
|
69
|
+
owner: 'packages/core',
|
|
70
|
+
sources: ['packages/core/src/**'],
|
|
71
|
+
dependents: [{ path: 'packages/core/this-file-does-not-exist.ts', regen: 'manual', hint: 'h' }]
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
it('flags dependents whose paths are missing on disk', () => {
|
|
75
|
+
const registry: ScaffoldRegistry = { version: '1', entries: [baseEntry] }
|
|
76
|
+
const missing = findMissingDependentPaths(registry, process.cwd())
|
|
77
|
+
expect(missing).toEqual([{ entryId: 'fake-entry', path: 'packages/core/this-file-does-not-exist.ts' }])
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
it('skips symbolic targets, globs, and (self)', () => {
|
|
81
|
+
const registry: ScaffoldRegistry = {
|
|
82
|
+
version: '1',
|
|
83
|
+
entries: [
|
|
84
|
+
{
|
|
85
|
+
...baseEntry,
|
|
86
|
+
dependents: [
|
|
87
|
+
{ path: 'docs: sync-preservation-matrix', regen: 'manual', hint: 'h' },
|
|
88
|
+
{ path: 'autogen-target:foo', regen: 'manual', hint: 'h' },
|
|
89
|
+
{ path: 'packages/core/src/**/*.ts', regen: 'manual', hint: 'h' },
|
|
90
|
+
{ path: '(self)', regen: 'manual', hint: 'h' }
|
|
91
|
+
]
|
|
92
|
+
}
|
|
93
|
+
]
|
|
94
|
+
}
|
|
95
|
+
expect(findMissingDependentPaths(registry, process.cwd())).toEqual([])
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
it('skips sync-preservation entries (their paths target derived external projects)', () => {
|
|
99
|
+
const registry: ScaffoldRegistry = {
|
|
100
|
+
version: '1',
|
|
101
|
+
entries: [
|
|
102
|
+
{
|
|
103
|
+
id: 'sync-entry',
|
|
104
|
+
kind: 'sync-preservation',
|
|
105
|
+
owner: '@elevasis/core',
|
|
106
|
+
category: 'merge',
|
|
107
|
+
strategy: 'merge-regions',
|
|
108
|
+
delete_policy: 'none',
|
|
109
|
+
sources: ['external/_template/foo.ts'],
|
|
110
|
+
dependents: [{ path: 'core/src/foo-not-in-monorepo.ts', regen: 'manual', hint: 'h' }]
|
|
111
|
+
}
|
|
112
|
+
]
|
|
113
|
+
}
|
|
114
|
+
expect(findMissingDependentPaths(registry, process.cwd())).toEqual([])
|
|
115
|
+
})
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
describe('findMatchingEntries (pattern matching)', () => {
|
|
119
|
+
it('normalizes Windows and relative paths', () => {
|
|
120
|
+
expect(normalizeScaffoldPath('.\\packages\\core\\src\\')).toBe('packages/core/src')
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
it('matches exact paths', () => {
|
|
124
|
+
const reg = registryFromEntries([makeEntry('exact', ['packages/core/src/index.ts'])])
|
|
125
|
+
expect(matchedIds(reg, 'packages/core/src/index.ts')).toEqual(['exact'])
|
|
126
|
+
expect(matchedIds(reg, 'packages/core/src/other.ts')).toEqual([])
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
it('matches /** suffix as deep prefix', () => {
|
|
130
|
+
const reg = registryFromEntries([makeEntry('deep', ['packages/core/src/**'])])
|
|
131
|
+
expect(matchedIds(reg, 'packages/core/src/foo.ts')).toEqual(['deep'])
|
|
132
|
+
expect(matchedIds(reg, 'packages/core/src/nested/deeply/foo.ts')).toEqual(['deep'])
|
|
133
|
+
expect(matchedIds(reg, 'packages/core/package.json')).toEqual([])
|
|
134
|
+
expect(matchedIds(reg, 'packages/core/src')).toEqual([])
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
it('matches /* suffix as single-level prefix', () => {
|
|
138
|
+
const reg = registryFromEntries([makeEntry('shallow', ['packages/*/package.json'])])
|
|
139
|
+
expect(matchedIds(reg, 'packages/core/package.json')).toEqual(['shallow'])
|
|
140
|
+
expect(matchedIds(reg, 'packages/ui/package.json')).toEqual(['shallow'])
|
|
141
|
+
expect(matchedIds(reg, 'packages/core/nested/package.json')).toEqual([])
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
it('matches mixed **/*.ext patterns at direct-child and nested depths', () => {
|
|
145
|
+
const reg = registryFromEntries([makeEntry('ext', ['apps/api/src/**/*.ts'])])
|
|
146
|
+
expect(matchedIds(reg, 'apps/api/src/foo.ts')).toEqual(['ext'])
|
|
147
|
+
expect(matchedIds(reg, 'apps/api/src/nested/deeply/foo.ts')).toEqual(['ext'])
|
|
148
|
+
expect(matchedIds(reg, 'apps/api/src/foo.tsx')).toEqual([])
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
it('normalizes Windows backslashes to forward slashes', () => {
|
|
152
|
+
const reg = registryFromEntries([makeEntry('winpath', ['packages/core/src/**'])])
|
|
153
|
+
expect(matchedIds(reg, 'packages\\core\\src\\foo.ts')).toEqual(['winpath'])
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
it('matches a directory-prefix pattern (no glob characters)', () => {
|
|
157
|
+
const reg = registryFromEntries([makeEntry('dir', ['packages/core/src'])])
|
|
158
|
+
expect(matchedIds(reg, 'packages/core/src/foo.ts')).toEqual(['dir'])
|
|
159
|
+
expect(matchedIds(reg, 'packages/core/srcfoo.ts')).toEqual([])
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
it('returns multiple entries when several match the same file', () => {
|
|
163
|
+
const reg = registryFromEntries([makeEntry('a', ['packages/core/src/**']), makeEntry('b', ['packages/core/src'])])
|
|
164
|
+
expect(matchedIds(reg, 'packages/core/src/foo.ts').sort()).toEqual(['a', 'b'])
|
|
165
|
+
})
|
|
166
|
+
|
|
167
|
+
it('returns empty when no entries match', () => {
|
|
168
|
+
const reg = registryFromEntries([makeEntry('a', ['packages/core/src/**'])])
|
|
169
|
+
expect(matchedIds(reg, 'apps/api/src/foo.ts')).toEqual([])
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
it('exposes the same matcher for direct consumers', () => {
|
|
173
|
+
expect(scaffoldPathMatchesPattern('packages/core/src/foo.test.ts', 'packages/core/src/**/*.ts')).toBe(true)
|
|
174
|
+
expect(scaffoldPathMatchesPattern('packages/core/src/foo.test.ts', 'packages/ui/src/**/*.ts')).toBe(false)
|
|
175
|
+
})
|
|
176
|
+
})
|
|
177
|
+
|
|
178
|
+
describe('findEmptySourcePatterns', () => {
|
|
179
|
+
it('flags source patterns that do not match any file or directory', () => {
|
|
180
|
+
const root = makeTempRoot()
|
|
181
|
+
mkdirSync(path.join(root, 'packages', 'core', 'src'), { recursive: true })
|
|
182
|
+
writeFileSync(path.join(root, 'packages', 'core', 'src', 'index.ts'), 'export {}\n', 'utf8')
|
|
183
|
+
|
|
184
|
+
const registry = registryFromEntries([
|
|
185
|
+
makeEntry('ok-exact', ['packages/core/src/index.ts']),
|
|
186
|
+
makeEntry('ok-glob', ['packages/core/src/**/*.ts']),
|
|
187
|
+
makeEntry('empty', ['packages/core/src/**/*.tsx'])
|
|
188
|
+
])
|
|
189
|
+
|
|
190
|
+
expect(findEmptySourcePatterns(registry, root)).toEqual([
|
|
191
|
+
{ entryId: 'empty', pattern: 'packages/core/src/**/*.tsx' }
|
|
192
|
+
])
|
|
193
|
+
})
|
|
194
|
+
|
|
195
|
+
it('treats exact directories as non-empty source coverage', () => {
|
|
196
|
+
const root = makeTempRoot()
|
|
197
|
+
mkdirSync(path.join(root, 'external', '_template', '.claude', 'skills'), { recursive: true })
|
|
198
|
+
|
|
199
|
+
const registry = registryFromEntries([
|
|
200
|
+
makeEntry('dir', ['external/_template/.claude/skills']),
|
|
201
|
+
makeEntry('missing-dir', ['external/_template/.claude/commands'])
|
|
202
|
+
])
|
|
203
|
+
|
|
204
|
+
expect(findEmptySourcePatterns(registry, root)).toEqual([
|
|
205
|
+
{ entryId: 'missing-dir', pattern: 'external/_template/.claude/commands' }
|
|
206
|
+
])
|
|
207
|
+
})
|
|
208
|
+
})
|
|
209
|
+
|
|
210
|
+
// ---------------------------------------------------------------------------
|
|
211
|
+
// Fixture helpers for compileScaffoldRegistry / loadScaffoldRegistry tests
|
|
212
|
+
// ---------------------------------------------------------------------------
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Write a minimal scaffold-registry.yml into the fixture root.
|
|
216
|
+
* `kind: manual-scaffold` is used to avoid the `regen` requirement of `kind: autogen`.
|
|
217
|
+
*/
|
|
218
|
+
function writeMinimalYaml(root: string, extraEntries?: string): void {
|
|
219
|
+
mkdirSync(path.join(root, '.claude', 'registries'), { recursive: true })
|
|
220
|
+
// Must have at least one real source path that exists so compileScaffoldRegistry
|
|
221
|
+
// does not reject it with "source pattern matches no files". We create the file.
|
|
222
|
+
mkdirSync(path.join(root, 'src'), { recursive: true })
|
|
223
|
+
writeFileSync(path.join(root, 'src', 'index.ts'), 'export {}\n', 'utf8')
|
|
224
|
+
const yaml = [
|
|
225
|
+
'version: "1"',
|
|
226
|
+
'entries:',
|
|
227
|
+
' - id: fixture-entry',
|
|
228
|
+
' kind: manual-scaffold',
|
|
229
|
+
' owner: packages/core',
|
|
230
|
+
' sources:',
|
|
231
|
+
' - src/index.ts',
|
|
232
|
+
' dependents:',
|
|
233
|
+
' - path: src/index.ts',
|
|
234
|
+
...(extraEntries ? [extraEntries] : [])
|
|
235
|
+
].join('\n')
|
|
236
|
+
writeFileSync(path.join(root, '.claude', 'registries', 'scaffold-registry.yml'), yaml + '\n', 'utf8')
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function compiledJsonPath(root: string): string {
|
|
240
|
+
return path.join(root, '.claude', 'registries', 'scaffold-registry.compiled.json')
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// ---------------------------------------------------------------------------
|
|
244
|
+
// compileScaffoldRegistry
|
|
245
|
+
// ---------------------------------------------------------------------------
|
|
246
|
+
|
|
247
|
+
describe('compileScaffoldRegistry', () => {
|
|
248
|
+
it('writes scaffold-registry.compiled.json that round-trips the YAML entries', () => {
|
|
249
|
+
const root = makeTempRoot()
|
|
250
|
+
writeMinimalYaml(root)
|
|
251
|
+
|
|
252
|
+
const registry = compileScaffoldRegistry(root)
|
|
253
|
+
|
|
254
|
+
// Return value has the entry
|
|
255
|
+
expect(registry.entries).toHaveLength(1)
|
|
256
|
+
expect(registry.entries[0].id).toBe('fixture-entry')
|
|
257
|
+
|
|
258
|
+
// Written file exists and is valid JSON
|
|
259
|
+
const raw = readFileSync(compiledJsonPath(root), 'utf-8')
|
|
260
|
+
const parsed = JSON.parse(raw) as { version: string; entries: Array<{ id: string }> }
|
|
261
|
+
expect(parsed.version).toBe('1')
|
|
262
|
+
expect(parsed.entries).toHaveLength(1)
|
|
263
|
+
expect(parsed.entries[0].id).toBe('fixture-entry')
|
|
264
|
+
})
|
|
265
|
+
|
|
266
|
+
it('written JSON ends with a trailing newline', () => {
|
|
267
|
+
const root = makeTempRoot()
|
|
268
|
+
writeMinimalYaml(root)
|
|
269
|
+
|
|
270
|
+
compileScaffoldRegistry(root)
|
|
271
|
+
|
|
272
|
+
const raw = readFileSync(compiledJsonPath(root), 'utf-8')
|
|
273
|
+
expect(raw.endsWith('\n')).toBe(true)
|
|
274
|
+
})
|
|
275
|
+
|
|
276
|
+
it('throws when a non-glob dependent path does not exist on disk', () => {
|
|
277
|
+
const root = makeTempRoot()
|
|
278
|
+
mkdirSync(path.join(root, '.claude', 'registries'), { recursive: true })
|
|
279
|
+
mkdirSync(path.join(root, 'src'), { recursive: true })
|
|
280
|
+
writeFileSync(path.join(root, 'src', 'index.ts'), 'export {}\n', 'utf8')
|
|
281
|
+
// Dependent references a file that does NOT exist
|
|
282
|
+
const yaml = [
|
|
283
|
+
'version: "1"',
|
|
284
|
+
'entries:',
|
|
285
|
+
' - id: missing-dep',
|
|
286
|
+
' kind: manual-scaffold',
|
|
287
|
+
' owner: packages/core',
|
|
288
|
+
' sources:',
|
|
289
|
+
' - src/index.ts',
|
|
290
|
+
' dependents:',
|
|
291
|
+
' - path: src/does-not-exist.ts'
|
|
292
|
+
].join('\n')
|
|
293
|
+
writeFileSync(path.join(root, '.claude', 'registries', 'scaffold-registry.yml'), yaml + '\n', 'utf8')
|
|
294
|
+
|
|
295
|
+
expect(() => compileScaffoldRegistry(root)).toThrow('scaffold-registry:')
|
|
296
|
+
expect(() => compileScaffoldRegistry(root)).toThrow('dependent path(s) do not exist on disk')
|
|
297
|
+
expect(() => compileScaffoldRegistry(root)).toThrow('[missing-dep] src/does-not-exist.ts')
|
|
298
|
+
})
|
|
299
|
+
|
|
300
|
+
it('throws when a source pattern matches no files or directories', () => {
|
|
301
|
+
const root = makeTempRoot()
|
|
302
|
+
mkdirSync(path.join(root, '.claude', 'registries'), { recursive: true })
|
|
303
|
+
mkdirSync(path.join(root, 'src'), { recursive: true })
|
|
304
|
+
writeFileSync(path.join(root, 'src', 'index.ts'), 'export {}\n', 'utf8')
|
|
305
|
+
// Source glob matches nothing
|
|
306
|
+
const yaml = [
|
|
307
|
+
'version: "1"',
|
|
308
|
+
'entries:',
|
|
309
|
+
' - id: empty-source',
|
|
310
|
+
' kind: manual-scaffold',
|
|
311
|
+
' owner: packages/core',
|
|
312
|
+
' sources:',
|
|
313
|
+
' - src/**/*.tsx',
|
|
314
|
+
' dependents:',
|
|
315
|
+
' - path: src/index.ts'
|
|
316
|
+
].join('\n')
|
|
317
|
+
writeFileSync(path.join(root, '.claude', 'registries', 'scaffold-registry.yml'), yaml + '\n', 'utf8')
|
|
318
|
+
|
|
319
|
+
expect(() => compileScaffoldRegistry(root)).toThrow('scaffold-registry:')
|
|
320
|
+
expect(() => compileScaffoldRegistry(root)).toThrow('source pattern(s) match no files or directories')
|
|
321
|
+
expect(() => compileScaffoldRegistry(root)).toThrow('[empty-source] src/**/*.tsx')
|
|
322
|
+
})
|
|
323
|
+
})
|
|
324
|
+
|
|
325
|
+
// ---------------------------------------------------------------------------
|
|
326
|
+
// loadScaffoldRegistry — hash-drift detection
|
|
327
|
+
// ---------------------------------------------------------------------------
|
|
328
|
+
|
|
329
|
+
describe('loadScaffoldRegistry', () => {
|
|
330
|
+
it('returns the registry when YAML and compiled JSON hashes match', () => {
|
|
331
|
+
const root = makeTempRoot()
|
|
332
|
+
writeMinimalYaml(root)
|
|
333
|
+
// Compile first so the hashes are in sync
|
|
334
|
+
compileScaffoldRegistry(root)
|
|
335
|
+
|
|
336
|
+
const registry = loadScaffoldRegistry(root)
|
|
337
|
+
expect(registry.entries[0].id).toBe('fixture-entry')
|
|
338
|
+
})
|
|
339
|
+
|
|
340
|
+
it('throws when compiled JSON is out of sync with YAML', () => {
|
|
341
|
+
const root = makeTempRoot()
|
|
342
|
+
writeMinimalYaml(root)
|
|
343
|
+
// Compile to create a valid compiled JSON
|
|
344
|
+
compileScaffoldRegistry(root)
|
|
345
|
+
|
|
346
|
+
// Now write a DIFFERENT YAML (adds a new entry) without recompiling
|
|
347
|
+
mkdirSync(path.join(root, 'src'), { recursive: true })
|
|
348
|
+
writeFileSync(path.join(root, 'src', 'other.ts'), 'export {}\n', 'utf8')
|
|
349
|
+
const updatedYaml = [
|
|
350
|
+
'version: "1"',
|
|
351
|
+
'entries:',
|
|
352
|
+
' - id: fixture-entry',
|
|
353
|
+
' kind: manual-scaffold',
|
|
354
|
+
' owner: packages/core',
|
|
355
|
+
' sources:',
|
|
356
|
+
' - src/index.ts',
|
|
357
|
+
' dependents:',
|
|
358
|
+
' - path: src/index.ts',
|
|
359
|
+
' - id: new-entry',
|
|
360
|
+
' kind: manual-scaffold',
|
|
361
|
+
' owner: packages/core',
|
|
362
|
+
' sources:',
|
|
363
|
+
' - src/other.ts',
|
|
364
|
+
' dependents:',
|
|
365
|
+
' - path: src/other.ts'
|
|
366
|
+
].join('\n')
|
|
367
|
+
writeFileSync(path.join(root, '.claude', 'registries', 'scaffold-registry.yml'), updatedYaml + '\n', 'utf8')
|
|
368
|
+
|
|
369
|
+
expect(() => loadScaffoldRegistry(root)).toThrow('compiled JSON is out of sync with YAML')
|
|
370
|
+
})
|
|
371
|
+
|
|
372
|
+
it('skips drift check silently when compiled JSON does not exist (ENOENT)', () => {
|
|
373
|
+
const root = makeTempRoot()
|
|
374
|
+
writeMinimalYaml(root)
|
|
375
|
+
// No compiled JSON — ENOENT path should be silent (no throw)
|
|
376
|
+
|
|
377
|
+
const registry = loadScaffoldRegistry(root)
|
|
378
|
+
expect(registry.entries[0].id).toBe('fixture-entry')
|
|
379
|
+
})
|
|
380
|
+
})
|