@usefragments/core 1.10.2 → 2.0.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 (130) hide show
  1. package/dist/{chunk-MZ4SW3TP.js → chunk-3IOWHECM.js} +8 -1
  2. package/dist/{chunk-MZ4SW3TP.js.map → chunk-3IOWHECM.js.map} +1 -1
  3. package/dist/chunk-7ULGH74M.js +66 -0
  4. package/dist/chunk-7ULGH74M.js.map +1 -0
  5. package/dist/{chunk-XN3LSDPY.js → chunk-BMPYIUZE.js} +612 -344
  6. package/dist/chunk-BMPYIUZE.js.map +1 -0
  7. package/dist/{chunk-YF65VYRY.js → chunk-ML5S6QNU.js} +191 -169
  8. package/dist/chunk-ML5S6QNU.js.map +1 -0
  9. package/dist/{chunk-3LLRNCPX.js → chunk-MZ2FS7U4.js} +1 -1
  10. package/dist/chunk-MZ2FS7U4.js.map +1 -0
  11. package/dist/chunk-PWIJMOI4.js +202 -0
  12. package/dist/chunk-PWIJMOI4.js.map +1 -0
  13. package/dist/chunk-RYFULE43.js +578 -0
  14. package/dist/chunk-RYFULE43.js.map +1 -0
  15. package/dist/codes/index.d.ts +2 -2
  16. package/dist/codes/index.js +3 -2
  17. package/dist/compiled-types/index.d.ts +327 -2
  18. package/dist/compiled-types/index.js +1 -1
  19. package/dist/generate/index.d.ts +3 -2
  20. package/dist/{governance-CLk_wkP9.d.ts → governance-hOPXGbbs.d.ts} +474 -515
  21. package/dist/governance-telemetry.d.ts +6 -0
  22. package/dist/governance-telemetry.js +1 -1
  23. package/dist/{index-_sxhUNqx.d.ts → index-C8bcXVav.d.ts} +448 -448
  24. package/dist/index.d.ts +3280 -1305
  25. package/dist/index.js +2414 -350
  26. package/dist/index.js.map +1 -1
  27. package/dist/manifest.d.ts +228 -0
  28. package/dist/manifest.js +24 -0
  29. package/dist/manifest.js.map +1 -0
  30. package/dist/preview/index.js +45 -1
  31. package/dist/preview/index.js.map +1 -1
  32. package/dist/preview-runtime.d.ts +1 -2
  33. package/dist/preview-runtime.js +150 -14
  34. package/dist/preview-runtime.js.map +1 -1
  35. package/dist/react-types.d.ts +1 -2
  36. package/dist/registry.d.ts +1412 -203
  37. package/dist/registry.js +30 -3
  38. package/dist/schemas/index.d.ts +1 -1
  39. package/dist/schemas/index.js +3 -2
  40. package/dist/storyAdapter.d.ts +1 -2
  41. package/dist/storyAdapter.js +11 -49
  42. package/dist/storyAdapter.js.map +1 -1
  43. package/dist/test-utils.d.ts +3 -2
  44. package/dist/topology/index.d.ts +1 -1
  45. package/dist/topology/index.js +1 -1
  46. package/package.json +8 -2
  47. package/src/__tests__/contract-parser.test.ts +318 -277
  48. package/src/__tests__/preview-runtime-hook.test.tsx +315 -0
  49. package/src/__tests__/preview-runtime.test.tsx +30 -8
  50. package/src/__tests__/schema.test.ts +191 -14
  51. package/src/analysis-plan/analysis-plan-v1.test.ts +320 -0
  52. package/src/analysis-plan/coverage.ts +181 -0
  53. package/src/analysis-plan/digest.ts +141 -0
  54. package/src/analysis-plan/index.ts +34 -0
  55. package/src/analysis-plan/types.ts +207 -0
  56. package/src/approved-contract-tokens.test.ts +39 -0
  57. package/src/approved-contract-tokens.ts +18 -0
  58. package/src/codes/__tests__/codes.test.ts +13 -0
  59. package/src/codes/codes.ts +40 -0
  60. package/src/compiled-types/index.ts +640 -39
  61. package/src/compiled-types/parse.test.ts +145 -4
  62. package/src/component-contract.ts +95 -53
  63. package/src/composition.ts +7 -13
  64. package/src/constants.ts +3 -6
  65. package/src/contract/hash.test.ts +20 -0
  66. package/src/contract/hash.ts +66 -9
  67. package/src/contract/index.ts +24 -1
  68. package/src/contract/manifest.test.ts +94 -0
  69. package/src/contract/manifest.ts +68 -0
  70. package/src/contract/preimage.test.ts +219 -1
  71. package/src/contract/preimage.ts +326 -6
  72. package/src/contract/stamp.test.ts +3 -0
  73. package/src/contract/stamp.ts +1 -1
  74. package/src/contract-parser.ts +54 -30
  75. package/src/defineFragment.test.ts +476 -91
  76. package/src/defineFragment.ts +204 -114
  77. package/src/domain-ids.test.ts +35 -0
  78. package/src/domain-ids.ts +61 -0
  79. package/src/evaluation/evaluate.test.ts +522 -0
  80. package/src/evaluation/evaluate.ts +690 -0
  81. package/src/evaluation/evaluation-v2-receipt-v1.test.ts +772 -0
  82. package/src/evaluation/index.ts +58 -0
  83. package/src/evaluation/receipt.ts +753 -0
  84. package/src/evaluation/types.ts +406 -0
  85. package/src/facts/builders.ts +2 -0
  86. package/src/facts/compile.ts +29 -6
  87. package/src/facts/fact-index.ts +13 -3
  88. package/src/facts/fact-integrity-v1.test.ts +172 -0
  89. package/src/facts/facts.test.ts +15 -0
  90. package/src/facts/ids.ts +46 -3
  91. package/src/facts/index.ts +14 -1
  92. package/src/facts/integrity.ts +134 -0
  93. package/src/facts/types.ts +36 -0
  94. package/src/governance-integrity.test.ts +1 -0
  95. package/src/governance-integrity.ts +5 -3
  96. package/src/governance-telemetry.ts +8 -0
  97. package/src/governance.ts +70 -8
  98. package/src/index.ts +230 -37
  99. package/src/preview/validation.test.ts +62 -0
  100. package/src/preview/validation.ts +48 -2
  101. package/src/preview-runtime.tsx +227 -20
  102. package/src/registry-install-plan.ts +200 -109
  103. package/src/registry-shards.test.ts +263 -0
  104. package/src/registry.ts +237 -0
  105. package/src/repository-binding.test.ts +50 -0
  106. package/src/repository-binding.ts +96 -0
  107. package/src/rules/families.test.ts +36 -0
  108. package/src/rules/finding.ts +7 -2
  109. package/src/rules/index.ts +17 -1
  110. package/src/rules/rule-config.test.ts +66 -0
  111. package/src/rules/rule-config.ts +73 -0
  112. package/src/rules/rules.test.ts +26 -0
  113. package/src/rules/tokens-css-vars-must-be-defined.test.ts +51 -2
  114. package/src/rules/tokens-css-vars-must-be-defined.ts +34 -1
  115. package/src/schema.ts +293 -113
  116. package/src/schemas/index.ts +1 -1
  117. package/src/storyAdapter.test.ts +68 -12
  118. package/src/storyAdapter.ts +44 -75
  119. package/src/topology/resolve-area.ts +1 -1
  120. package/src/types.ts +258 -40
  121. package/dist/chunk-3LLRNCPX.js.map +0 -1
  122. package/dist/chunk-RANPUC6C.js +0 -72
  123. package/dist/chunk-RANPUC6C.js.map +0 -1
  124. package/dist/chunk-XN3LSDPY.js.map +0 -1
  125. package/dist/chunk-YF65VYRY.js.map +0 -1
  126. package/src/fragment-types.ts +0 -214
  127. package/src/react-create-element.test.ts +0 -22
  128. package/src/react-create-element.ts +0 -12
  129. package/src/storyFilters.test.ts +0 -350
  130. package/src/storyFilters.ts +0 -253
