@usefragments/core 1.5.1 → 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 (45) hide show
  1. package/dist/{chunk-AOG4FTV6.js → chunk-WVFNDPM4.js} +448 -190
  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-B88uR3Zq.d.ts → governance-DxFipN5V.d.ts} +654 -22
  8. package/dist/index.d.ts +678 -40
  9. package/dist/index.js +534 -38
  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 +60 -0
  20. package/src/config.ts +20 -0
  21. package/src/facts/builders.ts +35 -0
  22. package/src/facts/compile.ts +135 -21
  23. package/src/facts/fact-index.ts +22 -2
  24. package/src/facts/facts.test.ts +19 -19
  25. package/src/facts/index.ts +9 -6
  26. package/src/facts/types.ts +45 -9
  27. package/src/governance-integrity.test.ts +277 -1
  28. package/src/governance-integrity.ts +616 -0
  29. package/src/governance.test.ts +20 -1
  30. package/src/governance.ts +131 -0
  31. package/src/index.ts +45 -2
  32. package/src/policy-exclude.ts +113 -0
  33. package/src/rules/families.test.ts +69 -0
  34. package/src/rules/families.ts +52 -0
  35. package/src/rules/index.ts +6 -0
  36. package/src/rules/jsx-preferred-import-path.ts +29 -11
  37. package/src/rules/rules.test.ts +125 -1
  38. package/src/rules/styles-no-raw-color.ts +13 -4
  39. package/src/rules/styles-no-raw-dimensions.ts +13 -4
  40. package/src/rules/styles-no-raw-spacing.test.ts +48 -0
  41. package/src/rules/styles-no-raw-spacing.ts +15 -7
  42. package/src/rules/styles-no-raw-typography.ts +13 -4
  43. package/src/rules/utils.ts +39 -0
  44. package/src/types.ts +21 -3
  45. package/dist/chunk-AOG4FTV6.js.map +0 -1
