@elevasis/core 0.6.0 → 0.7.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 +1 -0
- package/dist/index.js +9 -2
- package/dist/organization-model/index.d.ts +1 -0
- package/dist/organization-model/index.js +9 -2
- package/package.json +4 -3
- package/src/_gen/__tests__/__snapshots__/contracts.md.snap +1131 -0
- package/src/_gen/__tests__/scaffold-contracts.test.ts +53 -0
- package/src/_gen/scaffold-contracts.ts +45 -0
- package/src/business/projects/index.ts +1 -0
- package/src/business/projects/sse-events.ts +21 -0
- package/src/execution/engine/tools/registry.ts +4 -3
- package/src/index.ts +10 -0
- package/src/organization-model/__tests__/domains/identity.test.ts +1 -0
- package/src/organization-model/domains/identity.ts +9 -2
- package/src/platform/constants/versions.ts +1 -1
- package/src/platform/sse/events.ts +3 -0
- package/src/reference/_generated/contracts.md +5 -1
- package/src/scaffold-registry/__tests__/schema.test.ts +280 -0
- package/src/scaffold-registry/index.ts +194 -0
- package/src/scaffold-registry/schema.ts +144 -0
- package/src/supabase/database.types.ts +70 -6
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Snapshot test for the scaffold-contracts generator wrapper.
|
|
3
|
+
*
|
|
4
|
+
* Calls generate() (which runs the real script) and asserts:
|
|
5
|
+
* 1. The output file exists and has content.
|
|
6
|
+
* 2. The content matches the stored snapshot (drift detection).
|
|
7
|
+
*
|
|
8
|
+
* If the snapshot is missing it is created on first run. Subsequent runs fail
|
|
9
|
+
* if the output changes without an intentional snapshot update.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { readFileSync } from 'node:fs'
|
|
13
|
+
import { resolve } from 'node:path'
|
|
14
|
+
import { describe, it, expect } from 'vitest'
|
|
15
|
+
|
|
16
|
+
/** Monorepo root relative to packages/core/src/_gen/__tests__/ */
|
|
17
|
+
const ROOT = resolve(import.meta.dirname, '..', '..', '..', '..', '..')
|
|
18
|
+
|
|
19
|
+
const OUTPUT_PATH = resolve(ROOT, 'packages/core/src/reference/_generated/contracts.md')
|
|
20
|
+
|
|
21
|
+
describe('scaffold-contracts generator', () => {
|
|
22
|
+
it('output file exists and has content', () => {
|
|
23
|
+
// The generator must have been run (either manually or by CI gen step).
|
|
24
|
+
// This test validates the committed artifact — it does NOT re-run the generator
|
|
25
|
+
// so the test suite stays fast and deterministic.
|
|
26
|
+
let content: string
|
|
27
|
+
try {
|
|
28
|
+
content = readFileSync(OUTPUT_PATH, 'utf8')
|
|
29
|
+
} catch {
|
|
30
|
+
throw new Error(
|
|
31
|
+
`Generated file not found: ${OUTPUT_PATH}\n` +
|
|
32
|
+
`Run "pnpm scaffold:generate" or "node scripts/monorepo/generate-scaffold-contracts.js" first.`
|
|
33
|
+
)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
expect(content.length).toBeGreaterThan(0)
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
it('output file matches stored snapshot', async () => {
|
|
40
|
+
let content: string
|
|
41
|
+
try {
|
|
42
|
+
content = readFileSync(OUTPUT_PATH, 'utf8')
|
|
43
|
+
} catch {
|
|
44
|
+
throw new Error(
|
|
45
|
+
`Generated file not found: ${OUTPUT_PATH}\n` +
|
|
46
|
+
`Run "pnpm scaffold:generate" first to produce the artifact before snapshotting.`
|
|
47
|
+
)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Snapshot stored alongside test file in __snapshots__/
|
|
51
|
+
await expect(content).toMatchFileSnapshot(resolve(import.meta.dirname, '__snapshots__', 'contracts.md.snap'))
|
|
52
|
+
})
|
|
53
|
+
})
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Thin _gen/ wrapper for the scaffold-contracts generator.
|
|
3
|
+
*
|
|
4
|
+
* The actual generation logic lives in scripts/monorepo/generate-scaffold-contracts.js.
|
|
5
|
+
* This wrapper provides the canonical generate() entry point so the generator
|
|
6
|
+
* participates in the @repo/gen-utils pattern (snapshot tests, drift detection,
|
|
7
|
+
* Turbo gen task).
|
|
8
|
+
*
|
|
9
|
+
* DO NOT move logic here — edit the script instead, then re-run generate().
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { spawnSync } from 'node:child_process'
|
|
13
|
+
import { fileURLToPath } from 'node:url'
|
|
14
|
+
import { resolve, dirname } from 'node:path'
|
|
15
|
+
|
|
16
|
+
const __filename = fileURLToPath(import.meta.url)
|
|
17
|
+
const __dirname = dirname(__filename)
|
|
18
|
+
|
|
19
|
+
/** Monorepo root — four levels up from packages/core/src/_gen/ */
|
|
20
|
+
const ROOT = resolve(__dirname, '..', '..', '..', '..')
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Run the scaffold-contracts generator.
|
|
24
|
+
* Delegates to scripts/monorepo/generate-scaffold-contracts.js via child process
|
|
25
|
+
* so the existing script is the single source of implementation.
|
|
26
|
+
*/
|
|
27
|
+
export function generate(): void {
|
|
28
|
+
const result = spawnSync('node', ['scripts/monorepo/generate-scaffold-contracts.js'], {
|
|
29
|
+
stdio: 'inherit',
|
|
30
|
+
cwd: ROOT
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
if (result.error) {
|
|
34
|
+
throw new Error(`scaffold-contracts generator failed to spawn: ${result.error.message}`)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (result.status !== 0) {
|
|
38
|
+
throw new Error(`scaffold-contracts generator exited with status ${result.status}`)
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Allow direct invocation: node packages/core/src/_gen/scaffold-contracts.ts
|
|
43
|
+
if (process.argv[1] && resolve(process.argv[1]) === __filename) {
|
|
44
|
+
generate()
|
|
45
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export type DeliveryEntityType = 'project' | 'milestone' | 'task' | 'note' | 'activity'
|
|
2
|
+
|
|
3
|
+
export type DeliveryEntityChangedAction = 'created' | 'updated' | 'deleted'
|
|
4
|
+
|
|
5
|
+
export interface DeliveryConnectedEvent {
|
|
6
|
+
type: 'connected'
|
|
7
|
+
timestamp: number
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface DeliveryEntityChangedEvent {
|
|
11
|
+
type: 'entity_changed'
|
|
12
|
+
timestamp: number
|
|
13
|
+
data: {
|
|
14
|
+
entityType: DeliveryEntityType
|
|
15
|
+
action: DeliveryEntityChangedAction
|
|
16
|
+
projectId: string
|
|
17
|
+
entityId: string
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export type DeliverySSEEvent = DeliveryConnectedEvent | DeliveryEntityChangedEvent
|
|
@@ -174,6 +174,7 @@ type TaskUpdate = Database['public']['Tables']['prj_tasks']['Update']
|
|
|
174
174
|
type NoteInsert = Database['public']['Tables']['prj_notes']['Insert']
|
|
175
175
|
type NoteUpdate = Database['public']['Tables']['prj_notes']['Update']
|
|
176
176
|
type TaskResumeContextRow = Pick<TaskRow, 'id' | 'resume_context' | 'updated_at'>
|
|
177
|
+
type DeleteEntityResult = { id: string; project_id: string }
|
|
177
178
|
|
|
178
179
|
export interface IProjectsService {
|
|
179
180
|
listProjects(
|
|
@@ -203,7 +204,7 @@ export interface IProjectsService {
|
|
|
203
204
|
organizationId: string,
|
|
204
205
|
input: Omit<MilestoneUpdate, 'organization_id'>
|
|
205
206
|
): Promise<MilestoneRow | null>
|
|
206
|
-
deleteMilestone(id: string, organizationId: string): Promise<
|
|
207
|
+
deleteMilestone(id: string, organizationId: string): Promise<DeleteEntityResult | null>
|
|
207
208
|
listTasks(
|
|
208
209
|
projectId: string,
|
|
209
210
|
organizationId: string,
|
|
@@ -216,7 +217,7 @@ export interface IProjectsService {
|
|
|
216
217
|
getTask(id: string, organizationId: string): Promise<TaskRow | null>
|
|
217
218
|
createTask(input: Omit<TaskInsert, 'organization_id'> & { organizationId: string }): Promise<TaskRow>
|
|
218
219
|
updateTask(id: string, organizationId: string, input: Omit<TaskUpdate, 'organization_id'>): Promise<TaskRow | null>
|
|
219
|
-
deleteTask(id: string, organizationId: string): Promise<
|
|
220
|
+
deleteTask(id: string, organizationId: string): Promise<DeleteEntityResult | null>
|
|
220
221
|
mergeTaskResumeContext(
|
|
221
222
|
id: string,
|
|
222
223
|
organizationId: string,
|
|
@@ -225,7 +226,7 @@ export interface IProjectsService {
|
|
|
225
226
|
listNotes(projectId: string, organizationId: string): Promise<NoteRow[]>
|
|
226
227
|
createNote(input: Omit<NoteInsert, 'organization_id'> & { organizationId: string }): Promise<NoteRow>
|
|
227
228
|
updateNote(id: string, organizationId: string, input: Omit<NoteUpdate, 'organization_id'>): Promise<NoteRow | null>
|
|
228
|
-
deleteNote(id: string, organizationId: string): Promise<
|
|
229
|
+
deleteNote(id: string, organizationId: string): Promise<DeleteEntityResult | null>
|
|
229
230
|
}
|
|
230
231
|
|
|
231
232
|
// =============================================================================
|
package/src/index.ts
CHANGED
|
@@ -52,3 +52,13 @@ export * from './integrations/oauth/index'
|
|
|
52
52
|
|
|
53
53
|
// Credential types and utilities
|
|
54
54
|
export * from './integrations/credentials/index'
|
|
55
|
+
|
|
56
|
+
// Scaffold registry — browser-safe schema types and Zod schemas only.
|
|
57
|
+
// For server-side loader functions, use: import { ... } from '@repo/core/scaffold-registry'
|
|
58
|
+
export { ScaffoldRegistrySchema, ScaffoldEntryKindSchema } from './scaffold-registry/schema'
|
|
59
|
+
export type {
|
|
60
|
+
ScaffoldRegistry,
|
|
61
|
+
ScaffoldRegistryEntry,
|
|
62
|
+
ScaffoldRef,
|
|
63
|
+
ScaffoldEntryKind
|
|
64
|
+
} from './scaffold-registry/schema'
|
|
@@ -257,6 +257,7 @@ describe('resolveOrganizationModel — identity domain integration', () => {
|
|
|
257
257
|
industryCategory: 'Software / SaaS',
|
|
258
258
|
geographicFocus: 'Global',
|
|
259
259
|
timeZone: 'America/Chicago',
|
|
260
|
+
clientBrief: '',
|
|
260
261
|
businessHours: {
|
|
261
262
|
monday: { open: '09:00', close: '17:00' }
|
|
262
263
|
}
|
|
@@ -67,7 +67,13 @@ export const IdentityDomainSchema = z.object({
|
|
|
67
67
|
*/
|
|
68
68
|
timeZone: z.string().trim().max(100).default('UTC'),
|
|
69
69
|
/** Typical operating hours per day of week. Empty object means not configured. */
|
|
70
|
-
businessHours: BusinessHoursSchema
|
|
70
|
+
businessHours: BusinessHoursSchema,
|
|
71
|
+
/**
|
|
72
|
+
* Long-form markdown capturing client context, problem narrative, and domain
|
|
73
|
+
* background. Populated by /setup; surfaced to agents as organizational context.
|
|
74
|
+
* Optional — many projects have no external client.
|
|
75
|
+
*/
|
|
76
|
+
clientBrief: z.string().trim().default('')
|
|
71
77
|
})
|
|
72
78
|
|
|
73
79
|
// ---------------------------------------------------------------------------
|
|
@@ -83,5 +89,6 @@ export const DEFAULT_ORGANIZATION_MODEL_IDENTITY: z.infer<typeof IdentityDomainS
|
|
|
83
89
|
industryCategory: '',
|
|
84
90
|
geographicFocus: '',
|
|
85
91
|
timeZone: 'UTC',
|
|
86
|
-
businessHours: {}
|
|
92
|
+
businessHours: {},
|
|
93
|
+
clientBrief: ''
|
|
87
94
|
}
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
import type { NotificationSSEEvent, NotificationCountUpdatedEvent } from '../../operations/notifications/sse-events'
|
|
8
8
|
import type { ActivitySSEEvent, ActivityCreatedEvent, ActivityConnectedEvent } from '../../operations/activities/sse-events'
|
|
9
9
|
import type { CommandQueueSSEEvent, CommandQueueTaskUpdatedEvent, CommandQueueConnectedEvent } from '../../commands/queue/sse-events'
|
|
10
|
+
import type { DeliverySSEEvent, DeliveryConnectedEvent, DeliveryEntityChangedEvent } from '../../business/projects/sse-events'
|
|
10
11
|
import type {
|
|
11
12
|
ExecutionSSEEvent,
|
|
12
13
|
ExecutionStartedEvent,
|
|
@@ -19,6 +20,7 @@ import type {
|
|
|
19
20
|
export type { NotificationSSEEvent, NotificationCountUpdatedEvent }
|
|
20
21
|
export type { ActivitySSEEvent, ActivityCreatedEvent, ActivityConnectedEvent }
|
|
21
22
|
export type { CommandQueueSSEEvent, CommandQueueTaskUpdatedEvent, CommandQueueConnectedEvent }
|
|
23
|
+
export type { DeliverySSEEvent, DeliveryConnectedEvent, DeliveryEntityChangedEvent }
|
|
22
24
|
export type { ExecutionSSEEvent, ExecutionStartedEvent, ExecutionLogEvent, ExecutionCompleteEvent, ExecutionConnectedEvent }
|
|
23
25
|
|
|
24
26
|
/**
|
|
@@ -31,4 +33,5 @@ export type AppSSEEvent =
|
|
|
31
33
|
| NotificationSSEEvent
|
|
32
34
|
| ActivitySSEEvent
|
|
33
35
|
| CommandQueueSSEEvent
|
|
36
|
+
| DeliverySSEEvent
|
|
34
37
|
| ExecutionSSEEvent
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
<!-- Auto-generated on 2026-04-
|
|
1
|
+
<!-- Auto-generated on 2026-04-21 by scripts/monorepo/generate-scaffold-contracts.js -->
|
|
2
2
|
---
|
|
3
3
|
title: Reference Contracts
|
|
4
4
|
description: Auto-generated TypeScript contracts for SDK consumers. Do not edit manually.
|
|
@@ -363,6 +363,8 @@ export interface ElevasisFeaturesProviderProps {
|
|
|
363
363
|
timeRange?: TimeRange
|
|
364
364
|
operationsApiUrl?: string
|
|
365
365
|
operationsSSEManager?: SSEConnectionManagerLike
|
|
366
|
+
deliveryApiUrl?: string
|
|
367
|
+
deliverySSEManager?: SSEConnectionManagerLike
|
|
366
368
|
disabledSubsectionPaths?: string[]
|
|
367
369
|
children: ReactNode
|
|
368
370
|
}
|
|
@@ -381,6 +383,8 @@ export interface ElevasisFeaturesContextValue {
|
|
|
381
383
|
timeRange?: TimeRange
|
|
382
384
|
operationsApiUrl?: string
|
|
383
385
|
operationsSSEManager?: SSEConnectionManagerLike
|
|
386
|
+
deliveryApiUrl?: string
|
|
387
|
+
deliverySSEManager?: SSEConnectionManagerLike
|
|
384
388
|
disabledSubsectionPaths: string[]
|
|
385
389
|
isFeatureEnabled: (key: string) => boolean
|
|
386
390
|
getResolvedFeature: (key: string) => ResolvedFeatureModule | undefined
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import { ScaffoldEntryKindSchema, ScaffoldRegistryEntrySchema, ScaffoldRegistrySchema } from '../schema'
|
|
3
|
+
|
|
4
|
+
// ---------------------------------------------------------------------------
|
|
5
|
+
// Kind enum
|
|
6
|
+
// ---------------------------------------------------------------------------
|
|
7
|
+
|
|
8
|
+
describe('ScaffoldEntryKindSchema', () => {
|
|
9
|
+
it('accepts all declared kinds', () => {
|
|
10
|
+
const validKinds = [
|
|
11
|
+
'autogen',
|
|
12
|
+
'manual-scaffold',
|
|
13
|
+
'typed-id',
|
|
14
|
+
'sync-preservation',
|
|
15
|
+
'vibe-gated',
|
|
16
|
+
'sdk-cli-generator',
|
|
17
|
+
'validator',
|
|
18
|
+
'other'
|
|
19
|
+
]
|
|
20
|
+
for (const kind of validKinds) {
|
|
21
|
+
expect(ScaffoldEntryKindSchema.safeParse(kind).success).toBe(true)
|
|
22
|
+
}
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
it('rejects unknown kinds', () => {
|
|
26
|
+
const result = ScaffoldEntryKindSchema.safeParse('unknown-kind')
|
|
27
|
+
expect(result.success).toBe(false)
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
it('rejects empty string', () => {
|
|
31
|
+
expect(ScaffoldEntryKindSchema.safeParse('').success).toBe(false)
|
|
32
|
+
})
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
// ---------------------------------------------------------------------------
|
|
36
|
+
// Valid full entry
|
|
37
|
+
// ---------------------------------------------------------------------------
|
|
38
|
+
|
|
39
|
+
const validEntry = {
|
|
40
|
+
id: 'business-directory-rename',
|
|
41
|
+
kind: 'manual-scaffold',
|
|
42
|
+
owner: 'packages/core',
|
|
43
|
+
sources: ['packages/core/src/business/**'],
|
|
44
|
+
dependents: [
|
|
45
|
+
{
|
|
46
|
+
path: '.navigation/_generated/skeleton.md',
|
|
47
|
+
regen: 'pnpm navigation:generate',
|
|
48
|
+
kind: 'autogen',
|
|
49
|
+
hint: 'Regenerate after rename.'
|
|
50
|
+
}
|
|
51
|
+
]
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
describe('ScaffoldRegistryEntrySchema — valid entry', () => {
|
|
55
|
+
it('parses a complete valid entry', () => {
|
|
56
|
+
const result = ScaffoldRegistryEntrySchema.safeParse(validEntry)
|
|
57
|
+
expect(result.success).toBe(true)
|
|
58
|
+
if (result.success) {
|
|
59
|
+
expect(result.data.id).toBe('business-directory-rename')
|
|
60
|
+
expect(result.data.kind).toBe('manual-scaffold')
|
|
61
|
+
expect(result.data.sources).toHaveLength(1)
|
|
62
|
+
expect(result.data.dependents).toHaveLength(1)
|
|
63
|
+
}
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
it('accepts optional notes and cooldown_ms', () => {
|
|
67
|
+
const withOptionals = {
|
|
68
|
+
...validEntry,
|
|
69
|
+
notes: 'Extra context here.',
|
|
70
|
+
cooldown_ms: 60_000
|
|
71
|
+
}
|
|
72
|
+
const result = ScaffoldRegistryEntrySchema.safeParse(withOptionals)
|
|
73
|
+
expect(result.success).toBe(true)
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
it('accepts optional regen command when provided', () => {
|
|
77
|
+
const withRegen = { ...validEntry, regen: 'pnpm navigation:generate' }
|
|
78
|
+
const result = ScaffoldRegistryEntrySchema.safeParse(withRegen)
|
|
79
|
+
expect(result.success).toBe(true)
|
|
80
|
+
if (result.success) {
|
|
81
|
+
expect(result.data.regen).toBe('pnpm navigation:generate')
|
|
82
|
+
}
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
it('accepts entry without regen field (optional)', () => {
|
|
86
|
+
const result = ScaffoldRegistryEntrySchema.safeParse(validEntry)
|
|
87
|
+
expect(result.success).toBe(true)
|
|
88
|
+
if (result.success) {
|
|
89
|
+
expect(result.data.regen).toBeUndefined()
|
|
90
|
+
}
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
it('rejects regen as empty string', () => {
|
|
94
|
+
const withEmptyRegen = { ...validEntry, regen: '' }
|
|
95
|
+
const result = ScaffoldRegistryEntrySchema.safeParse(withEmptyRegen)
|
|
96
|
+
expect(result.success).toBe(false)
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
it('rejects regen as non-string (number)', () => {
|
|
100
|
+
const withBadRegen = { ...validEntry, regen: 42 }
|
|
101
|
+
const result = ScaffoldRegistryEntrySchema.safeParse(withBadRegen)
|
|
102
|
+
expect(result.success).toBe(false)
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
it('accepts entry without optional hint / regen in dependent', () => {
|
|
106
|
+
const minimalDependent = {
|
|
107
|
+
...validEntry,
|
|
108
|
+
dependents: [{ path: 'some/path' }]
|
|
109
|
+
}
|
|
110
|
+
expect(ScaffoldRegistryEntrySchema.safeParse(minimalDependent).success).toBe(true)
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
it('accepts auto_regen: true', () => {
|
|
114
|
+
const withAutoRegen = { ...validEntry, auto_regen: true }
|
|
115
|
+
expect(ScaffoldRegistryEntrySchema.safeParse(withAutoRegen).success).toBe(true)
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
it('accepts auto_regen: false', () => {
|
|
119
|
+
const withAutoRegen = { ...validEntry, auto_regen: false }
|
|
120
|
+
expect(ScaffoldRegistryEntrySchema.safeParse(withAutoRegen).success).toBe(true)
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
it('accepts entry without auto_regen (optional)', () => {
|
|
124
|
+
expect(ScaffoldRegistryEntrySchema.safeParse(validEntry).success).toBe(true)
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
it('rejects auto_regen as a non-boolean', () => {
|
|
128
|
+
const withBadAutoRegen = { ...validEntry, auto_regen: 'yes' }
|
|
129
|
+
expect(ScaffoldRegistryEntrySchema.safeParse(withBadAutoRegen).success).toBe(false)
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
it('parses all six seed-entry kinds', () => {
|
|
133
|
+
const kinds = ['autogen', 'manual-scaffold', 'typed-id', 'sync-preservation', 'vibe-gated', 'validator']
|
|
134
|
+
for (const kind of kinds) {
|
|
135
|
+
const result = ScaffoldRegistryEntrySchema.safeParse({ ...validEntry, kind })
|
|
136
|
+
expect(result.success, `kind ${kind} should parse`).toBe(true)
|
|
137
|
+
}
|
|
138
|
+
})
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
// ---------------------------------------------------------------------------
|
|
142
|
+
// Invalid entries — required fields missing
|
|
143
|
+
// ---------------------------------------------------------------------------
|
|
144
|
+
|
|
145
|
+
describe('ScaffoldRegistryEntrySchema — invalid entries', () => {
|
|
146
|
+
it('rejects entry missing id', () => {
|
|
147
|
+
const { id: _id, ...withoutId } = validEntry
|
|
148
|
+
expect(ScaffoldRegistryEntrySchema.safeParse(withoutId).success).toBe(false)
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
it('rejects entry missing kind', () => {
|
|
152
|
+
const { kind: _kind, ...withoutKind } = validEntry
|
|
153
|
+
expect(ScaffoldRegistryEntrySchema.safeParse(withoutKind).success).toBe(false)
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
it('rejects entry missing owner', () => {
|
|
157
|
+
const { owner: _owner, ...withoutOwner } = validEntry
|
|
158
|
+
expect(ScaffoldRegistryEntrySchema.safeParse(withoutOwner).success).toBe(false)
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
it('rejects entry missing sources', () => {
|
|
162
|
+
const { sources: _sources, ...withoutSources } = validEntry
|
|
163
|
+
expect(ScaffoldRegistryEntrySchema.safeParse(withoutSources).success).toBe(false)
|
|
164
|
+
})
|
|
165
|
+
|
|
166
|
+
it('rejects entry missing dependents', () => {
|
|
167
|
+
const { dependents: _dependents, ...withoutDependents } = validEntry
|
|
168
|
+
expect(ScaffoldRegistryEntrySchema.safeParse(withoutDependents).success).toBe(false)
|
|
169
|
+
})
|
|
170
|
+
|
|
171
|
+
it('rejects invalid kind', () => {
|
|
172
|
+
const withBadKind = { ...validEntry, kind: 'not-a-real-kind' }
|
|
173
|
+
const result = ScaffoldRegistryEntrySchema.safeParse(withBadKind)
|
|
174
|
+
expect(result.success).toBe(false)
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
it('rejects id with uppercase characters', () => {
|
|
178
|
+
const withBadId = { ...validEntry, id: 'BusinessDirectoryRename' }
|
|
179
|
+
expect(ScaffoldRegistryEntrySchema.safeParse(withBadId).success).toBe(false)
|
|
180
|
+
})
|
|
181
|
+
|
|
182
|
+
it('rejects id with spaces', () => {
|
|
183
|
+
const withBadId = { ...validEntry, id: 'business directory rename' }
|
|
184
|
+
expect(ScaffoldRegistryEntrySchema.safeParse(withBadId).success).toBe(false)
|
|
185
|
+
})
|
|
186
|
+
|
|
187
|
+
it('rejects empty sources array', () => {
|
|
188
|
+
const withEmptySources = { ...validEntry, sources: [] }
|
|
189
|
+
expect(ScaffoldRegistryEntrySchema.safeParse(withEmptySources).success).toBe(false)
|
|
190
|
+
})
|
|
191
|
+
|
|
192
|
+
it('rejects empty dependents array', () => {
|
|
193
|
+
const withEmptyDependents = { ...validEntry, dependents: [] }
|
|
194
|
+
expect(ScaffoldRegistryEntrySchema.safeParse(withEmptyDependents).success).toBe(false)
|
|
195
|
+
})
|
|
196
|
+
|
|
197
|
+
it('rejects dependent missing path', () => {
|
|
198
|
+
const withBadDependent = {
|
|
199
|
+
...validEntry,
|
|
200
|
+
dependents: [{ regen: 'pnpm nav' }]
|
|
201
|
+
}
|
|
202
|
+
expect(ScaffoldRegistryEntrySchema.safeParse(withBadDependent).success).toBe(false)
|
|
203
|
+
})
|
|
204
|
+
|
|
205
|
+
it('rejects negative cooldown_ms', () => {
|
|
206
|
+
const withBadCooldown = { ...validEntry, cooldown_ms: -1 }
|
|
207
|
+
expect(ScaffoldRegistryEntrySchema.safeParse(withBadCooldown).success).toBe(false)
|
|
208
|
+
})
|
|
209
|
+
|
|
210
|
+
it('rejects non-integer cooldown_ms', () => {
|
|
211
|
+
const withBadCooldown = { ...validEntry, cooldown_ms: 300.5 }
|
|
212
|
+
expect(ScaffoldRegistryEntrySchema.safeParse(withBadCooldown).success).toBe(false)
|
|
213
|
+
})
|
|
214
|
+
})
|
|
215
|
+
|
|
216
|
+
// ---------------------------------------------------------------------------
|
|
217
|
+
// Top-level registry document
|
|
218
|
+
// ---------------------------------------------------------------------------
|
|
219
|
+
|
|
220
|
+
describe('ScaffoldRegistrySchema', () => {
|
|
221
|
+
const validRegistry = {
|
|
222
|
+
version: '1',
|
|
223
|
+
entries: [validEntry]
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
it('parses a valid registry document', () => {
|
|
227
|
+
const result = ScaffoldRegistrySchema.safeParse(validRegistry)
|
|
228
|
+
expect(result.success).toBe(true)
|
|
229
|
+
if (result.success) {
|
|
230
|
+
expect(result.data.version).toBe('1')
|
|
231
|
+
expect(result.data.entries).toHaveLength(1)
|
|
232
|
+
}
|
|
233
|
+
})
|
|
234
|
+
|
|
235
|
+
it('applies default version when omitted', () => {
|
|
236
|
+
const withoutVersion = { entries: [validEntry] }
|
|
237
|
+
const result = ScaffoldRegistrySchema.safeParse(withoutVersion)
|
|
238
|
+
expect(result.success).toBe(true)
|
|
239
|
+
if (result.success) {
|
|
240
|
+
expect(result.data.version).toBe('1')
|
|
241
|
+
}
|
|
242
|
+
})
|
|
243
|
+
|
|
244
|
+
it('rejects registry with no entries', () => {
|
|
245
|
+
const empty = { version: '1', entries: [] }
|
|
246
|
+
expect(ScaffoldRegistrySchema.safeParse(empty).success).toBe(false)
|
|
247
|
+
})
|
|
248
|
+
|
|
249
|
+
it('rejects registry missing entries key', () => {
|
|
250
|
+
expect(ScaffoldRegistrySchema.safeParse({ version: '1' }).success).toBe(false)
|
|
251
|
+
})
|
|
252
|
+
|
|
253
|
+
it('accepts multiple entries of different kinds', () => {
|
|
254
|
+
const multiEntry = {
|
|
255
|
+
version: '1',
|
|
256
|
+
entries: [
|
|
257
|
+
validEntry,
|
|
258
|
+
{
|
|
259
|
+
id: 'meta-json-updated',
|
|
260
|
+
kind: 'validator',
|
|
261
|
+
owner: 'apps/docs',
|
|
262
|
+
sources: ['apps/docs/content/docs/**/meta.json'],
|
|
263
|
+
dependents: [{ path: '(self)', regen: 'pnpm check-docs-meta' }]
|
|
264
|
+
},
|
|
265
|
+
{
|
|
266
|
+
id: 'external-template-hook-added',
|
|
267
|
+
kind: 'sync-preservation',
|
|
268
|
+
owner: 'external/_template',
|
|
269
|
+
sources: ['external/_template/.claude/hooks/*.mjs'],
|
|
270
|
+
dependents: [{ path: 'external/_template/.claude/settings.json', regen: 'manual' }]
|
|
271
|
+
}
|
|
272
|
+
]
|
|
273
|
+
}
|
|
274
|
+
const result = ScaffoldRegistrySchema.safeParse(multiEntry)
|
|
275
|
+
expect(result.success).toBe(true)
|
|
276
|
+
if (result.success) {
|
|
277
|
+
expect(result.data.entries).toHaveLength(3)
|
|
278
|
+
}
|
|
279
|
+
})
|
|
280
|
+
})
|