@elevasis/core 0.10.0 → 0.11.1
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/index.d.ts +69 -159
- package/dist/index.js +324 -613
- package/dist/organization-model/index.d.ts +69 -159
- package/dist/organization-model/index.js +324 -613
- package/dist/test-utils/index.d.ts +192 -45
- package/dist/test-utils/index.js +260 -600
- package/package.json +1 -1
- package/src/__tests__/template-core-compatibility.test.ts +73 -91
- package/src/_gen/__tests__/__snapshots__/contracts.md.snap +94 -182
- package/src/auth/multi-tenancy/index.ts +20 -17
- package/src/auth/multi-tenancy/memberships/api-schemas.ts +142 -126
- package/src/auth/multi-tenancy/memberships/index.ts +26 -22
- package/src/auth/multi-tenancy/permissions.test.ts +42 -0
- package/src/auth/multi-tenancy/permissions.ts +104 -0
- package/src/organization-model/README.md +102 -97
- package/src/organization-model/__tests__/defaults.test.ts +19 -6
- package/src/organization-model/__tests__/domains/resource-mappings.test.ts +24 -93
- package/src/organization-model/__tests__/graph.test.ts +82 -894
- package/src/organization-model/__tests__/resolve.test.ts +59 -690
- package/src/organization-model/__tests__/schema.test.ts +83 -407
- package/src/organization-model/contracts.ts +4 -3
- package/src/organization-model/defaults.ts +277 -141
- package/src/organization-model/domains/features.ts +31 -22
- package/src/organization-model/domains/navigation.ts +27 -20
- package/src/organization-model/foundation.ts +42 -54
- package/src/organization-model/graph/build.ts +42 -217
- package/src/organization-model/graph/index.ts +4 -4
- package/src/organization-model/graph/link.ts +10 -0
- package/src/organization-model/graph/schema.ts +21 -16
- package/src/organization-model/graph/types.ts +10 -10
- package/src/organization-model/helpers.ts +74 -0
- package/src/organization-model/index.ts +7 -7
- package/src/organization-model/organization-graph.mdx +89 -272
- package/src/organization-model/organization-model.mdx +152 -320
- package/src/organization-model/published.ts +20 -19
- package/src/organization-model/resolve.ts +8 -33
- package/src/organization-model/schema.ts +63 -205
- package/src/organization-model/types.ts +12 -11
- package/src/platform/constants/versions.ts +3 -3
- package/src/platform/registry/__tests__/command-view.test.ts +6 -5
- package/src/platform/registry/__tests__/resource-link.test.ts +30 -0
- package/src/platform/registry/__tests__/resource-registry.integration.test.ts +15 -15
- package/src/platform/registry/command-view.ts +10 -12
- package/src/platform/registry/index.ts +93 -93
- package/src/platform/registry/resource-link.ts +32 -0
- package/src/platform/registry/resource-registry.ts +917 -876
- package/src/platform/registry/serialization.ts +56 -73
- package/src/platform/registry/serialized-types.ts +17 -12
- package/src/platform/registry/types.ts +14 -43
- package/src/reference/_generated/contracts.md +94 -182
- package/src/reference/glossary.md +71 -105
- package/src/scaffold-registry/__tests__/index.test.ts +125 -1
- package/src/scaffold-registry/__tests__/schema.test.ts +48 -20
- package/src/scaffold-registry/index.ts +236 -188
- package/src/scaffold-registry/schema.ts +47 -22
- package/src/supabase/database.types.ts +2880 -2719
- package/src/test-utils/fixtures/memberships.ts +82 -80
- package/src/platform/registry/domains.ts +0 -165
|
@@ -1,8 +1,10 @@
|
|
|
1
|
-
import { readFileSync, writeFileSync } from 'node:fs'
|
|
1
|
+
import { existsSync, readFileSync, writeFileSync } from 'node:fs'
|
|
2
2
|
import path from 'node:path'
|
|
3
3
|
import { parse as parseYaml } from 'yaml'
|
|
4
4
|
import { type ScaffoldRegistry, type ScaffoldRegistryEntry, ScaffoldRegistrySchema } from './schema'
|
|
5
5
|
|
|
6
|
+
const MODULE_DIR = path.dirname(new URL(import.meta.url).pathname.replace(/^\/([A-Z]:)/, '$1'))
|
|
7
|
+
|
|
6
8
|
export {
|
|
7
9
|
ExternalSyncCategorySchema,
|
|
8
10
|
ExternalSyncDeletePolicySchema,
|
|
@@ -21,190 +23,236 @@ export type {
|
|
|
21
23
|
ScaffoldRegistry,
|
|
22
24
|
ScaffoldRegistryEntry
|
|
23
25
|
} from './schema'
|
|
24
|
-
|
|
25
|
-
// ---------------------------------------------------------------------------
|
|
26
|
-
// Paths (resolved relative to the monorepo root, not this file's location)
|
|
27
|
-
// ---------------------------------------------------------------------------
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
* Resolve a path relative to the monorepo root.
|
|
31
|
-
* Works whether this module is running from packages/core/src or from dist/.
|
|
32
|
-
*/
|
|
33
|
-
function monorepoRoot(): string {
|
|
34
|
-
// Walk up from __dirname until we find the .claude directory (monorepo marker)
|
|
35
|
-
const { dirname } = path
|
|
36
|
-
let dir =
|
|
37
|
-
for (let i = 0; i < 8; i++) {
|
|
38
|
-
try {
|
|
39
|
-
readFileSync(path.join(dir, '.claude', 'settings.json'))
|
|
40
|
-
return dir
|
|
41
|
-
} catch {
|
|
42
|
-
dir = dirname(dir)
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
throw new Error(
|
|
46
|
-
'scaffold-registry: could not locate monorepo root (no .claude/settings.json found in ancestor directories)'
|
|
47
|
-
)
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
const YAML_FILENAME = '.claude/scaffold-registry.yml'
|
|
51
|
-
const JSON_FILENAME = '.claude/scaffold-registry.compiled.json'
|
|
52
|
-
|
|
53
|
-
// ---------------------------------------------------------------------------
|
|
54
|
-
// Load + validate
|
|
55
|
-
// ---------------------------------------------------------------------------
|
|
56
|
-
|
|
57
|
-
/**
|
|
58
|
-
* Load and Zod-validate the scaffold registry from `.claude/scaffold-registry.yml`.
|
|
59
|
-
*
|
|
60
|
-
* Throws if:
|
|
61
|
-
* - The YAML file is missing or unreadable
|
|
62
|
-
* - The YAML fails Zod validation
|
|
63
|
-
* - The compiled JSON is present but its `entries` count differs from the YAML
|
|
64
|
-
* (drift detection — regenerate with `compileScaffoldRegistry()` to fix)
|
|
65
|
-
*/
|
|
66
|
-
export function loadScaffoldRegistry(): ScaffoldRegistry {
|
|
67
|
-
const root = monorepoRoot()
|
|
68
|
-
const yamlPath = path.join(root, YAML_FILENAME)
|
|
69
|
-
|
|
70
|
-
let raw: string
|
|
71
|
-
try {
|
|
72
|
-
raw = readFileSync(yamlPath, 'utf-8')
|
|
73
|
-
} catch (err) {
|
|
74
|
-
throw new Error(`scaffold-registry: could not read ${YAML_FILENAME} — ${String(err)}`)
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
const parsed = parseYaml(raw) as unknown
|
|
78
|
-
const result = ScaffoldRegistrySchema.safeParse(parsed)
|
|
79
|
-
|
|
80
|
-
if (!result.success) {
|
|
81
|
-
const issues = result.error.issues.map((i) => ` [${i.path.join('.')}] ${i.message}`).join('\n')
|
|
82
|
-
throw new Error(`scaffold-registry: YAML validation failed:\n${issues}`)
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
const registry = result.data
|
|
86
|
-
|
|
87
|
-
// Drift check: if compiled JSON exists, verify entry count matches
|
|
88
|
-
const jsonPath = path.join(root, JSON_FILENAME)
|
|
89
|
-
try {
|
|
90
|
-
const compiledRaw = readFileSync(jsonPath, 'utf-8')
|
|
91
|
-
const compiled = JSON.parse(compiledRaw) as { entries?: unknown[] }
|
|
92
|
-
if (compiled.entries?.length !== registry.entries.length) {
|
|
93
|
-
throw new Error(
|
|
94
|
-
`scaffold-registry: compiled JSON is out of sync with YAML ` +
|
|
95
|
-
`(YAML has ${registry.entries.length} entries, JSON has ${compiled.entries?.length ?? 0}). ` +
|
|
96
|
-
`Run compileScaffoldRegistry() to regenerate.`
|
|
97
|
-
)
|
|
98
|
-
}
|
|
99
|
-
} catch (err) {
|
|
100
|
-
// If the file doesn't exist, skip drift check silently (first-run scenario)
|
|
101
|
-
if ((err as { code?: string }).code !== 'ENOENT') {
|
|
102
|
-
throw err
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
return registry
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
// ---------------------------------------------------------------------------
|
|
110
|
-
// Compile
|
|
111
|
-
// ---------------------------------------------------------------------------
|
|
112
|
-
|
|
113
|
-
/**
|
|
114
|
-
* Load, validate, and write the pre-compiled JSON lookup file.
|
|
115
|
-
* Run this whenever `.claude/scaffold-registry.yml` changes.
|
|
116
|
-
*
|
|
117
|
-
* Called by the `pnpm scaffold:compile-registry` script (wired in Step 2 CI).
|
|
118
|
-
*/
|
|
119
|
-
export function compileScaffoldRegistry(): ScaffoldRegistry {
|
|
120
|
-
const root = monorepoRoot()
|
|
121
|
-
const registry = loadScaffoldRegistryNoSyncCheck(root)
|
|
122
|
-
|
|
123
|
-
const
|
|
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
|
-
const
|
|
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
|
-
}
|
|
26
|
+
|
|
27
|
+
// ---------------------------------------------------------------------------
|
|
28
|
+
// Paths (resolved relative to the monorepo root, not this file's location)
|
|
29
|
+
// ---------------------------------------------------------------------------
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Resolve a path relative to the monorepo root.
|
|
33
|
+
* Works whether this module is running from packages/core/src or from dist/.
|
|
34
|
+
*/
|
|
35
|
+
function monorepoRoot(): string {
|
|
36
|
+
// Walk up from __dirname until we find the .claude directory (monorepo marker)
|
|
37
|
+
const { dirname } = path
|
|
38
|
+
let dir = MODULE_DIR
|
|
39
|
+
for (let i = 0; i < 8; i++) {
|
|
40
|
+
try {
|
|
41
|
+
readFileSync(path.join(dir, '.claude', 'settings.json'))
|
|
42
|
+
return dir
|
|
43
|
+
} catch {
|
|
44
|
+
dir = dirname(dir)
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
throw new Error(
|
|
48
|
+
'scaffold-registry: could not locate monorepo root (no .claude/settings.json found in ancestor directories)'
|
|
49
|
+
)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const YAML_FILENAME = '.claude/scaffold-registry.yml'
|
|
53
|
+
const JSON_FILENAME = '.claude/scaffold-registry.compiled.json'
|
|
54
|
+
|
|
55
|
+
// ---------------------------------------------------------------------------
|
|
56
|
+
// Load + validate
|
|
57
|
+
// ---------------------------------------------------------------------------
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Load and Zod-validate the scaffold registry from `.claude/scaffold-registry.yml`.
|
|
61
|
+
*
|
|
62
|
+
* Throws if:
|
|
63
|
+
* - The YAML file is missing or unreadable
|
|
64
|
+
* - The YAML fails Zod validation
|
|
65
|
+
* - The compiled JSON is present but its `entries` count differs from the YAML
|
|
66
|
+
* (drift detection — regenerate with `compileScaffoldRegistry()` to fix)
|
|
67
|
+
*/
|
|
68
|
+
export function loadScaffoldRegistry(): ScaffoldRegistry {
|
|
69
|
+
const root = monorepoRoot()
|
|
70
|
+
const yamlPath = path.join(root, YAML_FILENAME)
|
|
71
|
+
|
|
72
|
+
let raw: string
|
|
73
|
+
try {
|
|
74
|
+
raw = readFileSync(yamlPath, 'utf-8')
|
|
75
|
+
} catch (err) {
|
|
76
|
+
throw new Error(`scaffold-registry: could not read ${YAML_FILENAME} — ${String(err)}`)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const parsed = parseYaml(raw) as unknown
|
|
80
|
+
const result = ScaffoldRegistrySchema.safeParse(parsed)
|
|
81
|
+
|
|
82
|
+
if (!result.success) {
|
|
83
|
+
const issues = result.error.issues.map((i) => ` [${i.path.join('.')}] ${i.message}`).join('\n')
|
|
84
|
+
throw new Error(`scaffold-registry: YAML validation failed:\n${issues}`)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const registry = result.data
|
|
88
|
+
|
|
89
|
+
// Drift check: if compiled JSON exists, verify entry count matches
|
|
90
|
+
const jsonPath = path.join(root, JSON_FILENAME)
|
|
91
|
+
try {
|
|
92
|
+
const compiledRaw = readFileSync(jsonPath, 'utf-8')
|
|
93
|
+
const compiled = JSON.parse(compiledRaw) as { entries?: unknown[] }
|
|
94
|
+
if (compiled.entries?.length !== registry.entries.length) {
|
|
95
|
+
throw new Error(
|
|
96
|
+
`scaffold-registry: compiled JSON is out of sync with YAML ` +
|
|
97
|
+
`(YAML has ${registry.entries.length} entries, JSON has ${compiled.entries?.length ?? 0}). ` +
|
|
98
|
+
`Run compileScaffoldRegistry() to regenerate.`
|
|
99
|
+
)
|
|
100
|
+
}
|
|
101
|
+
} catch (err) {
|
|
102
|
+
// If the file doesn't exist, skip drift check silently (first-run scenario)
|
|
103
|
+
if ((err as { code?: string }).code !== 'ENOENT') {
|
|
104
|
+
throw err
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return registry
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// ---------------------------------------------------------------------------
|
|
112
|
+
// Compile
|
|
113
|
+
// ---------------------------------------------------------------------------
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Load, validate, and write the pre-compiled JSON lookup file.
|
|
117
|
+
* Run this whenever `.claude/scaffold-registry.yml` changes.
|
|
118
|
+
*
|
|
119
|
+
* Called by the `pnpm scaffold:compile-registry` script (wired in Step 2 CI).
|
|
120
|
+
*/
|
|
121
|
+
export function compileScaffoldRegistry(): ScaffoldRegistry {
|
|
122
|
+
const root = monorepoRoot()
|
|
123
|
+
const registry = loadScaffoldRegistryNoSyncCheck(root)
|
|
124
|
+
|
|
125
|
+
const missing = findMissingDependentPaths(registry, root)
|
|
126
|
+
if (missing.length > 0) {
|
|
127
|
+
const formatted = missing.map((m) => ` [${m.entryId}] ${m.path}`).join('\n')
|
|
128
|
+
throw new Error(
|
|
129
|
+
`scaffold-registry: ${missing.length} dependent path(s) do not exist on disk:\n${formatted}\n` +
|
|
130
|
+
`Fix the typo, create the file, or convert the path to a glob/symbolic target.`
|
|
131
|
+
)
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const jsonPath = path.join(root, JSON_FILENAME)
|
|
135
|
+
writeFileSync(jsonPath, JSON.stringify(registry, null, 2) + '\n', 'utf-8')
|
|
136
|
+
|
|
137
|
+
return registry
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Return dependent paths declared in the registry that don't exist on disk.
|
|
142
|
+
* Skips symbolic targets (`docs:`, `autogen-target:`) and glob patterns
|
|
143
|
+
* (those containing `*`, `?`, or `[`), which can't be resolved to a single file.
|
|
144
|
+
*
|
|
145
|
+
* Exported so external scripts (e.g. CI gates) can run the same check.
|
|
146
|
+
*/
|
|
147
|
+
export function findMissingDependentPaths(
|
|
148
|
+
registry: ScaffoldRegistry,
|
|
149
|
+
monorepoRootDir: string
|
|
150
|
+
): Array<{ entryId: string; path: string }> {
|
|
151
|
+
const missing: Array<{ entryId: string; path: string }> = []
|
|
152
|
+
for (const entry of registry.entries) {
|
|
153
|
+
// sync-preservation dependents describe paths inside derived external
|
|
154
|
+
// projects (not files that physically exist in this monorepo), so skip them.
|
|
155
|
+
if (entry.kind === 'sync-preservation') continue
|
|
156
|
+
for (const dependent of entry.dependents) {
|
|
157
|
+
if (isSymbolicTarget(dependent.path) || isGlobPattern(dependent.path) || dependent.path === '(self)') {
|
|
158
|
+
continue
|
|
159
|
+
}
|
|
160
|
+
const absolute = path.join(monorepoRootDir, dependent.path)
|
|
161
|
+
if (!existsSync(absolute)) {
|
|
162
|
+
missing.push({ entryId: entry.id, path: dependent.path })
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
return missing
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// ---------------------------------------------------------------------------
|
|
170
|
+
// Lookup helpers (used by hooks for fast path matching)
|
|
171
|
+
// ---------------------------------------------------------------------------
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Load from the pre-compiled JSON only (fastest path; used by PostToolUse hooks).
|
|
175
|
+
* Falls back to YAML if the JSON is missing.
|
|
176
|
+
*/
|
|
177
|
+
export function loadScaffoldRegistryFast(): ScaffoldRegistry {
|
|
178
|
+
const root = monorepoRoot()
|
|
179
|
+
const jsonPath = path.join(root, JSON_FILENAME)
|
|
180
|
+
|
|
181
|
+
try {
|
|
182
|
+
const raw = readFileSync(jsonPath, 'utf-8')
|
|
183
|
+
const parsed = JSON.parse(raw) as unknown
|
|
184
|
+
const result = ScaffoldRegistrySchema.safeParse(parsed)
|
|
185
|
+
if (result.success) return result.data
|
|
186
|
+
} catch {
|
|
187
|
+
// fall through to YAML
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return loadScaffoldRegistryNoSyncCheck(root)
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Return all entries whose `sources` contain at least one pattern that matches
|
|
195
|
+
* the given file path. Pattern matching is a simple substring/glob-prefix check
|
|
196
|
+
* suitable for hook use; Step 3 will upgrade to full micromatch when the hook
|
|
197
|
+
* is implemented.
|
|
198
|
+
*/
|
|
199
|
+
export function findMatchingEntries(registry: ScaffoldRegistry, filePath: string): ScaffoldRegistryEntry[] {
|
|
200
|
+
return registry.entries.filter((entry) => entry.sources.some((pattern) => pathMatchesPattern(filePath, pattern)))
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// ---------------------------------------------------------------------------
|
|
204
|
+
// Internal helpers
|
|
205
|
+
// ---------------------------------------------------------------------------
|
|
206
|
+
|
|
207
|
+
function loadScaffoldRegistryNoSyncCheck(root: string): ScaffoldRegistry {
|
|
208
|
+
const yamlPath = path.join(root, YAML_FILENAME)
|
|
209
|
+
let raw: string
|
|
210
|
+
try {
|
|
211
|
+
raw = readFileSync(yamlPath, 'utf-8')
|
|
212
|
+
} catch (err) {
|
|
213
|
+
throw new Error(`scaffold-registry: could not read ${YAML_FILENAME} — ${String(err)}`)
|
|
214
|
+
}
|
|
215
|
+
const parsed = parseYaml(raw) as unknown
|
|
216
|
+
const result = ScaffoldRegistrySchema.safeParse(parsed)
|
|
217
|
+
if (!result.success) {
|
|
218
|
+
const issues = result.error.issues.map((i) => ` [${i.path.join('.')}] ${i.message}`).join('\n')
|
|
219
|
+
throw new Error(`scaffold-registry: YAML validation failed:\n${issues}`)
|
|
220
|
+
}
|
|
221
|
+
return result.data
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function isSymbolicTarget(p: string): boolean {
|
|
225
|
+
return p.startsWith('docs:') || p.startsWith('autogen-target:')
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function isGlobPattern(p: string): boolean {
|
|
229
|
+
return /[*?[]/.test(p)
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Lightweight pattern match: handles exact paths, directory prefixes, and
|
|
234
|
+
* simple `*` wildcards at the end of a segment. Full micromatch lands in Step 3.
|
|
235
|
+
*/
|
|
236
|
+
function pathMatchesPattern(filePath: string, pattern: string): boolean {
|
|
237
|
+
// Normalize separators
|
|
238
|
+
const normalizedFile = filePath.replace(/\\/g, '/')
|
|
239
|
+
const normalizedPattern = pattern.replace(/\\/g, '/')
|
|
240
|
+
|
|
241
|
+
// Exact match
|
|
242
|
+
if (normalizedFile === normalizedPattern) return true
|
|
243
|
+
|
|
244
|
+
// If pattern ends with `/**` or `/*`, check prefix
|
|
245
|
+
if (normalizedPattern.endsWith('/**') || normalizedPattern.endsWith('/*')) {
|
|
246
|
+
const prefix = normalizedPattern.slice(0, normalizedPattern.lastIndexOf('/*'))
|
|
247
|
+
return normalizedFile.startsWith(prefix + '/')
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// If pattern contains `*`, convert to simple regex
|
|
251
|
+
if (normalizedPattern.includes('*')) {
|
|
252
|
+
const escaped = normalizedPattern.replace(/[.+^${}()|[\]\\]/g, '\\$&').replace(/\*/g, '[^/]*')
|
|
253
|
+
return new RegExp(`^${escaped}$`).test(normalizedFile)
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// Directory prefix match (pattern is a directory)
|
|
257
|
+
return normalizedFile.startsWith(normalizedPattern + '/')
|
|
258
|
+
}
|
|
@@ -9,23 +9,17 @@ import { z } from 'zod'
|
|
|
9
9
|
*
|
|
10
10
|
* - autogen: fully derivable from source; regen command is the fix
|
|
11
11
|
* - manual-scaffold: hand-authored structure that must be updated on source change
|
|
12
|
-
* -
|
|
13
|
-
* -
|
|
14
|
-
* -
|
|
15
|
-
|
|
16
|
-
* - validator: drift-detection scripts or CI checks (meta.json pages[] validators, etc.)
|
|
17
|
-
* - other: escape hatch; requires free-form notes
|
|
18
|
-
*/
|
|
12
|
+
* - sync-preservation: external-template files with a declared sync tier
|
|
13
|
+
* - validator: drift-detection scripts or CI checks (meta.json pages[] validators, etc.)
|
|
14
|
+
* - other: escape hatch; requires free-form notes
|
|
15
|
+
*/
|
|
19
16
|
export const ScaffoldEntryKindSchema = z.enum([
|
|
20
17
|
'autogen',
|
|
21
18
|
'manual-scaffold',
|
|
22
|
-
'typed-id',
|
|
23
19
|
'sync-preservation',
|
|
24
|
-
'
|
|
25
|
-
'
|
|
26
|
-
|
|
27
|
-
'other'
|
|
28
|
-
])
|
|
20
|
+
'validator',
|
|
21
|
+
'other'
|
|
22
|
+
])
|
|
29
23
|
|
|
30
24
|
export type ScaffoldEntryKind = z.infer<typeof ScaffoldEntryKindSchema>
|
|
31
25
|
|
|
@@ -78,15 +72,9 @@ export const ScaffoldRefSchema = z.object({
|
|
|
78
72
|
*/
|
|
79
73
|
regen: z.string().min(1).optional(),
|
|
80
74
|
|
|
81
|
-
/**
|
|
82
|
-
*
|
|
83
|
-
|
|
84
|
-
*/
|
|
85
|
-
kind: ScaffoldEntryKindSchema.optional(),
|
|
86
|
-
|
|
87
|
-
/**
|
|
88
|
-
* Human-readable hint shown in reminder messages.
|
|
89
|
-
*/
|
|
75
|
+
/**
|
|
76
|
+
* Human-readable hint shown in reminder messages.
|
|
77
|
+
*/
|
|
90
78
|
hint: z.string().optional()
|
|
91
79
|
})
|
|
92
80
|
|
|
@@ -191,6 +179,43 @@ export const ScaffoldRegistryEntrySchema = z
|
|
|
191
179
|
})
|
|
192
180
|
}
|
|
193
181
|
}
|
|
182
|
+
|
|
183
|
+
if (entry.kind === 'autogen' && !entry.regen) {
|
|
184
|
+
ctx.addIssue({
|
|
185
|
+
code: z.ZodIssueCode.custom,
|
|
186
|
+
path: ['regen'],
|
|
187
|
+
message: 'kind: autogen requires a top-level regen command'
|
|
188
|
+
})
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if (entry.kind === 'validator') {
|
|
192
|
+
const hasVerifier = entry.dependents.some((dependent) => dependent.regen && dependent.regen !== 'manual')
|
|
193
|
+
if (!hasVerifier) {
|
|
194
|
+
ctx.addIssue({
|
|
195
|
+
code: z.ZodIssueCode.custom,
|
|
196
|
+
path: ['dependents'],
|
|
197
|
+
message: 'kind: validator requires at least one executable dependent regen command'
|
|
198
|
+
})
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
if (entry.kind === 'other' && !entry.notes) {
|
|
203
|
+
ctx.addIssue({
|
|
204
|
+
code: z.ZodIssueCode.custom,
|
|
205
|
+
path: ['notes'],
|
|
206
|
+
message: 'kind: other requires notes explaining why no narrower kind fits'
|
|
207
|
+
})
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (entry.kind === 'sync-preservation') {
|
|
211
|
+
if (!entry.category || !entry.strategy || !entry.delete_policy) {
|
|
212
|
+
ctx.addIssue({
|
|
213
|
+
code: z.ZodIssueCode.custom,
|
|
214
|
+
path: ['category'],
|
|
215
|
+
message: 'kind: sync-preservation requires category, strategy, and delete_policy'
|
|
216
|
+
})
|
|
217
|
+
}
|
|
218
|
+
}
|
|
194
219
|
})
|
|
195
220
|
|
|
196
221
|
export type ScaffoldRegistryEntry = z.infer<typeof ScaffoldRegistryEntrySchema>
|