@usefragments/core 1.5.2 → 1.6.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 (41) hide show
  1. package/dist/{chunk-BAHCOAVG.js → chunk-WVFNDPM4.js} +423 -189
  2. package/dist/chunk-WVFNDPM4.js.map +1 -0
  3. package/dist/codes/index.d.ts +1 -1
  4. package/dist/codes/index.js +1 -1
  5. package/dist/compiled-types/index.d.ts +1 -1
  6. package/dist/generate/index.d.ts +1 -1
  7. package/dist/{governance-pKrfh517.d.ts → governance-DxFipN5V.d.ts} +609 -20
  8. package/dist/index.d.ts +638 -43
  9. package/dist/index.js +304 -37
  10. package/dist/index.js.map +1 -1
  11. package/dist/react-types.d.ts +1 -1
  12. package/dist/test-utils.d.ts +1 -1
  13. package/package.json +1 -1
  14. package/src/__tests__/policy-exclude.test.ts +180 -0
  15. package/src/canonical-bridge.ts +69 -1
  16. package/src/canonical-direction.test.ts +118 -0
  17. package/src/canonical-direction.ts +43 -2
  18. package/src/codes/__tests__/codes.test.ts +14 -1
  19. package/src/codes/codes.ts +40 -0
  20. package/src/facts/builders.ts +35 -0
  21. package/src/facts/compile.ts +135 -21
  22. package/src/facts/fact-index.ts +15 -2
  23. package/src/facts/facts.test.ts +6 -2
  24. package/src/facts/index.ts +9 -6
  25. package/src/facts/types.ts +45 -9
  26. package/src/governance-integrity.test.ts +174 -3
  27. package/src/governance-integrity.ts +305 -5
  28. package/src/governance.ts +87 -1
  29. package/src/index.ts +38 -1
  30. package/src/policy-exclude.ts +113 -0
  31. package/src/rules/families.test.ts +69 -0
  32. package/src/rules/families.ts +52 -0
  33. package/src/rules/index.ts +6 -0
  34. package/src/rules/jsx-preferred-import-path.ts +29 -11
  35. package/src/rules/rules.test.ts +125 -1
  36. package/src/rules/styles-no-raw-color.ts +13 -4
  37. package/src/rules/styles-no-raw-dimensions.ts +13 -4
  38. package/src/rules/styles-no-raw-spacing.ts +12 -4
  39. package/src/rules/styles-no-raw-typography.ts +13 -4
  40. package/src/rules/utils.ts +39 -0
  41. package/dist/chunk-BAHCOAVG.js.map +0 -1
@@ -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.exclude" },
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", () => {
@@ -18,14 +18,26 @@
18
18
  */
19
19
 
20
20
  import type { CanonicalSource, GovernanceConfig, GovernanceSeverity } from "./governance.js";
21
+ import {
22
+ normalizePolicyExcludes,
23
+ policyExcludeMatchesPath,
24
+ type PolicyExclude,
25
+ } from "./policy-exclude.js";
21
26
  import { compileGlobalGovernanceFacts } from "./facts/index.js";
27
+ import type { FactConflict } from "./facts/index.js";
28
+ import { RULE_FAMILY_IDS } from "./rules/families.js";
22
29
  import { FRAGMENTS_INTERNAL_RULE_IDS, RULE_TIER } from "./rules/tiers.js";
23
30
  import { BLOCKING_RULE_ALLOWLIST } from "./rules/emit-gate.js";
24
31
 
25
32
  export type GovernanceIntegrityStatus = "healthy" | "degraded" | "inert";
26
33
 
27
- export type InertConfigDiagnosticKind = "orphan-scale" | "unconsumed-key";
28
- export type InertConfigDiagnosticCode = "FUI9004" | "FUI9005";
34
+ export type InertConfigDiagnosticKind =
35
+ | "orphan-scale"
36
+ | "unconsumed-key"
37
+ | "unmatched-exclude"
38
+ | "colliding-record"
39
+ | "overridden-record-severity";
40
+ export type InertConfigDiagnosticCode = "FUI9004" | "FUI9005" | "FUI9006" | "FUI9007" | "FUI9008";
29
41
 
