@usefragments/core 1.3.0 → 1.5.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 (44) hide show
  1. package/dist/{chunk-MLRDNSSA.js → chunk-HXGE3O2F.js} +39 -1
  2. package/dist/chunk-HXGE3O2F.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-DYSirwJu.d.ts → governance-BLsyk56o.d.ts} +21 -0
  8. package/dist/index.d.ts +324 -82
  9. package/dist/index.js +507 -120
  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/codes/codes.ts +9 -0
  15. package/src/conform.ts +5 -0
  16. package/src/constants.ts +3 -0
  17. package/src/facts/builders.ts +46 -2
  18. package/src/facts/facts.test.ts +30 -0
  19. package/src/facts/index.ts +6 -0
  20. package/src/facts/types.ts +75 -2
  21. package/src/governance.test.ts +26 -0
  22. package/src/identity/classify.test.ts +185 -0
  23. package/src/identity/classify.ts +257 -0
  24. package/src/identity/component-key.test.ts +32 -0
  25. package/src/identity/component-key.ts +35 -0
  26. package/src/identity/usage.ts +39 -0
  27. package/src/index.ts +25 -0
  28. package/src/rules/components-prefer-library.test.ts +196 -0
  29. package/src/rules/components-prefer-library.ts +68 -1
  30. package/src/rules/components-shadow-component.test.ts +145 -0
  31. package/src/rules/components-shadow-component.ts +81 -0
  32. package/src/rules/fix-availability.ts +1 -0
  33. package/src/rules/index.ts +7 -0
  34. package/src/rules/styles-no-raw-color.test.ts +213 -0
  35. package/src/rules/styles-no-raw-color.ts +125 -0
  36. package/src/rules/taxonomy.test.ts +16 -1
  37. package/src/rules/tiers.ts +7 -5
  38. package/src/schema.ts +21 -2
  39. package/src/token-types.ts +12 -0
  40. package/src/tokens/categories.ts +54 -24
  41. package/src/tokens/scss.test.ts +94 -0
  42. package/src/tokens/scss.ts +11 -4
  43. package/src/types.ts +23 -0
  44. package/dist/chunk-MLRDNSSA.js.map +0 -1
