@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.
Files changed (58) hide show
  1. package/dist/index.d.ts +69 -159
  2. package/dist/index.js +324 -613
  3. package/dist/organization-model/index.d.ts +69 -159
  4. package/dist/organization-model/index.js +324 -613
  5. package/dist/test-utils/index.d.ts +192 -45
  6. package/dist/test-utils/index.js +260 -600
  7. package/package.json +1 -1
  8. package/src/__tests__/template-core-compatibility.test.ts +73 -91
  9. package/src/_gen/__tests__/__snapshots__/contracts.md.snap +94 -182
  10. package/src/auth/multi-tenancy/index.ts +20 -17
  11. package/src/auth/multi-tenancy/memberships/api-schemas.ts +142 -126
  12. package/src/auth/multi-tenancy/memberships/index.ts +26 -22
  13. package/src/auth/multi-tenancy/permissions.test.ts +42 -0
  14. package/src/auth/multi-tenancy/permissions.ts +104 -0
  15. package/src/organization-model/README.md +102 -97
  16. package/src/organization-model/__tests__/defaults.test.ts +19 -6
  17. package/src/organization-model/__tests__/domains/resource-mappings.test.ts +24 -93
  18. package/src/organization-model/__tests__/graph.test.ts +82 -894
  19. package/src/organization-model/__tests__/resolve.test.ts +59 -690
  20. package/src/organization-model/__tests__/schema.test.ts +83 -407
  21. package/src/organization-model/contracts.ts +4 -3
  22. package/src/organization-model/defaults.ts +277 -141
  23. package/src/organization-model/domains/features.ts +31 -22
  24. package/src/organization-model/domains/navigation.ts +27 -20
  25. package/src/organization-model/foundation.ts +42 -54
  26. package/src/organization-model/graph/build.ts +42 -217
  27. package/src/organization-model/graph/index.ts +4 -4
  28. package/src/organization-model/graph/link.ts +10 -0
  29. package/src/organization-model/graph/schema.ts +21 -16
  30. package/src/organization-model/graph/types.ts +10 -10
  31. package/src/organization-model/helpers.ts +74 -0
  32. package/src/organization-model/index.ts +7 -7
  33. package/src/organization-model/organization-graph.mdx +89 -272
  34. package/src/organization-model/organization-model.mdx +152 -320
  35. package/src/organization-model/published.ts +20 -19
  36. package/src/organization-model/resolve.ts +8 -33
  37. package/src/organization-model/schema.ts +63 -205
  38. package/src/organization-model/types.ts +12 -11
  39. package/src/platform/constants/versions.ts +3 -3
  40. package/src/platform/registry/__tests__/command-view.test.ts +6 -5
  41. package/src/platform/registry/__tests__/resource-link.test.ts +30 -0
  42. package/src/platform/registry/__tests__/resource-registry.integration.test.ts +15 -15
  43. package/src/platform/registry/command-view.ts +10 -12
  44. package/src/platform/registry/index.ts +93 -93
  45. package/src/platform/registry/resource-link.ts +32 -0
  46. package/src/platform/registry/resource-registry.ts +917 -876
  47. package/src/platform/registry/serialization.ts +56 -73
  48. package/src/platform/registry/serialized-types.ts +17 -12
  49. package/src/platform/registry/types.ts +14 -43
  50. package/src/reference/_generated/contracts.md +94 -182
  51. package/src/reference/glossary.md +71 -105
  52. package/src/scaffold-registry/__tests__/index.test.ts +125 -1
  53. package/src/scaffold-registry/__tests__/schema.test.ts +48 -20
  54. package/src/scaffold-registry/index.ts +236 -188
  55. package/src/scaffold-registry/schema.ts +47 -22
  56. package/src/supabase/database.types.ts +2880 -2719
  57. package/src/test-utils/fixtures/memberships.ts +82 -80
  58. 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 = __dirname
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 jsonPath = path.join(root, JSON_FILENAME)
124
- writeFileSync(jsonPath, JSON.stringify(registry, null, 2) + '\n', 'utf-8')
125
-
126
- return registry
127
- }
128
-
129
- // ---------------------------------------------------------------------------
130
- // Lookup helpers (used by hooks for fast path matching)
131
- // ---------------------------------------------------------------------------
132
-
133
- /**
134
- * Load from the pre-compiled JSON only (fastest path; used by PostToolUse hooks).
135
- * Falls back to YAML if the JSON is missing.
136
- */
137
- export function loadScaffoldRegistryFast(): ScaffoldRegistry {
138
- const root = monorepoRoot()
139
- const jsonPath = path.join(root, JSON_FILENAME)
140
-
141
- try {
142
- const raw = readFileSync(jsonPath, 'utf-8')
143
- const parsed = JSON.parse(raw) as unknown
144
- const result = ScaffoldRegistrySchema.safeParse(parsed)
145
- if (result.success) return result.data
146
- } catch {
147
- // fall through to YAML
148
- }
149
-
150
- return loadScaffoldRegistryNoSyncCheck(root)
151
- }
152
-
153
- /**
154
- * Return all entries whose `sources` contain at least one pattern that matches
155
- * the given file path. Pattern matching is a simple substring/glob-prefix check
156
- * suitable for hook use; Step 3 will upgrade to full micromatch when the hook
157
- * is implemented.
158
- */
159
- export function findMatchingEntries(registry: ScaffoldRegistry, filePath: string): ScaffoldRegistryEntry[] {
160
- return registry.entries.filter((entry) => entry.sources.some((pattern) => pathMatchesPattern(filePath, pattern)))
161
- }
162
-
163
- // ---------------------------------------------------------------------------
164
- // Internal helpers
165
- // ---------------------------------------------------------------------------
166
-
167
- function loadScaffoldRegistryNoSyncCheck(root: string): ScaffoldRegistry {
168
- const yamlPath = path.join(root, YAML_FILENAME)
169
- let raw: string
170
- try {
171
- raw = readFileSync(yamlPath, 'utf-8')
172
- } catch (err) {
173
- throw new Error(`scaffold-registry: could not read ${YAML_FILENAME} ${String(err)}`)
174
- }
175
- const parsed = parseYaml(raw) as unknown
176
- const result = ScaffoldRegistrySchema.safeParse(parsed)
177
- if (!result.success) {
178
- const issues = result.error.issues.map((i) => ` [${i.path.join('.')}] ${i.message}`).join('\n')
179
- throw new Error(`scaffold-registry: YAML validation failed:\n${issues}`)
180
- }
181
- return result.data
182
- }
183
-
184
- /**
185
- * Lightweight pattern match: handles exact paths, directory prefixes, and
186
- * simple `*` wildcards at the end of a segment. Full micromatch lands in Step 3.
187
- */
188
- function pathMatchesPattern(filePath: string, pattern: string): boolean {
189
- // Normalize separators
190
- const normalizedFile = filePath.replace(/\\/g, '/')
191
- const normalizedPattern = pattern.replace(/\\/g, '/')
192
-
193
- // Exact match
194
- if (normalizedFile === normalizedPattern) return true
195
-
196
- // If pattern ends with `/**` or `/*`, check prefix
197
- if (normalizedPattern.endsWith('/**') || normalizedPattern.endsWith('/*')) {
198
- const prefix = normalizedPattern.slice(0, normalizedPattern.lastIndexOf('/*'))
199
- return normalizedFile.startsWith(prefix + '/')
200
- }
201
-
202
- // If pattern contains `*`, convert to simple regex
203
- if (normalizedPattern.includes('*')) {
204
- const escaped = normalizedPattern.replace(/[.+^${}()|[\]\\]/g, '\\$&').replace(/\*/g, '[^/]*')
205
- return new RegExp(`^${escaped}$`).test(normalizedFile)
206
- }
207
-
208
- // Directory prefix match (pattern is a directory)
209
- return normalizedFile.startsWith(normalizedPattern + '/')
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
- * - typed-id: typed-ID constants that encode semantic naming decisions
13
- * - sync-preservation: external-template files with a declared sync tier
14
- * - vibe-gated: paths protected by the pre-edit-vibe-gate hook
15
- * - sdk-cli-generator: SDK CLI generators (generate-docs-index, generate-resources, etc.)
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
- 'vibe-gated',
25
- 'sdk-cli-generator',
26
- 'validator',
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
- * Optionally narrow the kind of this specific scaffold reference, when it
83
- * differs from the parent entry's kind.
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>