@@ -24,9 +24,35 @@
24
24
  *
25
25
  * Runtime-portable by construction: no Node APIs, no Convex imports, no React.
26
26
  */
27
- import type { GovernanceConfig } from "../governance.js";
27
+ import type { ComponentGovernanceRecord, GovernanceConfig } from "../governance.js";
28
+ import { resolveGovernanceRecordsForIdentity } from "../governance.js";
28
29
  import { projectV1OwnedComponentId, projectV1OwnedImportIdentity } from "../package-identity.js";
29
- import { canonicalPreimage, contractHash } from "./hash.js";
30
+ import { artifactContentHash, canonicalPreimage, contractHash } from "./hash.js";
31
+
32
+ /**
33
+ * Positive pick-list of per-component `contract` fields that enter the FCID.
34
+ * Display/guidance fields (`propsSummary`, `canonicalUsage`, `scenarioTags`,
35
+ * story `states`/`variants`, descriptions) are intentionally absent — adapters
36
+ * may still pass them, but {@link projectEnforcementContract} never copies them.
37
+ */
38
+ export const CONTRACT_ENFORCEMENT_FIELDS = [
39
+ "a11yRules",
40
+ "bans",
41
+ "performanceBudget",
42
+ "compoundChildren",
43
+ "composition",
44
+ ] as const;
45
+
46
+ export type ContractEnforcementField = (typeof CONTRACT_ENFORCEMENT_FIELDS)[number];
47
+
48
+ /** Display-only contract keys that must never enter the FCID positive pick-list. */
49
+ export const CONTRACT_DISPLAY_ONLY_FIELDS = [
50
+ "propsSummary",
51
+ "canonicalUsage",
52
+ "scenarioTags",
53
+ "states",
54
+ "variants",
55
+ ] as const;
30
56
 
