@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,438 @@
1
+ /**
2
+ * Context generation for AI agents.
3
+ *
4
+ * Generates AI-ready context documents from compiled fragment data.
5
+ */
6
+
7
+ import type { CompiledFragment, CompiledBlock, PropDefinition } from '../compiled-types/index.js';
8
+
9
+ /**
10
+ * Placeholder patterns to filter out from usage text.
11
+ */
12
+ export const PLACEHOLDER_PATTERNS = [
13
+ /^\w+ component is needed$/i,
14
+ /^Alternative component is more appropriate$/i,
15
+ /^Use \w+ when you need/i,
16
+ ];
17
+
18
+ /**
19
+ * Filter out placeholder text from usage arrays
20
+ */
21
+ export function filterPlaceholders(items: string[] | undefined): string[] {
22
+ if (!items) return [];
23
+ return items.filter(item =>
24
+ !PLACEHOLDER_PATTERNS.some(pattern => pattern.test(item.trim()))
25
+ );
26
+ }
27
+
28
+ /**
29
+ * Options for context generation
30
+ */
31
+ export interface ContextOptions {
32
+ format?: "markdown" | "json";
33
+ include?: {
34
+ props?: boolean;
35
+ variants?: boolean;
36
+ usage?: boolean;
37
+ relations?: boolean;
38
+ code?: boolean;
39
+ };
40
+ compact?: boolean;
41
+ }
42
+
43
+ /**
44
+ * Result of context generation
45
+ */
46
+ export interface ContextResult {
47
+ content: string;
48
+ tokenEstimate: number;
49
+ }
50
+
51
+ /**
52
+ * Generate AI-ready context from compiled fragments and optional blocks
53
+ */
54
+ export function generateContext(
55
+ fragments: CompiledFragment[],
56
+ options: ContextOptions = {},
57
+ blocks?: CompiledBlock[]
58
+ ): ContextResult {
59
+ const format = options.format ?? "markdown";
60
+ const compact = options.compact ?? false;
61
+
62
+ const include = {
63
+ props: options.include?.props ?? true,
64
+ variants: options.include?.variants ?? true,
65
+ usage: options.include?.usage ?? true,
66
+ relations: options.include?.relations ?? false,
67
+ code: options.include?.code ?? false,
68
+ };
69
+
70
+ const sorted = [...fragments].sort((a, b) => {
71
+ const catCompare = a.meta.category.localeCompare(b.meta.category);
72
+ if (catCompare !== 0) return catCompare;
73
+ return a.meta.name.localeCompare(b.meta.name);
74
+ });
75
+
76
+ if (format === "json") {
77
+ return generateJsonContext(sorted, include, compact, blocks);
78
+ }
79
+
80
+ return generateMarkdownContext(sorted, include, compact, blocks);
81
+ }
82
+
83
+ function generateMarkdownContext(
84
+ fragments: CompiledFragment[],
85
+ include: Required<NonNullable<ContextOptions["include"]>>,
86
+ compact: boolean,
87
+ blocks?: CompiledBlock[]
88
+ ): ContextResult {
89
+ const lines: string[] = [];
90
+
91
+ lines.push("# Design System Reference");
92
+ lines.push("");
93
+
94
+ lines.push("## Quick Reference");
95
+ lines.push("");
96
+ lines.push("| Component | Category | Use For |");
97
+ lines.push("|-----------|----------|---------|");
98
+
99
+ for (const fragment of fragments) {
100
+ const filteredWhen = filterPlaceholders(fragment.usage.when);
101
+ const useFor = filteredWhen.slice(0, 2).join(", ") || fragment.meta.description;
102
+ lines.push(`| ${fragment.meta.name} | ${fragment.meta.category} | ${truncate(useFor, 50)} |`);
103
+ }
104
+
105
+ lines.push("");
106
+
107
+ if (compact) {
108
+ const content = lines.join("\n");
109
+ return { content, tokenEstimate: estimateTokens(content) };
110
+ }
111
+
112
+ lines.push("## Components");
113
+ lines.push("");
114
+
115
+ for (const fragment of fragments) {
116
+ lines.push(`### ${fragment.meta.name}`);
117
+ lines.push("");
118
+
119
+ const statusParts = [`**Category:** ${fragment.meta.category}`];
120
+ if (fragment.meta.status) {
121
+ statusParts.push(`**Status:** ${fragment.meta.status}`);
122
+ }
123
+ lines.push(statusParts.join(" | "));
124
+ lines.push("");
125
+
126
+ if (fragment.meta.description) {
127
+ lines.push(fragment.meta.description);
128
+ lines.push("");
129
+ }
130
+
131
+ const whenFiltered = filterPlaceholders(fragment.usage.when);
132
+ const whenNotFiltered = filterPlaceholders(fragment.usage.whenNot);
133
+
134
+ if (include.usage && (whenFiltered.length > 0 || whenNotFiltered.length > 0)) {
135
+ if (whenFiltered.length > 0) {
136
+ lines.push("**When to use:**");
137
+ for (const when of whenFiltered) {
138
+ lines.push(`- ${when}`);
139
+ }
140
+ lines.push("");
141
+ }
142
+
143
+ if (whenNotFiltered.length > 0) {
144
+ lines.push("**When NOT to use:**");
145
+ for (const whenNot of whenNotFiltered) {
146
+ lines.push(`- ${whenNot}`);
147
+ }
148
+ lines.push("");
149
+ }
150
+ }
151
+
152
+ // Composition data (from ai metadata)
153
+ if (fragment.ai?.compositionPattern) {
154
+ const ai = fragment.ai;
155
+ const parts: string[] = [`**Composition:** ${ai.compositionPattern}`];
156
+ if (ai.subComponents && ai.subComponents.length > 0) {
157
+ parts.push(`Sub-components: ${ai.subComponents.map(s => `${fragment.meta.name}.${s}`).join(', ')}`);
158
+ }
159
+ if (ai.requiredChildren && ai.requiredChildren.length > 0) {
160
+ parts.push(`Required: ${ai.requiredChildren.map(c => `${fragment.meta.name}.${c}`).join(', ')}`);
161
+ }
162
+ lines.push(parts.join(' | '));
163
+ lines.push("");
164
+ if (ai.commonPatterns && ai.commonPatterns.length > 0) {
165
+ lines.push("**Patterns:**");
166
+ for (const pattern of ai.commonPatterns) {
167
+ lines.push(`- \`${pattern}\``);
168
+ }
169
+ lines.push("");
170
+ }
171
+ }
172
+
173
+ // Contract data
174
+ if (fragment.contract) {
175
+ const contract = fragment.contract;
176
+ if (contract.propsSummary && contract.propsSummary.length > 0) {
177
+ lines.push(`**Props:** ${contract.propsSummary.join(', ')}`);
178
+ lines.push("");
179
+ } else if (include.props && Object.keys(fragment.props).length > 0) {
180
+ lines.push("**Props:**");
181
+ for (const [name, prop] of Object.entries(fragment.props)) {
182
+ lines.push(`- \`${name}\`: ${formatPropType(prop)}${prop.required ? " (required)" : ""}`);
183
+ }
184
+ lines.push("");
185
+ }
186
+ if (contract.compoundChildren && Object.keys(contract.compoundChildren).length > 0) {
187
+ lines.push("**Sub-components:**");
188
+ for (const [childName, childMeta] of Object.entries(contract.compoundChildren)) {
189
+ const parts: string[] = [`\`${fragment.meta.name}.${childName}\``];
190
+ if (childMeta.required) parts.push("(required)");
191
+ if (childMeta.description) parts.push(`— ${childMeta.description}`);
192
+ lines.push(`- ${parts.join(' ')}`);
193
+ }
194
+ lines.push("");
195
+ }
196
+ if (contract.canonicalUsage && contract.canonicalUsage.length > 0) {
197
+ lines.push("**Usage examples:**");
198
+ for (const usage of contract.canonicalUsage) {
199
+ lines.push("```tsx");
200
+ lines.push(usage);
201
+ lines.push("```");
202
+ }
203
+ lines.push("");
204
+ }
205
+ if (contract.a11yRules && contract.a11yRules.length > 0) {
206
+ lines.push(`**A11y:** ${contract.a11yRules.join(', ')}`);
207
+ lines.push("");
208
+ }
209
+ if (contract.bans && contract.bans.length > 0) {
210
+ lines.push("**Banned patterns:**");
211
+ for (const ban of contract.bans) {
212
+ lines.push(`- \`${ban.pattern}\`: ${ban.message}`);
213
+ }
214
+ lines.push("");
215
+ }
216
+ } else if (include.props && Object.keys(fragment.props).length > 0) {
217
+ lines.push("**Props:**");
218
+ for (const [name, prop] of Object.entries(fragment.props)) {
219
+ lines.push(`- \`${name}\`: ${formatPropType(prop)}${prop.required ? " (required)" : ""}`);
220
+ }
221
+ lines.push("");
222
+ }
223
+
224
+ if (include.variants && fragment.variants.length > 0) {
225
+ const variantNames = fragment.variants.map((v) => v.name).join(", ");
226
+ lines.push(`**Variants:** ${variantNames}`);
227
+ lines.push("");
228
+
229
+ if (include.code) {
230
+ for (const variant of fragment.variants) {
231
+ if (variant.code) {
232
+ lines.push(`*${variant.name}:*`);
233
+ lines.push("```tsx");
234
+ lines.push(variant.code);
235
+ lines.push("```");
236
+ lines.push("");
237
+ }
238
+ }
239
+ }
240
+ }
241
+
242
+ if (include.relations && fragment.relations && fragment.relations.length > 0) {
243
+ lines.push("**Related:**");
244
+ for (const relation of fragment.relations) {
245
+ lines.push(`- ${relation.component} (${relation.relationship}): ${relation.note}`);
246
+ }
247
+ lines.push("");
248
+ }
249
+
250
+ lines.push("---");
251
+ lines.push("");
252
+ }
253
+
254
+ if (blocks && blocks.length > 0) {
255
+ lines.push("## Blocks");
256
+ lines.push("");
257
+ lines.push("Composition patterns showing how components wire together.");
258
+ lines.push("");
259
+
260
+ for (const block of blocks) {
261
+ lines.push(`### ${block.name}`);
262
+ lines.push("");
263
+ lines.push(block.description);
264
+ lines.push("");
265
+ lines.push(`**Category:** ${block.category}`);
266
+ lines.push(`**Components:** ${block.components.join(", ")}`);
267
+ if (block.tags && block.tags.length > 0) {
268
+ lines.push(`**Tags:** ${block.tags.join(", ")}`);
269
+ }
270
+ lines.push("");
271
+ lines.push("```tsx");
272
+ lines.push(block.code);
273
+ lines.push("```");
274
+ lines.push("");
275
+ lines.push("---");
276
+ lines.push("");
277
+ }
278
+ }
279
+
280
+ const content = lines.join("\n");
281
+ return { content, tokenEstimate: estimateTokens(content) };
282
+ }
283
+
284
+ function generateJsonContext(
285
+ fragments: CompiledFragment[],
286
+ include: Required<NonNullable<ContextOptions["include"]>>,
287
+ compact: boolean,
288
+ blocks?: CompiledBlock[]
289
+ ): ContextResult {
290
+ const categories = [...new Set(fragments.map((s) => s.meta.category))].sort();
291
+
292
+ interface JsonComponent {
293
+ category: string;
294
+ description: string;
295
+ status?: string;
296
+ whenToUse?: string[];
297
+ whenNotToUse?: string[];
298
+ composition?: {
299
+ pattern: string;
300
+ subComponents?: string[];
301
+ requiredChildren?: string[];
302
+ commonPatterns?: string[];
303
+ };
304
+ propsSummary?: string[];
305
+ compoundChildren?: Record<string, { required?: boolean; accepts?: string[]; description?: string }>;
306
+ canonicalUsage?: string[];
307
+ props?: Record<string, { type: string; required?: boolean; default?: unknown; description: string }>;
308
+ variants?: string[];
309
+ relations?: Array<{ component: string; relationship: string; note: string }>;
310
+ a11yRules?: string[];
311
+ bans?: Array<{ pattern: string; message: string }>;
312
+ }
313
+
314
+ const components: Record<string, JsonComponent> = {};
315
+
316
+ for (const fragment of fragments) {
317
+ const component: JsonComponent = {
318
+ category: fragment.meta.category,
319
+ description: fragment.meta.description,
320
+ };
321
+
322
+ if (fragment.meta.status) {
323
+ component.status = fragment.meta.status;
324
+ }
325
+
326
+ if (!compact) {
327
+ if (include.usage) {
328
+ const whenFiltered = filterPlaceholders(fragment.usage.when);
329
+ const whenNotFiltered = filterPlaceholders(fragment.usage.whenNot);
330
+ if (whenFiltered.length > 0) component.whenToUse = whenFiltered;
331
+ if (whenNotFiltered.length > 0) component.whenNotToUse = whenNotFiltered;
332
+ }
333
+
334
+ // Composition data
335
+ if (fragment.ai?.compositionPattern) {
336
+ const ai = fragment.ai;
337
+ const comp: NonNullable<JsonComponent['composition']> = { pattern: ai.compositionPattern! };
338
+ if (ai.subComponents && ai.subComponents.length > 0) {
339
+ comp.subComponents = ai.subComponents;
340
+ }
341
+ if (ai.requiredChildren && ai.requiredChildren.length > 0) {
342
+ comp.requiredChildren = ai.requiredChildren;
343
+ }
344
+ if (ai.commonPatterns && ai.commonPatterns.length > 0) {
345
+ comp.commonPatterns = ai.commonPatterns;
346
+ }
347
+ component.composition = comp;
348
+ }
349
+
350
+ // Contract data
351
+ if (fragment.contract?.propsSummary && fragment.contract.propsSummary.length > 0) {
352
+ component.propsSummary = fragment.contract.propsSummary;
353
+ }
354
+ if (fragment.contract?.compoundChildren && Object.keys(fragment.contract.compoundChildren).length > 0) {
355
+ component.compoundChildren = fragment.contract.compoundChildren;
356
+ }
357
+ if (fragment.contract?.canonicalUsage && fragment.contract.canonicalUsage.length > 0) {
358
+ component.canonicalUsage = fragment.contract.canonicalUsage;
359
+ }
360
+ if (fragment.contract?.a11yRules && fragment.contract.a11yRules.length > 0) {
361
+ component.a11yRules = fragment.contract.a11yRules;
362
+ }
363
+ if (fragment.contract?.bans && fragment.contract.bans.length > 0) {
364
+ component.bans = fragment.contract.bans;
365
+ }
366
+
367
+ if (include.props && Object.keys(fragment.props).length > 0) {
368
+ component.props = {};
369
+ for (const [name, prop] of Object.entries(fragment.props)) {
370
+ component.props[name] = {
371
+ type: formatPropType(prop),
372
+ description: prop.description,
373
+ };
374
+ if (prop.required) component.props[name].required = true;
375
+ if (prop.default !== undefined) component.props[name].default = prop.default;
376
+ }
377
+ }
378
+
379
+ if (include.variants && fragment.variants.length > 0) {
380
+ component.variants = fragment.variants.map((v) => v.name);
381
+ }
382
+
383
+ if (include.relations && fragment.relations && fragment.relations.length > 0) {
384
+ component.relations = fragment.relations.map((r) => ({
385
+ component: r.component,
386
+ relationship: r.relationship,
387
+ note: r.note,
388
+ }));
389
+ }
390
+ }
391
+
392
+ components[fragment.meta.name] = component;
393
+ }
394
+
395
+ const blocksMap = blocks && blocks.length > 0
396
+ ? Object.fromEntries(blocks.map(b => [b.name, {
397
+ description: b.description,
398
+ category: b.category,
399
+ components: b.components,
400
+ code: b.code,
401
+ tags: b.tags,
402
+ }]))
403
+ : undefined;
404
+
405
+ const output = {
406
+ version: "1.0",
407
+ generatedAt: new Date().toISOString(),
408
+ summary: {
409
+ totalComponents: fragments.length,
410
+ categories,
411
+ ...(blocksMap && { totalBlocks: blocks!.length }),
412
+ },
413
+ components,
414
+ ...(blocksMap && { blocks: blocksMap }),
415
+ };
416
+
417
+ const content = JSON.stringify(output, null, 2);
418
+ return { content, tokenEstimate: estimateTokens(content) };
419
+ }
420
+
421
+ function formatPropType(prop: PropDefinition): string {
422
+ if (prop.type === "enum" && prop.values) {
423
+ return prop.values.map((v) => `"${v}"`).join(" | ");
424
+ }
425
+ if (prop.default !== undefined) {
426
+ return `${prop.type} (default: ${JSON.stringify(prop.default)})`;
427
+ }
428
+ return prop.type;
429
+ }
430
+
431
+ function truncate(str: string, maxLength: number): string {
432
+ if (str.length <= maxLength) return str;
433
+ return str.slice(0, maxLength - 3) + "...";
434
+ }
435
+
436
+ function estimateTokens(text: string): number {
437
+ return Math.ceil(text.length / 4);
438
+ }
@@ -0,0 +1,186 @@
1
+ import { describe, expect, it } from "vitest";
2
+
3
+ import type { GovernanceConfig } from "./governance.js";
4
+ import {
5
+ evaluateGovernanceIntegrity,
6
+ hasEffectiveComponentVocabulary,
7
+ isEffectiveCanonicalSource,
8
+ type GovernanceIntegrityInput,
9
+ } from "./governance-integrity.js";
10
+ import { customerDefaultRuleStates } from "./rules/presets.js";
11
+
12
+ function evaluate(
13
+ overrides: Partial<GovernanceIntegrityInput>
14
+ ): ReturnType<typeof evaluateGovernanceIntegrity> {
15
+ return evaluateGovernanceIntegrity({
16
+ policy: undefined,
17
+ policySource: "none",
18
+ declared: false,
19
+ ...overrides,
20
+ });
21
+ }
22
+
23
+ describe("isEffectiveCanonicalSource", () => {
24
+ it("treats a directory source with no include as a wildcard (effective)", () => {
25
+ expect(isEffectiveCanonicalSource({ kind: "directory", path: "ui" })).toBe(true);
26
+ });
27
+
28
+ it("treats an npm/registry source with no include as NOT effective", () => {
29
+ expect(isEffectiveCanonicalSource({ kind: "npm", specifier: "@acme/ui" })).toBe(false);
30
+ });
31
+
32
+ it("treats any source with a non-empty include as effective", () => {
33
+ expect(
34
+ isEffectiveCanonicalSource({ kind: "npm", specifier: "@acme/ui", include: ["Button"] })
35
+ ).toBe(true);
36
+ });
37
+
38
+ it("treats a source with an empty include as NOT effective", () => {
39
+ expect(isEffectiveCanonicalSource({ kind: "directory", path: "ui", include: [] })).toBe(false);
40
+ });
41
+ });
42
+
43
+ describe("hasEffectiveComponentVocabulary", () => {
44
+ it("is false for undefined options", () => {
45
+ expect(hasEffectiveComponentVocabulary(undefined)).toBe(false);
46
+ });
47
+
48
+ it("is true when canonicalMappings is non-empty", () => {
49
+ expect(hasEffectiveComponentVocabulary({ canonicalMappings: [{ name: "Button" }] })).toBe(true);
50
+ });
51
+
52
+ it("is true when an effective canonical source is present", () => {
53
+ expect(
54
+ hasEffectiveComponentVocabulary({
55
+ canonicalSources: [{ kind: "directory", path: "ui" }],
56
+ })
57
+ ).toBe(true);
58
+ });
59
+
60
+ it("is false for a package source with no include", () => {
61
+ expect(
62
+ hasEffectiveComponentVocabulary({
63
+ canonicalSources: [{ kind: "npm", specifier: "@acme/ui" }],
64
+ })
65
+ ).toBe(false);
66
+ });
67
+ });
68
+
69
+ describe("evaluateGovernanceIntegrity", () => {
70
+ it("1. no policy at all → inert, not fatal, not blocking-capable", () => {
71
+ const verdict = evaluate({ policy: undefined, policySource: "none", declared: false });
72
+ expect(verdict.status).toBe("inert");
73
+ expect(verdict.fatalForCi).toBe(false);
74
+ expect(verdict.blockingCapable).toBe(false);
75
+ expect(verdict.armed).toEqual([]);
76
+ });
77
+
78
+ it("2. customer-default shape (prefer-library enabled, NO vocabulary) → inert, NOT healthy", () => {
79
+ const policy: GovernanceConfig = { rules: customerDefaultRuleStates() };
80
+ const verdict = evaluate({ policy, policySource: "preset", declared: true });
81
+ const components = verdict.families.find((f) => f.id === "components");
82
+ expect(components?.armed).toBe(false);
83
+ expect(components?.reason).toContain("no effective canonical source");
84
+ expect(verdict.status).toBe("inert");
85
+ // declared + inert → CI-fatal
86
+ expect(verdict.fatalForCi).toBe(true);
87
+ });
88
+
89
+ it("3. directory canonical source (no include) → components armed, healthy, warn is not blocking", () => {
90
+ const policy: GovernanceConfig = {
91
+ canonicalSources: [{ kind: "directory", path: "ui" }],
92
+ };
93
+ const verdict = evaluate({ policy, policySource: "config", declared: true });
94
+ expect(verdict.families.find((f) => f.id === "components")?.armed).toBe(true);
95
+ expect(verdict.status).toBe("healthy");
96
+ expect(verdict.blockingCapable).toBe(false);
97
+ expect(verdict.armed).toContain("components");
98
+ });
99
+
100
+ it("4a. directory source + prefer-library severity error → blocking-capable", () => {
101
+ const policy: GovernanceConfig = {
102
+ rules: { "components/prefer-library": { enabled: true, severity: "error" } },
103
+ canonicalSources: [{ kind: "directory", path: "ui" }],
104
+ };
105
+ const verdict = evaluate({ policy, policySource: "config", declared: true });
106
+ expect(verdict.status).toBe("healthy");
107
+ expect(verdict.blockingCapable).toBe(true);
108
+ expect(verdict.families.find((f) => f.id === "blocking-deny")?.rules).toContain(
109
+ "components/prefer-library"
110
+ );
111
+ });
112
+
113
+ it("4b. directory source + ci.failOnWarnings → blocking-capable even at warn", () => {
114
+ const policy: GovernanceConfig = {
115
+ canonicalSources: [{ kind: "directory", path: "ui" }],
116
+ ci: { failOnWarnings: true },
117
+ };
118
+ const verdict = evaluate({ policy, policySource: "config", declared: true });
119
+ expect(verdict.status).toBe("healthy");
120
+ expect(verdict.blockingCapable).toBe(true);
121
+ });
122
+
123
+ it("5. npm source with NO include → components NOT armed → inert (blocker-#2 lock)", () => {
124
+ const policy: GovernanceConfig = {
125
+ canonicalSources: [{ kind: "npm", specifier: "@acme/ui" }],
126
+ };
127
+ const verdict = evaluate({ policy, policySource: "config", declared: true });
128
+ expect(verdict.families.find((f) => f.id === "components")?.armed).toBe(false);
129
+ expect(verdict.status).toBe("inert");
130
+ expect(verdict.blockingCapable).toBe(false);
131
+ });
132
+
133
+ it("6. npm source WITH include → components armed", () => {
134
+ const policy: GovernanceConfig = {
135
+ canonicalSources: [{ kind: "npm", specifier: "@acme/ui", include: ["Button"] }],
136
+ };
137
+ const verdict = evaluate({ policy, policySource: "config", declared: true });
138
+ expect(verdict.families.find((f) => f.id === "components")?.armed).toBe(true);
139
+ expect(verdict.status).toBe("healthy");
140
+ });
141
+
142
+ it("7. css-vars active + token vocabulary → tokens armed → healthy", () => {
143
+ const policy: GovernanceConfig = {
144
+ styles: [{ kind: "style.cssVars.mustBeDefined", severity: "warn" }],
145
+ };
146
+ const verdict = evaluate({
147
+ policy,
148
+ policySource: "config",
149
+ declared: true,
150
+ cssVarsActive: true,
151
+ tokenVocabularySize: 5,
152
+ });
153
+ expect(verdict.families.find((f) => f.id === "tokens")?.armed).toBe(true);
154
+ expect(verdict.status).toBe("healthy");
155
+ expect(verdict.armed).toContain("tokens");
156
+ });
157
+
158
+ it("7b. css-vars active but empty token vocabulary → tokens NOT armed", () => {
159
+ const policy: GovernanceConfig = {
160
+ styles: [{ kind: "style.cssVars.mustBeDefined", severity: "warn" }],
161
+ };
162
+ const verdict = evaluate({
163
+ policy,
164
+ policySource: "config",
165
+ declared: true,
166
+ cssVarsActive: true,
167
+ tokenVocabularySize: 0,
168
+ });
169
+ const tokens = verdict.families.find((f) => f.id === "tokens");
170
+ expect(tokens?.armed).toBe(false);
171
+ expect(tokens?.reason).toBe("no token vocabulary");
172
+ });
173
+
174
+ it("8. hygiene-only policy (raw-style rule enabled, no contract) → degraded", () => {
175
+ const policy: GovernanceConfig = {
176
+ rules: { "styles/no-raw-color": { enabled: true, severity: "warn" } },
177
+ };
178
+ const verdict = evaluate({ policy, policySource: "config", declared: true });
179
+ const hygiene = verdict.families.find((f) => f.id === "hygiene");
180
+ expect(hygiene?.armed).toBe(true);
181
+ expect(hygiene?.rules).toContain("styles/no-raw-color");
182
+ expect(verdict.status).toBe("degraded");
183
+ // degraded is enforceable, so not CI-fatal
184
+ expect(verdict.fatalForCi).toBe(false);
185
+ });
186
+ });