@elevasis/core 0.11.2 → 0.13.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.
Files changed (50) hide show
  1. package/dist/index.d.ts +2 -1
  2. package/dist/index.js +8 -1
  3. package/dist/organization-model/index.d.ts +2 -1
  4. package/dist/organization-model/index.js +8 -1
  5. package/dist/test-utils/index.d.ts +27 -15
  6. package/dist/test-utils/index.js +25 -0
  7. package/package.json +1 -1
  8. package/src/_gen/__tests__/__snapshots__/contracts.md.snap +27 -270
  9. package/src/auth/multi-tenancy/credentials/__tests__/encryption.test.ts +217 -216
  10. package/src/auth/multi-tenancy/credentials/server/encryption.ts +69 -39
  11. package/src/auth/multi-tenancy/credentials/server/kek-loader.ts +37 -0
  12. package/src/auth/multi-tenancy/index.ts +3 -0
  13. package/src/auth/multi-tenancy/invitations/api-schemas.ts +104 -107
  14. package/src/auth/multi-tenancy/memberships/api-schemas.ts +6 -5
  15. package/src/auth/multi-tenancy/memberships/membership.ts +130 -138
  16. package/src/auth/multi-tenancy/permissions.ts +12 -5
  17. package/src/auth/multi-tenancy/role-management/api-schemas.ts +78 -0
  18. package/src/auth/multi-tenancy/role-management/index.ts +16 -0
  19. package/src/business/acquisition/activity-events.ts +142 -0
  20. package/src/business/acquisition/api-schemas.ts +694 -689
  21. package/src/business/acquisition/derive-actions.ts +90 -0
  22. package/src/business/acquisition/index.ts +111 -109
  23. package/src/execution/engine/index.ts +434 -434
  24. package/src/execution/engine/tools/integration/server/adapters/apify/__tests__/apify-run-actor.integration.test.ts +298 -293
  25. package/src/execution/engine/tools/integration/server/adapters/attio/__tests__/attio-crud.integration.test.ts +0 -1
  26. package/src/execution/engine/tools/integration/service.test.ts +214 -0
  27. package/src/execution/engine/tools/integration/service.ts +169 -161
  28. package/src/execution/engine/tools/lead-service-types.ts +882 -879
  29. package/src/execution/engine/tools/registry.ts +699 -700
  30. package/src/execution/engine/tools/tool-maps.ts +777 -780
  31. package/src/integrations/credentials/__tests__/api-schemas.test.ts +420 -496
  32. package/src/integrations/credentials/api-schemas.ts +127 -143
  33. package/src/integrations/webhook-endpoints/__tests__/api-schemas.test.ts +327 -318
  34. package/src/integrations/webhook-endpoints/api-schemas.ts +103 -102
  35. package/src/integrations/webhook-endpoints/types.ts +58 -51
  36. package/src/operations/activities/api-schemas.ts +80 -79
  37. package/src/operations/activities/types.ts +64 -63
  38. package/src/organization-model/contracts.ts +1 -0
  39. package/src/organization-model/defaults.ts +6 -0
  40. package/src/organization-model/domains/navigation.ts +37 -23
  41. package/src/organization-model/organization-graph.mdx +2 -2
  42. package/src/organization-model/published.ts +2 -1
  43. package/src/platform/constants/versions.ts +1 -1
  44. package/src/reference/_generated/contracts.md +27 -270
  45. package/src/scaffold-registry/__tests__/index.test.ts +72 -7
  46. package/src/scaffold-registry/index.ts +163 -29
  47. package/src/scaffold-registry/schema.ts +68 -62
  48. package/src/server.ts +281 -272
  49. package/src/supabase/database.types.ts +16 -10
  50. package/src/test-utils/rls/RLSTestContext.ts +585 -553