31
57
  // ---------------------------------------------------------------------------
32
58
  // Domains
@@ -56,11 +82,15 @@ export interface ContractComponentInput {
56
82
  /** Prop names the component accepts. */
57
83
  props?: readonly string[];
58
84
  /**
59
- * The per-component enforcement contract as authored (bans, a11y rules,
60
- * required compound children). Passed through whole it is enforcement data
61
- * by definition.
85
+ * The per-component contract as authored. Only {@link CONTRACT_ENFORCEMENT_FIELDS}
86
+ * are projected into the FCIDdisplay/guidance keys are stripped structurally.
62
87
  */
63
88
  contract?: unknown;
89
+ /**
90
+ * Resolved component governance records (`govern:` / `governance`). Editing
91
+ * these changes what CI allows, so they are FCID identity.
92
+ */
93
+ governance?: readonly ComponentGovernanceRecord[];
64
94
  /** Required compound-children names, when the structure is declared. */
65
95
  compoundChildren?: readonly string[];
66
96
  }
@@ -143,6 +173,18 @@ export interface ContractCatalogInput {
143
173
  policy?: ContractPolicyInput | null;
144
174
  }
145
175
 
176
+ /** Deterministic runtime refusal for malformed or ambiguous FCID inputs. */
177
+ export class ContractCatalogValidationError extends TypeError {
178
+ readonly issues: readonly string[];
179
+
180
+ constructor(issues: readonly string[]) {
181
+ const sortedIssues = [...issues].sort();
182
+ super(["Invalid contract catalog:", ...sortedIssues.map((issue) => ` - ${issue}`)].join("\n"));
183
+ this.name = "ContractCatalogValidationError";
184
+ this.issues = sortedIssues;
185
+ }
186
+ }
187
+
146
188
  // ---------------------------------------------------------------------------
147
189
  // Preimage
148
190
  // ---------------------------------------------------------------------------
@@ -226,13 +268,259 @@ function sortCanonical<T>(items: readonly T[]): T[] {
226
268
  .map(({ item }) => item);
227
269
  }
228
270
 
