@usefragments/core 1.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 (228) hide show
  1. package/LICENSE +21 -0
  2. package/dist/chunk-243QYRUF.js +2145 -0
  3. package/dist/chunk-243QYRUF.js.map +1 -0
  4. package/dist/chunk-3LLRNCPX.js +111 -0
  5. package/dist/chunk-3LLRNCPX.js.map +1 -0
  6. package/dist/chunk-EIYNNS77.js +48 -0
  7. package/dist/chunk-EIYNNS77.js.map +1 -0
  8. package/dist/chunk-JNBFJ34I.js +326 -0
  9. package/dist/chunk-JNBFJ34I.js.map +1 -0
  10. package/dist/chunk-R3X7UBHU.js +228 -0
  11. package/dist/chunk-R3X7UBHU.js.map +1 -0
  12. package/dist/chunk-SH4KPIYH.js +393 -0
  13. package/dist/chunk-SH4KPIYH.js.map +1 -0
  14. package/dist/chunk-V4VQB57N.js +56 -0
  15. package/dist/chunk-V4VQB57N.js.map +1 -0
  16. package/dist/chunk-X34IA4LR.js +319 -0
  17. package/dist/chunk-X34IA4LR.js.map +1 -0
  18. package/dist/chunk-YF65VYRY.js +493 -0
  19. package/dist/chunk-YF65VYRY.js.map +1 -0
  20. package/dist/codes/index.d.ts +40 -0
  21. package/dist/codes/index.js +23 -0
  22. package/dist/codes/index.js.map +1 -0
  23. package/dist/compiled-types/index.d.ts +5 -0
  24. package/dist/compiled-types/index.js +1 -0
  25. package/dist/compiled-types/index.js.map +1 -0
  26. package/dist/generate/index.d.ts +47 -0
  27. package/dist/generate/index.js +11 -0
  28. package/dist/generate/index.js.map +1 -0
  29. package/dist/governance-BOa3KyiJ.d.ts +3279 -0
  30. package/dist/graph/index.d.ts +66 -0
  31. package/dist/graph/index.js +17 -0
  32. package/dist/graph/index.js.map +1 -0
  33. package/dist/index.d.ts +11388 -0
  34. package/dist/index.js +8711 -0
  35. package/dist/index.js.map +1 -0
  36. package/dist/package-identity.d.ts +42 -0
  37. package/dist/package-identity.js +23 -0
  38. package/dist/package-identity.js.map +1 -0
  39. package/dist/preview/index.d.ts +484 -0
  40. package/dist/preview/index.js +1129 -0
  41. package/dist/preview/index.js.map +1 -0
  42. package/dist/react-types.d.ts +22 -0
  43. package/dist/react-types.js +1 -0
  44. package/dist/react-types.js.map +1 -0
  45. package/dist/registry.d.ts +2367 -0
  46. package/dist/registry.js +62 -0
  47. package/dist/registry.js.map +1 -0
  48. package/dist/schemas/index.d.ts +2584 -0
  49. package/dist/schemas/index.js +55 -0
  50. package/dist/schemas/index.js.map +1 -0
  51. package/dist/severity.d.ts +18 -0
  52. package/dist/severity.js +27 -0
  53. package/dist/severity.js.map +1 -0
  54. package/dist/test-utils.d.ts +14 -0
  55. package/dist/test-utils.js +63 -0
  56. package/dist/test-utils.js.map +1 -0
  57. package/dist/topology/index.d.ts +48 -0
  58. package/dist/topology/index.js +7 -0
  59. package/dist/topology/index.js.map +1 -0
  60. package/dist/types-xJ2xyp_G.d.ts +119 -0
  61. package/package.json +120 -0
  62. package/src/__tests__/contract-parser.test.ts +614 -0
  63. package/src/__tests__/dtcg-generators.test.ts +439 -0
  64. package/src/__tests__/dtcg-parser.test.ts +884 -0
  65. package/src/__tests__/preview-runtime.test.tsx +111 -0
  66. package/src/__tests__/schema.test.ts +31 -0
  67. package/src/__tests__/severity.test.ts +34 -0
  68. package/src/__tests__/style-comparison.test.ts +662 -0
  69. package/src/__tests__/tokens-compat.test.ts +51 -0
  70. package/src/agent-format.test.ts +105 -0
  71. package/src/agent-format.ts +141 -0
  72. package/src/bundle.ts +149 -0
  73. package/src/codes/__tests__/codes.test.ts +73 -0
  74. package/src/codes/codes.ts +463 -0
  75. package/src/codes/index.ts +7 -0
  76. package/src/codes/source-finding-bridge.ts +113 -0
  77. package/src/compiled-types/index.ts +293 -0
  78. package/src/component-contract.ts +231 -0
  79. package/src/component-discovery.ts +80 -0
  80. package/src/composition.test.ts +262 -0
  81. package/src/composition.ts +318 -0
  82. package/src/config-paths.test.ts +78 -0
  83. package/src/config-paths.ts +47 -0
  84. package/src/config.ts +18 -0
  85. package/src/conform-prove.test.ts +274 -0
  86. package/src/conform-prove.ts +326 -0
  87. package/src/conform.ts +160 -0
  88. package/src/constants.ts +114 -0
  89. package/src/context.ts +2 -0
  90. package/src/contract/fixture.ts +110 -0
  91. package/src/contract/hash.test.ts +86 -0
  92. package/src/contract/hash.ts +195 -0
  93. package/src/contract/index.ts +43 -0
  94. package/src/contract/preimage.test.ts +254 -0
  95. package/src/contract/preimage.ts +365 -0
  96. package/src/contract/stamp.test.ts +100 -0
  97. package/src/contract/stamp.ts +105 -0
  98. package/src/contract-parser.ts +141 -0
  99. package/src/defineFragment.test.ts +257 -0
  100. package/src/defineFragment.ts +293 -0
  101. package/src/dtcg-generators.ts +324 -0
  102. package/src/dtcg-parser.ts +1 -0
  103. package/src/dtcg.ts +237 -0
  104. package/src/facts/__snapshots__/facts.test.ts.snap +124 -0
  105. package/src/facts/builders.ts +835 -0
  106. package/src/facts/compile.ts +422 -0
  107. package/src/facts/fact-index.ts +394 -0
  108. package/src/facts/facts.test.ts +494 -0
  109. package/src/facts/ids.ts +125 -0
  110. package/src/facts/index.ts +119 -0
  111. package/src/facts/types.ts +614 -0
  112. package/src/figma.ts +263 -0
  113. package/src/fragment-types.ts +214 -0
  114. package/src/generate/generate.test.ts +594 -0
  115. package/src/generate/index.ts +438 -0
  116. package/src/governance-integrity.test.ts +186 -0
  117. package/src/governance-integrity.ts +282 -0
  118. package/src/governance.test.ts +300 -0
  119. package/src/governance.ts +605 -0
  120. package/src/governance.type-test.ts +75 -0
  121. package/src/graph/engine.ts +446 -0
  122. package/src/graph/graph.test.ts +746 -0
  123. package/src/graph/index.ts +32 -0
  124. package/src/graph/serialization.ts +55 -0
  125. package/src/graph/types.ts +178 -0
  126. package/src/index.ts +891 -0
  127. package/src/local-canonical.ts +102 -0
  128. package/src/mcp-snapshot.ts +222 -0
  129. package/src/package-identity-match.ts +33 -0
  130. package/src/package-identity.generated.ts +233 -0
  131. package/src/package-identity.test.ts +99 -0
  132. package/src/package-identity.ts +157 -0
  133. package/src/performance-presets.ts +142 -0
  134. package/src/preview/comparison-artifact.test.ts +228 -0
  135. package/src/preview/comparison-artifact.ts +181 -0
  136. package/src/preview/comparison-context.ts +93 -0
  137. package/src/preview/comparison-evidence.ts +112 -0
  138. package/src/preview/comparison-format.ts +21 -0
  139. package/src/preview/comparison-presentation.ts +187 -0
  140. package/src/preview/comparison-references.test.ts +430 -0
  141. package/src/preview/comparison-references.ts +72 -0
  142. package/src/preview/comparison-score.ts +59 -0
  143. package/src/preview/comparison-styles.ts +197 -0
  144. package/src/preview/comparison.test.ts +559 -0
  145. package/src/preview/comparison.ts +488 -0
  146. package/src/preview/index.ts +10 -0
  147. package/src/preview/types.ts +309 -0
  148. package/src/preview/validation.test.ts +290 -0
  149. package/src/preview/validation.ts +356 -0
  150. package/src/preview-runtime.tsx +144 -0
  151. package/src/raw-html-canonical.test.ts +41 -0
  152. package/src/raw-html-canonical.ts +179 -0
  153. package/src/react-types.ts +21 -0
  154. package/src/registry-install-plan.ts +344 -0
  155. package/src/registry.test.ts +245 -0
  156. package/src/registry.ts +369 -0
  157. package/src/rules/a11y-required-accessible-name.ts +93 -0
  158. package/src/rules/color-math.test.ts +72 -0
  159. package/src/rules/color-math.ts +192 -0
  160. package/src/rules/components-forbidden-prop-value.ts +108 -0
  161. package/src/rules/components-prefer-library.ts +943 -0
  162. package/src/rules/components-unknown-prop.ts +94 -0
  163. package/src/rules/composition-pattern-schema.ts +125 -0
  164. package/src/rules/composition-pattern.test.ts +320 -0
  165. package/src/rules/composition-pattern.ts +271 -0
  166. package/src/rules/emit-gate.test.ts +42 -0
  167. package/src/rules/emit-gate.ts +60 -0
  168. package/src/rules/finding.ts +54 -0
  169. package/src/rules/index.ts +235 -0
  170. package/src/rules/jsx-preferred-component.ts +68 -0
  171. package/src/rules/jsx-preferred-import-path.ts +67 -0
  172. package/src/rules/presets.ts +57 -0
  173. package/src/rules/props-invalid-value.ts +115 -0
  174. package/src/rules/rules.test.ts +3636 -0
  175. package/src/rules/spacing-resolution.ts +184 -0
  176. package/src/rules/styles-no-raw-color.ts +329 -0
  177. package/src/rules/styles-no-raw-dimensions.ts +357 -0
  178. package/src/rules/styles-no-raw-spacing.ts +236 -0
  179. package/src/rules/styles-no-raw-typography.ts +237 -0
  180. package/src/rules/tailwind-arbitrary-color.test.ts +128 -0
  181. package/src/rules/tailwind-arbitrary-color.ts +93 -0
  182. package/src/rules/tailwind-arbitrary-spacing.test.ts +157 -0
  183. package/src/rules/tailwind-arbitrary-spacing.ts +120 -0
  184. package/src/rules/tailwind-forbidden-palette.test.ts +116 -0
  185. package/src/rules/tailwind-forbidden-palette.ts +107 -0
  186. package/src/rules/tailwind-off-scale-spacing-token.test.ts +100 -0
  187. package/src/rules/tailwind-off-scale-spacing-token.ts +108 -0
  188. package/src/rules/tailwind-raw-color-via-token.test.ts +108 -0
  189. package/src/rules/tailwind-raw-color-via-token.ts +82 -0
  190. package/src/rules/tailwind-rule-utils.ts +106 -0
  191. package/src/rules/tailwind-unknown-class.test.ts +157 -0
  192. package/src/rules/tailwind-unknown-class.ts +121 -0
  193. package/src/rules/taxonomy.test.ts +238 -0
  194. package/src/rules/theme-no-theme-coupled-literal.ts +132 -0
  195. package/src/rules/tiers.ts +111 -0
  196. package/src/rules/tokens-css-vars-must-be-defined.test.ts +244 -0
  197. package/src/rules/tokens-css-vars-must-be-defined.ts +156 -0
  198. package/src/rules/tokens-require-dual-fallback.ts +78 -0
  199. package/src/rules/types.ts +27 -0
  200. package/src/rules/utils.test.ts +110 -0
  201. package/src/rules/utils.ts +599 -0
  202. package/src/schema.ts +427 -0
  203. package/src/schemas/index.ts +275 -0
  204. package/src/schemas/schemas.test.ts +124 -0
  205. package/src/severity.ts +54 -0
  206. package/src/storyAdapter.test.ts +571 -0
  207. package/src/storyAdapter.ts +761 -0
  208. package/src/storyFilters.test.ts +350 -0
  209. package/src/storyFilters.ts +253 -0
  210. package/src/storybook-csf.ts +11 -0
  211. package/src/style-comparison.ts +423 -0
  212. package/src/test-utils.ts +68 -0
  213. package/src/token-parser.ts +2 -0
  214. package/src/token-types.ts +287 -0
  215. package/src/tokens/categories.ts +131 -0
  216. package/src/tokens/color.ts +193 -0
  217. package/src/tokens/design-token-parser.test.ts +159 -0
  218. package/src/tokens/design-token-parser.ts +402 -0
  219. package/src/tokens/dtcg.ts +428 -0
  220. package/src/tokens/index.ts +130 -0
  221. package/src/tokens/lookup.ts +19 -0
  222. package/src/tokens/scss.ts +171 -0
  223. package/src/tokens/tailwind-v4.ts +187 -0
  224. package/src/tokens/types.ts +43 -0
  225. package/src/topology/index.ts +9 -0
  226. package/src/topology/resolve-area.test.ts +174 -0
  227. package/src/topology/resolve-area.ts +215 -0
  228. package/src/types.ts +1057 -0
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Frontend Contract — the versioned, content-addressed artifact that is both
3
+ * served to the agent at generation time and enforced in CI. This barrel
4
+ * exposes the cross-runtime hashing keystone (the FCID), the unified preimage
5
+ * projection that defines what the contract IS, the shared pin formatter, and
6
+ * the committed reference catalog (`catalog-fixture-a`).
7
+ */
8
+ export { canonicalPreimage, contractHash, sha256Hex } from "./hash.js";
9
+ export {
10
+ CONTRACT_DOMAINS,
11
+ CONTRACT_PREIMAGE_SCHEMA,
12
+ contractComponentsFromFragments,
13
+ contractPolicyFromGovernanceConfig,
14
+ diffContractDomains,
15
+ projectContractPreimage,
16
+ } from "./preimage.js";
17
+ export type {
18
+ CanonicalMappingStatus,
19
+ ContractCanonicalMappingInput,
20
+ ContractCatalogInput,
21
+ ContractComponentInput,
22
+ ContractDomain,
23
+ ContractPolicyInput,
24
+ ContractPreimage,
25
+ ContractPropMappingInput,
26
+ ContractTokenInput,
27
+ ContractWaiverInput,
28
+ } from "./preimage.js";
29
+ export {
30
+ AGENT_CONTEXT_RELATIVE_PATH,
31
+ formatStampHeader,
32
+ formatStampPin,
33
+ formatStampVersionHash,
34
+ parseContractStamp,
35
+ serializeContractStamp,
36
+ } from "./stamp.js";
37
+ export type { ContractStampPin, ContractStampRecord } from "./stamp.js";
38
+ export {
39
+ catalogFixtureA,
40
+ catalogFixtureAContractPayload,
41
+ catalogFixtureATokensDtcg,
42
+ CATALOG_FIXTURE_A_FCID,
43
+ } from "./fixture.js";
@@ -0,0 +1,254 @@
1
+ /**
2
+ * Contract-identity ACCEPTANCE §1–§4 (apps/cloud/docs/contract-identity).
3
+ * `catalog-fixture-a` + golden fcid live in `fixture.ts`; the Cloud-side parity
4
+ * half of §1 imports the same constants.
5
+ */
6
+ import { describe, expect, it } from "vitest";
7
+
8
+ import {
9
+ CATALOG_FIXTURE_A_FCID,
10
+ catalogFixtureA,
11
+ catalogFixtureAContractPayload,
12
+ } from "./fixture.js";
13
+ import { contractHash } from "./hash.js";
14
+ import {
15
+ type ContractCatalogInput,
16
+ type ContractComponentInput,
17
+ type ContractTokenInput,
18
+ contractComponentsFromFragments,
19
+ diffContractDomains,
20
+ projectContractPreimage,
21
+ } from "./preimage.js";
22
+ import { formatStampPin } from "./stamp.js";
23
+
24
+ function fcidOf(catalog: ContractCatalogInput): string {
25
+ return contractHash(projectContractPreimage(catalog));
26
+ }
27
+
28
+ describe("§1 byte parity — one fixture, one hash, every runtime", () => {
29
+ it("hashes catalog-fixture-a to the committed golden fcid", () => {
30
+ expect(fcidOf(catalogFixtureA)).toBe(CATALOG_FIXTURE_A_FCID);
31
+ });
32
+
33
+ it("is independent of adapter iteration order", () => {
34
+ const reordered: ContractCatalogInput = {
35
+ ...catalogFixtureA,
36
+ components: [...(catalogFixtureA.components ?? [])].reverse().map((component) => ({
37
+ ...component,
38
+ props: component.props ? [...component.props].reverse() : undefined,
39
+ })),
40
+ tokens: [...(catalogFixtureA.tokens ?? [])].reverse(),
41
+ canonicalMappings: [...(catalogFixtureA.canonicalMappings ?? [])].reverse(),
42
+ };
43
+ expect(fcidOf(reordered)).toBe(CATALOG_FIXTURE_A_FCID);
44
+ });
45
+
46
+ it("hashes the empty catalog deterministically too", () => {
47
+ expect(fcidOf({})).toMatch(/^[0-9a-f]{64}$/);
48
+ expect(fcidOf({})).toBe(fcidOf({ components: [], tokens: [], policy: null }));
49
+ });
50
+
51
+ it("the payload companion assembles to the fixture's components domain", () => {
52
+ // Guards the Cloud parity materials: editing `catalogFixtureA` without its
53
+ // payload-shaped companion (or vice versa) fails here, in core, before the
54
+ // Convex test ever runs.
55
+ expect(
56
+ fcidOf({
57
+ ...catalogFixtureA,
58
+ components: contractComponentsFromFragments(catalogFixtureAContractPayload.fragments),
59
+ })
60
+ ).toBe(CATALOG_FIXTURE_A_FCID);
61
+ });
62
+ });
63
+
64
+ describe("§2 enforced-only — prose doesn't move the hash, enforcement does", () => {
65
+ it("(a) display-data edits leave the identity unchanged", () => {
66
+ // Adapters may pass wider objects (enrichment descriptions, docs URLs…);
67
+ // the projection's pick-list is the enforced-only boundary.
68
+ const withProseEdits: ContractCatalogInput = {
69
+ ...catalogFixtureA,
70
+ components: (catalogFixtureA.components ?? []).map(
71
+ (component) =>
72
+ ({
73
+ ...component,
74
+ description: "A lovingly rewritten description",
75
+ docsUrl: "https://usefragments.com/docs/button",
76
+ }) as ContractComponentInput
77
+ ),
78
+ tokens: (catalogFixtureA.tokens ?? []).map(
79
+ (token) => ({ ...token, description: "now with prose" }) as ContractTokenInput
80
+ ),
81
+ };
82
+ expect(fcidOf(withProseEdits)).toBe(CATALOG_FIXTURE_A_FCID);
83
+ });
84
+
85
+ it("(b) confirming a mapping changes the identity — and only the canonicalMap domain", () => {
86
+ const withConfirmedMapping: ContractCatalogInput = {
87
+ ...catalogFixtureA,
88
+ canonicalMappings: (catalogFixtureA.canonicalMappings ?? []).map((mapping) =>
89
+ mapping.status === "proposed" ? { ...mapping, status: "confirmed" as const } : mapping
90
+ ),
91
+ };
92
+ expect(fcidOf(withConfirmedMapping)).not.toBe(CATALOG_FIXTURE_A_FCID);
93
+ expect(
94
+ diffContractDomains(
95
+ projectContractPreimage(catalogFixtureA),
96
+ projectContractPreimage(withConfirmedMapping)
97
+ )
98
+ ).toEqual(["canonicalMap"]);
99
+ });
100
+ });
101
+
102
+ describe("§3 proposed mappings are not identity", () => {
103
+ it("adding a new proposed mapping leaves the fcid unchanged", () => {
104
+ const withNewProposedMapping: ContractCatalogInput = {
105
+ ...catalogFixtureA,
106
+ canonicalMappings: [
107
+ ...(catalogFixtureA.canonicalMappings ?? []),
108
+ { component: "AppButton", canonical: "Button", status: "proposed" as const },
109
+ ],
110
+ };
111
+ expect(fcidOf(withNewProposedMapping)).toBe(CATALOG_FIXTURE_A_FCID);
112
+ });
113
+ });
114
+
115
+ describe("§4 domain diff names what moved", () => {
116
+ it("confirming a mapping + adding a composition rule changes canonicalMap and policy", () => {
117
+ const preimageBase = projectContractPreimage(catalogFixtureA);
118
+ const vNext: ContractCatalogInput = {
119
+ ...catalogFixtureA,
120
+ canonicalMappings: (catalogFixtureA.canonicalMappings ?? []).map((mapping) =>
121
+ mapping.status === "proposed" ? { ...mapping, status: "confirmed" as const } : mapping
122
+ ),
123
+ policy: {
124
+ ...catalogFixtureA.policy,
125
+ compositionPatterns: [
126
+ ...(catalogFixtureA.policy?.compositionPatterns ?? []),
127
+ { id: "dialog-structure", region: "Dialog", require: ["Dialog.Title"] },
128
+ ],
129
+ },
130
+ };
131
+ const preimageNext = projectContractPreimage(vNext);
132
+ expect(diffContractDomains(preimageBase, preimageNext).sort()).toEqual([
133
+ "canonicalMap",
134
+ "policy",
135
+ ]);
136
+ });
137
+
138
+ it("identical preimages diff to nothing", () => {
139
+ const preimage = projectContractPreimage(catalogFixtureA);
140
+ expect(diffContractDomains(preimage, projectContractPreimage(catalogFixtureA))).toEqual([]);
141
+ });
142
+ });
143
+
144
+ describe("fcid-preimage:v1 package identity compatibility", () => {
145
+ it("keeps old/new owned canonical-map imports byte-identical", () => {
146
+ const historical: ContractCatalogInput = {
147
+ canonicalMappings: [
148
+ {
149
+ component: "button",
150
+ canonical: "Button",
151
+ status: "confirmed",
152
+ importPath: "@fragments-sdk/ui",
153
+ },
154
+ ],
155
+ };
156
+ const current: ContractCatalogInput = {
157
+ canonicalMappings: [
158
+ {
159
+ component: "button",
160
+ canonical: "Button",
161
+ status: "confirmed",
162
+ importPath: "@usefragments/ui",
163
+ },
164
+ ],
165
+ };
166
+ expect(projectContractPreimage(current)).toEqual(projectContractPreimage(historical));
167
+ expect(fcidOf(current)).toBe(fcidOf(historical));
168
+ });
169
+
170
+ it("projects only typed policy identity fields and leaves provenance/repository bytes alone", () => {
171
+ const historical: ContractCatalogInput = {
172
+ policy: {
173
+ rules: {
174
+ canonicalSources: [{ kind: "npm", specifier: "@fragments-sdk/ui" }],
175
+ importRule: {
176
+ kind: "jsx.importPath.prefer",
177
+ from: "@fragments-sdk/ui/legacy",
178
+ to: "@fragments-sdk/ui",
179
+ },
180
+ match: { componentId: "@fragments-sdk/ui#Button" },
181
+ implementationProvenance: "@repo/cloud-ui",
182
+ repository: "fragments-sdk/fragments",
183
+ generated_by: "@fragments-sdk/ui",
184
+ },
185
+ },
186
+ };
187
+ const current: ContractCatalogInput = {
188
+ policy: {
189
+ rules: {
190
+ canonicalSources: [{ kind: "npm", specifier: "@usefragments/ui" }],
191
+ importRule: {
192
+ kind: "jsx.importPath.prefer",
193
+ from: "@usefragments/ui/legacy",
194
+ to: "@usefragments/ui",
195
+ },
196
+ match: { componentId: "@usefragments/ui#Button" },
197
+ implementationProvenance: "@repo/cloud-ui",
198
+ repository: "fragments-sdk/fragments",
199
+ // A generic marker is not an npm-identity field and must not be
200
+ // normalized just because its value resembles a package.
201
+ generated_by: "@fragments-sdk/ui",
202
+ },
203
+ },
204
+ };
205
+ expect(projectContractPreimage(current)).toEqual(projectContractPreimage(historical));
206
+
207
+ const changedMarker: ContractCatalogInput = {
208
+ policy: {
209
+ rules: {
210
+ ...(current.policy?.rules ?? {}),
211
+ generated_by: "@usefragments/ui",
212
+ },
213
+ },
214
+ };
215
+ expect(projectContractPreimage(changedMarker).domains.policy).not.toBe(
216
+ projectContractPreimage(current).domains.policy
217
+ );
218
+ });
219
+
220
+ it("does not normalize third-party package identities", () => {
221
+ const acme = projectContractPreimage({
222
+ canonicalMappings: [
223
+ {
224
+ component: "button",
225
+ canonical: "Button",
226
+ status: "confirmed",
227
+ importPath: "@acme/ui",
228
+ },
229
+ ],
230
+ });
231
+ const other = projectContractPreimage({
232
+ canonicalMappings: [
233
+ {
234
+ component: "button",
235
+ canonical: "Button",
236
+ status: "confirmed",
237
+ importPath: "@other/ui",
238
+ },
239
+ ],
240
+ });
241
+ expect(acme.domains.canonicalMap).not.toBe(other.domains.canonicalMap);
242
+ });
243
+ });
244
+
245
+ describe("formatStampPin", () => {
246
+ it("renders the display convention: version + abbreviated fcid", () => {
247
+ expect(
248
+ formatStampPin({
249
+ contractVersion: 12,
250
+ fcid: "a3f9e21b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b785",
251
+ })
252
+ ).toBe("contract v12 · a3f9e21");
253
+ });
254
+ });
@@ -0,0 +1,365 @@
1
+ /**
2
+ * Frontend Contract preimage — the projection that defines what the contract IS.
3
+ *
4
+ * The FCID used to be a hash of the raw `code-contracts` artifact payload. That
5
+ * conflated identity with display: a copy-edit to a component description moved
6
+ * the hash, and tokens / canonical mappings / policy — the things gates actually
7
+ * enforce — were not part of identity at all. This module widens the preimage to
8
+ * the full enforcement surface as ONE shared projection, imported by both Cloud
9
+ * (mint) and the CLI (reportedHash). Neither side may reimplement it: parity is
10
+ * a property of the shared import, not a test you can fail.
11
+ *
12
+ * Enforced-only is implemented structurally: {@link projectContractPreimage}
13
+ * copies an explicit pick-list of fields per domain. Anything an adapter passes
14
+ * beyond that list (descriptions, docs URLs, timestamps, heuristic signals)
15
+ * never reaches the hash. Proposed canonical mappings are excluded for the same
16
+ * reason — they cannot block, so they are not identity; confirmation is the
17
+ * moment a mapping enters the contract.
18
+ *
19
+ * The preimage itself is the record of four domain sub-hashes, so
20
+ * `contractHash(preimage)` is the fcid and a domain diff is a four-string
21
+ * compare — no deep structural diffing. Determinism (key sorting, undefined
22
+ * dropping, number normalization) is inherited from `canonicalPreimage`; this
23
+ * module adds element-level sorting for the arrays it owns and nothing else.
24
+ *
25
+ * Runtime-portable by construction: no Node APIs, no Convex imports, no React.
26
+ */
27
+ import type { GovernanceConfig } from "../governance.js";
28
+ import { projectV1OwnedComponentId, projectV1OwnedImportIdentity } from "../package-identity.js";
29
+ import { canonicalPreimage, contractHash } from "./hash.js";
30
+
31
+ // ---------------------------------------------------------------------------
32
+ // Domains
33
+ // ---------------------------------------------------------------------------
34
+
35
+ /**
36
+ * The fixed domain enum. Adding a domain is a spine change (update
37
+ * `01-architecture.md` first); never rename — deprecate and alias.
38
+ */
39
+ export const CONTRACT_DOMAINS = ["components", "tokens", "canonicalMap", "policy"] as const;
40
+
41
+ export type ContractDomain = (typeof CONTRACT_DOMAINS)[number];
42
+
43
+ // ---------------------------------------------------------------------------
44
+ // Catalog input — the neutral shape adapters assemble
45
+ // ---------------------------------------------------------------------------
46
+
47
+ /**
48
+ * One component's enforcement identity. Props are the *name* membership surface
49
+ * (what unknown-prop checks today); prop types/defaults are not enforced by the
50
+ * rules engine, so they are not identity — enforced-only cuts both ways.
51
+ */
52
+ export interface ContractComponentInput {
53
+ name: string;
54
+ /** Parent component name when this is a subcomponent (compound identity). */
55
+ parentName?: string;
56
+ /** Prop names the component accepts. */
57
+ props?: readonly string[];
58
+ /**
59
+ * The per-component enforcement contract as authored (bans, a11y rules,
60
+ * required compound children). Passed through whole — it is enforcement data
61
+ * by definition.
62
+ */
63
+ contract?: unknown;
64
+ /** Required compound-children names, when the structure is declared. */
65
+ compoundChildren?: readonly string[];
66
+ }
67
+
68
+ export interface ContractTokenInput {
69
+ name: string;
70
+ value: string | number;
71
+ type?: string;
72
+ }
73
+
74
+ export type CanonicalMappingStatus = "confirmed" | "proposed" | "unknown";
75
+
76
+ export interface ContractPropMappingInput {
77
+ rawProp: string;
78
+ canonicalProp: string;
79
+ valueMap?: Readonly<Record<string, string>>;
80
+ }
81
+
82
+ export interface ContractCanonicalMappingInput {
83
+ /** The raw/source component the mapping covers. */
84
+ component: string;
85
+ /** The canonical component it maps to. */
86
+ canonical: string;
87
+ status: CanonicalMappingStatus;
88
+ /** Local/offline canonical import target, when the mapping is config-authored. */
89
+ importPath?: string;
90
+ /** Explicit raw element or semantic resolutions covered by a local declaration. */
91
+ resolves?: readonly {
92
+ tag: string;
93
+ role?: string;
94
+ inputType?: string;
95
+ }[];
96
+ propMapping?: readonly ContractPropMappingInput[];
97
+ }
98
+
99
+ /**
100
+ * An authored waiver record. Hashed as data — including `expiresOn` as a plain
101
+ * date string. Expiry *evaluation* happens at enforcement time and must never
102
+ * change the fcid (no time-dependent hashing).
103
+ */
104
+ export interface ContractWaiverInput {
105
+ id: string;
106
+ /** The diagnostic code or rule the waiver suppresses. */
107
+ target: string;
108
+ reason: string;
109
+ /** YYYY-MM-DD, treated as opaque data. */
110
+ expiresOn?: string;
111
+ }
112
+
113
+ /**
114
+ * Active policy: everything that changes what a gate would allow or deny.
115
+ * Waivers live here (spine open question 1, resolved): a waiver changes what
116
+ * blocks, so it is enforcement-relevant.
117
+ */
118
+ export interface ContractPolicyInput {
119
+ rules?: Readonly<Record<string, unknown>>;
120
+ codes?: Readonly<Record<string, unknown>>;
121
+ compositionPatterns?: readonly unknown[];
122
+ waivers?: readonly ContractWaiverInput[];
123
+ }
124
+
125
+ /**
126
+ * The neutral catalog shape both adapters (Cloud mint, CLI scan) assemble.
127
+ * Adapters gather data; the projection owns all normalization — element order,
128
+ * optional fields, and display-data stripping never reach the hash.
129
+ */
130
+ export interface ContractCatalogInput {
131
+ components?: readonly ContractComponentInput[];
132
+ tokens?: readonly ContractTokenInput[];
133
+ canonicalMappings?: readonly ContractCanonicalMappingInput[];
134
+ policy?: ContractPolicyInput | null;
135
+ }
136
+
137
+ // ---------------------------------------------------------------------------
138
+ // Preimage
139
+ // ---------------------------------------------------------------------------
140
+
141
+ export const CONTRACT_PREIMAGE_SCHEMA = "fcid-preimage:v1" as const;
142
+
143
+ /**
144
+ * The hashed identity record: four domain sub-hashes plus the projection schema
145
+ * version (so an intentional projection change mints a new fcid). The fcid is
146
+ * `contractHash(preimage)` — hash of the domain hashes, per the spine pipeline.
147
+ */
148
+ export interface ContractPreimage {
149
+ schema: typeof CONTRACT_PREIMAGE_SCHEMA;
150
+ domains: Record<ContractDomain, string>;
151
+ }
152
+
153
+ /**
154
+ * Sort projected elements by their canonical serialization: total, deterministic,
155
+ * and independent of adapter iteration order, without per-type comparators.
156
+ */
157
+ function sortCanonical<T>(items: readonly T[]): T[] {
158
+ return items
159
+ .map((item) => ({ item, key: canonicalPreimage(item) }))
160
+ .sort((a, b) => (a.key < b.key ? -1 : a.key > b.key ? 1 : 0))
161
+ .map(({ item }) => item);
162
+ }
163
+
164
+ function projectComponents(components: readonly ContractComponentInput[] | undefined): unknown {
165
+ return sortCanonical(
166
+ (components ?? []).map((component) => ({
167
+ name: component.name,
168
+ parentName: component.parentName,
169
+ props: component.props ? [...component.props].sort() : undefined,
170
+ contract: component.contract,
171
+ compoundChildren: component.compoundChildren
172
+ ? [...component.compoundChildren].sort()
173
+ : undefined,
174
+ }))
175
+ );
176
+ }
177
+
178
+ function projectTokens(tokens: readonly ContractTokenInput[] | undefined): unknown {
179
+ return sortCanonical(
180
+ (tokens ?? []).map((token) => ({
181
+ name: token.name,
182
+ value: token.value,
183
+ type: token.type,
184
+ }))
185
+ );
186
+ }
187
+
188
+ function projectCanonicalMap(
189
+ mappings: readonly ContractCanonicalMappingInput[] | undefined
190
+ ): unknown {
191
+ return sortCanonical(
192
+ (mappings ?? [])
193
+ .filter((mapping) => mapping.status === "confirmed")
194
+ .map((mapping) => ({
195
+ component: mapping.component,
196
+ canonical: mapping.canonical,
197
+ importPath: mapping.importPath
198
+ ? projectV1OwnedImportIdentity(mapping.importPath)
199
+ : undefined,
200
+ resolves: mapping.resolves ? sortCanonical(mapping.resolves) : undefined,
201
+ propMapping: mapping.propMapping
202
+ ? sortCanonical(
203
+ mapping.propMapping.map((entry) => ({
204
+ rawProp: entry.rawProp,
205
+ canonicalProp: entry.canonicalProp,
206
+ valueMap: entry.valueMap,
207
+ }))
208
+ )
209
+ : undefined,
210
+ }))
211
+ );
212
+ }
213
+
214
+ /**
215
+ * Project only structured npm-identity fields inside policy material. This is
216
+ * deliberately key- and record-aware: arbitrary strings, repository names,
217
+ * provenance, product markers, and third-party package values retain their
218
+ * original bytes.
219
+ */
220
+ function projectV1PolicyMaterial(value: unknown, key?: string): unknown {
221
+ if (typeof value === "string") {
222
+ if (key === "specifier" || key === "importPath") {
223
+ return projectV1OwnedImportIdentity(value);
224
+ }
225
+ if (key === "componentId") {
226
+ return projectV1OwnedComponentId(value);
227
+ }
228
+ return value;
229
+ }
230
+ if (Array.isArray(value)) {
231
+ return value.map((entry) => projectV1PolicyMaterial(entry));
232
+ }
233
+ if (!value || typeof value !== "object") return value;
234
+
235
+ const record = value as Readonly<Record<string, unknown>>;
236
+ const isImportPathRule = record.kind === "jsx.importPath.prefer";
237
+ return Object.fromEntries(
238
+ Object.entries(record).map(([field, fieldValue]) => {
239
+ if (
240
+ isImportPathRule &&
241
+ (field === "from" || field === "to") &&
242
+ typeof fieldValue === "string"
243
+ ) {
244
+ return [field, projectV1OwnedImportIdentity(fieldValue)];
245
+ }
246
+ return [field, projectV1PolicyMaterial(fieldValue, field)];
247
+ })
248
+ );
249
+ }
250
+
251
+ function projectPolicy(policy: ContractPolicyInput | null | undefined): unknown {
252
+ if (!policy) return null;
253
+ return {
254
+ rules: projectV1PolicyMaterial(policy.rules),
255
+ codes: policy.codes,
256
+ compositionPatterns: policy.compositionPatterns
257
+ ? sortCanonical(policy.compositionPatterns)
258
+ : undefined,
259
+ waivers: policy.waivers
260
+ ? sortCanonical(
261
+ policy.waivers.map((waiver) => ({
262
+ id: waiver.id,
263
+ target: waiver.target,
264
+ reason: waiver.reason,
265
+ expiresOn: waiver.expiresOn,
266
+ }))
267
+ )
268
+ : undefined,
269
+ };
270
+ }
271
+
272
+ /**
273
+ * Project a catalog onto the contract preimage: four enforced-only domain
274
+ * bodies, each hashed independently. `contractHash(projectContractPreimage(x))`
275
+ * is the fcid.
276
+ */
277
+ export function projectContractPreimage(catalog: ContractCatalogInput): ContractPreimage {
278
+ return {
279
+ schema: CONTRACT_PREIMAGE_SCHEMA,
280
+ domains: {
281
+ components: contractHash(projectComponents(catalog.components)),
282
+ tokens: contractHash(projectTokens(catalog.tokens)),
283
+ canonicalMap: contractHash(projectCanonicalMap(catalog.canonicalMappings)),
284
+ policy: contractHash(projectPolicy(catalog.policy)),
285
+ },
286
+ };
287
+ }
288
+
289
+ /**
290
+ * Name the domains whose identity differs between two preimages — a four-string
291
+ * compare, no structural diffing. Returned in fixed {@link CONTRACT_DOMAINS}
292
+ * order. Staleness checks compare these, never whole fcids.
293
+ */
294
+ export function diffContractDomains(a: ContractPreimage, b: ContractPreimage): ContractDomain[] {
295
+ return CONTRACT_DOMAINS.filter((domain) => a.domains[domain] !== b.domains[domain]);
296
+ }
297
+
298
+ /**
299
+ * Map defineFragment-shaped records — the `code-contracts` payload's
300
+ * `fragments` array (Cloud mint) or the CLI's loaded fragment definitions —
301
+ * onto their enforcement identity. The one shared mapping is what keeps the
302
+ * two adapters from drifting into a second "catalog → preimage"
303
+ * implementation: both sides gather fragments however they like, then this
304
+ * picks the identity fields. Records without a string `meta.name` are skipped
305
+ * (no identity to hash).
306
+ */
307
+ /**
308
+ * The enforced-only slice of a resolved CLI governance config — the fields
309
+ * that change what a scan allows or denies. CI gate options, agent repair
310
+ * order, and runner config change how/when gates run, not what they enforce,
311
+ * so they are not identity. This pick-list lives here, beside the projection,
312
+ * so adapters cannot drift into private enforced-only boundaries: a new
313
+ * enforcement field on {@link GovernanceConfig} is added to the contract in
314
+ * exactly one place.
315
+ */
316
+ export function contractPolicyFromGovernanceConfig(
317
+ config: GovernanceConfig | null | undefined
318
+ ): ContractPolicyInput | null {
319
+ if (!config) return null;
320
+ return {
321
+ rules: {
322
+ severity: config.severity,
323
+ rules: config.rules,
324
+ scales: config.scales,
325
+ styles: config.styles,
326
+ jsx: config.jsx,
327
+ tailwind: config.tailwind,
328
+ components: config.components,
329
+ overrides: config.overrides,
330
+ },
331
+ codes: config.codes as Readonly<Record<string, unknown>> | undefined,
332
+ compositionPatterns: config.compositionPatterns as readonly unknown[] | undefined,
333
+ };
334
+ }
335
+
336
+ export function contractComponentsFromFragments(
337
+ fragments: readonly unknown[]
338
+ ): ContractComponentInput[] {
339
+ const components: ContractComponentInput[] = [];
340
+ for (const candidate of fragments) {
341
+ const fragment = candidate as {
342
+ meta?: { name?: unknown; parentComponentName?: unknown };
343
+ props?: Record<string, unknown>;
344
+ contract?: unknown;
345
+ structure?: { compoundChildren?: unknown };
346
+ } | null;
347
+ const name = fragment?.meta?.name;
348
+ if (typeof name !== "string" || !name) continue;
349
+ const parentName = fragment?.meta?.parentComponentName;
350
+ const rawChildren = fragment?.structure?.compoundChildren;
351
+ const compoundChildren = Array.isArray(rawChildren)
352
+ ? rawChildren
353
+ .map((child) => (child as { name?: unknown } | null)?.name)
354
+ .filter((childName): childName is string => typeof childName === "string")
355
+ : undefined;
356
+ components.push({
357
+ name,
358
+ parentName: typeof parentName === "string" ? parentName : undefined,
359
+ props: Object.keys(fragment?.props ?? {}),
360
+ contract: fragment?.contract,
361
+ compoundChildren,
362
+ });
363
+ }
364
+ return components;
365
+ }