30
42
  export interface InertConfigDiagnostic {
31
43
  code: InertConfigDiagnosticCode;
@@ -152,10 +164,13 @@ function dedupe(values: string[]): string[] {
152
164
  return [...new Set(values)];
153
165
  }
154
166
 
155
- const RULE_FAMILY_IDS = new Set(["tokens/hardcoded-values", "components/usage", "a11y/wcag"]);
156
167
  const CONSUMED_RULE_IDS = new Set(Object.keys(RULE_TIER));
157
168
  const RECOGNIZED_RULE_IDS = new Set([...CONSUMED_RULE_IDS, ...RULE_FAMILY_IDS]);
158
- const RULE_CONFIG_KEYS = new Set(["enabled", "severity", "options"]);
169
+ // `exclude` is consumed by the scan's finding-override pass (it scopes the rule off
170
+ // the matching paths and reports each drop in the ignored accounting). It was absent
171
+ // from this allow-set, so a working key was diagnosed as inert — the diagnostic was
172
+ // wrong, not the config.
173
+ const RULE_CONFIG_KEYS = new Set(["enabled", "severity", "options", "exclude"]);
159
174
 
160
175
  const PASSTHROUGH_KEYS = [
161
176
  {
@@ -195,7 +210,7 @@ const PASSTHROUGH_KEYS = [
195
210
  },
196
211
  {
197
212
  path: ["govern", "ci"],
198
- keys: ["failOnWarnings"],
213
+ keys: ["failOnWarnings", "failOnInert"],
199
214
  },
200
215
  ] as const;
201
216
 
@@ -237,6 +252,232 @@ function unconsumedKeyDiagnostic(path: readonly (string | number)[]): InertConfi
237
252
  };
238
253
  }
239
254
 
