@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.
- package/LICENSE +21 -0
- package/dist/chunk-243QYRUF.js +2145 -0
- package/dist/chunk-243QYRUF.js.map +1 -0
- package/dist/chunk-3LLRNCPX.js +111 -0
- package/dist/chunk-3LLRNCPX.js.map +1 -0
- package/dist/chunk-EIYNNS77.js +48 -0
- package/dist/chunk-EIYNNS77.js.map +1 -0
- package/dist/chunk-JNBFJ34I.js +326 -0
- package/dist/chunk-JNBFJ34I.js.map +1 -0
- package/dist/chunk-R3X7UBHU.js +228 -0
- package/dist/chunk-R3X7UBHU.js.map +1 -0
- package/dist/chunk-SH4KPIYH.js +393 -0
- package/dist/chunk-SH4KPIYH.js.map +1 -0
- package/dist/chunk-V4VQB57N.js +56 -0
- package/dist/chunk-V4VQB57N.js.map +1 -0
- package/dist/chunk-X34IA4LR.js +319 -0
- package/dist/chunk-X34IA4LR.js.map +1 -0
- package/dist/chunk-YF65VYRY.js +493 -0
- package/dist/chunk-YF65VYRY.js.map +1 -0
- package/dist/codes/index.d.ts +40 -0
- package/dist/codes/index.js +23 -0
- package/dist/codes/index.js.map +1 -0
- package/dist/compiled-types/index.d.ts +5 -0
- package/dist/compiled-types/index.js +1 -0
- package/dist/compiled-types/index.js.map +1 -0
- package/dist/generate/index.d.ts +47 -0
- package/dist/generate/index.js +11 -0
- package/dist/generate/index.js.map +1 -0
- package/dist/governance-BOa3KyiJ.d.ts +3279 -0
- package/dist/graph/index.d.ts +66 -0
- package/dist/graph/index.js +17 -0
- package/dist/graph/index.js.map +1 -0
- package/dist/index.d.ts +11388 -0
- package/dist/index.js +8711 -0
- package/dist/index.js.map +1 -0
- package/dist/package-identity.d.ts +42 -0
- package/dist/package-identity.js +23 -0
- package/dist/package-identity.js.map +1 -0
- package/dist/preview/index.d.ts +484 -0
- package/dist/preview/index.js +1129 -0
- package/dist/preview/index.js.map +1 -0
- package/dist/react-types.d.ts +22 -0
- package/dist/react-types.js +1 -0
- package/dist/react-types.js.map +1 -0
- package/dist/registry.d.ts +2367 -0
- package/dist/registry.js +62 -0
- package/dist/registry.js.map +1 -0
- package/dist/schemas/index.d.ts +2584 -0
- package/dist/schemas/index.js +55 -0
- package/dist/schemas/index.js.map +1 -0
- package/dist/severity.d.ts +18 -0
- package/dist/severity.js +27 -0
- package/dist/severity.js.map +1 -0
- package/dist/test-utils.d.ts +14 -0
- package/dist/test-utils.js +63 -0
- package/dist/test-utils.js.map +1 -0
- package/dist/topology/index.d.ts +48 -0
- package/dist/topology/index.js +7 -0
- package/dist/topology/index.js.map +1 -0
- package/dist/types-xJ2xyp_G.d.ts +119 -0
- package/package.json +120 -0
- package/src/__tests__/contract-parser.test.ts +614 -0
- package/src/__tests__/dtcg-generators.test.ts +439 -0
- package/src/__tests__/dtcg-parser.test.ts +884 -0
- package/src/__tests__/preview-runtime.test.tsx +111 -0
- package/src/__tests__/schema.test.ts +31 -0
- package/src/__tests__/severity.test.ts +34 -0
- package/src/__tests__/style-comparison.test.ts +662 -0
- package/src/__tests__/tokens-compat.test.ts +51 -0
- package/src/agent-format.test.ts +105 -0
- package/src/agent-format.ts +141 -0
- package/src/bundle.ts +149 -0
- package/src/codes/__tests__/codes.test.ts +73 -0
- package/src/codes/codes.ts +463 -0
- package/src/codes/index.ts +7 -0
- package/src/codes/source-finding-bridge.ts +113 -0
- package/src/compiled-types/index.ts +293 -0
- package/src/component-contract.ts +231 -0
- package/src/component-discovery.ts +80 -0
- package/src/composition.test.ts +262 -0
- package/src/composition.ts +318 -0
- package/src/config-paths.test.ts +78 -0
- package/src/config-paths.ts +47 -0
- package/src/config.ts +18 -0
- package/src/conform-prove.test.ts +274 -0
- package/src/conform-prove.ts +326 -0
- package/src/conform.ts +160 -0
- package/src/constants.ts +114 -0
- package/src/context.ts +2 -0
- package/src/contract/fixture.ts +110 -0
- package/src/contract/hash.test.ts +86 -0
- package/src/contract/hash.ts +195 -0
- package/src/contract/index.ts +43 -0
- package/src/contract/preimage.test.ts +254 -0
- package/src/contract/preimage.ts +365 -0
- package/src/contract/stamp.test.ts +100 -0
- package/src/contract/stamp.ts +105 -0
- package/src/contract-parser.ts +141 -0
- package/src/defineFragment.test.ts +257 -0
- package/src/defineFragment.ts +293 -0
- package/src/dtcg-generators.ts +324 -0
- package/src/dtcg-parser.ts +1 -0
- package/src/dtcg.ts +237 -0
- package/src/facts/__snapshots__/facts.test.ts.snap +124 -0
- package/src/facts/builders.ts +835 -0
- package/src/facts/compile.ts +422 -0
- package/src/facts/fact-index.ts +394 -0
- package/src/facts/facts.test.ts +494 -0
- package/src/facts/ids.ts +125 -0
- package/src/facts/index.ts +119 -0
- package/src/facts/types.ts +614 -0
- package/src/figma.ts +263 -0
- package/src/fragment-types.ts +214 -0
- package/src/generate/generate.test.ts +594 -0
- package/src/generate/index.ts +438 -0
- package/src/governance-integrity.test.ts +186 -0
- package/src/governance-integrity.ts +282 -0
- package/src/governance.test.ts +300 -0
- package/src/governance.ts +605 -0
- package/src/governance.type-test.ts +75 -0
- package/src/graph/engine.ts +446 -0
- package/src/graph/graph.test.ts +746 -0
- package/src/graph/index.ts +32 -0
- package/src/graph/serialization.ts +55 -0
- package/src/graph/types.ts +178 -0
- package/src/index.ts +891 -0
- package/src/local-canonical.ts +102 -0
- package/src/mcp-snapshot.ts +222 -0
- package/src/package-identity-match.ts +33 -0
- package/src/package-identity.generated.ts +233 -0
- package/src/package-identity.test.ts +99 -0
- package/src/package-identity.ts +157 -0
- package/src/performance-presets.ts +142 -0
- package/src/preview/comparison-artifact.test.ts +228 -0
- package/src/preview/comparison-artifact.ts +181 -0
- package/src/preview/comparison-context.ts +93 -0
- package/src/preview/comparison-evidence.ts +112 -0
- package/src/preview/comparison-format.ts +21 -0
- package/src/preview/comparison-presentation.ts +187 -0
- package/src/preview/comparison-references.test.ts +430 -0
- package/src/preview/comparison-references.ts +72 -0
- package/src/preview/comparison-score.ts +59 -0
- package/src/preview/comparison-styles.ts +197 -0
- package/src/preview/comparison.test.ts +559 -0
- package/src/preview/comparison.ts +488 -0
- package/src/preview/index.ts +10 -0
- package/src/preview/types.ts +309 -0
- package/src/preview/validation.test.ts +290 -0
- package/src/preview/validation.ts +356 -0
- package/src/preview-runtime.tsx +144 -0
- package/src/raw-html-canonical.test.ts +41 -0
- package/src/raw-html-canonical.ts +179 -0
- package/src/react-types.ts +21 -0
- package/src/registry-install-plan.ts +344 -0
- package/src/registry.test.ts +245 -0
- package/src/registry.ts +369 -0
- package/src/rules/a11y-required-accessible-name.ts +93 -0
- package/src/rules/color-math.test.ts +72 -0
- package/src/rules/color-math.ts +192 -0
- package/src/rules/components-forbidden-prop-value.ts +108 -0
- package/src/rules/components-prefer-library.ts +943 -0
- package/src/rules/components-unknown-prop.ts +94 -0
- package/src/rules/composition-pattern-schema.ts +125 -0
- package/src/rules/composition-pattern.test.ts +320 -0
- package/src/rules/composition-pattern.ts +271 -0
- package/src/rules/emit-gate.test.ts +42 -0
- package/src/rules/emit-gate.ts +60 -0
- package/src/rules/finding.ts +54 -0
- package/src/rules/index.ts +235 -0
- package/src/rules/jsx-preferred-component.ts +68 -0
- package/src/rules/jsx-preferred-import-path.ts +67 -0
- package/src/rules/presets.ts +57 -0
- package/src/rules/props-invalid-value.ts +115 -0
- package/src/rules/rules.test.ts +3636 -0
- package/src/rules/spacing-resolution.ts +184 -0
- package/src/rules/styles-no-raw-color.ts +329 -0
- package/src/rules/styles-no-raw-dimensions.ts +357 -0
- package/src/rules/styles-no-raw-spacing.ts +236 -0
- package/src/rules/styles-no-raw-typography.ts +237 -0
- package/src/rules/tailwind-arbitrary-color.test.ts +128 -0
- package/src/rules/tailwind-arbitrary-color.ts +93 -0
- package/src/rules/tailwind-arbitrary-spacing.test.ts +157 -0
- package/src/rules/tailwind-arbitrary-spacing.ts +120 -0
- package/src/rules/tailwind-forbidden-palette.test.ts +116 -0
- package/src/rules/tailwind-forbidden-palette.ts +107 -0
- package/src/rules/tailwind-off-scale-spacing-token.test.ts +100 -0
- package/src/rules/tailwind-off-scale-spacing-token.ts +108 -0
- package/src/rules/tailwind-raw-color-via-token.test.ts +108 -0
- package/src/rules/tailwind-raw-color-via-token.ts +82 -0
- package/src/rules/tailwind-rule-utils.ts +106 -0
- package/src/rules/tailwind-unknown-class.test.ts +157 -0
- package/src/rules/tailwind-unknown-class.ts +121 -0
- package/src/rules/taxonomy.test.ts +238 -0
- package/src/rules/theme-no-theme-coupled-literal.ts +132 -0
- package/src/rules/tiers.ts +111 -0
- package/src/rules/tokens-css-vars-must-be-defined.test.ts +244 -0
- package/src/rules/tokens-css-vars-must-be-defined.ts +156 -0
- package/src/rules/tokens-require-dual-fallback.ts +78 -0
- package/src/rules/types.ts +27 -0
- package/src/rules/utils.test.ts +110 -0
- package/src/rules/utils.ts +599 -0
- package/src/schema.ts +427 -0
- package/src/schemas/index.ts +275 -0
- package/src/schemas/schemas.test.ts +124 -0
- package/src/severity.ts +54 -0
- package/src/storyAdapter.test.ts +571 -0
- package/src/storyAdapter.ts +761 -0
- package/src/storyFilters.test.ts +350 -0
- package/src/storyFilters.ts +253 -0
- package/src/storybook-csf.ts +11 -0
- package/src/style-comparison.ts +423 -0
- package/src/test-utils.ts +68 -0
- package/src/token-parser.ts +2 -0
- package/src/token-types.ts +287 -0
- package/src/tokens/categories.ts +131 -0
- package/src/tokens/color.ts +193 -0
- package/src/tokens/design-token-parser.test.ts +159 -0
- package/src/tokens/design-token-parser.ts +402 -0
- package/src/tokens/dtcg.ts +428 -0
- package/src/tokens/index.ts +130 -0
- package/src/tokens/lookup.ts +19 -0
- package/src/tokens/scss.ts +171 -0
- package/src/tokens/tailwind-v4.ts +187 -0
- package/src/tokens/types.ts +43 -0
- package/src/topology/index.ts +9 -0
- package/src/topology/resolve-area.test.ts +174 -0
- package/src/topology/resolve-area.ts +215 -0
- 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
|
+
});
|