@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
package/src/contract/index.ts
CHANGED
|
@@ -5,15 +5,20 @@
|
|
|
5
5
|
* projection that defines what the contract IS, the shared pin formatter, and
|
|
6
6
|
* the committed reference catalog (`catalog-fixture-a`).
|
|
7
7
|
*/
|
|
8
|
-
export { canonicalPreimage, contractHash, sha256Hex } from "./hash.js";
|
|
8
|
+
export { artifactContentHash, canonicalPreimage, contractHash, sha256Hex } from "./hash.js";
|
|
9
9
|
export {
|
|
10
|
+
CONTRACT_DISPLAY_ONLY_FIELDS,
|
|
10
11
|
CONTRACT_DOMAINS,
|
|
12
|
+
CONTRACT_ENFORCEMENT_FIELDS,
|
|
11
13
|
CONTRACT_PREIMAGE_CAPABILITY_HEADER,
|
|
12
14
|
CONTRACT_PREIMAGE_SCHEMA,
|
|
15
|
+
ContractCatalogValidationError,
|
|
13
16
|
contractComponentsFromFragments,
|
|
14
17
|
contractPolicyFromGovernanceConfig,
|
|
18
|
+
deriveArtifactId,
|
|
15
19
|
diffContractDomains,
|
|
16
20
|
projectContractPreimage,
|
|
21
|
+
projectEnforcementContract,
|
|
17
22
|
verifiedContractPreimageFromPin,
|
|
18
23
|
} from "./preimage.js";
|
|
19
24
|
export type {
|
|
@@ -22,6 +27,7 @@ export type {
|
|
|
22
27
|
ContractCatalogInput,
|
|
23
28
|
ContractComponentInput,
|
|
24
29
|
ContractDomain,
|
|
30
|
+
ContractEnforcementField,
|
|
25
31
|
ContractIdentityPin,
|
|
26
32
|
ContractPolicyInput,
|
|
27
33
|
ContractPreimage,
|
|
@@ -44,3 +50,20 @@ export {
|
|
|
44
50
|
catalogFixtureATokensDtcg,
|
|
45
51
|
CATALOG_FIXTURE_A_FCID,
|
|
46
52
|
} from "./fixture.js";
|
|
53
|
+
export {
|
|
54
|
+
FRAGMENTS_MANIFEST_FILENAME,
|
|
55
|
+
FRAGMENTS_MANIFEST_SCHEMA_VERSION,
|
|
56
|
+
FRAGMENTS_MANIFEST_TOKEN_RULE,
|
|
57
|
+
fragmentsManifestFcid,
|
|
58
|
+
fragmentsManifestGrammarSchema,
|
|
59
|
+
fragmentsManifestPatternSchema,
|
|
60
|
+
fragmentsManifestPrimitiveSchema,
|
|
61
|
+
fragmentsManifestSchema,
|
|
62
|
+
parseFragmentsManifest,
|
|
63
|
+
} from "./manifest.js";
|
|
64
|
+
export type {
|
|
65
|
+
FragmentsManifest,
|
|
66
|
+
FragmentsManifestGrammar,
|
|
67
|
+
FragmentsManifestPattern,
|
|
68
|
+
FragmentsManifestPrimitive,
|
|
69
|
+
} from "./manifest.js";
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
FRAGMENTS_MANIFEST_FILENAME,
|
|
5
|
+
fragmentsManifestFcid,
|
|
6
|
+
fragmentsManifestSchema,
|
|
7
|
+
parseFragmentsManifest,
|
|
8
|
+
type FragmentsManifest,
|
|
9
|
+
} from "./manifest.js";
|
|
10
|
+
|
|
11
|
+
const SAMPLE: FragmentsManifest = {
|
|
12
|
+
schemaVersion: 1,
|
|
13
|
+
designSystem: { packageName: "@usefragments/ui", importPath: "@usefragments/ui" },
|
|
14
|
+
tokens: { prefix: "--fui-", rule: "no-raw-values" },
|
|
15
|
+
primitives: [
|
|
16
|
+
{
|
|
17
|
+
name: "Button",
|
|
18
|
+
members: [],
|
|
19
|
+
intents: ["action"],
|
|
20
|
+
props: ["variant", "size"],
|
|
21
|
+
do: ["one primary per view"],
|
|
22
|
+
dont: ["raw <button>"],
|
|
23
|
+
exemplar: "apps/cloud/src/page/findings/findings-page.tsx:212",
|
|
24
|
+
},
|
|
25
|
+
],
|
|
26
|
+
patterns: [],
|
|
27
|
+
grammar: [],
|
|
28
|
+
rules: { preset: "fragments", off: [] },
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
describe("fragments.manifest schema", () => {
|
|
32
|
+
it("parses the v1 shape", () => {
|
|
33
|
+
expect(parseFragmentsManifest(SAMPLE)).toEqual(SAMPLE);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("rejects a missing schemaVersion", () => {
|
|
37
|
+
const { schemaVersion: _schemaVersion, ...rest } = SAMPLE;
|
|
38
|
+
expect(() => parseFragmentsManifest(rest)).toThrow();
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("rejects a non-v1 schemaVersion", () => {
|
|
42
|
+
expect(() => parseFragmentsManifest({ ...SAMPLE, schemaVersion: 2 })).toThrow();
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("requires the no-raw-values token rule", () => {
|
|
46
|
+
expect(() =>
|
|
47
|
+
parseFragmentsManifest({
|
|
48
|
+
...SAMPLE,
|
|
49
|
+
tokens: { prefix: "--fui-", rule: "prefer-tokens" },
|
|
50
|
+
})
|
|
51
|
+
).toThrow();
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("keeps the committed filename stable", () => {
|
|
55
|
+
expect(FRAGMENTS_MANIFEST_FILENAME).toBe("fragments.manifest.json");
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
describe("fragmentsManifestFcid", () => {
|
|
60
|
+
it("returns a 64-char lowercase hex FCID", () => {
|
|
61
|
+
expect(fragmentsManifestFcid(SAMPLE)).toMatch(/^[0-9a-f]{64}$/);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("is stable across object key order", () => {
|
|
65
|
+
const reordered = fragmentsManifestSchema.parse({
|
|
66
|
+
rules: SAMPLE.rules,
|
|
67
|
+
grammar: SAMPLE.grammar,
|
|
68
|
+
patterns: SAMPLE.patterns,
|
|
69
|
+
primitives: SAMPLE.primitives,
|
|
70
|
+
tokens: SAMPLE.tokens,
|
|
71
|
+
designSystem: SAMPLE.designSystem,
|
|
72
|
+
schemaVersion: 1,
|
|
73
|
+
});
|
|
74
|
+
expect(fragmentsManifestFcid(reordered)).toBe(fragmentsManifestFcid(SAMPLE));
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("changes when a primitive is added", () => {
|
|
78
|
+
const extended: FragmentsManifest = {
|
|
79
|
+
...SAMPLE,
|
|
80
|
+
primitives: [
|
|
81
|
+
...SAMPLE.primitives,
|
|
82
|
+
{
|
|
83
|
+
name: "Input",
|
|
84
|
+
members: [],
|
|
85
|
+
intents: [],
|
|
86
|
+
props: [],
|
|
87
|
+
do: [],
|
|
88
|
+
dont: [],
|
|
89
|
+
},
|
|
90
|
+
],
|
|
91
|
+
};
|
|
92
|
+
expect(fragmentsManifestFcid(extended)).not.toBe(fragmentsManifestFcid(SAMPLE));
|
|
93
|
+
});
|
|
94
|
+
});
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Design-system manifest v1 — the committed, human-curated vocabulary that
|
|
3
|
+
* every local agent surface reads. FCID is `contractHash(manifest)`.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { z } from "zod";
|
|
7
|
+
|
|
8
|
+
import { contractHash } from "./hash.js";
|
|
9
|
+
|
|
10
|
+
export const FRAGMENTS_MANIFEST_FILENAME = "fragments.manifest.json";
|
|
11
|
+
export const FRAGMENTS_MANIFEST_SCHEMA_VERSION = 1;
|
|
12
|
+
export const FRAGMENTS_MANIFEST_TOKEN_RULE = "no-raw-values";
|
|
13
|
+
|
|
14
|
+
export const fragmentsManifestPrimitiveSchema = z.object({
|
|
15
|
+
name: z.string().min(1),
|
|
16
|
+
members: z.array(z.string()),
|
|
17
|
+
intents: z.array(z.string()),
|
|
18
|
+
props: z.array(z.string()),
|
|
19
|
+
do: z.array(z.string()),
|
|
20
|
+
dont: z.array(z.string()),
|
|
21
|
+
exemplar: z.string().min(1).optional(),
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
export const fragmentsManifestPatternSchema = z.object({
|
|
25
|
+
name: z.string().min(1),
|
|
26
|
+
intent: z.string(),
|
|
27
|
+
primitives: z.array(z.string()),
|
|
28
|
+
shape: z.string(),
|
|
29
|
+
exemplar: z.string().min(1).optional(),
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
export const fragmentsManifestGrammarSchema = z.object({
|
|
33
|
+
rule: z.string().min(1),
|
|
34
|
+
check: z.string().min(1).optional(),
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
export const fragmentsManifestSchema = z.object({
|
|
38
|
+
schemaVersion: z.literal(FRAGMENTS_MANIFEST_SCHEMA_VERSION),
|
|
39
|
+
designSystem: z.object({
|
|
40
|
+
packageName: z.string().min(1),
|
|
41
|
+
importPath: z.string().min(1),
|
|
42
|
+
}),
|
|
43
|
+
tokens: z.object({
|
|
44
|
+
prefix: z.string().min(1),
|
|
45
|
+
rule: z.literal(FRAGMENTS_MANIFEST_TOKEN_RULE),
|
|
46
|
+
}),
|
|
47
|
+
primitives: z.array(fragmentsManifestPrimitiveSchema),
|
|
48
|
+
patterns: z.array(fragmentsManifestPatternSchema),
|
|
49
|
+
grammar: z.array(fragmentsManifestGrammarSchema),
|
|
50
|
+
rules: z.object({
|
|
51
|
+
preset: z.string().min(1),
|
|
52
|
+
off: z.array(z.string()),
|
|
53
|
+
}),
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
export type FragmentsManifestPrimitive = z.infer<typeof fragmentsManifestPrimitiveSchema>;
|
|
57
|
+
export type FragmentsManifestPattern = z.infer<typeof fragmentsManifestPatternSchema>;
|
|
58
|
+
export type FragmentsManifestGrammar = z.infer<typeof fragmentsManifestGrammarSchema>;
|
|
59
|
+
export type FragmentsManifest = z.infer<typeof fragmentsManifestSchema>;
|
|
60
|
+
|
|
61
|
+
export function parseFragmentsManifest(input: unknown): FragmentsManifest {
|
|
62
|
+
return fragmentsManifestSchema.parse(input);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** FCID for a parsed manifest: content-address, key-order independent. */
|
|
66
|
+
export function fragmentsManifestFcid(manifest: FragmentsManifest): string {
|
|
67
|
+
return contractHash(manifest);
|
|
68
|
+
}
|
|
@@ -10,14 +10,19 @@ import {
|
|
|
10
10
|
catalogFixtureA,
|
|
11
11
|
catalogFixtureAContractPayload,
|
|
12
12
|
} from "./fixture.js";
|
|
13
|
-
import { contractHash } from "./hash.js";
|
|
13
|
+
import { artifactContentHash, contractHash } from "./hash.js";
|
|
14
14
|
import {
|
|
15
15
|
type ContractCatalogInput,
|
|
16
16
|
type ContractComponentInput,
|
|
17
17
|
type ContractTokenInput,
|
|
18
|
+
CONTRACT_DISPLAY_ONLY_FIELDS,
|
|
19
|
+
CONTRACT_ENFORCEMENT_FIELDS,
|
|
20
|
+
ContractCatalogValidationError,
|
|
18
21
|
contractComponentsFromFragments,
|
|
22
|
+
deriveArtifactId,
|
|
19
23
|
diffContractDomains,
|
|
20
24
|
projectContractPreimage,
|
|
25
|
+
projectEnforcementContract,
|
|
21
26
|
verifiedContractPreimageFromPin,
|
|
22
27
|
} from "./preimage.js";
|
|
23
28
|
import { formatStampPin } from "./stamp.js";
|
|
@@ -62,6 +67,70 @@ describe("§1 byte parity — one fixture, one hash, every runtime", () => {
|
|
|
62
67
|
});
|
|
63
68
|
});
|
|
64
69
|
|
|
70
|
+
describe("catalog runtime validation", () => {
|
|
71
|
+
it("accepts partial legacy catalogs and ignores unknown future fields", () => {
|
|
72
|
+
const legacy = projectContractPreimage({ components: [{ name: "Button" }] });
|
|
73
|
+
const future = projectContractPreimage({
|
|
74
|
+
components: [{ name: "Button", futureHint: { mode: "v4" } }],
|
|
75
|
+
futureDomain: { enabled: true },
|
|
76
|
+
} as ContractCatalogInput);
|
|
77
|
+
|
|
78
|
+
expect(future).toEqual(legacy);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it("rejects malformed known fields with sorted, deterministic issues", () => {
|
|
82
|
+
const invalid = {
|
|
83
|
+
components: [{ name: "", props: "variant", contract: [] }],
|
|
84
|
+
tokens: [{ name: "--fui-space", value: Number.NaN }],
|
|
85
|
+
policy: { waivers: "waiver-1" },
|
|
86
|
+
} as unknown as ContractCatalogInput;
|
|
87
|
+
|
|
88
|
+
expect(() => projectContractPreimage(invalid)).toThrow(ContractCatalogValidationError);
|
|
89
|
+
try {
|
|
90
|
+
projectContractPreimage(invalid);
|
|
91
|
+
} catch (error) {
|
|
92
|
+
expect(error).toBeInstanceOf(ContractCatalogValidationError);
|
|
93
|
+
expect((error as ContractCatalogValidationError).issues).toEqual([
|
|
94
|
+
"components[0].contract: expected a plain object",
|
|
95
|
+
"components[0].name: expected a non-empty string",
|
|
96
|
+
"components[0].props: expected an array",
|
|
97
|
+
"policy.waivers: expected an array",
|
|
98
|
+
"tokens[0].value: expected a finite number or string",
|
|
99
|
+
]);
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it("rejects duplicate semantic identities instead of hashing ambiguous catalogs", () => {
|
|
104
|
+
const invalid = {
|
|
105
|
+
components: [{ name: "Button" }, { name: "Button" }],
|
|
106
|
+
tokens: [
|
|
107
|
+
{ name: "--fui-space", value: "4px" },
|
|
108
|
+
{ name: "--fui-space", value: "8px" },
|
|
109
|
+
],
|
|
110
|
+
canonicalMappings: [
|
|
111
|
+
{ component: "LegacyButton", canonical: "Button", status: "confirmed" },
|
|
112
|
+
{ component: "LegacyButton", canonical: "LinkButton", status: "confirmed" },
|
|
113
|
+
],
|
|
114
|
+
policy: {
|
|
115
|
+
waivers: [
|
|
116
|
+
{ id: "waiver-1", target: "FUI1001", reason: "migration" },
|
|
117
|
+
{ id: "waiver-1", target: "FUI1002", reason: "migration" },
|
|
118
|
+
],
|
|
119
|
+
},
|
|
120
|
+
} satisfies ContractCatalogInput;
|
|
121
|
+
|
|
122
|
+
expect(() => projectContractPreimage(invalid)).toThrow(
|
|
123
|
+
[
|
|
124
|
+
"Invalid contract catalog:",
|
|
125
|
+
' - canonicalMappings: duplicate confirmed identity "LegacyButton"',
|
|
126
|
+
' - components: duplicate identity "#Button"',
|
|
127
|
+
' - policy.waivers: duplicate identity "waiver-1"',
|
|
128
|
+
' - tokens: duplicate identity "--fui-space"',
|
|
129
|
+
].join("\n")
|
|
130
|
+
);
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
|
|
65
134
|
describe("published identity pin verification", () => {
|
|
66
135
|
it("reconstructs the exact canonical preimage behind a matching FCID", () => {
|
|
67
136
|
const preimage = projectContractPreimage(catalogFixtureA);
|
|
@@ -123,6 +192,16 @@ describe("§2 enforced-only — prose doesn't move the hash, enforcement does",
|
|
|
123
192
|
...component,
|
|
124
193
|
description: "A lovingly rewritten description",
|
|
125
194
|
docsUrl: "https://usefragments.com/docs/button",
|
|
195
|
+
contract: {
|
|
196
|
+
...(typeof component.contract === "object" && component.contract
|
|
197
|
+
? component.contract
|
|
198
|
+
: {}),
|
|
199
|
+
propsSummary: ["variant: display-only"],
|
|
200
|
+
canonicalUsage: ["<Button>Save</Button>"],
|
|
201
|
+
scenarioTags: ["marketing-copy"],
|
|
202
|
+
states: [{ name: "Primary", props: { children: "Save" } }],
|
|
203
|
+
variants: [{ prop: "variant", options: ["primary"] }],
|
|
204
|
+
},
|
|
126
205
|
}) as ContractComponentInput
|
|
127
206
|
),
|
|
128
207
|
tokens: (catalogFixtureA.tokens ?? []).map(
|
|
@@ -149,6 +228,145 @@ describe("§2 enforced-only — prose doesn't move the hash, enforcement does",
|
|
|
149
228
|
});
|
|
150
229
|
});
|
|
151
230
|
|
|
231
|
+
describe("Brief 05 — FCID / artifact identity boundary", () => {
|
|
232
|
+
it("A05-02: governance, enforcement contract, compound, and prop-name edits move FCID", () => {
|
|
233
|
+
const base = catalogFixtureA;
|
|
234
|
+
const withGovernance: ContractCatalogInput = {
|
|
235
|
+
...base,
|
|
236
|
+
components: (base.components ?? []).map((component, index) =>
|
|
237
|
+
index === 0
|
|
238
|
+
? {
|
|
239
|
+
...component,
|
|
240
|
+
governance: [
|
|
241
|
+
{
|
|
242
|
+
kind: "a11y.requireName" as const,
|
|
243
|
+
because: "icon-only buttons need an accessible name",
|
|
244
|
+
severity: "error" as const,
|
|
245
|
+
},
|
|
246
|
+
],
|
|
247
|
+
}
|
|
248
|
+
: component
|
|
249
|
+
),
|
|
250
|
+
};
|
|
251
|
+
const withBan: ContractCatalogInput = {
|
|
252
|
+
...base,
|
|
253
|
+
components: (base.components ?? []).map((component, index) =>
|
|
254
|
+
index === 0
|
|
255
|
+
? {
|
|
256
|
+
...component,
|
|
257
|
+
contract: {
|
|
258
|
+
bans: [
|
|
259
|
+
{ prop: "style", reason: "token-governed surface" },
|
|
260
|
+
{ prop: "className", reason: "prefer variants" },
|
|
261
|
+
],
|
|
262
|
+
},
|
|
263
|
+
}
|
|
264
|
+
: component
|
|
265
|
+
),
|
|
266
|
+
};
|
|
267
|
+
const withCompound: ContractCatalogInput = {
|
|
268
|
+
...base,
|
|
269
|
+
components: (base.components ?? []).map((component) =>
|
|
270
|
+
component.name === "Card"
|
|
271
|
+
? {
|
|
272
|
+
...component,
|
|
273
|
+
compoundChildren: ["Card.Header", "Card.Body", "Card.Footer"],
|
|
274
|
+
}
|
|
275
|
+
: component
|
|
276
|
+
),
|
|
277
|
+
};
|
|
278
|
+
const withProp: ContractCatalogInput = {
|
|
279
|
+
...base,
|
|
280
|
+
components: (base.components ?? []).map((component, index) =>
|
|
281
|
+
index === 0 ? { ...component, props: [...(component.props ?? []), "loading"] } : component
|
|
282
|
+
),
|
|
283
|
+
};
|
|
284
|
+
|
|
285
|
+
expect(fcidOf(withGovernance)).not.toBe(CATALOG_FIXTURE_A_FCID);
|
|
286
|
+
expect(fcidOf(withBan)).not.toBe(CATALOG_FIXTURE_A_FCID);
|
|
287
|
+
expect(fcidOf(withCompound)).not.toBe(CATALOG_FIXTURE_A_FCID);
|
|
288
|
+
expect(fcidOf(withProp)).not.toBe(CATALOG_FIXTURE_A_FCID);
|
|
289
|
+
expect(
|
|
290
|
+
diffContractDomains(projectContractPreimage(base), projectContractPreimage(withGovernance))
|
|
291
|
+
).toEqual(["components"]);
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
it("A05-03: display-only fields are absent from the positive enforcement pick-list", () => {
|
|
295
|
+
for (const field of CONTRACT_DISPLAY_ONLY_FIELDS) {
|
|
296
|
+
expect(CONTRACT_ENFORCEMENT_FIELDS).not.toContain(field);
|
|
297
|
+
}
|
|
298
|
+
expect(
|
|
299
|
+
projectEnforcementContract({
|
|
300
|
+
propsSummary: ["variant: primary"],
|
|
301
|
+
canonicalUsage: ["<Button />"],
|
|
302
|
+
scenarioTags: ["hero"],
|
|
303
|
+
states: [{ name: "Primary" }],
|
|
304
|
+
variants: [{ prop: "variant", options: ["primary"] }],
|
|
305
|
+
a11yRules: ["no-empty-buttons"],
|
|
306
|
+
})
|
|
307
|
+
).toEqual({ a11yRules: ["no-empty-buttons"] });
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
it("A05-01: guidance/example content moves artifactId while FCID stays fixed", () => {
|
|
311
|
+
const fcid = fcidOf(catalogFixtureA);
|
|
312
|
+
const artifactA = deriveArtifactId({
|
|
313
|
+
modules: [{ path: "Button.stories.tsx" }],
|
|
314
|
+
states: [{ name: "Primary", props: { children: "Save" } }],
|
|
315
|
+
guidance: { purpose: "Primary actions" },
|
|
316
|
+
runtime: { framework: "react" },
|
|
317
|
+
});
|
|
318
|
+
const artifactB = deriveArtifactId({
|
|
319
|
+
modules: [{ path: "Button.stories.tsx" }],
|
|
320
|
+
states: [{ name: "Primary", props: { children: "Save changes" } }],
|
|
321
|
+
guidance: { purpose: "Primary actions, revised copy" },
|
|
322
|
+
runtime: { framework: "react" },
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
expect(fcidOf(catalogFixtureA)).toBe(fcid);
|
|
326
|
+
expect(artifactA).toMatch(/^[0-9a-f]{64}$/);
|
|
327
|
+
expect(artifactB).toMatch(/^[0-9a-f]{64}$/);
|
|
328
|
+
expect(artifactA).not.toBe(artifactB);
|
|
329
|
+
expect(artifactA).not.toBe(fcid);
|
|
330
|
+
expect(artifactContentHash({ schema: "artifact-content:v1" })).toMatch(/^[0-9a-f]{64}$/);
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
it("A05-04: fragment adapters project resolved governance without migration ambiguity", () => {
|
|
334
|
+
const withoutGov = contractComponentsFromFragments([
|
|
335
|
+
{
|
|
336
|
+
meta: { name: "Button" },
|
|
337
|
+
props: { variant: {} },
|
|
338
|
+
contract: {
|
|
339
|
+
bans: [{ pattern: "style", message: "no" }],
|
|
340
|
+
propsSummary: ["variant"],
|
|
341
|
+
},
|
|
342
|
+
},
|
|
343
|
+
]);
|
|
344
|
+
const withGov = contractComponentsFromFragments([
|
|
345
|
+
{
|
|
346
|
+
meta: { name: "Button" },
|
|
347
|
+
props: { variant: {} },
|
|
348
|
+
contract: {
|
|
349
|
+
bans: [{ pattern: "style", message: "no" }],
|
|
350
|
+
propsSummary: ["variant"],
|
|
351
|
+
},
|
|
352
|
+
governance: [
|
|
353
|
+
{
|
|
354
|
+
kind: "capability",
|
|
355
|
+
capability: "button",
|
|
356
|
+
},
|
|
357
|
+
],
|
|
358
|
+
},
|
|
359
|
+
]);
|
|
360
|
+
|
|
361
|
+
expect(withoutGov[0]?.governance).toBeUndefined();
|
|
362
|
+
expect(withGov[0]?.governance).toEqual([{ kind: "capability", capability: "button" }]);
|
|
363
|
+
expect(fcidOf({ components: withoutGov })).not.toBe(fcidOf({ components: withGov }));
|
|
364
|
+
expect(projectEnforcementContract(withoutGov[0]?.contract)).toEqual({
|
|
365
|
+
bans: [{ pattern: "style", message: "no" }],
|
|
366
|
+
});
|
|
367
|
+
});
|
|
368
|
+
});
|
|
369
|
+
|
|
152
370
|
describe("§3 proposed mappings are not identity", () => {
|
|
153
371
|
it("adding a new proposed mapping leaves the fcid unchanged", () => {
|
|
154
372
|
const withNewProposedMapping: ContractCatalogInput = {
|