255
+ /**
256
+ * Config-record fact kinds: the policy facts that a `govern.*` record compiles into,
257
+ * paired with the config section that authored them, the authored record that produced
258
+ * them, and the identity keys their fact id is built from.
259
+ *
260
+ * `record` is load-bearing, not decoration. Several kinds in one section key on nothing
261
+ * (`style.rawColors.forbid`, `style.cssVars.mustBeDefined`, …) or on the same fields
262
+ * (`jsx.importPath.prefer` and `jsx.component.prefer` both on from/to), so a
263
+ * section-only diagnostic path makes two DIFFERENT collisions look like one — and the
264
+ * (code, path) dedupe every consumer runs then discards all but the last, which is the
265
+ * silence this diagnostic exists to end.
266
+ *
267
+ * Deliberately NOT the whole `PolicyFact` union — `token_definition` and
268
+ * `contract_token` are compiled from token *files*, not authored config records, and
269
+ * their collisions belong to the token-catalog diagnostics channel. Naming a duplicate
270
+ * token as a config defect would misattribute it and drown the real signal.
271
+ */
272
+ const CONFIG_RECORD_FACT_KINDS: Readonly<
273
+ Record<string, { section: string; record: string; keys: string[] }>
274
+ > = {
275
+ scale: { section: "govern.scales", record: "scale", keys: ["name"] },
276
+ scale_value: { section: "govern.scales", record: "scale.values", keys: ["scale", "value"] },
277
+ style_raw_color_forbidden: {
278
+ section: "govern.styles",
279
+ record: "style.rawColors.forbid",
280
+ keys: [],
281
+ },
282
+ style_raw_dimension_forbidden: {
283
+ section: "govern.styles",
284
+ record: "style.rawDimensions.forbid",
285
+ keys: [],
286
+ },
287
+ style_property_scale: {
288
+ section: "govern.styles",
289
+ record: "style.rawSpacing.mustMatchScale",
290
+ keys: ["property"],
291
+ },
292
+ style_font_size_scale: {
293
+ section: "govern.styles",
294
+ record: "style.fontSize.mustMatchScale",
295
+ keys: [],
296
+ },
297
+ style_css_vars_must_be_defined: {
298
+ section: "govern.styles",
299
+ record: "style.cssVars.mustBeDefined",
300
+ keys: [],
301
+ },
302
+ jsx_unknown_props_forbidden: {
303
+ section: "govern.jsx",
304
+ record: "jsx.unknownProps.forbid",
305
+ keys: [],
306
+ },
307
+ jsx_inline_style_forbidden_raw: {
308
+ section: "govern.jsx",
309
+ record: "jsx.inlineStyle.forbidRaw",
310
+ keys: ["property"],
311
+ },
312
+ jsx_import_path_preferred: {
313
+ section: "govern.jsx",
314
+ record: "jsx.importPath.prefer",
315
+ keys: ["from", "to", "imported"],
316
+ },
317
+ jsx_component_preferred: {
318
+ section: "govern.jsx",
319
+ record: "jsx.component.prefer",
320
+ keys: ["from", "to"],
321
+ },
322
+ prop_value_avoided: {
323
+ section: "govern.components",
324
+ record: "prop.value.avoid",
325
+ keys: ["componentId", "prop", "value"],
326
+ },
327
+ prop_value_forbidden: {
328
+ section: "govern.components",
329
+ record: "prop.value.forbid",
330
+ keys: ["componentId", "prop", "value", "pathPattern"],
331
+ },
332
+ a11y_name_required: {
333
+ section: "govern.components",
334
+ record: "a11y.requireName",
335
+ keys: ["componentId"],
336
+ },
337
+ tailwind_palette_allow: {
338
+ section: "govern.tailwind",
339
+ record: "palette.allow",
340
+ keys: [],
341
+ },
342
+ tailwind_palette_deny: { section: "govern.tailwind", record: "palette.deny", keys: [] },
343
+ tailwind_unknown_class_enabled: {
344
+ section: "govern.tailwind",
345
+ record: "unknownClass",
346
+ keys: [],
347
+ },
348
+ governance_rule_config: { section: "govern.rules", record: "rule", keys: ["ruleId"] },
349
+ };
350
+
351
+ /** The authored settings a reader needs to see to tell two colliding records apart. */
352
+ const COLLISION_DETAIL_KEYS = [
353
+ "severity",
354
+ "scale",
355
+ "prefer",
356
+ "enabled",
357
+ "appliesTo",
358
+ "except",
359
+ "properties",
360
+ "because",
361
+ ] as const;
362
+
363
+ function renderFactFields(fact: Record<string, unknown>, keys: readonly string[]): string {
364
+ return keys
365
+ .filter((key) => fact[key] !== undefined)
366
+ .map((key) => `${key}=${Array.isArray(fact[key]) ? JSON.stringify(fact[key]) : fact[key]}`)
367
+ .join(" ");
368
+ }
369
+
370
+ /**
371
+ * Name a config record that the fact index dropped as a first-wins duplicate.
372
+ *
373
+ * This is the Layer-6 backstop, not the primary mechanism: merge-time displacement
374
+ * (`mergeGovernanceConfigs`) already makes the later record win for every shape it can
375
+ * key. What reaches here is a collision displacement could NOT resolve — two records
376
+ * that compile to one fact id from different channels (config vs bridge vs Cloud
377
+ * policy). The invariant it defends: a dropped policy fact is never silent.
378
+ *
379
+ * Returns `null` for non-config facts, so usage/token collisions stay on the internal
380
+ * debug channel.
381
+ */
382
+ export function collidingRecordDiagnostic(conflict: FactConflict): InertConfigDiagnostic | null {
383
+ return collidingRecordDiagnostics([conflict])[0] ?? null;
384
+ }
385
+
386
+ /**
387
+ * Aggregate every observed collision into one diagnostic per colliding fact id.
388
+ *
389
+ * Three same-kind records on one fact id produce TWO conflicts (first-wins keeps the
390
+ * first and skips both others), and both render the same path. Emitting them separately
391
+ * meant the `(code, path)` dedupe every consumer runs kept only the last pair, so the
392
+ * intermediate record was dropped in silence — the same failure the per-kind path fixed
393
+ * one layer down. Aggregating names the count and lists every dropped setting, and one
394
+ * user edit still resolves the whole group.
395
+ */
396
+ export function collidingRecordDiagnostics(
397
+ conflicts: readonly FactConflict[]
398
+ ): InertConfigDiagnostic[] {
399
+ const groups = new Map<
400
+ string,
401
+ { record: string; kept: string; dropped: string[]; keptFallback: string }
402
+ >();
403
+
404
+ for (const conflict of conflicts) {
405
+ const shape = CONFIG_RECORD_FACT_KINDS[conflict.kept.kind];
406
+ if (!shape || conflict.kept.kind !== conflict.skipped.kind) continue;
407
+
408
+ const keptFact = conflict.kept as unknown as Record<string, unknown>;
409
+ const skippedFact = conflict.skipped as unknown as Record<string, unknown>;
410
+ const identity = renderFactFields(keptFact, shape.keys);
411
+ // The record kind is part of the identity, not just the prose: two singleton-keyed
412
+ // kinds in one section would otherwise share a path and collapse under the (code,
413
+ // path) dedupe, dropping one collision silently.
414
+ const path = `${shape.section}[${identity ? `${shape.record} ${identity}` : shape.record}]`;
415
+ const group = groups.get(path) ?? {
416
+ record: shape.record,
417
+ kept: renderFactFields(keptFact, COLLISION_DETAIL_KEYS),
418
+ dropped: [],
419
+ keptFallback: "the first record",
420
+ };
421
+ const dropped = renderFactFields(skippedFact, COLLISION_DETAIL_KEYS);
422
+ group.dropped.push(dropped || `record ${group.dropped.length + 2}`);
423
+ groups.set(path, group);
424
+ }
425
+
426
+ return [...groups].map(([path, group]) => ({
427
+ code: "FUI9007" as const,
428
+ kind: "colliding-record" as const,
429
+ severity: "warn" as const,
430
+ path,
431
+ message:
432
+ `${path} has ${group.dropped.length + 1} \`${group.record}\` records that compile to the same policy fact — ` +
433
+ `enforcing ${group.kept || group.keptFallback} and dropping ${group.dropped.join("; ")}. ` +
434
+ "Author one record for this policy, or scope them apart, so the enforced setting is the one you can read.",
435
+ }));
436
+ }
437
+
438
+ /**
439
+ * The authored config record a policy fact was compiled from, or `null` when the fact
440
+ * did not come from a `govern.*` record (token facts, usage facts). One table, read by
441
+ * both the collision diagnostic and the override backstop below.
442
+ */
443
+ export function configRecordShape(kind: string): { section: string; record: string } | null {
444
+ const shape = CONFIG_RECORD_FACT_KINDS[kind];
445
+ return shape ? { section: shape.section, record: shape.record } : null;
446
+ }
447
+
448
+ /**
449
+ * Name a `govern.rules` override that outranks the severity a policy record authored.
450
+ *
451
+ * The backstop for the path provenance cannot reach. Locally, presets are merged here,
452
+ * so a preset's broad rule entry is tagged and treated as a default — a user record's
453
+ * `severity: "error"` wins (report #2 B4). A Cloud-served policy arrives whole: no merge
454
+ * ran, nothing is tagged, and a broad `tokens/hardcoded-values` entry in it still
455
+ * displaces the severity a record next to it authored. That cannot be fixed at this
456
+ * boundary, so it is named instead. Silence is the defect; an unfixable case must at
457
+ * least speak.
458
+ */
459
+ export function overriddenRecordSeverityDiagnostic(input: {
460
+ ruleId: string;
461
+ /** Config path of the override that won — `govern.rules[...]` or `govern.severity`. */
462
+ source: string;
463
+ section: string;
464
+ record: string;
465
+ authored: string;
466
+ enforced: string;
467
+ }): InertConfigDiagnostic {
468
+ const path = `${input.section}[${input.record}]`;
469
+ return {
470
+ code: "FUI9008",
471
+ kind: "overridden-record-severity",
472
+ severity: "warn",
473
+ path,
474
+ message:
475
+ `${path} authored severity=${input.authored}, but ${input.source} enforces ` +
476
+ `severity=${input.enforced} on \`${input.ruleId}\`. The record's severity is not what ` +
477
+ "gates CI — drop the rule override, or author the record at the severity you want enforced.",
478
+ };
479
+ }
480
+
240
481
  function collectStrippedKeys(
241
482
  authored: unknown,
242
483
  parsed: unknown,
@@ -369,6 +610,65 @@ export function detectUnconsumedConfigKeys(
369
610
  return stableConfigDiagnostics(diagnostics);
370
611
  }
371
612
 
613
+ /** Every authored exclude in a policy, paired with the config path that declared it. */
614
+ function authoredPolicyExcludes(
615
+ policy: GovernanceConfig | undefined
616
+ ): Array<{ path: string; scope: string; exclude: PolicyExclude }> {
617
+ const out: Array<{ path: string; scope: string; exclude: PolicyExclude }> = [];
618
+ const collect = (path: string, scope: string, raw: unknown) => {
619
+ for (const exclude of normalizePolicyExcludes(raw) ?? []) {
620
+ out.push({ path, scope, exclude });
621
+ }
622
+ };
623
+
624
+ (policy?.styles ?? []).forEach((record, index) => {
625
+ collect(`govern.styles[${index}].exclude`, record.kind, record.exclude);
626
+ });
627
+ (policy?.jsx ?? []).forEach((record, index) => {
628
+ collect(`govern.jsx[${index}].exclude`, record.kind, record.exclude);
629
+ });
630
+ for (const ruleId of Object.keys(policy?.rules ?? {}).sort()) {
631
+ const record = objectRecord(policy?.rules?.[ruleId]);
632
+ if (!record) continue;
633
+ collect(`govern.rules.${ruleId}.exclude`, ruleId, record.exclude);
634
+ }
635
+
636
+ return out;
637
+ }
638
+
639
+ /**
640
+ * Diagnose excludes that scope nothing: the key is consumed and the glob is valid, but
641
+ * it matched no scanned file, so it silently exempts nothing while reading as active
642
+ * policy. That is the same "declared ≠ armed" failure this module exists to name — a
643
+ * stale path after a refactor is the common cause.
644
+ *
645
+ * `scannedFiles` are repo-relative paths supplied by the caller; core never reads the
646
+ * filesystem. An empty scan (nothing to compare against) yields no diagnostics rather
647
+ * than flagging every exclude.
648
+ */
649
+ export function detectUnmatchedPolicyExcludes(
650
+ policy: GovernanceConfig | undefined,
651
+ scannedFiles: readonly string[]
652
+ ): InertConfigDiagnostic[] {
653
+ if (scannedFiles.length === 0) return [];
654
+ const diagnostics: InertConfigDiagnostic[] = [];
655
+
656
+ for (const { path, scope, exclude } of authoredPolicyExcludes(policy)) {
657
+ if (scannedFiles.some((file) => policyExcludeMatchesPath(exclude, file))) continue;
658
+ diagnostics.push({
659
+ code: "FUI9006",
660
+ kind: "unmatched-exclude",
661
+ severity: "warn",
662
+ path,
663
+ message:
664
+ `${path} pattern "${exclude.glob}" matched no scanned file, so ${scope} is not ` +
665
+ "actually scoped by it. Correct the glob to a path this scan covers, or remove it.",
666
+ });
667
+ }
668
+
669
+ return stableConfigDiagnostics(diagnostics);
670
+ }
671
+
372
672
  interface ScaleBinding {
373
673
  label: string;
374
674
  mechanism: string;