@@ -1,5 +1,15 @@
1
- import { describe, expect, it } from 'vitest'
2
- import { findMatchingEntries, findMissingDependentPaths, loadScaffoldRegistryFast } from '../index'
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
+ findEmptySourcePatterns,
7
+ findMatchingEntries,
8
+ findMissingDependentPaths,
9
+ loadScaffoldRegistryFast,
10
+ normalizeScaffoldPath,
11
+ scaffoldPathMatchesPattern
12
+ } from '../index'
3
13
  import type { ScaffoldRegistry, ScaffoldRegistryEntry } from '../schema'
4
14
 
5
15
  function makeEntry(id: string, sources: string[]): ScaffoldRegistryEntry {
@@ -20,6 +30,21 @@ function matchedIds(registry: ScaffoldRegistry, filePath: string): string[] {
20
30
  return findMatchingEntries(registry, filePath).map((e) => e.id)
21
31
  }
22
32
 
33
+ let tempRoots: string[] = []
34
+
35
+ function makeTempRoot(): string {
36
+ const root = mkdtempSync(path.join(tmpdir(), 'scaffold-registry-'))
37
+ tempRoots.push(root)
38
+ return root
39
+ }
40
+
41
+ afterEach(() => {
42
+ for (const root of tempRoots) {
43
+ rmSync(root, { recursive: true, force: true })
44
+ }
45
+ tempRoots = []
46
+ })
47
+
23
48
  describe('loadScaffoldRegistryFast', () => {
24
49
  it('preserves external sync metadata from the compiled registry', () => {
25
50
  const registry = loadScaffoldRegistryFast()
@@ -89,6 +114,10 @@ describe('findMissingDependentPaths', () => {
89
114
  })
90
115
 
91
116
  describe('findMatchingEntries (pattern matching)', () => {
117
+ it('normalizes Windows and relative paths', () => {
118
+ expect(normalizeScaffoldPath('.\\packages\\core\\src\\')).toBe('packages/core/src')
119
+ })
120
+
92
121
  it('matches exact paths', () => {
93
122
  const reg = registryFromEntries([makeEntry('exact', ['packages/core/src/index.ts'])])
94
123
  expect(matchedIds(reg, 'packages/core/src/index.ts')).toEqual(['exact'])
@@ -110,12 +139,11 @@ describe('findMatchingEntries (pattern matching)', () => {
110
139
  expect(matchedIds(reg, 'packages/core/nested/package.json')).toEqual([])
111
140
  })
112
141
 
113
- it('current matcher does NOT support mixed **/*.ext patterns (Step 3 upgrade)', () => {
114
- // The simple matcher's `/*` suffix branch only handles a single trailing
115
- // wildcard. Patterns like `**/*.ts` fall through and effectively miss real
116
- // files. Locked in to flag any change when full micromatch lands.
142
+ it('matches mixed **/*.ext patterns at direct-child and nested depths', () => {
117
143
  const reg = registryFromEntries([makeEntry('ext', ['apps/api/src/**/*.ts'])])
118
- expect(matchedIds(reg, 'apps/api/src/foo.ts')).toEqual([])
144
+ expect(matchedIds(reg, 'apps/api/src/foo.ts')).toEqual(['ext'])
145
+ expect(matchedIds(reg, 'apps/api/src/nested/deeply/foo.ts')).toEqual(['ext'])
146
+ expect(matchedIds(reg, 'apps/api/src/foo.tsx')).toEqual([])
119
147
  })
120
148
 
121
149
  it('normalizes Windows backslashes to forward slashes', () => {
@@ -138,4 +166,41 @@ describe('findMatchingEntries (pattern matching)', () => {
138
166
  const reg = registryFromEntries([makeEntry('a', ['packages/core/src/**'])])
139
167
  expect(matchedIds(reg, 'apps/api/src/foo.ts')).toEqual([])
140
168
  })
169
+
170
+ it('exposes the same matcher for direct consumers', () => {
171
+ expect(scaffoldPathMatchesPattern('packages/core/src/foo.test.ts', 'packages/core/src/**/*.ts')).toBe(true)
172
+ expect(scaffoldPathMatchesPattern('packages/core/src/foo.test.ts', 'packages/ui/src/**/*.ts')).toBe(false)
173
+ })
174
+ })
175
+
176
+ describe('findEmptySourcePatterns', () => {
177
+ it('flags source patterns that do not match any file or directory', () => {
178
+ const root = makeTempRoot()
179
+ mkdirSync(path.join(root, 'packages', 'core', 'src'), { recursive: true })
180
+ writeFileSync(path.join(root, 'packages', 'core', 'src', 'index.ts'), 'export {}\n', 'utf8')
181
+
182
+ const registry = registryFromEntries([
183
+ makeEntry('ok-exact', ['packages/core/src/index.ts']),
184
+ makeEntry('ok-glob', ['packages/core/src/**/*.ts']),
185
+ makeEntry('empty', ['packages/core/src/**/*.tsx'])
186
+ ])
187
+
188
+ expect(findEmptySourcePatterns(registry, root)).toEqual([
189
+ { entryId: 'empty', pattern: 'packages/core/src/**/*.tsx' }
190
+ ])
191
+ })
192
+
193
+ it('treats exact directories as non-empty source coverage', () => {
194
+ const root = makeTempRoot()
195
+ mkdirSync(path.join(root, 'external', '_template', '.claude', 'skills'), { recursive: true })
196
+
197
+ const registry = registryFromEntries([
198
+ makeEntry('dir', ['external/_template/.claude/skills']),
199
+ makeEntry('missing-dir', ['external/_template/.claude/commands'])
200
+ ])
201
+
202
+ expect(findEmptySourcePatterns(registry, root)).toEqual([
203
+ { entryId: 'missing-dir', pattern: 'external/_template/.claude/commands' }
204
+ ])
205
+ })
141
206
  })
@@ -1,4 +1,5 @@
1
- import { existsSync, readFileSync, writeFileSync } from 'node:fs'
1
+ import { createHash } from 'node:crypto'
2
+ import { existsSync, readdirSync, readFileSync, statSync, writeFileSync } from 'node:fs'
2
3
  import path from 'node:path'
3
4
  import { parse as parseYaml } from 'yaml'
4
5
  import { type ScaffoldRegistry, type ScaffoldRegistryEntry, ScaffoldRegistrySchema } from './schema'
@@ -86,15 +87,23 @@ export function loadScaffoldRegistry(): ScaffoldRegistry {
86
87
 
87
88
  const registry = result.data
88
89
 
89
- // Drift check: if compiled JSON exists, verify entry count matches
90
+ // Drift check: if compiled JSON exists, verify the full normalized content matches.
90
91
  const jsonPath = path.join(root, JSON_FILENAME)
91
92
  try {
92
93
  const compiledRaw = readFileSync(jsonPath, 'utf-8')
93
- const compiled = JSON.parse(compiledRaw) as { entries?: unknown[] }
94
- if (compiled.entries?.length !== registry.entries.length) {
94
+ const compiledParsed = JSON.parse(compiledRaw) as unknown
95
+ const compiledResult = ScaffoldRegistrySchema.safeParse(compiledParsed)
96
+ if (!compiledResult.success) {
97
+ const issues = compiledResult.error.issues.map((i) => ` [${i.path.join('.')}] ${i.message}`).join('\n')
98
+ throw new Error(`scaffold-registry: compiled JSON validation failed:\n${issues}`)
99
+ }
100
+
101
+ const yamlHash = stableRegistryHash(registry)
102
+ const compiledHash = stableRegistryHash(compiledResult.data)
103
+ if (compiledHash !== yamlHash) {
95
104
  throw new Error(
96
105
  `scaffold-registry: compiled JSON is out of sync with YAML ` +
97
- `(YAML has ${registry.entries.length} entries, JSON has ${compiled.entries?.length ?? 0}). ` +
106
+ `(YAML hash ${yamlHash.slice(0, 12)}, JSON hash ${compiledHash.slice(0, 12)}). ` +
98
107
  `Run compileScaffoldRegistry() to regenerate.`
99
108
  )
100
109
  }
@@ -131,6 +140,15 @@ export function compileScaffoldRegistry(): ScaffoldRegistry {
131
140
  )
132
141
  }
133
142
 
143
+ const emptySources = findEmptySourcePatterns(registry, root)
144
+ if (emptySources.length > 0) {
145
+ const formatted = emptySources.map((source) => ` [${source.entryId}] ${source.pattern}`).join('\n')
146
+ throw new Error(
147
+ `scaffold-registry: ${emptySources.length} source pattern(s) match no files or directories:\n${formatted}\n` +
148
+ `Fix the stale source glob, create the scaffold surface, or add explicit registry support for intentional empty patterns.`
149
+ )
150
+ }
151
+
134
152
  const jsonPath = path.join(root, JSON_FILENAME)
135
153
  writeFileSync(jsonPath, JSON.stringify(registry, null, 2) + '\n', 'utf-8')
136
154
 
@@ -166,6 +184,29 @@ export function findMissingDependentPaths(
166
184
  return missing
167
185
  }
168
186
 
187
+ /**
188
+ * Return source patterns that do not currently match any file or directory.
189
+ * Symbolic sources are skipped because they intentionally do not resolve to
190
+ * monorepo paths.
191
+ */
192
+ export function findEmptySourcePatterns(
193
+ registry: ScaffoldRegistry,
194
+ monorepoRootDir: string
195
+ ): Array<{ entryId: string; pattern: string }> {
196
+ const empty: Array<{ entryId: string; pattern: string }> = []
197
+
198
+ for (const entry of registry.entries) {
199
+ for (const sourcePattern of entry.sources) {
200
+ if (isSymbolicTarget(sourcePattern)) continue
201
+ if (!sourcePatternMatchesAnyPath(sourcePattern, monorepoRootDir)) {
202
+ empty.push({ entryId: entry.id, pattern: sourcePattern })
203
+ }
204
+ }
205
+ }
206
+
207
+ return empty
208
+ }
209
+
169
210
  // ---------------------------------------------------------------------------
170
211
  // Lookup helpers (used by hooks for fast path matching)
171
212
  // ---------------------------------------------------------------------------
@@ -192,12 +233,17 @@ export function loadScaffoldRegistryFast(): ScaffoldRegistry {
192
233
 
193
234
  /**
194
235
  * 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.
236
+ * the given file path AND whose `excludes` contain no pattern that matches.
237
+ * Pattern matching is a simple substring/glob-prefix check suitable for hook
238
+ * use; Step 3 will upgrade to full micromatch when the hook is implemented.
198
239
  */
199
240
  export function findMatchingEntries(registry: ScaffoldRegistry, filePath: string): ScaffoldRegistryEntry[] {
200
- return registry.entries.filter((entry) => entry.sources.some((pattern) => pathMatchesPattern(filePath, pattern)))
241
+ return registry.entries.filter((entry) => {
242
+ const sourceMatch = entry.sources.some((pattern) => scaffoldPathMatchesPattern(filePath, pattern))
243
+ if (!sourceMatch) return false
244
+ const excluded = (entry.excludes ?? []).some((pattern) => scaffoldPathMatchesPattern(filePath, pattern))
245
+ return !excluded
246
+ })
201
247
  }
202
248
 
203
249
  // ---------------------------------------------------------------------------
@@ -221,6 +267,24 @@ function loadScaffoldRegistryNoSyncCheck(root: string): ScaffoldRegistry {
221
267
  return result.data
222
268
  }
223
269
 
270
+ export function normalizeScaffoldPath(p: string): string {
271
+ return p.replace(/\\/g, '/').replace(/^\.\//, '').replace(/\/+$/, '')
272
+ }
273
+
274
+ export function scaffoldPathMatchesPattern(filePath: string, pattern: string): boolean {
275
+ const normalizedFile = normalizeScaffoldPath(filePath)
276
+ const normalizedPattern = normalizeScaffoldPath(pattern)
277
+
278
+ if (!normalizedFile || !normalizedPattern) return false
279
+ if (normalizedFile === normalizedPattern) return true
280
+
281
+ if (!isGlobPattern(normalizedPattern)) {
282
+ return normalizedFile.startsWith(normalizedPattern + '/')
283
+ }
284
+
285
+ return globToRegExp(normalizedPattern).test(normalizedFile)
286
+ }
287
+
224
288
  function isSymbolicTarget(p: string): boolean {
225
289
  return p.startsWith('docs:') || p.startsWith('autogen-target:')
226
290
  }
@@ -229,30 +293,100 @@ function isGlobPattern(p: string): boolean {
229
293
  return /[*?[]/.test(p)
230
294
  }
231
295
 
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, '/')
296
+ function stableRegistryHash(registry: ScaffoldRegistry): string {
297
+ return createHash('sha256').update(JSON.stringify(registry)).digest('hex')
298
+ }
240
299
 
241
- // Exact match
242
- if (normalizedFile === normalizedPattern) return true
300
+ function globToRegExp(pattern: string): RegExp {
301
+ const segments = pattern.split('/')
302
+ let regex = '^'
303
+
304
+ for (let index = 0; index < segments.length; index++) {
305
+ const segment = segments[index]
306
+ const isLast = index === segments.length - 1
307
+
308
+ if (segment === '**') {
309
+ regex += isLast ? '(?:.*)?' : '(?:[^/]+/)*'
310
+ continue
311
+ }
312
+
313
+ regex += segmentToRegExpSource(segment)
314
+ if (!isLast) regex += '/'
315
+ }
316
+
317
+ regex += '$'
318
+ return new RegExp(regex)
319
+ }
320
+
321
+ function segmentToRegExpSource(segment: string): string {
322
+ let source = ''
323
+ for (let index = 0; index < segment.length; index++) {
324
+ const char = segment[index]
325
+ if (char === '*') {
326
+ source += '[^/]*'
327
+ } else if (char === '?') {
328
+ source += '[^/]'
329
+ } else {
330
+ source += escapeRegExp(char)
331
+ }
332
+ }
333
+ return source
334
+ }
335
+
336
+ function escapeRegExp(value: string): string {
337
+ return value.replace(/[\\^$.*+?()[\]{}|]/g, '\\$&')
338
+ }
339
+
340
+ function sourcePatternMatchesAnyPath(sourcePattern: string, monorepoRootDir: string): boolean {
341
+ const normalizedPattern = normalizeScaffoldPath(sourcePattern)
243
342
 
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 + '/')
343
+ if (!isGlobPattern(normalizedPattern)) {
344
+ return existsSync(path.join(monorepoRootDir, normalizedPattern))
248
345
  }
249
346
 
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)
347
+ const baseDir = findGlobBaseDirectory(normalizedPattern)
348
+ const absoluteBase = path.join(monorepoRootDir, baseDir)
349
+ if (!existsSync(absoluteBase)) return false
350
+
351
+ return walkUntilMatch(absoluteBase, monorepoRootDir, normalizedPattern)
352
+ }
353
+
354
+ function findGlobBaseDirectory(pattern: string): string {
355
+ const segments = pattern.split('/')
356
+ const baseSegments: string[] = []
357
+ for (const segment of segments) {
358
+ if (isGlobPattern(segment)) break
359
+ baseSegments.push(segment)
360
+ }
361
+ return baseSegments.join('/')
362
+ }
363
+
364
+ function walkUntilMatch(currentPath: string, monorepoRootDir: string, pattern: string): boolean {
365
+ const rel = normalizeScaffoldPath(path.relative(monorepoRootDir, currentPath))
366
+ if (rel && scaffoldPathMatchesPattern(rel, pattern)) return true
367
+
368
+ let entries
369
+ try {
370
+ entries = readdirSync(currentPath, { withFileTypes: true })
371
+ } catch {
372
+ return false
373
+ }
374
+
375
+ for (const entry of entries) {
376
+ const absolutePath = path.join(currentPath, entry.name)
377
+ const relativePath = normalizeScaffoldPath(path.relative(monorepoRootDir, absolutePath))
378
+
379
+ if (scaffoldPathMatchesPattern(relativePath, pattern)) return true
380
+ if (entry.isDirectory()) {
381
+ try {
382
+ if (statSync(absolutePath).isDirectory() && walkUntilMatch(absolutePath, monorepoRootDir, pattern)) {
383
+ return true
384
+ }
385
+ } catch {
386
+ continue
387
+ }
388
+ }
254
389
  }
255
390
 
256
- // Directory prefix match (pattern is a directory)
257
- return normalizedFile.startsWith(normalizedPattern + '/')
391
+ return false
258
392
  }
@@ -1,25 +1,19 @@
1
- import { z } from 'zod'
2
-
3
- // ---------------------------------------------------------------------------
4
- // Kind taxonomy
5
- // ---------------------------------------------------------------------------
6
-
7
- /**
8
- * The kind of scaffold entry:
9
- *
10
- * - autogen: fully derivable from source; regen command is the fix
11
- * - manual-scaffold: hand-authored structure that must be updated on source change
1
+ import { z } from 'zod'
2
+
3
+ // ---------------------------------------------------------------------------
4
+ // Kind taxonomy
5
+ // ---------------------------------------------------------------------------
6
+
7
+ /**
8
+ * The kind of scaffold entry:
9
+ *
10
+ * - autogen: fully derivable from source; regen command is the fix
11
+ * - manual-scaffold: hand-authored structure that must be updated on source change
12
12
  * - sync-preservation: external-template files with a declared sync tier
13
13
  * - validator: drift-detection scripts or CI checks (meta.json pages[] validators, etc.)
14
14
  * - other: escape hatch; requires free-form notes
15
15
  */
16
- export const ScaffoldEntryKindSchema = z.enum([
17
- 'autogen',
18
- 'manual-scaffold',
19
- 'sync-preservation',
20
- 'validator',
21
- 'other'
22
- ])
16
+ export const ScaffoldEntryKindSchema = z.enum(['autogen', 'manual-scaffold', 'sync-preservation', 'validator', 'other'])
23
17
 
24
18
  export type ScaffoldEntryKind = z.infer<typeof ScaffoldEntryKindSchema>
25
19
 
@@ -55,35 +49,35 @@ export type ExternalSyncStrategy = z.infer<typeof ExternalSyncStrategySchema>
55
49
  export const ExternalSyncDeletePolicySchema = z.enum(['none', 'manifest-only'])
56
50
 
57
51
  export type ExternalSyncDeletePolicy = z.infer<typeof ExternalSyncDeletePolicySchema>
58
-
59
- // ---------------------------------------------------------------------------
60
- // Scaffold reference (a single downstream artifact that needs attention)
61
- // ---------------------------------------------------------------------------
62
-
63
- export const ScaffoldRefSchema = z.object({
64
- /**
65
- * File path, glob, or symbolic target (e.g. "docs: sync-preservation-matrix").
66
- * Symbolic targets begin with "docs:" or "autogen-target:".
67
- */
68
- path: z.string().min(1),
69
-
70
- /**
71
- * Command to regenerate, or "manual" if human judgment is required.
72
- */
73
- regen: z.string().min(1).optional(),
74
-
52
+
53
+ // ---------------------------------------------------------------------------
54
+ // Scaffold reference (a single downstream artifact that needs attention)
55
+ // ---------------------------------------------------------------------------
56
+
57
+ export const ScaffoldRefSchema = z.object({
58
+ /**
59
+ * File path, glob, or symbolic target (e.g. "docs: sync-preservation-matrix").
60
+ * Symbolic targets begin with "docs:" or "autogen-target:".
61
+ */
62
+ path: z.string().min(1),
63
+
64
+ /**
65
+ * Command to regenerate, or "manual" if human judgment is required.
66
+ */
67
+ regen: z.string().min(1).optional(),
68
+
75
69
  /**
76
70
  * Human-readable hint shown in reminder messages.
77
71
  */
78
- hint: z.string().optional()
79
- })
80
-
81
- export type ScaffoldRef = z.infer<typeof ScaffoldRefSchema>
82
-
83
- // ---------------------------------------------------------------------------
84
- // Registry entry
85
- // ---------------------------------------------------------------------------
86
-
72
+ hint: z.string().optional()
73
+ })
74
+
75
+ export type ScaffoldRef = z.infer<typeof ScaffoldRefSchema>
76
+
77
+ // ---------------------------------------------------------------------------
78
+ // Registry entry
79
+ // ---------------------------------------------------------------------------
80
+
87
81
  export const ScaffoldRegistryEntrySchema = z
88
82
  .object({
89
83
  /**
@@ -106,6 +100,18 @@ export const ScaffoldRegistryEntrySchema = z
106
100
  */
107
101
  sources: z.array(z.string().min(1)).min(1, 'At least one source pattern is required'),
108
102
 
103
+ /**
104
+ * Optional glob patterns that suppress this entry's reminder when the
105
+ * touched file matches. Applied after `sources`: a path that matches any
106
+ * `sources` pattern AND any `excludes` pattern is treated as no-match.
107
+ *
108
+ * Use case: a broad source glob (e.g. `apps/docs/content/docs/**\/*.mdx`)
109
+ * that should not fire on a sub-tree where the dependent scaffold does not
110
+ * apply (e.g. `apps/docs/content/docs/in-progress/**`, where MDX docs do
111
+ * not take meta.json files).
112
+ */
113
+ excludes: z.array(z.string().min(1)).optional(),
114
+
109
115
  /**
110
116
  * Downstream artifacts that need attention when a source changes.
111
117
  */
@@ -217,21 +223,21 @@ export const ScaffoldRegistryEntrySchema = z
217
223
  }
218
224
  }
219
225
  })
220
-
221
- export type ScaffoldRegistryEntry = z.infer<typeof ScaffoldRegistryEntrySchema>
222
-
223
- // ---------------------------------------------------------------------------
224
- // Top-level registry document
225
- // ---------------------------------------------------------------------------
226
-
227
- export const ScaffoldRegistrySchema = z.object({
228
- /**
229
- * Schema version for forward-compatibility detection.
230
- * Bump when the shape changes in a breaking way.
231
- */
232
- version: z.string().default('1'),
233
-
234
- entries: z.array(ScaffoldRegistryEntrySchema).min(1)
235
- })
236
-
237
- export type ScaffoldRegistry = z.infer<typeof ScaffoldRegistrySchema>
226
+
227
+ export type ScaffoldRegistryEntry = z.infer<typeof ScaffoldRegistryEntrySchema>
228
+
229
+ // ---------------------------------------------------------------------------
230
+ // Top-level registry document
231
+ // ---------------------------------------------------------------------------
232
+
233
+ export const ScaffoldRegistrySchema = z.object({
234
+ /**
235
+ * Schema version for forward-compatibility detection.
236
+ * Bump when the shape changes in a breaking way.
237
+ */
238
+ version: z.string().default('1'),
239
+
240
+ entries: z.array(ScaffoldRegistryEntrySchema).min(1)
241
+ })
242
+
243
+ export type ScaffoldRegistry = z.infer<typeof ScaffoldRegistrySchema>