@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,157 @@
1
+ import {
2
+ OWNED_PACKAGE_AUTHORING_EPOCH,
3
+ OWNED_PACKAGE_EXPLICIT_POLICIES,
4
+ OWNED_PACKAGE_IDENTITY_SOURCE_SHA256,
5
+ OWNED_PACKAGE_ROOT_POLICIES,
6
+ } from "./package-identity.generated.js";
7
+
8
+ export type OwnedPackageStatus = "retained" | "moved" | "retired";
9
+
10
+ export interface OwnedPackageIdentityResolution {
11
+ readonly input: string;
12
+ readonly owned: boolean;
13
+ readonly identityId?: string;
14
+ readonly status?: OwnedPackageStatus;
15
+ /** Supported public read target. Absent for retired identities. */
16
+ readonly canonicalSpecifier?: string;
17
+ /** What first-party generators write in the active authoring epoch. */
18
+ readonly authoringSpecifier?: string;
19
+ /** Permanent legacy spelling used only inside the fcid-preimage:v1 domain. */
20
+ readonly v1Specifier: string;
21
+ /** Supported public spellings for this exact logical read identity. */
22
+ readonly readEquivalents?: readonly string[];
23
+ }
24
+
25
+ type ExplicitPolicy = (typeof OWNED_PACKAGE_EXPLICIT_POLICIES)[number];
26
+ type RootPolicy = (typeof OWNED_PACKAGE_ROOT_POLICIES)[number];
27
+
28
+ function appendSubpath(root: string | null, suffix: string): string | undefined {
29
+ if (!root) return undefined;
30
+ return suffix ? `${root}/${suffix}` : root;
31
+ }
32
+
33
+ function rootSuffix(specifier: string, root: string): string | null {
34
+ if (specifier === root) return "";
35
+ const prefix = `${root}/`;
36
+ return specifier.startsWith(prefix) ? specifier.slice(prefix.length) : null;
37
+ }
38
+
39
+ function authoringValue(policy: {
40
+ readonly authoring: { readonly legacy: string; readonly current: string | null };
41
+ }): string | undefined {
42
+ return policy.authoring[OWNED_PACKAGE_AUTHORING_EPOCH] ?? undefined;
43
+ }
44
+
45
+ function resolveExplicit(specifier: string): OwnedPackageIdentityResolution | undefined {
46
+ for (const policy of OWNED_PACKAGE_EXPLICIT_POLICIES) {
47
+ if (specifier !== policy.legacySpecifier && specifier !== policy.currentSpecifier) continue;
48
+
49
+ // A target can also be the canonical identity of another package. The
50
+ // historical spelling is always unambiguous. Current targets participate
51
+ // here only when the policy explicitly owns the v1 projection (Preview ->
52
+ // Core/preview); CLI/core deliberately stays distinct from Core root.
53
+ if (specifier === policy.currentSpecifier && !policy.matchCurrentForV1) continue;
54
+
55
+ return {
56
+ input: specifier,
57
+ owned: true,
58
+ identityId: policy.id,
59
+ status: policy.status,
60
+ canonicalSpecifier: policy.currentSpecifier ?? undefined,
61
+ authoringSpecifier: authoringValue(policy),
62
+ v1Specifier: policy.v1Projection,
63
+ readEquivalents: policy.readEquivalents,
64
+ };
65
+ }
66
+ return undefined;
67
+ }
68
+
69
+ function resolveRoot(specifier: string): OwnedPackageIdentityResolution | undefined {
70
+ for (const policy of OWNED_PACKAGE_ROOT_POLICIES) {
71
+ const legacySuffix = rootSuffix(specifier, policy.legacyRoot);
72
+ const currentSuffix = policy.currentRoot ? rootSuffix(specifier, policy.currentRoot) : null;
73
+ const suffix = legacySuffix ?? currentSuffix;
74
+ if (suffix === null) continue;
75
+
76
+ const canonicalSpecifier =
77
+ policy.status === "retained"
78
+ ? appendSubpath(policy.currentRoot, suffix)
79
+ : suffix === ""
80
+ ? (policy.targetSpecifier ?? undefined)
81
+ : undefined;
82
+ const authoredRoot = authoringValue(policy);
83
+
84
+ return {
85
+ input: specifier,
86
+ owned: true,
87
+ identityId: policy.id,
88
+ status: policy.status,
89
+ canonicalSpecifier,
90
+ authoringSpecifier: appendSubpath(authoredRoot ?? null, suffix),
91
+ v1Specifier: appendSubpath(policy.v1Projection, suffix) ?? specifier,
92
+ readEquivalents: policy.readEquivalents.map((root) => appendSubpath(root, suffix) ?? root),
93
+ };
94
+ }
95
+ return undefined;
96
+ }
97
+
98
+ /**
99
+ * Resolve only exact Fragments-owned npm roots/subpaths. Unknown packages and
100
+ * arbitrary strings are intentionally opaque; this is not a substring rewrite.
101
+ */
102
+ export function resolveOwnedPackageImport(specifier: string): OwnedPackageIdentityResolution {
103
+ return (
104
+ resolveExplicit(specifier) ??
105
+ resolveRoot(specifier) ?? {
106
+ input: specifier,
107
+ owned: false,
108
+ v1Specifier: specifier,
109
+ }
110
+ );
111
+ }
112
+
113
+ /**
114
+ * Canonical current-read spelling. Retired/unknown identities are returned
115
+ * unchanged so a caller cannot accidentally expose a private @repo/* target.
116
+ */
117
+ export function canonicalizeOwnedImport(specifier: string): string {
118
+ return resolveOwnedPackageImport(specifier).canonicalSpecifier ?? specifier;
119
+ }
120
+
121
+ /** Active-epoch spelling for generators; retired/unknown inputs stay unchanged. */
122
+ export function authorOwnedImport(specifier: string): string {
123
+ return resolveOwnedPackageImport(specifier).authoringSpecifier ?? specifier;
124
+ }
125
+
126
+ /**
127
+ * Permanent compatibility encoder for fcid-preimage:v1 only. Never use this
128
+ * result for source edits, suggestions, provenance, or generated guidance.
129
+ */
130
+ export function projectV1OwnedImportIdentity(specifier: string): string {
131
+ return resolveOwnedPackageImport(specifier).v1Specifier;
132
+ }
133
+
134
+ export function ownedImportEquivalents(specifier: string): readonly string[] {
135
+ return resolveOwnedPackageImport(specifier).readEquivalents ?? [specifier];
136
+ }
137
+
138
+ function mapComponentId(value: string, mapPackage: (packageName: string) => string): string {
139
+ const separator = value.lastIndexOf("#");
140
+ if (separator <= 0 || separator === value.length - 1) return value;
141
+ const packageName = value.slice(0, separator);
142
+ const componentName = value.slice(separator + 1);
143
+ return `${mapPackage(packageName)}#${componentName}`;
144
+ }
145
+
146
+ export function canonicalizeOwnedComponentId(value: string): string {
147
+ return mapComponentId(value, canonicalizeOwnedImport);
148
+ }
149
+
150
+ /** fcid-preimage:v1 component-id compatibility; subject to the same restriction. */
151
+ export function projectV1OwnedComponentId(value: string): string {
152
+ return mapComponentId(value, projectV1OwnedImportIdentity);
153
+ }
154
+
155
+ /** Exposed for diagnostics and generator tests, not as a second policy owner. */
156
+ export const OWNED_PACKAGE_IDENTITY_EPOCH = OWNED_PACKAGE_AUTHORING_EPOCH;
157
+ export { OWNED_PACKAGE_IDENTITY_SOURCE_SHA256 };
@@ -0,0 +1,142 @@
1
+ /**
2
+ * Performance budget presets and classification utilities.
3
+ *
4
+ * ESLint model: global defaults, zero-config, auto-measurement.
5
+ * Users configure 0-3 numbers. Per-component overrides are the `eslint-disable` equivalent.
6
+ */
7
+
8
+ // ---------------------------------------------------------------------------
9
+ // Types
10
+ // ---------------------------------------------------------------------------
11
+
12
+ export interface PerformanceBudgets {
13
+ /** Maximum gzipped bundle size in bytes */
14
+ bundleSize: number;
15
+ }
16
+
17
+ export interface PerformanceConfig {
18
+ preset: string;
19
+ budgets: PerformanceBudgets;
20
+ }
21
+
22
+ export type ComplexityTier = 'lightweight' | 'moderate' | 'heavy';
23
+
24
+ export interface PerformanceData {
25
+ /** Gzipped bundle size in bytes */
26
+ bundleSize: number;
27
+ /** Raw (minified, not gzipped) bundle size in bytes */
28
+ rawSize: number;
29
+ /** Complexity classification */
30
+ complexity: ComplexityTier;
31
+ /** Percentage of budget used (0-100+) */
32
+ budgetPercent: number;
33
+ /** Whether the component exceeds its budget */
34
+ overBudget: boolean;
35
+ /** ISO timestamp when measured */
36
+ measuredAt: string;
37
+ }
38
+
39
+ export interface PerformanceSummary {
40
+ /** Preset name used */
41
+ preset: string;
42
+ /** Budget applied in bytes */
43
+ budget: number;
44
+ /** Total components measured */
45
+ total: number;
46
+ /** Number of components over budget */
47
+ overBudget: number;
48
+ /** Distribution by tier */
49
+ tiers: Record<ComplexityTier, number>;
50
+ }
51
+
52
+ // ---------------------------------------------------------------------------
53
+ // Presets
54
+ // ---------------------------------------------------------------------------
55
+
56
+ const PRESETS: Record<string, PerformanceBudgets> = {
57
+ strict: { bundleSize: 8 * 1024 }, // 8KB gzipped
58
+ standard: { bundleSize: 15 * 1024 }, // 15KB gzipped
59
+ relaxed: { bundleSize: 30 * 1024 }, // 30KB gzipped
60
+ };
61
+
62
+ export const PRESET_NAMES = Object.keys(PRESETS) as readonly string[];
63
+
64
+ // ---------------------------------------------------------------------------
65
+ // Resolution
66
+ // ---------------------------------------------------------------------------
67
+
68
+ /**
69
+ * Resolve a performance config from user input.
70
+ * Accepts a preset name string or a custom config object.
71
+ */
72
+ export function resolvePerformanceConfig(
73
+ input: string | { preset?: string; budgets?: Partial<PerformanceBudgets> } | undefined
74
+ ): PerformanceConfig {
75
+ if (!input) {
76
+ return { preset: 'standard', budgets: PRESETS.standard };
77
+ }
78
+
79
+ if (typeof input === 'string') {
80
+ const budgets = PRESETS[input];
81
+ if (!budgets) {
82
+ throw new Error(
83
+ `Unknown performance preset "${input}". Available: ${PRESET_NAMES.join(', ')}`
84
+ );
85
+ }
86
+ return { preset: input, budgets };
87
+ }
88
+
89
+ const presetName = input.preset ?? 'standard';
90
+ const baseBudgets = PRESETS[presetName];
91
+ if (!baseBudgets) {
92
+ throw new Error(
93
+ `Unknown performance preset "${presetName}". Available: ${PRESET_NAMES.join(', ')}`
94
+ );
95
+ }
96
+
97
+ return {
98
+ preset: presetName,
99
+ budgets: {
100
+ bundleSize: input.budgets?.bundleSize ?? baseBudgets.bundleSize,
101
+ },
102
+ };
103
+ }
104
+
105
+ // ---------------------------------------------------------------------------
106
+ // Classification
107
+ // ---------------------------------------------------------------------------
108
+
109
+ /**
110
+ * Classify a component's complexity based on gzipped bundle size.
111
+ *
112
+ * - lightweight: < 5KB — simple, leaf components
113
+ * - moderate: < 15KB — typical composed components
114
+ * - heavy: >= 15KB — complex widgets with dependencies
115
+ */
116
+ export function classifyComplexity(gzipBytes: number): ComplexityTier {
117
+ if (gzipBytes < 5 * 1024) return 'lightweight';
118
+ if (gzipBytes < 15 * 1024) return 'moderate';
119
+ return 'heavy';
120
+ }
121
+
122
+ // ---------------------------------------------------------------------------
123
+ // Formatting helpers
124
+ // ---------------------------------------------------------------------------
125
+
126
+ /**
127
+ * Format bytes to a human-readable string (e.g. "2.1KB", "15.3KB").
128
+ */
129
+ export function formatBytes(bytes: number): string {
130
+ if (bytes < 1024) return `${bytes}B`;
131
+ const kb = bytes / 1024;
132
+ return kb < 10 ? `${kb.toFixed(1)}KB` : `${Math.round(kb)}KB`;
133
+ }
134
+
135
+ /**
136
+ * Create a visual budget bar for terminal output.
137
+ */
138
+ export function budgetBar(percent: number, width = 20): string {
139
+ const filled = Math.min(Math.round((percent / 100) * width), width);
140
+ const bar = '█'.repeat(filled) + '░'.repeat(width - filled);
141
+ return percent > 100 ? `\x1b[31m${bar}\x1b[0m` : `\x1b[32m${bar}\x1b[0m`;
142
+ }
@@ -0,0 +1,228 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import {
3
+ applyPreviewComparisonArtifactPayload,
4
+ buildPreviewComparisonArtifactPayload,
5
+ comparePreviewToDesign,
6
+ type PreviewArtifactManifest,
7
+ } from "./index";
8
+
9
+ describe("preview comparison artifact payloads", () => {
10
+ it("applies reusable evidence to a variant design reference", () => {
11
+ const manifest = buildManifest();
12
+ const report = comparePreviewToDesign({
13
+ design: {
14
+ kind: "figma",
15
+ label: "Button / Primary",
16
+ imageDataUrl: "data:image/png;base64,figma",
17
+ size: { width: 120, height: 40 },
18
+ },
19
+ preview: {
20
+ kind: "preview",
21
+ label: "Button / Primary",
22
+ componentKey: "Button",
23
+ variantId: "primary",
24
+ theme: "light",
25
+ viewport: { width: 120, height: 40 },
26
+ imageDataUrl: "data:image/png;base64,preview",
27
+ size: { width: 120, height: 40 },
28
+ },
29
+ imageDiff: {
30
+ diffPercentage: 0.2,
31
+ diffImageDataUrl: "data:image/png;base64,diff",
32
+ },
33
+ });
34
+ const payload = buildPreviewComparisonArtifactPayload({ report });
35
+
36
+ const result = applyPreviewComparisonArtifactPayload(manifest, payload!);
37
+
38
+ expect(result.status).toBe("applied");
39
+ expect(result.applied).toBe(true);
40
+ expect(result.manifest).not.toBe(manifest);
41
+ expect(manifest.components.Button.variants[0]?.design).toBeUndefined();
42
+ expect(result.manifest.components.Button.design).toBeUndefined();
43
+ expect(result.manifest.components.Button.variants[0]?.design).toMatchObject({
44
+ label: "Button / Primary",
45
+ comparison: {
46
+ previewImageUrl: "data:image/png;base64,preview",
47
+ diffImageUrl: "data:image/png;base64,diff",
48
+ diffPercentage: 0.2,
49
+ },
50
+ });
51
+ });
52
+
53
+ it("applies component-scoped evidence to the component design reference", () => {
54
+ const manifest = buildManifest();
55
+ const report = comparePreviewToDesign({
56
+ design: {
57
+ kind: "figma",
58
+ label: "Button / Default",
59
+ imageDataUrl: "data:image/png;base64,figma",
60
+ size: { width: 120, height: 40 },
61
+ },
62
+ preview: {
63
+ kind: "preview",
64
+ label: "Button / Default",
65
+ componentKey: "Button",
66
+ variantId: "primary",
67
+ theme: "light",
68
+ viewport: { width: 120, height: 40 },
69
+ imageDataUrl: "data:image/png;base64,preview",
70
+ size: { width: 120, height: 40 },
71
+ },
72
+ imageDiff: {
73
+ diffPercentage: 0.2,
74
+ },
75
+ context: {
76
+ scope: "component",
77
+ active: {
78
+ variantId: "primary",
79
+ theme: "light",
80
+ viewport: { width: 120, height: 40 },
81
+ },
82
+ stale: false,
83
+ staleReasons: [],
84
+ },
85
+ });
86
+ const payload = buildPreviewComparisonArtifactPayload({ report });
87
+
88
+ const result = applyPreviewComparisonArtifactPayload(manifest, payload!);
89
+
90
+ expect(result.status).toBe("applied");
91
+ expect(result.manifest.components.Button.design).toMatchObject({
92
+ label: "Button / Default",
93
+ comparison: {
94
+ diffPercentage: 0.2,
95
+ },
96
+ });
97
+ expect(result.manifest.components.Button.variants[0]?.design).toBeUndefined();
98
+ });
99
+
100
+ it("refuses to apply stale reports with no reusable evidence", () => {
101
+ const manifest = buildManifest();
102
+ const report = comparePreviewToDesign({
103
+ design: {
104
+ kind: "figma",
105
+ label: "Button",
106
+ size: { width: 120, height: 40 },
107
+ },
108
+ preview: {
109
+ kind: "preview",
110
+ label: "Button",
111
+ componentKey: "Button",
112
+ size: { width: 120, height: 40 },
113
+ },
114
+ context: {
115
+ scope: "component",
116
+ captured: { theme: "light" },
117
+ active: { theme: "dark" },
118
+ stale: true,
119
+ staleReasons: ["theme"],
120
+ },
121
+ });
122
+ const payload = buildPreviewComparisonArtifactPayload({ report });
123
+
124
+ const result = applyPreviewComparisonArtifactPayload(manifest, payload!);
125
+
126
+ expect(result).toMatchObject({
127
+ status: "missing-comparison-evidence",
128
+ applied: false,
129
+ manifest,
130
+ });
131
+ });
132
+
133
+ it("reports missing target components and variants without mutating the manifest", () => {
134
+ const manifest = buildManifest();
135
+ const report = comparePreviewToDesign({
136
+ design: { kind: "figma", label: "Missing", size: { width: 120, height: 40 } },
137
+ preview: {
138
+ kind: "preview",
139
+ label: "Missing",
140
+ componentKey: "Missing",
141
+ variantId: "primary",
142
+ size: { width: 120, height: 40 },
143
+ },
144
+ imageDiff: { diffPercentage: 0.2 },
145
+ });
146
+ const payload = buildPreviewComparisonArtifactPayload({ report });
147
+
148
+ expect(applyPreviewComparisonArtifactPayload(manifest, payload!)).toMatchObject({
149
+ status: "component-not-found",
150
+ applied: false,
151
+ manifest,
152
+ });
153
+
154
+ expect(
155
+ applyPreviewComparisonArtifactPayload(manifest, {
156
+ ...payload!,
157
+ target: { ...payload!.target, componentKey: "Button", variantId: "missing" },
158
+ })
159
+ ).toMatchObject({
160
+ status: "variant-not-found",
161
+ applied: false,
162
+ manifest,
163
+ });
164
+ });
165
+ });
166
+
167
+ function buildManifest(): PreviewArtifactManifest {
168
+ return {
169
+ artifactVersion: "preview-artifact/v1",
170
+ identity: {
171
+ artifactId: "artifact",
172
+ artifactVersion: "preview-artifact/v1",
173
+ orgId: "org",
174
+ projectId: "project",
175
+ bindingId: "binding",
176
+ repoFullName: "acme/ui",
177
+ commitSha: "abc123",
178
+ sourceRef: "main",
179
+ createdAt: "2026-05-24T00:00:00.000Z",
180
+ },
181
+ builder: {
182
+ name: "test",
183
+ version: "1.0.0",
184
+ typecheck: "none",
185
+ sources: ["manual"],
186
+ adapters: [],
187
+ },
188
+ runtime: {
189
+ framework: "react",
190
+ moduleFormat: "esm",
191
+ entry: "index.js",
192
+ css: [],
193
+ assetsBaseUrl: "/preview/",
194
+ },
195
+ capabilities: {
196
+ supportsThemes: true,
197
+ supportsViewports: true,
198
+ supportsActions: false,
199
+ supportsSlots: false,
200
+ supportsA11yScan: false,
201
+ supportsPlayFunctions: false,
202
+ supportsSceneNodes: false,
203
+ },
204
+ components: {
205
+ Button: {
206
+ componentKey: "Button",
207
+ publicRef: "Button",
208
+ displayName: "Button",
209
+ exportName: "Button",
210
+ importPath: "./button",
211
+ tier: "primitive",
212
+ previewability: "ready",
213
+ definitionSource: "manual",
214
+ diagnostics: [],
215
+ props: {},
216
+ variants: [
217
+ {
218
+ id: "primary",
219
+ name: "Primary",
220
+ source: "manual",
221
+ props: {},
222
+ },
223
+ ],
224
+ },
225
+ },
226
+ diagnostics: [],
227
+ };
228
+ }
@@ -0,0 +1,181 @@
1
+ import type {
2
+ PreviewArtifactManifest,
3
+ PreviewDesignComparisonReference,
4
+ PreviewDesignReference,
5
+ } from "./types";
6
+ import {
7
+ buildPreviewDesignComparisonEvidence,
8
+ buildPreviewDesignReferenceFromReport,
9
+ type PreviewComparisonReferenceScope,
10
+ type PreviewComparisonReport,
11
+ } from "./comparison";
12
+
13
+ export interface PreviewComparisonArtifactAssetPaths {
14
+ rendered?: string;
15
+ figma?: string;
16
+ diff?: string;
17
+ }
18
+
19
+ export interface PreviewComparisonArtifactTarget {
20
+ componentKey: string;
21
+ variantId?: string;
22
+ designReferenceScope: PreviewComparisonReferenceScope;
23
+ }
24
+
25
+ export interface BuildPreviewComparisonArtifactPayloadInput {
26
+ report?: PreviewComparisonReport;
27
+ designReference?: PreviewDesignReference;
28
+ comparisonEvidence?: PreviewDesignComparisonReference;
29
+ assets?: PreviewComparisonArtifactAssetPaths;
30
+ }
31
+
32
+ export interface PreviewComparisonArtifactPayload {
33
+ target: PreviewComparisonArtifactTarget;
34
+ report: PreviewComparisonReport;
35
+ designReference: PreviewDesignReference;
36
+ comparisonEvidence?: PreviewDesignComparisonReference;
37
+ }
38
+
39
+ export type ApplyPreviewComparisonArtifactStatus =
40
+ | "applied"
41
+ | "component-not-found"
42
+ | "variant-not-found"
43
+ | "missing-comparison-evidence";
44
+
45
+ export interface ApplyPreviewComparisonArtifactPayloadResult {
46
+ status: ApplyPreviewComparisonArtifactStatus;
47
+ applied: boolean;
48
+ manifest: PreviewArtifactManifest;
49
+ message: string;
50
+ }
51
+
52
+ export function buildPreviewComparisonArtifactPayload(
53
+ input: BuildPreviewComparisonArtifactPayloadInput
54
+ ): PreviewComparisonArtifactPayload | null {
55
+ if (!input.report) return null;
56
+
57
+ const report = input.report;
58
+ const designReference: PreviewDesignReference = {
59
+ ...(input.designReference ?? buildPreviewDesignReferenceFromReport(report)),
60
+ };
61
+ const reportEvidence = buildPreviewDesignComparisonEvidence(report);
62
+ const comparisonEvidence: PreviewDesignComparisonReference | undefined = report.context?.stale
63
+ ? undefined
64
+ : input.comparisonEvidence
65
+ ? { ...input.comparisonEvidence }
66
+ : designReference.comparison
67
+ ? { ...designReference.comparison }
68
+ : reportEvidence
69
+ ? { ...reportEvidence }
70
+ : undefined;
71
+
72
+ if (report.context?.stale) {
73
+ delete designReference.comparison;
74
+ }
75
+ if (input.assets?.figma) {
76
+ designReference.imageUrl = input.assets.figma;
77
+ }
78
+ if (comparisonEvidence && input.assets?.rendered) {
79
+ comparisonEvidence.previewImageUrl = input.assets.rendered;
80
+ }
81
+ if (comparisonEvidence && input.assets?.diff) {
82
+ comparisonEvidence.diffImageUrl = input.assets.diff;
83
+ }
84
+ if (comparisonEvidence) {
85
+ designReference.comparison = comparisonEvidence;
86
+ }
87
+
88
+ return {
89
+ target: buildComparisonArtifactTarget(report),
90
+ report,
91
+ designReference,
92
+ ...(comparisonEvidence && { comparisonEvidence }),
93
+ };
94
+ }
95
+
96
+ export function applyPreviewComparisonArtifactPayload(
97
+ manifest: PreviewArtifactManifest,
98
+ payload: PreviewComparisonArtifactPayload
99
+ ): ApplyPreviewComparisonArtifactPayloadResult {
100
+ if (!payload.comparisonEvidence) {
101
+ return {
102
+ status: "missing-comparison-evidence",
103
+ applied: false,
104
+ manifest,
105
+ message: "Comparison payload has no reusable evidence to apply.",
106
+ };
107
+ }
108
+
109
+ const component = manifest.components[payload.target.componentKey];
110
+ if (!component) {
111
+ return {
112
+ status: "component-not-found",
113
+ applied: false,
114
+ manifest,
115
+ message: `Component ${payload.target.componentKey} was not found in the manifest.`,
116
+ };
117
+ }
118
+
119
+ if (payload.target.designReferenceScope === "variant") {
120
+ const variantIndex = component.variants.findIndex(
121
+ (variant) => variant.id === payload.target.variantId
122
+ );
123
+ if (variantIndex < 0) {
124
+ return {
125
+ status: "variant-not-found",
126
+ applied: false,
127
+ manifest,
128
+ message: `Variant ${payload.target.variantId ?? "(missing)"} was not found on component ${
129
+ payload.target.componentKey
130
+ }.`,
131
+ };
132
+ }
133
+
134
+ const variants = component.variants.map((variant, index) =>
135
+ index === variantIndex ? { ...variant, design: payload.designReference } : variant
136
+ );
137
+
138
+ return {
139
+ status: "applied",
140
+ applied: true,
141
+ manifest: {
142
+ ...manifest,
143
+ components: {
144
+ ...manifest.components,
145
+ [payload.target.componentKey]: {
146
+ ...component,
147
+ variants,
148
+ },
149
+ },
150
+ },
151
+ message: `Applied comparison evidence to ${payload.target.componentKey} variant ${payload.target.variantId}.`,
152
+ };
153
+ }
154
+
155
+ return {
156
+ status: "applied",
157
+ applied: true,
158
+ manifest: {
159
+ ...manifest,
160
+ components: {
161
+ ...manifest.components,
162
+ [payload.target.componentKey]: {
163
+ ...component,
164
+ design: payload.designReference,
165
+ },
166
+ },
167
+ },
168
+ message: `Applied comparison evidence to ${payload.target.componentKey}.`,
169
+ };
170
+ }
171
+
172
+ function buildComparisonArtifactTarget(
173
+ report: PreviewComparisonReport
174
+ ): PreviewComparisonArtifactTarget {
175
+ return {
176
+ componentKey: report.preview.componentKey,
177
+ ...(report.preview.variantId && { variantId: report.preview.variantId }),
178
+ designReferenceScope:
179
+ report.context?.scope ?? (report.preview.variantId ? "variant" : "component"),
180
+ };
181
+ }