271
+ function isPlainRecord(value: unknown): value is Readonly<Record<string, unknown>> {
272
+ if (!value || typeof value !== "object" || Array.isArray(value)) return false;
273
+ const prototype = Object.getPrototypeOf(value);
274
+ return prototype === Object.prototype || prototype === null;
275
+ }
276
+
277
+ function requireNonEmptyString(value: unknown, path: string, issues: string[]): void {
278
+ if (typeof value !== "string" || value.length === 0) {
279
+ issues.push(`${path}: expected a non-empty string`);
280
+ }
281
+ }
282
+
283
+ function validateStringArray(value: unknown, path: string, issues: string[]): void {
284
+ if (!Array.isArray(value)) {
285
+ issues.push(`${path}: expected an array`);
286
+ return;
287
+ }
288
+ const seen = new Set<string>();
289
+ for (const [index, entry] of value.entries()) {
290
+ requireNonEmptyString(entry, `${path}[${index}]`, issues);
291
+ if (typeof entry === "string" && entry.length > 0) {
292
+ if (seen.has(entry)) issues.push(`${path}: duplicate value "${entry}"`);
293
+ seen.add(entry);
294
+ }
295
+ }
296
+ }
297
+
298
+ function validateContractCatalog(catalog: unknown): asserts catalog is ContractCatalogInput {
299
+ const issues: string[] = [];
300
+ if (!isPlainRecord(catalog)) {
301
+ throw new ContractCatalogValidationError(["(root): expected a plain object"]);
302
+ }
303
+
304
+ const componentIdentities = new Set<string>();
305
+ if (catalog.components !== undefined) {
306
+ if (!Array.isArray(catalog.components)) {
307
+ issues.push("components: expected an array");
308
+ } else {
309
+ for (const [index, value] of catalog.components.entries()) {
310
+ const path = `components[${index}]`;
311
+ if (!isPlainRecord(value)) {
312
+ issues.push(`${path}: expected a plain object`);
313
+ continue;
314
+ }
315
+ requireNonEmptyString(value.name, `${path}.name`, issues);
316
+ if (value.parentName !== undefined) {
317
+ requireNonEmptyString(value.parentName, `${path}.parentName`, issues);
318
+ }
319
+ if (typeof value.name === "string" && value.name.length > 0) {
320
+ const identity = `${typeof value.parentName === "string" ? value.parentName : ""}#${value.name}`;
321
+ if (componentIdentities.has(identity)) {
322
+ issues.push(`components: duplicate identity "${identity}"`);
323
+ }
324
+ componentIdentities.add(identity);
325
+ }
326
+ if (value.props !== undefined) validateStringArray(value.props, `${path}.props`, issues);
327
+ if (value.compoundChildren !== undefined) {
328
+ validateStringArray(value.compoundChildren, `${path}.compoundChildren`, issues);
329
+ }
330
+ if (value.contract !== undefined && !isPlainRecord(value.contract)) {
331
+ issues.push(`${path}.contract: expected a plain object`);
332
+ }
333
+ if (value.governance !== undefined && !Array.isArray(value.governance)) {
334
+ issues.push(`${path}.governance: expected an array`);
335
+ }
336
+ }
337
+ }
338
+ }
339
+
340
+ const tokenNames = new Set<string>();
341
+ if (catalog.tokens !== undefined) {
342
+ if (!Array.isArray(catalog.tokens)) {
343
+ issues.push("tokens: expected an array");
344
+ } else {
345
+ for (const [index, value] of catalog.tokens.entries()) {
346
+ const path = `tokens[${index}]`;
347
+ if (!isPlainRecord(value)) {
348
+ issues.push(`${path}: expected a plain object`);
349
+ continue;
350
+ }
351
+ requireNonEmptyString(value.name, `${path}.name`, issues);
352
+ if (typeof value.name === "string" && value.name.length > 0) {
353
+ if (tokenNames.has(value.name)) issues.push(`tokens: duplicate identity "${value.name}"`);
354
+ tokenNames.add(value.name);
355
+ }
356
+ if (
357
+ (typeof value.value !== "string" && typeof value.value !== "number") ||
358
+ (typeof value.value === "number" && !Number.isFinite(value.value))
359
+ ) {
360
+ issues.push(`${path}.value: expected a finite number or string`);
361
+ }
362
+ if (value.type !== undefined) requireNonEmptyString(value.type, `${path}.type`, issues);
363
+ }
364
+ }
365
+ }
366
+
367
+ const confirmedMappings = new Set<string>();
368
+ if (catalog.canonicalMappings !== undefined) {
369
+ if (!Array.isArray(catalog.canonicalMappings)) {
370
+ issues.push("canonicalMappings: expected an array");
371
+ } else {
372
+ for (const [index, value] of catalog.canonicalMappings.entries()) {
373
+ const path = `canonicalMappings[${index}]`;
374
+ if (!isPlainRecord(value)) {
375
+ issues.push(`${path}: expected a plain object`);
376
+ continue;
377
+ }
378
+ requireNonEmptyString(value.component, `${path}.component`, issues);
379
+ requireNonEmptyString(value.canonical, `${path}.canonical`, issues);
380
+ if (!(["confirmed", "proposed", "unknown"] as const).includes(value.status as never)) {
381
+ issues.push(`${path}.status: expected confirmed, proposed, or unknown`);
382
+ }
383
+ if (value.status === "confirmed" && typeof value.component === "string") {
384
+ if (confirmedMappings.has(value.component)) {
385
+ issues.push(`canonicalMappings: duplicate confirmed identity "${value.component}"`);
386
+ }
387
+ confirmedMappings.add(value.component);
388
+ }
389
+ if (value.importPath !== undefined) {
390
+ requireNonEmptyString(value.importPath, `${path}.importPath`, issues);
391
+ }
392
+ if (value.propMapping !== undefined && !Array.isArray(value.propMapping)) {
393
+ issues.push(`${path}.propMapping: expected an array`);
394
+ }
395
+ if (value.resolves !== undefined && !Array.isArray(value.resolves)) {
396
+ issues.push(`${path}.resolves: expected an array`);
397
+ }
398
+ if (value.bridge !== undefined && !isPlainRecord(value.bridge)) {
399
+ issues.push(`${path}.bridge: expected a plain object`);
400
+ }
401
+ }
402
+ }
403
+ }
404
+
405
+ if (catalog.policy !== undefined && catalog.policy !== null) {
406
+ if (!isPlainRecord(catalog.policy)) {
407
+ issues.push("policy: expected a plain object or null");
408
+ } else {
409
+ if (catalog.policy.rules !== undefined && !isPlainRecord(catalog.policy.rules)) {
410
+ issues.push("policy.rules: expected a plain object");
411
+ }
412
+ if (catalog.policy.codes !== undefined && !isPlainRecord(catalog.policy.codes)) {
413
+ issues.push("policy.codes: expected a plain object");
414
+ }
415
+ if (
416
+ catalog.policy.compositionPatterns !== undefined &&
417
+ !Array.isArray(catalog.policy.compositionPatterns)
418
+ ) {
419
+ issues.push("policy.compositionPatterns: expected an array");
420
+ }
421
+ if (catalog.policy.waivers !== undefined) {
422
+ if (!Array.isArray(catalog.policy.waivers)) {
423
+ issues.push("policy.waivers: expected an array");
424
+ } else {
425
+ const waiverIds = new Set<string>();
426
+ for (const [index, value] of catalog.policy.waivers.entries()) {
427
+ const path = `policy.waivers[${index}]`;
428
+ if (!isPlainRecord(value)) {
429
+ issues.push(`${path}: expected a plain object`);
430
+ continue;
431
+ }
432
+ requireNonEmptyString(value.id, `${path}.id`, issues);
433
+ requireNonEmptyString(value.target, `${path}.target`, issues);
434
+ requireNonEmptyString(value.reason, `${path}.reason`, issues);
435
+ if (value.expiresOn !== undefined) {
436
+ requireNonEmptyString(value.expiresOn, `${path}.expiresOn`, issues);
437
+ }
438
+ if (typeof value.id === "string" && value.id.length > 0) {
439
+ if (waiverIds.has(value.id)) {
440
+ issues.push(`policy.waivers: duplicate identity "${value.id}"`);
441
+ }
442
+ waiverIds.add(value.id);
443
+ }
444
+ }
445
+ }
446
+ }
447
+ }
448
+ }
449
+
450
+ if (issues.length > 0) throw new ContractCatalogValidationError(issues);
451
+ }
452
+
453
+ /**
454
+ * Project only enforcement fields from a per-component `contract` blob.
455
+ * Unknown / display keys are dropped; an all-display contract becomes `undefined`.
456
+ */
457
+ export function projectEnforcementContract(contract: unknown): unknown {
458
+ if (!contract || typeof contract !== "object" || Array.isArray(contract)) {
459
+ return undefined;
460
+ }
461
+ const source = contract as Readonly<Record<string, unknown>>;
462
+ const projected: Record<string, unknown> = {};
463
+
464
+ if (Array.isArray(source.a11yRules)) {
465
+ projected.a11yRules = sortCanonical([...source.a11yRules]);
466
+ }
467
+ if (Array.isArray(source.bans)) {
468
+ projected.bans = sortCanonical([...source.bans]);
469
+ }
470
+ if (typeof source.performanceBudget === "number") {
471
+ projected.performanceBudget = source.performanceBudget;
472
+ }
473
+ if (Array.isArray(source.composition)) {
474
+ projected.composition = sortCanonical([...source.composition]);
475
+ }
476
+ const compoundChildren = projectContractCompoundChildren(source.compoundChildren);
477
+ if (compoundChildren !== undefined) {
478
+ projected.compoundChildren = compoundChildren;
479
+ }
480
+
481
+ return Object.keys(projected).length > 0 ? projected : undefined;
482
+ }
483
+
484
+ /**
485
+ * Compound-children structure is enforcement (required / accepts); prose
486
+ * descriptions are display-only and never enter the FCID.
487
+ */
488
+ function projectContractCompoundChildren(value: unknown): unknown {
489
+ if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
490
+ const entries = Object.entries(value as Readonly<Record<string, unknown>>)
491
+ .map(([name, raw]) => {
492
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
493
+ return [name, {}] as const;
494
+ }
495
+ const child = raw as Readonly<Record<string, unknown>>;
496
+ const projected: Record<string, unknown> = {};
497
+ if (typeof child.required === "boolean") projected.required = child.required;
498
+ if (Array.isArray(child.accepts)) {
499
+ projected.accepts = [...child.accepts]
500
+ .filter((entry): entry is string => typeof entry === "string")
501
+ .sort();
502
+ }
503
+ return [name, projected] as const;
504
+ })
505
+ .sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0));
506
+ return Object.fromEntries(entries);
507
+ }
508
+
509
+ function projectGovernanceRecords(
510
+ records: readonly ComponentGovernanceRecord[] | undefined
511
+ ): unknown {
512
+ if (!records || records.length === 0) return undefined;
513
+ return sortCanonical([...records]);
514
+ }
515
+
229
516
  function projectComponents(components: readonly ContractComponentInput[] | undefined): unknown {
230
517
  return sortCanonical(
231
518
  (components ?? []).map((component) => ({
232
519
  name: component.name,
233
520
  parentName: component.parentName,
234
521
  props: component.props ? [...component.props].sort() : undefined,
235
- contract: component.contract,
522
+ contract: projectEnforcementContract(component.contract),
523
+ governance: projectGovernanceRecords(component.governance),
236
524
  compoundChildren: component.compoundChildren
237
525
  ? [...component.compoundChildren].sort()
238
526
  : undefined,
@@ -351,6 +639,7 @@ function projectPolicy(policy: ContractPolicyInput | null | undefined): unknown
351
639
  * is the fcid.
352
640
  */
353
641
  export function projectContractPreimage(catalog: ContractCatalogInput): ContractPreimage {
642
+ validateContractCatalog(catalog);
354
643
  return {
355
644
  schema: CONTRACT_PREIMAGE_SCHEMA,
356
645
  domains: {
@@ -418,6 +707,8 @@ export function contractComponentsFromFragments(
418
707
  meta?: { name?: unknown; parentComponentName?: unknown };
419
708
  props?: Record<string, unknown>;
420
709
  contract?: unknown;
710
+ governance?: unknown;
711
+ govern?: unknown;
421
712
  structure?: { compoundChildren?: unknown };
422
713
  } | null;
423
714
  const name = fragment?.meta?.name;
@@ -429,13 +720,42 @@ export function contractComponentsFromFragments(
429
720
  .map((child) => (child as { name?: unknown } | null)?.name)
430
721
  .filter((childName): childName is string => typeof childName === "string")
431
722
  : undefined;
723
+ const governance = resolveGovernanceRecordsForIdentity({
724
+ governance: Array.isArray(fragment?.governance)
725
+ ? (fragment.governance as ComponentGovernanceRecord[])
726
+ : undefined,
727
+ govern: typeof fragment?.govern === "function" ? (fragment.govern as never) : undefined,
728
+ });
432
729
  components.push({
433
730
  name,
434
731
  parentName: typeof parentName === "string" ? parentName : undefined,
435
732
  props: Object.keys(fragment?.props ?? {}),
436
733
  contract: fragment?.contract,
734
+ governance,
437
735
  compoundChildren,
438
736
  });
439
737
  }
440
738
  return components;
441
739
  }
740
+
741
+ /**
742
+ * Derive a preview `artifactId` from modules, states, guidance/render content,
743
+ * and runtime config. Never an FCID — callers must not assign
744
+ * `derivedFromContractHash` into `artifactId` (or the reverse).
745
+ */
746
+ export function deriveArtifactId(content: {
747
+ modules?: unknown;
748
+ states?: unknown;
749
+ guidance?: unknown;
750
+ render?: unknown;
751
+ runtime?: unknown;
752
+ }): string {
753
+ return artifactContentHash({
754
+ schema: "artifact-content:v1",
755
+ modules: content.modules,
756
+ states: content.states,
757
+ guidance: content.guidance,
758
+ render: content.render,
759
+ runtime: content.runtime,
760
+ });
761
+ }
@@ -75,6 +75,9 @@ Prefer canonical components over raw HTML elements.
75
75
  // Header without the fcid line (and vice versa) is not a stamp.
76
76
  expect(parseContractStamp("contract: fragments contract v12 · a3f9e21\n")).toBeNull();
77
77
  expect(parseContractStamp(`contract-fcid: ${FCID}\n`)).toBeNull();
78
+ expect(
79
+ parseContractStamp(`contract: fragments contract v0 · a3f9e21\ncontract-fcid: ${FCID}\n`)
80
+ ).toBeNull();
78
81
  // Short fcid in the header must prefix the full fcid.
79
82
  expect(
80
83
  parseContractStamp(`contract: fragments contract v12 · 0000000\ncontract-fcid: ${FCID}\n`)
@@ -87,7 +87,7 @@ export function parseContractStamp(text: string): ContractStampRecord | null {
87
87
  const fcidLine = STAMP_FCID_LINE.exec(text);
88
88
  if (!header || !fcidLine) return null;
89
89
  const contractVersion = Number(header[1]);
90
- if (!Number.isSafeInteger(contractVersion)) return null;
90
+ if (!Number.isSafeInteger(contractVersion) || contractVersion < 1) return null;
91
91
  const fcid = fcidLine[1];
92
92
  if (!fcid.startsWith(header[2])) return null;
93
93
 
@@ -6,8 +6,8 @@
6
6
  * (MCP tools, governance, search) work unchanged.
7
7
  */
8
8
 
9
- import { componentContractSchema } from './component-contract.js';
10
- import type { ComponentContract } from './component-contract.js';
9
+ import { componentContractSchema } from "./component-contract.js";
10
+ import type { ComponentContract } from "./component-contract.js";
11
11
 
12
12
  // Use the context package's CompiledFragment type via re-export from types.ts
13
13
  // Since core can't depend on context, we define the minimal output shape here
@@ -20,7 +20,7 @@ export interface CompiledContractOutput {
20
20
  description: string;
21
21
  category: string;
22
22
  tags?: string[];
23
- status?: 'stable' | 'beta' | 'deprecated' | 'experimental';
23
+ status?: "stable" | "beta" | "deprecated" | "experimental";
24
24
  dependencies?: Array<{ name: string; version: string; reason?: string }>;
25
25
  figma?: string;
26
26
  figmaProps?: Record<string, unknown>;
@@ -31,17 +31,27 @@ export interface CompiledContractOutput {
31
31
  guidelines?: string[];
32
32
  accessibility?: string[];
33
33
  };
34
- props: Record<string, {
35
- type: string;
36
- description: string;
37
- default?: unknown;
38
- required?: boolean;
39
- values?: string[];
40
- constraints?: string[];
41
- }>;
34
+ props: Record<
35
+ string,
36
+ {
37
+ type: string;
38
+ description: string;
39
+ default?: unknown;
40
+ required?: boolean;
41
+ values?: string[];
42
+ constraints?: string[];
43
+ }
44
+ >;
42
45
  relations?: Array<{
43
46
  component: string;
44
- relationship: 'alternative' | 'parent' | 'child' | 'sibling' | 'composition' | 'complementary' | 'used-by';
47
+ relationship:
48
+ | "alternative"
49
+ | "parent"
50
+ | "child"
51
+ | "sibling"
52
+ | "composition"
53
+ | "complementary"
54
+ | "used-by";
45
55
  note: string;
46
56
  }>;
47
57
  variants: Array<{
@@ -55,19 +65,32 @@ export interface CompiledContractOutput {
55
65
  scenarioTags?: string[];
56
66
  a11yRules?: string[];
57
67
  bans?: Array<{ pattern: string; message: string }>;
58
- compoundChildren?: Record<string, {
59
- required?: boolean;
60
- accepts?: string[];
61
- description?: string;
62
- }>;
68
+ compoundChildren?: Record<
69
+ string,
70
+ {
71
+ required?: boolean;
72
+ accepts?: string[];
73
+ description?: string;
74
+ }
75
+ >;
63
76
  canonicalUsage?: string[];
64
77
  performanceBudget?: number;
65
78
  };
79
+ /** Per-component preview configuration */
80
+ preview?: {
81
+ setupModule?: string;
82
+ wrapperModule?: string;
83
+ wrapperExport?: string;
84
+ css?: string[];
85
+ theme?: "light" | "dark";
86
+ };
87
+ /** Design-token references used by this component */
88
+ tokens?: string[];
66
89
  /** Framework hint from contract, used by extractor adapter selection */
67
90
  framework?: string;
68
91
  /** AI metadata for compound component generation */
69
92
  ai?: {
70
- compositionPattern?: 'compound' | 'simple' | 'controlled' | 'wrapper';
93
+ compositionPattern?: "compound" | "simple" | "controlled" | "wrapper";
71
94
  subComponents?: string[];
72
95
  requiredChildren?: string[];
73
96
  commonPatterns?: string[];
@@ -92,16 +115,13 @@ export interface CompiledContractOutput {
92
115
  * Check if a file path is a component contract file.
93
116
  */
94
117
  export function isContractFile(filePath: string): boolean {
95
- return filePath.endsWith('.contract.json');
118
+ return filePath.endsWith(".contract.json");
96
119
  }
97
120
 
98
121
  /**
99
122
  * Parse a component contract JSON file into CompiledFragment-compatible output.
100
123
  */
101
- export function parseComponentContract(
102
- content: string,
103
- filePath: string,
104
- ): CompiledContractOutput {
124
+ export function parseComponentContract(content: string, filePath: string): CompiledContractOutput {
105
125
  const raw = JSON.parse(content);
106
126
  const validated: ComponentContract = componentContractSchema.parse(raw);
107
127
 
@@ -126,13 +146,17 @@ export function parseComponentContract(
126
146
  code: ex.code,
127
147
  args: ex.args,
128
148
  })),
129
- contract: validated.contract ? {
130
- ...validated.contract,
131
- // Merge top-level propsSummary into contract.propsSummary if not already set
132
- propsSummary: validated.contract.propsSummary ?? validated.propsSummary,
133
- } : {
134
- propsSummary: validated.propsSummary,
135
- },
149
+ contract: validated.contract
150
+ ? {
151
+ ...validated.contract,
152
+ // Merge top-level propsSummary into contract.propsSummary if not already set
153
+ propsSummary: validated.contract.propsSummary ?? validated.propsSummary,
154
+ }
155
+ : {
156
+ propsSummary: validated.propsSummary,
157
+ },
158
+ preview: validated.preview,
159
+ tokens: validated.tokens,
136
160
  framework: validated.framework,
137
161
  ai: validated.ai,
138
162
  propsSummary: validated.propsSummary,