@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
@@ -0,0 +1,315 @@
1
+ import { beforeEach, describe, expect, it, vi } from "vitest";
2
+ import { usePreviewVariantRuntime } from "../preview-runtime.js";
3
+
4
+ const hookHarness = vi.hoisted(() => ({
5
+ cursor: 0,
6
+ renderRequested: false,
7
+ pendingEffects: [] as Array<() => void>,
8
+ slots: [] as Array<
9
+ | { kind: "state"; value: unknown }
10
+ | { kind: "ref"; value: { current: unknown } }
11
+ | {
12
+ kind: "effect";
13
+ cleanup?: () => void;
14
+ deps?: readonly unknown[];
15
+ }
16
+ >,
17
+ updates: [] as Array<{
18
+ state?: { content: unknown; isLoading: boolean; error: unknown };
19
+ }>,
20
+ }));
21
+
22
+ vi.mock("react", async () => {
23
+ const actual = await vi.importActual<typeof import("react")>("react");
24
+ return {
25
+ ...actual,
26
+ useEffect: (effect: () => void | (() => void), deps?: readonly unknown[]) => {
27
+ const slotIndex = hookHarness.cursor++;
28
+ const prior = hookHarness.slots[slotIndex];
29
+ const dependenciesChanged =
30
+ prior?.kind !== "effect" ||
31
+ deps === undefined ||
32
+ prior.deps === undefined ||
33
+ deps.length !== prior.deps.length ||
34
+ deps.some((dependency, index) => !Object.is(dependency, prior.deps?.[index]));
35
+
36
+ if (!dependenciesChanged) return;
37
+
38
+ hookHarness.pendingEffects.push(() => {
39
+ if (prior?.kind === "effect") prior.cleanup?.();
40
+ const cleanup = effect();
41
+ hookHarness.slots[slotIndex] = {
42
+ kind: "effect",
43
+ ...(typeof cleanup === "function" ? { cleanup } : {}),
44
+ deps,
45
+ };
46
+ });
47
+ },
48
+ useRef: <T,>(initial: T) => {
49
+ const slotIndex = hookHarness.cursor++;
50
+ const prior = hookHarness.slots[slotIndex];
51
+ if (prior?.kind === "ref") return prior.value;
52
+
53
+ const value = { current: initial };
54
+ hookHarness.slots[slotIndex] = { kind: "ref", value };
55
+ return value;
56
+ },
57
+ useState: <T,>(initial: T) => {
58
+ const slotIndex = hookHarness.cursor++;
59
+ const prior = hookHarness.slots[slotIndex];
60
+ if (prior?.kind !== "state") {
61
+ hookHarness.slots[slotIndex] = { kind: "state", value: initial };
62
+ }
63
+
64
+ return [
65
+ (hookHarness.slots[slotIndex] as { kind: "state"; value: T }).value,
66
+ (next: T | ((current: T) => T)) => {
67
+ const current = (hookHarness.slots[slotIndex] as { kind: "state"; value: T }).value;
68
+ const value = typeof next === "function" ? (next as (current: T) => T)(current) : next;
69
+ (hookHarness.slots[slotIndex] as { kind: "state"; value: T }).value = value;
70
+ hookHarness.renderRequested = true;
71
+ hookHarness.updates.push(
72
+ value as {
73
+ state?: { content: unknown; isLoading: boolean; error: unknown };
74
+ }
75
+ );
76
+ },
77
+ ];
78
+ },
79
+ };
80
+ });
81
+
82
+ function renderHook<T>(render: () => T): T {
83
+ hookHarness.cursor = 0;
84
+ hookHarness.renderRequested = false;
85
+ const result = render();
86
+ const effects = hookHarness.pendingEffects.splice(0);
87
+ for (const effect of effects) effect();
88
+ return result;
89
+ }
90
+
91
+ async function flushResolution(): Promise<void> {
92
+ await Promise.resolve();
93
+ await Promise.resolve();
94
+ }
95
+
96
+ async function settleHook<T>(render: () => T, maxRenders = 8): Promise<T> {
97
+ let result = renderHook(render);
98
+ for (
99
+ let renderCount = 0;
100
+ renderCount < maxRenders && hookHarness.renderRequested;
101
+ renderCount++
102
+ ) {
103
+ await flushResolution();
104
+ result = renderHook(render);
105
+ }
106
+ return result;
107
+ }
108
+
109
+ function makeWideLoadedData(version: string): Record<string, unknown> {
110
+ return {
111
+ version,
112
+ items: Array.from({ length: 1_005 }, (_, index) => ({ index })),
113
+ };
114
+ }
115
+
116
+ function makeDeepLoadedData(depth: number, label = "leaf"): Record<string, unknown> {
117
+ let node: Record<string, unknown> = { label };
118
+ for (let index = 0; index < depth; index++) {
119
+ node = { next: node };
120
+ }
121
+ return { node };
122
+ }
123
+
124
+ function readDeepLabel(loadedData: Record<string, unknown> | undefined): unknown {
125
+ let node = loadedData?.node as Record<string, unknown> | undefined;
126
+ while (node && "next" in node) {
127
+ node = node.next as Record<string, unknown> | undefined;
128
+ }
129
+ return node?.label;
130
+ }
131
+
132
+ function expectLoadedDataComparisonError(state: ReturnType<typeof usePreviewVariantRuntime>): void {
133
+ expect(state.content).toBeNull();
134
+ expect(state.isLoading).toBe(false);
135
+ expect(state.loadedData).toBeUndefined();
136
+ expect(state.error).toBeInstanceOf(Error);
137
+ expect(state.error?.message).toContain("stable object reference");
138
+ }
139
+
140
+ describe("usePreviewVariantRuntime", () => {
141
+ beforeEach(() => {
142
+ hookHarness.cursor = 0;
143
+ hookHarness.renderRequested = false;
144
+ hookHarness.pendingEffects.length = 0;
145
+ hookHarness.slots.length = 0;
146
+ hookHarness.updates.length = 0;
147
+ });
148
+
149
+ it("enters loading state while resolving a variant without loaders", async () => {
150
+ const variant = { render: () => null };
151
+ const initialState = renderHook(() => usePreviewVariantRuntime({ variant }));
152
+
153
+ expect(initialState).toMatchObject({
154
+ content: null,
155
+ isLoading: true,
156
+ error: null,
157
+ });
158
+
159
+ expect(hookHarness.updates[0]).toMatchObject({
160
+ state: {
161
+ content: null,
162
+ isLoading: true,
163
+ error: null,
164
+ },
165
+ });
166
+
167
+ await flushResolution();
168
+ const settledState = renderHook(() => usePreviewVariantRuntime({ variant }));
169
+
170
+ expect(settledState).toMatchObject({
171
+ content: null,
172
+ isLoading: false,
173
+ error: null,
174
+ });
175
+ });
176
+
177
+ it("settles when callers recreate equivalent inline loadedData", async () => {
178
+ const variant = {
179
+ render: (options?: { loadedData?: Record<string, unknown> }) =>
180
+ (options?.loadedData?.user as { profile?: { label?: string } } | undefined)?.profile?.label,
181
+ };
182
+
183
+ const state = await settleHook(() =>
184
+ usePreviewVariantRuntime({
185
+ variant,
186
+ loadedData: { user: { profile: { label: "from-host" } } },
187
+ })
188
+ );
189
+
190
+ expect(state).toEqual({
191
+ content: "from-host",
192
+ isLoading: false,
193
+ error: null,
194
+ loadedData: { user: { profile: { label: "from-host" } } },
195
+ });
196
+ expect(hookHarness.renderRequested).toBe(false);
197
+ });
198
+
199
+ it("re-resolves when a nested inline loadedData value changes", async () => {
200
+ const variant = {
201
+ render: (options?: { loadedData?: Record<string, unknown> }) =>
202
+ (options?.loadedData?.user as { label?: string } | undefined)?.label,
203
+ };
204
+ let label = "before";
205
+ const render = () => usePreviewVariantRuntime({ variant, loadedData: { user: { label } } });
206
+
207
+ expect(await settleHook(render)).toMatchObject({ content: "before", isLoading: false });
208
+
209
+ label = "after";
210
+ expect(renderHook(render)).toMatchObject({ content: null, isLoading: true });
211
+ await flushResolution();
212
+ expect(await settleHook(render)).toMatchObject({ content: "after", isLoading: false });
213
+ });
214
+
215
+ it("compares recreated cyclic loadedData without recursing forever", async () => {
216
+ const variant = { render: () => "settled" };
217
+ const render = () => {
218
+ const node: Record<string, unknown> = { label: "same" };
219
+ node.self = node;
220
+ return usePreviewVariantRuntime({ variant, loadedData: { node } });
221
+ };
222
+
223
+ expect(await settleHook(render)).toMatchObject({ content: "settled", isLoading: false });
224
+ expect(hookHarness.renderRequested).toBe(false);
225
+ });
226
+
227
+ it("fails closed for recreated loadedData above the comparison-count bound", async () => {
228
+ const stableLoadedData = makeWideLoadedData("same");
229
+ let recreateLoadedData = false;
230
+ const variant = {
231
+ render: (options?: { loadedData?: Record<string, unknown> }) => options?.loadedData?.version,
232
+ };
233
+ const render = () =>
234
+ usePreviewVariantRuntime({
235
+ variant,
236
+ loadedData: recreateLoadedData ? makeWideLoadedData("same") : stableLoadedData,
237
+ });
238
+
239
+ expect(await settleHook(render)).toMatchObject({ content: "same", isLoading: false });
240
+
241
+ recreateLoadedData = true;
242
+ expectLoadedDataComparisonError(await settleHook(render));
243
+ expect(hookHarness.renderRequested).toBe(false);
244
+ });
245
+
246
+ it("fails closed for recreated loadedData above the comparison-depth bound", async () => {
247
+ const stableLoadedData = makeDeepLoadedData(40);
248
+ let recreateLoadedData = false;
249
+ const variant = { render: () => "deep-settled" };
250
+ const render = () =>
251
+ usePreviewVariantRuntime({
252
+ variant,
253
+ loadedData: recreateLoadedData ? makeDeepLoadedData(40) : stableLoadedData,
254
+ });
255
+
256
+ expect(await settleHook(render)).toMatchObject({ content: "deep-settled", isLoading: false });
257
+
258
+ recreateLoadedData = true;
259
+ expectLoadedDataComparisonError(await settleHook(render));
260
+ expect(hookHarness.renderRequested).toBe(false);
261
+ });
262
+
263
+ it("fails closed before accepting a stable Date identity change", async () => {
264
+ const variant = {
265
+ render: (options?: { loadedData?: Record<string, unknown> }) =>
266
+ (options?.loadedData?.createdAt as Date | undefined)?.getTime(),
267
+ };
268
+ let loadedData = { createdAt: new Date(0) };
269
+ const render = () => usePreviewVariantRuntime({ variant, loadedData });
270
+
271
+ expect(await settleHook(render)).toMatchObject({ content: 0, isLoading: false, error: null });
272
+
273
+ loadedData = { createdAt: new Date(1) };
274
+ expectLoadedDataComparisonError(renderHook(render));
275
+ expect(await settleHook(render)).toMatchObject({ content: 1, isLoading: false, error: null });
276
+ });
277
+
278
+ it("fails closed before accepting a stable function identity change", async () => {
279
+ const variant = {
280
+ render: (options?: { loadedData?: Record<string, unknown> }) =>
281
+ (options?.loadedData?.getValue as (() => number) | undefined)?.(),
282
+ };
283
+ let loadedData = { getValue: () => 1 };
284
+ const render = () => usePreviewVariantRuntime({ variant, loadedData });
285
+
286
+ expect(await settleHook(render)).toMatchObject({ content: 1, isLoading: false, error: null });
287
+
288
+ loadedData = { getValue: () => 2 };
289
+ expectLoadedDataComparisonError(renderHook(render));
290
+ expect(await settleHook(render)).toMatchObject({ content: 2, isLoading: false, error: null });
291
+ });
292
+
293
+ it("fails closed before accepting a stable deep-leaf change beyond the depth bound", async () => {
294
+ const variant = {
295
+ render: (options?: { loadedData?: Record<string, unknown> }) =>
296
+ readDeepLabel(options?.loadedData),
297
+ };
298
+ let loadedData = makeDeepLoadedData(40, "before");
299
+ const render = () => usePreviewVariantRuntime({ variant, loadedData });
300
+
301
+ expect(await settleHook(render)).toMatchObject({
302
+ content: "before",
303
+ isLoading: false,
304
+ error: null,
305
+ });
306
+
307
+ loadedData = makeDeepLoadedData(40, "after");
308
+ expectLoadedDataComparisonError(renderHook(render));
309
+ expect(await settleHook(render)).toMatchObject({
310
+ content: "after",
311
+ isLoading: false,
312
+ error: null,
313
+ });
314
+ });
315
+ });
@@ -14,11 +14,8 @@ describe("executeVariantLoaders", () => {
14
14
 
15
15
  it("merges loader results and applies host loadedData last", async () => {
16
16
  const result = await executeVariantLoaders(
17
- [
18
- async () => ({ count: 1, fromFirst: true }),
19
- async () => ({ count: 2, fromSecond: true }),
20
- ],
21
- { count: 3, fromHost: true },
17
+ [async () => ({ count: 1, fromFirst: true }), async () => ({ count: 2, fromSecond: true })],
18
+ { count: 3, fromHost: true }
22
19
  );
23
20
 
24
21
  expect(result).toEqual({
@@ -43,6 +40,21 @@ describe("resolvePreviewRuntimeState", () => {
43
40
  expect(state.content).toBe("Preview content");
44
41
  });
45
42
 
43
+ it("settles a null render without remaining in a loading state", async () => {
44
+ const variant: PreviewVariantLike = {
45
+ render: () => null,
46
+ };
47
+
48
+ const state = await resolvePreviewRuntimeState({ variant });
49
+
50
+ expect(state).toEqual({
51
+ content: null,
52
+ isLoading: false,
53
+ error: null,
54
+ loadedData: undefined,
55
+ });
56
+ });
57
+
46
58
  it("renders with successful loaders", async () => {
47
59
  const variant: PreviewVariantLike = {
48
60
  loaders: [async () => ({ label: "from-loader" })],
@@ -58,7 +70,11 @@ describe("resolvePreviewRuntimeState", () => {
58
70
 
59
71
  it("returns an error when a loader rejects", async () => {
60
72
  const variant: PreviewVariantLike = {
61
- loaders: [async () => { throw new Error("loader failed"); }],
73
+ loaders: [
74
+ async () => {
75
+ throw new Error("loader failed");
76
+ },
77
+ ],
62
78
  render: () => "should not render",
63
79
  };
64
80
 
@@ -83,7 +99,11 @@ describe("resolvePreviewRuntimeState", () => {
83
99
 
84
100
  it("normalizes non-Error throwables", async () => {
85
101
  const variant: PreviewVariantLike = {
86
- loaders: [async () => { throw "string-failure"; }],
102
+ loaders: [
103
+ async () => {
104
+ throw "string-failure";
105
+ },
106
+ ],
87
107
  render: () => "never",
88
108
  };
89
109
 
@@ -93,7 +113,9 @@ describe("resolvePreviewRuntimeState", () => {
93
113
  });
94
114
 
95
115
  it("uses host data as override input in render options", async () => {
96
- const renderSpy = vi.fn((options?: { loadedData?: Record<string, unknown> }) => options?.loadedData);
116
+ const renderSpy = vi.fn(
117
+ (options?: { loadedData?: Record<string, unknown> }) => options?.loadedData
118
+ );
97
119
  const variant: PreviewVariantLike = {
98
120
  loaders: [async () => ({ count: 1 })],
99
121
  render: renderSpy,
@@ -1,31 +1,208 @@
1
- import { describe, expect, it } from 'vitest';
1
+ import { readFileSync } from "node:fs";
2
+ import { dirname, join } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import Ajv2020 from "ajv/dist/2020.js";
5
+ import addFormats from "ajv-formats";
6
+ import { describe, expect, it } from "vitest";
2
7
 
3
- import { observedUsagePropSchema } from '../schema.js';
8
+ import {
9
+ fragmentDefinitionV2Schema,
10
+ fragmentDefinitionV3BodySchema,
11
+ fragmentDefinitionV3Schema,
12
+ fragmentGeneratedSchema,
13
+ fragmentProvenanceSchema,
14
+ fragmentsConfigSchema,
15
+ observedUsagePropSchema,
16
+ propAnnotationSchema,
17
+ } from "../schema.js";
18
+ import type { FragmentGenerated, FragmentProvenance } from "../types.js";
19
+ import type { FragmentGenerated as CompiledFragmentGenerated } from "../compiled-types/index.js";
4
20
 
5
- describe('observedUsagePropSchema', () => {
6
- it('accepts structural JSX children observations', () => {
21
+ const testsDir = dirname(fileURLToPath(import.meta.url));
22
+ const repoRoot = join(testsDir, "../../../..");
23
+
24
+ const publicV3Schema = JSON.parse(
25
+ readFileSync(join(repoRoot, "apps/docs/public/schemas/fragment.v3.json"), "utf8")
26
+ ) as Record<string, unknown>;
27
+
28
+ /**
29
+ * The v3 parity corpus. Owned by Core (A06-04 is the parity owner) and read
30
+ * verbatim by the compiler's loader test, so "the loader accepts what the
31
+ * published schema accepts" is one fixture set, not two that can drift. It
32
+ * lives under `test-fixtures/` because `src/` ships inside the package.
33
+ */
34
+ const parityFixtures = JSON.parse(
35
+ readFileSync(join(testsDir, "../../test-fixtures/fragment-v3-parity.json"), "utf8")
36
+ ) as {
37
+ validBody: Record<string, unknown>;
38
+ validTimestamps: string[];
39
+ invalidTimestamps: string[];
40
+ invalidBodies: Array<{ name: string; body: Record<string, unknown> }>;
41
+ };
42
+
43
+ const publicSchemaValidator = new Ajv2020({ allErrors: true, strict: false });
44
+ addFormats(publicSchemaValidator);
45
+ const validatePublicV3 = publicSchemaValidator.compile(publicV3Schema);
46
+
47
+ function publicV3Accepts(value: unknown): boolean {
48
+ return validatePublicV3(value) as boolean;
49
+ }
50
+
51
+ const scannedAuthoringFragment = { source: "scan" } satisfies FragmentGenerated;
52
+ const scannedCompiledFragment = { source: "scan" } satisfies CompiledFragmentGenerated;
53
+ const verifiedScannedFragment = {
54
+ source: "scan",
55
+ verified: true,
56
+ } satisfies FragmentGenerated & CompiledFragmentGenerated;
57
+ const provenanceWithoutVerification = { source: "scan" } satisfies FragmentProvenance;
58
+
59
+ describe("fragmentGeneratedSchema", () => {
60
+ it("keeps scan provenance aligned across runtime and public types", () => {
61
+ expect(fragmentGeneratedSchema.parse(scannedAuthoringFragment).source).toBe("scan");
62
+ expect(fragmentGeneratedSchema.parse(scannedCompiledFragment).source).toBe("scan");
63
+ });
64
+
65
+ it("preserves verification accepted by both public generated-fragment types", () => {
66
+ expect(fragmentGeneratedSchema.parse(verifiedScannedFragment)).toEqual({
67
+ source: "scan",
68
+ verified: true,
69
+ });
70
+ });
71
+ });
72
+
73
+ describe("fragmentProvenanceSchema", () => {
74
+ it("keeps verification optional across runtime and public types", () => {
75
+ expect(fragmentProvenanceSchema.parse(provenanceWithoutVerification)).toEqual({
76
+ source: "scan",
77
+ });
78
+ });
79
+ });
80
+
81
+ describe("fragmentsConfigSchema", () => {
82
+ it("does not expose the retired storybook filter option", () => {
83
+ expect(Object.hasOwn(fragmentsConfigSchema.shape, "storybook")).toBe(false);
84
+ });
85
+ });
86
+
87
+ describe("observedUsagePropSchema", () => {
88
+ it("accepts structural JSX children observations", () => {
7
89
  expect(
8
90
  observedUsagePropSchema.safeParse({
9
- name: 'children',
10
- kind: 'jsx',
11
- }).success,
91
+ name: "children",
92
+ kind: "jsx",
93
+ }).success
12
94
  ).toBe(true);
13
95
  });
14
96
 
15
- it('accepts boolean and null prop observations', () => {
97
+ it("accepts boolean and null prop observations", () => {
16
98
  expect(
17
99
  observedUsagePropSchema.safeParse({
18
- name: 'disabled',
19
- kind: 'boolean',
100
+ name: "disabled",
101
+ kind: "boolean",
20
102
  value: true,
21
- }).success,
103
+ }).success
22
104
  ).toBe(true);
23
105
  expect(
24
106
  observedUsagePropSchema.safeParse({
25
- name: 'tone',
26
- kind: 'null',
107
+ name: "tone",
108
+ kind: "null",
27
109
  value: null,
28
- }).success,
110
+ }).success
29
111
  ).toBe(true);
30
112
  });
31
113
  });
114
+
115
+ describe("fragmentDefinitionV2Schema", () => {
116
+ it("accepts migrated provenance with verification fields", () => {
117
+ const result = fragmentDefinitionV2Schema.safeParse({
118
+ component: () => null,
119
+ meta: { name: "Button", description: "A button", category: "actions" },
120
+ guidance: { when: [], whenNot: [] },
121
+ props: {},
122
+ examples: [],
123
+ _provenance: {
124
+ source: "migrated",
125
+ verified: true,
126
+ frameworkSupport: "native",
127
+ sourceHash: "abc123",
128
+ },
129
+ });
130
+
131
+ expect(result.success).toBe(true);
132
+ if (result.success) {
133
+ expect(result.data._provenance).toEqual({
134
+ source: "migrated",
135
+ verified: true,
136
+ frameworkSupport: "native",
137
+ sourceHash: "abc123",
138
+ });
139
+ }
140
+ });
141
+ });
142
+
143
+ describe("fragmentDefinitionV3Schema parity with public schema (A06-04)", () => {
144
+ const { validBody, validTimestamps, invalidTimestamps, invalidBodies } = parityFixtures;
145
+
146
+ it("accepts the same valid fixture in Zod and the published schema", () => {
147
+ expect(fragmentDefinitionV3BodySchema.safeParse(validBody).success).toBe(true);
148
+ expect(
149
+ fragmentDefinitionV3Schema.safeParse({
150
+ ...validBody,
151
+ component: () => null,
152
+ governance: [],
153
+ }).success
154
+ ).toBe(true);
155
+ expect(publicV3Accepts(validBody)).toBe(true);
156
+ });
157
+
158
+ it.each(validTimestamps)(
159
+ "accepts RFC 3339 provenance timestamp %s in Zod and the published schema",
160
+ (timestamp) => {
161
+ const withTimestamp = {
162
+ ...validBody,
163
+ _provenance: { ...(validBody._provenance as object), timestamp },
164
+ };
165
+ expect(fragmentDefinitionV3BodySchema.safeParse(withTimestamp).success).toBe(true);
166
+ expect(publicV3Accepts(withTimestamp)).toBe(true);
167
+ }
168
+ );
169
+
170
+ it.each(invalidTimestamps)(
171
+ "rejects invalid provenance timestamp %s in Zod and the published schema",
172
+ (timestamp) => {
173
+ const withTimestamp = {
174
+ ...validBody,
175
+ _provenance: { ...(validBody._provenance as object), timestamp },
176
+ };
177
+ expect(fragmentDefinitionV3BodySchema.safeParse(withTimestamp).success).toBe(false);
178
+ expect(publicV3Accepts(withTimestamp)).toBe(false);
179
+ }
180
+ );
181
+
182
+ it.each(invalidBodies.map(({ name, body }) => [name, body] as const))(
183
+ "rejects %s in both Zod and the published schema",
184
+ (_name, invalidBody) => {
185
+ expect(fragmentDefinitionV3BodySchema.safeParse(invalidBody).success).toBe(false);
186
+ expect(publicV3Accepts(invalidBody)).toBe(false);
187
+ }
188
+ );
189
+
190
+ it("rejects prop annotations that redefine type/default at the leaf schema", () => {
191
+ expect(
192
+ propAnnotationSchema.safeParse({ description: "x", type: "enum", default: "primary" }).success
193
+ ).toBe(false);
194
+ });
195
+
196
+ it("keeps callable govern authoring out of the JSON interchange projection", () => {
197
+ const withGovern = { ...validBody, govern: () => [] };
198
+ expect(fragmentDefinitionV3BodySchema.safeParse(withGovern).success).toBe(true);
199
+ expect(publicV3Accepts(withGovern)).toBe(false);
200
+ });
201
+
202
+ it("publishes the versioned schema document with the usefragments $id", () => {
203
+ expect(publicV3Schema).toMatchObject({
204
+ $id: "https://usefragments.com/schemas/fragment.v3.json",
205
+ });
206
+ expect(publicV3Schema).toHaveProperty("$defs.state.not.required", ["code"]);
207
+ });
208
+ });