@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,10 +1,6 @@
1
- import { describe, it, expect } from 'vitest';
2
- import { defineFragment, compileFragment, normalizeToV1 } from './defineFragment.js';
3
- import type {
4
- FragmentDefinition,
5
- FragmentDefinitionV2,
6
- FragmentVariant,
7
- } from './types.js';
1
+ import { afterEach, describe, it, expect, vi } from "vitest";
2
+ import { defineBlock, defineFragment, compileFragment, normalizeToV1 } from "./defineFragment.js";
3
+ import type { FragmentDefinition, FragmentDefinitionV2 } from "./types.js";
8
4
 
9
5
  // ---------------------------------------------------------------------------
10
6
  // Helpers — minimal component stubs
@@ -16,28 +12,30 @@ function StubComponent(_props: { children?: string }) {
16
12
 
17
13
  const renderStub = () => null;
18
14
 
15
+ afterEach(() => {
16
+ vi.unstubAllEnvs();
17
+ });
18
+
19
19
  function makeV1(): FragmentDefinition<{ children?: string }> {
20
20
  return {
21
21
  component: StubComponent,
22
- meta: { name: 'Stub', description: 'A stub', category: 'test', status: 'stable' },
22
+ meta: { name: "Stub", description: "A stub", category: "test", status: "stable" },
23
23
  usage: {
24
- when: ['Testing v1'],
25
- whenNot: ['Production'],
24
+ when: ["Testing v1"],
25
+ whenNot: ["Production"],
26
26
  },
27
27
  props: {
28
- children: { type: 'node', description: 'Content', required: false },
28
+ children: { type: "node", description: "Content", required: false },
29
29
  },
30
- variants: [
31
- { name: 'Default', description: 'Default variant', render: renderStub },
32
- ],
30
+ variants: [{ name: "Default", description: "Default variant", render: renderStub }],
33
31
  ai: {
34
- compositionPattern: 'simple',
32
+ compositionPattern: "simple",
35
33
  },
36
34
  contract: {
37
- propsSummary: ['children: node'],
35
+ propsSummary: ["children: node"],
38
36
  },
39
37
  _generated: {
40
- source: 'manual',
38
+ source: "manual",
41
39
  confidence: 1,
42
40
  },
43
41
  };
@@ -46,31 +44,29 @@ function makeV1(): FragmentDefinition<{ children?: string }> {
46
44
  function makeV2(): FragmentDefinitionV2<{ children?: string }> {
47
45
  return {
48
46
  component: StubComponent,
49
- meta: { name: 'Stub', description: 'A stub', category: 'test', status: 'stable' },
47
+ meta: { name: "Stub", description: "A stub", category: "test", status: "stable" },
50
48
  guidance: {
51
- when: ['Testing v2'],
52
- whenNot: ['Production'],
49
+ when: ["Testing v2"],
50
+ whenNot: ["Production"],
53
51
  },
54
52
  props: {
55
- children: { type: 'node', description: 'Content', required: false },
53
+ children: { type: "node", description: "Content", required: false },
56
54
  },
57
- examples: [
58
- { name: 'Default', description: 'Default example', render: renderStub },
59
- ],
55
+ examples: [{ name: "Default", description: "Default example", render: renderStub }],
60
56
  composition: {
61
- pattern: 'compound',
62
- subComponents: ['Header', 'Body'],
63
- requiredChildren: ['Body'],
64
- commonPatterns: ['<Stub><Stub.Header /><Stub.Body /></Stub>'],
57
+ pattern: "compound",
58
+ subComponents: ["Header", "Body"],
59
+ requiredChildren: ["Body"],
60
+ commonPatterns: ["<Stub><Stub.Header /><Stub.Body /></Stub>"],
65
61
  },
66
62
  contract: {
67
- propsSummary: ['children: node'],
63
+ propsSummary: ["children: node"],
68
64
  },
69
65
  _provenance: {
70
- source: 'scan',
66
+ source: "scan",
71
67
  confidence: 0.85,
72
- autoFields: ['props', 'composition'],
73
- humanFields: ['guidance', 'examples'],
68
+ autoFields: ["props", "composition"],
69
+ humanFields: ["guidance", "examples"],
74
70
  },
75
71
  };
76
72
  }
@@ -79,26 +75,26 @@ function makeV2(): FragmentDefinitionV2<{ children?: string }> {
79
75
  // defineFragment — v1 shape
80
76
  // ---------------------------------------------------------------------------
81
77
 
82
- describe('defineFragment — v1 shape', () => {
83
- it('accepts and returns v1 definition unchanged', () => {
78
+ describe("defineFragment — v1 shape", () => {
79
+ it("accepts and returns v1 definition unchanged", () => {
84
80
  const v1 = makeV1();
85
81
  const result = defineFragment(v1);
86
82
  expect(result).toBe(v1);
87
- expect(result.usage.when).toEqual(['Testing v1']);
88
- expect(result.variants[0].name).toBe('Default');
89
- expect(result.ai?.compositionPattern).toBe('simple');
83
+ expect(result.usage.when).toEqual(["Testing v1"]);
84
+ expect(result.variants[0].name).toBe("Default");
85
+ expect(result.ai?.compositionPattern).toBe("simple");
90
86
  });
91
87
 
92
- it('throws on invalid v1 definition', () => {
88
+ it("throws on invalid v1 definition", () => {
93
89
  const invalid = {
94
90
  component: StubComponent,
95
- meta: { name: '', description: 'bad', category: 'test' }, // empty name
91
+ meta: { name: "", description: "bad", category: "test" }, // empty name
96
92
  usage: { when: [], whenNot: [] },
97
93
  props: {},
98
94
  variants: [],
99
95
  } as unknown as FragmentDefinition;
100
96
 
101
- expect(() => defineFragment(invalid)).toThrow('Invalid fragment definition');
97
+ expect(() => defineFragment(invalid)).toThrow("Invalid fragment definition");
102
98
  });
103
99
  });
104
100
 
@@ -106,43 +102,43 @@ describe('defineFragment — v1 shape', () => {
106
102
  // defineFragment — v2 shape
107
103
  // ---------------------------------------------------------------------------
108
104
 
109
- describe('defineFragment — v2 shape', () => {
110
- it('accepts and returns v2 definition unchanged', () => {
105
+ describe("defineFragment — v2 shape", () => {
106
+ it("accepts and returns v2 definition unchanged", () => {
111
107
  const v2 = makeV2();
112
108
  const result = defineFragment(v2);
113
109
  expect(result).toBe(v2);
114
- expect((result as FragmentDefinitionV2).guidance.when).toEqual(['Testing v2']);
115
- expect((result as FragmentDefinitionV2).examples[0].name).toBe('Default');
116
- expect((result as FragmentDefinitionV2).composition?.pattern).toBe('compound');
110
+ expect((result as FragmentDefinitionV2).guidance.when).toEqual(["Testing v2"]);
111
+ expect((result as FragmentDefinitionV2).examples[0].name).toBe("Default");
112
+ expect((result as FragmentDefinitionV2).composition?.pattern).toBe("compound");
117
113
  });
118
114
 
119
- it('validates v2 field names correctly', () => {
115
+ it("validates v2 field names correctly", () => {
120
116
  const v2 = makeV2();
121
117
  // Should not throw
122
118
  expect(() => defineFragment(v2)).not.toThrow();
123
119
  });
124
120
 
125
- it('throws on invalid v2 definition', () => {
121
+ it("throws on invalid v2 definition", () => {
126
122
  const invalid = {
127
123
  component: StubComponent,
128
- meta: { name: 'Test', description: 'test', category: 'test' },
129
- guidance: { when: 'not-an-array', whenNot: [] }, // wrong type
124
+ meta: { name: "Test", description: "test", category: "test" },
125
+ guidance: { when: "not-an-array", whenNot: [] }, // wrong type
130
126
  props: {},
131
127
  examples: [],
132
128
  } as unknown as FragmentDefinitionV2;
133
129
 
134
- expect(() => defineFragment(invalid)).toThrow('Invalid fragment definition');
130
+ expect(() => defineFragment(invalid)).toThrow("Invalid fragment definition");
135
131
  });
136
132
 
137
- it('accepts wrapper composition pattern', () => {
133
+ it("accepts wrapper composition pattern", () => {
138
134
  const v2 = makeV2();
139
- v2.composition = { pattern: 'wrapper' };
135
+ v2.composition = { pattern: "wrapper" };
140
136
  expect(() => defineFragment(v2)).not.toThrow();
141
137
  });
142
138
 
143
- it('accepts scan provenance source', () => {
139
+ it("accepts scan provenance source", () => {
144
140
  const v2 = makeV2();
145
- v2._provenance = { source: 'scan', confidence: 0.9 };
141
+ v2._provenance = { source: "scan", confidence: 0.9 };
146
142
  expect(() => defineFragment(v2)).not.toThrow();
147
143
  });
148
144
  });
@@ -151,68 +147,73 @@ describe('defineFragment — v2 shape', () => {
151
147
  // normalizeToV1
152
148
  // ---------------------------------------------------------------------------
153
149
 
154
- describe('normalizeToV1', () => {
155
- it('maps guidance → usage', () => {
150
+ describe("normalizeToV1", () => {
151
+ it("maps guidance → usage", () => {
156
152
  const v2 = makeV2();
157
153
  const v1 = normalizeToV1(v2);
158
154
  expect(v1.usage).toBe(v2.guidance);
159
- expect(v1.usage.when).toEqual(['Testing v2']);
155
+ expect(v1.usage.when).toEqual(["Testing v2"]);
160
156
  });
161
157
 
162
- it('maps examples → variants', () => {
158
+ it("maps examples → variants", () => {
163
159
  const v2 = makeV2();
164
160
  const v1 = normalizeToV1(v2);
165
161
  expect(v1.variants).toBe(v2.examples);
166
- expect(v1.variants[0].name).toBe('Default');
162
+ expect(v1.variants[0].name).toBe("Default");
167
163
  });
168
164
 
169
- it('maps composition → ai', () => {
165
+ it("maps composition → ai", () => {
170
166
  const v2 = makeV2();
171
167
  const v1 = normalizeToV1(v2);
172
168
  expect(v1.ai).toEqual({
173
- compositionPattern: 'compound',
174
- subComponents: ['Header', 'Body'],
175
- requiredChildren: ['Body'],
176
- commonPatterns: ['<Stub><Stub.Header /><Stub.Body /></Stub>'],
169
+ compositionPattern: "compound",
170
+ subComponents: ["Header", "Body"],
171
+ requiredChildren: ["Body"],
172
+ commonPatterns: ["<Stub><Stub.Header /><Stub.Body /></Stub>"],
177
173
  });
178
174
  });
179
175
 
180
- it('maps _provenance → _generated (narrows scan → ai)', () => {
176
+ it("maps _provenance → _generated without rewriting scan provenance", () => {
181
177
  const v2 = makeV2();
182
178
  const v1 = normalizeToV1(v2);
183
179
  expect(v1._generated).toEqual({
184
- source: 'ai', // scan → ai (v1 doesn't have scan)
180
+ source: "scan",
185
181
  sourceFile: undefined,
186
182
  confidence: 0.85,
187
183
  timestamp: undefined,
188
184
  });
189
185
  });
190
186
 
191
- it('preserves storybook source as-is', () => {
187
+ it("preserves storybook source as-is", () => {
192
188
  const v2 = makeV2();
193
- v2._provenance = { source: 'storybook', sourceFile: 'Button.stories.tsx' };
189
+ v2._provenance = {
190
+ source: "storybook",
191
+ sourceFile: "Button.stories.tsx",
192
+ verified: true,
193
+ };
194
194
  const v1 = normalizeToV1(v2);
195
- expect(v1._generated!.source).toBe('storybook');
196
- expect(v1._generated!.sourceFile).toBe('Button.stories.tsx');
195
+ expect(v1._generated!.source).toBe("storybook");
196
+ expect(v1._generated!.sourceFile).toBe("Button.stories.tsx");
197
+ expect(v1._generated!.verified).toBe(true);
197
198
  });
198
199
 
199
- it('handles undefined composition gracefully', () => {
200
+ it("handles undefined composition gracefully", () => {
200
201
  const v2 = makeV2();
201
202
  v2.composition = undefined;
202
203
  const v1 = normalizeToV1(v2);
203
204
  expect(v1.ai).toBeUndefined();
204
205
  });
205
206
 
206
- it('handles undefined _provenance gracefully', () => {
207
+ it("handles undefined _provenance gracefully", () => {
207
208
  const v2 = makeV2();
208
209
  v2._provenance = undefined;
209
210
  const v1 = normalizeToV1(v2);
210
211
  expect(v1._generated).toBeUndefined();
211
212
  });
212
213
 
213
- it('preserves contract and relations', () => {
214
+ it("preserves contract and relations", () => {
214
215
  const v2 = makeV2();
215
- v2.relations = [{ component: 'Other', relationship: 'sibling', note: 'test' }];
216
+ v2.relations = [{ component: "Other", relationship: "sibling", note: "test" }];
216
217
  const v1 = normalizeToV1(v2);
217
218
  expect(v1.contract).toBe(v2.contract);
218
219
  expect(v1.relations).toBe(v2.relations);
@@ -223,35 +224,419 @@ describe('normalizeToV1', () => {
223
224
  // compileFragment — v2 support
224
225
  // ---------------------------------------------------------------------------
225
226
 
226
- describe('compileFragment — v2 support', () => {
227
- it('compiles v1 definition', () => {
227
+ describe("compileFragment — v2 support", () => {
228
+ it("compiles v1 definition", () => {
228
229
  const v1 = makeV1();
229
- const compiled = compileFragment(v1, 'test/Stub.fragment.tsx');
230
- expect(compiled.meta.name).toBe('Stub');
231
- expect(compiled.usage.when).toEqual(['Testing v1']);
232
- expect(compiled.variants[0].name).toBe('Default');
233
- expect(compiled.ai?.compositionPattern).toBe('simple');
230
+ const compiled = compileFragment(v1, "test/Stub.fragment.tsx");
231
+ expect(compiled.meta.name).toBe("Stub");
232
+ expect(compiled.usage.when).toEqual(["Testing v1"]);
233
+ expect(compiled.variants[0].name).toBe("Default");
234
+ expect(compiled.ai?.compositionPattern).toBe("simple");
234
235
  });
235
236
 
236
- it('compiles v2 definition (normalizes to v1 output)', () => {
237
+ it("compiles v2 definition (normalizes to v1 output)", () => {
237
238
  const v2 = makeV2();
238
- const compiled = compileFragment(v2, 'test/Stub.fragment.tsx');
239
- expect(compiled.meta.name).toBe('Stub');
240
- expect(compiled.usage.when).toEqual(['Testing v2']);
241
- expect(compiled.variants[0].name).toBe('Default');
242
- expect(compiled.ai?.compositionPattern).toBe('compound');
243
- expect(compiled.ai?.subComponents).toEqual(['Header', 'Body']);
244
- expect(compiled._generated?.source).toBe('ai'); // scan normalized to ai
239
+ const compiled = compileFragment(v2, "test/Stub.fragment.tsx");
240
+ expect(compiled.meta.name).toBe("Stub");
241
+ expect(compiled.usage.when).toEqual(["Testing v2"]);
242
+ expect(compiled.variants[0].name).toBe("Default");
243
+ expect(compiled.ai?.compositionPattern).toBe("compound");
244
+ expect(compiled.ai?.subComponents).toEqual(["Header", "Body"]);
245
+ expect(compiled._generated?.source).toBe("scan");
245
246
  });
246
247
 
247
- it('compiles v2 without optional fields', () => {
248
+ it("compiles v2 without optional fields", () => {
248
249
  const v2 = makeV2();
249
250
  v2.composition = undefined;
250
251
  v2._provenance = undefined;
251
252
  v2.contract = undefined;
252
- const compiled = compileFragment(v2, 'test/Stub.fragment.tsx');
253
+ const compiled = compileFragment(v2, "test/Stub.fragment.tsx");
253
254
  expect(compiled.ai).toBeUndefined();
254
255
  expect(compiled._generated).toBeUndefined();
255
256
  expect(compiled.contract).toBeUndefined();
256
257
  });
258
+
259
+ it("maps governed composition metadata to compiled ai metadata", () => {
260
+ const compiled = compileFragment(
261
+ {
262
+ component: StubComponent,
263
+ meta: { name: "Stub", description: "A stub", category: "test" },
264
+ guidance: { when: ["Testing"], whenNot: [] },
265
+ composition: {
266
+ pattern: "compound",
267
+ subComponents: ["Header", "Body"],
268
+ requiredChildren: ["Body"],
269
+ commonPatterns: ["<Stub><Stub.Body /></Stub>"],
270
+ },
271
+ _provenance: { source: "scan" },
272
+ governance: [],
273
+ },
274
+ "test/Stub.fragment.ts"
275
+ );
276
+
277
+ expect(compiled.ai).toEqual({
278
+ compositionPattern: "compound",
279
+ subComponents: ["Header", "Body"],
280
+ requiredChildren: ["Body"],
281
+ commonPatterns: ["<Stub><Stub.Body /></Stub>"],
282
+ });
283
+ expect(compiled._generated?.source).toBe("scan");
284
+ });
285
+ });
286
+
287
+ // ---------------------------------------------------------------------------
288
+ // defineFragment — v3 arity discrimination (A06-01..A06-03)
289
+ // ---------------------------------------------------------------------------
290
+
291
+ describe("defineFragment — v3 two-argument arity", () => {
292
+ type ButtonProps = {
293
+ variant?: "primary" | "secondary";
294
+ children?: string;
295
+ };
296
+
297
+ function Button(_props: ButtonProps) {
298
+ return null;
299
+ }
300
+
301
+ it("returns a bound fragment definition from two arguments (A06-01)", () => {
302
+ const result = defineFragment(Button, {
303
+ meta: {
304
+ name: "Button",
305
+ purpose: "Primary page action",
306
+ category: "forms",
307
+ aliases: ["cta"],
308
+ },
309
+ states: {
310
+ primary: {
311
+ render: null,
312
+ canonical: true,
313
+ },
314
+ },
315
+ guidance: {
316
+ when: ["Confirming a form"],
317
+ whenNot: ["Navigation"],
318
+ dont: [
319
+ {
320
+ reason: "Raw styling bypasses the contract",
321
+ bad: '<button style={{ background: "#3b82f6" }}>Save</button>',
322
+ good: null,
323
+ },
324
+ ],
325
+ },
326
+ matrix: {
327
+ axes: { variant: "auto", theme: ["light", "dark"] },
328
+ forced: ["hover", "disabled"],
329
+ },
330
+ preview: { providers: [], dynamicRegions: [] },
331
+ design: { figmaNode: "https://figma.com/file/x" },
332
+ annotations: { notes: ["pilot"] },
333
+ props: {
334
+ variant: { description: "Visual style", visibility: "public" },
335
+ },
336
+ });
337
+
338
+ expect(result.component).toBe(Button);
339
+ expect(result.meta.purpose).toBe("Primary page action");
340
+ expect(result.states.primary.canonical).toBe(true);
341
+ expect(result.guidance.dont?.[0]?.reason).toContain("Raw styling");
342
+ });
343
+
344
+ it("type-checks real component props on annotations (A06-01)", () => {
345
+ const result = defineFragment(Button, {
346
+ meta: { name: "Button", purpose: "Action", category: "forms" },
347
+ states: { primary: { render: () => null } },
348
+ guidance: { when: [], whenNot: [] },
349
+ props: {
350
+ variant: { description: "Visual style" },
351
+ // @ts-expect-error unknown prop is not on ButtonProps
352
+ madeUp: { description: "nope" },
353
+ },
354
+ });
355
+ expect(result.props?.variant?.description).toBe("Visual style");
356
+ });
357
+
358
+ it("preserves one-argument v2 behavior (A06-01)", () => {
359
+ const v2 = makeV2();
360
+ const result = defineFragment(v2);
361
+ expect(result).toBe(v2);
362
+ });
363
+
364
+ it("does not misclassify plain v2 as governed (A06-02)", () => {
365
+ const v2 = makeV2();
366
+ const result = defineFragment(v2);
367
+ expect("governance" in result).toBe(false);
368
+ expect((result as FragmentDefinitionV2).guidance.when).toEqual(["Testing v2"]);
369
+ });
370
+
371
+ it("keeps governed-without-govern (governance array only) as governed (A06-02)", () => {
372
+ const result = defineFragment({
373
+ component: StubComponent,
374
+ meta: { name: "Stub", description: "A stub", category: "test" },
375
+ guidance: { when: ["Testing"], whenNot: [] },
376
+ governance: [],
377
+ });
378
+ expect(result.governance).toEqual([]);
379
+ });
380
+
381
+ it("rejects an optional-key impostor with neither govern nor governance (A06-02)", () => {
382
+ const compileOnlyInvalidDefinition = () => {
383
+ // @ts-expect-error governed definitions require govern or governance
384
+ defineFragment({
385
+ component: StubComponent,
386
+ meta: { name: "Stub", description: "A stub", category: "test" },
387
+ guidance: { when: ["Testing"], whenNot: [] },
388
+ });
389
+ };
390
+ expect(compileOnlyInvalidDefinition).toBeTypeOf("function");
391
+
392
+ expect(() =>
393
+ defineFragment({
394
+ component: StubComponent,
395
+ meta: { name: "Stub", description: "A stub", category: "test" },
396
+ guidance: { when: ["Testing"], whenNot: [] },
397
+ govern: undefined,
398
+ } as never)
399
+ ).toThrow(/props|examples|govern/);
400
+ });
401
+
402
+ it.each([
403
+ ["v1 govern", makeV1(), "govern", "not-a-function"],
404
+ ["v1 governance", makeV1(), "governance", { kind: "capability" }],
405
+ ["v2 govern", makeV2(), "govern", "not-a-function"],
406
+ ["v2 governance", makeV2(), "governance", { kind: "capability" }],
407
+ ])(
408
+ "rejects malformed reserved governance values on an otherwise-valid %s",
409
+ (_name, body, key, value) => {
410
+ expect(() => defineFragment({ ...body, [key]: value } as never)).toThrow(
411
+ /Invalid fragment definition.*govern/s
412
+ );
413
+ }
414
+ );
415
+
416
+ it("rejects authored code: strings on states (A06-03)", () => {
417
+ expect(() =>
418
+ defineFragment(Button, {
419
+ meta: { name: "Button", purpose: "Action", category: "forms" },
420
+ states: {
421
+ primary: {
422
+ render: null,
423
+ // @ts-expect-error code is forbidden on v3 states
424
+ code: "<Button>Save</Button>",
425
+ },
426
+ },
427
+ guidance: { when: [], whenNot: [] },
428
+ } as never)
429
+ ).toThrow(/code/);
430
+ });
431
+
432
+ it("rejects prop annotations that redefine extracted type/default (A06-03)", () => {
433
+ expect(() =>
434
+ defineFragment(Button, {
435
+ meta: { name: "Button", purpose: "Action", category: "forms" },
436
+ states: { primary: { render: null } },
437
+ guidance: { when: [], whenNot: [] },
438
+ props: {
439
+ variant: {
440
+ description: "Visual style",
441
+ type: "enum",
442
+ default: "primary",
443
+ } as never,
444
+ },
445
+ })
446
+ ).toThrow(/Unrecognized key|redefine|type|default/);
447
+ });
448
+
449
+ it("resolves govern on the two-argument path", () => {
450
+ const result = defineFragment(Button, {
451
+ meta: { name: "Button", purpose: "Action", category: "forms" },
452
+ states: { primary: { render: null } },
453
+ guidance: { when: [], whenNot: [] },
454
+ govern: (g) => [g.capability("submit-action")],
455
+ });
456
+ expect(result.governance).toEqual([{ kind: "capability", capability: "submit-action" }]);
457
+ });
458
+ });
459
+
460
+ describe("defineFragment — production v3 runtime boundary", () => {
461
+ type RescueButtonProps = {
462
+ variant?: "primary" | "secondary";
463
+ };
464
+
465
+ function RescueButton(_props: RescueButtonProps) {
466
+ return null;
467
+ }
468
+
469
+ const validBody = () => ({
470
+ meta: { name: "RescueButton", purpose: "Action", category: "forms" },
471
+ states: { primary: { render: null } },
472
+ guidance: { when: ["Submitting"], whenNot: [] },
473
+ });
474
+
475
+ it("rejects authored state code in production (A06A-01)", () => {
476
+ vi.stubEnv("NODE_ENV", "production");
477
+
478
+ expect(() =>
479
+ defineFragment(RescueButton, {
480
+ ...validBody(),
481
+ states: {
482
+ primary: {
483
+ render: null,
484
+ code: "<RescueButton />",
485
+ },
486
+ },
487
+ } as never)
488
+ ).toThrow(/states\.primary.*code/s);
489
+ });
490
+
491
+ it.each([
492
+ ["undefined", undefined],
493
+ ["null", null],
494
+ ])("rejects a malformed v3 body (%s) with the coded validation error", (_label, body) => {
495
+ vi.stubEnv("NODE_ENV", "production");
496
+
497
+ expect(() => defineFragment(RescueButton, body as never)).toThrow(
498
+ /Invalid fragment definition/
499
+ );
500
+ });
501
+
502
+ it.each([
503
+ ["undefined", undefined],
504
+ ["null", null],
505
+ ["number", 1],
506
+ ["string", "invalid"],
507
+ ["boolean", true],
508
+ ])(
509
+ "rejects a non-object one-argument definition (%s) with the coded validation error",
510
+ (_label, value) => {
511
+ vi.stubEnv("NODE_ENV", "production");
512
+
513
+ expect(() => defineFragment(value as never)).toThrow(/Invalid fragment definition/);
514
+ }
515
+ );
516
+
517
+ it.each([
518
+ ["type", { type: "enum" }],
519
+ ["default", { default: "primary" }],
520
+ ])("rejects authored prop %s truth in production (A06A-02)", (_field, truth) => {
521
+ vi.stubEnv("NODE_ENV", "production");
522
+
523
+ expect(() =>
524
+ defineFragment(RescueButton, {
525
+ ...validBody(),
526
+ props: {
527
+ variant: { description: "Visual style", ...truth },
528
+ },
529
+ } as never)
530
+ ).toThrow(/props\.variant|Unrecognized key|type|default/);
531
+ });
532
+
533
+ it.each([
534
+ ["non-function govern", { govern: "not-a-function" }],
535
+ ["non-array governance", { governance: { kind: "capability" } }],
536
+ ["malformed governance record", { governance: [{ kind: "capability", capability: "" }] }],
537
+ ])("rejects %s in production (A06A-03)", (_case, invalidGovernance) => {
538
+ vi.stubEnv("NODE_ENV", "production");
539
+
540
+ expect(() =>
541
+ defineFragment(RescueButton, {
542
+ ...validBody(),
543
+ ...invalidGovernance,
544
+ } as never)
545
+ ).toThrow(/govern|capability/);
546
+ });
547
+
548
+ it("returns a valid governed definition and preserves v1/v2 in production (A06A-04)", () => {
549
+ vi.stubEnv("NODE_ENV", "production");
550
+
551
+ const result = defineFragment(RescueButton, {
552
+ ...validBody(),
553
+ govern: (g) => [g.capability("submit-action")],
554
+ });
555
+
556
+ expect(result).not.toBe(RescueButton);
557
+ expect(result.component).toBe(RescueButton);
558
+ expect(result.governance).toEqual([{ kind: "capability", capability: "submit-action" }]);
559
+ expect(defineFragment(makeV1())).toEqual(makeV1());
560
+ expect(defineFragment(makeV2())).toEqual(makeV2());
561
+ });
562
+
563
+ it.each([
564
+ [
565
+ "v1",
566
+ {
567
+ ...makeV1(),
568
+ meta: { ...makeV1().meta, name: "" },
569
+ },
570
+ ],
571
+ [
572
+ "v2",
573
+ {
574
+ ...makeV2(),
575
+ guidance: { when: "not-an-array", whenNot: [] },
576
+ },
577
+ ],
578
+ ])("rejects an invalid %s definition in production", (_api, definition) => {
579
+ vi.stubEnv("NODE_ENV", "production");
580
+
581
+ expect(() => defineFragment(definition as never)).toThrow(/Invalid fragment definition/);
582
+ });
583
+
584
+ it("rejects invalid blocks in production", () => {
585
+ vi.stubEnv("NODE_ENV", "production");
586
+
587
+ expect(() =>
588
+ defineBlock({
589
+ name: "",
590
+ description: "",
591
+ category: "",
592
+ components: [],
593
+ code: "",
594
+ })
595
+ ).toThrow(/Invalid block definition/);
596
+ });
597
+
598
+ it("orders validation issues deterministically", () => {
599
+ vi.stubEnv("NODE_ENV", "production");
600
+
601
+ let thrown: unknown;
602
+ try {
603
+ defineFragment({
604
+ ...makeV1(),
605
+ meta: { name: "", description: "", category: "" },
606
+ });
607
+ } catch (error) {
608
+ thrown = error;
609
+ }
610
+
611
+ expect((thrown as { issues: string[] }).issues).toEqual([
612
+ "meta.category: String must contain at least 1 character(s)",
613
+ "meta.description: String must contain at least 1 character(s)",
614
+ "meta.name: String must contain at least 1 character(s)",
615
+ ]);
616
+ });
617
+ });
618
+
619
+ describe("defineFragment — compiler validation hand-off", () => {
620
+ it("marks authoring validation errors without conflating arbitrary runtime failures", () => {
621
+ let thrown: unknown;
622
+ try {
623
+ defineFragment(StubComponent, {
624
+ meta: { name: "Stub", purpose: "Test", category: "test" },
625
+ states: { primary: { render: null, code: "<Stub />" } },
626
+ guidance: { when: [], whenNot: [] },
627
+ } as never);
628
+ } catch (error) {
629
+ thrown = error;
630
+ }
631
+
632
+ expect(thrown).toMatchObject({
633
+ name: "InvalidFragmentDefinitionError",
634
+ code: "FUI9010",
635
+ source: "@usefragments/core/defineFragment",
636
+ api: "v3",
637
+ });
638
+ expect((thrown as { issues: string[] }).issues).toEqual(
639
+ expect.arrayContaining([expect.stringMatching(/states\.primary.*code/s)])
640
+ );
641
+ });
257
642
  });