@usefragments/core 1.5.0 → 1.5.2

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 (65) hide show
  1. package/dist/{chunk-HXGE3O2F.js → chunk-BAHCOAVG.js} +133 -7
  2. package/dist/chunk-BAHCOAVG.js.map +1 -0
  3. package/dist/{chunk-57QDBEHQ.js → chunk-ZHS52OT4.js} +3 -9
  4. package/dist/chunk-ZHS52OT4.js.map +1 -0
  5. package/dist/codes/index.d.ts +2 -2
  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-BLsyk56o.d.ts → governance-pKrfh517.d.ts} +264 -4
  10. package/dist/{index-h_yWj15D.d.ts → index-DbkPE46t.d.ts} +40 -0
  11. package/dist/index.d.ts +472 -28
  12. package/dist/index.js +561 -39
  13. package/dist/index.js.map +1 -1
  14. package/dist/react-types.d.ts +1 -1
  15. package/dist/schemas/index.d.ts +1 -1
  16. package/dist/schemas/index.js +1 -1
  17. package/dist/test-utils.d.ts +1 -1
  18. package/package.json +1 -1
  19. package/src/agent-format.test.ts +1 -0
  20. package/src/canonical-bridge.ts +46 -0
  21. package/src/canonical-direction.ts +118 -0
  22. package/src/codes/__tests__/codes.test.ts +1 -1
  23. package/src/codes/codes.ts +29 -0
  24. package/src/config.ts +20 -0
  25. package/src/contract/preimage.test.ts +40 -0
  26. package/src/contract/preimage.ts +20 -0
  27. package/src/effective-governance-inputs.test.ts +52 -0
  28. package/src/effective-governance-inputs.ts +106 -0
  29. package/src/facts/builders.ts +25 -1
  30. package/src/facts/compile.ts +20 -2
  31. package/src/facts/fact-index.ts +12 -1
  32. package/src/facts/facts.test.ts +14 -18
  33. package/src/facts/types.ts +25 -6
  34. package/src/governance-integrity.test.ts +127 -0
  35. package/src/governance-integrity.ts +326 -4
  36. package/src/governance.test.ts +87 -1
  37. package/src/governance.ts +121 -0
  38. package/src/index.ts +34 -5
  39. package/src/rules/__tests__/fix-emission-invariant.test.ts +35 -4
  40. package/src/rules/components-prefer-library.test.ts +103 -0
  41. package/src/rules/components-prefer-library.ts +30 -4
  42. package/src/rules/fix-availability.ts +1 -0
  43. package/src/rules/index.ts +7 -0
  44. package/src/rules/jsx-preferred-import-path.ts +45 -9
  45. package/src/rules/rules.test.ts +202 -6
  46. package/src/rules/spacing-resolution.ts +2 -2
  47. package/src/rules/styles-no-raw-color.test.ts +6 -1
  48. package/src/rules/styles-no-raw-color.ts +3 -2
  49. package/src/rules/styles-no-raw-dimensions.ts +5 -7
  50. package/src/rules/styles-no-raw-spacing.test.ts +48 -0
  51. package/src/rules/styles-no-raw-spacing.ts +10 -7
  52. package/src/rules/styles-no-raw-typography.ts +7 -4
  53. package/src/rules/taxonomy.test.ts +3 -0
  54. package/src/rules/tiers.ts +2 -0
  55. package/src/rules/token-candidates.ts +36 -0
  56. package/src/rules/tokens-require-dual-fallback.ts +2 -1
  57. package/src/rules/tokens-upstream-drift.test.ts +111 -0
  58. package/src/rules/tokens-upstream-drift.ts +57 -0
  59. package/src/rules/utils.ts +22 -14
  60. package/src/schema.ts +13 -0
  61. package/src/schemas/index.ts +5 -8
  62. package/src/token-types.ts +71 -1
  63. package/src/types.ts +27 -3
  64. package/dist/chunk-57QDBEHQ.js.map +0 -1
  65. package/dist/chunk-HXGE3O2F.js.map +0 -1
