@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
@@ -1,6 +1,6 @@
1
- import { describe, it, expect } from 'vitest';
2
- import { parseComponentContract, isContractFile } from '../contract-parser.js';
3
- import { componentContractSchema } from '../component-contract.js';
1
+ import { describe, it, expect } from "vitest";
2
+ import { parseComponentContract, isContractFile } from "../contract-parser.js";
3
+ import { componentContractSchema } from "../component-contract.js";
4
4
 
5
5
  // ---------------------------------------------------------------------------
6
6
  // Helpers
@@ -8,42 +8,42 @@ import { componentContractSchema } from '../component-contract.js';
8
8
 
9
9
  function makeContract(overrides: Record<string, unknown> = {}): string {
10
10
  return JSON.stringify({
11
- $schema: 'https://usefragments.com/schemas/contract.v1.json',
12
- name: 'Button',
13
- description: 'A clickable button component',
14
- category: 'actions',
15
- sourcePath: 'src/components/Button/index.tsx',
16
- exportName: 'Button',
11
+ $schema: "https://usefragments.com/schemas/contract.v1.json",
12
+ name: "Button",
13
+ description: "A clickable button component",
14
+ category: "actions",
15
+ sourcePath: "src/components/Button/index.tsx",
16
+ exportName: "Button",
17
17
  propsSummary: [
18
- 'variant: primary|secondary|ghost (default: primary)',
19
- 'size: sm|md|lg (default: md)',
20
- 'disabled: boolean (default: false)',
18
+ "variant: primary|secondary|ghost (default: primary)",
19
+ "size: sm|md|lg (default: md)",
20
+ "disabled: boolean (default: false)",
21
21
  ],
22
22
  props: {
23
23
  variant: {
24
- type: 'enum',
25
- values: ['primary', 'secondary', 'ghost'],
26
- default: 'primary',
27
- description: 'Visual style variant',
24
+ type: "enum",
25
+ values: ["primary", "secondary", "ghost"],
26
+ default: "primary",
27
+ description: "Visual style variant",
28
28
  },
29
29
  size: {
30
- type: 'enum',
31
- values: ['sm', 'md', 'lg'],
32
- default: 'md',
33
- description: 'Button size',
30
+ type: "enum",
31
+ values: ["sm", "md", "lg"],
32
+ default: "md",
33
+ description: "Button size",
34
34
  },
35
35
  disabled: {
36
- type: 'boolean',
36
+ type: "boolean",
37
37
  default: false,
38
- description: 'Whether the button is disabled',
38
+ description: "Whether the button is disabled",
39
39
  },
40
40
  },
41
41
  usage: {
42
- when: ['User needs to trigger an action'],
43
- whenNot: ['Navigation — use Link instead'],
42
+ when: ["User needs to trigger an action"],
43
+ whenNot: ["Navigation — use Link instead"],
44
44
  },
45
45
  provenance: {
46
- source: 'manual',
46
+ source: "manual",
47
47
  verified: false,
48
48
  },
49
49
  ...overrides,
@@ -54,17 +54,17 @@ function makeContract(overrides: Record<string, unknown> = {}): string {
54
54
  // isContractFile
55
55
  // ---------------------------------------------------------------------------
56
56
 
57
- describe('isContractFile', () => {
58
- it('detects .contract.json extension', () => {
59
- expect(isContractFile('Button.contract.json')).toBe(true);
60
- expect(isContractFile('src/components/Card.contract.json')).toBe(true);
57
+ describe("isContractFile", () => {
58
+ it("detects .contract.json extension", () => {
59
+ expect(isContractFile("Button.contract.json")).toBe(true);
60
+ expect(isContractFile("src/components/Card.contract.json")).toBe(true);
61
61
  });
62
62
 
63
- it('rejects non-contract files', () => {
64
- expect(isContractFile('Button.fragment.tsx')).toBe(false);
65
- expect(isContractFile('contract.json')).toBe(false);
66
- expect(isContractFile('button.json')).toBe(false);
67
- expect(isContractFile('button.contract.ts')).toBe(false);
63
+ it("rejects non-contract files", () => {
64
+ expect(isContractFile("Button.fragment.tsx")).toBe(false);
65
+ expect(isContractFile("contract.json")).toBe(false);
66
+ expect(isContractFile("button.json")).toBe(false);
67
+ expect(isContractFile("button.contract.ts")).toBe(false);
68
68
  });
69
69
  });
70
70
 
@@ -72,311 +72,352 @@ describe('isContractFile', () => {
72
72
  // Schema validation
73
73
  // ---------------------------------------------------------------------------
74
74
 
75
- describe('componentContractSchema', () => {
75
+ describe("componentContractSchema", () => {
76
76
  const minimalValid = {
77
- $schema: 'https://usefragments.com/schemas/contract.v1.json',
78
- name: 'Button',
79
- description: 'A button',
80
- category: 'actions',
81
- sourcePath: 'src/Button.tsx',
82
- exportName: 'Button',
77
+ $schema: "https://usefragments.com/schemas/contract.v1.json",
78
+ name: "Button",
79
+ description: "A button",
80
+ category: "actions",
81
+ sourcePath: "src/Button.tsx",
82
+ exportName: "Button",
83
83
  propsSummary: [],
84
84
  props: {},
85
- usage: { when: ['click'], whenNot: ['nav'] },
86
- provenance: { source: 'manual', verified: false },
85
+ usage: { when: ["click"], whenNot: ["nav"] },
86
+ provenance: { source: "manual", verified: false },
87
87
  };
88
88
 
89
- it('validates a minimal contract', () => {
89
+ it("validates a minimal contract", () => {
90
90
  const result = componentContractSchema.safeParse(minimalValid);
91
91
  expect(result.success).toBe(true);
92
92
  });
93
93
 
94
- it('rejects contract missing required fields', () => {
95
- const data = { name: 'Button' };
94
+ it("rejects contract missing required fields", () => {
95
+ const data = { name: "Button" };
96
96
  const result = componentContractSchema.safeParse(data);
97
97
  expect(result.success).toBe(false);
98
98
  });
99
99
 
100
- it('rejects contract missing $schema', () => {
100
+ it("rejects contract missing $schema", () => {
101
101
  const { $schema: _, ...noSchema } = minimalValid;
102
102
  const result = componentContractSchema.safeParse(noSchema);
103
103
  expect(result.success).toBe(false);
104
104
  });
105
105
 
106
- it('rejects contract missing sourcePath', () => {
106
+ it("rejects contract missing sourcePath", () => {
107
107
  const { sourcePath: _, ...noSource } = minimalValid;
108
108
  const result = componentContractSchema.safeParse(noSource);
109
109
  expect(result.success).toBe(false);
110
110
  });
111
111
 
112
- it('rejects contract missing exportName', () => {
112
+ it("rejects contract missing exportName", () => {
113
113
  const { exportName: _, ...noExport } = minimalValid;
114
114
  const result = componentContractSchema.safeParse(noExport);
115
115
  expect(result.success).toBe(false);
116
116
  });
117
117
 
118
- it('rejects contract missing provenance', () => {
118
+ it("rejects contract missing provenance", () => {
119
119
  const { provenance: _, ...noProv } = minimalValid;
120
120
  const result = componentContractSchema.safeParse(noProv);
121
121
  expect(result.success).toBe(false);
122
122
  });
123
123
 
124
- it('rejects contract with invalid status', () => {
125
- const result = componentContractSchema.safeParse({ ...minimalValid, status: 'invalid-status' });
124
+ it("rejects contract with invalid status", () => {
125
+ const result = componentContractSchema.safeParse({ ...minimalValid, status: "invalid-status" });
126
126
  expect(result.success).toBe(false);
127
127
  });
128
128
 
129
- it('accepts all valid status values', () => {
130
- for (const status of ['stable', 'beta', 'deprecated', 'experimental']) {
129
+ it("accepts all valid status values", () => {
130
+ for (const status of ["stable", "beta", "deprecated", "experimental"]) {
131
131
  const result = componentContractSchema.safeParse({ ...minimalValid, status });
132
132
  expect(result.success).toBe(true);
133
133
  }
134
134
  });
135
135
 
136
- it('accepts all valid framework values', () => {
137
- for (const framework of ['react', 'vue', 'svelte', 'web-components', 'angular']) {
136
+ it("accepts all valid framework values", () => {
137
+ for (const framework of ["react", "vue", "svelte", "web-components", "angular"]) {
138
138
  const result = componentContractSchema.safeParse({ ...minimalValid, framework });
139
139
  expect(result.success).toBe(true);
140
140
  }
141
141
  });
142
142
 
143
- it('rejects invalid relationship type', () => {
143
+ it("rejects invalid relationship type", () => {
144
144
  const result = componentContractSchema.safeParse({
145
145
  ...minimalValid,
146
- relations: [{ component: 'Link', relationship: 'replaces', note: 'test' }],
146
+ relations: [{ component: "Link", relationship: "replaces", note: "test" }],
147
147
  });
148
148
  expect(result.success).toBe(false);
149
149
  });
150
150
 
151
- it('accepts all valid relationship types', () => {
152
- for (const relationship of ['alternative', 'parent', 'child', 'sibling']) {
151
+ it("accepts all valid relationship types", () => {
152
+ for (const relationship of ["alternative", "parent", "child", "sibling"]) {
153
153
  const result = componentContractSchema.safeParse({
154
154
  ...minimalValid,
155
- relations: [{ component: 'Link', relationship, note: 'test' }],
155
+ relations: [{ component: "Link", relationship, note: "test" }],
156
156
  });
157
157
  expect(result.success).toBe(true);
158
158
  }
159
159
  });
160
160
 
161
- it('validates ai metadata', () => {
161
+ it("validates ai metadata", () => {
162
162
  const result = componentContractSchema.safeParse({
163
163
  ...minimalValid,
164
164
  ai: {
165
- compositionPattern: 'compound',
166
- subComponents: ['Header', 'Body', 'Footer'],
167
- requiredChildren: ['Body'],
168
- commonPatterns: ['<Card><Card.Header /><Card.Body /></Card>'],
165
+ compositionPattern: "compound",
166
+ subComponents: ["Header", "Body", "Footer"],
167
+ requiredChildren: ["Body"],
168
+ commonPatterns: ["<Card><Card.Header /><Card.Body /></Card>"],
169
169
  },
170
170
  });
171
171
  expect(result.success).toBe(true);
172
172
  });
173
173
 
174
- it('validates preview config', () => {
174
+ it("validates preview config", () => {
175
175
  const result = componentContractSchema.safeParse({
176
176
  ...minimalValid,
177
177
  preview: {
178
- setupModule: './preview-setup.ts',
179
- wrapperModule: './ThemeProvider.tsx',
180
- wrapperExport: 'ThemeProvider',
181
- css: ['./globals.css'],
182
- theme: 'dark',
178
+ setupModule: "./preview-setup.ts",
179
+ wrapperModule: "./ThemeProvider.tsx",
180
+ wrapperExport: "ThemeProvider",
181
+ css: ["./globals.css"],
182
+ theme: "dark",
183
183
  },
184
184
  });
185
185
  expect(result.success).toBe(true);
186
186
  });
187
187
 
188
- it('validates provenance with all fields', () => {
188
+ it("validates provenance with all fields", () => {
189
189
  const result = componentContractSchema.safeParse({
190
190
  ...minimalValid,
191
191
  provenance: {
192
- source: 'extracted',
192
+ source: "extracted",
193
193
  verified: true,
194
- frameworkSupport: 'native',
195
- sourceHash: 'abc123',
196
- extractedAt: '2026-03-13T00:00:00Z',
194
+ frameworkSupport: "native",
195
+ sourceHash: "abc123",
196
+ extractedAt: "2026-03-13T00:00:00Z",
197
197
  },
198
198
  });
199
199
  expect(result.success).toBe(true);
200
200
  });
201
+
202
+ it("accepts only explicit positive performance budgets (A08-02)", () => {
203
+ for (const performanceBudget of [0, -1, Number.POSITIVE_INFINITY]) {
204
+ const result = componentContractSchema.safeParse({
205
+ ...minimalValid,
206
+ contract: { performanceBudget },
207
+ });
208
+ expect(result.success).toBe(false);
209
+ }
210
+
211
+ expect(
212
+ componentContractSchema.safeParse({
213
+ ...minimalValid,
214
+ contract: { performanceBudget: 1 },
215
+ }).success
216
+ ).toBe(true);
217
+ expect(componentContractSchema.safeParse(minimalValid).success).toBe(true);
218
+ });
201
219
  });
202
220
 
203
221
  // ---------------------------------------------------------------------------
204
222
  // parseComponentContract
205
223
  // ---------------------------------------------------------------------------
206
224
 
207
- describe('parseComponentContract', () => {
208
- describe('basic parsing', () => {
209
- it('parses a minimal contract', () => {
225
+ describe("parseComponentContract", () => {
226
+ describe("basic parsing", () => {
227
+ it("parses a minimal contract", () => {
210
228
  const content = makeContract();
211
- const result = parseComponentContract(content, 'Button.contract.json');
229
+ const result = parseComponentContract(content, "Button.contract.json");
212
230
 
213
- expect(result.meta.name).toBe('Button');
214
- expect(result.meta.description).toBe('A clickable button component');
215
- expect(result.meta.category).toBe('actions');
216
- expect(result.filePath).toBe('Button.contract.json');
231
+ expect(result.meta.name).toBe("Button");
232
+ expect(result.meta.description).toBe("A clickable button component");
233
+ expect(result.meta.category).toBe("actions");
234
+ expect(result.filePath).toBe("Button.contract.json");
217
235
  });
218
236
 
219
- it('maps props correctly', () => {
237
+ it("maps props correctly", () => {
220
238
  const content = makeContract();
221
- const result = parseComponentContract(content, 'Button.contract.json');
239
+ const result = parseComponentContract(content, "Button.contract.json");
222
240
 
223
241
  expect(Object.keys(result.props)).toHaveLength(3);
224
- expect(result.props.variant.type).toBe('enum');
225
- expect(result.props.variant.values).toEqual(['primary', 'secondary', 'ghost']);
226
- expect(result.props.variant.default).toBe('primary');
227
- expect(result.props.variant.description).toBe('Visual style variant');
242
+ expect(result.props.variant.type).toBe("enum");
243
+ expect(result.props.variant.values).toEqual(["primary", "secondary", "ghost"]);
244
+ expect(result.props.variant.default).toBe("primary");
245
+ expect(result.props.variant.description).toBe("Visual style variant");
228
246
  });
229
247
 
230
- it('maps usage correctly', () => {
248
+ it("maps usage correctly", () => {
231
249
  const content = makeContract();
232
- const result = parseComponentContract(content, 'Button.contract.json');
250
+ const result = parseComponentContract(content, "Button.contract.json");
233
251
 
234
- expect(result.usage.when).toEqual(['User needs to trigger an action']);
235
- expect(result.usage.whenNot).toEqual(['Navigation — use Link instead']);
252
+ expect(result.usage.when).toEqual(["User needs to trigger an action"]);
253
+ expect(result.usage.whenNot).toEqual(["Navigation — use Link instead"]);
236
254
  });
237
255
  });
238
256
 
239
- describe('optional fields', () => {
240
- it('handles tags', () => {
241
- const content = makeContract({ tags: ['interactive', 'form'] });
242
- const result = parseComponentContract(content, 'Button.contract.json');
257
+ describe("optional fields", () => {
258
+ it("handles tags", () => {
259
+ const content = makeContract({ tags: ["interactive", "form"] });
260
+ const result = parseComponentContract(content, "Button.contract.json");
243
261
 
244
- expect(result.meta.tags).toEqual(['interactive', 'form']);
262
+ expect(result.meta.tags).toEqual(["interactive", "form"]);
245
263
  });
246
264
 
247
- it('handles status', () => {
248
- const content = makeContract({ status: 'stable' });
249
- const result = parseComponentContract(content, 'Button.contract.json');
265
+ it("handles status", () => {
266
+ const content = makeContract({ status: "stable" });
267
+ const result = parseComponentContract(content, "Button.contract.json");
250
268
 
251
- expect(result.meta.status).toBe('stable');
269
+ expect(result.meta.status).toBe("stable");
252
270
  });
253
271
 
254
- it('handles guidelines and accessibility in usage', () => {
272
+ it("handles guidelines and accessibility in usage", () => {
255
273
  const content = makeContract({
256
274
  usage: {
257
- when: ['Action needed'],
258
- whenNot: ['No action'],
259
- guidelines: ['Always provide a label'],
275
+ when: ["Action needed"],
276
+ whenNot: ["No action"],
277
+ guidelines: ["Always provide a label"],
260
278
  accessibility: ['Must have role="button"'],
261
279
  },
262
280
  });
263
- const result = parseComponentContract(content, 'Button.contract.json');
281
+ const result = parseComponentContract(content, "Button.contract.json");
264
282
 
265
- expect(result.usage.guidelines).toEqual(['Always provide a label']);
283
+ expect(result.usage.guidelines).toEqual(["Always provide a label"]);
266
284
  expect(result.usage.accessibility).toEqual(['Must have role="button"']);
267
285
  });
268
286
 
269
- it('handles contract metadata', () => {
287
+ it("preserves every contract field plus preview and tokens (A08-01)", () => {
270
288
  const content = makeContract({
271
289
  contract: {
272
- propsSummary: ['variant: primary|secondary|ghost (default: primary)'],
273
- scenarioTags: ['form', 'action'],
274
- a11yRules: ['no-empty-buttons'],
275
- bans: [{ pattern: 'onClick without type', message: 'Always specify button type' }],
290
+ propsSummary: ["variant: primary|secondary|ghost (default: primary)"],
291
+ scenarioTags: ["form", "action"],
292
+ a11yRules: ["no-empty-buttons"],
293
+ bans: [{ pattern: "onClick without type", message: "Always specify button type" }],
294
+ performanceBudget: 1024,
295
+ compoundChildren: {
296
+ Icon: { required: true, accepts: ["svg"], description: "Leading icon" },
297
+ },
298
+ canonicalUsage: ['<Button variant="primary" />'],
299
+ },
300
+ preview: {
301
+ setupModule: "./preview-setup.ts",
302
+ wrapperModule: "./ThemeProvider.tsx",
303
+ wrapperExport: "ThemeProvider",
304
+ css: ["./globals.css"],
305
+ theme: "dark",
276
306
  },
307
+ tokens: ["--ds-color-primary", "--ds-space-4"],
277
308
  });
278
- const result = parseComponentContract(content, 'Button.contract.json');
279
-
280
- expect(result.contract).toBeDefined();
281
- expect(result.contract!.propsSummary).toHaveLength(1);
282
- expect(result.contract!.scenarioTags).toEqual(['form', 'action']);
283
- expect(result.contract!.bans).toHaveLength(1);
309
+ const result = parseComponentContract(content, "Button.contract.json");
310
+
311
+ expect(result.contract).toEqual({
312
+ propsSummary: ["variant: primary|secondary|ghost (default: primary)"],
313
+ scenarioTags: ["form", "action"],
314
+ a11yRules: ["no-empty-buttons"],
315
+ bans: [{ pattern: "onClick without type", message: "Always specify button type" }],
316
+ performanceBudget: 1024,
317
+ compoundChildren: {
318
+ Icon: { required: true, accepts: ["svg"], description: "Leading icon" },
319
+ },
320
+ canonicalUsage: ['<Button variant="primary" />'],
321
+ });
322
+ expect(result.preview).toEqual({
323
+ setupModule: "./preview-setup.ts",
324
+ wrapperModule: "./ThemeProvider.tsx",
325
+ wrapperExport: "ThemeProvider",
326
+ css: ["./globals.css"],
327
+ theme: "dark",
328
+ });
329
+ expect(result.tokens).toEqual(["--ds-color-primary", "--ds-space-4"]);
284
330
  });
285
331
 
286
- it('handles tokens list', () => {
287
- const content = makeContract({
288
- tokens: ['--ds-color-primary', '--ds-space-4'],
289
- });
332
+ it("leaves an absent performance budget absent (A08-02)", () => {
333
+ const result = parseComponentContract(makeContract(), "Button.contract.json");
290
334
 
291
- // tokens is part of the contract but not mapped to CompiledFragment output
292
- // Just verify it parses without error
293
- const result = parseComponentContract(content, 'Button.contract.json');
294
- expect(result.meta.name).toBe('Button');
335
+ expect(result.contract).not.toHaveProperty("performanceBudget");
295
336
  });
296
337
  });
297
338
 
298
- describe('examples → variants mapping', () => {
299
- it('maps examples to variants', () => {
339
+ describe("examples → variants mapping", () => {
340
+ it("maps examples to variants", () => {
300
341
  const content = makeContract({
301
342
  examples: [
302
343
  {
303
- name: 'Primary',
304
- description: 'Primary button style',
344
+ name: "Primary",
345
+ description: "Primary button style",
305
346
  code: '<Button variant="primary">Click me</Button>',
306
347
  },
307
348
  {
308
- name: 'With icon',
309
- description: 'Button with leading icon',
349
+ name: "With icon",
350
+ description: "Button with leading icon",
310
351
  code: '<Button><Icon name="plus" /> Add</Button>',
311
- args: { variant: 'secondary' },
352
+ args: { variant: "secondary" },
312
353
  },
313
354
  ],
314
355
  });
315
- const result = parseComponentContract(content, 'Button.contract.json');
356
+ const result = parseComponentContract(content, "Button.contract.json");
316
357
 
317
358
  expect(result.variants).toHaveLength(2);
318
- expect(result.variants[0].name).toBe('Primary');
359
+ expect(result.variants[0].name).toBe("Primary");
319
360
  expect(result.variants[0].code).toBe('<Button variant="primary">Click me</Button>');
320
- expect(result.variants[1].args).toEqual({ variant: 'secondary' });
361
+ expect(result.variants[1].args).toEqual({ variant: "secondary" });
321
362
  });
322
363
 
323
- it('returns empty variants when no examples', () => {
364
+ it("returns empty variants when no examples", () => {
324
365
  const content = makeContract();
325
- const result = parseComponentContract(content, 'Button.contract.json');
366
+ const result = parseComponentContract(content, "Button.contract.json");
326
367
 
327
368
  expect(result.variants).toEqual([]);
328
369
  });
329
370
  });
330
371
 
331
- describe('relations mapping', () => {
332
- it('maps all relationship types', () => {
372
+ describe("relations mapping", () => {
373
+ it("maps all relationship types", () => {
333
374
  const content = makeContract({
334
375
  relations: [
335
- { component: 'Link', relationship: 'alternative', note: 'For navigation' },
336
- { component: 'Form', relationship: 'parent', note: 'Used inside forms' },
337
- { component: 'Icon', relationship: 'child', note: 'Can contain icons' },
338
- { component: 'Input', relationship: 'sibling', note: 'Often paired' },
376
+ { component: "Link", relationship: "alternative", note: "For navigation" },
377
+ { component: "Form", relationship: "parent", note: "Used inside forms" },
378
+ { component: "Icon", relationship: "child", note: "Can contain icons" },
379
+ { component: "Input", relationship: "sibling", note: "Often paired" },
339
380
  ],
340
381
  });
341
- const result = parseComponentContract(content, 'Button.contract.json');
382
+ const result = parseComponentContract(content, "Button.contract.json");
342
383
 
343
384
  expect(result.relations).toHaveLength(4);
344
- expect(result.relations![0].relationship).toBe('alternative');
345
- expect(result.relations![1].relationship).toBe('parent');
346
- expect(result.relations![2].relationship).toBe('child');
347
- expect(result.relations![3].relationship).toBe('sibling');
385
+ expect(result.relations![0].relationship).toBe("alternative");
386
+ expect(result.relations![1].relationship).toBe("parent");
387
+ expect(result.relations![2].relationship).toBe("child");
388
+ expect(result.relations![3].relationship).toBe("sibling");
348
389
  });
349
390
  });
350
391
 
351
- describe('figma mappings', () => {
352
- it('maps figma nodeUrl to meta.figma', () => {
392
+ describe("figma mappings", () => {
393
+ it("maps figma nodeUrl to meta.figma", () => {
353
394
  const content = makeContract({
354
395
  figma: {
355
- nodeUrl: 'https://www.figma.com/file/abc123/Design?node-id=1-2',
396
+ nodeUrl: "https://www.figma.com/file/abc123/Design?node-id=1-2",
356
397
  },
357
398
  });
358
- const result = parseComponentContract(content, 'Button.contract.json');
399
+ const result = parseComponentContract(content, "Button.contract.json");
359
400
 
360
- expect(result.meta.figma).toBe('https://www.figma.com/file/abc123/Design?node-id=1-2');
401
+ expect(result.meta.figma).toBe("https://www.figma.com/file/abc123/Design?node-id=1-2");
361
402
  });
362
403
 
363
- it('maps figma propMappings to meta.figmaProps', () => {
404
+ it("maps figma propMappings to meta.figmaProps", () => {
364
405
  const content = makeContract({
365
406
  figma: {
366
407
  propMappings: {
367
408
  variant: {
368
- type: 'enum',
369
- figmaProperty: 'Style',
370
- values: { primary: 'Primary', secondary: 'Secondary' },
409
+ type: "enum",
410
+ figmaProperty: "Style",
411
+ values: { primary: "Primary", secondary: "Secondary" },
371
412
  },
372
413
  disabled: {
373
- type: 'boolean',
374
- figmaProperty: 'Disabled',
414
+ type: "boolean",
415
+ figmaProperty: "Disabled",
375
416
  },
376
417
  },
377
418
  },
378
419
  });
379
- const result = parseComponentContract(content, 'Button.contract.json');
420
+ const result = parseComponentContract(content, "Button.contract.json");
380
421
 
381
422
  expect(result.meta.figmaProps).toBeDefined();
382
423
  const mappings = result.meta.figmaProps as Record<string, unknown>;
@@ -384,231 +425,231 @@ describe('parseComponentContract', () => {
384
425
  });
385
426
  });
386
427
 
387
- describe('prop constraints', () => {
388
- it('preserves prop constraints', () => {
428
+ describe("prop constraints", () => {
429
+ it("preserves prop constraints", () => {
389
430
  const content = makeContract({
390
431
  props: {
391
432
  maxLength: {
392
- type: 'number',
393
- description: 'Maximum character length',
394
- constraints: ['Must be positive', 'Max 1000'],
433
+ type: "number",
434
+ description: "Maximum character length",
435
+ constraints: ["Must be positive", "Max 1000"],
395
436
  },
396
437
  },
397
438
  });
398
- const result = parseComponentContract(content, 'Button.contract.json');
439
+ const result = parseComponentContract(content, "Button.contract.json");
399
440
 
400
- expect(result.props.maxLength.constraints).toEqual(['Must be positive', 'Max 1000']);
441
+ expect(result.props.maxLength.constraints).toEqual(["Must be positive", "Max 1000"]);
401
442
  });
402
443
 
403
- it('preserves required flag', () => {
444
+ it("preserves required flag", () => {
404
445
  const content = makeContract({
405
446
  props: {
406
447
  label: {
407
- type: 'string',
408
- description: 'Button label',
448
+ type: "string",
449
+ description: "Button label",
409
450
  required: true,
410
451
  },
411
452
  },
412
453
  });
413
- const result = parseComponentContract(content, 'Button.contract.json');
454
+ const result = parseComponentContract(content, "Button.contract.json");
414
455
 
415
456
  expect(result.props.label.required).toBe(true);
416
457
  });
417
458
  });
418
459
 
419
- describe('error handling', () => {
420
- it('throws on invalid JSON', () => {
421
- expect(() => parseComponentContract('not json', 'test.contract.json')).toThrow();
460
+ describe("error handling", () => {
461
+ it("throws on invalid JSON", () => {
462
+ expect(() => parseComponentContract("not json", "test.contract.json")).toThrow();
422
463
  });
423
464
 
424
- it('throws on missing required fields', () => {
425
- const content = JSON.stringify({ name: 'Button' });
426
- expect(() => parseComponentContract(content, 'test.contract.json')).toThrow();
465
+ it("throws on missing required fields", () => {
466
+ const content = JSON.stringify({ name: "Button" });
467
+ expect(() => parseComponentContract(content, "test.contract.json")).toThrow();
427
468
  });
428
469
 
429
- it('throws on invalid prop type structure', () => {
470
+ it("throws on invalid prop type structure", () => {
430
471
  const content = JSON.stringify({
431
- name: 'Button',
432
- description: 'A button',
433
- category: 'actions',
472
+ name: "Button",
473
+ description: "A button",
474
+ category: "actions",
434
475
  props: {
435
- variant: 'string', // Should be an object, not a string
476
+ variant: "string", // Should be an object, not a string
436
477
  },
437
478
  usage: { when: [], whenNot: [] },
438
479
  });
439
- expect(() => parseComponentContract(content, 'test.contract.json')).toThrow();
480
+ expect(() => parseComponentContract(content, "test.contract.json")).toThrow();
440
481
  });
441
482
  });
442
483
 
443
- describe('CompiledFragment compatibility', () => {
444
- it('output has all required CompiledFragment fields', () => {
484
+ describe("CompiledFragment compatibility", () => {
485
+ it("output has all required CompiledFragment fields", () => {
445
486
  const content = makeContract();
446
- const result = parseComponentContract(content, 'Button.contract.json');
487
+ const result = parseComponentContract(content, "Button.contract.json");
447
488
 
448
489
  // Required fields
449
- expect(result).toHaveProperty('filePath');
450
- expect(result).toHaveProperty('meta');
451
- expect(result).toHaveProperty('usage');
452
- expect(result).toHaveProperty('props');
453
- expect(result).toHaveProperty('variants');
490
+ expect(result).toHaveProperty("filePath");
491
+ expect(result).toHaveProperty("meta");
492
+ expect(result).toHaveProperty("usage");
493
+ expect(result).toHaveProperty("props");
494
+ expect(result).toHaveProperty("variants");
454
495
 
455
496
  // Meta required fields
456
- expect(result.meta).toHaveProperty('name');
457
- expect(result.meta).toHaveProperty('description');
458
- expect(result.meta).toHaveProperty('category');
497
+ expect(result.meta).toHaveProperty("name");
498
+ expect(result.meta).toHaveProperty("description");
499
+ expect(result.meta).toHaveProperty("category");
459
500
 
460
501
  // Usage required fields
461
- expect(result.usage).toHaveProperty('when');
462
- expect(result.usage).toHaveProperty('whenNot');
502
+ expect(result.usage).toHaveProperty("when");
503
+ expect(result.usage).toHaveProperty("whenNot");
463
504
  });
464
505
  });
465
506
 
466
- describe('real-world contracts', () => {
467
- it('parses a Vue component contract', () => {
507
+ describe("real-world contracts", () => {
508
+ it("parses a Vue component contract", () => {
468
509
  const content = JSON.stringify({
469
- $schema: 'https://usefragments.com/schemas/contract.v1.json',
470
- name: 'VButton',
471
- description: 'Vuetify button component',
472
- category: 'actions',
473
- framework: 'vue',
474
- status: 'stable',
475
- tags: ['vuetify', 'material'],
476
- sourcePath: 'src/VButton.vue',
477
- exportName: 'VButton',
478
- propsSummary: ['color: string', 'outlined: boolean', 'block: boolean'],
510
+ $schema: "https://usefragments.com/schemas/contract.v1.json",
511
+ name: "VButton",
512
+ description: "Vuetify button component",
513
+ category: "actions",
514
+ framework: "vue",
515
+ status: "stable",
516
+ tags: ["vuetify", "material"],
517
+ sourcePath: "src/VButton.vue",
518
+ exportName: "VButton",
519
+ propsSummary: ["color: string", "outlined: boolean", "block: boolean"],
479
520
  props: {
480
- color: { type: 'string', description: 'Button color', default: 'primary' },
481
- outlined: { type: 'boolean', description: 'Outlined style', default: false },
482
- block: { type: 'boolean', description: 'Full width', default: false },
521
+ color: { type: "string", description: "Button color", default: "primary" },
522
+ outlined: { type: "boolean", description: "Outlined style", default: false },
523
+ block: { type: "boolean", description: "Full width", default: false },
483
524
  },
484
525
  usage: {
485
- when: ['User action needed'],
486
- whenNot: ['Navigation — use v-link'],
526
+ when: ["User action needed"],
527
+ whenNot: ["Navigation — use v-link"],
487
528
  },
488
529
  examples: [
489
530
  {
490
- name: 'Default',
491
- description: 'Default button',
531
+ name: "Default",
532
+ description: "Default button",
492
533
  code: '<v-btn color="primary">Click</v-btn>',
493
534
  },
494
535
  ],
495
- provenance: { source: 'manual', verified: false },
536
+ provenance: { source: "manual", verified: false },
496
537
  });
497
538
 
498
- const result = parseComponentContract(content, 'VButton.contract.json');
539
+ const result = parseComponentContract(content, "VButton.contract.json");
499
540
 
500
- expect(result.meta.name).toBe('VButton');
501
- expect(result.meta.tags).toContain('vuetify');
502
- expect(result.framework).toBe('vue');
541
+ expect(result.meta.name).toBe("VButton");
542
+ expect(result.meta.tags).toContain("vuetify");
543
+ expect(result.framework).toBe("vue");
503
544
  expect(result.variants).toHaveLength(1);
504
- expect(result.variants[0].code).toContain('<v-btn');
505
- expect(result.provenance?.source).toBe('manual');
545
+ expect(result.variants[0].code).toContain("<v-btn");
546
+ expect(result.provenance?.source).toBe("manual");
506
547
  });
507
548
 
508
- it('parses a Web Components contract', () => {
549
+ it("parses a Web Components contract", () => {
509
550
  const content = JSON.stringify({
510
- $schema: 'https://usefragments.com/schemas/contract.v1.json',
511
- name: 'MyButton',
512
- description: 'Custom element button',
513
- category: 'actions',
514
- framework: 'web-components',
515
- sourcePath: 'src/my-button.ts',
516
- exportName: 'MyButton',
517
- propsSummary: ['variant: primary|secondary'],
551
+ $schema: "https://usefragments.com/schemas/contract.v1.json",
552
+ name: "MyButton",
553
+ description: "Custom element button",
554
+ category: "actions",
555
+ framework: "web-components",
556
+ sourcePath: "src/my-button.ts",
557
+ exportName: "MyButton",
558
+ propsSummary: ["variant: primary|secondary"],
518
559
  props: {
519
560
  variant: {
520
- type: 'string',
521
- description: 'Visual variant',
522
- values: ['primary', 'secondary'],
561
+ type: "string",
562
+ description: "Visual variant",
563
+ values: ["primary", "secondary"],
523
564
  },
524
565
  },
525
566
  usage: {
526
- when: ['Need a custom button'],
527
- whenNot: ['Use native <button> when possible'],
567
+ when: ["Need a custom button"],
568
+ whenNot: ["Use native <button> when possible"],
528
569
  },
529
- provenance: { source: 'manual', verified: false },
570
+ provenance: { source: "manual", verified: false },
530
571
  });
531
572
 
532
- const result = parseComponentContract(content, 'MyButton.contract.json');
533
- expect(result.meta.name).toBe('MyButton');
534
- expect(result.framework).toBe('web-components');
573
+ const result = parseComponentContract(content, "MyButton.contract.json");
574
+ expect(result.meta.name).toBe("MyButton");
575
+ expect(result.framework).toBe("web-components");
535
576
  });
536
577
 
537
- it('parses a contract with many props', () => {
578
+ it("parses a contract with many props", () => {
538
579
  const props: Record<string, unknown> = {};
539
580
  const summaries: string[] = [];
540
581
  for (let i = 0; i < 50; i++) {
541
582
  props[`prop${i}`] = {
542
- type: 'string',
583
+ type: "string",
543
584
  description: `Property ${i}`,
544
585
  };
545
586
  summaries.push(`prop${i}: string`);
546
587
  }
547
588
 
548
589
  const content = JSON.stringify({
549
- $schema: 'https://usefragments.com/schemas/contract.v1.json',
550
- name: 'ComplexComponent',
551
- description: 'Component with many props',
552
- category: 'complex',
553
- sourcePath: 'src/Complex.tsx',
554
- exportName: 'ComplexComponent',
590
+ $schema: "https://usefragments.com/schemas/contract.v1.json",
591
+ name: "ComplexComponent",
592
+ description: "Component with many props",
593
+ category: "complex",
594
+ sourcePath: "src/Complex.tsx",
595
+ exportName: "ComplexComponent",
555
596
  propsSummary: summaries,
556
597
  props,
557
- usage: { when: ['Complex use case'], whenNot: [] },
558
- provenance: { source: 'manual', verified: false },
598
+ usage: { when: ["Complex use case"], whenNot: [] },
599
+ provenance: { source: "manual", verified: false },
559
600
  });
560
601
 
561
- const result = parseComponentContract(content, 'Complex.contract.json');
602
+ const result = parseComponentContract(content, "Complex.contract.json");
562
603
  expect(Object.keys(result.props)).toHaveLength(50);
563
604
  expect(result.propsSummary).toHaveLength(50);
564
605
  });
565
606
  });
566
607
 
567
- describe('new V2 fields', () => {
568
- it('maps propsSummary to top-level and contract', () => {
608
+ describe("new V2 fields", () => {
609
+ it("maps propsSummary to top-level and contract", () => {
569
610
  const content = makeContract();
570
- const result = parseComponentContract(content, 'Button.contract.json');
611
+ const result = parseComponentContract(content, "Button.contract.json");
571
612
 
572
613
  expect(result.propsSummary).toHaveLength(3);
573
614
  expect(result.contract?.propsSummary).toHaveLength(3);
574
615
  });
575
616
 
576
- it('maps provenance', () => {
617
+ it("maps provenance", () => {
577
618
  const content = makeContract();
578
- const result = parseComponentContract(content, 'Button.contract.json');
619
+ const result = parseComponentContract(content, "Button.contract.json");
579
620
 
580
- expect(result.provenance?.source).toBe('manual');
621
+ expect(result.provenance?.source).toBe("manual");
581
622
  expect(result.provenance?.verified).toBe(false);
582
623
  });
583
624
 
584
- it('maps sourcePath and exportName', () => {
625
+ it("maps sourcePath and exportName", () => {
585
626
  const content = makeContract();
586
- const result = parseComponentContract(content, 'Button.contract.json');
627
+ const result = parseComponentContract(content, "Button.contract.json");
587
628
 
588
- expect(result.sourcePath).toBe('src/components/Button/index.tsx');
589
- expect(result.exportName).toBe('Button');
629
+ expect(result.sourcePath).toBe("src/components/Button/index.tsx");
630
+ expect(result.exportName).toBe("Button");
590
631
  });
591
632
 
592
- it('maps ai metadata', () => {
633
+ it("maps ai metadata", () => {
593
634
  const content = makeContract({
594
635
  ai: {
595
- compositionPattern: 'compound',
596
- subComponents: ['Header', 'Body'],
597
- requiredChildren: ['Body'],
598
- commonPatterns: ['<Card><Card.Body>content</Card.Body></Card>'],
636
+ compositionPattern: "compound",
637
+ subComponents: ["Header", "Body"],
638
+ requiredChildren: ["Body"],
639
+ commonPatterns: ["<Card><Card.Body>content</Card.Body></Card>"],
599
640
  },
600
641
  });
601
- const result = parseComponentContract(content, 'Button.contract.json');
642
+ const result = parseComponentContract(content, "Button.contract.json");
602
643
 
603
- expect(result.ai?.compositionPattern).toBe('compound');
604
- expect(result.ai?.subComponents).toEqual(['Header', 'Body']);
644
+ expect(result.ai?.compositionPattern).toBe("compound");
645
+ expect(result.ai?.subComponents).toEqual(["Header", "Body"]);
605
646
  });
606
647
 
607
- it('maps framework', () => {
608
- const content = makeContract({ framework: 'react' });
609
- const result = parseComponentContract(content, 'Button.contract.json');
648
+ it("maps framework", () => {
649
+ const content = makeContract({ framework: "react" });
650
+ const result = parseComponentContract(content, "Button.contract.json");
610
651
 
611
- expect(result.framework).toBe('react');
652
+ expect(result.framework).toBe("react");
612
653
  });
613
654
  });
614
655
  });