@usefragments/core 1.2.0 → 1.4.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 (57) hide show
  1. package/dist/{chunk-BQQTMGLA.js → chunk-57QDBEHQ.js} +79 -6
  2. package/dist/chunk-57QDBEHQ.js.map +1 -0
  3. package/dist/{chunk-DBVTSUMT.js → chunk-CBSNXFOD.js} +20 -6
  4. package/dist/chunk-CBSNXFOD.js.map +1 -0
  5. package/dist/codes/index.d.ts +1 -1
  6. package/dist/codes/index.js +2 -2
  7. package/dist/compiled-types/index.d.ts +1 -1
  8. package/dist/generate/index.d.ts +1 -1
  9. package/dist/{governance-DYSirwJu.d.ts → governance-DxdJV6lx.d.ts} +10 -0
  10. package/dist/index-0lmh0Lbo.d.ts +6981 -0
  11. package/dist/index.d.ts +65 -29
  12. package/dist/index.js +397 -127
  13. package/dist/index.js.map +1 -1
  14. package/dist/react-types.d.ts +1 -1
  15. package/dist/registry.d.ts +18 -18
  16. package/dist/schemas/index.d.ts +3 -3377
  17. package/dist/schemas/index.js +1 -1
  18. package/dist/test-utils.d.ts +1 -1
  19. package/package.json +1 -1
  20. package/src/agent-format.test.ts +75 -3
  21. package/src/agent-format.ts +28 -6
  22. package/src/constants.ts +3 -0
  23. package/src/evidence.ts +27 -0
  24. package/src/facts/builders.ts +3 -1
  25. package/src/facts/fact-index.ts +22 -2
  26. package/src/facts/facts.test.ts +1 -1
  27. package/src/facts/types.ts +16 -1
  28. package/src/index.ts +4 -0
  29. package/src/rules/__tests__/fix-emission-invariant.test.ts +174 -0
  30. package/src/rules/__tests__/tokens-require-dual-fallback.test.ts +90 -0
  31. package/src/rules/components-prefer-library.test.ts +148 -0
  32. package/src/rules/components-prefer-library.ts +52 -1
  33. package/src/rules/emit-gate.test.ts +16 -0
  34. package/src/rules/emit-gate.ts +6 -3
  35. package/src/rules/finding.ts +3 -0
  36. package/src/rules/rules.test.ts +2 -1
  37. package/src/rules/spacing-resolution.ts +5 -8
  38. package/src/rules/styles-no-raw-color.test.ts +213 -0
  39. package/src/rules/styles-no-raw-color.ts +157 -23
  40. package/src/rules/styles-no-raw-dimensions.ts +31 -12
  41. package/src/rules/styles-no-raw-spacing.ts +33 -11
  42. package/src/rules/styles-no-raw-typography.ts +37 -15
  43. package/src/rules/taxonomy.test.ts +13 -0
  44. package/src/rules/tokens-require-dual-fallback.ts +54 -17
  45. package/src/rules/utils.ts +51 -0
  46. package/src/schema.ts +6 -2
  47. package/src/schemas/index.ts +81 -3
  48. package/src/token-types.ts +18 -0
  49. package/src/tokens/__tests__/source-names.test.ts +42 -0
  50. package/src/tokens/categories.ts +54 -24
  51. package/src/tokens/design-token-parser.test.ts +24 -5
  52. package/src/tokens/design-token-parser.ts +40 -8
  53. package/src/tokens/scss.test.ts +94 -0
  54. package/src/tokens/scss.ts +11 -4
  55. package/src/types.ts +11 -0
  56. package/dist/chunk-BQQTMGLA.js.map +0 -1
  57. package/dist/chunk-DBVTSUMT.js.map +0 -1
package/dist/index.js CHANGED
@@ -109,7 +109,7 @@ import {
109
109
  ownedImportsEqual,
110
110
  resolveComponentGovernance,
111
111
  scaleGovernanceRecordSchema
112
- } from "./chunk-DBVTSUMT.js";
112
+ } from "./chunk-CBSNXFOD.js";
113
113
  import {
114
114
  AGENT_FORMAT_SCHEMA_VERSION,
115
115
  agentErrorEnvelopeSchema,
@@ -134,7 +134,7 @@ import {
134
134
  suppressionDirectiveSchema,
135
135
  validatorResultSchema,
136
136
  violationSchema
137
- } from "./chunk-BQQTMGLA.js";
137
+ } from "./chunk-57QDBEHQ.js";
138
138
  import {
139
139
  OWNED_PACKAGE_IDENTITY_EPOCH,
140
140
  OWNED_PACKAGE_IDENTITY_SOURCE_SHA256,
@@ -185,6 +185,8 @@ var BRAND = {
185
185
  legacyConfigFile: "segments.config.ts",
186
186
  /** CLI command name (e.g., "fragments") */
187
187
  cliCommand: "fragments",
188
+ /** Full user-facing CLI invocation for copy printed to end users (e.g., "npx @usefragments/cli") */
189
+ cliInvocation: "npx @usefragments/cli",
188
190
  /** Directory for storing fragments, registry, and cache */
189
191
  dataDir: ".fragments",
190
192
  /** Components subdirectory within .fragments/ */
@@ -238,6 +240,18 @@ var DEFAULTS = {
238
240
  port: 6006
239
241
  };
240
242
 
243
+ // src/evidence.ts
244
+ var EVIDENCE_ORDER = [
245
+ "none",
246
+ "runtime_advisory",
247
+ "provenance_bound",
248
+ "source_backed",
249
+ "receipt_backed"
250
+ ];
251
+ function canBlock(grade) {
252
+ return EVIDENCE_ORDER.indexOf(grade) >= EVIDENCE_ORDER.indexOf("source_backed");
253
+ }
254
+
241
255
  // src/conform-prove.ts
242
256
  var PROVE_MAX_PASSES = 8;
243
257
  var PROVE_DEFAULT_MAX_PASSES = 4;
@@ -386,6 +400,7 @@ async function proveCompliant(input, callbacks, options = {}) {
386
400
 
387
401
  // src/agent-format.ts
388
402
  function buildAgentFormat(input) {
403
+ const zeroRulesLoaded = input.integrity.rulesLoaded === 0;
389
404
  const integrityFailed = !input.integrity.healthy || input.integrity.inert;
390
405
  const score = integrityFailed ? 0 : scoreFindings(input.findings);
391
406
  const visibleFindings = uniqueFindings(input.findings).slice(0, input.maxFindings ?? 50);
@@ -398,15 +413,14 @@ function buildAgentFormat(input) {
398
413
  severity: finding.severity,
399
414
  level: severityLevel(finding.severity),
400
415
  message: finding.message,
416
+ evidenceGrade: finding.evidenceGrade,
401
417
  evidence: input.includeEvidence ? finding.evidence : [],
402
418
  plan: planForFinding(finding)
403
419
  }));
404
420
  const hasErrors = input.findings.some((finding) => severityLevel(finding.severity) === "error");
405
- return {
421
+ const shared = {
406
422
  schemaVersion: AGENT_FORMAT_SCHEMA_VERSION,
407
- passed: !integrityFailed && (input.passed ?? true) && !hasErrors,
408
- score,
409
- summary: integrityFailed ? `failed with score ${score}; ${input.integrity.reasons.join("; ") || "governance integrity is unhealthy"}` : agentSummary(input.findings.length, input.filesScanned, score),
423
+ summary: zeroRulesLoaded ? "indeterminate; zero_rules_loaded" : integrityFailed ? `failed with score ${score}; ${input.integrity.reasons.join("; ") || "governance integrity is unhealthy"}` : agentSummary(input.findings.length, input.filesScanned, score),
410
424
  integrity: input.integrity,
411
425
  fix_first: agentFindings.filter((finding) => finding.plan.confidence >= 0.8),
412
426
  remaining: agentFindings.filter((finding) => finding.plan.confidence < 0.8),
@@ -428,7 +442,23 @@ function buildAgentFormat(input) {
428
442
  ...suppression.expiresOn ? { expiresOn: suppression.expiresOn } : {},
429
443
  scope: "line"
430
444
  }))
431
- } : {}
445
+ } : {},
446
+ ...input.provenance ? { provenance: input.provenance } : {},
447
+ ...input.verdict ? { verdict: input.verdict } : {}
448
+ };
449
+ if (zeroRulesLoaded) {
450
+ return {
451
+ ...shared,
452
+ integrity: { ...input.integrity, rulesLoaded: 0 },
453
+ passed: false,
454
+ status: "indeterminate",
455
+ reasons: ["zero_rules_loaded"]
456
+ };
457
+ }
458
+ return {
459
+ ...shared,
460
+ passed: !integrityFailed && (input.passed ?? true) && !hasErrors,
461
+ score
432
462
  };