package/src/index.ts CHANGED
@@ -227,6 +227,12 @@ export type {
227
227
  // Token types
228
228
  export type {
229
229
  TokenCategory,
230
+ TokenCatalog,
231
+ TokenCatalogDefinition,
232
+ TokenCatalogDiagnostic,
233
+ TokenCatalogLocation,
234
+ TokenCatalogReference,
235
+ TokenCatalogSourceState,
230
236
  DesignToken,
231
237
  TokenRegistry,
232
238
  TokenRegistryMeta,
@@ -419,7 +425,7 @@ export {
419
425
  } from "./schema.js";
420
426
 
421
427
  // Main API
422
- export { defineConfig } from "./config.js";
428
+ export { configDeclarationForDiagnostics, defineConfig } from "./config.js";
423
429
  export {
424
430
  defineFragment,
425
431
  compileFragment,
@@ -435,6 +441,7 @@ export type { InferProps } from "./defineFragment.js";
435
441
  export {
436
442
  componentGovernanceRecordSchema,
437
443
  componentGovernanceRecordsSchema,
444
+ canonicalBridgeV1Schema,
438
445
  g,
439
446
  globalGovernanceRecordSchema,
440
447
  globalJsxGovernanceRecordSchema,
@@ -446,6 +453,7 @@ export {
446
453
  } from "./governance.js";
447
454
  export type {
448
455
  AccessibilityGovernanceBuilder,
456
+ CanonicalBridgeV1,
449
457
  CanonicalSource,
450
458
  ComponentGovernanceBuilder,
451
459
  ComponentGovernanceRecord,
@@ -645,10 +653,7 @@ export {
645
653
  ruleTokensRequireDualFallback,
646
654
  ruleA11yRequiredAccessibleName,
647
655
  } from "./rules/index.js";
648
- export {
649
- indexComponentIdentityUsage,
650
- type ComponentIdentityUsage,
651
- } from "./identity/usage.js";
656
+ export { indexComponentIdentityUsage, type ComponentIdentityUsage } from "./identity/usage.js";
652
657
  export { nearestSignedScaleValue } from "./rules/utils.js";
653
658
  export {
654
659
  buildSpacingTokenLookup,
@@ -708,6 +713,8 @@ export { BLOCKING_RULE_ALLOWLIST, gatesCi, isDenyEligible } from "./rules/index.
708
713
  // Governance integrity — armed-vs-declared verdict over a fully-resolved policy.
709
714
  // Encodes "enabled ≠ armed": a rule with no vocabulary enforces nothing.
710
715
  export {
716
+ detectOrphanGovernanceScales,
717
+ detectUnconsumedConfigKeys,
711
718
  evaluateGovernanceIntegrity,
712
719
  hasEffectiveComponentVocabulary,
713
720
  isEffectiveCanonicalSource,
@@ -716,10 +723,32 @@ export type {
716
723
  GovernanceIntegrityFamily,
717
724
  GovernanceIntegrityFamilyId,
718
725
  GovernanceIntegrityInput,
726
+ GovernanceIntegrityRoster,
719
727
  GovernanceIntegrityStatus,
720
728
  GovernanceIntegrityVerdict,
729
+ InertConfigDiagnostic,
730
+ InertConfigDiagnosticCode,
731
+ InertConfigDiagnosticKind,
721
732
  } from "./governance-integrity.js";
722
733
 
734
+ export {
735
+ canonicalDirectionConflictsTarget,
736
+ projectCanonicalDirectionConflicts,
737
+ } from "./canonical-direction.js";
738
+ export type { CanonicalDirectionConflict } from "./canonical-direction.js";
739
+
740
+ export { canonicalBridgeContractMappings } from "./canonical-bridge.js";
741
+
742
+ export {
743
+ compileEffectiveGovernanceInputs,
744
+ EFFECTIVE_GOVERNANCE_INPUTS_SCHEMA,
745
+ } from "./effective-governance-inputs.js";
746
+ export type {
747
+ CompileEffectiveGovernanceInputsOptions,
748
+ EffectiveGovernanceInputs,
749
+ EffectiveGovernanceInputsSourceState,
750
+ } from "./effective-governance-inputs.js";
751
+
723
752
  // Fact IR + index (Phase 3 of the governance compiler refactor)
724
753
  export {
725
754
  componentId,
@@ -65,6 +65,34 @@ describe("proof-carrying fix invariant", () => {
65
65
  "token reference is not directly applicable"
66
66
  );
67
67
  });
68
+
69
+ it("keeps JavaScript member tokens advisory without inventing a CSS variable", () => {
70
+ const ix = buildFixtureCorpus("js-member", "radii.md");
71
+ const dimension = RULES.find((rule) => rule.id === "styles/no-raw-dimensions")
72
+ ?.run(ix)
73
+ .find((finding) => finding.fix);
74
+
75
+ expect(dimension?.fix).toMatchObject({
76
+ value: "radii.md",
77
+ deterministic: false,
78
+ });
79
+ expect(JSON.stringify(dimension?.fix)).not.toContain("var(--radii-md)");
80
+ expect(dimension?.attributes?.downgradeReason).toContain(
81
+ "token reference is not directly applicable"
82
+ );
83
+ });
84
+
85
+ it("fails closed when a legacy token fact has no authored reference format", () => {
86
+ const ix = buildFixtureCorpus("unknown");
87
+ const dimension = RULES.find((rule) => rule.id === "styles/no-raw-dimensions")
88
+ ?.run(ix)
89
+ .find((finding) => finding.fix);
90
+
91
+ expect(dimension?.fix?.deterministic).toBe(false);
92
+ expect(dimension?.attributes?.downgradeReason).toContain(
93
+ "token reference is not directly applicable"
94
+ );
95
+ });
68
96
  });
69
97
 
70
98
  function runEveryRule(ix: FactIndex): Finding[] {
@@ -98,7 +126,10 @@ function withoutDeterminism(fix: FindingFix): Omit<FindingFix, "deterministic">
98
126
  return rest;
99
127
  }
100
128
 
101
- function buildFixtureCorpus(radiusReferenceFormat: "css-var" | "dtcg" = "css-var"): FactIndex {
129
+ function buildFixtureCorpus(
130
+ radiusReferenceFormat: "css-var" | "dtcg" | "js-member" | "unknown" = "css-var",
131
+ radiusName = "--proof-radius"
132
+ ): FactIndex {
102
133
  const ix = new FactIndex();
103
134
  ix.addMany([
104
135
  makeTokenDefinitionFact({
@@ -116,11 +147,11 @@ function buildFixtureCorpus(radiusReferenceFormat: "css-var" | "dtcg" = "css-var
116
147
  sourceNames: ["--proof-space"],
117
148
  }),
118
149
  makeTokenDefinitionFact({
119
- name: "--proof-radius",
150
+ name: radiusName,
120
151
  value: "4px",
121
152
  category: "radius",
122
- referenceFormat: radiusReferenceFormat,
123
- sourceNames: ["--proof-radius"],
153
+ referenceFormat: radiusReferenceFormat === "unknown" ? undefined : radiusReferenceFormat,
154
+ sourceNames: [radiusName],
124
155
  }),
125
156
  makeTokenDefinitionFact({
126
157
  name: "--proof-font",
@@ -4,6 +4,7 @@ import {
4
4
  asComponentId,
5
5
  compileGlobalGovernanceFacts,
6
6
  FactIndex,
7
+ makeComponentDefinitionFact,
7
8
  makeComponentIdentityFact,
8
9
  makeUsageComponentFact,
9
10
  makeUsageImportFact,
@@ -11,6 +12,108 @@ import {
11
12
  ruleComponentsPreferLibrary,
12
13
  } from "../index.js";
13
14
 
15
+ describe("components/prefer-library — canonical direction containment", () => {
16
+ const componentKey = "src/components/Button.tsx#Button";
17
+
18
+ function indexFor(decision?: "sanction" | "reject", exported = true) {
19
+ const ix = new FactIndex();
20
+ ix.addMany(
21
+ compileGlobalGovernanceFacts({
22
+ rules: {
23
+ "components/prefer-library": {
24
+ enabled: true,
25
+ severity: "warning",
26
+ options: {
27
+ canonicalSources: [
28
+ { kind: "npm", specifier: "@mui/material", include: ["Button", "Stepper"] },
29
+ ],
30
+ },
31
+ },
32
+ },
33
+ })
34
+ );
35
+ const definition = makeComponentDefinitionFact({
36
+ file: "src/components/Button.tsx",
37
+ exportName: "Button",
38
+ exported,
39
+ componentKey,
40
+ renderRoot: {
41
+ resolution: "canonical",
42
+ canonical: "@mui/material#Button",
43
+ importSource: "@mui/material",
44
+ },
45
+ propSurface: [],
46
+ });
47
+ ix.add(definition);
48
+ ix.add(
49
+ makeComponentIdentityFact({
50
+ componentKey,
51
+ state: decision === "reject" ? "shadow" : "variant",
52
+ confidence: decision ? "confirmed" : "review",
53
+ canonicalTarget: "Button",
54
+ ...(decision ? { decisionId: `local:${decision}:${componentKey}` } : {}),
55
+ evidence: [definition.id],
56
+ })
57
+ );
58
+ return ix;
59
+ }
60
+
61
+ function addLocalUsage(ix: FactIndex, name: "Button" | "Stepper") {
62
+ ix.add(
63
+ makeUsageNodeFact({
64
+ file: "src/App.tsx",
65
+ nodePath: `0:${name}`,
66
+ element: name,
67
+ location: { file: "src/App.tsx", line: 3, column: 2 },
68
+ })
69
+ );
70
+ ix.add(
71
+ makeUsageImportFact({
72
+ file: "src/App.tsx",
73
+ local: name,
74
+ imported: name,
75
+ source: `./components/${name}`,
76
+ location: { file: "src/App.tsx", line: 1, column: 0 },
77
+ })
78
+ );
79
+ }
80
+
81
+ it("suppresses only the exact conflicted npm export when direction is unresolved", () => {
82
+ const ix = indexFor();
83
+ addLocalUsage(ix, "Button");
84
+ addLocalUsage(ix, "Stepper");
85
+ ix.add(
86
+ makeUsageNodeFact({
87
+ file: "src/App.tsx",
88
+ nodePath: "0:raw-button",
89
+ element: "button",
90
+ interactive: true,
91
+ location: { file: "src/App.tsx", line: 4, column: 2 },
92
+ })
93
+ );
94
+
95
+ const findings = ruleComponentsPreferLibrary(ix);
96
+ expect(findings).toHaveLength(1);
97
+ expect(findings[0]?.attributes?.suggestedComponent).toBe("Stepper");
98
+ });
99
+
100
+ it("keeps a sanctioned wrapper exempt and restores npm guidance after explicit rejection", () => {
101
+ const sanctioned = indexFor("sanction");
102
+ addLocalUsage(sanctioned, "Button");
103
+ expect(ruleComponentsPreferLibrary(sanctioned)).toHaveLength(0);
104
+
105
+ const rejected = indexFor("reject");
106
+ addLocalUsage(rejected, "Button");
107
+ expect(ruleComponentsPreferLibrary(rejected)).toHaveLength(1);
108
+ });
109
+
110
+ it("does not let a private helper deactivate canonical policy", () => {
111
+ const ix = indexFor(undefined, false);
112
+ addLocalUsage(ix, "Button");
113
+ expect(ruleComponentsPreferLibrary(ix)).toHaveLength(1);
114
+ });
115
+ });
116
+
14
117
  // #9e — import-path identity for a DIRECTORY canonical source. A directory
15
118
  // source (e.g. `src/components/ui`) declares its canonical component NAMES via an
16
119
  // include list. The rule must:
@@ -15,6 +15,11 @@ import {
15
15
  type RawHtmlPrecisionTier,
16
16
  } from "../raw-html-canonical.js";
17
17
  import { ownedImportMatchesRoot, ownedImportsEqual } from "../package-identity-match.js";
18
+ import {
19
+ canonicalDirectionConflictsTarget,
20
+ projectCanonicalDirectionConflicts,
21
+ type CanonicalDirectionConflict,
22
+ } from "../canonical-direction.js";
18
23
 
19
24
  import { makeFinding } from "./finding.js";
20
25
  import type { Finding } from "./types.js";
@@ -254,8 +259,12 @@ function findClassNameReimpl(
254
259
  export function ruleComponentsPreferLibrary(ix: FactIndex): Finding[] {
255
260
  const policy = ix.policy.ruleConfig(RULE_ID);
256
261
  if (!policy?.enabled) return [];
257
- const sources = canonicalSourcesFromPolicy(policy.options?.canonicalSources);
258
- const mappings = canonicalMappingsFromPolicy(policy.options?.canonicalMappings);
262
+ const configuredSources = canonicalSourcesFromPolicy(policy.options?.canonicalSources);
263
+ const conflicts = projectCanonicalDirectionConflicts(ix, configuredSources);
264
+ const sources = configuredSources.map((source) => suppressConflictedExports(source, conflicts));
265
+ const mappings = canonicalMappingsFromPolicy(policy.options?.canonicalMappings).filter(
266
+ (mapping) => !canonicalDirectionConflictsTarget(conflicts, mapping.importPath, mapping.name)
267
+ );
259
268
  if (sources.length === 0 && mappings.length === 0) return [];
260
269
 
261
270
  const importsByFileAndLocal = indexImportsByFileAndLocal(ix);
@@ -605,6 +614,22 @@ export function ruleComponentsPreferLibrary(ix: FactIndex): Finding[] {
605
614
  return findings;
606
615
  }
607
616
 
617
+ function suppressConflictedExports(
618
+ source: CanonicalSource,
619
+ conflicts: readonly CanonicalDirectionConflict[]
620
+ ): CanonicalSource {
621
+ if (source.kind !== "npm") return source;
622
+ const names = conflicts
623
+ .filter((conflict) => ownedImportMatchesRoot(source.specifier, conflict.packageName))
624
+ .map((conflict) => conflict.exportName);
625
+ if (names.length === 0) return source;
626
+ const blocked = new Set(names);
627
+ if (source.include) {
628
+ return { ...source, include: source.include.filter((name) => !blocked.has(name)) };
629
+ }
630
+ return { ...source, exclude: [...new Set([...(source.exclude ?? []), ...names])].sort() };
631
+ }
632
+
608
633
  function indexShadowRenderRootNodeIds(ix: FactIndex): Set<FactId> {
609
634
  const identities = ix.byKind("component_identity");
610
635
  if (identities.length === 0) return new Set();
@@ -1113,8 +1138,9 @@ function indexCanonicalDirectoryResolvedNodes(
1113
1138
  ): Set<FactId> {
1114
1139
  const out = new Set<FactId>();
1115
1140
  const directoryPaths = sources
1116
- .filter((source): source is Extract<CanonicalSource, { kind: "directory" }> =>
1117
- source.kind === "directory"
1141
+ .filter(
1142
+ (source): source is Extract<CanonicalSource, { kind: "directory" }> =>
1143
+ source.kind === "directory"
1118
1144
  )
1119
1145
  .map((source) => normalizePath(source.path));
1120
1146
  if (directoryPaths.length === 0) return out;
@@ -25,6 +25,7 @@ export const RULE_FIX_AVAILABLE = {
25
25
  "tailwind/unknown-class": false,
26
26
  "tokens/require-dual-fallback": true,
27
27
  "tokens/css-vars-must-be-defined": false,
28
+ "tokens/upstream-drift": false,
28
29
  "theme/no-theme-coupled-literal": false,
29
30
  "a11y/required-accessible-name": false,
30
31
  "composition/cardinality": false,
@@ -31,6 +31,7 @@ import { ruleTailwindRawColorViaToken } from "./tailwind-raw-color-via-token.js"
31
31
  import { ruleTailwindUnknownClass } from "./tailwind-unknown-class.js";
32
32
  import { ruleTokensRequireDualFallback } from "./tokens-require-dual-fallback.js";
33
33
  import { ruleTokensCssVarsMustBeDefined } from "./tokens-css-vars-must-be-defined.js";
34
+ import { ruleTokensUpstreamDrift } from "./tokens-upstream-drift.js";
34
35
  import type { Finding } from "./types.js";
35
36
 
36
37
  export type RuleFn = (ix: FactIndex) => Finding[];
@@ -137,6 +138,11 @@ export const RULES: readonly Rule[] = [
137
138
  version: "1",
138
139
  run: ruleTokensCssVarsMustBeDefined,
139
140
  },
141
+ {
142
+ id: "tokens/upstream-drift",
143
+ version: "1",
144
+ run: ruleTokensUpstreamDrift,
145
+ },
140
146
  {
141
147
  id: "theme/no-theme-coupled-literal",
142
148
  version: "1",
@@ -211,6 +217,7 @@ export { ruleTailwindRawColorViaToken } from "./tailwind-raw-color-via-token.js"
211
217
  export { ruleTailwindUnknownClass } from "./tailwind-unknown-class.js";
212
218
  export { ruleTokensRequireDualFallback } from "./tokens-require-dual-fallback.js";
213
219
  export { ruleTokensCssVarsMustBeDefined } from "./tokens-css-vars-must-be-defined.js";
220
+ export { ruleTokensUpstreamDrift } from "./tokens-upstream-drift.js";
214
221
  export { ruleThemeNoThemeCoupledLiteral } from "./theme-no-theme-coupled-literal.js";
215
222
  export { ruleA11yRequiredAccessibleName } from "./a11y-required-accessible-name.js";
216
223
  export { ruleCompositionCardinality, ruleCompositionCoOccurrence } from "./composition-pattern.js";
@@ -1,5 +1,5 @@
1
1
  import type { FactIndex } from "../facts/index.js";
2
- import { ownedImportsEqual } from "../package-identity-match.js";
2
+ import { ownedImportMatchesRoot, ownedImportsEqual } from "../package-identity-match.js";
3
3
 
4
4
  import { makeFinding } from "./finding.js";
5
5
  import type { Finding } from "./types.js";
@@ -16,6 +16,21 @@ export function ruleJsxPreferredImportPath(ix: FactIndex): Finding[] {
16
16
 
17
17
  for (const usage of ix.byKind("usage_import")) {
18
18
  const matches = policies.filter((policy) => {
19
+ if (policy.bridge) {
20
+ if (policy.bridge.implementationFiles.some((file) => sameRepoPath(file, usage.file))) {
21
+ return false;
22
+ }
23
+ if (!ownedImportMatchesRoot(usage.source, policy.from)) return false;
24
+ if (ownedImportsEqual(usage.source, policy.from)) {
25
+ return usage.imported === policy.bridge.underlyingExportName;
26
+ }
27
+ const subpath = usage.source.slice(policy.from.length + 1);
28
+ const leaf = subpath.split("/").at(-1);
29
+ return (
30
+ leaf === policy.bridge.underlyingExportName &&
31
+ (usage.imported === "default" || usage.imported === policy.bridge.underlyingExportName)
32
+ );
33
+ }
19
34
  if (!ownedImportsEqual(policy.from, usage.source)) return false;
20
35
  if (policy.imported !== undefined && policy.imported !== usage.imported) return false;
21
36
  return true;
@@ -33,7 +48,9 @@ export function ruleJsxPreferredImportPath(ix: FactIndex): Finding[] {
33
48
  ruleId: RULE_ID,
34
49
  ruleVersion: RULE_VERSION,
35
50
  severity: policy.severity,
36
- message: `Import from "${usage.source}" should use "${policy.to}".`,
51
+ message: policy.bridge
52
+ ? `Import ${policy.bridge.underlyingExportName} through the confirmed local ${policy.bridge.localExportName} wrapper from "${policy.to}".`
53
+ : `Import from "${usage.source}" should use "${policy.to}".`,
37
54
  location: usage.location,
38
55
  evidence: ix.evidence([usage.id, policy.id]),
39
56
  fingerprintIdentity: {
@@ -44,19 +61,30 @@ export function ruleJsxPreferredImportPath(ix: FactIndex): Finding[] {
44
61
  line: usage.location.line,
45
62
  column: usage.location.column,
46
63
  },
47
- fix: {
48
- kind: "replaceImport",
49
- title: policy.because ?? `Replace import path with "${policy.to}"`,
50
- from: usage.source,
51
- to: policy.to,
52
- deterministic: true,
53
- },
64
+ fix: policy.bridge
65
+ ? undefined
66
+ : {
67
+ kind: "replaceImport",
68
+ title: policy.because ?? `Replace import path with "${policy.to}"`,
69
+ from: usage.source,
70
+ to: policy.to,
71
+ deterministic: true,
72
+ },
54
73
  attributes: {
55
74
  importPath: usage.source,
56
75
  suggestedImport: policy.to,
57
76
  imported: usage.imported,
58
77
  local: usage.local,
59
78
  rawValue: usage.source,
79
+ ...(policy.bridge
80
+ ? {
81
+ canonicalBridge: true,
82
+ canonicalTarget: policy.bridge.componentKey,
83
+ suggestedComponent: policy.bridge.localExportName,
84
+ implementationFiles: policy.bridge.implementationFiles,
85
+ decisionSource: policy.bridge.decisionSource,
86
+ }
87
+ : {}),
60
88
  },
61
89
  })
62
90
  );
@@ -65,3 +93,11 @@ export function ruleJsxPreferredImportPath(ix: FactIndex): Finding[] {
65
93
 
66
94
  return findings;
67
95
  }
96
+
97
+ function sameRepoPath(left: string, right: string): boolean {
98
+ return normalizeRepoPath(left) === normalizeRepoPath(right);
99
+ }
100
+
101
+ function normalizeRepoPath(path: string): string {
102
+ return path.replace(/\\/gu, "/").replace(/^\.\//u, "");
103
+ }