@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,488 @@
1
+ import type {
2
+ PreviewComponentDefinition,
3
+ PreviewDesignComparisonReference,
4
+ PreviewDesignComparisonState,
5
+ PreviewDesignReference,
6
+ PreviewViewport,
7
+ } from "./types";
8
+ import { analyzeComparisonContext } from "./comparison-context";
9
+ import { scoreComparison, resolveStatus, summarizeStatus } from "./comparison-score";
10
+ import { compareStyles } from "./comparison-styles";
11
+ import { formatDelta, formatPercent, round } from "./comparison-format";
12
+ import { isPositiveComparisonSize, normalizeImageDiffEvidence } from "./comparison-evidence";
13
+ import { buildFigmaDesignSource, normalizePreviewDesignReference } from "./comparison-references";
14
+
15
+ export type PreviewComparisonSourceKind = "figma" | "preview";
16
+
17
+ export type PreviewComparisonStatus = "pass" | "review" | "fail" | "incomplete";
18
+
19
+ export type PreviewComparisonSeverity = "info" | "warning" | "error";
20
+
21
+ export interface PreviewComparisonSize {
22
+ width: number;
23
+ height: number;
24
+ }
25
+
26
+ export interface PreviewComparisonBounds extends PreviewComparisonSize {
27
+ x: number;
28
+ y: number;
29
+ }
30
+
31
+ export type PreviewComparisonStyleMap = Record<string, string | undefined>;
32
+
33
+ export interface PreviewComparisonSource {
34
+ kind: PreviewComparisonSourceKind;
35
+ label: string;
36
+ imageDataUrl?: string;
37
+ imageUrl?: string;
38
+ size?: PreviewComparisonSize;
39
+ styles?: PreviewComparisonStyleMap;
40
+ capturedAt?: string;
41
+ }
42
+
43
+ export interface FigmaDesignComparisonSource extends PreviewComparisonSource {
44
+ kind: "figma";
45
+ fileKey?: string;
46
+ nodeId?: string;
47
+ nodeUrl?: string;
48
+ }
49
+
50
+ export interface RenderedPreviewComparisonSource extends PreviewComparisonSource {
51
+ kind: "preview";
52
+ componentKey: string;
53
+ variantId?: string;
54
+ theme?: string;
55
+ viewport?: PreviewViewport;
56
+ }
57
+
58
+ export interface PreviewComparisonThresholds {
59
+ imageDiffPercentage: number;
60
+ dimensionPx: number;
61
+ numericPx: number;
62
+ colorChannel: number;
63
+ alpha: number;
64
+ }
65
+
66
+ export interface PreviewImageDiffInput {
67
+ diffPercentage: number;
68
+ changedRegions?: PreviewComparisonBounds[];
69
+ diffImageDataUrl?: string;
70
+ diffImageUrl?: string;
71
+ diffPixelCount?: number;
72
+ totalPixels?: number;
73
+ }
74
+
75
+ export interface PreviewComparisonInput {
76
+ design: FigmaDesignComparisonSource;
77
+ preview: RenderedPreviewComparisonSource;
78
+ imageDiff?: PreviewImageDiffInput;
79
+ thresholds?: Partial<PreviewComparisonThresholds>;
80
+ findings?: PreviewComparisonFinding[];
81
+ context?: PreviewComparisonContext;
82
+ }
83
+
84
+ export interface BuildPreviewComparisonReportInput {
85
+ component: PreviewComponentDefinition;
86
+ variantId?: string;
87
+ designReference?: PreviewDesignReference;
88
+ designReferenceScope?: "component" | "variant";
89
+ preview?: Partial<Omit<RenderedPreviewComparisonSource, "kind" | "componentKey">> & {
90
+ componentKey?: string;
91
+ };
92
+ imageDiff?: PreviewImageDiffInput;
93
+ thresholds?: Partial<PreviewComparisonThresholds>;
94
+ }
95
+
96
+ export interface PreviewStyleComparisonItem {
97
+ property: string;
98
+ design: string;
99
+ preview: string;
100
+ match: boolean;
101
+ }
102
+
103
+ export interface PreviewComparisonFinding {
104
+ code:
105
+ | "image-diff-threshold-exceeded"
106
+ | "dimension-mismatch"
107
+ | "style-mismatch"
108
+ | "missing-comparison-signal"
109
+ | "invalid-comparison-signal"
110
+ | "stale-comparison-signal";
111
+ severity: PreviewComparisonSeverity;
112
+ message: string;
113
+ bounds?: PreviewComparisonBounds;
114
+ property?: string;
115
+ designValue?: string;
116
+ previewValue?: string;
117
+ }
118
+
119
+ export type PreviewComparisonReferenceScope = "component" | "variant";
120
+
121
+ export type PreviewComparisonContextMismatch = "variant" | "theme" | "viewport";
122
+
123
+ export interface PreviewComparisonContext {
124
+ scope: PreviewComparisonReferenceScope;
125
+ captured?: PreviewDesignComparisonState;
126
+ active: PreviewDesignComparisonState;
127
+ stale: boolean;
128
+ staleReasons: PreviewComparisonContextMismatch[];
129
+ }
130
+
131
+ export interface PreviewComparisonReport {
132
+ status: PreviewComparisonStatus;
133
+ score: number | null;
134
+ summary: string;
135
+ design: FigmaDesignComparisonSource;
136
+ preview: RenderedPreviewComparisonSource;
137
+ thresholds: PreviewComparisonThresholds;
138
+ image?: {
139
+ match: boolean;
140
+ diffPercentage: number;
141
+ diffImageDataUrl?: string;
142
+ diffImageUrl?: string;
143
+ changedRegions: PreviewComparisonBounds[];
144
+ diffPixelCount?: number;
145
+ totalPixels?: number;
146
+ };
147
+ dimensions?: {
148
+ match: boolean;
149
+ design: PreviewComparisonSize;
150
+ preview: PreviewComparisonSize;
151
+ delta: PreviewComparisonSize;
152
+ };
153
+ styles?: {
154
+ match: boolean;
155
+ properties: PreviewStyleComparisonItem[];
156
+ };
157
+ findings: PreviewComparisonFinding[];
158
+ context?: PreviewComparisonContext;
159
+ }
160
+
161
+ export type PreviewComparisonSignalId = "image" | "dimensions" | "styles";
162
+
163
+ export type PreviewComparisonSignalStatus = "pass" | "review" | "fail" | "missing";
164
+
165
+ export interface PreviewComparisonSignal {
166
+ id: PreviewComparisonSignalId;
167
+ label: string;
168
+ status: PreviewComparisonSignalStatus;
169
+ value: string;
170
+ detail: string;
171
+ }
172
+
173
+ export interface PreviewComparisonContextSummary {
174
+ status: "current" | "stale";
175
+ label: string;
176
+ value: string;
177
+ detail: string;
178
+ }
179
+
180
+ export interface PreviewComparisonRegionOverlay extends PreviewComparisonBounds {
181
+ label: string;
182
+ leftPercent: number;
183
+ topPercent: number;
184
+ widthPercent: number;
185
+ heightPercent: number;
186
+ }
187
+
188
+ const DEFAULT_THRESHOLDS: PreviewComparisonThresholds = {
189
+ imageDiffPercentage: 1,
190
+ dimensionPx: 1,
191
+ numericPx: 1,
192
+ colorChannel: 5,
193
+ alpha: 0.05,
194
+ };
195
+
196
+ export function comparePreviewToDesign(input: PreviewComparisonInput): PreviewComparisonReport {
197
+ const thresholds = resolvePreviewComparisonThresholds(input.thresholds);
198
+ const findings: PreviewComparisonFinding[] = [...(input.findings ?? [])];
199
+ const image = compareImageDiff(input.imageDiff, thresholds, findings);
200
+ const dimensions = compareDimensions(input.design.size, input.preview.size, thresholds, findings);
201
+ const styles = compareStyles(input.design.styles, input.preview.styles, thresholds, findings);
202
+ const score = scoreComparison({ image, dimensions, styles });
203
+
204
+ if (score === null) {
205
+ findings.push({
206
+ code: "missing-comparison-signal",
207
+ severity: "warning",
208
+ message: "No image, dimension, or style comparison data was provided.",
209
+ });
210
+ }
211
+
212
+ const status = resolveStatus(score, findings);
213
+
214
+ return {
215
+ status,
216
+ score,
217
+ summary: summarizeStatus(status, score, findings),
218
+ design: input.design,
219
+ preview: input.preview,
220
+ thresholds,
221
+ ...(image && { image }),
222
+ ...(dimensions && { dimensions }),
223
+ ...(styles && { styles }),
224
+ findings,
225
+ ...(input.context && { context: input.context }),
226
+ };
227
+ }
228
+
229
+ export function resolvePreviewComparisonThresholds(
230
+ thresholds?: Partial<PreviewComparisonThresholds> | null
231
+ ): PreviewComparisonThresholds {
232
+ return {
233
+ imageDiffPercentage: readThreshold(
234
+ thresholds?.imageDiffPercentage,
235
+ DEFAULT_THRESHOLDS.imageDiffPercentage,
236
+ { min: 0, max: 100 }
237
+ ),
238
+ dimensionPx: readThreshold(thresholds?.dimensionPx, DEFAULT_THRESHOLDS.dimensionPx, {
239
+ min: 0,
240
+ }),
241
+ numericPx: readThreshold(thresholds?.numericPx, DEFAULT_THRESHOLDS.numericPx, { min: 0 }),
242
+ colorChannel: readThreshold(thresholds?.colorChannel, DEFAULT_THRESHOLDS.colorChannel, {
243
+ min: 0,
244
+ max: 255,
245
+ }),
246
+ alpha: readThreshold(thresholds?.alpha, DEFAULT_THRESHOLDS.alpha, { min: 0, max: 1 }),
247
+ };
248
+ }
249
+
250
+ export function buildPreviewComparisonReport(
251
+ input: BuildPreviewComparisonReportInput
252
+ ): PreviewComparisonReport | null {
253
+ const variant = input.variantId
254
+ ? input.component.variants.find((candidate) => candidate.id === input.variantId)
255
+ : undefined;
256
+ const rawReference = input.designReference ?? variant?.design ?? input.component.design;
257
+ const reference = normalizePreviewDesignReference(rawReference);
258
+ if (!reference) return null;
259
+
260
+ const fallbackLabel = `${input.component.displayName}${variant ? ` / ${variant.name}` : ""}`;
261
+ const comparison = reference.comparison;
262
+ const comparisonContext = analyzeComparisonContext(
263
+ comparison,
264
+ input.preview,
265
+ input.variantId,
266
+ input.component,
267
+ resolveDesignReferenceScope(input, rawReference, variant)
268
+ );
269
+ const scoredComparison = comparisonContext.findings.length === 0 ? comparison : undefined;
270
+ const imageDiff = input.imageDiff ?? imageDiffFromComparison(scoredComparison);
271
+ const previewSize = scoredComparison?.previewSize ?? input.preview?.size;
272
+
273
+ return comparePreviewToDesign({
274
+ design: {
275
+ ...buildFigmaDesignSource(reference, fallbackLabel),
276
+ ...(scoredComparison?.designStyles && { styles: scoredComparison.designStyles }),
277
+ },
278
+ preview: {
279
+ kind: "preview",
280
+ label: input.preview?.label ?? fallbackLabel,
281
+ componentKey: input.preview?.componentKey ?? input.component.componentKey,
282
+ ...((input.preview?.variantId ?? input.variantId) && {
283
+ variantId: input.preview?.variantId ?? input.variantId,
284
+ }),
285
+ ...(input.preview?.theme && { theme: input.preview.theme }),
286
+ ...(input.preview?.viewport && { viewport: input.preview.viewport }),
287
+ ...(previewSize && { size: previewSize }),
288
+ ...((input.preview?.styles ?? scoredComparison?.previewStyles) && {
289
+ styles: input.preview?.styles ?? scoredComparison?.previewStyles,
290
+ }),
291
+ ...(input.preview?.imageDataUrl && { imageDataUrl: input.preview.imageDataUrl }),
292
+ ...((input.preview?.imageUrl ?? scoredComparison?.previewImageUrl) && {
293
+ imageUrl: input.preview?.imageUrl ?? scoredComparison?.previewImageUrl,
294
+ }),
295
+ ...((input.preview?.capturedAt ?? scoredComparison?.capturedAt) && {
296
+ capturedAt: input.preview?.capturedAt ?? scoredComparison?.capturedAt,
297
+ }),
298
+ },
299
+ ...(imageDiff && { imageDiff }),
300
+ ...(input.thresholds && { thresholds: input.thresholds }),
301
+ ...(comparisonContext.findings.length > 0 && { findings: comparisonContext.findings }),
302
+ ...(comparisonContext.context && { context: comparisonContext.context }),
303
+ });
304
+ }
305
+
306
+ function resolveDesignReferenceScope(
307
+ input: BuildPreviewComparisonReportInput,
308
+ reference: PreviewDesignReference | undefined,
309
+ variant: PreviewComponentDefinition["variants"][number] | undefined
310
+ ): "component" | "variant" {
311
+ if (input.designReferenceScope) return input.designReferenceScope;
312
+ return reference === variant?.design ? "variant" : "component";
313
+ }
314
+
315
+ export function buildPreviewDesignComparisonEvidence(
316
+ report: PreviewComparisonReport
317
+ ): PreviewDesignComparisonReference | null {
318
+ if (report.context?.stale) return null;
319
+
320
+ const state = buildStoredComparisonState(report.preview);
321
+ const previewImageUrl = report.preview.imageUrl ?? report.preview.imageDataUrl;
322
+ const diffImageUrl = report.image?.diffImageUrl ?? report.image?.diffImageDataUrl;
323
+ const designStyles = cleanStyleMap(report.design.styles);
324
+ const previewStyles = cleanStyleMap(report.preview.styles);
325
+ const previewSize =
326
+ report.preview.size && isPositiveComparisonSize(report.preview.size)
327
+ ? report.preview.size
328
+ : report.dimensions?.preview;
329
+
330
+ const evidence: PreviewDesignComparisonReference = {
331
+ ...(state && { state }),
332
+ ...(previewImageUrl && { previewImageUrl }),
333
+ ...(previewSize && { previewSize }),
334
+ ...(designStyles && { designStyles }),
335
+ ...(previewStyles && { previewStyles }),
336
+ ...(diffImageUrl && { diffImageUrl }),
337
+ ...(report.image && { diffPercentage: report.image.diffPercentage }),
338
+ ...(report.image?.diffPixelCount !== undefined && {
339
+ diffPixelCount: report.image.diffPixelCount,
340
+ }),
341
+ ...(report.image?.totalPixels !== undefined && {
342
+ totalPixels: report.image.totalPixels,
343
+ }),
344
+ ...(report.image?.changedRegions.length && {
345
+ changedRegions: report.image.changedRegions,
346
+ }),
347
+ ...(report.preview.capturedAt && { capturedAt: report.preview.capturedAt }),
348
+ };
349
+
350
+ return Object.keys(evidence).length > 0 ? evidence : null;
351
+ }
352
+
353
+ export function buildPreviewDesignReferenceFromReport(
354
+ report: PreviewComparisonReport
355
+ ): PreviewDesignReference {
356
+ const comparison = buildPreviewDesignComparisonEvidence(report);
357
+ const imageUrl = report.design.imageUrl ?? report.design.imageDataUrl;
358
+
359
+ return {
360
+ provider: "figma",
361
+ label: report.design.label,
362
+ ...(report.design.nodeUrl && { nodeUrl: report.design.nodeUrl }),
363
+ ...(report.design.fileKey && { fileKey: report.design.fileKey }),
364
+ ...(report.design.nodeId && { nodeId: report.design.nodeId }),
365
+ ...(imageUrl && { imageUrl }),
366
+ ...(report.design.size &&
367
+ isPositiveComparisonSize(report.design.size) && {
368
+ size: report.design.size,
369
+ }),
370
+ ...(comparison && { comparison }),
371
+ };
372
+ }
373
+
374
+ function buildStoredComparisonState(
375
+ preview: RenderedPreviewComparisonSource
376
+ ): PreviewDesignComparisonState | undefined {
377
+ const state: PreviewDesignComparisonState = {
378
+ ...(preview.variantId && { variantId: preview.variantId }),
379
+ ...(preview.theme && { theme: preview.theme }),
380
+ ...(preview.viewport && { viewport: preview.viewport }),
381
+ };
382
+ return Object.keys(state).length > 0 ? state : undefined;
383
+ }
384
+
385
+ function cleanStyleMap(
386
+ styles: PreviewComparisonStyleMap | undefined
387
+ ): Record<string, string> | undefined {
388
+ if (!styles) return undefined;
389
+ const entries = Object.entries(styles).filter(
390
+ (entry): entry is [string, string] => typeof entry[1] === "string"
391
+ );
392
+ return entries.length > 0 ? Object.fromEntries(entries) : undefined;
393
+ }
394
+
395
+ function imageDiffFromComparison(
396
+ comparison: PreviewDesignComparisonReference | undefined
397
+ ): PreviewImageDiffInput | undefined {
398
+ if (!comparison || comparison.diffPercentage === undefined) return undefined;
399
+
400
+ return {
401
+ diffPercentage: comparison.diffPercentage,
402
+ changedRegions: comparison.changedRegions,
403
+ diffImageUrl: comparison.diffImageUrl,
404
+ diffPixelCount: comparison.diffPixelCount,
405
+ totalPixels: comparison.totalPixels,
406
+ };
407
+ }
408
+
409
+ function compareImageDiff(
410
+ imageDiff: PreviewImageDiffInput | undefined,
411
+ thresholds: PreviewComparisonThresholds,
412
+ findings: PreviewComparisonFinding[]
413
+ ): PreviewComparisonReport["image"] | undefined {
414
+ if (!imageDiff) return undefined;
415
+ const normalized = normalizeImageDiffEvidence(imageDiff, findings);
416
+ if (!normalized) return undefined;
417
+
418
+ const match = normalized.diffPercentage <= thresholds.imageDiffPercentage;
419
+
420
+ if (!match) {
421
+ findings.push({
422
+ code: "image-diff-threshold-exceeded",
423
+ severity: "error",
424
+ message: `Image diff ${formatPercent(normalized.diffPercentage)} exceeds ${formatPercent(
425
+ thresholds.imageDiffPercentage
426
+ )}.`,
427
+ bounds: normalized.changedRegions[0],
428
+ });
429
+ }
430
+
431
+ return {
432
+ match,
433
+ diffPercentage: round(normalized.diffPercentage),
434
+ diffImageDataUrl: normalized.diffImageDataUrl,
435
+ diffImageUrl: normalized.diffImageUrl,
436
+ changedRegions: normalized.changedRegions,
437
+ diffPixelCount: normalized.diffPixelCount,
438
+ totalPixels: normalized.totalPixels,
439
+ };
440
+ }
441
+
442
+ function compareDimensions(
443
+ design: PreviewComparisonSize | undefined,
444
+ preview: PreviewComparisonSize | undefined,
445
+ thresholds: PreviewComparisonThresholds,
446
+ findings: PreviewComparisonFinding[]
447
+ ): PreviewComparisonReport["dimensions"] | undefined {
448
+ if (!design || !preview) return undefined;
449
+ if (!isPositiveComparisonSize(design) || !isPositiveComparisonSize(preview)) {
450
+ findings.push({
451
+ code: "invalid-comparison-signal",
452
+ severity: "warning",
453
+ message: "Design and preview sizes must be finite positive numbers.",
454
+ });
455
+ return undefined;
456
+ }
457
+
458
+ const delta = {
459
+ width: round(preview.width - design.width),
460
+ height: round(preview.height - design.height),
461
+ };
462
+ const match =
463
+ Math.abs(delta.width) <= thresholds.dimensionPx &&
464
+ Math.abs(delta.height) <= thresholds.dimensionPx;
465
+
466
+ if (!match) {
467
+ findings.push({
468
+ code: "dimension-mismatch",
469
+ severity: "warning",
470
+ message: `Preview is ${formatDelta(delta.width)}px wide and ${formatDelta(
471
+ delta.height
472
+ )}px tall compared with the Figma frame.`,
473
+ });
474
+ }
475
+
476
+ return { match, design, preview, delta };
477
+ }
478
+
479
+ function readThreshold(
480
+ value: unknown,
481
+ fallback: number,
482
+ bounds: { min: number; max?: number }
483
+ ): number {
484
+ if (typeof value !== "number" || !Number.isFinite(value)) return fallback;
485
+ if (value < bounds.min) return fallback;
486
+ if (bounds.max !== undefined && value > bounds.max) return fallback;
487
+ return value;
488
+ }
@@ -0,0 +1,10 @@
1
+ export * from "./comparison";
2
+ export * from "./comparison-artifact";
3
+ export * from "./comparison-presentation";
4
+ export {
5
+ buildFigmaDesignSource,
6
+ getPreviewDesignReference,
7
+ parseFigmaNodeUrl,
8
+ } from "./comparison-references";
9
+ export * from "./types";
10
+ export * from "./validation";