@usefragments/core 1.5.2 → 1.7.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/dist/chunk-RANPUC6C.js +72 -0
- package/dist/chunk-RANPUC6C.js.map +1 -0
- package/dist/{chunk-BAHCOAVG.js → chunk-WNMWKUYG.js} +447 -201
- package/dist/chunk-WNMWKUYG.js.map +1 -0
- package/dist/codes/index.d.ts +2 -2
- package/dist/codes/index.js +1 -1
- package/dist/compiled-types/index.d.ts +1 -1
- package/dist/compiled-types/index.js +8 -0
- package/dist/generate/index.d.ts +1 -1
- package/dist/{governance-pKrfh517.d.ts → governance-D9KtH-vg.d.ts} +617 -22
- package/dist/index.d.ts +800 -179
- package/dist/index.js +1003 -237
- package/dist/index.js.map +1 -1
- package/dist/react-types.d.ts +1 -1
- package/dist/registry.d.ts +36 -36
- package/dist/schemas/index.d.ts +1 -1
- package/dist/test-utils.d.ts +1 -1
- package/package.json +2 -1
- package/src/__tests__/policy-exclude.test.ts +180 -0
- package/src/agent-format.test.ts +13 -0
- package/src/agent-format.ts +9 -3
- package/src/canonical-bridge.ts +69 -1
- package/src/canonical-direction.test.ts +118 -0
- package/src/canonical-direction.ts +43 -2
- package/src/codes/__tests__/codes.test.ts +14 -2
- package/src/codes/codes.ts +41 -2
- package/src/compiled-types/index.ts +81 -0
- package/src/compiled-types/parse.test.ts +47 -0
- package/src/facts/builders.ts +48 -0
- package/src/facts/compile.ts +148 -34
- package/src/facts/fact-index.ts +15 -2
- package/src/facts/facts.test.ts +44 -3
- package/src/facts/index.ts +11 -6
- package/src/facts/types.ts +60 -9
- package/src/governance-integrity.test.ts +272 -4
- package/src/governance-integrity.ts +345 -25
- package/src/governance.ts +87 -1
- package/src/index.ts +46 -1
- package/src/policy-exclude.ts +113 -0
- package/src/rules/a11y-required-accessible-name.ts +175 -28
- package/src/rules/a11y-standard.ts +102 -0
- package/src/rules/a11y-utils.ts +7 -0
- package/src/rules/components-prefer-library.test.ts +75 -28
- package/src/rules/components-prefer-library.ts +35 -15
- package/src/rules/components-shadow-component.test.ts +21 -9
- package/src/rules/emit-gate.test.ts +74 -4
- package/src/rules/emit-gate.ts +24 -9
- package/src/rules/families.test.ts +69 -0
- package/src/rules/families.ts +52 -0
- package/src/rules/fix-availability.ts +1 -0
- package/src/rules/index.ts +18 -2
- package/src/rules/jsx-preferred-import-path.ts +29 -11
- package/src/rules/rules.test.ts +188 -8
- package/src/rules/styles-no-raw-color.ts +13 -4
- package/src/rules/styles-no-raw-dimensions.ts +13 -4
- package/src/rules/styles-no-raw-spacing.ts +12 -4
- package/src/rules/styles-no-raw-typography.ts +13 -4
- package/src/rules/tiers.ts +1 -0
- package/src/rules/utils.ts +39 -0
- package/src/tokens/design-token-parser.test.ts +131 -0
- package/src/tokens/design-token-parser.ts +362 -49
- package/src/types.ts +2 -2
- package/dist/chunk-BAHCOAVG.js.map +0 -1
- package/dist/{index-DbkPE46t.d.ts → index-hZAlYCli.d.ts} +8 -8
package/src/facts/types.ts
CHANGED
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
|
|
15
15
|
import type { GovernanceSeverity } from "../governance.js";
|
|
16
16
|
import type { IdentityConfidence, IdentityState } from "../identity/classify.js";
|
|
17
|
+
import type { PolicyExclude } from "../policy-exclude.js";
|
|
17
18
|
|
|
18
19
|
// ---------------------------------------------------------------------------
|
|
19
20
|
// Branded IDs
|
|
@@ -121,34 +122,42 @@ export interface ScaleValueFact extends BaseFact {
|
|
|
121
122
|
value: number;
|
|
122
123
|
}
|
|
123
124
|
|
|
124
|
-
|
|
125
|
+
/**
|
|
126
|
+
* Path excludes carried by a compiled policy fact. Present only when the authoring
|
|
127
|
+
* record declared them, so fact shapes are byte-identical for configs without
|
|
128
|
+
* excludes. Never part of the fact's `factId` inputs: scoping a policy off a path
|
|
129
|
+
* must not move fact ids or the finding fingerprints Cloud and baselines dedupe on.
|
|
130
|
+
*/
|
|
131
|
+
type PolicyFactExcludes = { exclude?: PolicyExclude[] };
|
|
132
|
+
|
|
133
|
+
export interface StyleRawColorForbiddenFact extends BaseFact, PolicyFactExcludes {
|
|
125
134
|
kind: "style_raw_color_forbidden";
|
|
126
135
|
except: string[];
|
|
127
136
|
prefer: "token" | "css-variable";
|
|
128
137
|
severity: GovernanceSeverity;
|
|
129
138
|
}
|
|
130
139
|
|
|
131
|
-
export interface StyleRawDimensionForbiddenFact extends BaseFact {
|
|
140
|
+
export interface StyleRawDimensionForbiddenFact extends BaseFact, PolicyFactExcludes {
|
|
132
141
|
kind: "style_raw_dimension_forbidden";
|
|
133
142
|
appliesTo: string[];
|
|
134
143
|
prefer: "token" | "css-variable";
|
|
135
144
|
severity: GovernanceSeverity;
|
|
136
145
|
}
|
|
137
146
|
|
|
138
|
-
export interface StylePropertyScaleFact extends BaseFact {
|
|
147
|
+
export interface StylePropertyScaleFact extends BaseFact, PolicyFactExcludes {
|
|
139
148
|
kind: "style_property_scale";
|
|
140
149
|
property: string;
|
|
141
150
|
scale: string;
|
|
142
151
|
severity: GovernanceSeverity;
|
|
143
152
|
}
|
|
144
153
|
|
|
145
|
-
export interface StyleFontSizeScaleFact extends BaseFact {
|
|
154
|
+
export interface StyleFontSizeScaleFact extends BaseFact, PolicyFactExcludes {
|
|
146
155
|
kind: "style_font_size_scale";
|
|
147
156
|
scale: string;
|
|
148
157
|
severity: GovernanceSeverity;
|
|
149
158
|
}
|
|
150
159
|
|
|
151
|
-
export interface StyleCssVarsMustBeDefinedFact extends BaseFact {
|
|
160
|
+
export interface StyleCssVarsMustBeDefinedFact extends BaseFact, PolicyFactExcludes {
|
|
152
161
|
kind: "style_css_vars_must_be_defined";
|
|
153
162
|
severity: GovernanceSeverity;
|
|
154
163
|
}
|
|
@@ -166,18 +175,18 @@ export interface ContractTokenFact extends BaseFact {
|
|
|
166
175
|
name: string;
|
|
167
176
|
}
|
|
168
177
|
|
|
169
|
-
export interface JsxUnknownPropsForbiddenFact extends BaseFact {
|
|
178
|
+
export interface JsxUnknownPropsForbiddenFact extends BaseFact, PolicyFactExcludes {
|
|
170
179
|
kind: "jsx_unknown_props_forbidden";
|
|
171
180
|
severity: GovernanceSeverity;
|
|
172
181
|
}
|
|
173
182
|
|
|
174
|
-
export interface JsxInlineStyleForbiddenRawFact extends BaseFact {
|
|
183
|
+
export interface JsxInlineStyleForbiddenRawFact extends BaseFact, PolicyFactExcludes {
|
|
175
184
|
kind: "jsx_inline_style_forbidden_raw";
|
|
176
185
|
property: string;
|
|
177
186
|
severity: GovernanceSeverity;
|
|
178
187
|
}
|
|
179
188
|
|
|
180
|
-
export interface JsxImportPathPreferredFact extends BaseFact {
|
|
189
|
+
export interface JsxImportPathPreferredFact extends BaseFact, PolicyFactExcludes {
|
|
181
190
|
kind: "jsx_import_path_preferred";
|
|
182
191
|
from: string;
|
|
183
192
|
to: string;
|
|
@@ -194,7 +203,7 @@ export interface JsxImportPathPreferredFact extends BaseFact {
|
|
|
194
203
|
};
|
|
195
204
|
}
|
|
196
205
|
|
|
197
|
-
export interface JsxComponentPreferredFact extends BaseFact {
|
|
206
|
+
export interface JsxComponentPreferredFact extends BaseFact, PolicyFactExcludes {
|
|
198
207
|
kind: "jsx_component_preferred";
|
|
199
208
|
from: ComponentId;
|
|
200
209
|
to: ComponentId;
|
|
@@ -343,6 +352,9 @@ export interface UsageInlineStyleFact extends BaseFact {
|
|
|
343
352
|
*/
|
|
344
353
|
valueKind: "static" | "number" | "css-variable" | "dynamic-raw";
|
|
345
354
|
value: string;
|
|
355
|
+
/** Set when `value` was resolved through a hop rather than authored inline.
|
|
356
|
+
* Additive — the fact's identity key is unchanged. */
|
|
357
|
+
valueFrom?: StyleValueProvenance;
|
|
346
358
|
}
|
|
347
359
|
|
|
348
360
|
export interface UsageTextChildFact extends BaseFact {
|
|
@@ -352,6 +364,20 @@ export interface UsageTextChildFact extends BaseFact {
|
|
|
352
364
|
index: number;
|
|
353
365
|
}
|
|
354
366
|
|
|
367
|
+
/**
|
|
368
|
+
* Framework-neutral summary of the rendered child content for one usage node.
|
|
369
|
+
*
|
|
370
|
+
* An adapter that supports semantic child analysis preserves its existing prop
|
|
371
|
+
* facts and additionally emits exactly one summary per usage node, so rules do
|
|
372
|
+
* not have to interpret React's `children`, Vue slots, or Svelte snippets
|
|
373
|
+
* themselves. Adapters may add support independently.
|
|
374
|
+
*/
|
|
375
|
+
export interface UsageChildContentFact extends BaseFact {
|
|
376
|
+
kind: "usage_child_content";
|
|
377
|
+
nodeId: FactId;
|
|
378
|
+
content: "static-text" | "runtime" | "structural" | "empty";
|
|
379
|
+
}
|
|
380
|
+
|
|
355
381
|
// ---------------------------------------------------------------------------
|
|
356
382
|
// Tailwind className facts.
|
|
357
383
|
//
|
|
@@ -511,6 +537,27 @@ export interface TailwindTokenResolvedFact extends BaseFact {
|
|
|
511
537
|
* selector chain. Identity is `{ file, selector, declarationPath, property }`
|
|
512
538
|
* so the ID stays stable when other lines move.
|
|
513
539
|
*/
|
|
540
|
+
/**
|
|
541
|
+
* How a style value reached the fact when the authored text at the use site was
|
|
542
|
+
* not the value itself. Present only for `const-binding` today: the extractor
|
|
543
|
+
* followed exactly one hop to a same-file, immutable, statically-initialized
|
|
544
|
+
* `const` (`const c = '#FFC107'; sx={{ color: c }}`).
|
|
545
|
+
*
|
|
546
|
+
* Two consequences for rules, both load-bearing:
|
|
547
|
+
* - The declaration line is the evidence a reader needs — the use site only
|
|
548
|
+
* shows an identifier.
|
|
549
|
+
* - **No deterministic fix may be attached.** The authored text at the finding's
|
|
550
|
+
* location is the identifier, so any value replacement would be an edit the
|
|
551
|
+
* user never wrote.
|
|
552
|
+
*/
|
|
553
|
+
export interface StyleValueProvenance {
|
|
554
|
+
kind: "const-binding";
|
|
555
|
+
/** Identifier the use site referenced. */
|
|
556
|
+
name: string;
|
|
557
|
+
/** Where the binding was declared. */
|
|
558
|
+
location: FactLocation;
|
|
559
|
+
}
|
|
560
|
+
|
|
514
561
|
export interface StyleDeclarationFact extends BaseFact {
|
|
515
562
|
kind: "style_declaration";
|
|
516
563
|
file: string;
|
|
@@ -527,6 +574,9 @@ export interface StyleDeclarationFact extends BaseFact {
|
|
|
527
574
|
* flagging ad hoc custom properties in product code.
|
|
528
575
|
*/
|
|
529
576
|
declaredTokenSource?: boolean;
|
|
577
|
+
/** Set when `value` was resolved through a hop rather than authored inline.
|
|
578
|
+
* Additive — the fact's identity key is unchanged. */
|
|
579
|
+
valueFrom?: StyleValueProvenance;
|
|
530
580
|
}
|
|
531
581
|
|
|
532
582
|
export type UnsupportedStyleReason =
|
|
@@ -686,6 +736,7 @@ export type UsageFact =
|
|
|
686
736
|
| UsagePropResolvedFact
|
|
687
737
|
| UsageInlineStyleFact
|
|
688
738
|
| UsageTextChildFact
|
|
739
|
+
| UsageChildContentFact
|
|
689
740
|
| StyleDeclarationFact
|
|
690
741
|
| StyleUnsupportedFact
|
|
691
742
|
| ClassNameLiteralFact
|
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
import { describe, expect, it } from "vitest";
|
|
2
2
|
|
|
3
|
-
import type { GovernanceConfig } from "./governance.js";
|
|
3
|
+
import type { GovernanceConfig, GovernanceSeverity } from "./governance.js";
|
|
4
4
|
import {
|
|
5
|
+
collidingRecordDiagnostic,
|
|
6
|
+
collidingRecordDiagnostics,
|
|
7
|
+
configRecordShape,
|
|
8
|
+
overriddenRecordSeverityDiagnostic,
|
|
5
9
|
detectOrphanGovernanceScales,
|
|
6
10
|
detectUnconsumedConfigKeys,
|
|
7
11
|
evaluateGovernanceIntegrity,
|
|
@@ -9,6 +13,12 @@ import {
|
|
|
9
13
|
isEffectiveCanonicalSource,
|
|
10
14
|
type GovernanceIntegrityInput,
|
|
11
15
|
} from "./governance-integrity.js";
|
|
16
|
+
import {
|
|
17
|
+
makeStyleFontSizeScaleFact,
|
|
18
|
+
makeStylePropertyScaleFact,
|
|
19
|
+
makeStyleRawColorForbiddenFact,
|
|
20
|
+
makeTokenDefinitionFact,
|
|
21
|
+
} from "./facts/index.js";
|
|
12
22
|
import { customerDefaultRuleStates } from "./rules/presets.js";
|
|
13
23
|
import { fragmentsConfigSchema } from "./schema.js";
|
|
14
24
|
|
|
@@ -80,7 +90,7 @@ describe("inert config diagnostics", () => {
|
|
|
80
90
|
space: { kind: "scale", unit: "px", values: [0, 4, 8], source: "legacy" },
|
|
81
91
|
},
|
|
82
92
|
rules: {
|
|
83
|
-
"styles/no-raw-color": { enabled: true, exclude: ["vendor/**"] },
|
|
93
|
+
"styles/no-raw-color": { enabled: true, exclude: ["vendor/**"], scope: "app" },
|
|
84
94
|
"styles/not-a-real-rule": { enabled: true },
|
|
85
95
|
},
|
|
86
96
|
},
|
|
@@ -88,7 +98,7 @@ describe("inert config diagnostics", () => {
|
|
|
88
98
|
const parsed = fragmentsConfigSchema.parse(authored);
|
|
89
99
|
|
|
90
100
|
expect(detectUnconsumedConfigKeys(authored, parsed)).toMatchObject([
|
|
91
|
-
{ code: "FUI9004", path: "govern.rules.styles/no-raw-color.
|
|
101
|
+
{ code: "FUI9004", path: "govern.rules.styles/no-raw-color.scope" },
|
|
92
102
|
{ code: "FUI9004", path: "govern.rules.styles/not-a-real-rule" },
|
|
93
103
|
{ code: "FUI9004", path: "govern.scales.space.source" },
|
|
94
104
|
{ code: "FUI9004", path: "screenshots.renderer" },
|
|
@@ -97,6 +107,17 @@ describe("inert config diagnostics", () => {
|
|
|
97
107
|
]);
|
|
98
108
|
});
|
|
99
109
|
|
|
110
|
+
it("does not diagnose a rule-keyed exclude, which the scan consumes", () => {
|
|
111
|
+
const authored = {
|
|
112
|
+
govern: {
|
|
113
|
+
rules: { "styles/no-raw-color": { exclude: ["src/legacy/vendor/**"] } },
|
|
114
|
+
},
|
|
115
|
+
};
|
|
116
|
+
const parsed = fragmentsConfigSchema.parse(authored);
|
|
117
|
+
|
|
118
|
+
expect(detectUnconsumedConfigKeys(authored, parsed)).toEqual([]);
|
|
119
|
+
});
|
|
120
|
+
|
|
100
121
|
it("reports an authored spacing scale that no effective property policy references", () => {
|
|
101
122
|
const declared: GovernanceConfig = {
|
|
102
123
|
scales: {
|
|
@@ -145,6 +166,156 @@ describe("inert config diagnostics", () => {
|
|
|
145
166
|
|
|
146
167
|
expect(detectOrphanGovernanceScales(policy, policy)).toEqual([]);
|
|
147
168
|
});
|
|
169
|
+
|
|
170
|
+
it("accepts govern.ci.failOnInert as a consumed key", () => {
|
|
171
|
+
const authored = { govern: { ci: { failOnInert: true } } };
|
|
172
|
+
|
|
173
|
+
expect(detectUnconsumedConfigKeys(authored, fragmentsConfigSchema.parse(authored))).toEqual([]);
|
|
174
|
+
});
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
describe("collidingRecordDiagnostic", () => {
|
|
178
|
+
const scaleFact = (property: string, scale: string, severity: GovernanceSeverity) =>
|
|
179
|
+
makeStylePropertyScaleFact({ property, scale, severity });
|
|
180
|
+
|
|
181
|
+
it("names the enforced and dropped settings of a colliding config record", () => {
|
|
182
|
+
const kept = scaleFact("padding", "space", "warn");
|
|
183
|
+
const skipped = scaleFact("padding", "myScale", "error");
|
|
184
|
+
|
|
185
|
+
expect(collidingRecordDiagnostic({ kept, skipped })).toEqual({
|
|
186
|
+
code: "FUI9007",
|
|
187
|
+
kind: "colliding-record",
|
|
188
|
+
severity: "warn",
|
|
189
|
+
path: "govern.styles[style.rawSpacing.mustMatchScale property=padding]",
|
|
190
|
+
message: expect.stringContaining("scale=space") as unknown as string,
|
|
191
|
+
});
|
|
192
|
+
expect(collidingRecordDiagnostic({ kept, skipped })?.message).toContain("scale=myScale");
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
it("names the colliding record kind for singleton records", () => {
|
|
196
|
+
expect(
|
|
197
|
+
collidingRecordDiagnostic({
|
|
198
|
+
kept: makeStyleRawColorForbiddenFact({ except: [], prefer: "token", severity: "warn" }),
|
|
199
|
+
skipped: makeStyleRawColorForbiddenFact({
|
|
200
|
+
except: [],
|
|
201
|
+
prefer: "css-variable",
|
|
202
|
+
severity: "error",
|
|
203
|
+
}),
|
|
204
|
+
})?.path
|
|
205
|
+
).toBe("govern.styles[style.rawColors.forbid]");
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
// Two singleton-keyed kinds live in one section. If their diagnostics share a path,
|
|
209
|
+
// the (code, path) dedupe every consumer runs discards one collision entirely — the
|
|
210
|
+
// silent drop this code exists to end, reintroduced one layer up.
|
|
211
|
+
it("gives two different singleton kinds in one section distinct identities", () => {
|
|
212
|
+
const rawColor = collidingRecordDiagnostic({
|
|
213
|
+
kept: makeStyleRawColorForbiddenFact({ except: [], prefer: "token", severity: "warn" }),
|
|
214
|
+
skipped: makeStyleRawColorForbiddenFact({
|
|
215
|
+
except: [],
|
|
216
|
+
prefer: "css-variable",
|
|
217
|
+
severity: "error",
|
|
218
|
+
}),
|
|
219
|
+
});
|
|
220
|
+
const fontSize = collidingRecordDiagnostic({
|
|
221
|
+
kept: makeStyleFontSizeScaleFact({ scale: "type", severity: "warn" }),
|
|
222
|
+
skipped: makeStyleFontSizeScaleFact({ scale: "legacyType", severity: "error" }),
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
expect(rawColor?.path).not.toBe(fontSize?.path);
|
|
226
|
+
expect(fontSize?.path).toBe("govern.styles[style.fontSize.mustMatchScale]");
|
|
227
|
+
expect(fontSize?.message).toContain("style.fontSize.mustMatchScale");
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
it("leaves token-channel collisions to the token diagnostics, not config truth", () => {
|
|
231
|
+
const token = (value: string) =>
|
|
232
|
+
makeTokenDefinitionFact({
|
|
233
|
+
name: "--fui-color-accent",
|
|
234
|
+
value,
|
|
235
|
+
location: { file: "tokens/base.css", line: 1, column: 1 },
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
expect(collidingRecordDiagnostic({ kept: token("#000"), skipped: token("#fff") })).toBeNull();
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
it("ignores collisions between different fact kinds", () => {
|
|
242
|
+
expect(
|
|
243
|
+
collidingRecordDiagnostic({
|
|
244
|
+
kept: scaleFact("padding", "space", "warn"),
|
|
245
|
+
skipped: makeStyleFontSizeScaleFact({ scale: "type", severity: "warn" }),
|
|
246
|
+
})
|
|
247
|
+
).toBeNull();
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
// Three records on one fact id produce TWO conflicts that render the same path. Emitted
|
|
251
|
+
// separately, the (code, path) dedupe every consumer runs kept only the last pair and
|
|
252
|
+
// the intermediate record vanished — the silence this code exists to end, one layer up.
|
|
253
|
+
it("aggregates a three-way collision into one diagnostic that names every drop", () => {
|
|
254
|
+
const diagnostics = collidingRecordDiagnostics([
|
|
255
|
+
{
|
|
256
|
+
kept: scaleFact("padding", "space", "warn"),
|
|
257
|
+
skipped: scaleFact("padding", "myScale", "error"),
|
|
258
|
+
},
|
|
259
|
+
{
|
|
260
|
+
kept: scaleFact("padding", "space", "warn"),
|
|
261
|
+
skipped: scaleFact("padding", "legacy", "info"),
|
|
262
|
+
},
|
|
263
|
+
]);
|
|
264
|
+
|
|
265
|
+
expect(diagnostics).toHaveLength(1);
|
|
266
|
+
expect(diagnostics[0]?.path).toBe(
|
|
267
|
+
"govern.styles[style.rawSpacing.mustMatchScale property=padding]"
|
|
268
|
+
);
|
|
269
|
+
expect(diagnostics[0]?.message).toContain("has 3 `style.rawSpacing.mustMatchScale` records");
|
|
270
|
+
expect(diagnostics[0]?.message).toContain("scale=myScale");
|
|
271
|
+
expect(diagnostics[0]?.message).toContain("scale=legacy");
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
it("keeps distinct fact ids in distinct diagnostics", () => {
|
|
275
|
+
expect(
|
|
276
|
+
collidingRecordDiagnostics([
|
|
277
|
+
{
|
|
278
|
+
kept: scaleFact("padding", "space", "warn"),
|
|
279
|
+
skipped: scaleFact("padding", "myScale", "error"),
|
|
280
|
+
},
|
|
281
|
+
{ kept: scaleFact("gap", "space", "warn"), skipped: scaleFact("gap", "myScale", "error") },
|
|
282
|
+
]).map((diagnostic) => diagnostic.path)
|
|
283
|
+
).toEqual([
|
|
284
|
+
"govern.styles[style.rawSpacing.mustMatchScale property=padding]",
|
|
285
|
+
"govern.styles[style.rawSpacing.mustMatchScale property=gap]",
|
|
286
|
+
]);
|
|
287
|
+
});
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
describe("overriddenRecordSeverityDiagnostic", () => {
|
|
291
|
+
it("names the record, the override that outranked it, and both severities", () => {
|
|
292
|
+
expect(
|
|
293
|
+
overriddenRecordSeverityDiagnostic({
|
|
294
|
+
ruleId: "styles/no-raw-color",
|
|
295
|
+
source: "govern.rules[tokens/hardcoded-values]",
|
|
296
|
+
section: "govern.styles",
|
|
297
|
+
record: "style.rawColors.forbid",
|
|
298
|
+
authored: "serious",
|
|
299
|
+
enforced: "moderate",
|
|
300
|
+
})
|
|
301
|
+
).toEqual({
|
|
302
|
+
code: "FUI9008",
|
|
303
|
+
kind: "overridden-record-severity",
|
|
304
|
+
severity: "warn",
|
|
305
|
+
path: "govern.styles[style.rawColors.forbid]",
|
|
306
|
+
message: expect.stringContaining(
|
|
307
|
+
"govern.rules[tokens/hardcoded-values]"
|
|
308
|
+
) as unknown as string,
|
|
309
|
+
});
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
it("reads the authoring record off the one config-record table", () => {
|
|
313
|
+
expect(configRecordShape("style_raw_color_forbidden")).toEqual({
|
|
314
|
+
section: "govern.styles",
|
|
315
|
+
record: "style.rawColors.forbid",
|
|
316
|
+
});
|
|
317
|
+
expect(configRecordShape("token_definition")).toBeNull();
|
|
318
|
+
});
|
|
148
319
|
});
|
|
149
320
|
|
|
150
321
|
describe("evaluateGovernanceIntegrity", () => {
|
|
@@ -172,7 +343,10 @@ describe("evaluateGovernanceIntegrity", () => {
|
|
|
172
343
|
canonicalSources: [{ kind: "directory", path: "ui" }],
|
|
173
344
|
};
|
|
174
345
|
const verdict = evaluate({ policy, policySource: "config", declared: true });
|
|
175
|
-
expect(verdict.families.find((f) => f.id === "components")
|
|
346
|
+
expect(verdict.families.find((f) => f.id === "components")).toMatchObject({
|
|
347
|
+
armed: true,
|
|
348
|
+
rules: ["components/prefer-library", "components/shadow-component"],
|
|
349
|
+
});
|
|
176
350
|
expect(verdict.status).toBe("healthy");
|
|
177
351
|
expect(verdict.blockingCapable).toBe(false);
|
|
178
352
|
expect(verdict.armed).toContain("components");
|
|
@@ -201,6 +375,83 @@ describe("evaluateGovernanceIntegrity", () => {
|
|
|
201
375
|
expect(verdict.blockingCapable).toBe(true);
|
|
202
376
|
});
|
|
203
377
|
|
|
378
|
+
it("4c. prefer-library disabled + canonical source leaves shadow governance armed", () => {
|
|
379
|
+
const policy: GovernanceConfig = {
|
|
380
|
+
severity: "error",
|
|
381
|
+
canonicalSources: [{ kind: "directory", path: "ui" }],
|
|
382
|
+
rules: { "components/prefer-library": false },
|
|
383
|
+
};
|
|
384
|
+
const verdict = evaluate({ policy, policySource: "config", declared: true });
|
|
385
|
+
expect(verdict.families.find((f) => f.id === "components")).toMatchObject({
|
|
386
|
+
armed: true,
|
|
387
|
+
rules: ["components/shadow-component"],
|
|
388
|
+
});
|
|
389
|
+
expect(verdict.families.find((f) => f.id === "blocking-deny")?.rules).toEqual([
|
|
390
|
+
"components/shadow-component",
|
|
391
|
+
]);
|
|
392
|
+
expect(verdict.status).toBe("healthy");
|
|
393
|
+
expect(verdict.blockingCapable).toBe(true);
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
it("4d. shadow-component disabled + canonical source leaves prefer-library armed", () => {
|
|
397
|
+
const policy: GovernanceConfig = {
|
|
398
|
+
severity: "error",
|
|
399
|
+
canonicalSources: [{ kind: "directory", path: "ui" }],
|
|
400
|
+
rules: { "components/shadow-component": false },
|
|
401
|
+
};
|
|
402
|
+
const verdict = evaluate({ policy, policySource: "config", declared: true });
|
|
403
|
+
expect(verdict.families.find((f) => f.id === "components")).toMatchObject({
|
|
404
|
+
armed: true,
|
|
405
|
+
rules: ["components/prefer-library"],
|
|
406
|
+
});
|
|
407
|
+
expect(verdict.families.find((f) => f.id === "blocking-deny")?.rules).toEqual([
|
|
408
|
+
"components/prefer-library",
|
|
409
|
+
]);
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
it("4e. explicitly disabling both canonical-component rules leaves the policy inert", () => {
|
|
413
|
+
const policy: GovernanceConfig = {
|
|
414
|
+
severity: "error",
|
|
415
|
+
canonicalSources: [{ kind: "directory", path: "ui" }],
|
|
416
|
+
rules: {
|
|
417
|
+
"components/prefer-library": false,
|
|
418
|
+
"components/shadow-component": false,
|
|
419
|
+
},
|
|
420
|
+
};
|
|
421
|
+
const verdict = evaluate({ policy, policySource: "config", declared: true });
|
|
422
|
+
expect(verdict.families.find((f) => f.id === "components")).toMatchObject({
|
|
423
|
+
armed: false,
|
|
424
|
+
rules: [],
|
|
425
|
+
reason: "canonical-component rules not enabled",
|
|
426
|
+
});
|
|
427
|
+
expect(verdict.status).toBe("inert");
|
|
428
|
+
expect(verdict.blockingCapable).toBe(false);
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
it("4f. vocabulary on a disabled sibling rule cannot arm shadow governance", () => {
|
|
432
|
+
const policy: GovernanceConfig = {
|
|
433
|
+
rules: {
|
|
434
|
+
"components/prefer-library": {
|
|
435
|
+
enabled: false,
|
|
436
|
+
options: {
|
|
437
|
+
canonicalMappings: [
|
|
438
|
+
{ name: "Button", htmlEquivalent: "button", importPath: "@acme/ui" },
|
|
439
|
+
],
|
|
440
|
+
},
|
|
441
|
+
},
|
|
442
|
+
"components/shadow-component": { enabled: true, severity: "error" },
|
|
443
|
+
},
|
|
444
|
+
};
|
|
445
|
+
const verdict = evaluate({ policy, policySource: "config", declared: true });
|
|
446
|
+
expect(verdict.families.find((f) => f.id === "components")).toMatchObject({
|
|
447
|
+
armed: false,
|
|
448
|
+
rules: ["components/shadow-component"],
|
|
449
|
+
reason: "component governance enabled but no effective canonical source",
|
|
450
|
+
});
|
|
451
|
+
expect(verdict.status).toBe("inert");
|
|
452
|
+
expect(verdict.blockingCapable).toBe(false);
|
|
453
|
+
});
|
|
454
|
+
|
|
204
455
|
it("5. npm source with NO include → components NOT armed → inert (blocker-#2 lock)", () => {
|
|
205
456
|
const policy: GovernanceConfig = {
|
|
206
457
|
canonicalSources: [{ kind: "npm", specifier: "@acme/ui" }],
|
|
@@ -274,6 +525,23 @@ describe("evaluateGovernanceIntegrity", () => {
|
|
|
274
525
|
expect(tokens?.reason).toBe("no token vocabulary");
|
|
275
526
|
});
|
|
276
527
|
|
|
528
|
+
it("7c. error-level token drift with a vocabulary is blocking-capable", () => {
|
|
529
|
+
const policy: GovernanceConfig = {
|
|
530
|
+
styles: [{ kind: "style.cssVars.mustBeDefined", severity: "error" }],
|
|
531
|
+
};
|
|
532
|
+
const verdict = evaluate({
|
|
533
|
+
policy,
|
|
534
|
+
policySource: "config",
|
|
535
|
+
declared: true,
|
|
536
|
+
cssVarsActive: true,
|
|
537
|
+
tokenVocabularySize: 5,
|
|
538
|
+
});
|
|
539
|
+
expect(verdict.families.find((family) => family.id === "blocking-deny")?.rules).toContain(
|
|
540
|
+
"tokens/css-vars-must-be-defined"
|
|
541
|
+
);
|
|
542
|
+
expect(verdict.blockingCapable).toBe(true);
|
|
543
|
+
});
|
|
544
|
+
|
|
277
545
|
it("8. hygiene-only policy (raw-style rule enabled, no contract) → degraded", () => {
|
|
278
546
|
const policy: GovernanceConfig = {
|
|
279
547
|
rules: { "styles/no-raw-color": { enabled: true, severity: "warn" } },
|