package/dist/index.js CHANGED
@@ -71,6 +71,8 @@ import {
71
71
  makeClassNameDynamicFact,
72
72
  makeClassNameLiteralFact,
73
73
  makeComponentCapabilityFact,
74
+ makeComponentDefinitionFact,
75
+ makeComponentIdentityFact,
74
76
  makeComponentMetadataFact,
75
77
  makeContractTokenFact,
76
78
  makeJsxComponentPreferredFact,
@@ -109,7 +111,7 @@ import {
109
111
  ownedImportsEqual,
110
112
  resolveComponentGovernance,
111
113
  scaleGovernanceRecordSchema
112
- } from "./chunk-MLRDNSSA.js";
114
+ } from "./chunk-HXGE3O2F.js";
113
115
  import {
114
116
  AGENT_FORMAT_SCHEMA_VERSION,
115
117
  agentErrorEnvelopeSchema,
@@ -185,6 +187,8 @@ var BRAND = {
185
187
  legacyConfigFile: "segments.config.ts",
186
188
  /** CLI command name (e.g., "fragments") */
187
189
  cliCommand: "fragments",
190
+ /** Full user-facing CLI invocation for copy printed to end users (e.g., "npx @usefragments/cli") */
191
+ cliInvocation: "npx @usefragments/cli",
188
192
  /** Directory for storing fragments, registry, and cache */
189
193
  dataDir: ".fragments",
190
194
  /** Components subdirectory within .fragments/ */
@@ -1679,16 +1683,20 @@ var tokenSourceSchema = z4.object({
1679
1683
  var tokenConfigSchema = z4.object({
1680
1684
  include: repoRelativePathsSchema.optional(),
1681
1685
  sources: z4.array(tokenSourceSchema).min(1).optional(),
1686
+ // Declared design-system packages whose shipped fragments.json token
1687
+ // vocabulary seeds the known-token set (#7c). A packages-only tokens block
1688
+ // is valid — see the superRefine below.
1689
+ packages: z4.array(z4.string().min(1)).optional(),
1682
1690
  exclude: z4.array(repoRelativePathSchema).optional(),
1683
1691
  themeSelectors: z4.record(z4.string()).optional(),
1684
1692
  enabled: z4.boolean().optional(),
1685
1693
  format: z4.enum(["auto", "css", "scss", "dtcg", "tailwind"]).optional(),
1686
1694
  namespace: z4.string().min(1).optional()
1687
1695
  }).passthrough().superRefine((tokens, ctx) => {
1688
- if (!tokens.include?.length && !tokens.sources?.length) {
1696
+ if (!tokens.include?.length && !tokens.sources?.length && !tokens.packages?.length) {
1689
1697
  ctx.addIssue({
1690
1698
  code: z4.ZodIssueCode.custom,
1691
- message: "Add tokens.include or tokens.sources",
1699
+ message: "Add tokens.include, tokens.sources, or tokens.packages",
1692
1700
  path: ["sources"]
1693
1701
  });
1694
1702
  }
@@ -1770,7 +1778,18 @@ var fragmentsConfigSchema = z4.object({
1770
1778
  })
1771
1779
  ).optional()
1772
1780
  }).optional(),
1773
- recognizedClassHelpers: z4.array(z4.string().min(1)).optional()
1781
+ recognizedClassHelpers: z4.array(z4.string().min(1)).optional(),
1782
+ identity: z4.object({
1783
+ decisions: z4.array(
1784
+ z4.object({
1785
+ component: z4.string().min(1),
1786
+ kind: z4.enum(["sanction", "reject", "dismiss"]),
1787
+ canonicalTarget: z4.string().min(1).optional(),
1788
+ reason: z4.string().optional(),
1789
+ decisionId: z4.string().min(1).optional()
1790
+ })
1791
+ ).optional()
1792
+ }).optional()
1774
1793
  });
1775
1794
  var compositionMetadataSchema = z4.object({
1776
1795
  pattern: z4.enum(["compound", "simple", "controlled", "wrapper"]).optional(),
@@ -2931,30 +2950,41 @@ function inferTokenGroup(name) {
2931
2950
  return category;
2932
2951
  }
2933
2952
  }
2953
+ var TOKEN_GROUP_COMMENT_MAPPINGS = {
2954
+ "base configuration": "base",
2955
+ typography: "typography",
2956
+ "spacing (micro)": "spacing",
2957
+ spacing: "spacing",
2958
+ "density padding": "spacing",
2959
+ "border radius": "radius",
2960
+ transitions: "transitions",
2961
+ colors: "colors",
2962
+ surfaces: "surfaces",
2963
+ text: "text",
2964
+ borders: "borders",
2965
+ shadows: "shadows",
2966
+ focus: "focus",
2967
+ scrollbar: "scrollbar",
2968
+ "component heights": "component-sizing",
2969
+ "appshell layout": "layout",
2970
+ codeblock: "code",
2971
+ tooltip: "tooltip",
2972
+ "hero/marketing gradient": "marketing"
2973
+ };
2974
+ function looksLikeCleanHeading(text) {
2975
+ if (!/^[a-z0-9][a-z0-9 -]*$/.test(text)) return false;
2976
+ const wordCount = text.split(/\s+/).filter(Boolean).length;
2977
+ return wordCount > 0 && wordCount <= 3;
2978
+ }
2934
2979
  function normalizeTokenGroupComment(comment) {
2935
2980
  const text = comment.trim().replace(/^\/\/\s*/, "").replace(/^\/\*+\s*/, "").replace(/\s*\*+\/$/, "").trim().toLowerCase();
2936
- const mappings = {
2937
- "base configuration": "base",
2938
- typography: "typography",
2939
- "spacing (micro)": "spacing",
2940
- spacing: "spacing",
2941
- "density padding": "spacing",
2942
- "border radius": "radius",
2943
- transitions: "transitions",
2944
- colors: "colors",
2945
- surfaces: "surfaces",
2946
- text: "text",
2947
- borders: "borders",
2948
- shadows: "shadows",
2949
- focus: "focus",
2950
- scrollbar: "scrollbar",
2951
- "component heights": "component-sizing",
2952
- "appshell layout": "layout",
2953
- codeblock: "code",
2954
- tooltip: "tooltip",
2955
- "hero/marketing gradient": "marketing"
2956
- };
2957
- return mappings[text] ?? text.replace(/\s+/g, "-");
2981
+ if (text in TOKEN_GROUP_COMMENT_MAPPINGS) {
2982
+ return TOKEN_GROUP_COMMENT_MAPPINGS[text];
2983
+ }
2984
+ if (!looksLikeCleanHeading(text)) {
2985
+ return null;
2986
+ }
2987
+ return text.replace(/\s+/g, "-");
2958
2988
  }
2959
2989
 
2960
2990
  // src/tokens/scss.ts
@@ -3021,7 +3051,7 @@ function parseScssTokens(content, filePath = "tokens.scss") {
3021
3051
  const tokens = [];
3022
3052
  const seenNames = /* @__PURE__ */ new Set();
3023
3053
  let currentCategory = "other";
3024
- let hasCommentCategories = false;
3054
+ let currentCategoryIsCurated = false;
3025
3055
  const scssVars = parseScssVariables(content);
3026
3056
  const varDeclRegex = /^\s*(--[\w-]+)\s*:\s*(.+?)\s*;/;
3027
3057
  const sectionCommentRegex = /^\s*\/\/\s+([A-Z].+)$/;
@@ -3031,7 +3061,9 @@ function parseScssTokens(content, filePath = "tokens.scss") {
3031
3061
  const normalized = normalizeTokenGroupComment(commentMatch[0]);
3032
3062
  if (normalized) {
3033
3063
  currentCategory = normalized;
3034
- hasCommentCategories = true;
3064
+ currentCategoryIsCurated = true;
3065
+ } else {
3066
+ currentCategoryIsCurated = false;
3035
3067
  }
3036
3068
  continue;
3037
3069
  }
@@ -3047,7 +3079,7 @@ function parseScssTokens(content, filePath = "tokens.scss") {
3047
3079
  tokens.push({
3048
3080
  name,
3049
3081
  value: cleanValue || void 0,
3050
- category: hasCommentCategories ? currentCategory : inferTokenGroup(name),
3082
+ category: currentCategoryIsCurated ? currentCategory : inferTokenGroup(name),
3051
3083
  description
3052
3084
  });
3053
3085
  }
@@ -3058,7 +3090,7 @@ function parseScssTokens(content, filePath = "tokens.scss") {
3058
3090
  tokens.push({
3059
3091
  name,
3060
3092
  value,
3061
- category: hasCommentCategories ? currentCategory : inferTokenGroup(name)
3093
+ category: currentCategoryIsCurated ? currentCategory : inferTokenGroup(name)
3062
3094
  });
3063
3095
  }
3064
3096
  const cssVarValues = /* @__PURE__ */ new Map();
@@ -5057,11 +5089,15 @@ function ruleComponentsPreferLibrary(ix) {
5057
5089
  const textByNode = indexTextChildrenByNodeId(ix);
5058
5090
  const classTokensByNode = indexClassTokensByNode(ix);
5059
5091
  const reimplTargets = canonicalReimplTargets(sources, mappings);
5092
+ const canonicalDirectoryResolvedNodes = indexCanonicalDirectoryResolvedNodes(ix, sources);
5093
+ const shadowRenderRootNodeIds = indexShadowRenderRootNodeIds(ix);
5060
5094
  const findings = [];
5061
5095
  const seenImportFixes = /* @__PURE__ */ new Set();
5062
5096
  for (const node of ix.byKind("usage_node")) {
5063
5097
  if (isCanonicalSourceImplementationFile(node.file, sources)) continue;
5064
5098
  if (node.element.includes(".")) continue;
5099
+ if (canonicalDirectoryResolvedNodes.has(node.id)) continue;
5100
+ if (shadowRenderRootNodeIds.has(node.id)) continue;
5065
5101
  const props = propsByNode.get(node.id) ?? [];
5066
5102
  const textChildren = textByNode.get(node.id) ?? [];
5067
5103
  const importsForFile = importsByFileAndLocal.get(node.file);
@@ -5309,6 +5345,18 @@ function ruleComponentsPreferLibrary(ix) {
5309
5345
  }
5310
5346
  return findings;
5311
5347
  }
5348
+ function indexShadowRenderRootNodeIds(ix) {
5349
+ const identities = ix.byKind("component_identity");
5350
+ if (identities.length === 0) return /* @__PURE__ */ new Set();
5351
+ const nodeIds = /* @__PURE__ */ new Set();
5352
+ for (const identity of identities) {
5353
+ if (identity.state !== "shadow") continue;
5354
+ for (const evidenceId of identity.evidence) {
5355
+ if (ix.get(evidenceId)?.kind === "usage_node") nodeIds.add(evidenceId);
5356
+ }
5357
+ }
5358
+ return nodeIds;
5359
+ }
5312
5360
  function findBuiltInRawHtmlMatch(node, props, textChildren, inputType) {
5313
5361
  if (isIconOnlyButton(node, props, textChildren)) {
5314
5362
  return { canonical: "IconButton", tier: "icon-only" };
@@ -5587,7 +5635,27 @@ function sourceLabel(source) {
5587
5635
  function canonicalImportPath(source) {
5588
5636
  if (source.kind === "npm") return source.specifier;
5589
5637
  if (source.kind === "registry") return source.importPath;
5590
- return void 0;
5638
+ return source.include?.length ? source.path : void 0;
5639
+ }
5640
+ function indexCanonicalDirectoryResolvedNodes(ix, sources) {
5641
+ const out = /* @__PURE__ */ new Set();
5642
+ const directoryPaths = sources.filter(
5643
+ (source) => source.kind === "directory"
5644
+ ).map((source) => normalizePath(source.path));
5645
+ if (directoryPaths.length === 0) return out;
5646
+ for (const fact of ix.byKind("usage_component")) {
5647
+ const module = componentIdModule(fact.componentId);
5648
+ if (module === void 0) continue;
5649
+ const normalizedModule = normalizePath(module);
5650
+ if (directoryPaths.some((path) => isPathInSource(normalizedModule, path))) {
5651
+ out.add(fact.nodeId);
5652
+ }
5653
+ }
5654
+ return out;
5655
+ }
5656
+ function componentIdModule(componentId2) {
5657
+ const hash = componentId2.indexOf("#");
5658
+ return hash > 0 ? componentId2.slice(0, hash) : void 0;
5591
5659
  }
5592
5660
  function isCanonicalSourceImplementationFile(file, sources) {
5593
5661
  const normalizedFile = normalizePath(file);
@@ -5615,9 +5683,95 @@ function isIdentifier(value) {
5615
5683
  return /^[A-Za-z_$][\w$]*$/.test(value);
5616
5684
  }
5617
5685
 
5618
- // src/rules/components-forbidden-prop-value.ts
5619
- var RULE_ID3 = "components/forbidden-prop-value";
5686
+ // src/identity/usage.ts
5687
+ function indexComponentIdentityUsage(ix) {
5688
+ const nodesById = new Map(ix.byKind("usage_node").map((node) => [node.id, node]));
5689
+ const nodesByComponent = /* @__PURE__ */ new Map();
5690
+ for (const usage of ix.byKind("usage_component")) {
5691
+ const node = nodesById.get(usage.nodeId);
5692
+ if (!node) continue;
5693
+ const componentKey = String(usage.componentId);
5694
+ const nodes = nodesByComponent.get(componentKey) ?? /* @__PURE__ */ new Map();
5695
+ nodes.set(node.id, node);
5696
+ nodesByComponent.set(componentKey, nodes);
5697
+ }
5698
+ return new Map(
5699
+ [...nodesByComponent].map(([componentKey, nodesByIdForComponent]) => {
5700
+ const nodes = [...nodesByIdForComponent.values()];
5701
+ return [
5702
+ componentKey,
5703
+ {
5704
+ usageCount: nodes.length,
5705
+ blastRadius: new Set(nodes.map((node) => node.file)).size,
5706
+ nodes
5707
+ }
5708
+ ];
5709
+ })
5710
+ );
5711
+ }
5712
+
5713
+ // src/rules/components-shadow-component.ts
5714
+ var RULE_ID3 = "components/shadow-component";
5620
5715
  var RULE_VERSION3 = "1";
5716
+ function ruleComponentsShadowComponent(ix) {
5717
+ const policy = ix.policy.ruleConfig(RULE_ID3);
5718
+ if (!policy?.enabled) return [];
5719
+ const definitions = new Map(
5720
+ ix.byKind("component_definition").map((definition) => [definition.componentKey, definition])
5721
+ );
5722
+ const usageByComponent = indexComponentIdentityUsage(ix);
5723
+ const findings = [];
5724
+ for (const identity of ix.byKind("component_identity")) {
5725
+ const reviewVariant = identity.state === "variant" && identity.confidence === "review";
5726
+ if (identity.state !== "shadow" && !reviewVariant) continue;
5727
+ const definition = definitions.get(identity.componentKey);
5728
+ if (!definition) continue;
5729
+ const usage = usageByComponent.get(identity.componentKey);
5730
+ const usageCount = usage?.usageCount ?? 0;
5731
+ const blastRadius = usage?.blastRadius ?? 0;
5732
+ findings.push(
5733
+ makeFinding({
5734
+ ruleId: RULE_ID3,
5735
+ ruleVersion: RULE_VERSION3,
5736
+ severity: reviewVariant ? "info" : policy.severity ?? "warn",
5737
+ message: reviewVariant ? `${definition.exportName} wraps ${identity.canonicalTarget ?? "a canonical component"}; sanction it as a variant or consolidate its usages.` : `${definition.exportName} shadows ${identity.canonicalTarget ?? "a canonical component"} across ${usageCount} usage${usageCount === 1 ? "" : "s"}.`,
5738
+ location: definitionLocation(ix, definition, identity),
5739
+ evidence: ix.evidence(identity.evidence),
5740
+ fingerprintIdentity: {
5741
+ componentKey: identity.componentKey,
5742
+ canonicalTarget: identity.canonicalTarget
5743
+ },
5744
+ attributes: {
5745
+ canonicalTarget: identity.canonicalTarget,
5746
+ usageCount,
5747
+ blastRadius,
5748
+ confidence: identity.confidence,
5749
+ state: identity.state,
5750
+ ...reviewVariant ? { advisory: true } : {}
5751
+ }
5752
+ })
5753
+ );
5754
+ }
5755
+ return findings;
5756
+ }
5757
+ function definitionLocation(ix, definition, identity) {
5758
+ const evidence = ix.evidence(identity.evidence).map(({ fact }) => fact);
5759
+ const inDefinitionFile = evidence.find(
5760
+ (fact) => "location" in fact && fact.location?.file === definition.file
5761
+ );
5762
+ if (inDefinitionFile && "location" in inDefinitionFile && inDefinitionFile.location) {
5763
+ return inDefinitionFile.location;
5764
+ }
5765
+ const sourceLocated = evidence.find((fact) => "location" in fact && fact.location);
5766
+ if (sourceLocated && "location" in sourceLocated && sourceLocated.location) {
5767
+ return sourceLocated.location;
5768
+ }
5769
+ return { file: definition.file, line: 1, column: 0 };
5770
+ }
5771
+
5772
+ // src/rules/components-forbidden-prop-value.ts
5773
+ var RULE_ID4 = "components/forbidden-prop-value";
5774
+ var RULE_VERSION4 = "1";
5621
5775
  function ruleComponentsForbiddenPropValue(ix) {
5622
5776
  const findings = [];
5623
5777
  const componentByNode = indexComponentByNodeId(ix);
@@ -5647,8 +5801,8 @@ function ruleComponentsForbiddenPropValue(ix) {
5647
5801
  } : void 0;
5648
5802
  findings.push(
5649
5803
  makeFinding({
5650
- ruleId: RULE_ID3,
5651
- ruleVersion: RULE_VERSION3,
5804
+ ruleId: RULE_ID4,
5805
+ ruleVersion: RULE_VERSION4,
5652
5806
  severity: policy.severity,
5653
5807
  message: buildMessage(
5654
5808
  node.element,
@@ -5692,8 +5846,8 @@ function buildMessage(element, prop, value, because, replacement) {
5692
5846
  }
5693
5847
 
5694
5848
  // src/rules/components-unknown-prop.ts
5695
- var RULE_ID4 = "components/unknown-prop";
5696
- var RULE_VERSION4 = "1";
5849
+ var RULE_ID5 = "components/unknown-prop";
5850
+ var RULE_VERSION5 = "1";
5697
5851
  function ruleComponentsUnknownProp(ix) {
5698
5852
  const policy = ix.policy.jsxUnknownPropsForbidden();
5699
5853
  if (!policy) return [];
@@ -5714,8 +5868,8 @@ function ruleComponentsUnknownProp(ix) {
5714
5868
  if (knownNames.has(propUsage.prop)) continue;
5715
5869
  findings.push(
5716
5870
  makeFinding({
5717
- ruleId: RULE_ID4,
5718
- ruleVersion: RULE_VERSION4,
5871
+ ruleId: RULE_ID5,
5872
+ ruleVersion: RULE_VERSION5,
5719
5873
  severity: policy.severity,
5720
5874
  message: `Unknown prop "${propUsage.prop}" on <${node.element}>. Allowed props: ${formatPropList(knownNames)}.`,
5721
5875
  location: node.location,
@@ -5747,7 +5901,7 @@ function formatPropList(names) {
5747
5901
  // src/rules/composition-pattern.ts
5748
5902
  var CARDINALITY_RULE_ID = "composition/cardinality";
5749
5903
  var CO_OCCURRENCE_RULE_ID = "composition/co-occurrence";
5750
- var RULE_VERSION5 = "1";
5904
+ var RULE_VERSION6 = "1";
5751
5905
  function ruleCompositionCardinality(ix) {
5752
5906
  return evaluate(ix, CARDINALITY_RULE_ID, "cardinality");
5753
5907
  }
@@ -5815,7 +5969,7 @@ function checkCardinality(ctx, region, pattern, constraint, selected) {
5815
5969
  const extras = selected.slice(constraint.max);
5816
5970
  return makeFinding({
5817
5971
  ruleId: ctx.ruleId,
5818
- ruleVersion: RULE_VERSION5,
5972
+ ruleVersion: RULE_VERSION6,
5819
5973
  severity: ctx.severity,
5820
5974
  message: cardinalityMaxMessage(pattern, constraint.max, count),
5821
5975
  location: extras[0].location,
@@ -5826,7 +5980,7 @@ function checkCardinality(ctx, region, pattern, constraint, selected) {
5826
5980
  if (constraint.min !== void 0 && count < constraint.min) {
5827
5981
  return makeFinding({
5828
5982
  ruleId: ctx.ruleId,
5829
- ruleVersion: RULE_VERSION5,
5983
+ ruleVersion: RULE_VERSION6,
5830
5984
  severity: ctx.severity,
5831
5985
  message: cardinalityMinMessage(pattern, constraint.min, count),
5832
5986
  location: region.container.location,
@@ -5847,7 +6001,7 @@ function checkCoOccurrence(ctx, region, pattern, constraint, selected) {
5847
6001
  if (satisfied) return null;
5848
6002
  return makeFinding({
5849
6003
  ruleId: ctx.ruleId,
5850
- ruleVersion: RULE_VERSION5,
6004
+ ruleVersion: RULE_VERSION6,
5851
6005
  severity: ctx.severity,
5852
6006
  message: coOccurrenceMessage(pattern, constraint),
5853
6007
  location: region.container.location,
@@ -5899,8 +6053,8 @@ function coOccurrenceMessage(pattern, constraint) {
5899
6053
  }
5900
6054
 
5901
6055
  // src/rules/jsx-preferred-component.ts
5902
- var RULE_ID5 = "components/preferred-component";
5903
- var RULE_VERSION6 = "1";
6056
+ var RULE_ID6 = "components/preferred-component";
6057
+ var RULE_VERSION7 = "1";
5904
6058
  function ruleJsxPreferredComponent(ix) {
5905
6059
  const policies = ix.policy.jsxComponentPreferred();
5906
6060
  if (policies.length === 0) return [];
@@ -5916,8 +6070,8 @@ function ruleJsxPreferredComponent(ix) {
5916
6070
  if (!replacement) continue;
5917
6071
  findings.push(
5918
6072
  makeFinding({
5919
- ruleId: RULE_ID5,
5920
- ruleVersion: RULE_VERSION6,
6073
+ ruleId: RULE_ID6,
6074
+ ruleVersion: RULE_VERSION7,
5921
6075
  severity: policy.severity,
5922
6076
  message: `<${node.element}> should use <${replacement}>.`,
5923
6077
  location: node.location,
@@ -5954,8 +6108,8 @@ function componentNameFromId(componentId2) {
5954
6108
  }
5955
6109
 
5956
6110
  // src/rules/jsx-preferred-import-path.ts
5957
- var RULE_ID6 = "imports/preferred-path";
5958
- var RULE_VERSION7 = "1";
6111
+ var RULE_ID7 = "imports/preferred-path";
6112
+ var RULE_VERSION8 = "1";
5959
6113
  function ruleJsxPreferredImportPath(ix) {
5960
6114
  const policies = ix.policy.jsxImportPathPreferred();
5961
6115
  if (policies.length === 0) return [];
@@ -5975,8 +6129,8 @@ function ruleJsxPreferredImportPath(ix) {
5975
6129
  seen.add(key);
5976
6130
  findings.push(
5977
6131
  makeFinding({
5978
- ruleId: RULE_ID6,
5979
- ruleVersion: RULE_VERSION7,
6132
+ ruleId: RULE_ID7,
6133
+ ruleVersion: RULE_VERSION8,
5980
6134
  severity: policy.severity,
5981
6135
  message: `Import from "${usage.source}" should use "${policy.to}".`,
5982
6136
  location: usage.location,
@@ -6011,10 +6165,10 @@ function ruleJsxPreferredImportPath(ix) {
6011
6165
  }
6012
6166
 
6013
6167
  // src/rules/props-invalid-value.ts
6014
- var RULE_ID7 = "props/invalid-value";
6015
- var RULE_VERSION8 = "1";
6168
+ var RULE_ID8 = "props/invalid-value";
6169
+ var RULE_VERSION9 = "1";
6016
6170
  function rulePropsInvalidValue(ix) {
6017
- const policy = ix.policy.ruleConfig(RULE_ID7);
6171
+ const policy = ix.policy.ruleConfig(RULE_ID8);
6018
6172
  if (!policy?.enabled) return [];
6019
6173
  const findings = [];
6020
6174
  const componentByNode = indexComponentByNodeId(ix);
@@ -6043,8 +6197,8 @@ function rulePropsInvalidValue(ix) {
6043
6197
  } : void 0;
6044
6198
  findings.push(
6045
6199
  makeFinding({
6046
- ruleId: RULE_ID7,
6047
- ruleVersion: RULE_VERSION8,
6200
+ ruleId: RULE_ID8,
6201
+ ruleVersion: RULE_VERSION9,
6048
6202
  severity: policy.severity ?? "warn",
6049
6203
  message: buildMessage2(node.element, propUsage.prop, propUsage.value, allowedValues),
6050
6204
  location: propUsage.location ?? node.location,
@@ -6220,14 +6374,15 @@ function srgbGamma(linear) {
6220
6374
  }
6221
6375
 
6222
6376
  // src/rules/styles-no-raw-color.ts
6223
- var RULE_ID8 = "styles/no-raw-color";
6224
- var RULE_VERSION9 = "1";
6377
+ var RULE_ID9 = "styles/no-raw-color";
6378
+ var RULE_VERSION10 = "1";
6225
6379
  function ruleStylesNoRawColor(ix) {
6226
6380
  const policy = ix.policy.rawColorPolicy();
6227
6381
  if (!policy) return [];
6228
6382
  const colorTokens = ix.tokens.byCategory("color");
6229
6383
  const preferLabel = policy.prefer === "token" ? "design token" : "CSS variable";
6230
6384
  const findings = [];
6385
+ const advisorySeverity = policy.severity === "error" ? "warn" : policy.severity;
6231
6386
  for (const decl of ix.byKind("style_declaration")) {
6232
6387
  if (decl.property.startsWith("--") && decl.declaredTokenSource === true) continue;
6233
6388
  const color = detectRawColor(decl.value);
@@ -6238,8 +6393,8 @@ function ruleStylesNoRawColor(ix) {
6238
6393
  const evidenceIds = resolution ? [decl.id, policy.id, resolution.token.id] : [decl.id, policy.id];
6239
6394
  findings.push(
6240
6395
  makeFinding({
6241
- ruleId: RULE_ID8,
6242
- ruleVersion: RULE_VERSION9,
6396
+ ruleId: RULE_ID9,
6397
+ ruleVersion: RULE_VERSION10,
6243
6398
  severity: policy.severity,
6244
6399
  message: colorMessage(color, `\`${decl.property}\``, resolution, preferLabel),
6245
6400
  location: decl.location,
@@ -6267,6 +6422,37 @@ function ruleStylesNoRawColor(ix) {
6267
6422
  }
6268
6423
  const componentByNode = indexComponentByNodeId(ix);
6269
6424
  for (const inline of ix.byKind("usage_inline_style")) {
6425
+ if (inline.valueKind === "dynamic-raw") {
6426
+ const node2 = readUsageNode(ix, inline.nodeId);
6427
+ if (!node2) continue;
6428
+ const componentEvidenceId2 = componentByNode.get(node2.id)?.id;
6429
+ const evidenceIds2 = componentEvidenceId2 ? [node2.id, componentEvidenceId2, inline.id, policy.id] : [node2.id, inline.id, policy.id];
6430
+ findings.push(
6431
+ makeFinding({
6432
+ ruleId: RULE_ID9,
6433
+ ruleVersion: RULE_VERSION10,
6434
+ severity: advisorySeverity,
6435
+ message: `Runtime-constructed raw color \`${inline.value}\` on inline \`${inline.property}\`. Use a ${preferLabel} instead.`,
6436
+ location: node2.location,
6437
+ evidence: ix.evidence(evidenceIds2),
6438
+ fingerprintIdentity: {
6439
+ source: "usage_inline_style_dynamic",
6440
+ file: node2.file,
6441
+ nodePath: node2.nodePath,
6442
+ element: node2.element,
6443
+ property: inline.property,
6444
+ value: inline.value
6445
+ },
6446
+ attributes: {
6447
+ property: inline.property,
6448
+ rawValue: inline.value,
6449
+ source: "jsx",
6450
+ dynamic: true
6451
+ }
6452
+ })
6453
+ );
6454
+ continue;
6455
+ }
6270
6456
  if (inline.valueKind === "css-variable") continue;
6271
6457
  const color = detectRawColor(inline.value);
6272
6458
  if (!color) continue;
@@ -6280,8 +6466,8 @@ function ruleStylesNoRawColor(ix) {
6280
6466
  const evidenceIds = resolution ? [...baseEvidence, resolution.token.id] : baseEvidence;
6281
6467
  findings.push(
6282
6468
  makeFinding({
6283
- ruleId: RULE_ID8,
6284
- ruleVersion: RULE_VERSION9,
6469
+ ruleId: RULE_ID9,
6470
+ ruleVersion: RULE_VERSION10,
6285
6471
  severity: policy.severity,
6286
6472
  message: colorMessage(color, `inline \`${inline.property}\``, resolution, preferLabel),
6287
6473
  location: node.location,
@@ -6307,8 +6493,68 @@ function ruleStylesNoRawColor(ix) {
6307
6493
  })
6308
6494
  );
6309
6495
  }
6496
+ for (const prop of ix.byKind("usage_prop_resolved")) {
6497
+ if (prop.resolution !== "static") continue;
6498
+ if (typeof prop.value !== "string") continue;
6499
+ if (!SVG_PAINT_ATTRS.has(prop.prop)) continue;
6500
+ const color = detectRawColor(prop.value);
6501
+ if (!color) continue;
6502
+ if (isExemptColor(color, policy.except)) continue;
6503
+ const node = readUsageNode(ix, prop.nodeId);
6504
+ if (!node) continue;
6505
+ const componentEvidenceId = componentByNode.get(node.id)?.id;
6506
+ const cssProperty = SVG_PAINT_CSS_PROPERTY[prop.prop] ?? prop.prop;
6507
+ const resolution = resolveColorToken(cssProperty, color, colorTokens);
6508
+ const fixEmission = resolution?.kind === "exact" ? buildTokenFix(cssProperty, prop.value, color, resolution, ix) : void 0;
6509
+ const baseEvidence = componentEvidenceId ? [node.id, componentEvidenceId, prop.id, policy.id] : [node.id, prop.id, policy.id];
6510
+ const evidenceIds = resolution ? [...baseEvidence, resolution.token.id] : baseEvidence;
6511
+ findings.push(
6512
+ makeFinding({
6513
+ ruleId: RULE_ID9,
6514
+ ruleVersion: RULE_VERSION10,
6515
+ severity: advisorySeverity,
6516
+ message: colorMessage(color, `\`${prop.prop}\``, resolution, preferLabel),
6517
+ location: prop.location ?? node.location,
6518
+ evidence: ix.evidence(evidenceIds),
6519
+ fingerprintIdentity: {
6520
+ source: "usage_prop_resolved",
6521
+ file: node.file,
6522
+ nodePath: node.nodePath,
6523
+ element: node.element,
6524
+ property: prop.prop,
6525
+ value: color
6526
+ },
6527
+ fix: fixEmission?.fix,
6528
+ attributes: {
6529
+ property: prop.prop,
6530
+ rawValue: prop.value,
6531
+ color,
6532
+ source: "jsx",
6533
+ suggestedToken: resolution?.token.name,
6534
+ ...resolution ? { tokenMatch: resolution.kind } : {},
6535
+ ...fixEmission?.downgradeReason ? { downgradeReason: fixEmission.downgradeReason } : {}
6536
+ }
6537
+ })
6538
+ );
6539
+ }
6310
6540
  return findings;
6311
6541
  }
6542
+ var SVG_PAINT_ATTRS = /* @__PURE__ */ new Set([
6543
+ "fill",
6544
+ "stroke",
6545
+ "color",
6546
+ "stop-color",
6547
+ "stopColor",
6548
+ "flood-color",
6549
+ "floodColor",
6550
+ "lighting-color",
6551
+ "lightingColor"
6552
+ ]);
6553
+ var SVG_PAINT_CSS_PROPERTY = {
6554
+ stopColor: "stop-color",
6555
+ floodColor: "flood-color",
6556
+ lightingColor: "lighting-color"
6557
+ };
6312
6558
  function resolveColorToken(property, raw, tokens) {
6313
6559
  const exact = findTokenMatch(property, raw, tokens);
6314
6560
  if (exact) return { kind: "exact", token: exact.token, ambiguous: exact.ambiguous };
@@ -6416,8 +6662,8 @@ function escapeRegExp(value) {
6416
6662
  }
6417
6663
 
6418
6664
  // src/rules/styles-no-raw-dimensions.ts
6419
- var RULE_ID9 = "styles/no-raw-dimensions";
6420
- var RULE_VERSION10 = "1";
6665
+ var RULE_ID10 = "styles/no-raw-dimensions";
6666
+ var RULE_VERSION11 = "1";
6421
6667
  function ruleStylesNoRawDimensions(ix) {
6422
6668
  const policy = ix.policy.rawDimensionPolicy();
6423
6669
  if (!policy) return [];
@@ -6437,8 +6683,8 @@ function ruleStylesNoRawDimensions(ix) {
6437
6683
  const fixEmission = dimensionFix(decl.property, resolution, ix);
6438
6684
  findings.push(
6439
6685
  makeFinding({
6440
- ruleId: RULE_ID9,
6441
- ruleVersion: RULE_VERSION10,
6686
+ ruleId: RULE_ID10,
6687
+ ruleVersion: RULE_VERSION11,
6442
6688
  severity: policy.severity,
6443
6689
  message: dimensionMessage(decl.value, decl.property, preferLabel, resolution, false),
6444
6690
  location: decl.location,
@@ -6479,8 +6725,8 @@ function ruleStylesNoRawDimensions(ix) {
6479
6725
  const fixEmission = dimensionFix(inline.property, resolution, ix);
6480
6726
  findings.push(
6481
6727
  makeFinding({
6482
- ruleId: RULE_ID9,
6483
- ruleVersion: RULE_VERSION10,
6728
+ ruleId: RULE_ID10,
6729
+ ruleVersion: RULE_VERSION11,
6484
6730
  severity: policy.severity,
6485
6731
  message: dimensionMessage(inline.value, inline.property, preferLabel, resolution, true),
6486
6732
  location: node.location,
@@ -6750,8 +6996,8 @@ function parseLengthParts(raw) {
6750
6996
  }
6751
6997
 
6752
6998
  // src/rules/styles-no-raw-spacing.ts
6753
- var RULE_ID10 = "styles/no-raw-spacing";
6754
- var RULE_VERSION11 = "1";
6999
+ var RULE_ID11 = "styles/no-raw-spacing";
7000
+ var RULE_VERSION12 = "1";
6755
7001
  function ruleStylesNoRawSpacing(ix) {
6756
7002
  const findings = [];
6757
7003
  const lookupCache = /* @__PURE__ */ new Map();
@@ -6793,8 +7039,8 @@ function checkDeclaration(ix, decl, lookupFor) {
6793
7039
  if (!checked) return null;
6794
7040
  const fixEmission = buildSpacingFix(decl.property, checked, ix);
6795
7041
  return makeFinding({
6796
- ruleId: RULE_ID10,
6797
- ruleVersion: RULE_VERSION11,
7042
+ ruleId: RULE_ID11,
7043
+ ruleVersion: RULE_VERSION12,
6798
7044
  severity: policy.severity,
6799
7045
  message: spacingMessage(decl.property, decl.value, allowed, scale.unit, checked),
6800
7046
  location: decl.location,
@@ -6848,8 +7094,8 @@ function checkInlineStyle(ix, inline, componentByNode, lookupFor) {
6848
7094
  const componentEvidenceId = componentByNode.get(node.id)?.id;
6849
7095
  const evidenceIds = componentEvidenceId ? [node.id, componentEvidenceId, inline.id, policy.id, scale.id] : [node.id, inline.id, policy.id, scale.id];
6850
7096
  return makeFinding({
6851
- ruleId: RULE_ID10,
6852
- ruleVersion: RULE_VERSION11,
7097
+ ruleId: RULE_ID11,
7098
+ ruleVersion: RULE_VERSION12,
6853
7099
  severity: policy.severity,
6854
7100
  message: spacingMessage(inline.property, inline.value, allowed, scale.unit, checked),
6855
7101
  location: node.location,
@@ -6910,8 +7156,8 @@ function spacingMessage(property, value, allowed, unit, checked) {
6910
7156
  }
6911
7157
 
6912
7158
  // src/rules/styles-no-raw-typography.ts
6913
- var RULE_ID11 = "styles/no-raw-typography";
6914
- var RULE_VERSION12 = "1";
7159
+ var RULE_ID12 = "styles/no-raw-typography";
7160
+ var RULE_VERSION13 = "1";
6915
7161
  var FONT_SIZE_PROPERTIES = /* @__PURE__ */ new Set(["font-size", "fontsize"]);
6916
7162
  function ruleStylesNoRawTypography(ix) {
6917
7163
  const policy = ix.policy.fontSizeScale();
@@ -6929,8 +7175,8 @@ function ruleStylesNoRawTypography(ix) {
6929
7175
  const fixEmission = buildFix(decl.property, checked, ix);
6930
7176
  findings.push(
6931
7177
  makeFinding({
6932
- ruleId: RULE_ID11,
6933
- ruleVersion: RULE_VERSION12,
7178
+ ruleId: RULE_ID12,
7179
+ ruleVersion: RULE_VERSION13,
6934
7180
  severity: policy.severity,
6935
7181
  message: typographyMessage(decl.property, decl.value, allowed, scale.unit, checked),
6936
7182
  location: decl.location,
@@ -6976,8 +7222,8 @@ function ruleStylesNoRawTypography(ix) {
6976
7222
  const evidenceIds = componentEvidenceId ? [node.id, componentEvidenceId, inline.id, policy.id, scale.id] : [node.id, inline.id, policy.id, scale.id];
6977
7223
  findings.push(
6978
7224
  makeFinding({
6979
- ruleId: RULE_ID11,
6980
- ruleVersion: RULE_VERSION12,
7225
+ ruleId: RULE_ID12,
7226
+ ruleVersion: RULE_VERSION13,
6981
7227
  severity: policy.severity,
6982
7228
  message: typographyMessage(inline.property, inline.value, allowed, scale.unit, checked),
6983
7229
  location: node.location,
@@ -7079,15 +7325,15 @@ function typographyMessage(property, value, allowed, unit, checked) {
7079
7325
  }
7080
7326
 
7081
7327
  // src/rules/theme-no-theme-coupled-literal.ts
7082
- var RULE_ID12 = "theme/no-theme-coupled-literal";
7083
- var RULE_VERSION13 = "1";
7328
+ var RULE_ID13 = "theme/no-theme-coupled-literal";
7329
+ var RULE_VERSION14 = "1";
7084
7330
  var COLOR_LITERAL_GLOBAL = /#[0-9a-fA-F]{3,8}\b|\b(?:rgba?|hsla?|oklab|oklch|lab|lch)\(\s*[\d.%\s,\/]+\s*\)/g;
7085
7331
  var WHITE_RGBA = /^rgba?\(\s*255\s*,\s*255\s*,\s*255/i;
7086
7332
  var BLACK_RGBA = /^rgba?\(\s*0\s*,\s*0\s*,\s*0/i;
7087
7333
  var BOX_SHADOW_PROP = /^(?:-webkit-|-moz-)?box-shadow$/i;
7088
7334
  var NEUTRAL_SHADOW = /^(?:none|inherit|initial|unset|revert|revert-layer)\s*$/i;
7089
7335
  function ruleThemeNoThemeCoupledLiteral(ix) {
7090
- const policy = ix.policy.ruleConfig(RULE_ID12);
7336
+ const policy = ix.policy.ruleConfig(RULE_ID13);
7091
7337
  if (!policy?.enabled) return [];
7092
7338
  const findings = [];
7093
7339
  for (const decl of ix.byKind("style_declaration")) {
@@ -7096,8 +7342,8 @@ function ruleThemeNoThemeCoupledLiteral(ix) {
7096
7342
  if (!hit) continue;
7097
7343
  findings.push(
7098
7344
  makeFinding({
7099
- ruleId: RULE_ID12,
7100
- ruleVersion: RULE_VERSION13,
7345
+ ruleId: RULE_ID13,
7346
+ ruleVersion: RULE_VERSION14,
7101
7347
  severity: policy.severity ?? "warn",
7102
7348
  message: hit.message,
7103
7349
  location: decl.location,
@@ -7187,8 +7433,8 @@ function isTokenFile(filePath) {
7187
7433
  }
7188
7434
 
7189
7435
  // src/rules/tailwind-arbitrary-color.ts
7190
- var RULE_ID13 = "tailwind/arbitrary-color";
7191
- var RULE_VERSION14 = "1";
7436
+ var RULE_ID14 = "tailwind/arbitrary-color";
7437
+ var RULE_VERSION15 = "1";
7192
7438
  var COLOR_UTILITIES = /* @__PURE__ */ new Set([
7193
7439
  "accent",
7194
7440
  "bg",
@@ -7230,8 +7476,8 @@ function ruleTailwindArbitraryColor(ix) {
7230
7476
  const evidenceIds = node ? [node.id, klass.id, policy.id] : [klass.id, policy.id];
7231
7477
  findings.push(
7232
7478
  makeFinding({
7233
- ruleId: RULE_ID13,
7234
- ruleVersion: RULE_VERSION14,
7479
+ ruleId: RULE_ID14,
7480
+ ruleVersion: RULE_VERSION15,
7235
7481
  severity: policy.severity,
7236
7482
  message: `\`${klass.raw}\` uses arbitrary raw color ${color}. Use a ${policy.prefer === "token" ? "design token" : "CSS variable"} instead.`,
7237
7483
  location: klass.location,
@@ -7262,8 +7508,8 @@ function fingerprintIdentity(klass, color) {
7262
7508
  }
7263
7509
 
7264
7510
  // src/rules/tailwind-arbitrary-spacing.ts
7265
- var RULE_ID14 = "tailwind/arbitrary-spacing";
7266
- var RULE_VERSION15 = "1";
7511
+ var RULE_ID15 = "tailwind/arbitrary-spacing";
7512
+ var RULE_VERSION16 = "1";
7267
7513
  var SPACING_UTILITY_TO_PROPERTY = /* @__PURE__ */ new Map([
7268
7514
  ["m", "margin"],
7269
7515
  ["mt", "margin"],
@@ -7309,8 +7555,8 @@ function ruleTailwindArbitrarySpacing(ix) {
7309
7555
  const fix = buildFix2(klass);
7310
7556
  findings.push(
7311
7557
  makeFinding({
7312
- ruleId: RULE_ID14,
7313
- ruleVersion: RULE_VERSION15,
7558
+ ruleId: RULE_ID15,
7559
+ ruleVersion: RULE_VERSION16,
7314
7560
  severity: policy.severity,
7315
7561
  message: `\`${klass.raw}\` is an arbitrary spacing value not on the project's spacing scale.`,
7316
7562
  location: klass.location,
@@ -7441,8 +7687,8 @@ function matchesTokenGlob(value, pattern) {
7441
7687
  }
7442
7688
 
7443
7689
  // src/rules/tailwind-forbidden-palette.ts
7444
- var RULE_ID15 = "tailwind/forbidden-palette";
7445
- var RULE_VERSION16 = "1";
7690
+ var RULE_ID16 = "tailwind/forbidden-palette";
7691
+ var RULE_VERSION17 = "1";
7446
7692
  function ruleTailwindForbiddenPalette(ix) {
7447
7693
  const deny = ix.policy.tailwindPaletteDeny();
7448
7694
  const allow = ix.policy.tailwindPaletteAllow();
@@ -7467,8 +7713,8 @@ function ruleTailwindForbiddenPalette(ix) {
7467
7713
  const evidenceIds = node ? [node.id, klass.id, resolved.id, policy.id] : [klass.id, resolved.id, policy.id];
7468
7714
  findings.push(
7469
7715
  makeFinding({
7470
- ruleId: RULE_ID15,
7471
- ruleVersion: RULE_VERSION16,
7716
+ ruleId: RULE_ID16,
7717
+ ruleVersion: RULE_VERSION17,
7472
7718
  severity: policy.severity,
7473
7719
  message: messageFor(klass, token, deniedPattern, allow?.patterns ?? []),
7474
7720
  location: klass.location,
@@ -7515,8 +7761,8 @@ function buildFix3(klass) {
7515
7761
  }
7516
7762
 
7517
7763
  // src/rules/tailwind-off-scale-spacing-token.ts
7518
- var RULE_ID16 = "tailwind/off-scale-spacing-token";
7519
- var RULE_VERSION17 = "1";
7764
+ var RULE_ID17 = "tailwind/off-scale-spacing-token";
7765
+ var RULE_VERSION18 = "1";
7520
7766
  function ruleTailwindOffScaleSpacingToken(ix) {
7521
7767
  const resolvedByKey = indexResolvedTailwindTokens(ix);
7522
7768
  const findings = [];
@@ -7542,8 +7788,8 @@ function ruleTailwindOffScaleSpacingToken(ix) {
7542
7788
  const evidenceIds = node ? [node.id, klass.id, resolved.id, policy.id, scale.id] : [klass.id, resolved.id, policy.id, scale.id];
7543
7789
  findings.push(
7544
7790
  makeFinding({
7545
- ruleId: RULE_ID16,
7546
- ruleVersion: RULE_VERSION17,
7791
+ ruleId: RULE_ID17,
7792
+ ruleVersion: RULE_VERSION18,
7547
7793
  severity: policy.severity,
7548
7794
  message: `\`${klass.raw}\` resolves to ${formatResolvedValue(signedValue, scale.unit)}, which is not on the project's spacing scale [${allowed.join(", ")}].`,
7549
7795
  location: klass.location,
@@ -7592,8 +7838,8 @@ function buildFix4(raw, utility) {
7592
7838
  }
7593
7839
 
7594
7840
  // src/rules/tailwind-raw-color-via-token.ts
7595
- var RULE_ID17 = "tailwind/raw-color-via-token";
7596
- var RULE_VERSION18 = "1";
7841
+ var RULE_ID18 = "tailwind/raw-color-via-token";
7842
+ var RULE_VERSION19 = "1";
7597
7843
  function ruleTailwindRawColorViaToken(ix) {
7598
7844
  const allow = ix.policy.tailwindPaletteAllow();
7599
7845
  if (!allow || allow.patterns.length === 0) return [];
@@ -7613,8 +7859,8 @@ function ruleTailwindRawColorViaToken(ix) {
7613
7859
  const evidenceIds = node ? [node.id, klass.id, resolved.id, allow.id] : [klass.id, resolved.id, allow.id];
7614
7860
  findings.push(
7615
7861
  makeFinding({
7616
- ruleId: RULE_ID17,
7617
- ruleVersion: RULE_VERSION18,
7862
+ ruleId: RULE_ID18,
7863
+ ruleVersion: RULE_VERSION19,
7618
7864
  severity: allow.severity,
7619
7865
  message: `\`${klass.raw}\` resolves to raw color \`${color}\` (${sourceLabel2(resolved.resolved.source)}). Project policy requires \`${allow.patterns.join("`, `")}\` palette tokens.`,
7620
7866
  location: klass.location,
@@ -7647,8 +7893,8 @@ function sourceLabel2(source) {
7647
7893
  }
7648
7894
 
7649
7895
  // src/rules/tailwind-unknown-class.ts
7650
- var RULE_ID18 = "tailwind/unknown-class";
7651
- var RULE_VERSION19 = "1";
7896
+ var RULE_ID19 = "tailwind/unknown-class";
7897
+ var RULE_VERSION20 = "1";
7652
7898
  var UNIVERSAL_COLOR_KEYWORDS = /* @__PURE__ */ new Set([
7653
7899
  "white",
7654
7900
  "black",
@@ -7669,8 +7915,8 @@ function ruleTailwindUnknownClass(ix) {
7669
7915
  const evidenceIds2 = node2 ? [node2.id, klass.id, policy.id] : [klass.id, policy.id];
7670
7916
  findings.push(
7671
7917
  makeFinding({
7672
- ruleId: RULE_ID18,
7673
- ruleVersion: RULE_VERSION19,
7918
+ ruleId: RULE_ID19,
7919
+ ruleVersion: RULE_VERSION20,
7674
7920
  severity: policy.severity,
7675
7921
  message: `\`${klass.raw}\` could not be parsed as a known Tailwind utility.`,
7676
7922
  location: klass.location,
@@ -7704,8 +7950,8 @@ function ruleTailwindUnknownClass(ix) {
7704
7950
  const evidenceIds = node ? [node.id, klass.id, resolved.id, policy.id] : [klass.id, resolved.id, policy.id];
7705
7951
  findings.push(
7706
7952
  makeFinding({
7707
- ruleId: RULE_ID18,
7708
- ruleVersion: RULE_VERSION19,
7953
+ ruleId: RULE_ID19,
7954
+ ruleVersion: RULE_VERSION20,
7709
7955
  severity: policy.severity,
7710
7956
  message: `\`${klass.raw}\` did not resolve to any known palette token.`,
7711
7957
  location: klass.location,
@@ -7733,12 +7979,12 @@ function ruleTailwindUnknownClass(ix) {
7733
7979
  }
7734
7980
 
7735
7981
  // src/rules/tokens-require-dual-fallback.ts
7736
- var RULE_ID19 = "tokens/require-dual-fallback";
7737
- var RULE_VERSION20 = "1";
7982
+ var RULE_ID20 = "tokens/require-dual-fallback";
7983
+ var RULE_VERSION21 = "1";
7738
7984
  var CSS_VAR_WITH_OPTIONAL_FALLBACK = /var\(\s*(--[A-Za-z0-9_-]+)(\s*,\s*[^)]*)?\)/g;
7739
7985
  var SASS_FILE = /\.(scss|sass)$/i;
7740
7986
  function ruleTokensRequireDualFallback(ix) {
7741
- const policy = ix.policy.ruleConfig(RULE_ID19);
7987
+ const policy = ix.policy.ruleConfig(RULE_ID20);
7742
7988
  if (!policy?.enabled) return [];
7743
7989
  const tokensByCssVariable = indexTokensByCssVariable(ix.tokens.list());
7744
7990
  const findings = [];
@@ -7773,8 +8019,8 @@ function ruleTokensRequireDualFallback(ix) {
7773
8019
  ) : void 0;
7774
8020
  findings.push(
7775
8021
  makeFinding({
7776
- ruleId: RULE_ID19,
7777
- ruleVersion: RULE_VERSION20,
8022
+ ruleId: RULE_ID20,
8023
+ ruleVersion: RULE_VERSION21,
7778
8024
  severity: policy.severity ?? "warn",
7779
8025
  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}.`,
7780
8026
  location: decl.location,
@@ -7814,8 +8060,8 @@ function indexTokensByCssVariable(tokens) {
7814
8060
  }
7815
8061
 
7816
8062
  // src/rules/tokens-css-vars-must-be-defined.ts
7817
- var RULE_ID20 = "tokens/css-vars-must-be-defined";
7818
- var RULE_VERSION21 = "1";
8063
+ var RULE_ID21 = "tokens/css-vars-must-be-defined";
8064
+ var RULE_VERSION22 = "1";
7819
8065
  var CSS_VAR_REFERENCE = /var\(\s*(--[A-Za-z0-9_-]+)/gi;
7820
8066
  function ruleTokensCssVarsMustBeDefined(ix) {
7821
8067
  const policy = ix.policy.cssVarsMustBeDefined();
@@ -7842,8 +8088,8 @@ function ruleTokensCssVarsMustBeDefined(ix) {
7842
8088
  seen.add(tokenName);
7843
8089
  findings.push(
7844
8090
  makeFinding({
7845
- ruleId: RULE_ID20,
7846
- ruleVersion: RULE_VERSION21,
8091
+ ruleId: RULE_ID21,
8092
+ ruleVersion: RULE_VERSION22,
7847
8093
  severity: policy.severity,
7848
8094
  message: `var(${tokenName}) is not in the contract token vocabulary. Use a token defined in your contract's token source files.`,
7849
8095
  location: decl.location,
@@ -7923,6 +8169,8 @@ function isDenyEligible(finding, failOnWarnings) {
7923
8169
  // src/rules/tiers.ts
7924
8170
  var RULE_TIER = {
7925
8171
  // ---- Tier A — contract vocabulary -------------------------------------
8172
+ "components/shadow-component": "contract",
8173
+ // canonical-component identity drift
7926
8174
  "components/prefer-library": "contract",
7927
8175
  // canonical-component bypass (FUI1004)
7928
8176
  "tokens/css-vars-must-be-defined": "contract",
@@ -7968,6 +8216,7 @@ function hygieneTierRuleIds() {
7968
8216
  return Object.keys(RULE_TIER).filter((id) => !isContractTierRule(id));
7969
8217
  }
7970
8218
  var VOCABULARY_ORDER = [
8219
+ "components/shadow-component",
7971
8220
  "components/prefer-library",
7972
8221
  "tokens/css-vars-must-be-defined"
7973
8222
  ];
@@ -8028,6 +8277,11 @@ var RULES = [
8028
8277
  version: "1",
8029
8278
  run: ruleComponentsPreferLibrary
8030
8279
  },
8280
+ {
8281
+ id: "components/shadow-component",
8282
+ version: "1",
8283
+ run: ruleComponentsShadowComponent
8284
+ },
8031
8285
  {
8032
8286
  id: "styles/no-raw-color",
8033
8287
  version: "1",
@@ -8622,6 +8876,133 @@ function readPreferLibraryRule(config) {
8622
8876
  const raw = config?.rules?.["components/prefer-library"];
8623
8877
  return raw && typeof raw === "object" && !Array.isArray(raw) ? raw : {};
8624
8878
  }
8879
+
8880
+ // src/identity/component-key.ts
8881
+ function buildComponentKey({ repoRelativeFile, exportName }) {
8882
+ const file = normalizeRepoRelativeFile(repoRelativeFile);
8883
+ const name = exportName.trim();
8884
+ if (!file) throw new Error("repoRelativeFile must not be empty");
8885
+ if (!name) throw new Error("exportName must not be empty");
8886
+ return `${file}#${name}`;
8887
+ }
8888
+ function normalizeRepoRelativeFile(file) {
8889
+ const slashNormalized = file.trim().replace(/\\/g, "/").replace(/^\.\/+/, "");
8890
+ const absolute = slashNormalized.startsWith("/");
8891
+ const parts = [];
8892
+ for (const part of slashNormalized.split("/")) {
8893
+ if (!part || part === ".") continue;
8894
+ if (part === ".." && parts.length > 0 && parts.at(-1) !== "..") {
8895
+ parts.pop();
8896
+ continue;
8897
+ }
8898
+ parts.push(part);
8899
+ }
8900
+ return `${absolute ? "/" : ""}${parts.join("/")}`;
8901
+ }
8902
+
8903
+ // src/identity/classify.ts
8904
+ function classifyIdentity(input) {
8905
+ const { definition, renderRoot } = input;
8906
+ if (definition.canonical) return { state: "canonical", confidence: "confirmed" };
8907
+ const decision = input.decisions?.find(
8908
+ (candidate) => candidate.componentKey === definition.componentKey
8909
+ );
8910
+ if (decision?.decision === "sanctioned-wrapper") {
8911
+ return withTarget("variant", "confirmed", decision.canonicalTarget ?? rootTarget(input));
8912
+ }
8913
+ if (decision?.decision === "rejected-wrapper") {
8914
+ return withTarget("shadow", "confirmed", decision.canonicalTarget ?? shadowTarget(input));
8915
+ }
8916
+ if (renderRoot.kind === "canonical") {
8917
+ if ((input.usageCounts?.nestedElements ?? 0) > 0 || (input.usageCounts?.staticTextChildren ?? 0) > 0) {
8918
+ return withTarget("composite", "confirmed", renderRoot.target.componentKey);
8919
+ }
8920
+ const confidence = strongOverlap(input.propSurfaceOverlap) ? "likely" : "review";
8921
+ return withTarget("variant", confidence, renderRoot.target.componentKey);
8922
+ }
8923
+ if (renderRoot.kind === "intrinsic") {
8924
+ return classifyIntrinsic(input, renderRoot);
8925
+ }
8926
+ if (renderRoot.kind === "mixed") {
8927
+ if (renderRoot.canonicalTargets.length > 0) {
8928
+ const target = bestCanonicalReference(
8929
+ definition.exportName,
8930
+ renderRoot.canonicalTargets
8931
+ )?.componentKey;
8932
+ const fullyResolved = renderRoot.externalTargets.length === 0 && renderRoot.unresolvedNames.length === 0;
8933
+ return withTarget("composite", fullyResolved ? "confirmed" : "likely", target);
8934
+ }
8935
+ if (renderRoot.externalTargets.length > 0 && renderRoot.localTargets.length === 0 && renderRoot.unresolvedNames.length === 0) {
8936
+ return { state: "external", confidence: "likely" };
8937
+ }
8938
+ return withTarget("unresolved", "review", rootTarget(input));
8939
+ }
8940
+ if (renderRoot.kind === "external") {
8941
+ return { state: "external", confidence: "confirmed" };
8942
+ }
8943
+ return withTarget("unresolved", "review", rootTarget(input));
8944
+ }
8945
+ function classifyIntrinsic(input, root) {
8946
+ const target = bestCanonicalReference(input.definition.exportName, root.canonicalTargets);
8947
+ if (!target || !root.interactive && !root.hasRole) {
8948
+ return withTarget("unresolved", "review", rootTarget(input));
8949
+ }
8950
+ let confidence = root.precisionTier === "exact-html" || root.precisionTier === "role-reimpl" ? "confirmed" : "likely";
8951
+ if (confidence === "likely" && input.stylingOwnership !== "layout-only" && (strongOverlap(input.propSurfaceOverlap) || input.stylingOwnership === "visual-role")) {
8952
+ confidence = "confirmed";
8953
+ }
8954
+ return { state: "shadow", confidence, canonicalTarget: target.role };
8955
+ }
8956
+ function rootTarget(input) {
8957
+ const { renderRoot } = input;
8958
+ if (renderRoot.kind === "canonical") return renderRoot.target.componentKey;
8959
+ if (renderRoot.kind === "intrinsic") {
8960
+ return bestCanonicalReference(input.definition.exportName, renderRoot.canonicalTargets)?.role;
8961
+ }
8962
+ if (renderRoot.kind === "mixed") {
8963
+ return bestCanonicalReference(input.definition.exportName, renderRoot.canonicalTargets)?.componentKey;
8964
+ }
8965
+ return bestMessageTarget(input.definition.exportName, input.nameSimilarityTargets ?? []);
8966
+ }
8967
+ function shadowTarget(input) {
8968
+ const { renderRoot } = input;
8969
+ if (renderRoot.kind === "canonical") return renderRoot.target.role;
8970
+ if (renderRoot.kind === "intrinsic") {
8971
+ return bestCanonicalReference(input.definition.exportName, renderRoot.canonicalTargets)?.role;
8972
+ }
8973
+ if (renderRoot.kind === "mixed") {
8974
+ return bestCanonicalReference(input.definition.exportName, renderRoot.canonicalTargets)?.role;
8975
+ }
8976
+ return bestMessageTarget(input.definition.exportName, input.nameSimilarityTargets ?? []);
8977
+ }
8978
+ function bestCanonicalReference(definitionName, candidates) {
8979
+ return [...candidates].sort(
8980
+ (left, right) => nameSimilarityScore(definitionName, right.role) - nameSimilarityScore(definitionName, left.role) || left.componentKey.localeCompare(right.componentKey)
8981
+ )[0];
8982
+ }
8983
+ function bestMessageTarget(definitionName, candidates) {
8984
+ return [...candidates].sort(
8985
+ (left, right) => nameSimilarityScore(definitionName, right) - nameSimilarityScore(definitionName, left) || left.localeCompare(right)
8986
+ )[0];
8987
+ }
8988
+ function nameSimilarityScore(definitionName, target) {
8989
+ const definition = normalizeName(definitionName);
8990
+ const candidate = normalizeName(target);
8991
+ if (!candidate) return 0;
8992
+ if (definition === candidate) return 3;
8993
+ if (definition.endsWith(candidate) || definition.startsWith(candidate)) return 2;
8994
+ if (definition.includes(candidate) || candidate.includes(definition)) return 1;
8995
+ return 0;
8996
+ }
8997
+ function normalizeName(value) {
8998
+ return value.split(/[.#/]/).at(-1)?.replace(/[^a-z0-9]/gi, "").toLowerCase() ?? "";
8999
+ }
9000
+ function strongOverlap(overlap) {
9001
+ return overlap !== void 0 && overlap >= 0.7;
9002
+ }
9003
+ function withTarget(state, confidence, canonicalTarget) {
9004
+ return canonicalTarget ? { state, confidence, canonicalTarget } : { state, confidence };
9005
+ }
8625
9006
  export {
8626
9007
  AGENT_CONTEXT_RELATIVE_PATH,
8627
9008
  AGENT_FORMAT_SCHEMA_VERSION,
@@ -8672,6 +9053,7 @@ export {
8672
9053
  bridgeSourceViolations,
8673
9054
  budgetBar,
8674
9055
  buildAgentFormat,
9056
+ buildComponentKey,
8675
9057
  buildRegistryArtifact,
8676
9058
  buildRegistryFile,
8677
9059
  buildRegistryInstallPlan,
@@ -8698,6 +9080,7 @@ export {
8698
9080
  checkStoryExclusion,
8699
9081
  clampMaxPasses,
8700
9082
  classifyComplexity,
9083
+ classifyIdentity,
8701
9084
  collectRegistryInstallDependencies,
8702
9085
  colorSimilarity,
8703
9086
  compareByVocabularyRank,
@@ -8793,6 +9176,7 @@ export {
8793
9176
  hashRegistryFileContent,
8794
9177
  hexToRgb,
8795
9178
  hygieneTierRuleIds,
9179
+ indexComponentIdentityUsage,
8796
9180
  inferTokenCategory,
8797
9181
  inferTokenGroup,
8798
9182
  isColorLike,
@@ -8819,6 +9203,8 @@ export {
8819
9203
  makeClassNameDynamicFact,
8820
9204
  makeClassNameLiteralFact,
8821
9205
  makeComponentCapabilityFact,
9206
+ makeComponentDefinitionFact,
9207
+ makeComponentIdentityFact,
8822
9208
  makeComponentMetadataFact,
8823
9209
  makeContractTokenFact,
8824
9210
  makeFinding,
@@ -8943,6 +9329,7 @@ export {
8943
9329
  ruleA11yRequiredAccessibleName,
8944
9330
  ruleComponentsForbiddenPropValue,
8945
9331
  ruleComponentsPreferLibrary,
9332
+ ruleComponentsShadowComponent,
8946
9333
  ruleComponentsUnknownProp,
8947
9334
  ruleJsxPreferredComponent,
8948
9335
  ruleJsxPreferredImportPath,