@usefragments/core 1.2.0 → 1.3.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 (43) 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-MLRDNSSA.js} +20 -6
  4. package/dist/chunk-MLRDNSSA.js.map +1 -0
  5. package/dist/codes/index.d.ts +1 -1
  6. package/dist/codes/index.js +2 -2
  7. package/dist/index-h_yWj15D.d.ts +6981 -0
  8. package/dist/index.d.ts +106 -92
  9. package/dist/index.js +235 -98
  10. package/dist/index.js.map +1 -1
  11. package/dist/registry.d.ts +36 -36
  12. package/dist/schemas/index.d.ts +3 -3377
  13. package/dist/schemas/index.js +1 -1
  14. package/package.json +1 -1
  15. package/src/agent-format.test.ts +75 -3
  16. package/src/agent-format.ts +28 -6
  17. package/src/evidence.ts +27 -0
  18. package/src/facts/builders.ts +2 -0
  19. package/src/facts/fact-index.ts +22 -2
  20. package/src/facts/facts.test.ts +1 -1
  21. package/src/facts/types.ts +2 -0
  22. package/src/index.ts +4 -0
  23. package/src/rules/__tests__/fix-emission-invariant.test.ts +174 -0
  24. package/src/rules/__tests__/tokens-require-dual-fallback.test.ts +90 -0
  25. package/src/rules/emit-gate.test.ts +16 -0
  26. package/src/rules/emit-gate.ts +6 -3
  27. package/src/rules/finding.ts +3 -0
  28. package/src/rules/rules.test.ts +2 -1
  29. package/src/rules/spacing-resolution.ts +5 -8
  30. package/src/rules/styles-no-raw-color.ts +32 -23
  31. package/src/rules/styles-no-raw-dimensions.ts +31 -12
  32. package/src/rules/styles-no-raw-spacing.ts +33 -11
  33. package/src/rules/styles-no-raw-typography.ts +37 -15
  34. package/src/rules/taxonomy.test.ts +13 -0
  35. package/src/rules/tokens-require-dual-fallback.ts +54 -17
  36. package/src/rules/utils.ts +51 -0
  37. package/src/schemas/index.ts +81 -3
  38. package/src/token-types.ts +6 -0
  39. package/src/tokens/__tests__/source-names.test.ts +42 -0
  40. package/src/tokens/design-token-parser.test.ts +24 -5
  41. package/src/tokens/design-token-parser.ts +40 -8
  42. package/dist/chunk-BQQTMGLA.js.map +0 -1
  43. 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-MLRDNSSA.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,
@@ -238,6 +238,18 @@ var DEFAULTS = {
238
238
  port: 6006
239
239
  };
240
240
 
241
+ // src/evidence.ts
242
+ var EVIDENCE_ORDER = [
243
+ "none",
244
+ "runtime_advisory",
245
+ "provenance_bound",
246
+ "source_backed",
247
+ "receipt_backed"
248
+ ];
249
+ function canBlock(grade) {
250
+ return EVIDENCE_ORDER.indexOf(grade) >= EVIDENCE_ORDER.indexOf("source_backed");
251
+ }
252
+
241
253
  // src/conform-prove.ts
242
254
  var PROVE_MAX_PASSES = 8;
243
255
  var PROVE_DEFAULT_MAX_PASSES = 4;
@@ -386,6 +398,7 @@ async function proveCompliant(input, callbacks, options = {}) {
386
398
 
387
399
  // src/agent-format.ts
388
400
  function buildAgentFormat(input) {
401
+ const zeroRulesLoaded = input.integrity.rulesLoaded === 0;
389
402
  const integrityFailed = !input.integrity.healthy || input.integrity.inert;
390
403
  const score = integrityFailed ? 0 : scoreFindings(input.findings);
391
404
  const visibleFindings = uniqueFindings(input.findings).slice(0, input.maxFindings ?? 50);
@@ -398,15 +411,14 @@ function buildAgentFormat(input) {
398
411
  severity: finding.severity,
399
412
  level: severityLevel(finding.severity),
400
413
  message: finding.message,
414
+ evidenceGrade: finding.evidenceGrade,
401
415
  evidence: input.includeEvidence ? finding.evidence : [],
402
416
  plan: planForFinding(finding)
403
417
  }));
404
418
  const hasErrors = input.findings.some((finding) => severityLevel(finding.severity) === "error");
