foundry-design-protocol 0.2.0-beta.2 → 0.2.0-beta.21

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.js CHANGED
@@ -1,7 +1,15 @@
1
1
  import { z } from 'zod';
2
- export const PROTOCOL_VERSION = '1.2.0';
2
+ export { verificationValueMatches } from './verification-value.js';
3
+ export const PROTOCOL_VERSION = '1.3.0';
4
+ export const PREVIOUS_PROTOCOL_VERSION = '1.2.0';
3
5
  export const LEGACY_PROTOCOL_VERSION = '1.0.0';
4
6
  export const APPLY_RUN_PROTOCOL_VERSION = '1.1.0';
7
+ const readableProtocolVersionSchema = z.enum([
8
+ PROTOCOL_VERSION,
9
+ PREVIOUS_PROTOCOL_VERSION,
10
+ APPLY_RUN_PROTOCOL_VERSION,
11
+ LEGACY_PROTOCOL_VERSION,
12
+ ]);
5
13
  export const platformSchema = z.enum(['web', 'swiftui', 'react-native']);
6
14
  export const changeCategorySchema = z.enum([
7
15
  'layout',
@@ -18,7 +26,7 @@ export const changeCategorySchema = z.enum([
18
26
  'state',
19
27
  'motion',
20
28
  ]);
21
- export const scopeSchema = z.enum(['instance', 'component']);
29
+ export const scopeSchema = z.enum(['instance', 'variant', 'component']);
22
30
  export const confidenceSchema = z.enum(['measured', 'instrumented', 'inferred', 'unresolved']);
23
31
  export const geometrySchema = z.object({
24
32
  x: z.number().finite(),
@@ -39,6 +47,7 @@ export const sessionContextSchema = z.object({
39
47
  designGraphRevision: z.string().optional(),
40
48
  platform: platformSchema,
41
49
  targetUrl: z.string().url().optional(),
50
+ previewOrigin: z.string().url().optional(),
42
51
  targetName: z.string().optional(),
43
52
  device: z.string().optional(),
44
53
  viewport: z.object({ width: z.number().positive(), height: z.number().positive() }).optional(),
@@ -46,6 +55,27 @@ export const sessionContextSchema = z.object({
46
55
  breakpoint: z.string().default('current'),
47
56
  state: z.string().default('current'),
48
57
  });
58
+ export const changeContextSchema = z.object({
59
+ breakpoint: z.string().default('current'),
60
+ theme: z.string().default('current'),
61
+ state: z.string().default('current'),
62
+ });
63
+ const contextSetValuesSchema = z
64
+ .array(z.string().min(1))
65
+ .min(1)
66
+ .transform((values) => [...new Set(values)]);
67
+ export const contextSetSchema = z.object({
68
+ breakpoints: contextSetValuesSchema,
69
+ themes: contextSetValuesSchema,
70
+ states: contextSetValuesSchema,
71
+ });
72
+ function singletonContextSet(context) {
73
+ return {
74
+ breakpoints: [context.breakpoint],
75
+ themes: [context.theme],
76
+ states: [context.state],
77
+ };
78
+ }
49
79
  export const targetRefSchema = z.object({
50
80
  id: z.string().min(1),
51
81
  platform: platformSchema,
@@ -107,16 +137,53 @@ export const designTokenSchema = z.object({
107
137
  'other',
108
138
  ]),
109
139
  cssVariable: z.string().optional(),
140
+ aliasOfTokenId: z.string().optional(),
141
+ aliasOfTokenName: z.string().optional(),
142
+ resolvedValue: z.string().optional(),
143
+ aliasChain: z.array(z.string()).optional(),
144
+ aliasStatus: z.enum(['direct', 'resolved', 'broken', 'circular']).optional(),
110
145
  source: sourceRefSchema.optional(),
111
146
  confidence: confidenceSchema.default('inferred'),
112
147
  evidence: z.array(z.string()).default([]),
113
148
  });
149
+ export const tokenPromotionCandidateSchema = z.object({
150
+ id: z.string().min(1),
151
+ value: z.string().min(1),
152
+ category: designTokenSchema.shape.category,
153
+ property: z.string().min(1),
154
+ occurrenceCount: z.number().int().positive(),
155
+ sources: z.array(sourceRefSchema).min(1),
156
+ componentIds: z.array(z.string()).default([]),
157
+ recommendation: z.enum(['use-existing', 'create-token']),
158
+ relation: z.enum(['exact', 'near', 'new']),
159
+ suggestedTokenId: z.string().optional(),
160
+ suggestedTokenName: z.string().min(1),
161
+ suggestedValue: z.string().min(1),
162
+ aliasChain: z.array(z.string()).default([]),
163
+ canStage: z.boolean().default(true),
164
+ blockers: z.array(z.string()).default([]),
165
+ evidence: z.array(z.string()).default([]),
166
+ });
114
167
  export const componentVariantSchema = z.object({
115
168
  id: z.string().min(1),
116
169
  label: z.string().min(1),
117
170
  property: z.string().min(1),
118
171
  value: changeValueSchema,
172
+ props: z.record(z.string(), changeValueSchema).default({}),
119
173
  source: sourceRefSchema.optional(),
174
+ adapter: z.enum(['storybook', 'cva', 'typescript', 'configured']).optional(),
175
+ sourceProperty: z.string().optional(),
176
+ });
177
+ export const componentVariantAxisSchema = z.object({
178
+ id: z.string().min(1),
179
+ label: z.string().min(1),
180
+ property: z.string().min(1),
181
+ values: z.array(changeValueSchema).default([]),
182
+ adapter: z.enum(['storybook', 'cva', 'typescript', 'configured']),
183
+ source: sourceRefSchema,
184
+ sourceProperty: z.string().min(1),
185
+ canCreate: z.boolean().default(false),
186
+ evidence: z.array(z.string()).default([]),
120
187
  });
121
188
  export const componentDefinitionSchema = z.object({
122
189
  id: z.string().min(1),
@@ -125,6 +192,7 @@ export const componentDefinitionSchema = z.object({
125
192
  selector: z.string().optional(),
126
193
  instances: z.number().int().nonnegative().default(0),
127
194
  variants: z.array(componentVariantSchema).default([]),
195
+ variantAxes: z.array(componentVariantAxisSchema).default([]),
128
196
  evidence: z.array(z.string()).default([]),
129
197
  });
130
198
  export const stateDefinitionSchema = z.object({
@@ -147,6 +215,17 @@ export const breakpointDefinitionSchema = z.object({
147
215
  mediaQuery: z.string().optional(),
148
216
  source: sourceRefSchema.optional(),
149
217
  });
218
+ export const containerQueryDefinitionSchema = z.object({
219
+ id: z.string().min(1),
220
+ label: z.string().min(1),
221
+ name: z.string().optional(),
222
+ condition: z.string().min(1),
223
+ axis: z.enum(['inline-size', 'block-size', 'size']).default('inline-size'),
224
+ minWidth: z.number().nonnegative().optional(),
225
+ maxWidth: z.number().nonnegative().optional(),
226
+ source: sourceRefSchema,
227
+ evidence: z.array(z.string()).default([]),
228
+ });
150
229
  export const themeDefinitionSchema = z.object({
151
230
  id: z.string().min(1),
152
231
  label: z.string().min(1),
@@ -154,6 +233,8 @@ export const themeDefinitionSchema = z.object({
154
233
  attribute: z.string().optional(),
155
234
  value: z.string().optional(),
156
235
  source: sourceRefSchema.optional(),
236
+ confidence: confidenceSchema.default('instrumented'),
237
+ evidence: z.array(z.string()).default([]),
157
238
  });
158
239
  export const motionPresetSchema = z.object({
159
240
  id: z.string().min(1),
@@ -161,20 +242,58 @@ export const motionPresetSchema = z.object({
161
242
  duration: z.number().nonnegative().optional(),
162
243
  easing: z.string().optional(),
163
244
  delay: z.number().nonnegative().optional(),
245
+ adapter: z.enum(['css', 'motion', 'gsap', 'react-spring']).optional(),
246
+ sourceProperty: z.string().optional(),
247
+ configuration: z.record(z.string(), z.unknown()).default({}),
164
248
  source: sourceRefSchema.optional(),
249
+ evidence: z.array(z.string()).default([]),
165
250
  });
166
- export const projectDesignGraphSchema = z.object({
167
- protocolVersion: z.literal(PROTOCOL_VERSION),
251
+ export const designTokenUsageSchema = z.object({
252
+ id: z.string().min(1),
253
+ tokenId: z.string().min(1),
254
+ tokenName: z.string().min(1),
255
+ value: z.string().min(1),
256
+ category: designTokenSchema.shape.category,
257
+ kind: z.enum(['reference', 'literal', 'alias']),
258
+ property: z.string().optional(),
259
+ componentId: z.string().optional(),
260
+ source: sourceRefSchema,
261
+ evidence: z.array(z.string()).default([]),
262
+ });
263
+ export const designSystemFindingSchema = z.object({
264
+ id: z.string().min(1),
265
+ kind: z.enum(['near-duplicate', 'literal-drift', 'unused-token', 'component-drift']),
266
+ severity: z.enum(['info', 'warning']).default('info'),
267
+ title: z.string().min(1),
268
+ detail: z.string().min(1),
269
+ category: designTokenSchema.shape.category.optional(),
270
+ tokenIds: z.array(z.string()).default([]),
271
+ usageIds: z.array(z.string()).default([]),
272
+ componentIds: z.array(z.string()).default([]),
273
+ suggestedTokenId: z.string().optional(),
274
+ source: sourceRefSchema.optional(),
275
+ evidence: z.array(z.string()).default([]),
276
+ });
277
+ const projectDesignGraphInputSchema = z.object({
278
+ protocolVersion: z.union([z.literal(PROTOCOL_VERSION), z.literal(PREVIOUS_PROTOCOL_VERSION)]),
168
279
  projectRoot: z.string().min(1),
169
280
  revision: z.string().optional(),
170
281
  tokens: z.array(designTokenSchema).default([]),
171
282
  components: z.array(componentDefinitionSchema).default([]),
172
283
  breakpoints: z.array(breakpointDefinitionSchema).default([]),
284
+ containerQueries: z.array(containerQueryDefinitionSchema).optional(),
173
285
  themes: z.array(themeDefinitionSchema).default([]),
174
286
  states: z.array(stateDefinitionSchema).default([]),
175
287
  motionPresets: z.array(motionPresetSchema).default([]),
288
+ tokenUsages: z.array(designTokenUsageSchema).default([]),
289
+ designSystemFindings: z.array(designSystemFindingSchema).default([]),
290
+ tokenPromotions: z.array(tokenPromotionCandidateSchema).optional(),
176
291
  indexedAt: z.string().datetime(),
177
292
  });
293
+ export const projectDesignGraphSchema = projectDesignGraphInputSchema.transform((graph) => ({
294
+ ...graph,
295
+ protocolVersion: PROTOCOL_VERSION,
296
+ }));
178
297
  export const sourceMappingCandidateSchema = z.object({
179
298
  id: z.string().min(1),
180
299
  label: z.string().min(1),
@@ -188,6 +307,8 @@ export const sourceMappingCandidateSchema = z.object({
188
307
  'content',
189
308
  'motion',
190
309
  'state',
310
+ 'component-variant',
311
+ 'token-refactor',
191
312
  ]),
192
313
  property: z.string().min(1),
193
314
  targetId: z.string().optional(),
@@ -198,9 +319,21 @@ export const sourceMappingCandidateSchema = z.object({
198
319
  evidence: z.array(z.string()).default([]),
199
320
  blastRadius: z.number().int().nonnegative().default(1),
200
321
  });
201
- export const designOperationSchema = z.object({
322
+ export const designOperationSchema = z
323
+ .object({
202
324
  id: z.string().min(1),
203
- kind: z.enum(['resize', 'spacing', 'align', 'distribute', 'style', 'content', 'motion', 'state']),
325
+ kind: z.enum([
326
+ 'resize',
327
+ 'spacing',
328
+ 'align',
329
+ 'distribute',
330
+ 'style',
331
+ 'content',
332
+ 'motion',
333
+ 'state',
334
+ 'component-variant',
335
+ 'token-refactor',
336
+ ]),
204
337
  label: z.string().min(1),
205
338
  targetIds: z.array(z.string().min(1)).min(1),
206
339
  changeIds: z.array(z.string().min(1)).default([]),
@@ -210,8 +343,35 @@ export const designOperationSchema = z.object({
210
343
  status: z.enum(['preview', 'resolved', 'unresolved']).default('preview'),
211
344
  createdAt: z.string().datetime(),
212
345
  updatedAt: z.string().datetime(),
346
+ })
347
+ .superRefine((operation, context) => {
348
+ const candidateIds = operation.mappingCandidates.map((candidate) => candidate.id);
349
+ if (new Set(candidateIds).size !== candidateIds.length) {
350
+ context.addIssue({
351
+ code: 'custom',
352
+ path: ['mappingCandidates'],
353
+ message: 'Source mapping candidate ids must be unique',
354
+ });
355
+ }
356
+ if (operation.selectedMappingId &&
357
+ !operation.mappingCandidates.some((candidate) => candidate.id === operation.selectedMappingId)) {
358
+ context.addIssue({
359
+ code: 'custom',
360
+ path: ['selectedMappingId'],
361
+ message: 'Selected source mapping must name an existing candidate',
362
+ });
363
+ }
364
+ if (operation.status === 'resolved' &&
365
+ (operation.mappingCandidates.length === 0 ||
366
+ (operation.mappingCandidates.length > 1 && !operation.selectedMappingId))) {
367
+ context.addIssue({
368
+ code: 'custom',
369
+ path: ['selectedMappingId'],
370
+ message: 'Resolved operations require a selected source mapping',
371
+ });
372
+ }
213
373
  });
214
- export const designChangeSchema = z.object({
374
+ const designChangeInputSchema = z.object({
215
375
  id: z.string().min(1),
216
376
  target: targetRefSchema,
217
377
  category: changeCategorySchema,
@@ -225,19 +385,51 @@ export const designChangeSchema = z.object({
225
385
  mappingCandidates: z.array(sourceMappingCandidateSchema).default([]),
226
386
  selectedMappingId: z.string().optional(),
227
387
  scope: scopeSchema.default('instance'),
228
- context: z.object({
229
- breakpoint: z.string().default('current'),
230
- theme: z.string().default('current'),
231
- state: z.string().default('current'),
232
- }),
388
+ context: changeContextSchema,
389
+ contextSet: contextSetSchema.optional(),
233
390
  confidence: confidenceSchema,
234
391
  evidence: z.array(z.string()).default([]),
235
392
  createdAt: z.string().datetime(),
236
393
  updatedAt: z.string().datetime(),
237
394
  status: z.enum(['draft', 'approved', 'applied', 'rejected', 'unresolved']).default('draft'),
238
395
  });
239
- export const changeSetSchema = z.object({
240
- protocolVersion: z.literal(PROTOCOL_VERSION),
396
+ export const designChangeSchema = designChangeInputSchema
397
+ .transform((change) => ({
398
+ ...change,
399
+ contextSet: change.contextSet ?? singletonContextSet(change.context),
400
+ }))
401
+ .superRefine((change, context) => {
402
+ const candidateIds = change.mappingCandidates.map((candidate) => candidate.id);
403
+ if (new Set(candidateIds).size !== candidateIds.length) {
404
+ context.addIssue({
405
+ code: 'custom',
406
+ path: ['mappingCandidates'],
407
+ message: 'Source mapping candidate ids must be unique',
408
+ });
409
+ }
410
+ if (change.selectedMappingId &&
411
+ !change.mappingCandidates.some((candidate) => candidate.id === change.selectedMappingId)) {
412
+ context.addIssue({
413
+ code: 'custom',
414
+ path: ['selectedMappingId'],
415
+ message: 'Selected source mapping must name an existing candidate',
416
+ });
417
+ }
418
+ const missingAxes = [
419
+ change.contextSet.breakpoints.includes(change.context.breakpoint) ? undefined : 'breakpoint',
420
+ change.contextSet.themes.includes(change.context.theme) ? undefined : 'theme',
421
+ change.contextSet.states.includes(change.context.state) ? undefined : 'state',
422
+ ].filter((axis) => Boolean(axis));
423
+ if (missingAxes.length) {
424
+ context.addIssue({
425
+ code: 'custom',
426
+ path: ['contextSet'],
427
+ message: `Context set must contain the exact capture ${missingAxes.join(', ')}`,
428
+ });
429
+ }
430
+ });
431
+ const changeSetInputSchema = z.object({
432
+ protocolVersion: readableProtocolVersionSchema,
241
433
  sessionId: z.string().min(1),
242
434
  context: sessionContextSchema,
243
435
  changes: z.array(designChangeSchema),
@@ -253,14 +445,138 @@ export const changeSetSchema = z.object({
253
445
  createdAt: z.string().datetime(),
254
446
  updatedAt: z.string().datetime(),
255
447
  });
448
+ function refineChangeCollection(value, context) {
449
+ const changeIds = value.changes.map((change) => change.id);
450
+ if (new Set(changeIds).size !== changeIds.length) {
451
+ context.addIssue({
452
+ code: 'custom',
453
+ path: ['changes'],
454
+ message: 'Design change ids must be unique within a change collection',
455
+ });
456
+ }
457
+ const operationIds = value.operations.map((operation) => operation.id);
458
+ if (new Set(operationIds).size !== operationIds.length) {
459
+ context.addIssue({
460
+ code: 'custom',
461
+ path: ['operations'],
462
+ message: 'Design operation ids must be unique within a change collection',
463
+ });
464
+ }
465
+ const changes = new Map(value.changes.map((change) => [change.id, change]));
466
+ const operations = new Map(value.operations.map((operation) => [operation.id, operation]));
467
+ for (const [index, operation] of value.operations.entries()) {
468
+ if (new Set(operation.changeIds).size !== operation.changeIds.length) {
469
+ context.addIssue({
470
+ code: 'custom',
471
+ path: ['operations', index, 'changeIds'],
472
+ message: 'Operation change ids must be unique',
473
+ });
474
+ }
475
+ if (new Set(operation.targetIds).size !== operation.targetIds.length) {
476
+ context.addIssue({
477
+ code: 'custom',
478
+ path: ['operations', index, 'targetIds'],
479
+ message: 'Operation target ids must be unique',
480
+ });
481
+ }
482
+ for (const referencedId of operation.changeIds) {
483
+ const change = changes.get(referencedId);
484
+ if (!change || change.operationId !== operation.id) {
485
+ context.addIssue({
486
+ code: 'custom',
487
+ path: ['operations', index, 'changeIds'],
488
+ message: `Operation ${operation.id} references an inconsistent change ${referencedId}`,
489
+ });
490
+ }
491
+ }
492
+ }
493
+ for (const [index, change] of value.changes.entries()) {
494
+ if (!change.operationId)
495
+ continue;
496
+ const operation = operations.get(change.operationId);
497
+ if (!operation || !operation.changeIds.includes(change.id)) {
498
+ context.addIssue({
499
+ code: 'custom',
500
+ path: ['changes', index, 'operationId'],
501
+ message: `Change ${change.id} references an inconsistent operation ${change.operationId}`,
502
+ });
503
+ }
504
+ }
505
+ }
506
+ export const changeSetSchema = changeSetInputSchema
507
+ .transform((changeSet) => ({
508
+ ...changeSet,
509
+ protocolVersion: PROTOCOL_VERSION,
510
+ }))
511
+ .superRefine((changeSet, context) => {
512
+ refineChangeCollection(changeSet, context);
513
+ });
514
+ export const designBranchSchema = z
515
+ .object({
516
+ id: z.string().min(1),
517
+ name: z.string().min(1).max(80),
518
+ status: z.enum(['exploring', 'chosen', 'rejected', 'archived']).default('exploring'),
519
+ originBranchId: z.string().optional(),
520
+ changes: z.array(designChangeSchema).default([]),
521
+ operations: z.array(designOperationSchema).default([]),
522
+ rejectionReason: z.string().max(280).optional(),
523
+ createdAt: z.string().datetime(),
524
+ updatedAt: z.string().datetime(),
525
+ })
526
+ .superRefine((branch, context) => {
527
+ refineChangeCollection(branch, context);
528
+ });
529
+ export const designBranchRecordSourceSchema = z.object({
530
+ targetId: z.string().min(1),
531
+ targetLabel: z.string().min(1),
532
+ property: z.string().min(1),
533
+ componentPath: z.array(z.string()).default([]),
534
+ source: sourceRefSchema.optional(),
535
+ });
536
+ export const designBranchRecordSchema = z
537
+ .object({
538
+ version: z.literal(1),
539
+ id: z.string().min(1),
540
+ branchId: z.string().min(1),
541
+ name: z.string().min(1).max(80),
542
+ outcome: z.enum(['chosen', 'rejected']),
543
+ rationale: z.string().max(280).optional(),
544
+ context: sessionContextSchema,
545
+ changes: z.array(designChangeSchema).default([]),
546
+ operations: z.array(designOperationSchema).default([]),
547
+ sourceRelationships: z.array(designBranchRecordSourceSchema).default([]),
548
+ compatibility: z.object({
549
+ status: z.enum(['current', 'stale', 'missing']),
550
+ matchedSources: z.number().int().nonnegative(),
551
+ totalSources: z.number().int().nonnegative(),
552
+ warnings: z.array(z.string()).default([]),
553
+ checkedAt: z.string().datetime(),
554
+ }),
555
+ importedAt: z.string().datetime().optional(),
556
+ createdAt: z.string().datetime(),
557
+ updatedAt: z.string().datetime(),
558
+ })
559
+ .superRefine((record, context) => {
560
+ refineChangeCollection(record, context);
561
+ });
562
+ export const designBranchRecordBundleSchema = z.object({
563
+ format: z.literal('foundry.design-branch-records'),
564
+ version: z.literal(1),
565
+ exportedAt: z.string().datetime(),
566
+ records: z.array(designBranchRecordSchema).max(100),
567
+ });
256
568
  export const verificationResultSchema = z.object({
257
569
  changeId: z.string().min(1),
258
570
  property: z.string().min(1),
259
571
  requested: changeValueSchema,
260
572
  rendered: changeValueSchema,
573
+ context: changeContextSchema.optional(),
574
+ applyRunId: z.string().min(1).optional(),
575
+ claimAttemptId: z.string().min(1).optional(),
261
576
  passed: z.boolean(),
262
577
  reason: z.string().optional(),
263
578
  geometry: geometrySchema.optional(),
579
+ evidence: z.array(z.string().min(1)).default([]),
264
580
  screenshotPath: z.string().optional(),
265
581
  verifiedAt: z.string().datetime(),
266
582
  });
@@ -285,11 +601,44 @@ export const validationResultSchema = z.object({
285
601
  name: z.string().min(1),
286
602
  passed: z.boolean(),
287
603
  summary: z.string().optional(),
604
+ applyRunId: z.string().min(1).optional(),
605
+ claimAttemptId: z.string().min(1).optional(),
606
+ validatedRevision: z.string().min(1).optional(),
607
+ validatedAt: z.string().datetime().optional(),
288
608
  });
289
- export const applyRunSchema = z.object({
609
+ export const sourceLineAnchorSchema = z
610
+ .object({
611
+ line: z.number().int().positive(),
612
+ endLine: z.number().int().positive().optional(),
613
+ symbol: z.string().min(1).optional(),
614
+ sha256: z.string().regex(/^[a-f0-9]{64}$/),
615
+ })
616
+ .superRefine((anchor, context) => {
617
+ if (anchor.endLine !== undefined && anchor.endLine < anchor.line) {
618
+ context.addIssue({
619
+ code: 'custom',
620
+ path: ['endLine'],
621
+ message: 'Source anchor endLine must not precede line',
622
+ });
623
+ }
624
+ });
625
+ export const sourceFileSnapshotSchema = z.object({
626
+ path: z.string().min(1),
627
+ exists: z.boolean(),
628
+ sha256: z
629
+ .string()
630
+ .regex(/^[a-f0-9]{64}$/)
631
+ .nullable(),
632
+ lineAnchors: z.array(sourceLineAnchorSchema).optional(),
633
+ });
634
+ export const applyRunSchema = z
635
+ .object({
290
636
  id: z.string().min(1),
291
637
  sessionId: z.string().min(1),
292
638
  changeIds: z.array(z.string().min(1)).min(1),
639
+ reviewedChangeSet: changeSetSchema.optional(),
640
+ /** Enables only the pre-1.3 acknowledgement shape for a run migrated from that protocol. */
641
+ legacyApplyCompatibility: z.boolean().optional(),
293
642
  revision: z.string().optional(),
294
643
  designGraphRevision: z.string().optional(),
295
644
  state: applyRunStateSchema,
@@ -304,13 +653,297 @@ export const applyRunSchema = z.object({
304
653
  changedFiles: z.array(z.string()).default([]),
305
654
  validationResults: z.array(validationResultSchema).default([]),
306
655
  verificationResults: z.array(verificationResultSchema).default([]),
656
+ applyResultAcknowledgedAt: z.string().datetime().optional(),
657
+ applyResultClaimAttemptId: z.string().min(1).optional(),
658
+ sourceProofScope: z.enum(['git', 'mapped-files']).optional(),
659
+ sourceHeadRevision: z.string().min(1).optional(),
660
+ baselineSourceFiles: z.array(sourceFileSnapshotSchema).default([]),
661
+ appliedRevision: z.string().min(1).optional(),
662
+ appliedChangedFiles: z.array(z.string().min(1)).default([]),
663
+ appliedSourceFiles: z.array(sourceFileSnapshotSchema).default([]),
307
664
  attempts: z.number().int().positive().default(1),
665
+ claimAttemptId: z.string().min(1).optional(),
666
+ claimCapabilityHash: z
667
+ .string()
668
+ .regex(/^[a-f0-9]{64}$/)
669
+ .optional(),
670
+ claimExpiresAt: z.string().datetime().optional(),
671
+ claimHeartbeatAt: z.string().datetime().optional(),
672
+ requeueCount: z.number().int().nonnegative().default(0),
673
+ interruptedState: z.enum(['applying', 'rebuilding', 'verifying']).optional(),
674
+ resumedAt: z.string().datetime().optional(),
308
675
  retryOf: z.string().optional(),
309
676
  error: z.string().optional(),
310
677
  requestedAt: z.string().datetime(),
311
678
  claimedAt: z.string().datetime().optional(),
312
679
  completedAt: z.string().datetime().optional(),
313
680
  updatedAt: z.string().datetime(),
681
+ })
682
+ .superRefine((run, context) => {
683
+ if (Boolean(run.applyResultAcknowledgedAt) !== Boolean(run.applyResultClaimAttemptId)) {
684
+ context.addIssue({
685
+ code: 'custom',
686
+ path: ['applyResultAcknowledgedAt'],
687
+ message: 'Apply result acknowledgement time and claim attempt must be stored together',
688
+ });
689
+ }
690
+ if (run.applyResultAcknowledgedAt &&
691
+ (!run.appliedRevision ||
692
+ run.appliedChangedFiles.length === 0 ||
693
+ run.appliedSourceFiles.length === 0)) {
694
+ context.addIssue({
695
+ code: 'custom',
696
+ path: ['appliedRevision'],
697
+ message: 'Acknowledged Apply results require a proven revision and changed source files',
698
+ });
699
+ }
700
+ if (!run.reviewedChangeSet)
701
+ return;
702
+ if (run.reviewedChangeSet.sessionId !== run.sessionId) {
703
+ context.addIssue({
704
+ code: 'custom',
705
+ path: ['reviewedChangeSet', 'sessionId'],
706
+ message: 'Reviewed change set must belong to the apply run session',
707
+ });
708
+ }
709
+ const runIds = [...new Set(run.changeIds)].sort();
710
+ const reviewedIds = [
711
+ ...new Set(run.reviewedChangeSet.changes.map((change) => change.id)),
712
+ ].sort();
713
+ if (runIds.length !== run.changeIds.length ||
714
+ reviewedIds.length !== run.reviewedChangeSet.changes.length ||
715
+ runIds.length !== reviewedIds.length ||
716
+ runIds.some((id, index) => id !== reviewedIds[index])) {
717
+ context.addIssue({
718
+ code: 'custom',
719
+ path: ['reviewedChangeSet', 'changes'],
720
+ message: 'Reviewed change set must contain each apply run change exactly once',
721
+ });
722
+ }
723
+ for (const [index, change] of run.reviewedChangeSet.changes.entries()) {
724
+ if (change.status !== 'approved') {
725
+ context.addIssue({
726
+ code: 'custom',
727
+ path: ['reviewedChangeSet', 'changes', index, 'status'],
728
+ message: 'Frozen reviewed changes must remain approved',
729
+ });
730
+ }
731
+ }
732
+ });
733
+ export const deliveryRecordStatusSchema = z.enum([
734
+ 'draft',
735
+ 'ready',
736
+ 'implementing',
737
+ 'verified',
738
+ 'superseded',
739
+ ]);
740
+ export const deliveryNarrativeSourceSchema = z.enum(['deterministic', 'agent', 'authored']);
741
+ export const deliveryAcceptanceCriterionSchema = z.object({
742
+ id: z.string().min(1),
743
+ label: z.string().min(1),
744
+ status: z.enum(['pending', 'passed', 'failed']).default('pending'),
745
+ evidence: z.array(z.string()).default([]),
746
+ });
747
+ export const deliveryRecordSchema = z.object({
748
+ version: z.literal(1),
749
+ id: z.string().min(1),
750
+ sessionId: z.string().min(1),
751
+ applyRunId: z.string().min(1),
752
+ title: z.string().min(1).max(120),
753
+ summary: z.string().max(2000).default(''),
754
+ intent: z.string().max(4000).default(''),
755
+ narrativeSource: deliveryNarrativeSourceSchema.default('deterministic'),
756
+ status: deliveryRecordStatusSchema,
757
+ changeIds: z.array(z.string().min(1)).min(1),
758
+ operationIds: z.array(z.string().min(1)).default([]),
759
+ affectedFiles: z.array(z.string().min(1)).default([]),
760
+ affectedComponents: z.array(z.string().min(1)).default([]),
761
+ affectedTokens: z.array(z.string().min(1)).default([]),
762
+ contexts: z
763
+ .array(z.object({
764
+ breakpoint: z.string(),
765
+ theme: z.string(),
766
+ state: z.string(),
767
+ }))
768
+ .default([]),
769
+ risks: z.array(z.string()).default([]),
770
+ questions: z.array(z.string()).default([]),
771
+ acceptanceCriteria: z.array(deliveryAcceptanceCriterionSchema).default([]),
772
+ blockers: z.array(z.string()).default([]),
773
+ validationResults: z.array(validationResultSchema).default([]),
774
+ verificationResults: z.array(verificationResultSchema).default([]),
775
+ evidence: z
776
+ .array(z.object({
777
+ label: z.string().min(1),
778
+ path: z.string().min(1),
779
+ createdAt: z.string().datetime(),
780
+ }))
781
+ .default([]),
782
+ revision: z.string().optional(),
783
+ baselineRevision: z.string().optional(),
784
+ appliedRevision: z.string().optional(),
785
+ designGraphRevision: z.string().optional(),
786
+ createdAt: z.string().datetime(),
787
+ updatedAt: z.string().datetime(),
788
+ verifiedAt: z.string().datetime().optional(),
789
+ supersededAt: z.string().datetime().optional(),
790
+ });
791
+ export const documentationPageSchema = z.object({
792
+ version: z.literal(1),
793
+ id: z.string().min(1),
794
+ kind: z.enum(['component', 'system', 'feature', 'screen']),
795
+ slug: z.string().min(1),
796
+ title: z.string().min(1),
797
+ summary: z.string().default(''),
798
+ body: z.string().default(''),
799
+ narrativeSource: deliveryNarrativeSourceSchema.default('deterministic'),
800
+ freshness: z.enum(['current', 'stale', 'conflicted']).default('current'),
801
+ sourceFiles: z.array(z.string()).default([]),
802
+ componentIds: z.array(z.string()).default([]),
803
+ deliveryRecordIds: z.array(z.string()).default([]),
804
+ revision: z.string().optional(),
805
+ designGraphRevision: z.string().optional(),
806
+ contentHash: z.string().min(1),
807
+ exportedHash: z.string().optional(),
808
+ exportedPath: z.string().optional(),
809
+ createdAt: z.string().datetime(),
810
+ updatedAt: z.string().datetime(),
811
+ });
812
+ export const designHistoryEntrySchema = z.object({
813
+ version: z.literal(1),
814
+ id: z.string().min(1),
815
+ deliveryRecordId: z.string().min(1),
816
+ applyRunId: z.string().min(1),
817
+ title: z.string().min(1),
818
+ summary: z.string().default(''),
819
+ changeIds: z.array(z.string().min(1)).min(1),
820
+ affectedFiles: z.array(z.string()).default([]),
821
+ contexts: z.array(z.object({ breakpoint: z.string(), theme: z.string(), state: z.string() })),
822
+ validationResults: z.array(validationResultSchema).default([]),
823
+ verificationResults: z.array(verificationResultSchema).default([]),
824
+ revision: z.string().optional(),
825
+ baselineRevision: z.string().optional(),
826
+ appliedRevision: z.string().optional(),
827
+ createdAt: z.string().datetime(),
828
+ });
829
+ export const deliveryMilestoneSchema = z.object({
830
+ version: z.literal(1),
831
+ id: z.string().min(1),
832
+ name: z.string().min(1).max(120),
833
+ summary: z.string().max(2000).default(''),
834
+ entryIds: z.array(z.string().min(1)).default([]),
835
+ status: z.enum(['draft', 'published']).default('draft'),
836
+ createdAt: z.string().datetime(),
837
+ updatedAt: z.string().datetime(),
838
+ publishedAt: z.string().datetime().optional(),
839
+ });
840
+ export const visualAgentContextTargetSchema = z.object({
841
+ id: z.string().min(1),
842
+ selector: z.string().min(1),
843
+ label: z.string().min(1),
844
+ kind: z.string().min(1),
845
+ component: z.string().nullable().optional(),
846
+ source: z.string().optional(),
847
+ confidence: confidenceSchema,
848
+ geometry: geometrySchema,
849
+ measurements: z.record(z.string(), changeValueSchema).default({}),
850
+ });
851
+ export const visualAgentCommentSchema = z.object({
852
+ id: z.string().min(1),
853
+ body: z.string().min(1).max(2000),
854
+ targetId: z.string().optional(),
855
+ regionId: z.string().optional(),
856
+ createdAt: z.string().datetime(),
857
+ });
858
+ export const visualAgentContextSchema = z.object({
859
+ targets: z.array(visualAgentContextTargetSchema).default([]),
860
+ region: z
861
+ .object({
862
+ id: z.string().min(1),
863
+ x: z.number().finite(),
864
+ y: z.number().finite(),
865
+ width: z.number().nonnegative().finite(),
866
+ height: z.number().nonnegative().finite(),
867
+ label: z.string().default('Canvas region'),
868
+ })
869
+ .optional(),
870
+ comments: z.array(visualAgentCommentSchema).default([]),
871
+ viewport: z.object({ width: z.number().positive(), height: z.number().positive() }),
872
+ breakpoint: z.string().default('current'),
873
+ theme: z.string().default('current'),
874
+ state: z.string().default('current'),
875
+ tokens: z.array(designTokenSchema).default([]),
876
+ designGraphRevision: z.string().optional(),
877
+ });
878
+ export const visualAgentProposalSchema = z.object({
879
+ id: z.string().min(1),
880
+ name: z.string().min(1).max(80),
881
+ summary: z.string().min(1).max(2000),
882
+ reasoning: z.array(z.string().min(1)).default([]),
883
+ exactValues: z.array(z.string().min(1)).default([]),
884
+ sourceLocations: z.array(z.string().min(1)).default([]),
885
+ responsiveImpact: z.string().min(1).max(2000),
886
+ verificationPlan: z.array(z.string().min(1)).default([]),
887
+ changes: z.array(designChangeSchema).default([]),
888
+ branchId: z.string().optional(),
889
+ status: z.enum(['proposed', 'previewing', 'promoted', 'rejected']).default('proposed'),
890
+ createdAt: z.string().datetime(),
891
+ updatedAt: z.string().datetime(),
892
+ });
893
+ export const visualAgentMessageSchema = z.object({
894
+ id: z.string().min(1),
895
+ role: z.enum(['user', 'agent', 'system']),
896
+ body: z.string().min(1),
897
+ createdAt: z.string().datetime(),
898
+ });
899
+ export const visualAgentRequestSchema = z.object({
900
+ id: z.string().min(1),
901
+ sessionId: z.string().min(1),
902
+ title: z.string().min(1).max(120),
903
+ prompt: z.string().min(1).max(8000),
904
+ status: z.enum(['queued', 'thinking', 'ready', 'needs_attention', 'closed']),
905
+ context: visualAgentContextSchema,
906
+ messages: z.array(visualAgentMessageSchema).default([]),
907
+ proposals: z.array(visualAgentProposalSchema).default([]),
908
+ agent: z
909
+ .object({
910
+ name: z.string().min(1),
911
+ version: z.string().optional(),
912
+ taskId: z.string().optional(),
913
+ })
914
+ .optional(),
915
+ claimAttemptId: z.string().optional(),
916
+ claimCapabilityHash: z
917
+ .string()
918
+ .regex(/^[a-f0-9]{64}$/)
919
+ .optional(),
920
+ claimExpiresAt: z.string().datetime().optional(),
921
+ error: z.string().optional(),
922
+ createdAt: z.string().datetime(),
923
+ updatedAt: z.string().datetime(),
924
+ });
925
+ export const stressConditionIdSchema = z.enum([
926
+ 'long-content',
927
+ 'empty-content',
928
+ 'large-numbers',
929
+ 'missing-images',
930
+ 'loading-state',
931
+ 'error-state',
932
+ 'offline-state',
933
+ 'text-200',
934
+ 'browser-zoom-200',
935
+ 'high-contrast',
936
+ 'monochrome',
937
+ 'reduced-motion',
938
+ 'keyboard-only',
939
+ ]);
940
+ export const stressTestSessionSchema = z.object({
941
+ conditions: z.array(stressConditionIdSchema).default([]),
942
+ scope: z.enum(['selection', 'canvas']).default('selection'),
943
+ targetId: z.string().optional(),
944
+ viewport: z.object({ width: z.number().positive(), height: z.number().positive() }),
945
+ temporary: z.literal(true),
946
+ appliedAt: z.string().datetime(),
314
947
  });
315
948
  export const surfaceSnapshotSchema = z.object({
316
949
  platform: z.enum(['swiftui', 'react-native']),
@@ -321,6 +954,71 @@ export const surfaceSnapshotSchema = z.object({
321
954
  controlsByTarget: z.record(z.string(), z.array(controlDescriptorSchema)).default({}),
322
955
  updatedAt: z.string().datetime(),
323
956
  });
957
+ export const previewContextSchema = z.object({
958
+ version: z.literal(1),
959
+ requestRevision: z.number().int().nonnegative(),
960
+ viewport: z.object({
961
+ id: z.string().min(1),
962
+ width: z.number().positive().optional(),
963
+ height: z.number().positive().optional(),
964
+ }),
965
+ theme: z.string().min(1),
966
+ state: z.string().min(1),
967
+ motionPreference: z.enum(['system', 'reduce', 'no-preference']),
968
+ selectedTarget: z
969
+ .object({
970
+ id: z.string().min(1),
971
+ selector: z.string().min(1),
972
+ })
973
+ .optional(),
974
+ });
975
+ export const previewAxisResultSchema = z.object({
976
+ status: z.enum(['applied', 'current', 'unsupported']),
977
+ method: z.string().min(1),
978
+ evidence: z.array(z.string()).default([]),
979
+ failureReason: z.string().min(1).optional(),
980
+ });
981
+ export const previewContextResultSchema = z.object({
982
+ version: z.literal(1),
983
+ requestRevision: z.number().int().nonnegative(),
984
+ applied: z.boolean(),
985
+ reloadQuery: z.record(z.string(), z.string()).optional(),
986
+ failureReason: z.string().min(1).optional(),
987
+ context: previewContextSchema,
988
+ axes: z.object({
989
+ viewport: previewAxisResultSchema,
990
+ theme: previewAxisResultSchema,
991
+ state: previewAxisResultSchema,
992
+ motion: previewAxisResultSchema,
993
+ }),
994
+ });
995
+ export const previewCapabilitiesSchema = z.object({
996
+ version: z.literal(1),
997
+ viewports: z.array(z.object({
998
+ id: z.string().min(1),
999
+ width: z.number().positive().optional(),
1000
+ height: z.number().positive().optional(),
1001
+ })),
1002
+ themes: z.array(z.object({
1003
+ id: z.string().min(1),
1004
+ selector: z.string().min(1).optional(),
1005
+ attribute: z.string().min(1).optional(),
1006
+ value: z.string().optional(),
1007
+ })),
1008
+ states: z.array(z.object({
1009
+ id: z.string().min(1),
1010
+ methods: z
1011
+ .array(z.enum(['query', 'variant', 'pseudo', 'native', 'reduced-motion']))
1012
+ .default([]),
1013
+ })),
1014
+ motionPreferences: z.array(z.enum(['system', 'reduce', 'no-preference'])).default(['system']),
1015
+ });
1016
+ export const workspaceSnapshotSchema = z.object({
1017
+ version: z.literal(1),
1018
+ capabilities: previewCapabilitiesSchema,
1019
+ currentPreviewContext: previewContextSchema.nullable().default(null),
1020
+ lastPreviewApplication: previewContextResultSchema.nullable().default(null),
1021
+ });
324
1022
  export const previewCommandSchema = z.object({
325
1023
  id: z.string().min(1),
326
1024
  targetId: z.string().min(1),
@@ -330,37 +1028,43 @@ export const previewCommandSchema = z.object({
330
1028
  createdAt: z.string().datetime(),
331
1029
  });
332
1030
  export function changeKey(change) {
333
- const context = change.context;
334
- return [
1031
+ const contextSet = change.contextSet;
1032
+ return JSON.stringify([
335
1033
  change.target.id,
336
1034
  change.property,
337
1035
  change.scope,
338
- context.breakpoint,
339
- context.theme,
340
- context.state,
341
- [...change.stateIds].sort().join(','),
342
- ].join('::');
1036
+ [...contextSet.breakpoints].sort(),
1037
+ [...contextSet.themes].sort(),
1038
+ [...contextSet.states].sort(),
1039
+ [...change.stateIds].sort(),
1040
+ ]);
343
1041
  }
344
1042
  export function coalesceChanges(changes) {
345
- const order = [];
346
- const byKey = new Map();
1043
+ const result = [];
1044
+ const editableIndexByKey = new Map();
347
1045
  for (const change of changes) {
348
1046
  const parsed = designChangeSchema.parse(change);
349
1047
  const key = changeKey(parsed);
350
- const existing = byKey.get(key);
351
- if (!existing) {
352
- order.push(key);
353
- byKey.set(key, parsed);
1048
+ if (!['draft', 'unresolved'].includes(parsed.status)) {
1049
+ result.push(parsed);
1050
+ editableIndexByKey.delete(key);
354
1051
  continue;
355
1052
  }
356
- byKey.set(key, {
1053
+ const existingIndex = editableIndexByKey.get(key);
1054
+ if (existingIndex === undefined) {
1055
+ editableIndexByKey.set(key, result.length);
1056
+ result.push(parsed);
1057
+ continue;
1058
+ }
1059
+ const existing = result[existingIndex];
1060
+ result[existingIndex] = {
357
1061
  ...parsed,
358
1062
  id: existing.id,
359
1063
  before: existing.before,
360
1064
  createdAt: existing.createdAt,
361
- });
1065
+ };
362
1066
  }
363
- return order.map((key) => byKey.get(key));
1067
+ return result;
364
1068
  }
365
1069
  function renderValue(value, unit) {
366
1070
  const rendered = typeof value === 'string' ? value : JSON.stringify(value);
@@ -385,12 +1089,13 @@ export function renderChangePrompt(changeSet) {
385
1089
  return lines.join('\n');
386
1090
  }
387
1091
  for (const [index, change] of approved.entries()) {
388
- lines.push(`${index + 1}. ${change.target.label} — ${change.property}`, ` - Change: ${renderValue(change.before, change.unit)} → ${renderValue(change.after, change.unit)}`, ` - Scope: ${change.scope}; breakpoint=${change.context.breakpoint}; theme=${change.context.theme}; state=${change.context.state}`, ` - Target: ${change.target.source ? `${change.target.source.file}${change.target.source.line ? `:${change.target.source.line}` : ''}` : JSON.stringify(change.target.locator)}`, ` - Evidence: ${change.confidence}; ${[...new Set([...change.target.evidence, ...change.evidence])].join('; ') || 'none recorded'}`);
1092
+ lines.push(`${index + 1}. ${change.target.label} — ${change.property}`, ` - Change: ${renderValue(change.before, change.unit)} → ${renderValue(change.after, change.unit)}`, ` - Scope: ${change.scope}; breakpoint=${change.context.breakpoint}; theme=${change.context.theme}; state=${change.context.state}`, ` - Affected contexts: breakpoints=${change.contextSet.breakpoints.join(', ')}; themes=${change.contextSet.themes.join(', ')}; states=${change.contextSet.states.join(', ')}`, ` - Target: ${change.target.source ? `${change.target.source.file}${change.target.source.line ? `:${change.target.source.line}` : ''}` : JSON.stringify(change.target.locator)}`, ` - Evidence: ${change.confidence}; ${[...new Set([...change.target.evidence, ...change.evidence])].join('; ') || 'none recorded'}`);
389
1093
  if (change.token)
390
1094
  lines.push(` - Token: ${change.token}`);
391
1095
  if (change.stateIds.length)
392
1096
  lines.push(` - State set: ${change.stateIds.join(', ')}`);
393
- const mapping = change.mappingCandidates.find((candidate) => candidate.id === change.selectedMappingId);
1097
+ const mapping = change.mappingCandidates.find((candidate) => candidate.id === change.selectedMappingId) ??
1098
+ (change.mappingCandidates.length === 1 ? change.mappingCandidates[0] : undefined);
394
1099
  if (mapping) {
395
1100
  lines.push(` - Source intent: ${mapping.label}; property=${mapping.property}; confidence=${mapping.confidence}; blast-radius=${mapping.blastRadius}`);
396
1101
  }