433
463
  }
434
464
  function uniqueFindings(findings) {
@@ -1651,16 +1681,20 @@ var tokenSourceSchema = z4.object({
1651
1681
  var tokenConfigSchema = z4.object({
1652
1682
  include: repoRelativePathsSchema.optional(),
1653
1683
  sources: z4.array(tokenSourceSchema).min(1).optional(),
1684
+ // Declared design-system packages whose shipped fragments.json token
1685
+ // vocabulary seeds the known-token set (#7c). A packages-only tokens block
1686
+ // is valid — see the superRefine below.
1687
+ packages: z4.array(z4.string().min(1)).optional(),
1654
1688
  exclude: z4.array(repoRelativePathSchema).optional(),
1655
1689
  themeSelectors: z4.record(z4.string()).optional(),
1656
1690
  enabled: z4.boolean().optional(),
1657
1691
  format: z4.enum(["auto", "css", "scss", "dtcg", "tailwind"]).optional(),
1658
1692
  namespace: z4.string().min(1).optional()
1659
1693
  }).passthrough().superRefine((tokens, ctx) => {
1660
- if (!tokens.include?.length && !tokens.sources?.length) {
1694
+ if (!tokens.include?.length && !tokens.sources?.length && !tokens.packages?.length) {
1661
1695
  ctx.addIssue({
1662
1696
  code: z4.ZodIssueCode.custom,
1663
- message: "Add tokens.include or tokens.sources",
1697
+ message: "Add tokens.include, tokens.sources, or tokens.packages",
1664
1698
  path: ["sources"]
1665
1699
  });
1666
1700
  }
@@ -2903,30 +2937,41 @@ function inferTokenGroup(name) {
2903
2937
  return category;
2904
2938
  }
2905
2939
  }
2940
+ var TOKEN_GROUP_COMMENT_MAPPINGS = {
2941
+ "base configuration": "base",
2942
+ typography: "typography",
2943
+ "spacing (micro)": "spacing",
2944
+ spacing: "spacing",
2945
+ "density padding": "spacing",
2946
+ "border radius": "radius",
2947
+ transitions: "transitions",
2948
+ colors: "colors",
2949
+ surfaces: "surfaces",
2950
+ text: "text",
2951
+ borders: "borders",
2952
+ shadows: "shadows",
2953
+ focus: "focus",
2954
+ scrollbar: "scrollbar",
2955
+ "component heights": "component-sizing",
2956
+ "appshell layout": "layout",
2957
+ codeblock: "code",
2958
+ tooltip: "tooltip",
2959
+ "hero/marketing gradient": "marketing"
2960
+ };
2961
+ function looksLikeCleanHeading(text) {
2962
+ if (!/^[a-z0-9][a-z0-9 -]*$/.test(text)) return false;
2963
+ const wordCount = text.split(/\s+/).filter(Boolean).length;
2964
+ return wordCount > 0 && wordCount <= 3;
2965
+ }
2906
2966
  function normalizeTokenGroupComment(comment) {
2907
2967
  const text = comment.trim().replace(/^\/\/\s*/, "").replace(/^\/\*+\s*/, "").replace(/\s*\*+\/$/, "").trim().toLowerCase();
2908
- const mappings = {
2909
- "base configuration": "base",
2910
- typography: "typography",
2911
- "spacing (micro)": "spacing",
2912
- spacing: "spacing",
2913
- "density padding": "spacing",
2914
- "border radius": "radius",
2915
- transitions: "transitions",
2916
- colors: "colors",
2917
- surfaces: "surfaces",
2918
- text: "text",
2919
- borders: "borders",
2920
- shadows: "shadows",
2921
- focus: "focus",
2922
- scrollbar: "scrollbar",
2923
- "component heights": "component-sizing",
2924
- "appshell layout": "layout",
2925
- codeblock: "code",
2926
- tooltip: "tooltip",
2927
- "hero/marketing gradient": "marketing"
2928
- };
2929
- return mappings[text] ?? text.replace(/\s+/g, "-");
2968
+ if (text in TOKEN_GROUP_COMMENT_MAPPINGS) {
2969
+ return TOKEN_GROUP_COMMENT_MAPPINGS[text];
2970
+ }
2971
+ if (!looksLikeCleanHeading(text)) {
2972
+ return null;
2973
+ }
2974
+ return text.replace(/\s+/g, "-");
2930
2975
  }
2931
2976
 
2932
2977
  // src/tokens/scss.ts
@@ -2993,7 +3038,7 @@ function parseScssTokens(content, filePath = "tokens.scss") {
2993
3038
  const tokens = [];
2994
3039
  const seenNames = /* @__PURE__ */ new Set();
2995
3040
  let currentCategory = "other";
2996
- let hasCommentCategories = false;
3041
+ let currentCategoryIsCurated = false;
2997
3042
  const scssVars = parseScssVariables(content);
2998
3043
  const varDeclRegex = /^\s*(--[\w-]+)\s*:\s*(.+?)\s*;/;
2999
3044
  const sectionCommentRegex = /^\s*\/\/\s+([A-Z].+)$/;
@@ -3003,7 +3048,9 @@ function parseScssTokens(content, filePath = "tokens.scss") {
3003
3048
  const normalized = normalizeTokenGroupComment(commentMatch[0]);
3004
3049
  if (normalized) {
3005
3050
  currentCategory = normalized;
3006
- hasCommentCategories = true;
3051
+ currentCategoryIsCurated = true;
3052
+ } else {
3053
+ currentCategoryIsCurated = false;
3007
3054
  }
3008
3055
  continue;
3009
3056
  }
@@ -3019,7 +3066,7 @@ function parseScssTokens(content, filePath = "tokens.scss") {
3019
3066
  tokens.push({
3020
3067
  name,
3021
3068
  value: cleanValue || void 0,
3022
- category: hasCommentCategories ? currentCategory : inferTokenGroup(name),
3069
+ category: currentCategoryIsCurated ? currentCategory : inferTokenGroup(name),
3023
3070
  description
3024
3071
  });
3025
3072
  }
@@ -3030,7 +3077,7 @@ function parseScssTokens(content, filePath = "tokens.scss") {
3030
3077
  tokens.push({
3031
3078
  name,
3032
3079
  value,
3033
- category: hasCommentCategories ? currentCategory : inferTokenGroup(name)
3080
+ category: currentCategoryIsCurated ? currentCategory : inferTokenGroup(name)
3034
3081
  });
3035
3082
  }
3036
3083
  const cssVarValues = /* @__PURE__ */ new Map();
@@ -3326,19 +3373,35 @@ function parsedOutputToDesignTokens(output, filePath) {
3326
3373
  return tokens;
3327
3374
  }
3328
3375
  function scssOutputToDesignTokens(output, filePath) {
3329
- const names = /* @__PURE__ */ new Set();
3330
- const tokens = [];
3376
+ const tokensByName = /* @__PURE__ */ new Map();
3331
3377
  for (const cat of Object.values(output.categories)) {
3332
3378
  for (const t of cat) {
3333
- const cssName = t.name.startsWith("$") ? `--${t.name.slice(1)}` : t.name;
3334
- if (!cssName.startsWith("--") || names.has(cssName)) continue;
3335
- names.add(cssName);
3336
- tokens.push(designTokenFromParsed(cssName, t, filePath));
3379
+ const isScssVariable = t.name.startsWith("$");
3380
+ const cssName = isScssVariable ? `--${t.name.slice(1)}` : t.name;
3381
+ if (!cssName.startsWith("--")) continue;
3382
+ const existing = tokensByName.get(cssName);
3383
+ if (!existing) {
3384
+ tokensByName.set(cssName, {
3385
+ parsed: t,
3386
+ referenceFormat: isScssVariable ? "scss-var" : "css-var",
3387
+ sourceNames: /* @__PURE__ */ new Set([t.name])
3388
+ });
3389
+ } else {
3390
+ existing.sourceNames.add(t.name);
3391
+ if (isScssVariable) {
3392
+ existing.parsed = t;
3393
+ existing.referenceFormat = "scss-var";
3394
+ }
3395
+ }
3337
3396
  }
3338
3397
  }
3339
- return tokens;
3398
+ return [...tokensByName].map(
3399
+ ([name, token]) => designTokenFromParsed(name, token.parsed, filePath, token.referenceFormat, [
3400
+ ...token.sourceNames
3401
+ ])
3402
+ );
3340
3403
  }
3341
- function designTokenFromParsed(name, parsed, filePath) {
3404
+ function designTokenFromParsed(name, parsed, filePath, referenceFormat, sourceNames) {
3342
3405
  return {
3343
3406
  name,
3344
3407
  rawValue: parsed.value ?? "",
@@ -3351,6 +3414,8 @@ function designTokenFromParsed(name, parsed, filePath) {
3351
3414
  level: 1,
3352
3415
  referenceChain: [],
3353
3416
  sourceFile: filePath,
3417
+ referenceFormat,
3418
+ ...sourceNames?.length ? { sourceNames } : {},
3354
3419
  theme: "default",
3355
3420
  selector: ":root",
3356
3421
  description: parsed.description
@@ -4289,12 +4354,37 @@ function makeFinding(input) {
4289
4354
  fingerprint,
4290
4355
  location: input.location,
4291
4356
  evidence: input.evidence,
4357
+ evidenceGrade: input.evidenceGrade ?? "source_backed",
4292
4358
  fix: input.fix,
4293
4359
  attributes: input.attributes
4294
4360
  });
4295
4361
  }
4296
4362
 
4297
4363
  // src/rules/utils.ts
4364
+ function emitFix(fix, proof, ix) {
4365
+ const reasons = [];
4366
+ if (proof.editKind !== fix.kind) reasons.push("edit kind does not match proof");
4367
+ if (!proof.valuePreserving) reasons.push("value-preservation proof missing");
4368
+ if (proof.symbols.length === 0) reasons.push("token-symbol proof missing");
4369
+ const missingSymbols = proof.symbols.filter((symbol) => !ix.tokens.symbolExists(symbol));
4370
+ if (missingSymbols.length > 0) {
4371
+ reasons.push(`absent token symbol: ${missingSymbols.join(", ")}`);
4372
+ }
4373
+ const inapplicableSymbols = proof.symbols.filter((symbol) => {
4374
+ const definition = ix.tokens.definitionForSymbol(symbol);
4375
+ return definition !== void 0 && !isApplicableTokenReference(definition.referenceFormat);
4376
+ });
4377
+ if (inapplicableSymbols.length > 0) {
4378
+ reasons.push(`token reference is not directly applicable: ${inapplicableSymbols.join(", ")}`);
4379
+ }
4380
+ return {
4381
+ fix: { ...fix, deterministic: reasons.length === 0 },
4382
+ ...reasons.length > 0 ? { downgradeReason: reasons.join("; ") } : {}
4383
+ };
4384
+ }
4385
+ function tokenSymbolsInText(value) {
4386
+ return [...new Set(value.match(/\$[A-Za-z0-9_-]+|--[A-Za-z0-9_-]+/g) ?? [])];
4387
+ }
4298
4388
  function indexPropsByNodeId(ix) {
4299
4389
  const out = /* @__PURE__ */ new Map();
4300
4390
  for (const fact of ix.byKind("usage_prop_resolved")) {
@@ -4373,13 +4463,6 @@ function readUsageNode(ix, id) {
4373
4463
  const fact = ix.get(id);
4374
4464
  return fact && fact.kind === "usage_node" ? fact : void 0;
4375
4465
  }
4376
- function cssVariableNames(tokens) {
4377
- const names = /* @__PURE__ */ new Set();
4378
- for (const token of tokens) {
4379
- names.add(token.name.startsWith("--") ? token.name : `--${token.name}`);
4380
- }
4381
- return names;
4382
- }
4383
4466
  function contractPrefixFamilies(names) {
4384
4467
  const families = /* @__PURE__ */ new Set();
4385
4468
  for (const name of names) {
@@ -4993,11 +5076,13 @@ function ruleComponentsPreferLibrary(ix) {
4993
5076
  const textByNode = indexTextChildrenByNodeId(ix);
4994
5077
  const classTokensByNode = indexClassTokensByNode(ix);
4995
5078
  const reimplTargets = canonicalReimplTargets(sources, mappings);
5079
+ const canonicalDirectoryResolvedNodes = indexCanonicalDirectoryResolvedNodes(ix, sources);
4996
5080
  const findings = [];
4997
5081
  const seenImportFixes = /* @__PURE__ */ new Set();
4998
5082
  for (const node of ix.byKind("usage_node")) {
4999
5083
  if (isCanonicalSourceImplementationFile(node.file, sources)) continue;
5000
5084
  if (node.element.includes(".")) continue;
5085
+ if (canonicalDirectoryResolvedNodes.has(node.id)) continue;
5001
5086
  const props = propsByNode.get(node.id) ?? [];
5002
5087
  const textChildren = textByNode.get(node.id) ?? [];
5003
5088
  const importsForFile = importsByFileAndLocal.get(node.file);
@@ -5523,7 +5608,27 @@ function sourceLabel(source) {
5523
5608
  function canonicalImportPath(source) {
5524
5609
  if (source.kind === "npm") return source.specifier;
5525
5610
  if (source.kind === "registry") return source.importPath;
5526
- return void 0;
5611
+ return source.include?.length ? source.path : void 0;
5612
+ }
5613
+ function indexCanonicalDirectoryResolvedNodes(ix, sources) {
5614
+ const out = /* @__PURE__ */ new Set();
5615
+ const directoryPaths = sources.filter(
5616
+ (source) => source.kind === "directory"
5617
+ ).map((source) => normalizePath(source.path));
5618
+ if (directoryPaths.length === 0) return out;
5619
+ for (const fact of ix.byKind("usage_component")) {
5620
+ const module = componentIdModule(fact.componentId);
5621
+ if (module === void 0) continue;
5622
+ const normalizedModule = normalizePath(module);
5623
+ if (directoryPaths.some((path) => isPathInSource(normalizedModule, path))) {
5624
+ out.add(fact.nodeId);
5625
+ }
5626
+ }
5627
+ return out;
5628
+ }
5629
+ function componentIdModule(componentId2) {
5630
+ const hash = componentId2.indexOf("#");
5631
+ return hash > 0 ? componentId2.slice(0, hash) : void 0;
5527
5632
  }
5528
5633
  function isCanonicalSourceImplementationFile(file, sources) {
5529
5634
  const normalizedFile = normalizePath(file);
@@ -6164,12 +6269,14 @@ function ruleStylesNoRawColor(ix) {
6164
6269
  const colorTokens = ix.tokens.byCategory("color");
6165
6270
  const preferLabel = policy.prefer === "token" ? "design token" : "CSS variable";
6166
6271
  const findings = [];
6272
+ const advisorySeverity = policy.severity === "error" ? "warn" : policy.severity;
6167
6273
  for (const decl of ix.byKind("style_declaration")) {
6168
6274
  if (decl.property.startsWith("--") && decl.declaredTokenSource === true) continue;
6169
6275
  const color = detectRawColor(decl.value);
6170
6276
  if (!color) continue;
6171
6277
  if (isExemptColor(color, policy.except)) continue;
6172
6278
  const resolution = resolveColorToken(decl.property, color, colorTokens);
6279
+ const fixEmission = resolution?.kind === "exact" ? buildTokenFix(decl.property, decl.value, color, resolution, ix) : void 0;
6173
6280
  const evidenceIds = resolution ? [decl.id, policy.id, resolution.token.id] : [decl.id, policy.id];
6174
6281
  findings.push(
6175
6282
  makeFinding({
@@ -6187,20 +6294,52 @@ function ruleStylesNoRawColor(ix) {
6187
6294
  property: decl.property,
6188
6295
  value: color
6189
6296
  },
6190
- fix: resolution?.kind === "exact" ? buildTokenFix(decl.property, decl.value, color, resolution) : void 0,
6297
+ fix: fixEmission?.fix,
6191
6298
  attributes: {
6192
6299
  property: decl.property,
6193
6300
  rawValue: decl.value,
6194
6301
  color,
6195
6302
  source: "css",
6196
6303
  suggestedToken: resolution?.token.name,
6197
- ...resolution ? { tokenMatch: resolution.kind } : {}
6304
+ ...resolution ? { tokenMatch: resolution.kind } : {},
6305
+ ...fixEmission?.downgradeReason ? { downgradeReason: fixEmission.downgradeReason } : {}
6198
6306
  }
6199
6307
  })
6200
6308
  );
6201
6309
  }
6202
6310
  const componentByNode = indexComponentByNodeId(ix);
6203
6311
  for (const inline of ix.byKind("usage_inline_style")) {
6312
+ if (inline.valueKind === "dynamic-raw") {
6313
+ const node2 = readUsageNode(ix, inline.nodeId);
6314
+ if (!node2) continue;
6315
+ const componentEvidenceId2 = componentByNode.get(node2.id)?.id;
6316
+ const evidenceIds2 = componentEvidenceId2 ? [node2.id, componentEvidenceId2, inline.id, policy.id] : [node2.id, inline.id, policy.id];
6317
+ findings.push(
6318
+ makeFinding({
6319
+ ruleId: RULE_ID8,
6320
+ ruleVersion: RULE_VERSION9,
6321
+ severity: advisorySeverity,
6322
+ message: `Runtime-constructed raw color \`${inline.value}\` on inline \`${inline.property}\`. Use a ${preferLabel} instead.`,
6323
+ location: node2.location,
6324
+ evidence: ix.evidence(evidenceIds2),
6325
+ fingerprintIdentity: {
6326
+ source: "usage_inline_style_dynamic",
6327
+ file: node2.file,
6328
+ nodePath: node2.nodePath,
6329
+ element: node2.element,
6330
+ property: inline.property,
6331
+ value: inline.value
6332
+ },
6333
+ attributes: {
6334
+ property: inline.property,
6335
+ rawValue: inline.value,
6336
+ source: "jsx",
6337
+ dynamic: true
6338
+ }
6339
+ })
6340
+ );
6341
+ continue;
6342
+ }
6204
6343
  if (inline.valueKind === "css-variable") continue;
6205
6344
  const color = detectRawColor(inline.value);
6206
6345
  if (!color) continue;
@@ -6209,6 +6348,7 @@ function ruleStylesNoRawColor(ix) {
6209
6348
  if (!node) continue;
6210
6349
  const componentEvidenceId = componentByNode.get(node.id)?.id;
6211
6350
  const resolution = resolveColorToken(inline.property, color, colorTokens);
6351
+ const fixEmission = resolution?.kind === "exact" ? buildTokenFix(inline.property, inline.value, color, resolution, ix) : void 0;
6212
6352
  const baseEvidence = componentEvidenceId ? [node.id, componentEvidenceId, inline.id, policy.id] : [node.id, inline.id, policy.id];
6213
6353
  const evidenceIds = resolution ? [...baseEvidence, resolution.token.id] : baseEvidence;
6214
6354
  findings.push(
@@ -6227,20 +6367,81 @@ function ruleStylesNoRawColor(ix) {
6227
6367
  property: inline.property,
6228
6368
  value: color
6229
6369
  },
6230
- fix: resolution?.kind === "exact" ? buildTokenFix(inline.property, inline.value, color, resolution) : void 0,
6370
+ fix: fixEmission?.fix,
6231
6371
  attributes: {
6232
6372
  property: inline.property,
6233
6373
  rawValue: inline.value,
6234
6374
  color,
6235
6375
  source: "jsx",
6236
6376
  suggestedToken: resolution?.token.name,
6237
- ...resolution ? { tokenMatch: resolution.kind } : {}
6377
+ ...resolution ? { tokenMatch: resolution.kind } : {},
6378
+ ...fixEmission?.downgradeReason ? { downgradeReason: fixEmission.downgradeReason } : {}
6379
+ }
6380
+ })
6381
+ );
6382
+ }
6383
+ for (const prop of ix.byKind("usage_prop_resolved")) {
6384
+ if (prop.resolution !== "static") continue;
6385
+ if (typeof prop.value !== "string") continue;
6386
+ if (!SVG_PAINT_ATTRS.has(prop.prop)) continue;
6387
+ const color = detectRawColor(prop.value);
6388
+ if (!color) continue;
6389
+ if (isExemptColor(color, policy.except)) continue;
6390
+ const node = readUsageNode(ix, prop.nodeId);
6391
+ if (!node) continue;
6392
+ const componentEvidenceId = componentByNode.get(node.id)?.id;
6393
+ const cssProperty = SVG_PAINT_CSS_PROPERTY[prop.prop] ?? prop.prop;
6394
+ const resolution = resolveColorToken(cssProperty, color, colorTokens);
6395
+ const fixEmission = resolution?.kind === "exact" ? buildTokenFix(cssProperty, prop.value, color, resolution, ix) : void 0;
6396
+ const baseEvidence = componentEvidenceId ? [node.id, componentEvidenceId, prop.id, policy.id] : [node.id, prop.id, policy.id];
6397
+ const evidenceIds = resolution ? [...baseEvidence, resolution.token.id] : baseEvidence;
6398
+ findings.push(
6399
+ makeFinding({
6400
+ ruleId: RULE_ID8,
6401
+ ruleVersion: RULE_VERSION9,
6402
+ severity: advisorySeverity,
6403
+ message: colorMessage(color, `\`${prop.prop}\``, resolution, preferLabel),
6404
+ location: prop.location ?? node.location,
6405
+ evidence: ix.evidence(evidenceIds),
6406
+ fingerprintIdentity: {
6407
+ source: "usage_prop_resolved",
6408
+ file: node.file,
6409
+ nodePath: node.nodePath,
6410
+ element: node.element,
6411
+ property: prop.prop,
6412
+ value: color
6413
+ },
6414
+ fix: fixEmission?.fix,
6415
+ attributes: {
6416
+ property: prop.prop,
6417
+ rawValue: prop.value,
6418
+ color,
6419
+ source: "jsx",
6420
+ suggestedToken: resolution?.token.name,
6421
+ ...resolution ? { tokenMatch: resolution.kind } : {},
6422
+ ...fixEmission?.downgradeReason ? { downgradeReason: fixEmission.downgradeReason } : {}
6238
6423
  }
6239
6424
  })
6240
6425
  );
6241
6426
  }
6242
6427
  return findings;
6243
6428
  }
6429
+ var SVG_PAINT_ATTRS = /* @__PURE__ */ new Set([
6430
+ "fill",
6431
+ "stroke",
6432
+ "color",
6433
+ "stop-color",
6434
+ "stopColor",
6435
+ "flood-color",
6436
+ "floodColor",
6437
+ "lighting-color",
6438
+ "lightingColor"
6439
+ ]);
6440
+ var SVG_PAINT_CSS_PROPERTY = {
6441
+ stopColor: "stop-color",
6442
+ floodColor: "flood-color",
6443
+ lightingColor: "lighting-color"
6444
+ };
6244
6445
  function resolveColorToken(property, raw, tokens) {
6245
6446
  const exact = findTokenMatch(property, raw, tokens);
6246
6447
  if (exact) return { kind: "exact", token: exact.token, ambiguous: exact.ambiguous };
@@ -6318,21 +6519,24 @@ function normalizeColor2(value) {
6318
6519
  }
6319
6520
  return lower;
6320
6521
  }
6321
- function buildTokenFix(property, rawValue, rawColor, match) {
6522
+ function buildTokenFix(property, rawValue, rawColor, match, ix) {
6322
6523
  const reference = tokenReference(match.token.name);
6323
6524
  const value = replaceRawColor(rawValue, rawColor, reference);
6324
- return {
6325
- kind: "replaceStyleValue",
6326
- title: `Replace ${property} with ${value}`,
6327
- property,
6328
- value,
6329
- // Apply automatically only when the swap is unambiguous AND the reference
6330
- // resolves verbatim. An ambiguous match (several tokens share the value,
6331
- // none role-matching) or a token that can't be referenced from CSS (a Sass
6332
- // *map* member, or a DTCG JSON token with no emitted custom property) is a
6333
- // suggestion — applying it would cross roles or write non-compiling code.
6334
- deterministic: !match.ambiguous && isApplicableTokenReference(match.token.referenceFormat)
6335
- };
6525
+ return emitFix(
6526
+ {
6527
+ kind: "replaceStyleValue",
6528
+ title: `Replace ${property} with ${value}`,
6529
+ property,
6530
+ value,
6531
+ deterministic: true
6532
+ },
6533
+ {
6534
+ symbols: [match.token.name],
6535
+ editKind: "replaceStyleValue",
6536
+ valuePreserving: !match.ambiguous
6537
+ },
6538
+ ix
6539
+ );
6336
6540
  }
6337
6541
  function replaceRawColor(rawValue, rawColor, replacement) {
6338
6542
  if (rawValue === rawColor) return replacement;
@@ -6363,6 +6567,7 @@ function ruleStylesNoRawDimensions(ix) {
6363
6567
  if (!resolution) continue;
6364
6568
  const tokenIds = resolutionTokenIds(resolution);
6365
6569
  const evidenceIds = tokenIds.length ? [decl.id, policy.id, ...tokenIds] : [decl.id, policy.id];
6570
+ const fixEmission = dimensionFix(decl.property, resolution, ix);
6366
6571
  findings.push(
6367
6572
  makeFinding({
6368
6573
  ruleId: RULE_ID9,
@@ -6379,12 +6584,13 @@ function ruleStylesNoRawDimensions(ix) {
6379
6584
  property: decl.property,
6380
6585
  value: decl.value
6381
6586
  },
6382
- fix: dimensionFix(decl.property, resolution),
6587
+ fix: fixEmission?.fix,
6383
6588
  attributes: {
6384
6589
  property: decl.property,
6385
6590
  rawValue: decl.value,
6386
6591
  source: "css",
6387
- suggestedToken: resolutionSuggestedToken(resolution)
6592
+ suggestedToken: resolutionSuggestedToken(resolution),
6593
+ ...fixEmission?.downgradeReason ? { downgradeReason: fixEmission.downgradeReason } : {}
6388
6594
  }
6389
6595
  })
6390
6596
  );
@@ -6403,6 +6609,7 @@ function ruleStylesNoRawDimensions(ix) {
6403
6609
  const baseEvidence = componentEvidenceId ? [node.id, componentEvidenceId, inline.id, policy.id] : [node.id, inline.id, policy.id];
6404
6610
  const tokenIds = resolutionTokenIds(resolution);
6405
6611
  const evidenceIds = tokenIds.length ? [...baseEvidence, ...tokenIds] : baseEvidence;
6612
+ const fixEmission = dimensionFix(inline.property, resolution, ix);
6406
6613
  findings.push(
6407
6614
  makeFinding({
6408
6615
  ruleId: RULE_ID9,
@@ -6419,12 +6626,13 @@ function ruleStylesNoRawDimensions(ix) {
6419
6626
  property: inline.property,
6420
6627
  value: inline.value
6421
6628
  },
6422
- fix: dimensionFix(inline.property, resolution),
6629
+ fix: fixEmission?.fix,
6423
6630
  attributes: {
6424
6631
  property: inline.property,
6425
6632
  rawValue: inline.value,
6426
6633
  source: "jsx",
6427
- suggestedToken: resolutionSuggestedToken(resolution)
6634
+ suggestedToken: resolutionSuggestedToken(resolution),
6635
+ ...fixEmission?.downgradeReason ? { downgradeReason: fixEmission.downgradeReason } : {}
6428
6636
  }
6429
6637
  })
6430
6638
  );
@@ -6469,15 +6677,23 @@ function dimensionMessage(rawValue, property, preferLabel, resolution, inline) {
6469
6677
  }
6470
6678
  return `Raw dimension ${rawValue} on ${where}. Use a ${preferLabel} instead.`;
6471
6679
  }
6472
- function dimensionFix(property, resolution) {
6680
+ function dimensionFix(property, resolution, ix) {
6473
6681
  if (resolution.kind === "exact") {
6474
- return {
6475
- kind: "replaceStyleValue",
6476
- title: `Replace ${property} with ${resolution.fixValue}`,
6477
- property,
6478
- value: resolution.fixValue,
6479
- deterministic: true
6480
- };
6682
+ return emitFix(
6683
+ {
6684
+ kind: "replaceStyleValue",
6685
+ title: `Replace ${property} with ${resolution.fixValue}`,
6686
+ property,
6687
+ value: resolution.fixValue,
6688
+ deterministic: true
6689
+ },
6690
+ {
6691
+ symbols: resolution.tokens.map((token) => token.name),
6692
+ editKind: "replaceStyleValue",
6693
+ valuePreserving: true
6694
+ },
6695
+ ix
6696
+ );
6481
6697
  }
6482
6698
  return void 0;
6483
6699
  }
@@ -6578,7 +6794,7 @@ function resolveSpacingValue(input) {
6578
6794
  let anyOffScale = false;
6579
6795
  let canFixAll = true;
6580
6796
  let reliableSuggestion = true;
6581
- let allApplicableValuePreserving = true;
6797
+ let allValuePreserving = true;
6582
6798
  for (const part of parts) {
6583
6799
  const normalized = normalizeLengthForScale(part.parsed, input.scale);
6584
6800
  if (normalized === null) return null;
@@ -6592,8 +6808,7 @@ function resolveSpacingValue(input) {
6592
6808
  }
6593
6809
  anyViolation = true;
6594
6810
  const reference = tokenReference(exactToken.name);
6595
- const applicable = isApplicableTokenReference(exactToken.referenceFormat) && normalized.deterministicFix;
6596
- if (!applicable) allApplicableValuePreserving = false;
6811
+ if (!normalized.deterministicFix) allValuePreserving = false;
6597
6812
  suggestedParts.push(reference);
6598
6813
  if (!firstViolation) {
6599
6814
  firstViolation = {
@@ -6601,7 +6816,7 @@ function resolveSpacingValue(input) {
6601
6816
  suggestedValue: reference,
6602
6817
  suggestedToken: exactToken.name,
6603
6818
  fixEmittable: true,
6604
- deterministicFix: applicable,
6819
+ deterministicFix: normalized.deterministicFix,
6605
6820
  reason: "token-equivalent",
6606
6821
  matchedToken: exactToken.name,
6607
6822
  assumedRootFontSizePx: normalized.assumedRootFontSizePx,
@@ -6612,7 +6827,7 @@ function resolveSpacingValue(input) {
6612
6827
  }
6613
6828
  anyViolation = true;
6614
6829
  anyOffScale = true;
6615
- allApplicableValuePreserving = false;
6830
+ allValuePreserving = false;
6616
6831
  const nearest = nearestSignedScaleValue(normalized.value, input.allowed);
6617
6832
  const snapsToZero = nearest === 0 && normalized.value !== 0;
6618
6833
  let suggestedPart;
@@ -6645,7 +6860,7 @@ function resolveSpacingValue(input) {
6645
6860
  const joined = suggestedParts.join(" ");
6646
6861
  const changes = joined !== input.raw.trim().replace(/\s+/g, " ");
6647
6862
  const fixEmittable = canFixAll && reliableSuggestion && changes;
6648
- const deterministic = fixEmittable && !anyOffScale && allApplicableValuePreserving;
6863
+ const deterministic = fixEmittable && !anyOffScale && allValuePreserving;
6649
6864
  return {
6650
6865
  ...firstViolation,
6651
6866
  reason: anyOffScale ? "off-scale" : "token-equivalent",
@@ -6709,6 +6924,7 @@ function checkDeclaration(ix, decl, lookupFor) {
6709
6924
  tokens: lookupFor(scale)
6710
6925
  });
6711
6926
  if (!checked) return null;
6927
+ const fixEmission = buildSpacingFix(decl.property, checked, ix);
6712
6928
  return makeFinding({
6713
6929
  ruleId: RULE_ID10,
6714
6930
  ruleVersion: RULE_VERSION11,
@@ -6724,7 +6940,7 @@ function checkDeclaration(ix, decl, lookupFor) {
6724
6940
  property: decl.property,
6725
6941
  value: decl.value
6726
6942
  },
6727
- fix: buildSpacingFix(decl.property, checked),
6943
+ fix: fixEmission?.fix,
6728
6944
  attributes: {
6729
6945
  property: decl.property,
6730
6946
  rawValue: decl.value,
@@ -6738,7 +6954,8 @@ function checkDeclaration(ix, decl, lookupFor) {
6738
6954
  normalizedUnit: scale.unit,
6739
6955
  assumedRootFontSizePx: checked.assumedRootFontSizePx,
6740
6956
  assumedEmBasePx: checked.assumedEmBasePx,
6741
- source: "css"
6957
+ source: "css",
6958
+ ...fixEmission?.downgradeReason ? { downgradeReason: fixEmission.downgradeReason } : {}
6742
6959
  }
6743
6960
  });
6744
6961
  }
@@ -6758,6 +6975,7 @@ function checkInlineStyle(ix, inline, componentByNode, lookupFor) {
6758
6975
  bareNumberFix: inline.valueKind === "number"
6759
6976
  });
6760
6977
  if (!checked) return null;
6978
+ const fixEmission = buildSpacingFix(inline.property, checked, ix);
6761
6979
  const node = readUsageNode(ix, inline.nodeId);
6762
6980
  if (!node) return null;
6763
6981
  const componentEvidenceId = componentByNode.get(node.id)?.id;
@@ -6777,7 +6995,7 @@ function checkInlineStyle(ix, inline, componentByNode, lookupFor) {
6777
6995
  property: inline.property,
6778
6996
  value: inline.value
6779
6997
  },
6780
- fix: buildSpacingFix(inline.property, checked),
6998
+ fix: fixEmission?.fix,
6781
6999
  attributes: {
6782
7000
  property: inline.property,
6783
7001
  rawValue: inline.value,
@@ -6791,19 +7009,28 @@ function checkInlineStyle(ix, inline, componentByNode, lookupFor) {
6791
7009
  normalizedUnit: scale.unit,
6792
7010
  assumedRootFontSizePx: checked.assumedRootFontSizePx,
6793
7011
  assumedEmBasePx: checked.assumedEmBasePx,
6794
- source: "jsx"
7012
+ source: "jsx",
7013
+ ...fixEmission?.downgradeReason ? { downgradeReason: fixEmission.downgradeReason } : {}
6795
7014
  }
6796
7015
  });
6797
7016
  }
6798
- function buildSpacingFix(property, checked) {
7017
+ function buildSpacingFix(property, checked, ix) {
6799
7018
  if (!checked.fixEmittable || checked.suggestedValue === void 0) return void 0;
6800
- return {
6801
- kind: "replaceStyleValue",
6802
- title: `Replace ${property} with ${checked.suggestedValue}`,
6803
- property,
6804
- value: checked.suggestedValue,
6805
- deterministic: checked.deterministicFix
6806
- };
7019
+ return emitFix(
7020
+ {
7021
+ kind: "replaceStyleValue",
7022
+ title: `Replace ${property} with ${checked.suggestedValue}`,
7023
+ property,
7024
+ value: checked.suggestedValue,
7025
+ deterministic: true
7026
+ },
7027
+ {
7028
+ symbols: tokenSymbolsInText(checked.suggestedValue),
7029
+ editKind: "replaceStyleValue",
7030
+ valuePreserving: checked.deterministicFix
7031
+ },
7032
+ ix
7033
+ );
6807
7034
  }
6808
7035
  function spacingMessage(property, value, allowed, unit, checked) {
6809
7036
  if (checked.reason === "token-equivalent" && checked.matchedToken) {
@@ -6832,6 +7059,7 @@ function ruleStylesNoRawTypography(ix) {
6832
7059
  if (decl.property.toLowerCase() !== "font-size") continue;
6833
7060
  const checked = checkFontSize(decl.value, allowed, scale, tokens);
6834
7061
  if (!checked) continue;
7062
+ const fixEmission = buildFix(decl.property, checked, ix);
6835
7063
  findings.push(
6836
7064
  makeFinding({
6837
7065
  ruleId: RULE_ID11,
@@ -6848,7 +7076,7 @@ function ruleStylesNoRawTypography(ix) {
6848
7076
  property: decl.property,
6849
7077
  value: decl.value
6850
7078
  },
6851
- fix: buildFix(decl.property, checked),
7079
+ fix: fixEmission?.fix,
6852
7080
  attributes: {
6853
7081
  property: decl.property,
6854
7082
  rawValue: decl.value,
@@ -6856,7 +7084,8 @@ function ruleStylesNoRawTypography(ix) {
6856
7084
  allowed,
6857
7085
  suggestedValue: checked.suggestedValue,
6858
7086
  matchedToken: checked.matchedToken,
6859
- source: "css"
7087
+ source: "css",
7088
+ ...fixEmission?.downgradeReason ? { downgradeReason: fixEmission.downgradeReason } : {}
6860
7089
  }
6861
7090
  })
6862
7091
  );
@@ -6865,8 +7094,15 @@ function ruleStylesNoRawTypography(ix) {
6865
7094
  for (const inline of ix.byKind("usage_inline_style")) {
6866
7095
  if (inline.valueKind === "css-variable") continue;
6867
7096
  if (!FONT_SIZE_PROPERTIES.has(inline.property.toLowerCase())) continue;
6868
- const checked = checkFontSize(inline.value, allowed, scale, tokens, inline.valueKind === "number");
7097
+ const checked = checkFontSize(
7098
+ inline.value,
7099
+ allowed,
7100
+ scale,
7101
+ tokens,
7102
+ inline.valueKind === "number"
7103
+ );
6869
7104
  if (!checked) continue;
7105
+ const fixEmission = buildFix(inline.property, checked, ix);
6870
7106
  const node = readUsageNode(ix, inline.nodeId);
6871
7107
  if (!node) continue;
6872
7108
  const componentEvidenceId = componentByNode.get(node.id)?.id;
@@ -6887,7 +7123,7 @@ function ruleStylesNoRawTypography(ix) {
6887
7123
  property: inline.property,
6888
7124
  value: inline.value
6889
7125
  },
6890
- fix: buildFix(inline.property, checked),
7126
+ fix: fixEmission?.fix,
6891
7127
  attributes: {
6892
7128
  property: inline.property,
6893
7129
  rawValue: inline.value,
@@ -6895,7 +7131,8 @@ function ruleStylesNoRawTypography(ix) {
6895
7131
  allowed,
6896
7132
  suggestedValue: checked.suggestedValue,
6897
7133
  matchedToken: checked.matchedToken,
6898
- source: "jsx"
7134
+ source: "jsx",
7135
+ ...fixEmission?.downgradeReason ? { downgradeReason: fixEmission.downgradeReason } : {}
6899
7136
  }
6900
7137
  })
6901
7138
  );
@@ -6925,11 +7162,10 @@ function checkFontSize(raw, allowed, scale, tokens, bareNumberFix = false) {
6925
7162
  const exactToken = tokens.get(magnitude);
6926
7163
  if (matchesScale(normalized.value, allowed)) {
6927
7164
  if (!exactToken) return null;
6928
- const applicable = isApplicableTokenReference(exactToken.referenceFormat) && normalized.deterministicFix;
6929
7165
  return {
6930
7166
  suggestedValue: tokenReference(exactToken.name),
6931
7167
  fixEmittable: true,
6932
- deterministicFix: applicable,
7168
+ deterministicFix: normalized.deterministicFix,
6933
7169
  reason: "token-equivalent",
6934
7170
  matchedToken: exactToken.name
6935
7171
  };
@@ -6947,15 +7183,23 @@ function checkFontSize(raw, allowed, scale, tokens, bareNumberFix = false) {
6947
7183
  reason: "off-scale"
6948
7184
  };
6949
7185
  }
6950
- function buildFix(property, checked) {
7186
+ function buildFix(property, checked, ix) {
6951
7187
  if (!checked.fixEmittable || checked.suggestedValue === void 0) return void 0;
6952
- return {
6953
- kind: "replaceStyleValue",
6954
- title: `Replace ${property} with ${checked.suggestedValue}`,
6955
- property,
6956
- value: checked.suggestedValue,
6957
- deterministic: checked.deterministicFix
6958
- };
7188
+ return emitFix(
7189
+ {
7190
+ kind: "replaceStyleValue",
7191
+ title: `Replace ${property} with ${checked.suggestedValue}`,
7192
+ property,
7193
+ value: checked.suggestedValue,
7194
+ deterministic: true
7195
+ },
7196
+ {
7197
+ symbols: tokenSymbolsInText(checked.suggestedValue),
7198
+ editKind: "replaceStyleValue",
7199
+ valuePreserving: checked.deterministicFix
7200
+ },
7201
+ ix
7202
+ );
6959
7203
  }
6960
7204
  function typographyMessage(property, value, allowed, unit, checked) {
6961
7205
  if (checked.reason === "token-equivalent" && checked.matchedToken) {
@@ -7629,7 +7873,7 @@ var SASS_FILE = /\.(scss|sass)$/i;
7629
7873
  function ruleTokensRequireDualFallback(ix) {
7630
7874
  const policy = ix.policy.ruleConfig(RULE_ID19);
7631
7875
  if (!policy?.enabled) return [];
7632
- const knownTokenVars = cssVariableNames(ix.tokens.list());
7876
+ const tokensByCssVariable = indexTokensByCssVariable(ix.tokens.list());
7633
7877
  const findings = [];
7634
7878
  for (const decl of ix.byKind("style_declaration")) {
7635
7879
  if (!SASS_FILE.test(decl.file)) continue;
@@ -7639,19 +7883,35 @@ function ruleTokensRequireDualFallback(ix) {
7639
7883
  const tokenName = match[1];
7640
7884
  const hasFallback = match[2] !== void 0;
7641
7885
  if (hasFallback) continue;
7642
- if (!knownTokenVars.has(tokenName) && !tokenName.startsWith("--fui-")) continue;
7886
+ const token = tokensByCssVariable.get(tokenName);
7887
+ if (!token) continue;
7643
7888
  const rawValue = match[0];
7644
7889
  const scssVar = tokenName.replace(/^--/, "$");
7645
- const replacement = `var(${tokenName}, ${scssVar})`;
7646
- const nextValue = decl.value.replace(rawValue, replacement);
7890
+ const hasScssVariable = token.referenceFormat === "scss-var";
7891
+ const nextValue = hasScssVariable ? decl.value.replace(rawValue, `var(${tokenName}, ${scssVar})`) : void 0;
7892
+ const fixEmission = nextValue ? emitFix(
7893
+ {
7894
+ kind: "replaceStyleValue",
7895
+ title: `Add fallback for ${tokenName}`,
7896
+ property: decl.property,
7897
+ value: nextValue,
7898
+ deterministic: true
7899
+ },
7900
+ {
7901
+ symbols: [tokenName, scssVar],
7902
+ editKind: "replaceStyleValue",
7903
+ valuePreserving: true
7904
+ },
7905
+ ix
7906
+ ) : void 0;
7647
7907
  findings.push(
7648
7908
  makeFinding({
7649
7909
  ruleId: RULE_ID19,
7650
7910
  ruleVersion: RULE_VERSION20,
7651
7911
  severity: policy.severity ?? "warn",
7652
- message: `var(${tokenName}) is missing an SCSS fallback. Use var(${tokenName}, ${scssVar}).`,
7912
+ message: hasScssVariable ? `var(${tokenName}) is missing an SCSS fallback. Use var(${tokenName}, ${scssVar}).` : `var(${tokenName}) is missing a fallback. No matching SCSS variable was found in the token source; add a fallback manually or define ${scssVar}.`,
7653
7913
  location: decl.location,
7654
- evidence: ix.evidence([decl.id, policy.id]),
7914
+ evidence: ix.evidence([decl.id, token.id, policy.id]),
7655
7915
  fingerprintIdentity: {
7656
7916
  file: decl.file,
7657
7917
  selector: decl.selector,
@@ -7659,18 +7919,13 @@ function ruleTokensRequireDualFallback(ix) {
7659
7919
  property: decl.property,
7660
7920
  tokenName
7661
7921
  },
7662
- fix: {
7663
- kind: "replaceStyleValue",
7664
- title: `Add fallback for ${tokenName}`,
7665
- property: decl.property,
7666
- value: nextValue,
7667
- deterministic: true
7668
- },
7922
+ fix: fixEmission?.fix,
7669
7923
  attributes: {
7670
7924
  property: decl.property,
7671
7925
  rawValue: decl.value,
7672
7926
  tokenName,
7673
- suggestedValue: nextValue,
7927
+ ...nextValue ? { suggestedValue: nextValue } : {},
7928
+ ...fixEmission?.downgradeReason ? { downgradeReason: fixEmission.downgradeReason } : {},
7674
7929
  source: "css"
7675
7930
  }
7676
7931
  })
@@ -7679,6 +7934,17 @@ function ruleTokensRequireDualFallback(ix) {
7679
7934
  }
7680
7935
  return findings;
7681
7936
  }
7937
+ function indexTokensByCssVariable(tokens) {
7938
+ const indexed = /* @__PURE__ */ new Map();
7939
+ for (const token of tokens) {
7940
+ const cssVariable = token.name.startsWith("--") ? token.name : token.name.startsWith("$") ? `--${token.name.slice(1)}` : `--${token.name}`;
7941
+ const existing = indexed.get(cssVariable);
7942
+ if (!existing || token.referenceFormat === "scss-var") {
7943
+ indexed.set(cssVariable, token);
7944
+ }
7945
+ }
7946
+ return indexed;
7947
+ }
7682
7948
 
7683
7949
  // src/rules/tokens-css-vars-must-be-defined.ts
7684
7950
  var RULE_ID20 = "tokens/css-vars-must-be-defined";
@@ -7770,6 +8036,8 @@ var BLOCKING_RULE_ALLOWLIST = /* @__PURE__ */ new Set([
7770
8036
  // imported component bypasses its canonical
7771
8037
  ]);
7772
8038
  function gatesCi(finding, failOnWarnings) {
8039
+ if (!canBlock(finding.evidenceGrade ?? "source_backed")) return false;
8040
+ if (finding.attributes?.advisory === true) return false;
7773
8041
  const level = severityLevel(finding.severity);
7774
8042
  return level === "error" || failOnWarnings && level === "warn";
7775
8043
  }
@@ -8500,6 +8768,7 @@ export {
8500
8768
  DEFAULTS,
8501
8769
  DEFAULT_ENHANCED_STYLE_PROPERTIES,
8502
8770
  DEFAULT_STYLE_PROPERTIES,
8771
+ EVIDENCE_ORDER,
8503
8772
  EXPLAIN_URL_BASE,
8504
8773
  FRAGMENTS_INTERNAL_RULE_IDS,
8505
8774
  FactIndex,
@@ -8551,6 +8820,7 @@ export {
8551
8820
  byCode,
8552
8821
  byRuleId,
8553
8822
  calculateDeltaE,
8823
+ canBlock,
8554
8824
  canonicalJson,
8555
8825
  canonicalPreimage,
8556
8826
  canonicalizeOwnedComponentId,