405
- return {
419
+ const shared = {
406
420
  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),
421
+ 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
422
  integrity: input.integrity,
411
423
  fix_first: agentFindings.filter((finding) => finding.plan.confidence >= 0.8),
412
424
  remaining: agentFindings.filter((finding) => finding.plan.confidence < 0.8),
@@ -428,7 +440,23 @@ function buildAgentFormat(input) {
428
440
  ...suppression.expiresOn ? { expiresOn: suppression.expiresOn } : {},
429
441
  scope: "line"
430
442
  }))
431
- } : {}
443
+ } : {},
444
+ ...input.provenance ? { provenance: input.provenance } : {},
445
+ ...input.verdict ? { verdict: input.verdict } : {}
446
+ };
447
+ if (zeroRulesLoaded) {
448
+ return {
449
+ ...shared,
450
+ integrity: { ...input.integrity, rulesLoaded: 0 },
451
+ passed: false,
452
+ status: "indeterminate",
453
+ reasons: ["zero_rules_loaded"]
454
+ };
455
+ }
456
+ return {
457
+ ...shared,
458
+ passed: !integrityFailed && (input.passed ?? true) && !hasErrors,
459
+ score
432
460
  };
433
461
  }
434
462
  function uniqueFindings(findings) {
@@ -3326,19 +3354,35 @@ function parsedOutputToDesignTokens(output, filePath) {
3326
3354
  return tokens;
3327
3355
  }
3328
3356
  function scssOutputToDesignTokens(output, filePath) {
3329
- const names = /* @__PURE__ */ new Set();
3330
- const tokens = [];
3357
+ const tokensByName = /* @__PURE__ */ new Map();
3331
3358
  for (const cat of Object.values(output.categories)) {
3332
3359
  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));
3360
+ const isScssVariable = t.name.startsWith("$");
3361
+ const cssName = isScssVariable ? `--${t.name.slice(1)}` : t.name;
3362
+ if (!cssName.startsWith("--")) continue;
3363
+ const existing = tokensByName.get(cssName);
3364
+ if (!existing) {
3365
+ tokensByName.set(cssName, {
3366
+ parsed: t,
3367
+ referenceFormat: isScssVariable ? "scss-var" : "css-var",
3368
+ sourceNames: /* @__PURE__ */ new Set([t.name])
3369
+ });
3370
+ } else {
3371
+ existing.sourceNames.add(t.name);
3372
+ if (isScssVariable) {
3373
+ existing.parsed = t;
3374
+ existing.referenceFormat = "scss-var";
3375
+ }
3376
+ }
3337
3377
  }
3338
3378
  }
3339
- return tokens;
3379
+ return [...tokensByName].map(
3380
+ ([name, token]) => designTokenFromParsed(name, token.parsed, filePath, token.referenceFormat, [
3381
+ ...token.sourceNames
3382
+ ])
3383
+ );
3340
3384
  }