@@ -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
- export interface StyleRawColorForbiddenFact extends BaseFact {
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 {
@@ -511,6 +523,27 @@ export interface TailwindTokenResolvedFact extends BaseFact {
511
523
  * selector chain. Identity is `{ file, selector, declarationPath, property }`
512
524
  * so the ID stays stable when other lines move.
513
525
  */
526
+ /**
527
+ * How a style value reached the fact when the authored text at the use site was
528
+ * not the value itself. Present only for `const-binding` today: the extractor
529
+ * followed exactly one hop to a same-file, immutable, statically-initialized
530
+ * `const` (`const c = '#FFC107'; sx={{ color: c }}`).
531
+ *
532
+ * Two consequences for rules, both load-bearing:
533
+ * - The declaration line is the evidence a reader needs — the use site only
534
+ * shows an identifier.
535
+ * - **No deterministic fix may be attached.** The authored text at the finding's
536
+ * location is the identifier, so any value replacement would be an edit the
537
+ * user never wrote.
538
+ */
539
+ export interface StyleValueProvenance {
540
+ kind: "const-binding";
541
+ /** Identifier the use site referenced. */
542
+ name: string;
543
+ /** Where the binding was declared. */
544
+ location: FactLocation;
545
+ }
546
+
514
547
  export interface StyleDeclarationFact extends BaseFact {
515
548
  kind: "style_declaration";
516
549
  file: string;
@@ -527,6 +560,9 @@ export interface StyleDeclarationFact extends BaseFact {
527
560
  * flagging ad hoc custom properties in product code.
528
561
  */
529
562
  declaredTokenSource?: boolean;
563
+ /** Set when `value` was resolved through a hop rather than authored inline.
564
+ * Additive — the fact's identity key is unchanged. */
565
+ valueFrom?: StyleValueProvenance;
530
566
  }
531
567
 
532
568
  export type UnsupportedStyleReason =
@@ -1,13 +1,26 @@
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,
9
+ detectOrphanGovernanceScales,
10
+ detectUnconsumedConfigKeys,
5
11
  evaluateGovernanceIntegrity,
6
12
  hasEffectiveComponentVocabulary,
7
13
  isEffectiveCanonicalSource,
8
14
  type GovernanceIntegrityInput,
9
15
  } from "./governance-integrity.js";
16
+ import {
17
+ makeStyleFontSizeScaleFact,
18
+ makeStylePropertyScaleFact,
19
+ makeStyleRawColorForbiddenFact,
20
+ makeTokenDefinitionFact,
21
+ } from "./facts/index.js";
10
22
  import { customerDefaultRuleStates } from "./rules/presets.js";
23
+ import { fragmentsConfigSchema } from "./schema.js";
11
24
 
12
25
  function evaluate(
13
26
  overrides: Partial<GovernanceIntegrityInput>
@@ -66,6 +79,245 @@ describe("hasEffectiveComponentVocabulary", () => {
66
79
  });
67
80
  });
68
81
 
82
+ describe("inert config diagnostics", () => {
83
+ it("reports stripped, passthrough, unknown-rule, and rule-field keys in stable order", () => {
84
+ const authored = {
85
+ styles: { spacing: true },
86
+ screenshots: { threshold: 0.1, renderer: "chromium" },
87
+ tokens: { include: ["tokens.css"], mystery: true },
88
+ govern: {
89
+ scales: {
90
+ space: { kind: "scale", unit: "px", values: [0, 4, 8], source: "legacy" },
91
+ },
92
+ rules: {
93
+ "styles/no-raw-color": { enabled: true, exclude: ["vendor/**"], scope: "app" },
94
+ "styles/not-a-real-rule": { enabled: true },
95
+ },
96
+ },
97
+ };
98
+ const parsed = fragmentsConfigSchema.parse(authored);
99
+
100
+ expect(detectUnconsumedConfigKeys(authored, parsed)).toMatchObject([
101
+ { code: "FUI9004", path: "govern.rules.styles/no-raw-color.scope" },
102
+ { code: "FUI9004", path: "govern.rules.styles/not-a-real-rule" },
103
+ { code: "FUI9004", path: "govern.scales.space.source" },
104
+ { code: "FUI9004", path: "screenshots.renderer" },
105
+ { code: "FUI9004", path: "styles" },
106
+ { code: "FUI9004", path: "tokens.mystery" },
107
+ ]);
108
+ });
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
+
121
+ it("reports an authored spacing scale that no effective property policy references", () => {
122
+ const declared: GovernanceConfig = {
123
+ scales: {
124
+ spacing: { kind: "scale", unit: "px", values: [0, 4, 8] },
125
+ },
126
+ };
127
+ const effective: GovernanceConfig = {
128
+ ...declared,
129
+ scales: {
130
+ ...declared.scales,
131
+ space: { kind: "scale", unit: "px", values: [0, 4, 8] },
132
+ },
133
+ styles: [
134
+ {
135
+ kind: "style.rawSpacing.mustMatchScale",
136
+ scale: "space",
137
+ appliesTo: ["margin", "padding"],
138
+ severity: "warn",
139
+ },
140
+ ],
141
+ };
142
+
143
+ expect(detectOrphanGovernanceScales(declared, effective)).toEqual([
144
+ expect.objectContaining({
145
+ code: "FUI9005",
146
+ path: "govern.scales.spacing",
147
+ message: expect.stringContaining('bound to the scale named "space"'),
148
+ }),
149
+ ]);
150
+ });
151
+
152
+ it("does not report a scale referenced by an effective property policy", () => {
153
+ const policy: GovernanceConfig = {
154
+ scales: {
155
+ space: { kind: "scale", unit: "px", values: [0, 4, 8] },
156
+ },
157
+ styles: [
158
+ {
159
+ kind: "style.rawSpacing.mustMatchScale",
160
+ scale: "space",
161
+ appliesTo: ["margin"],
162
+ severity: "warn",
163
+ },
164
+ ],
165
+ };
166
+
167
+ expect(detectOrphanGovernanceScales(policy, policy)).toEqual([]);
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
+ });
319
+ });
320
+
69
321
  describe("evaluateGovernanceIntegrity", () => {
70
322
  it("1. no policy at all → inert, not fatal, not blocking-capable", () => {
71
323
  const verdict = evaluate({ policy: undefined, policySource: "none", declared: false });
@@ -207,4 +459,28 @@ describe("evaluateGovernanceIntegrity", () => {
207
459
  // degraded is enforceable, so not CI-fatal
208
460
  expect(verdict.fatalForCi).toBe(false);
209
461
  });
462
+
463
+ it("includes inert config diagnostics in the doctor roster without changing integrity status", () => {
464
+ const policy: GovernanceConfig = {
465
+ rules: { "styles/no-raw-color": { enabled: true, severity: "warn" } },
466
+ };
467
+ const baseline = evaluate({ policy, policySource: "config", declared: true });
468
+ const withDiagnostic = evaluate({
469
+ policy,
470
+ policySource: "config",
471
+ declared: true,
472
+ configDiagnostics: Array.from({ length: 2 }, () => ({
473
+ code: "FUI9005",
474
+ kind: "orphan-scale",
475
+ severity: "warn",
476
+ path: "govern.scales.spacing",
477
+ message: "orphan",
478
+ })),
479
+ });
480
+
481
+ expect(withDiagnostic.status).toBe(baseline.status);
482
+ expect(withDiagnostic.fatalForCi).toBe(baseline.fatalForCi);
483
+ expect(withDiagnostic.roster).toMatchObject({ active: 1, inert: 1 });
484
+ expect(withDiagnostic.configDiagnostics).toHaveLength(1);
485
+ });
210
486
  });