@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,282 @@
1
+ /**
2
+ * Governance integrity — a pure verdict over a FULLY-resolved governance policy
3
+ * that answers "is this governance actually armed, or merely declared?".
4
+ *
5
+ * The load-bearing distinction is **enabled ≠ armed**. A rule can be `enabled`
6
+ * in the policy yet inert because it has no vocabulary to measure against:
7
+ * `components/prefer-library` with no effective canonical source enforces
8
+ * nothing; `tokens/css-vars-must-be-defined` with an empty token vocabulary
9
+ * enforces nothing. This module derives, per rule *family*, whether the family
10
+ * is armed (has both an enabled rule AND the vocabulary/signal it needs), then
11
+ * rolls those up into a single status.
12
+ *
13
+ * Browser-safe and filesystem-free: it never reads the repo. All file-derived
14
+ * signals (token vocabulary size, whether the css-vars rule is active) are
15
+ * supplied by the caller, which is also responsible for fully resolving the
16
+ * policy (merging presets / localCanonical / designSystem / enrichment) BEFORE
17
+ * calling in.
18
+ */
19
+
20
+ import type { CanonicalSource, GovernanceConfig, GovernanceSeverity } from "./governance.js";
21
+ import { compileGlobalGovernanceFacts } from "./facts/index.js";
22
+ import { FRAGMENTS_INTERNAL_RULE_IDS, RULE_TIER } from "./rules/tiers.js";
23
+ import { BLOCKING_RULE_ALLOWLIST } from "./rules/emit-gate.js";
24
+
25
+ export type GovernanceIntegrityStatus = "healthy" | "degraded" | "inert";
26
+
27
+ export type GovernanceIntegrityFamilyId =
28
+ "policy" | "components" | "tokens" | "hygiene" | "blocking-deny";
29
+
30
+ export interface GovernanceIntegrityFamily {
31
+ id: GovernanceIntegrityFamilyId;
32
+ armed: boolean;
33
+ rules: string[];
34
+ reason?: string;
35
+ remediation?: string;
36
+ }
37
+
38
+ export interface GovernanceIntegrityInput {
39
+ /**
40
+ * FULLY resolved policy — the caller merges presets / localCanonical /
41
+ * designSystem / enrichment first. `undefined` means no policy at all.
42
+ */
43
+ policy: GovernanceConfig | undefined;
44
+ policySource: "config" | "preset" | "fallback" | "none";
45
+ /** Any governance intent signal present (caller computes). */
46
+ declared: boolean;
47
+ /** Size of the contract token vocabulary; core cannot read files. */
48
+ tokenVocabularySize?: number;
49
+ /** Whether `tokens/css-vars-must-be-defined` is activated (caller supplies). */
50
+ cssVarsActive?: boolean;
51
+ mode?: "scan" | "ci" | "hook" | "doctor" | "setup";
52
+ }
53
+
54
+ export interface GovernanceIntegrityVerdict {
55
+ status: GovernanceIntegrityStatus;
56
+ declared: boolean;
57
+ fatalForCi: boolean;
58
+ blockingCapable: boolean;
59
+ families: GovernanceIntegrityFamily[];
60
+ armed: GovernanceIntegrityFamilyId[];
61
+ summary: string;
62
+ remediations: string[];
63
+ }
64
+
65
+ interface EffectiveRuleConfig {
66
+ enabled: boolean;
67
+ severity?: GovernanceSeverity;
68
+ options?: Record<string, unknown>;
69
+ }
70
+
71
+ /**
72
+ * Whether a canonical source contributes an effective component vocabulary.
73
+ *
74
+ * Mirrors `sourceIncludesComponent` in `./rules/components-prefer-library.ts`
75
+ * (a follow-up refactor will make that rule import this): a `directory` source
76
+ * with no `include` is a wildcard (armed); an explicit `include` must be
77
+ * non-empty; a package-like `npm`/`registry` source with no `include` is NOT an
78
+ * effective vocabulary, because it has no export inventory to enumerate.
79
+ */
80
+ export function isEffectiveCanonicalSource(source: CanonicalSource): boolean {
81
+ return source.include !== undefined ? source.include.length > 0 : source.kind === "directory";
82
+ }
83
+
84
+ /**
85
+ * Whether `components/prefer-library`'s effective options carry a real component
86
+ * vocabulary: at least one canonical mapping, or at least one effective
87
+ * canonical source. Accepts the raw fact `options` bag (values are `unknown`).
88
+ */
89
+ export function hasEffectiveComponentVocabulary(
90
+ options: Record<string, unknown> | undefined
91
+ ): boolean {
92
+ const mappings = options?.["canonicalMappings"];
93
+ if (Array.isArray(mappings) && mappings.length > 0) return true;
94
+ const sources = options?.["canonicalSources"];
95
+ return (
96
+ Array.isArray(sources) &&
97
+ sources.some((source) => isEffectiveCanonicalSource(source as CanonicalSource))
98
+ );
99
+ }
100
+
101
+ /**
102
+ * Compile the policy to effective per-rule configs, keyed by rule id. Reuses the
103
+ * fact compiler so the effective enabled/severity/options match exactly what the
104
+ * scan engine sees (including the canonicalSources → prefer-library injection).
105
+ */
106
+ function effectiveRuleConfigs(
107
+ policy: GovernanceConfig | undefined
108
+ ): Map<string, EffectiveRuleConfig> {
109
+ const configs = new Map<string, EffectiveRuleConfig>();
110
+ for (const fact of compileGlobalGovernanceFacts(policy)) {
111
+ if (fact.kind !== "governance_rule_config") continue;
112
+ configs.set(fact.ruleId, {
113
+ enabled: fact.enabled,
114
+ severity: fact.severity,
115
+ options: fact.options,
116
+ });
117
+ }
118
+ return configs;
119
+ }
120
+
121
+ /** Whether the policy declares the css-vars-must-be-defined style rule. */
122
+ function policyDeclaresCssVars(policy: GovernanceConfig | undefined): boolean {
123
+ return Boolean(policy?.styles?.some((style) => style.kind === "style.cssVars.mustBeDefined"));
124
+ }
125
+
126
+ function dedupe(values: string[]): string[] {
127
+ return [...new Set(values)];
128
+ }
129
+
130
+ function summarize(
131
+ status: GovernanceIntegrityStatus,
132
+ flags: { componentsArmed: boolean; tokensArmed: boolean; blockingArmed: boolean }
133
+ ): string {
134
+ if (status === "inert") {
135
+ return "Governance inert — no enforceable rule family is armed";
136
+ }
137
+ if (status === "degraded") {
138
+ return "Governance degraded — hygiene rules armed but no contract vocabulary is declared";
139
+ }
140
+ const contract: string[] = [];
141
+ if (flags.componentsArmed) contract.push("components");
142
+ if (flags.tokensArmed) contract.push("tokens");
143
+ const suffix = flags.blockingArmed ? ", blocking-capable" : "";
144
+ return `Governance healthy — contract vocabulary armed (${contract.join(" + ")})${suffix}`;
145
+ }
146
+
147
+ export function evaluateGovernanceIntegrity(
148
+ input: GovernanceIntegrityInput
149
+ ): GovernanceIntegrityVerdict {
150
+ const configs = effectiveRuleConfigs(input.policy);
151
+
152
+ // --- policy family --------------------------------------------------------
153
+ const policyArmed = input.policy !== undefined && input.policySource !== "none";
154
+ const policyFamily: GovernanceIntegrityFamily = {
155
+ id: "policy",
156
+ armed: policyArmed,
157
+ rules: [],
158
+ };
159
+ if (!policyArmed) {
160
+ policyFamily.reason = "no governance policy resolved";
161
+ }
162
+
163
+ // --- components family ----------------------------------------------------
164
+ const componentsConfig = configs.get("components/prefer-library");
165
+ const componentsArmed =
166
+ componentsConfig?.enabled === true && hasEffectiveComponentVocabulary(componentsConfig.options);
167
+ const componentsFamily: GovernanceIntegrityFamily = {
168
+ id: "components",
169
+ armed: componentsArmed,
170
+ rules: ["components/prefer-library"],
171
+ };
172
+ if (!componentsArmed) {
173
+ if (componentsConfig?.enabled === true) {
174
+ componentsFamily.reason =
175
+ "components/prefer-library enabled but no effective canonical source";
176
+ componentsFamily.remediation =
177
+ "add govern.canonicalSources (a directory source, or npm/registry with a non-empty include) or designSystem.path/packageName";
178
+ } else {
179
+ componentsFamily.reason = "components/prefer-library not enabled";
180
+ }
181
+ }
182
+
183
+ // --- tokens family --------------------------------------------------------
184
+ const cssVarsActive = input.cssVarsActive ?? policyDeclaresCssVars(input.policy);
185
+ const tokenVocabularySize = input.tokenVocabularySize ?? 0;
186
+ const tokensArmed = cssVarsActive === true && tokenVocabularySize > 0;
187
+ const tokensFamily: GovernanceIntegrityFamily = {
188
+ id: "tokens",
189
+ armed: tokensArmed,
190
+ rules: ["tokens/css-vars-must-be-defined"],
191
+ };
192
+ if (!tokensArmed) {
193
+ if (!cssVarsActive) {
194
+ tokensFamily.reason = "tokens/css-vars-must-be-defined not active";
195
+ tokensFamily.remediation =
196
+ "activate the css-vars rule (govern.styles: style.cssVars.mustBeDefined)";
197
+ } else {
198
+ tokensFamily.reason = "no token vocabulary";
199
+ tokensFamily.remediation =
200
+ "declare token source files so the contract token vocabulary is non-empty";
201
+ }
202
+ }
203
+
204
+ // --- hygiene family -------------------------------------------------------
205
+ const hygieneRules: string[] = [];
206
+ for (const [ruleId, config] of configs) {
207
+ if (config.enabled !== true) continue;
208
+ if (RULE_TIER[ruleId] !== "hygiene") continue;
209
+ if (FRAGMENTS_INTERNAL_RULE_IDS.has(ruleId)) continue;
210
+ hygieneRules.push(ruleId);
211
+ }
212
+ hygieneRules.sort();
213
+ const hygieneArmed = hygieneRules.length > 0;
214
+ const hygieneFamily: GovernanceIntegrityFamily = {
215
+ id: "hygiene",
216
+ armed: hygieneArmed,
217
+ rules: hygieneRules,
218
+ };
219
+ if (!hygieneArmed) {
220
+ hygieneFamily.reason = "no hygiene-tier rule enabled";
221
+ }
222
+
223
+ // --- blocking-deny family -------------------------------------------------
224
+ const failOnWarnings = input.policy?.ci?.failOnWarnings === true;
225
+ const blockingRules: string[] = [];
226
+ for (const ruleId of BLOCKING_RULE_ALLOWLIST) {
227
+ const config = configs.get(ruleId);
228
+ if (config?.enabled !== true) continue;
229
+ const familyArmed = ruleId === "components/prefer-library" ? componentsArmed : hygieneArmed;
230
+ if (!familyArmed) continue;
231
+ if (config.severity !== "error" && !failOnWarnings) continue;
232
+ blockingRules.push(ruleId);
233
+ }
234
+ blockingRules.sort();
235
+ const blockingArmed = blockingRules.length > 0;
236
+ const blockingFamily: GovernanceIntegrityFamily = {
237
+ id: "blocking-deny",
238
+ armed: blockingArmed,
239
+ rules: blockingRules,
240
+ };
241
+ if (!blockingArmed) {
242
+ blockingFamily.reason = "no blocking-capable rule (armed rules are warn-severity only)";
243
+ blockingFamily.remediation =
244
+ 'raise a blocking-eligible rule to severity "error", or set ci.failOnWarnings: true';
245
+ }
246
+
247
+ // --- roll-up --------------------------------------------------------------
248
+ const contractArmed = componentsArmed || tokensArmed;
249
+ const enforceableArmed = componentsArmed || tokensArmed || hygieneArmed;
250
+ const status: GovernanceIntegrityStatus = !enforceableArmed
251
+ ? "inert"
252
+ : contractArmed
253
+ ? "healthy"
254
+ : "degraded";
255
+ const fatalForCi = input.declared && status === "inert";
256
+
257
+ const families: GovernanceIntegrityFamily[] = [
258
+ policyFamily,
259
+ componentsFamily,
260
+ tokensFamily,
261
+ hygieneFamily,
262
+ blockingFamily,
263
+ ];
264
+ const armed = families.filter((family) => family.armed).map((family) => family.id);
265
+ const remediations = dedupe(
266
+ families
267
+ .map((family) => family.remediation)
268
+ .filter((remediation): remediation is string => remediation !== undefined)
269
+ );
270
+ const summary = summarize(status, { componentsArmed, tokensArmed, blockingArmed });
271
+
272
+ return {
273
+ status,
274
+ declared: input.declared,
275
+ fatalForCi,
276
+ blockingCapable: blockingArmed,
277
+ families,
278
+ armed,
279
+ summary,
280
+ remediations,
281
+ };
282
+ }
@@ -0,0 +1,300 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { compileFragment, defineConfig, defineFragment, g } from "./index.js";
3
+
4
+ type ButtonProps = {
5
+ variant?: "primary" | "secondary" | "ghost" | "link";
6
+ size?: "sm" | "md" | "lg";
7
+ disabled?: boolean;
8
+ };
9
+
10
+ function Button(_props: ButtonProps) {
11
+ return null;
12
+ }
13
+
14
+ describe("governance DSL", () => {
15
+ it("defineConfig accepts global governance records", () => {
16
+ const config = defineConfig({
17
+ include: ["src/**/*.fragment.ts"],
18
+ tokens: {
19
+ include: ["src/styles/tokens.scss"],
20
+ },
21
+ govern: {
22
+ presets: ["fragments/recommended"],
23
+ scales: {
24
+ space: g.scale.px([0, 4, 8, 12, 16]),
25
+ typeSize: g.scale.px([12, 14, 16]),
26
+ },
27
+ styles: [
28
+ g.styles.rawColors().forbid({
29
+ except: ["transparent", "currentColor"],
30
+ prefer: "token",
31
+ severity: "error",
32
+ }),
33
+ g.styles.rawSpacing().mustMatchScale("space", {
34
+ appliesTo: ["gap", "padding"],
35
+ severity: "error",
36
+ }),
37
+ g.styles.fontSize().mustMatchScale("typeSize", {
38
+ severity: "warn",
39
+ }),
40
+ g.styles.cssVars().mustBeDefined({
41
+ severity: "error",
42
+ }),
43
+ ],
44
+ jsx: [
45
+ g.jsx.unknownProps().forbid({ severity: "error" }),
46
+ g.jsx.inlineStyle().forbidRaw(["color", "backgroundColor"]),
47
+ ],
48
+ agent: {
49
+ repairOrder: ["apply-deterministic-fixes"],
50
+ },
51
+ },
52
+ });
53
+
54
+ expect(config.govern?.scales?.space).toEqual({
55
+ kind: "scale",
56
+ name: "space",
57
+ unit: "px",
58
+ values: [0, 4, 8, 12, 16],
59
+ });
60
+ expect(config.govern?.styles?.[0]).toMatchObject({
61
+ kind: "style.rawColors.forbid",
62
+ severity: "error",
63
+ });
64
+ });
65
+
66
+ it("throws when a governance severity is malformed", () => {
67
+ expect(() =>
68
+ defineConfig({
69
+ include: ["src/**/*.fragment.ts"],
70
+ govern: {
71
+ styles: [
72
+ g.styles.rawColors().forbid({
73
+ severity: "loud" as never,
74
+ }),
75
+ ],
76
+ },
77
+ })
78
+ ).toThrow("Invalid fragments config");
79
+ });
80
+
81
+ it("throws when scale values are not numbers", () => {
82
+ expect(() =>
83
+ defineConfig({
84
+ include: ["src/**/*.fragment.ts"],
85
+ govern: {
86
+ scales: {
87
+ space: g.scale.px([0, "4" as never]),
88
+ },
89
+ },
90
+ })
91
+ ).toThrow("Invalid fragments config");
92
+ });
93
+
94
+ it("accepts registry canonical sources for install receipt governance", () => {
95
+ const config = defineConfig({
96
+ include: ["src/**/*.fragment.ts"],
97
+ govern: {
98
+ canonicalSources: [
99
+ {
100
+ kind: "registry",
101
+ registryId: "fragments-ui",
102
+ registryHash: "a".repeat(64),
103
+ receiptPath: ".fragments/registry-lock.json",
104
+ installPath: "src/fragments/ui",
105
+ importPath: "@/fragments/ui",
106
+ severity: "error",
107
+ },
108
+ ],
109
+ },
110
+ });
111
+
112
+ expect(config.govern?.canonicalSources?.[0]).toMatchObject({
113
+ kind: "registry",
114
+ registryId: "fragments-ui",
115
+ installPath: "src/fragments/ui",
116
+ });
117
+ });
118
+
119
+ it("accepts canonical source include lists", () => {
120
+ const config = defineConfig({
121
+ include: ["src/**/*.fragment.ts"],
122
+ govern: {
123
+ canonicalSources: [
124
+ {
125
+ kind: "npm",
126
+ specifier: "@acme/ui",
127
+ implementationPath: "src/ui",
128
+ include: ["Button", "Tabs"],
129
+ },
130
+ ],
131
+ },
132
+ });
133
+
134
+ expect(config.govern?.canonicalSources?.[0]).toMatchObject({
135
+ kind: "npm",
136
+ specifier: "@acme/ui",
137
+ implementationPath: "src/ui",
138
+ include: ["Button", "Tabs"],
139
+ });
140
+ });
141
+
142
+ it("rejects unsafe registry canonical source paths", () => {
143
+ expect(() =>
144
+ defineConfig({
145
+ include: ["src/**/*.fragment.ts"],
146
+ govern: {
147
+ canonicalSources: [
148
+ {
149
+ kind: "registry",
150
+ registryId: "fragments-ui",
151
+ installPath: "../ui",
152
+ },
153
+ ],
154
+ },
155
+ })
156
+ ).toThrow("Invalid fragments config");
157
+ });
158
+
159
+ it("accepts component-specific governance overrides in config", () => {
160
+ const config = defineConfig({
161
+ include: ["src/**/*.fragment.ts"],
162
+ govern: {
163
+ rules: {
164
+ "components/usage": { enabled: true, severity: "warn" },
165
+ },
166
+ overrides: [
167
+ {
168
+ match: {
169
+ path: "src/components/ui/button.tsx",
170
+ export: "Button",
171
+ },
172
+ rules: [
173
+ g.component.prop("variant").forbid("link", {
174
+ because: "Use Link for navigation.",
175
+ severity: "error",
176
+ }),
177
+ g.component.accessibility().requireName({
178
+ because: "Buttons must announce their action.",
179
+ }),
180
+ ],
181
+ },
182
+ ],
183
+ },
184
+ });
185
+
186
+ expect(config.govern?.overrides?.[0]).toMatchObject({
187
+ match: {
188
+ path: "src/components/ui/button.tsx",
189
+ export: "Button",
190
+ },
191
+ rules: [
192
+ {
193
+ kind: "prop.value.forbid",
194
+ prop: "variant",
195
+ value: "link",
196
+ severity: "error",
197
+ },
198
+ {
199
+ kind: "a11y.requireName",
200
+ severity: "error",
201
+ },
202
+ ],
203
+ });
204
+ });
205
+
206
+ it("normalizes component governance records from defineFragment", () => {
207
+ const fragment = defineFragment({
208
+ component: Button,
209
+ meta: {
210
+ name: "Button",
211
+ description: "Interactive element for user-triggered actions.",
212
+ category: "forms",
213
+ },
214
+ guidance: {
215
+ when: ["Submitting forms"],
216
+ whenNot: ["Simple navigation without side effects"],
217
+ },
218
+ govern: (govern) => [
219
+ govern.capability("dom.button"),
220
+ govern.prop("variant").avoid("link", {
221
+ because: "Use Link for ordinary navigation.",
222
+ severity: "warn",
223
+ }),
224
+ govern.prop("variant").forbid("secondary", {
225
+ when: { path: "apps/checkout/**" },
226
+ because: "Checkout CTAs should stay visually dominant.",
227
+ fix: { replaceWith: "primary" },
228
+ severity: "error",
229
+ }),
230
+ govern.accessibility().requireName({
231
+ because: "Buttons must announce their action.",
232
+ severity: "error",
233
+ }),
234
+ ],
235
+ });
236
+
237
+ expect(fragment.governance).toEqual([
238
+ { kind: "capability", capability: "dom.button" },
239
+ {
240
+ kind: "prop.value.avoid",
241
+ prop: "variant",
242
+ value: "link",
243
+ because: "Use Link for ordinary navigation.",
244
+ severity: "warn",
245
+ },
246
+ {
247
+ kind: "prop.value.forbid",
248
+ prop: "variant",
249
+ value: "secondary",
250
+ when: { path: "apps/checkout/**" },
251
+ because: "Checkout CTAs should stay visually dominant.",
252
+ fix: { replaceWith: "primary" },
253
+ severity: "error",
254
+ },
255
+ {
256
+ kind: "a11y.requireName",
257
+ because: "Buttons must announce their action.",
258
+ severity: "error",
259
+ },
260
+ ]);
261
+ });
262
+
263
+ it("compiles governed fragments with guidance and governance records", () => {
264
+ const fragment = defineFragment({
265
+ component: Button,
266
+ meta: {
267
+ name: "Button",
268
+ description: "Interactive element for user-triggered actions.",
269
+ category: "forms",
270
+ },
271
+ guidance: {
272
+ when: ["Submitting forms"],
273
+ whenNot: ["Simple navigation without side effects"],
274
+ },
275
+ govern: (govern) => [
276
+ govern.capability("dom.button"),
277
+ govern.prop("variant").forbid("secondary", {
278
+ because: "Checkout CTAs should stay visually dominant.",
279
+ severity: "error",
280
+ }),
281
+ ],
282
+ });
283
+
284
+ const compiled = compileFragment(fragment, "src/components/Button.fragment.ts");
285
+
286
+ expect(compiled.meta.name).toBe("Button");
287
+ expect(compiled.guidance?.when).toEqual(["Submitting forms"]);
288
+ expect(compiled.usage.when).toEqual(["Submitting forms"]);
289
+ expect(compiled.governance).toEqual([
290
+ { kind: "capability", capability: "dom.button" },
291
+ {
292
+ kind: "prop.value.forbid",
293
+ prop: "variant",
294
+ value: "secondary",
295
+ because: "Checkout CTAs should stay visually dominant.",
296
+ severity: "error",
297
+ },
298
+ ]);
299
+ });
300
+ });