@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.
- package/dist/{chunk-MZ4SW3TP.js → chunk-3IOWHECM.js} +8 -1
- package/dist/{chunk-MZ4SW3TP.js.map → chunk-3IOWHECM.js.map} +1 -1
- package/dist/chunk-7ULGH74M.js +66 -0
- package/dist/chunk-7ULGH74M.js.map +1 -0
- package/dist/{chunk-XN3LSDPY.js → chunk-BMPYIUZE.js} +612 -344
- package/dist/chunk-BMPYIUZE.js.map +1 -0
- package/dist/{chunk-YF65VYRY.js → chunk-ML5S6QNU.js} +191 -169
- package/dist/chunk-ML5S6QNU.js.map +1 -0
- package/dist/{chunk-3LLRNCPX.js → chunk-MZ2FS7U4.js} +1 -1
- package/dist/chunk-MZ2FS7U4.js.map +1 -0
- package/dist/chunk-PWIJMOI4.js +202 -0
- package/dist/chunk-PWIJMOI4.js.map +1 -0
- package/dist/chunk-RYFULE43.js +578 -0
- package/dist/chunk-RYFULE43.js.map +1 -0
- package/dist/codes/index.d.ts +2 -2
- package/dist/codes/index.js +3 -2
- package/dist/compiled-types/index.d.ts +327 -2
- package/dist/compiled-types/index.js +1 -1
- package/dist/generate/index.d.ts +3 -2
- package/dist/{governance-CLk_wkP9.d.ts → governance-hOPXGbbs.d.ts} +474 -515
- package/dist/governance-telemetry.d.ts +6 -0
- package/dist/governance-telemetry.js +1 -1
- package/dist/{index-_sxhUNqx.d.ts → index-C8bcXVav.d.ts} +448 -448
- package/dist/index.d.ts +3280 -1305
- package/dist/index.js +2414 -350
- package/dist/index.js.map +1 -1
- package/dist/manifest.d.ts +228 -0
- package/dist/manifest.js +24 -0
- package/dist/manifest.js.map +1 -0
- package/dist/preview/index.js +45 -1
- package/dist/preview/index.js.map +1 -1
- package/dist/preview-runtime.d.ts +1 -2
- package/dist/preview-runtime.js +150 -14
- package/dist/preview-runtime.js.map +1 -1
- package/dist/react-types.d.ts +1 -2
- package/dist/registry.d.ts +1412 -203
- package/dist/registry.js +30 -3
- package/dist/schemas/index.d.ts +1 -1
- package/dist/schemas/index.js +3 -2
- package/dist/storyAdapter.d.ts +1 -2
- package/dist/storyAdapter.js +11 -49
- package/dist/storyAdapter.js.map +1 -1
- package/dist/test-utils.d.ts +3 -2
- package/dist/topology/index.d.ts +1 -1
- package/dist/topology/index.js +1 -1
- package/package.json +8 -2
- package/src/__tests__/contract-parser.test.ts +318 -277
- package/src/__tests__/preview-runtime-hook.test.tsx +315 -0
- package/src/__tests__/preview-runtime.test.tsx +30 -8
- package/src/__tests__/schema.test.ts +191 -14
- package/src/analysis-plan/analysis-plan-v1.test.ts +320 -0
- package/src/analysis-plan/coverage.ts +181 -0
- package/src/analysis-plan/digest.ts +141 -0
- package/src/analysis-plan/index.ts +34 -0
- package/src/analysis-plan/types.ts +207 -0
- package/src/approved-contract-tokens.test.ts +39 -0
- package/src/approved-contract-tokens.ts +18 -0
- package/src/codes/__tests__/codes.test.ts +13 -0
- package/src/codes/codes.ts +40 -0
- package/src/compiled-types/index.ts +640 -39
- package/src/compiled-types/parse.test.ts +145 -4
- package/src/component-contract.ts +95 -53
- package/src/composition.ts +7 -13
- package/src/constants.ts +3 -6
- package/src/contract/hash.test.ts +20 -0
- package/src/contract/hash.ts +66 -9
- package/src/contract/index.ts +24 -1
- package/src/contract/manifest.test.ts +94 -0
- package/src/contract/manifest.ts +68 -0
- package/src/contract/preimage.test.ts +219 -1
- package/src/contract/preimage.ts +326 -6
- package/src/contract/stamp.test.ts +3 -0
- package/src/contract/stamp.ts +1 -1
- package/src/contract-parser.ts +54 -30
- package/src/defineFragment.test.ts +476 -91
- package/src/defineFragment.ts +204 -114
- package/src/domain-ids.test.ts +35 -0
- package/src/domain-ids.ts +61 -0
- package/src/evaluation/evaluate.test.ts +522 -0
- package/src/evaluation/evaluate.ts +690 -0
- package/src/evaluation/evaluation-v2-receipt-v1.test.ts +772 -0
- package/src/evaluation/index.ts +58 -0
- package/src/evaluation/receipt.ts +753 -0
- package/src/evaluation/types.ts +406 -0
- package/src/facts/builders.ts +2 -0
- package/src/facts/compile.ts +29 -6
- package/src/facts/fact-index.ts +13 -3
- package/src/facts/fact-integrity-v1.test.ts +172 -0
- package/src/facts/facts.test.ts +15 -0
- package/src/facts/ids.ts +46 -3
- package/src/facts/index.ts +14 -1
- package/src/facts/integrity.ts +134 -0
- package/src/facts/types.ts +36 -0
- package/src/governance-integrity.test.ts +1 -0
- package/src/governance-integrity.ts +5 -3
- package/src/governance-telemetry.ts +8 -0
- package/src/governance.ts +70 -8
- package/src/index.ts +230 -37
- package/src/preview/validation.test.ts +62 -0
- package/src/preview/validation.ts +48 -2
- package/src/preview-runtime.tsx +227 -20
- package/src/registry-install-plan.ts +200 -109
- package/src/registry-shards.test.ts +263 -0
- package/src/registry.ts +237 -0
- package/src/repository-binding.test.ts +50 -0
- package/src/repository-binding.ts +96 -0
- package/src/rules/families.test.ts +36 -0
- package/src/rules/finding.ts +7 -2
- package/src/rules/index.ts +17 -1
- package/src/rules/rule-config.test.ts +66 -0
- package/src/rules/rule-config.ts +73 -0
- package/src/rules/rules.test.ts +26 -0
- package/src/rules/tokens-css-vars-must-be-defined.test.ts +51 -2
- package/src/rules/tokens-css-vars-must-be-defined.ts +34 -1
- package/src/schema.ts +293 -113
- package/src/schemas/index.ts +1 -1
- package/src/storyAdapter.test.ts +68 -12
- package/src/storyAdapter.ts +44 -75
- package/src/topology/resolve-area.ts +1 -1
- package/src/types.ts +258 -40
- package/dist/chunk-3LLRNCPX.js.map +0 -1
- package/dist/chunk-RANPUC6C.js +0 -72
- package/dist/chunk-RANPUC6C.js.map +0 -1
- package/dist/chunk-XN3LSDPY.js.map +0 -1
- package/dist/chunk-YF65VYRY.js.map +0 -1
- package/src/fragment-types.ts +0 -214
- package/src/react-create-element.test.ts +0 -22
- package/src/react-create-element.ts +0 -12
- package/src/storyFilters.test.ts +0 -350
- package/src/storyFilters.ts +0 -253
|
@@ -1,10 +1,6 @@
|
|
|
1
|
-
import { describe, it, expect } from
|
|
2
|
-
import { defineFragment, compileFragment, normalizeToV1 } from
|
|
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:
|
|
22
|
+
meta: { name: "Stub", description: "A stub", category: "test", status: "stable" },
|
|
23
23
|
usage: {
|
|
24
|
-
when: [
|
|
25
|
-
whenNot: [
|
|
24
|
+
when: ["Testing v1"],
|
|
25
|
+
whenNot: ["Production"],
|
|
26
26
|
},
|
|
27
27
|
props: {
|
|
28
|
-
children: { type:
|
|
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:
|
|
32
|
+
compositionPattern: "simple",
|
|
35
33
|
},
|
|
36
34
|
contract: {
|
|
37
|
-
propsSummary: [
|
|
35
|
+
propsSummary: ["children: node"],
|
|
38
36
|
},
|
|
39
37
|
_generated: {
|
|
40
|
-
source:
|
|
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:
|
|
47
|
+
meta: { name: "Stub", description: "A stub", category: "test", status: "stable" },
|
|
50
48
|
guidance: {
|
|
51
|
-
when: [
|
|
52
|
-
whenNot: [
|
|
49
|
+
when: ["Testing v2"],
|
|
50
|
+
whenNot: ["Production"],
|
|
53
51
|
},
|
|
54
52
|
props: {
|
|
55
|
-
children: { type:
|
|
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:
|
|
62
|
-
subComponents: [
|
|
63
|
-
requiredChildren: [
|
|
64
|
-
commonPatterns: [
|
|
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: [
|
|
63
|
+
propsSummary: ["children: node"],
|
|
68
64
|
},
|
|
69
65
|
_provenance: {
|
|
70
|
-
source:
|
|
66
|
+
source: "scan",
|
|
71
67
|
confidence: 0.85,
|
|
72
|
-
autoFields: [
|
|
73
|
-
humanFields: [
|
|
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(
|
|
83
|
-
it(
|
|
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([
|
|
88
|
-
expect(result.variants[0].name).toBe(
|
|
89
|
-
expect(result.ai?.compositionPattern).toBe(
|
|
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(
|
|
88
|
+
it("throws on invalid v1 definition", () => {
|
|
93
89
|
const invalid = {
|
|
94
90
|
component: StubComponent,
|
|
95
|
-
meta: { 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(
|
|
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(
|
|
110
|
-
it(
|
|
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([
|
|
115
|
-
expect((result as FragmentDefinitionV2).examples[0].name).toBe(
|
|
116
|
-
expect((result as FragmentDefinitionV2).composition?.pattern).toBe(
|
|
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(
|
|
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(
|
|
121
|
+
it("throws on invalid v2 definition", () => {
|
|
126
122
|
const invalid = {
|
|
127
123
|
component: StubComponent,
|
|
128
|
-
meta: { name:
|
|
129
|
-
guidance: { when:
|
|
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(
|
|
130
|
+
expect(() => defineFragment(invalid)).toThrow("Invalid fragment definition");
|
|
135
131
|
});
|
|
136
132
|
|
|
137
|
-
it(
|
|
133
|
+
it("accepts wrapper composition pattern", () => {
|
|
138
134
|
const v2 = makeV2();
|
|
139
|
-
v2.composition = { pattern:
|
|
135
|
+
v2.composition = { pattern: "wrapper" };
|
|
140
136
|
expect(() => defineFragment(v2)).not.toThrow();
|
|
141
137
|
});
|
|
142
138
|
|
|
143
|
-
it(
|
|
139
|
+
it("accepts scan provenance source", () => {
|
|
144
140
|
const v2 = makeV2();
|
|
145
|
-
v2._provenance = { source:
|
|
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(
|
|
155
|
-
it(
|
|
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([
|
|
155
|
+
expect(v1.usage.when).toEqual(["Testing v2"]);
|
|
160
156
|
});
|
|
161
157
|
|
|
162
|
-
it(
|
|
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(
|
|
162
|
+
expect(v1.variants[0].name).toBe("Default");
|
|
167
163
|
});
|
|
168
164
|
|
|
169
|
-
it(
|
|
165
|
+
it("maps composition → ai", () => {
|
|
170
166
|
const v2 = makeV2();
|
|
171
167
|
const v1 = normalizeToV1(v2);
|
|
172
168
|
expect(v1.ai).toEqual({
|
|
173
|
-
compositionPattern:
|
|
174
|
-
subComponents: [
|
|
175
|
-
requiredChildren: [
|
|
176
|
-
commonPatterns: [
|
|
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(
|
|
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:
|
|
180
|
+
source: "scan",
|
|
185
181
|
sourceFile: undefined,
|
|
186
182
|
confidence: 0.85,
|
|
187
183
|
timestamp: undefined,
|
|
188
184
|
});
|
|
189
185
|
});
|
|
190
186
|
|
|
191
|
-
it(
|
|
187
|
+
it("preserves storybook source as-is", () => {
|
|
192
188
|
const v2 = makeV2();
|
|
193
|
-
v2._provenance = {
|
|
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(
|
|
196
|
-
expect(v1._generated!.sourceFile).toBe(
|
|
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(
|
|
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(
|
|
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(
|
|
214
|
+
it("preserves contract and relations", () => {
|
|
214
215
|
const v2 = makeV2();
|
|
215
|
-
v2.relations = [{ component:
|
|
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(
|
|
227
|
-
it(
|
|
227
|
+
describe("compileFragment — v2 support", () => {
|
|
228
|
+
it("compiles v1 definition", () => {
|
|
228
229
|
const v1 = makeV1();
|
|
229
|
-
const compiled = compileFragment(v1,
|
|
230
|
-
expect(compiled.meta.name).toBe(
|
|
231
|
-
expect(compiled.usage.when).toEqual([
|
|
232
|
-
expect(compiled.variants[0].name).toBe(
|
|
233
|
-
expect(compiled.ai?.compositionPattern).toBe(
|
|
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(
|
|
237
|
+
it("compiles v2 definition (normalizes to v1 output)", () => {
|
|
237
238
|
const v2 = makeV2();
|
|
238
|
-
const compiled = compileFragment(v2,
|
|
239
|
-
expect(compiled.meta.name).toBe(
|
|
240
|
-
expect(compiled.usage.when).toEqual([
|
|
241
|
-
expect(compiled.variants[0].name).toBe(
|
|
242
|
-
expect(compiled.ai?.compositionPattern).toBe(
|
|
243
|
-
expect(compiled.ai?.subComponents).toEqual([
|
|
244
|
-
expect(compiled._generated?.source).toBe(
|
|
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(
|
|
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,
|
|
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
|
});
|