@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,9 +1,6 @@
|
|
|
1
1
|
import { describe, expect, it } from "vitest";
|
|
2
2
|
|
|
3
|
-
import {
|
|
4
|
-
CompiledFragmentsFileValidationError,
|
|
5
|
-
parseCompiledFragmentsFile,
|
|
6
|
-
} from "./index.js";
|
|
3
|
+
import { CompiledFragmentsFileValidationError, parseCompiledFragmentsFile } from "./index.js";
|
|
7
4
|
|
|
8
5
|
const validCatalog = {
|
|
9
6
|
version: "1.0.0",
|
|
@@ -24,6 +21,64 @@ describe("parseCompiledFragmentsFile", () => {
|
|
|
24
21
|
expect(parseCompiledFragmentsFile(JSON.stringify(validCatalog))).toMatchObject(validCatalog);
|
|
25
22
|
});
|
|
26
23
|
|
|
24
|
+
it("accepts legacy optional omissions and preserves additive future fields without cloning", () => {
|
|
25
|
+
const legacyWithFutureData = {
|
|
26
|
+
...validCatalog,
|
|
27
|
+
futureRoot: { schemaVersion: 2 },
|
|
28
|
+
fragments: {
|
|
29
|
+
Button: {
|
|
30
|
+
...validCatalog.fragments.Button,
|
|
31
|
+
futureFragment: { interactionModel: "press" },
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const parsed = parseCompiledFragmentsFile(legacyWithFutureData);
|
|
37
|
+
|
|
38
|
+
expect(parsed).toBe(legacyWithFutureData);
|
|
39
|
+
expect(parsed).toHaveProperty("futureRoot.schemaVersion", 2);
|
|
40
|
+
expect(parsed).toHaveProperty("fragments.Button.futureFragment.interactionModel", "press");
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("round-trips a valid current artifact without reinterpretation", () => {
|
|
44
|
+
const current = {
|
|
45
|
+
...validCatalog,
|
|
46
|
+
generatorVersion: "sha256:generator",
|
|
47
|
+
buildInputs: ["src/Button.fragment.ts"],
|
|
48
|
+
packageName: "@acme/ui",
|
|
49
|
+
fragments: {
|
|
50
|
+
Button: {
|
|
51
|
+
...validCatalog.fragments.Button,
|
|
52
|
+
meta: {
|
|
53
|
+
...validCatalog.fragments.Button.meta,
|
|
54
|
+
aliases: ["Action"],
|
|
55
|
+
},
|
|
56
|
+
guidance: validCatalog.fragments.Button.usage,
|
|
57
|
+
preview: { theme: "dark", dynamicRegions: ["[data-clock]"] },
|
|
58
|
+
matrix: { axes: { tone: ["neutral", "danger"], density: "auto" } },
|
|
59
|
+
design: { figmaNode: "node:1" },
|
|
60
|
+
tokens: ["--color-action"],
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
graph: {
|
|
64
|
+
nodes: [{ name: "Button", category: "Actions", status: "stable" }],
|
|
65
|
+
edges: [],
|
|
66
|
+
health: {
|
|
67
|
+
orphans: ["Button"],
|
|
68
|
+
hubs: [],
|
|
69
|
+
compositionCoverage: 0,
|
|
70
|
+
connectedComponents: [["Button"]],
|
|
71
|
+
averageDegree: 0,
|
|
72
|
+
nodeCount: 1,
|
|
73
|
+
edgeCount: 0,
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
const serialized = JSON.stringify(current);
|
|
78
|
+
|
|
79
|
+
expect(JSON.stringify(parseCompiledFragmentsFile(serialized))).toBe(serialized);
|
|
80
|
+
});
|
|
81
|
+
|
|
27
82
|
it("reports invalid fragment paths instead of allowing command-specific crashes", () => {
|
|
28
83
|
expect(() =>
|
|
29
84
|
parseCompiledFragmentsFile(
|
|
@@ -44,4 +99,90 @@ describe("parseCompiledFragmentsFile", () => {
|
|
|
44
99
|
)
|
|
45
100
|
).toThrow("fragments.Button.meta.name");
|
|
46
101
|
});
|
|
102
|
+
|
|
103
|
+
it("rejects malformed nested and compact graph shapes with sorted deterministic issues", () => {
|
|
104
|
+
let thrown: unknown;
|
|
105
|
+
try {
|
|
106
|
+
parseCompiledFragmentsFile(
|
|
107
|
+
{
|
|
108
|
+
...validCatalog,
|
|
109
|
+
fragments: {
|
|
110
|
+
Button: {
|
|
111
|
+
...validCatalog.fragments.Button,
|
|
112
|
+
meta: { name: "Button", description: false, category: "Actions" },
|
|
113
|
+
props: {
|
|
114
|
+
tone: { type: 1, description: "Tone", values: ["neutral", 2] },
|
|
115
|
+
},
|
|
116
|
+
variants: [{ name: "Default", description: 2 }],
|
|
117
|
+
},
|
|
118
|
+
},
|
|
119
|
+
graph: {
|
|
120
|
+
nodes: [{ name: "Button", category: "Actions", status: "stable" }],
|
|
121
|
+
edges: [{ s: "Button", t: "Card", ty: "renders", w: "heavy", p: "usage" }],
|
|
122
|
+
health: {
|
|
123
|
+
orphans: [],
|
|
124
|
+
hubs: [],
|
|
125
|
+
compositionCoverage: 0,
|
|
126
|
+
connectedComponents: [["Button"]],
|
|
127
|
+
averageDegree: 0,
|
|
128
|
+
nodeCount: 1,
|
|
129
|
+
edgeCount: 1,
|
|
130
|
+
},
|
|
131
|
+
},
|
|
132
|
+
},
|
|
133
|
+
"/repo/fragments.json"
|
|
134
|
+
);
|
|
135
|
+
} catch (error) {
|
|
136
|
+
thrown = error;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
expect(thrown).toBeInstanceOf(CompiledFragmentsFileValidationError);
|
|
140
|
+
expect((thrown as CompiledFragmentsFileValidationError).issues).toEqual([
|
|
141
|
+
"fragments.Button.meta.description must be a string",
|
|
142
|
+
"fragments.Button.props.tone.type must be a string",
|
|
143
|
+
"fragments.Button.props.tone.values must be an array of strings",
|
|
144
|
+
"fragments.Button.variants.0.description must be a string",
|
|
145
|
+
"graph.edges.0.w must be a finite number",
|
|
146
|
+
]);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it("rejects duplicate semantic fragment, variant, and graph-node identities", () => {
|
|
150
|
+
expect(() =>
|
|
151
|
+
parseCompiledFragmentsFile({
|
|
152
|
+
...validCatalog,
|
|
153
|
+
fragments: {
|
|
154
|
+
PrimaryButton: {
|
|
155
|
+
...validCatalog.fragments.Button,
|
|
156
|
+
variants: [
|
|
157
|
+
{ name: "Default", description: "First" },
|
|
158
|
+
{ name: "Default", description: "Second" },
|
|
159
|
+
],
|
|
160
|
+
},
|
|
161
|
+
SecondaryButton: validCatalog.fragments.Button,
|
|
162
|
+
},
|
|
163
|
+
graph: {
|
|
164
|
+
nodes: [
|
|
165
|
+
{ name: "Button", category: "Actions", status: "stable" },
|
|
166
|
+
{ name: "Button", category: "Actions", status: "stable" },
|
|
167
|
+
],
|
|
168
|
+
edges: [],
|
|
169
|
+
health: {
|
|
170
|
+
orphans: [],
|
|
171
|
+
hubs: [],
|
|
172
|
+
compositionCoverage: 0,
|
|
173
|
+
connectedComponents: [],
|
|
174
|
+
averageDegree: 0,
|
|
175
|
+
nodeCount: 2,
|
|
176
|
+
edgeCount: 0,
|
|
177
|
+
},
|
|
178
|
+
},
|
|
179
|
+
})
|
|
180
|
+
).toThrow(/duplicates/);
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it("uses a runtime-independent invalid JSON diagnostic", () => {
|
|
184
|
+
expect(() => parseCompiledFragmentsFile('{"fragments":', "memory.json")).toThrow(
|
|
185
|
+
"Invalid compiled Fragments catalog at memory.json: invalid JSON"
|
|
186
|
+
);
|
|
187
|
+
});
|
|
47
188
|
});
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* files instead of `.fragment.tsx`. Maps trivially to CompiledFragment.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import { z } from
|
|
8
|
+
import { z } from "zod";
|
|
9
9
|
|
|
10
10
|
// ---------------------------------------------------------------------------
|
|
11
11
|
// Component Contract interface
|
|
@@ -20,10 +20,10 @@ export interface ComponentContract {
|
|
|
20
20
|
description: string;
|
|
21
21
|
category: string;
|
|
22
22
|
tags?: string[];
|
|
23
|
-
status?:
|
|
23
|
+
status?: "stable" | "beta" | "deprecated" | "experimental";
|
|
24
24
|
|
|
25
25
|
/** Framework hint (for extraction and documentation) */
|
|
26
|
-
framework?:
|
|
26
|
+
framework?: "react" | "vue" | "svelte" | "web-components" | "angular";
|
|
27
27
|
|
|
28
28
|
/** External npm packages required by this component (displayed in docs Setup section) */
|
|
29
29
|
dependencies?: Array<{
|
|
@@ -42,14 +42,17 @@ export interface ComponentContract {
|
|
|
42
42
|
propsSummary: string[];
|
|
43
43
|
|
|
44
44
|
/** Component props schema */
|
|
45
|
-
props: Record<
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
45
|
+
props: Record<
|
|
46
|
+
string,
|
|
47
|
+
{
|
|
48
|
+
type: string;
|
|
49
|
+
values?: string[];
|
|
50
|
+
default?: unknown;
|
|
51
|
+
description: string;
|
|
52
|
+
required?: boolean;
|
|
53
|
+
constraints?: string[];
|
|
54
|
+
}
|
|
55
|
+
>;
|
|
53
56
|
|
|
54
57
|
/** Usage guidelines */
|
|
55
58
|
usage: {
|
|
@@ -70,7 +73,14 @@ export interface ComponentContract {
|
|
|
70
73
|
/** Relations to other components */
|
|
71
74
|
relations?: Array<{
|
|
72
75
|
component: string;
|
|
73
|
-
relationship:
|
|
76
|
+
relationship:
|
|
77
|
+
| "alternative"
|
|
78
|
+
| "parent"
|
|
79
|
+
| "child"
|
|
80
|
+
| "sibling"
|
|
81
|
+
| "composition"
|
|
82
|
+
| "complementary"
|
|
83
|
+
| "used-by";
|
|
74
84
|
note: string;
|
|
75
85
|
}>;
|
|
76
86
|
|
|
@@ -81,11 +91,14 @@ export interface ComponentContract {
|
|
|
81
91
|
a11yRules?: string[];
|
|
82
92
|
bans?: Array<{ pattern: string; message: string }>;
|
|
83
93
|
/** Sub-component slot metadata for compound components */
|
|
84
|
-
compoundChildren?: Record<
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
94
|
+
compoundChildren?: Record<
|
|
95
|
+
string,
|
|
96
|
+
{
|
|
97
|
+
required?: boolean;
|
|
98
|
+
accepts?: string[];
|
|
99
|
+
description?: string;
|
|
100
|
+
}
|
|
101
|
+
>;
|
|
89
102
|
/** Canonical JSX usage examples */
|
|
90
103
|
canonicalUsage?: string[];
|
|
91
104
|
/** Per-component performance budget override in bytes (gzipped) */
|
|
@@ -94,7 +107,7 @@ export interface ComponentContract {
|
|
|
94
107
|
|
|
95
108
|
/** AI metadata for compound component generation */
|
|
96
109
|
ai?: {
|
|
97
|
-
compositionPattern?:
|
|
110
|
+
compositionPattern?: "compound" | "simple" | "controlled" | "wrapper";
|
|
98
111
|
subComponents?: string[];
|
|
99
112
|
requiredChildren?: string[];
|
|
100
113
|
commonPatterns?: string[];
|
|
@@ -106,14 +119,14 @@ export interface ComponentContract {
|
|
|
106
119
|
wrapperModule?: string;
|
|
107
120
|
wrapperExport?: string;
|
|
108
121
|
css?: string[];
|
|
109
|
-
theme?:
|
|
122
|
+
theme?: "light" | "dark";
|
|
110
123
|
};
|
|
111
124
|
|
|
112
125
|
/** Provenance tracking — required for verification */
|
|
113
126
|
provenance: {
|
|
114
|
-
source:
|
|
127
|
+
source: "manual" | "extracted" | "merged" | "migrated";
|
|
115
128
|
verified: boolean;
|
|
116
|
-
frameworkSupport?:
|
|
129
|
+
frameworkSupport?: "native" | "manual-only";
|
|
117
130
|
sourceHash?: string;
|
|
118
131
|
extractedAt?: string;
|
|
119
132
|
};
|
|
@@ -124,11 +137,14 @@ export interface ComponentContract {
|
|
|
124
137
|
/** Figma mappings (same DSL as .fragment.tsx but in JSON form) */
|
|
125
138
|
figma?: {
|
|
126
139
|
nodeUrl?: string;
|
|
127
|
-
propMappings?: Record<
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
140
|
+
propMappings?: Record<
|
|
141
|
+
string,
|
|
142
|
+
{
|
|
143
|
+
type: "string" | "boolean" | "enum" | "instance" | "children" | "textContent";
|
|
144
|
+
figmaProperty: string;
|
|
145
|
+
values?: Record<string, string>;
|
|
146
|
+
}
|
|
147
|
+
>;
|
|
132
148
|
};
|
|
133
149
|
}
|
|
134
150
|
|
|
@@ -161,7 +177,15 @@ const contractExampleSchema = z.object({
|
|
|
161
177
|
|
|
162
178
|
const contractRelationSchema = z.object({
|
|
163
179
|
component: z.string(),
|
|
164
|
-
relationship: z.enum([
|
|
180
|
+
relationship: z.enum([
|
|
181
|
+
"alternative",
|
|
182
|
+
"parent",
|
|
183
|
+
"child",
|
|
184
|
+
"sibling",
|
|
185
|
+
"composition",
|
|
186
|
+
"complementary",
|
|
187
|
+
"used-by",
|
|
188
|
+
]),
|
|
165
189
|
note: z.string(),
|
|
166
190
|
});
|
|
167
191
|
|
|
@@ -169,21 +193,30 @@ const contractContractSchema = z.object({
|
|
|
169
193
|
propsSummary: z.array(z.string()).optional(),
|
|
170
194
|
scenarioTags: z.array(z.string()).optional(),
|
|
171
195
|
a11yRules: z.array(z.string()).optional(),
|
|
172
|
-
bans: z
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
196
|
+
bans: z
|
|
197
|
+
.array(
|
|
198
|
+
z.object({
|
|
199
|
+
pattern: z.string(),
|
|
200
|
+
message: z.string(),
|
|
201
|
+
})
|
|
202
|
+
)
|
|
203
|
+
.optional(),
|
|
204
|
+
compoundChildren: z
|
|
205
|
+
.record(
|
|
206
|
+
z.string(),
|
|
207
|
+
z.object({
|
|
208
|
+
required: z.boolean().optional(),
|
|
209
|
+
accepts: z.array(z.string()).optional(),
|
|
210
|
+
description: z.string().optional(),
|
|
211
|
+
})
|
|
212
|
+
)
|
|
213
|
+
.optional(),
|
|
181
214
|
canonicalUsage: z.array(z.string()).optional(),
|
|
182
|
-
performanceBudget: z.number().optional(),
|
|
215
|
+
performanceBudget: z.number().positive().finite().optional(),
|
|
183
216
|
});
|
|
184
217
|
|
|
185
218
|
const contractAiSchema = z.object({
|
|
186
|
-
compositionPattern: z.enum([
|
|
219
|
+
compositionPattern: z.enum(["compound", "simple", "controlled", "wrapper"]).optional(),
|
|
187
220
|
subComponents: z.array(z.string()).optional(),
|
|
188
221
|
requiredChildren: z.array(z.string()).optional(),
|
|
189
222
|
commonPatterns: z.array(z.string()).optional(),
|
|
@@ -194,24 +227,29 @@ const contractPreviewSchema = z.object({
|
|
|
194
227
|
wrapperModule: z.string().optional(),
|
|
195
228
|
wrapperExport: z.string().optional(),
|
|
196
229
|
css: z.array(z.string()).optional(),
|
|
197
|
-
theme: z.enum([
|
|
230
|
+
theme: z.enum(["light", "dark"]).optional(),
|
|
198
231
|
});
|
|
199
232
|
|
|
200
233
|
const contractProvenanceSchema = z.object({
|
|
201
|
-
source: z.enum([
|
|
234
|
+
source: z.enum(["manual", "extracted", "merged", "migrated"]),
|
|
202
235
|
verified: z.boolean(),
|
|
203
|
-
frameworkSupport: z.enum([
|
|
236
|
+
frameworkSupport: z.enum(["native", "manual-only"]).optional(),
|
|
204
237
|
sourceHash: z.string().optional(),
|
|
205
238
|
extractedAt: z.string().optional(),
|
|
206
239
|
});
|
|
207
240
|
|
|
208
241
|
const contractFigmaSchema = z.object({
|
|
209
242
|
nodeUrl: z.string().optional(),
|
|
210
|
-
propMappings: z
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
243
|
+
propMappings: z
|
|
244
|
+
.record(
|
|
245
|
+
z.string(),
|
|
246
|
+
z.object({
|
|
247
|
+
type: z.enum(["string", "boolean", "enum", "instance", "children", "textContent"]),
|
|
248
|
+
figmaProperty: z.string(),
|
|
249
|
+
values: z.record(z.string(), z.string()).optional(),
|
|
250
|
+
})
|
|
251
|
+
)
|
|
252
|
+
.optional(),
|
|
215
253
|
});
|
|
216
254
|
|
|
217
255
|
export const componentContractSchema = z.object({
|
|
@@ -220,13 +258,17 @@ export const componentContractSchema = z.object({
|
|
|
220
258
|
description: z.string(),
|
|
221
259
|
category: z.string(),
|
|
222
260
|
tags: z.array(z.string()).optional(),
|
|
223
|
-
status: z.enum([
|
|
224
|
-
framework: z.enum([
|
|
225
|
-
dependencies: z
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
261
|
+
status: z.enum(["stable", "beta", "deprecated", "experimental"]).optional(),
|
|
262
|
+
framework: z.enum(["react", "vue", "svelte", "web-components", "angular"]).optional(),
|
|
263
|
+
dependencies: z
|
|
264
|
+
.array(
|
|
265
|
+
z.object({
|
|
266
|
+
name: z.string(),
|
|
267
|
+
version: z.string(),
|
|
268
|
+
reason: z.string().optional(),
|
|
269
|
+
})
|
|
270
|
+
)
|
|
271
|
+
.optional(),
|
|
230
272
|
sourcePath: z.string(),
|
|
231
273
|
exportName: z.string(),
|
|
232
274
|
propsSummary: z.array(z.string()),
|
package/src/composition.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import type { CompiledFragment
|
|
1
|
+
import type { CompiledFragment } from "./compiled-types/index.js";
|
|
2
|
+
import type { RelationshipType } from "./types.js";
|
|
2
3
|
import type { ComponentGraph } from "./graph/index.js";
|
|
3
4
|
import { ComponentGraphEngine } from "./graph/index.js";
|
|
4
5
|
|
|
@@ -50,6 +51,7 @@ export interface CompositionAnalysis {
|
|
|
50
51
|
|
|
51
52
|
const CATEGORY_AFFINITIES: Record<string, string[]> = {
|
|
52
53
|
forms: ["feedback"],
|
|
54
|
+
inputs: ["feedback"],
|
|
53
55
|
actions: ["feedback"],
|
|
54
56
|
};
|
|
55
57
|
|
|
@@ -69,7 +71,7 @@ export function analyzeComposition(
|
|
|
69
71
|
fragments: Record<string, CompiledFragment>,
|
|
70
72
|
componentNames: string[],
|
|
71
73
|
_context?: string,
|
|
72
|
-
options?: { graph?: ComponentGraph }
|
|
74
|
+
options?: { graph?: ComponentGraph }
|
|
73
75
|
): CompositionAnalysis {
|
|
74
76
|
const allNames = new Set(Object.keys(fragments));
|
|
75
77
|
|
|
@@ -193,9 +195,7 @@ export function analyzeComposition(
|
|
|
193
195
|
}
|
|
194
196
|
|
|
195
197
|
// 5. Category gap analysis
|
|
196
|
-
const selectedCategories = new Set(
|
|
197
|
-
components.map((name) => fragments[name].meta.category)
|
|
198
|
-
);
|
|
198
|
+
const selectedCategories = new Set(components.map((name) => fragments[name].meta.category));
|
|
199
199
|
|
|
200
200
|
for (const [category, affinities] of Object.entries(CATEGORY_AFFINITIES)) {
|
|
201
201
|
if (!selectedCategories.has(category)) continue;
|
|
@@ -215,9 +215,7 @@ export function analyzeComposition(
|
|
|
215
215
|
component: candidate,
|
|
216
216
|
reason: `Compositions using "${category}" components often benefit from a "${neededCategory}" component`,
|
|
217
217
|
relationship: "category_gap",
|
|
218
|
-
sourceComponent: components.find(
|
|
219
|
-
(n) => fragments[n].meta.category === category
|
|
220
|
-
)!,
|
|
218
|
+
sourceComponent: components.find((n) => fragments[n].meta.category === category)!,
|
|
221
219
|
});
|
|
222
220
|
suggestedSet.add(candidate);
|
|
223
221
|
}
|
|
@@ -263,11 +261,7 @@ export function analyzeComposition(
|
|
|
263
261
|
.map((e) => (e.source === name ? e.target : e.source));
|
|
264
262
|
|
|
265
263
|
for (const comp of blockComps) {
|
|
266
|
-
if (
|
|
267
|
-
!selectedSet.has(comp) &&
|
|
268
|
-
!suggestedSet.has(comp) &&
|
|
269
|
-
allNames.has(comp)
|
|
270
|
-
) {
|
|
264
|
+
if (!selectedSet.has(comp) && !suggestedSet.has(comp) && allNames.has(comp)) {
|
|
271
265
|
suggestions.push({
|
|
272
266
|
component: comp,
|
|
273
267
|
reason: `"${name}" and "${comp}" are used together in the "${blockName}" block`,
|
package/src/constants.ts
CHANGED
|
@@ -15,9 +15,6 @@ export const BRAND = {
|
|
|
15
15
|
/** Legacy file extension for segments (still supported for migration) */
|
|
16
16
|
legacyFileExtension: ".segment.tsx",
|
|
17
17
|
|
|
18
|
-
/** JSON file extension for compiled output */
|
|
19
|
-
jsonExtension: ".fragment.json",
|
|
20
|
-
|
|
21
18
|
/** Default output file name (e.g., "fragments.json") */
|
|
22
19
|
outFile: "fragments.json",
|
|
23
20
|
|
|
@@ -72,13 +69,13 @@ export const BRAND = {
|
|
|
72
69
|
/** File extension for block definition files */
|
|
73
70
|
blockFileExtension: ".block.ts",
|
|
74
71
|
|
|
75
|
-
/** @deprecated Use blockFileExtension instead */
|
|
76
|
-
recipeFileExtension: ".recipe.ts",
|
|
77
|
-
|
|
78
72
|
/** Vite plugin namespace */
|
|
79
73
|
vitePluginNamespace: "fragments-core-shim",
|
|
80
74
|
} as const;
|
|
81
75
|
|
|
76
|
+
/** Public schema URL for `.fragment` v3 definition bodies. */
|
|
77
|
+
export const FRAGMENT_V3_SCHEMA_URL = "https://usefragments.com/schemas/fragment.v3.json" as const;
|
|
78
|
+
|
|
82
79
|
export type Brand = typeof BRAND;
|
|
83
80
|
|
|
84
81
|
/**
|
|
@@ -61,6 +61,26 @@ describe("canonicalPreimage", () => {
|
|
|
61
61
|
'{"xs":[1,{"y":"q","z":true}]}'
|
|
62
62
|
);
|
|
63
63
|
});
|
|
64
|
+
|
|
65
|
+
it("rejects circular and runtime-only values with deterministic paths", () => {
|
|
66
|
+
const circular: Record<string, unknown> = {};
|
|
67
|
+
circular.self = circular;
|
|
68
|
+
|
|
69
|
+
expect(() => canonicalPreimage(circular)).toThrow("Circular contract value at (root).self");
|
|
70
|
+
expect(() => canonicalPreimage({ nested: () => null })).toThrow(
|
|
71
|
+
"Unsupported contract value at (root).nested: function"
|
|
72
|
+
);
|
|
73
|
+
expect(() => canonicalPreimage({ createdAt: new Date(0) })).toThrow(
|
|
74
|
+
"Unsupported contract object at (root).createdAt: Date"
|
|
75
|
+
);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it("permits a shared acyclic object without treating it as a cycle", () => {
|
|
79
|
+
const shared = { value: 1 };
|
|
80
|
+
expect(canonicalPreimage({ left: shared, right: shared })).toBe(
|
|
81
|
+
'{"left":{"value":1},"right":{"value":1}}'
|
|
82
|
+
);
|
|
83
|
+
});
|
|
64
84
|
});
|
|
65
85
|
|
|
66
86
|
describe("contractHash", () => {
|
package/src/contract/hash.ts
CHANGED
|
@@ -30,8 +30,8 @@
|
|
|
30
30
|
* numbers, but renders `NaN`/`Infinity` as `null` (ambiguous) and does not
|
|
31
31
|
* collapse `-0`. Token values flow into the contract body, so a serializer that
|
|
32
32
|
* silently maps `NaN` to `null` would hash two different inputs to the same
|
|
33
|
-
* preimage. Non-finite numbers get
|
|
34
|
-
* to `0`.
|
|
33
|
+
* preimage. Non-finite numbers get legacy v1 sentinels instead; `-0` collapses
|
|
34
|
+
* to `0`.
|
|
35
35
|
*/
|
|
36
36
|
function normalizeNumber(value: number): string {
|
|
37
37
|
if (Number.isNaN(value)) return '"@num:nan"';
|
|
@@ -48,29 +48,72 @@ function normalizeNumber(value: number): string {
|
|
|
48
48
|
* - `undefined` properties dropped (so optional fields don't perturb the hash)
|
|
49
49
|
* - numbers normalized via {@link normalizeNumber}
|
|
50
50
|
* - `bigint` encoded as its decimal string (JSON cannot represent it natively)
|
|
51
|
+
* - circular, executable, and non-plain runtime values rejected with a stable path
|
|
51
52
|
*
|
|
52
53
|
* This is intentionally a separate serializer from `facts/ids.ts#canonicalJson`:
|
|
53
54
|
* that one targets in-process fact IDs and does not normalize numbers, and we
|
|
54
55
|
* must not change its output (it would silently re-key existing fact IDs).
|
|
55
56
|
*/
|
|
56
57
|
export function canonicalPreimage(value: unknown): string {
|
|
58
|
+
return canonicalPreimageValue(value, new Set<object>(), "(root)");
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Locale-independent lexical order for authority-bearing canonical records.
|
|
63
|
+
*
|
|
64
|
+
* `String#localeCompare` follows the runtime's default locale, so it can order
|
|
65
|
+
* the same Unicode identifiers differently across a developer machine, CI,
|
|
66
|
+
* and a browser. Relational string comparison is defined by ECMAScript over
|
|
67
|
+
* UTF-16 code units and therefore gives every runtime the same ordering.
|
|
68
|
+
*/
|
|
69
|
+
export function compareCanonicalStrings(left: string, right: string): number {
|
|
70
|
+
return left < right ? -1 : left > right ? 1 : 0;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function canonicalPreimageValue(value: unknown, ancestors: Set<object>, path: string): string {
|
|
57
74
|
if (value === undefined || value === null) return "null";
|
|
58
75
|
const type = typeof value;
|
|
59
76
|
if (type === "number") return normalizeNumber(value as number);
|
|
60
77
|
if (type === "string" || type === "boolean") return JSON.stringify(value);
|
|
61
78
|
if (type === "bigint") return JSON.stringify((value as bigint).toString());
|
|
79
|
+
if (type === "function" || type === "symbol") {
|
|
80
|
+
throw new TypeError(`Unsupported contract value at ${path}: ${type}`);
|
|
81
|
+
}
|
|
62
82
|
if (Array.isArray(value)) {
|
|
63
|
-
|
|
83
|
+
if (ancestors.has(value)) throw new TypeError(`Circular contract value at ${path}`);
|
|
84
|
+
ancestors.add(value);
|
|
85
|
+
try {
|
|
86
|
+
return `[${value
|
|
87
|
+
.map((entry, index) => canonicalPreimageValue(entry, ancestors, `${path}[${index}]`))
|
|
88
|
+
.join(",")}]`;
|
|
89
|
+
} finally {
|
|
90
|
+
ancestors.delete(value);
|
|
91
|
+
}
|
|
64
92
|
}
|
|
65
93
|
if (type !== "object") {
|
|
66
|
-
|
|
67
|
-
// serializer stays total rather than throwing on the ingest path.
|
|
68
|
-
return "null";
|
|
94
|
+
throw new TypeError(`Unsupported contract value at ${path}: ${type}`);
|
|
69
95
|
}
|
|
70
|
-
const
|
|
96
|
+
const object = value as object;
|
|
97
|
+
const prototype = Object.getPrototypeOf(object);
|
|
98
|
+
if (prototype !== Object.prototype && prototype !== null) {
|
|
99
|
+
const objectType = prototype?.constructor?.name ?? "unknown";
|
|
100
|
+
throw new TypeError(`Unsupported contract object at ${path}: ${objectType}`);
|
|
101
|
+
}
|
|
102
|
+
if (ancestors.has(object)) throw new TypeError(`Circular contract value at ${path}`);
|
|
103
|
+
ancestors.add(object);
|
|
104
|
+
const entries = Object.entries(object as Record<string, unknown>)
|
|
71
105
|
.filter(([, v]) => v !== undefined)
|
|
72
|
-
.sort(([a], [b]) => (a
|
|
73
|
-
|
|
106
|
+
.sort(([a], [b]) => compareCanonicalStrings(a, b));
|
|
107
|
+
try {
|
|
108
|
+
return `{${entries
|
|
109
|
+
.map(
|
|
110
|
+
([key, entry]) =>
|
|
111
|
+
`${JSON.stringify(key)}:${canonicalPreimageValue(entry, ancestors, `${path}.${key}`)}`
|
|
112
|
+
)
|
|
113
|
+
.join(",")}}`;
|
|
114
|
+
} finally {
|
|
115
|
+
ancestors.delete(object);
|
|
116
|
+
}
|
|
74
117
|
}
|
|
75
118
|
|
|
76
119
|
// ---------------------------------------------------------------------------
|
|
@@ -189,7 +232,21 @@ export function sha256Hex(message: string): string {
|
|
|
189
232
|
* chars. Identical inputs produce an identical FCID in the CLI, the Convex
|
|
190
233
|
* isolate, and the browser — which is what lets the gen-time agent view and the
|
|
191
234
|
* CI enforcement reference the same contract by hash.
|
|
235
|
+
*
|
|
236
|
+
* Never feed preview modules, story states, guidance, or runtime config into
|
|
237
|
+
* this function and call the result an FCID — those belong to
|
|
238
|
+
* {@link artifactContentHash}.
|
|
192
239
|
*/
|
|
193
240
|
export function contractHash(body: unknown): string {
|
|
194
241
|
return sha256Hex(canonicalPreimage(body));
|
|
195
242
|
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Content-address for preview artifacts (`artifactId`): the same deterministic
|
|
246
|
+
* SHA-256 canonicalization as {@link contractHash}, but a separate named seam so
|
|
247
|
+
* callers never assign `artifactId` from an FCID (or vice versa). Hash preview
|
|
248
|
+
* modules, states, guidance/render content, and runtime config here.
|
|
249
|
+
*/
|
|
250
|
+
export function artifactContentHash(body: unknown): string {
|
|
251
|
+
return sha256Hex(canonicalPreimage(body));
|
|
252
|
+
}
|