3341
- function designTokenFromParsed(name, parsed, filePath) {
3385
+ function designTokenFromParsed(name, parsed, filePath, referenceFormat, sourceNames) {
3342
3386
  return {
3343
3387
  name,
3344
3388
  rawValue: parsed.value ?? "",
@@ -3351,6 +3395,8 @@ function designTokenFromParsed(name, parsed, filePath) {
3351
3395
  level: 1,
3352
3396
  referenceChain: [],
3353
3397
  sourceFile: filePath,
3398
+ referenceFormat,
3399
+ ...sourceNames?.length ? { sourceNames } : {},
3354
3400
  theme: "default",
3355
3401
  selector: ":root",
3356
3402
  description: parsed.description
@@ -4289,12 +4335,37 @@ function makeFinding(input) {
4289
4335
  fingerprint,
4290
4336
  location: input.location,
4291
4337
  evidence: input.evidence,
4338
+ evidenceGrade: input.evidenceGrade ?? "source_backed",
4292
4339
  fix: input.fix,
4293
4340
  attributes: input.attributes
4294
4341
  });
4295
4342
  }
4296
4343
 
4297
4344
  // src/rules/utils.ts
4345
+ function emitFix(fix, proof, ix) {
4346
+ const reasons = [];
4347
+ if (proof.editKind !== fix.kind) reasons.push("edit kind does not match proof");
4348
+ if (!proof.valuePreserving) reasons.push("value-preservation proof missing");
4349
+ if (proof.symbols.length === 0) reasons.push("token-symbol proof missing");
4350
+ const missingSymbols = proof.symbols.filter((symbol) => !ix.tokens.symbolExists(symbol));
4351
+ if (missingSymbols.length > 0) {
4352
+ reasons.push(`absent token symbol: ${missingSymbols.join(", ")}`);
4353
+ }
4354
+ const inapplicableSymbols = proof.symbols.filter((symbol) => {
4355
+ const definition = ix.tokens.definitionForSymbol(symbol);
4356
+ return definition !== void 0 && !isApplicableTokenReference(definition.referenceFormat);
4357
+ });
4358
+ if (inapplicableSymbols.length > 0) {
4359
+ reasons.push(`token reference is not directly applicable: ${inapplicableSymbols.join(", ")}`);
4360
+ }
4361
+ return {
4362
+ fix: { ...fix, deterministic: reasons.length === 0 },
4363
+ ...reasons.length > 0 ? { downgradeReason: reasons.join("; ") } : {}
4364
+ };
4365
+ }
4366
+ function tokenSymbolsInText(value) {
4367
+ return [...new Set(value.match(/\$[A-Za-z0-9_-]+|--[A-Za-z0-9_-]+/g) ?? [])];
4368
+ }
4298
4369
  function indexPropsByNodeId(ix) {
4299
4370
  const out = /* @__PURE__ */ new Map();
4300
4371
  for (const fact of ix.byKind("usage_prop_resolved")) {
@@ -4373,13 +4444,6 @@ function readUsageNode(ix, id) {
4373
4444
  const fact = ix.get(id);
4374
4445
  return fact && fact.kind === "usage_node" ? fact : void 0;
4375
4446
  }
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
4447
  function contractPrefixFamilies(names) {
4384
4448
  const families = /* @__PURE__ */ new Set();
4385
4449
  for (const name of names) {
@@ -6170,6 +6234,7 @@ function ruleStylesNoRawColor(ix) {
6170
6234
  if (!color) continue;
6171
6235
  if (isExemptColor(color, policy.except)) continue;
6172
6236
  const resolution = resolveColorToken(decl.property, color, colorTokens);
6237
+ const fixEmission = resolution?.kind === "exact" ? buildTokenFix(decl.property, decl.value, color, resolution, ix) : void 0;
6173
6238
  const evidenceIds = resolution ? [decl.id, policy.id, resolution.token.id] : [decl.id, policy.id];
6174
6239
  findings.push(
6175
6240
  makeFinding({
@@ -6187,14 +6252,15 @@ function ruleStylesNoRawColor(ix) {
6187
6252
  property: decl.property,
6188
6253
  value: color
6189
6254
  },
6190
- fix: resolution?.kind === "exact" ? buildTokenFix(decl.property, decl.value, color, resolution) : void 0,
6255
+ fix: fixEmission?.fix,
6191
6256
  attributes: {
6192
6257
  property: decl.property,
6193
6258
  rawValue: decl.value,
6194
6259
  color,
6195
6260
  source: "css",
6196
6261
  suggestedToken: resolution?.token.name,
6197
- ...resolution ? { tokenMatch: resolution.kind } : {}
6262
+ ...resolution ? { tokenMatch: resolution.kind } : {},
6263
+ ...fixEmission?.downgradeReason ? { downgradeReason: fixEmission.downgradeReason } : {}
6198
6264
  }
6199
6265
  })
6200
6266
  );
@@ -6209,6 +6275,7 @@ function ruleStylesNoRawColor(ix) {
6209
6275
  if (!node) continue;
6210
6276
  const componentEvidenceId = componentByNode.get(node.id)?.id;
6211
6277
  const resolution = resolveColorToken(inline.property, color, colorTokens);
6278
+ const fixEmission = resolution?.kind === "exact" ? buildTokenFix(inline.property, inline.value, color, resolution, ix) : void 0;
6212
6279
  const baseEvidence = componentEvidenceId ? [node.id, componentEvidenceId, inline.id, policy.id] : [node.id, inline.id, policy.id];
6213
6280
  const evidenceIds = resolution ? [...baseEvidence, resolution.token.id] : baseEvidence;
6214
6281
  findings.push(
@@ -6227,14 +6294,15 @@ function ruleStylesNoRawColor(ix) {
6227
6294
  property: inline.property,
6228
6295
  value: color
6229
6296
  },
6230
- fix: resolution?.kind === "exact" ? buildTokenFix(inline.property, inline.value, color, resolution) : void 0,
6297
+ fix: fixEmission?.fix,
6231
6298
  attributes: {
6232
6299
  property: inline.property,
6233
6300
  rawValue: inline.value,
6234
6301
  color,
6235
6302
  source: "jsx",
6236
6303
  suggestedToken: resolution?.token.name,
6237
- ...resolution ? { tokenMatch: resolution.kind } : {}
6304
+ ...resolution ? { tokenMatch: resolution.kind } : {},
6305
+ ...fixEmission?.downgradeReason ? { downgradeReason: fixEmission.downgradeReason } : {}
6238
6306
  }
6239
6307
  })
6240
6308
  );
@@ -6318,21 +6386,24 @@ function normalizeColor2(value) {
6318
6386
  }
6319
6387
  return lower;
6320
6388
  }
6321
- function buildTokenFix(property, rawValue, rawColor, match) {
6389
+ function buildTokenFix(property, rawValue, rawColor, match, ix) {
6322
6390
  const reference = tokenReference(match.token.name);
6323
6391
  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
- };
6392
+ return emitFix(
6393
+ {
6394
+ kind: "replaceStyleValue",
6395
+ title: `Replace ${property} with ${value}`,
6396
+ property,
6397
+ value,
6398
+ deterministic: true
6399
+ },
6400
+ {
6401
+ symbols: [match.token.name],
6402
+ editKind: "replaceStyleValue",
6403
+ valuePreserving: !match.ambiguous
6404
+ },
6405
+ ix
6406
+ );
6336
6407
  }
6337
6408
  function replaceRawColor(rawValue, rawColor, replacement) {
6338
6409
  if (rawValue === rawColor) return replacement;
@@ -6363,6 +6434,7 @@ function ruleStylesNoRawDimensions(ix) {
6363
6434
  if (!resolution) continue;
6364
6435
  const tokenIds = resolutionTokenIds(resolution);
6365
6436
  const evidenceIds = tokenIds.length ? [decl.id, policy.id, ...tokenIds] : [decl.id, policy.id];
6437
+ const fixEmission = dimensionFix(decl.property, resolution, ix);
6366
6438
  findings.push(
6367
6439
  makeFinding({
6368
6440
  ruleId: RULE_ID9,
@@ -6379,12 +6451,13 @@ function ruleStylesNoRawDimensions(ix) {
6379
6451
  property: decl.property,
6380
6452
  value: decl.value
6381
6453
  },
6382
- fix: dimensionFix(decl.property, resolution),
6454
+ fix: fixEmission?.fix,
6383
6455
  attributes: {
6384
6456
  property: decl.property,
6385
6457
  rawValue: decl.value,
6386
6458
  source: "css",
6387
- suggestedToken: resolutionSuggestedToken(resolution)
6459
+ suggestedToken: resolutionSuggestedToken(resolution),
6460
+ ...fixEmission?.downgradeReason ? { downgradeReason: fixEmission.downgradeReason } : {}
6388
6461
  }
6389
6462
  })
6390
6463
  );
@@ -6403,6 +6476,7 @@ function ruleStylesNoRawDimensions(ix) {
6403
6476
  const baseEvidence = componentEvidenceId ? [node.id, componentEvidenceId, inline.id, policy.id] : [node.id, inline.id, policy.id];
6404
6477
  const tokenIds = resolutionTokenIds(resolution);
6405
6478
  const evidenceIds = tokenIds.length ? [...baseEvidence, ...tokenIds] : baseEvidence;
6479
+ const fixEmission = dimensionFix(inline.property, resolution, ix);
6406
6480
  findings.push(
6407
6481
  makeFinding({
6408
6482
  ruleId: RULE_ID9,
@@ -6419,12 +6493,13 @@ function ruleStylesNoRawDimensions(ix) {
6419
6493
  property: inline.property,
6420
6494
  value: inline.value
6421
6495
  },
6422
- fix: dimensionFix(inline.property, resolution),
6496
+ fix: fixEmission?.fix,
6423
6497
  attributes: {
6424
6498
  property: inline.property,
6425
6499
  rawValue: inline.value,
6426
6500
  source: "jsx",
6427
- suggestedToken: resolutionSuggestedToken(resolution)
6501
+ suggestedToken: resolutionSuggestedToken(resolution),
6502
+ ...fixEmission?.downgradeReason ? { downgradeReason: fixEmission.downgradeReason } : {}
6428
6503
  }
6429
6504
  })
6430
6505
  );
@@ -6469,15 +6544,23 @@ function dimensionMessage(rawValue, property, preferLabel, resolution, inline) {
6469
6544
  }
6470
6545
  return `Raw dimension ${rawValue} on ${where}. Use a ${preferLabel} instead.`;
6471
6546
  }
6472
- function dimensionFix(property, resolution) {
6547
+ function dimensionFix(property, resolution, ix) {
6473
6548
  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
- };
6549
+ return emitFix(
6550
+ {
6551
+ kind: "replaceStyleValue",
6552
+ title: `Replace ${property} with ${resolution.fixValue}`,
6553
+ property,
6554
+ value: resolution.fixValue,
6555
+ deterministic: true
6556
+ },
6557
+ {
6558
+ symbols: resolution.tokens.map((token) => token.name),
6559
+ editKind: "replaceStyleValue",
6560
+ valuePreserving: true
6561
+ },
6562
+ ix
6563
+ );
6481
6564
  }
6482
6565
  return void 0;
6483
6566
  }
@@ -6578,7 +6661,7 @@ function resolveSpacingValue(input) {
6578
6661
  let anyOffScale = false;
6579
6662
  let canFixAll = true;
6580
6663
  let reliableSuggestion = true;
6581
- let allApplicableValuePreserving = true;
6664
+ let allValuePreserving = true;
6582
6665
  for (const part of parts) {
6583
6666
  const normalized = normalizeLengthForScale(part.parsed, input.scale);
6584
6667
  if (normalized === null) return null;
@@ -6592,8 +6675,7 @@ function resolveSpacingValue(input) {
6592
6675
  }
6593
6676
  anyViolation = true;
6594
6677
  const reference = tokenReference(exactToken.name);
6595
- const applicable = isApplicableTokenReference(exactToken.referenceFormat) && normalized.deterministicFix;
6596
- if (!applicable) allApplicableValuePreserving = false;
6678
+ if (!normalized.deterministicFix) allValuePreserving = false;
6597
6679
  suggestedParts.push(reference);
6598
6680
  if (!firstViolation) {
6599
6681
  firstViolation = {
@@ -6601,7 +6683,7 @@ function resolveSpacingValue(input) {
6601
6683
  suggestedValue: reference,
6602
6684
  suggestedToken: exactToken.name,
6603
6685
  fixEmittable: true,
6604
- deterministicFix: applicable,
6686
+ deterministicFix: normalized.deterministicFix,
6605
6687
  reason: "token-equivalent",
6606
6688
  matchedToken: exactToken.name,
6607
6689
  assumedRootFontSizePx: normalized.assumedRootFontSizePx,
@@ -6612,7 +6694,7 @@ function resolveSpacingValue(input) {
6612
6694
  }
6613
6695
  anyViolation = true;
6614
6696
  anyOffScale = true;
6615
- allApplicableValuePreserving = false;
6697
+ allValuePreserving = false;
6616
6698
  const nearest = nearestSignedScaleValue(normalized.value, input.allowed);
6617
6699
  const snapsToZero = nearest === 0 && normalized.value !== 0;
6618
6700
  let suggestedPart;
@@ -6645,7 +6727,7 @@ function resolveSpacingValue(input) {
6645
6727
  const joined = suggestedParts.join(" ");
6646
6728
  const changes = joined !== input.raw.trim().replace(/\s+/g, " ");
6647
6729
  const fixEmittable = canFixAll && reliableSuggestion && changes;
6648
- const deterministic = fixEmittable && !anyOffScale && allApplicableValuePreserving;
6730
+ const deterministic = fixEmittable && !anyOffScale && allValuePreserving;
6649
6731
  return {
6650
6732
  ...firstViolation,
6651
6733
  reason: anyOffScale ? "off-scale" : "token-equivalent",
@@ -6709,6 +6791,7 @@ function checkDeclaration(ix, decl, lookupFor) {
6709
6791
  tokens: lookupFor(scale)
6710
6792
  });
6711
6793
  if (!checked) return null;
6794
+ const fixEmission = buildSpacingFix(decl.property, checked, ix);
6712
6795
  return makeFinding({
6713
6796
  ruleId: RULE_ID10,
6714
6797
  ruleVersion: RULE_VERSION11,
@@ -6724,7 +6807,7 @@ function checkDeclaration(ix, decl, lookupFor) {
6724
6807
  property: decl.property,
6725
6808
  value: decl.value
6726
6809
  },
6727
- fix: buildSpacingFix(decl.property, checked),
6810
+ fix: fixEmission?.fix,
6728
6811
  attributes: {
6729
6812
  property: decl.property,
6730
6813
  rawValue: decl.value,
@@ -6738,7 +6821,8 @@ function checkDeclaration(ix, decl, lookupFor) {
6738
6821
  normalizedUnit: scale.unit,
6739
6822
  assumedRootFontSizePx: checked.assumedRootFontSizePx,
6740
6823
  assumedEmBasePx: checked.assumedEmBasePx,
6741
- source: "css"
6824
+ source: "css",
6825
+ ...fixEmission?.downgradeReason ? { downgradeReason: fixEmission.downgradeReason } : {}
6742
6826
  }
6743
6827
  });
6744
6828
  }
@@ -6758,6 +6842,7 @@ function checkInlineStyle(ix, inline, componentByNode, lookupFor) {
6758
6842
  bareNumberFix: inline.valueKind === "number"
6759
6843
  });
6760
6844
  if (!checked) return null;
6845
+ const fixEmission = buildSpacingFix(inline.property, checked, ix);
6761
6846
  const node = readUsageNode(ix, inline.nodeId);
6762
6847
  if (!node) return null;
6763
6848
  const componentEvidenceId = componentByNode.get(node.id)?.id;
@@ -6777,7 +6862,7 @@ function checkInlineStyle(ix, inline, componentByNode, lookupFor) {
6777
6862
  property: inline.property,
6778
6863
  value: inline.value
6779
6864
  },
6780
- fix: buildSpacingFix(inline.property, checked),
6865
+ fix: fixEmission?.fix,
6781
6866
  attributes: {
6782
6867
  property: inline.property,
6783
6868
  rawValue: inline.value,
@@ -6791,19 +6876,28 @@ function checkInlineStyle(ix, inline, componentByNode, lookupFor) {
6791
6876
  normalizedUnit: scale.unit,
6792
6877
  assumedRootFontSizePx: checked.assumedRootFontSizePx,
6793
6878
  assumedEmBasePx: checked.assumedEmBasePx,
6794
- source: "jsx"
6879
+ source: "jsx",
6880
+ ...fixEmission?.downgradeReason ? { downgradeReason: fixEmission.downgradeReason } : {}
6795
6881
  }
6796
6882
  });
6797
6883
  }
6798
- function buildSpacingFix(property, checked) {
6884
+ function buildSpacingFix(property, checked, ix) {
6799
6885
  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
- };
6886
+ return emitFix(
6887
+ {
6888
+ kind: "replaceStyleValue",
6889
+ title: `Replace ${property} with ${checked.suggestedValue}`,
6890
+ property,
6891
+ value: checked.suggestedValue,
6892
+ deterministic: true
6893
+ },
6894
+ {
6895
+ symbols: tokenSymbolsInText(checked.suggestedValue),
6896
+ editKind: "replaceStyleValue",
6897
+ valuePreserving: checked.deterministicFix
6898
+ },
6899
+ ix
6900
+ );
6807
6901
  }
6808
6902
  function spacingMessage(property, value, allowed, unit, checked) {
6809
6903
  if (checked.reason === "token-equivalent" && checked.matchedToken) {
@@ -6832,6 +6926,7 @@ function ruleStylesNoRawTypography(ix) {
6832
6926
  if (decl.property.toLowerCase() !== "font-size") continue;
6833
6927
  const checked = checkFontSize(decl.value, allowed, scale, tokens);
6834
6928
  if (!checked) continue;
6929
+ const fixEmission = buildFix(decl.property, checked, ix);
6835
6930
  findings.push(
6836
6931
  makeFinding({
6837
6932
  ruleId: RULE_ID11,
@@ -6848,7 +6943,7 @@ function ruleStylesNoRawTypography(ix) {
6848
6943
  property: decl.property,
6849
6944
  value: decl.value
6850
6945
  },
6851
- fix: buildFix(decl.property, checked),
6946
+ fix: fixEmission?.fix,
6852
6947
  attributes: {
6853
6948
  property: decl.property,
6854
6949
  rawValue: decl.value,
@@ -6856,7 +6951,8 @@ function ruleStylesNoRawTypography(ix) {
6856
6951
  allowed,
6857
6952
  suggestedValue: checked.suggestedValue,
6858
6953
  matchedToken: checked.matchedToken,
6859
- source: "css"
6954
+ source: "css",
6955
+ ...fixEmission?.downgradeReason ? { downgradeReason: fixEmission.downgradeReason } : {}
6860
6956
  }
6861
6957
  })
6862
6958
  );
@@ -6865,8 +6961,15 @@ function ruleStylesNoRawTypography(ix) {
6865
6961
  for (const inline of ix.byKind("usage_inline_style")) {
6866
6962
  if (inline.valueKind === "css-variable") continue;
6867
6963
  if (!FONT_SIZE_PROPERTIES.has(inline.property.toLowerCase())) continue;
6868
- const checked = checkFontSize(inline.value, allowed, scale, tokens, inline.valueKind === "number");
6964
+ const checked = checkFontSize(
6965
+ inline.value,
6966
+ allowed,
6967
+ scale,
6968
+ tokens,
6969
+ inline.valueKind === "number"
6970
+ );
6869
6971
  if (!checked) continue;
6972
+ const fixEmission = buildFix(inline.property, checked, ix);
6870
6973
  const node = readUsageNode(ix, inline.nodeId);
6871
6974
  if (!node) continue;
6872
6975
  const componentEvidenceId = componentByNode.get(node.id)?.id;
@@ -6887,7 +6990,7 @@ function ruleStylesNoRawTypography(ix) {
6887
6990
  property: inline.property,
6888
6991
  value: inline.value
6889
6992
  },
6890
- fix: buildFix(inline.property, checked),
6993
+ fix: fixEmission?.fix,
6891
6994
  attributes: {
6892
6995
  property: inline.property,
6893
6996
  rawValue: inline.value,
@@ -6895,7 +6998,8 @@ function ruleStylesNoRawTypography(ix) {
6895
6998
  allowed,
6896
6999
  suggestedValue: checked.suggestedValue,
6897
7000
  matchedToken: checked.matchedToken,
6898
- source: "jsx"
7001
+ source: "jsx",
7002
+ ...fixEmission?.downgradeReason ? { downgradeReason: fixEmission.downgradeReason } : {}
6899
7003
  }
6900
7004
  })
6901
7005
  );
@@ -6925,11 +7029,10 @@ function checkFontSize(raw, allowed, scale, tokens, bareNumberFix = false) {
6925
7029
  const exactToken = tokens.get(magnitude);
6926
7030
  if (matchesScale(normalized.value, allowed)) {
6927
7031
  if (!exactToken) return null;
6928
- const applicable = isApplicableTokenReference(exactToken.referenceFormat) && normalized.deterministicFix;
6929
7032
  return {
6930
7033
  suggestedValue: tokenReference(exactToken.name),
6931
7034
  fixEmittable: true,
6932
- deterministicFix: applicable,
7035
+ deterministicFix: normalized.deterministicFix,
6933
7036
  reason: "token-equivalent",
6934
7037
  matchedToken: exactToken.name
6935
7038
  };
@@ -6947,15 +7050,23 @@ function checkFontSize(raw, allowed, scale, tokens, bareNumberFix = false) {
6947
7050
  reason: "off-scale"
6948
7051
  };
6949
7052
  }
6950
- function buildFix(property, checked) {
7053
+ function buildFix(property, checked, ix) {
6951
7054
  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
- };
7055
+ return emitFix(
7056
+ {
7057
+ kind: "replaceStyleValue",
7058
+ title: `Replace ${property} with ${checked.suggestedValue}`,
7059
+ property,
7060
+ value: checked.suggestedValue,
7061
+ deterministic: true
7062
+ },
7063
+ {
7064
+ symbols: tokenSymbolsInText(checked.suggestedValue),
7065
+ editKind: "replaceStyleValue",
7066
+ valuePreserving: checked.deterministicFix
7067
+ },
7068
+ ix
7069
+ );
6959
7070
  }
6960
7071
  function typographyMessage(property, value, allowed, unit, checked) {
6961
7072
  if (checked.reason === "token-equivalent" && checked.matchedToken) {
@@ -7629,7 +7740,7 @@ var SASS_FILE = /\.(scss|sass)$/i;
7629
7740
  function ruleTokensRequireDualFallback(ix) {
7630
7741
  const policy = ix.policy.ruleConfig(RULE_ID19);
7631
7742
  if (!policy?.enabled) return [];
7632
- const knownTokenVars = cssVariableNames(ix.tokens.list());
7743
+ const tokensByCssVariable = indexTokensByCssVariable(ix.tokens.list());
7633
7744
  const findings = [];
7634
7745
  for (const decl of ix.byKind("style_declaration")) {
7635
7746
  if (!SASS_FILE.test(decl.file)) continue;
@@ -7639,19 +7750,35 @@ function ruleTokensRequireDualFallback(ix) {
7639
7750
  const tokenName = match[1];
7640
7751
  const hasFallback = match[2] !== void 0;
7641
7752
  if (hasFallback) continue;
7642
- if (!knownTokenVars.has(tokenName) && !tokenName.startsWith("--fui-")) continue;
7753
+ const token = tokensByCssVariable.get(tokenName);
7754
+ if (!token) continue;
7643
7755
  const rawValue = match[0];
7644
7756
  const scssVar = tokenName.replace(/^--/, "$");
7645
- const replacement = `var(${tokenName}, ${scssVar})`;
7646
- const nextValue = decl.value.replace(rawValue, replacement);
7757
+ const hasScssVariable = token.referenceFormat === "scss-var";
7758
+ const nextValue = hasScssVariable ? decl.value.replace(rawValue, `var(${tokenName}, ${scssVar})`) : void 0;
7759
+ const fixEmission = nextValue ? emitFix(
7760
+ {
7761
+ kind: "replaceStyleValue",
7762
+ title: `Add fallback for ${tokenName}`,
7763
+ property: decl.property,
7764
+ value: nextValue,
7765
+ deterministic: true
7766
+ },
7767
+ {
7768
+ symbols: [tokenName, scssVar],
7769
+ editKind: "replaceStyleValue",
7770
+ valuePreserving: true
7771
+ },
7772
+ ix
7773
+ ) : void 0;
7647
7774
  findings.push(
7648
7775
  makeFinding({
7649
7776
  ruleId: RULE_ID19,
7650
7777
  ruleVersion: RULE_VERSION20,
7651
7778
  severity: policy.severity ?? "warn",
7652
- message: `var(${tokenName}) is missing an SCSS fallback. Use var(${tokenName}, ${scssVar}).`,
7779
+ 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
7780
  location: decl.location,
7654
- evidence: ix.evidence([decl.id, policy.id]),
7781
+ evidence: ix.evidence([decl.id, token.id, policy.id]),
7655
7782
  fingerprintIdentity: {
7656
7783
  file: decl.file,
7657
7784
  selector: decl.selector,
@@ -7659,18 +7786,13 @@ function ruleTokensRequireDualFallback(ix) {
7659
7786
  property: decl.property,
7660
7787
  tokenName
7661
7788
  },
7662
- fix: {
7663
- kind: "replaceStyleValue",
7664
- title: `Add fallback for ${tokenName}`,
7665
- property: decl.property,
7666
- value: nextValue,
7667
- deterministic: true
7668
- },
7789
+ fix: fixEmission?.fix,
7669
7790
  attributes: {
7670
7791
  property: decl.property,
7671
7792
  rawValue: decl.value,
7672
7793
  tokenName,
7673
- suggestedValue: nextValue,
7794
+ ...nextValue ? { suggestedValue: nextValue } : {},
7795
+ ...fixEmission?.downgradeReason ? { downgradeReason: fixEmission.downgradeReason } : {},
7674
7796
  source: "css"
7675
7797
  }
7676
7798
  })
@@ -7679,6 +7801,17 @@ function ruleTokensRequireDualFallback(ix) {
7679
7801
  }
7680
7802
  return findings;
7681
7803
  }
7804
+ function indexTokensByCssVariable(tokens) {
7805
+ const indexed = /* @__PURE__ */ new Map();
7806
+ for (const token of tokens) {
7807
+ const cssVariable = token.name.startsWith("--") ? token.name : token.name.startsWith("$") ? `--${token.name.slice(1)}` : `--${token.name}`;
7808
+ const existing = indexed.get(cssVariable);
7809
+ if (!existing || token.referenceFormat === "scss-var") {
7810
+ indexed.set(cssVariable, token);
7811
+ }
7812
+ }
7813
+ return indexed;
7814
+ }
7682
7815
 
7683
7816
  // src/rules/tokens-css-vars-must-be-defined.ts
7684
7817
  var RULE_ID20 = "tokens/css-vars-must-be-defined";
@@ -7770,6 +7903,8 @@ var BLOCKING_RULE_ALLOWLIST = /* @__PURE__ */ new Set([
7770
7903
  // imported component bypasses its canonical
7771
7904
  ]);
7772
7905
  function gatesCi(finding, failOnWarnings) {
7906
+ if (!canBlock(finding.evidenceGrade ?? "source_backed")) return false;
7907
+ if (finding.attributes?.advisory === true) return false;
7773
7908
  const level = severityLevel(finding.severity);
7774
7909
  return level === "error" || failOnWarnings && level === "warn";
7775
7910
  }
@@ -8500,6 +8635,7 @@ export {
8500
8635
  DEFAULTS,
8501
8636
  DEFAULT_ENHANCED_STYLE_PROPERTIES,
8502
8637
  DEFAULT_STYLE_PROPERTIES,
8638
+ EVIDENCE_ORDER,
8503
8639
  EXPLAIN_URL_BASE,
8504
8640
  FRAGMENTS_INTERNAL_RULE_IDS,
8505
8641
  FactIndex,
@@ -8551,6 +8687,7 @@ export {
8551
8687
  byCode,
8552
8688
  byRuleId,
8553
8689
  calculateDeltaE,
8690
+ canBlock,
8554
8691
  canonicalJson,
8555
8692
  canonicalPreimage,
8556
8693
  canonicalizeOwnedComponentId,