@usefragments/core 1.4.0 → 1.5.1

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 (72) hide show
  1. package/dist/{chunk-CBSNXFOD.js → chunk-AOG4FTV6.js} +146 -6
  2. package/dist/chunk-AOG4FTV6.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-DxdJV6lx.d.ts → governance-B88uR3Zq.d.ts} +229 -1
  10. package/dist/{index-0lmh0Lbo.d.ts → index-DbkPE46t.d.ts} +48 -8
  11. package/dist/index.d.ts +779 -158
  12. package/dist/index.js +674 -127
  13. package/dist/index.js.map +1 -1
  14. package/dist/react-types.d.ts +1 -1
  15. package/dist/registry.d.ts +18 -18
  16. package/dist/schemas/index.d.ts +1 -1
  17. package/dist/schemas/index.js +1 -1
  18. package/dist/test-utils.d.ts +1 -1
  19. package/package.json +1 -1
  20. package/src/agent-format.test.ts +1 -0
  21. package/src/canonical-bridge.ts +46 -0
  22. package/src/canonical-direction.ts +118 -0
  23. package/src/codes/codes.ts +18 -0
  24. package/src/conform.ts +5 -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 +70 -2
  30. package/src/facts/compile.ts +20 -2
  31. package/src/facts/fact-index.ts +4 -0
  32. package/src/facts/facts.test.ts +30 -0
  33. package/src/facts/index.ts +6 -0
  34. package/src/facts/types.ts +86 -7
  35. package/src/governance-integrity.test.ts +22 -0
  36. package/src/governance-integrity.ts +10 -4
  37. package/src/governance.test.ts +93 -0
  38. package/src/governance.ts +76 -0
  39. package/src/identity/classify.test.ts +185 -0
  40. package/src/identity/classify.ts +257 -0
  41. package/src/identity/component-key.test.ts +32 -0
  42. package/src/identity/component-key.ts +35 -0
  43. package/src/identity/usage.ts +39 -0
  44. package/src/index.ts +48 -0
  45. package/src/rules/__tests__/fix-emission-invariant.test.ts +35 -4
  46. package/src/rules/components-prefer-library.test.ts +151 -0
  47. package/src/rules/components-prefer-library.ts +46 -4
  48. package/src/rules/components-shadow-component.test.ts +145 -0
  49. package/src/rules/components-shadow-component.ts +81 -0
  50. package/src/rules/fix-availability.ts +2 -0
  51. package/src/rules/index.ts +14 -0
  52. package/src/rules/jsx-preferred-import-path.ts +45 -9
  53. package/src/rules/rules.test.ts +202 -6
  54. package/src/rules/spacing-resolution.ts +2 -2
  55. package/src/rules/styles-no-raw-color.test.ts +6 -1
  56. package/src/rules/styles-no-raw-color.ts +3 -2
  57. package/src/rules/styles-no-raw-dimensions.ts +5 -7
  58. package/src/rules/styles-no-raw-spacing.ts +5 -2
  59. package/src/rules/styles-no-raw-typography.ts +7 -4
  60. package/src/rules/taxonomy.test.ts +19 -1
  61. package/src/rules/tiers.ts +9 -5
  62. package/src/rules/token-candidates.ts +36 -0
  63. package/src/rules/tokens-require-dual-fallback.ts +2 -1
  64. package/src/rules/tokens-upstream-drift.test.ts +111 -0
  65. package/src/rules/tokens-upstream-drift.ts +57 -0
  66. package/src/rules/utils.ts +22 -14
  67. package/src/schema.ts +28 -0
  68. package/src/schemas/index.ts +5 -8
  69. package/src/token-types.ts +71 -1
  70. package/src/types.ts +18 -0
  71. package/dist/chunk-57QDBEHQ.js.map +0 -1
  72. package/dist/chunk-CBSNXFOD.js.map +0 -1
@@ -0,0 +1,185 @@
1
+ import { describe, expect, it } from "vitest";
2
+
3
+ import { classifyIdentity, type ClassifyIdentityInput } from "./classify.js";
4
+
5
+ const definition = {
6
+ componentKey: "src/MyButton.tsx#MyButton",
7
+ exportName: "MyButton",
8
+ canonical: false,
9
+ };
10
+
11
+ function classify(overrides: Partial<ClassifyIdentityInput>) {
12
+ return classifyIdentity({
13
+ definition,
14
+ renderRoot: { kind: "unresolved", reason: "test" },
15
+ ...overrides,
16
+ });
17
+ }
18
+
19
+ describe("classifyIdentity", () => {
20
+ it("gives canonical membership primary authority", () => {
21
+ expect(
22
+ classify({
23
+ definition: { ...definition, canonical: true },
24
+ renderRoot: { kind: "external", localName: "Primitive", source: "third-party" },
25
+ })
26
+ ).toEqual({ state: "canonical", confidence: "confirmed" });
27
+ });
28
+
29
+ it("keeps an undecided canonical-root wrapper at review confidence", () => {
30
+ expect(
31
+ classify({
32
+ renderRoot: {
33
+ kind: "canonical",
34
+ localName: "Button",
35
+ target: { componentKey: "packages/ui/Button.tsx#Button", role: "Button" },
36
+ },
37
+ })
38
+ ).toEqual({
39
+ state: "variant",
40
+ confidence: "review",
41
+ canonicalTarget: "packages/ui/Button.tsx#Button",
42
+ });
43
+ });
44
+
45
+ it("classifies an evidenced intrinsic only when the declared vocabulary has its role", () => {
46
+ expect(
47
+ classify({
48
+ renderRoot: {
49
+ kind: "intrinsic",
50
+ tag: "button",
51
+ hasRole: false,
52
+ interactive: true,
53
+ precisionTier: "exact-html",
54
+ canonicalTargets: [{ componentKey: "packages/ui/Button.tsx#Button", role: "Button" }],
55
+ },
56
+ })
57
+ ).toEqual({ state: "shadow", confidence: "confirmed", canonicalTarget: "Button" });
58
+
59
+ expect(
60
+ classify({
61
+ renderRoot: {
62
+ kind: "intrinsic",
63
+ tag: "button",
64
+ hasRole: false,
65
+ interactive: true,
66
+ precisionTier: "exact-html",
67
+ canonicalTargets: [],
68
+ },
69
+ })
70
+ ).toEqual({ state: "unresolved", confidence: "review" });
71
+ });
72
+
73
+ it("uses supporting evidence only to upgrade confidence", () => {
74
+ expect(
75
+ classify({
76
+ propSurfaceOverlap: 0.8,
77
+ stylingOwnership: "visual-role",
78
+ renderRoot: {
79
+ kind: "intrinsic",
80
+ tag: "textarea",
81
+ hasRole: false,
82
+ interactive: true,
83
+ precisionTier: "html-advisory",
84
+ canonicalTargets: [
85
+ { componentKey: "packages/ui/Textarea.tsx#Textarea", role: "Textarea" },
86
+ ],
87
+ },
88
+ })
89
+ ).toEqual({ state: "shadow", confidence: "confirmed", canonicalTarget: "Textarea" });
90
+
91
+ expect(
92
+ classify({
93
+ propSurfaceOverlap: 0.8,
94
+ stylingOwnership: "layout-only",
95
+ renderRoot: {
96
+ kind: "intrinsic",
97
+ tag: "textarea",
98
+ hasRole: false,
99
+ interactive: true,
100
+ precisionTier: "html-advisory",
101
+ canonicalTargets: [
102
+ { componentKey: "packages/ui/Textarea.tsx#Textarea", role: "Textarea" },
103
+ ],
104
+ },
105
+ })
106
+ ).toEqual({ state: "shadow", confidence: "likely", canonicalTarget: "Textarea" });
107
+ });
108
+
109
+ it("applies wrapper decisions as classification inputs", () => {
110
+ const renderRoot: ClassifyIdentityInput["renderRoot"] = {
111
+ kind: "canonical",
112
+ localName: "Button",
113
+ target: { componentKey: "packages/ui/Button.tsx#Button", role: "Button" },
114
+ };
115
+ expect(
116
+ classify({
117
+ renderRoot,
118
+ decisions: [{ componentKey: definition.componentKey, decision: "sanctioned-wrapper" }],
119
+ })
120
+ ).toEqual({
121
+ state: "variant",
122
+ confidence: "confirmed",
123
+ canonicalTarget: "packages/ui/Button.tsx#Button",
124
+ });
125
+ expect(
126
+ classify({
127
+ renderRoot,
128
+ decisions: [{ componentKey: definition.componentKey, decision: "rejected-wrapper" }],
129
+ })
130
+ ).toEqual({ state: "shadow", confidence: "confirmed", canonicalTarget: "Button" });
131
+ });
132
+
133
+ it("classifies canonical mixtures as composites", () => {
134
+ expect(
135
+ classify({
136
+ renderRoot: {
137
+ kind: "mixed",
138
+ canonicalTargets: [{ componentKey: "packages/ui/Card.tsx#Card", role: "Card" }],
139
+ localTargets: [
140
+ { componentKey: "src/UserAvatar.tsx#UserAvatar", exportName: "UserAvatar" },
141
+ ],
142
+ externalTargets: [],
143
+ intrinsics: [],
144
+ unresolvedNames: [],
145
+ },
146
+ })
147
+ ).toEqual({
148
+ state: "composite",
149
+ confidence: "confirmed",
150
+ canonicalTarget: "packages/ui/Card.tsx#Card",
151
+ });
152
+ });
153
+
154
+ it("distinguishes a canonical consumer from a pass-through wrapper", () => {
155
+ expect(
156
+ classify({
157
+ usageCounts: { staticTextChildren: 1 },
158
+ renderRoot: {
159
+ kind: "canonical",
160
+ localName: "Button",
161
+ target: { componentKey: "packages/ui/Button.tsx#Button", role: "Button" },
162
+ },
163
+ })
164
+ ).toEqual({
165
+ state: "composite",
166
+ confidence: "confirmed",
167
+ canonicalTarget: "packages/ui/Button.tsx#Button",
168
+ });
169
+ });
170
+
171
+ it("classifies third-party roots and keeps name similarity message-only", () => {
172
+ expect(
173
+ classify({
174
+ renderRoot: { kind: "external", localName: "Widget", source: "third-party" },
175
+ })
176
+ ).toEqual({ state: "external", confidence: "confirmed" });
177
+
178
+ expect(
179
+ classify({
180
+ nameSimilarityTargets: ["Card", "Button"],
181
+ renderRoot: { kind: "unresolved", reason: "dynamic factory" },
182
+ })
183
+ ).toEqual({ state: "unresolved", confidence: "review", canonicalTarget: "Button" });
184
+ });
185
+ });
@@ -0,0 +1,257 @@
1
+ import type { RawHtmlPrecisionTier } from "../raw-html-canonical.js";
2
+
3
+ export type IdentityState =
4
+ | "canonical"
5
+ | "variant"
6
+ | "shadow"
7
+ | "composite"
8
+ | "external"
9
+ | "unresolved";
10
+
11
+ export type IdentityConfidence = "confirmed" | "likely" | "review";
12
+
13
+ export interface CanonicalIdentityReference {
14
+ componentKey: string;
15
+ role: string;
16
+ }
17
+
18
+ export interface LocalIdentityReference {
19
+ componentKey: string;
20
+ exportName: string;
21
+ }
22
+
23
+ export interface ExternalIdentityReference {
24
+ localName: string;
25
+ source: string;
26
+ }
27
+
28
+ export interface ResolvedIntrinsicIdentityRoot {
29
+ kind: "intrinsic";
30
+ tag: string;
31
+ hasRole: boolean;
32
+ interactive: boolean;
33
+ role?: string;
34
+ inputType?: string;
35
+ precisionTier?: RawHtmlPrecisionTier;
36
+ canonicalTargets: readonly CanonicalIdentityReference[];
37
+ }
38
+
39
+ export type ResolvedIdentityRenderRoot =
40
+ | {
41
+ kind: "canonical";
42
+ localName: string;
43
+ target: CanonicalIdentityReference;
44
+ }
45
+ | ResolvedIntrinsicIdentityRoot
46
+ | {
47
+ kind: "external";
48
+ localName: string;
49
+ source: string;
50
+ }
51
+ | {
52
+ kind: "mixed";
53
+ canonicalTargets: readonly CanonicalIdentityReference[];
54
+ localTargets: readonly LocalIdentityReference[];
55
+ externalTargets: readonly ExternalIdentityReference[];
56
+ intrinsics: readonly ResolvedIntrinsicIdentityRoot[];
57
+ unresolvedNames: readonly string[];
58
+ }
59
+ | { kind: "unresolved"; reason: string };
60
+
61
+ export interface IdentityDecision {
62
+ componentKey: string;
63
+ decision: "sanctioned-wrapper" | "rejected-wrapper";
64
+ canonicalTarget?: string;
65
+ decisionId?: string;
66
+ }
67
+
68
+ export interface ClassifyIdentityInput {
69
+ definition: {
70
+ componentKey: string;
71
+ exportName: string;
72
+ canonical: boolean;
73
+ };
74
+ renderRoot: ResolvedIdentityRenderRoot;
75
+ propSurfaceOverlap?: number;
76
+ stylingOwnership?: "visual-role" | "layout-only" | "none" | "unknown";
77
+ usageCounts?: {
78
+ importers?: number;
79
+ callSites?: number;
80
+ nestedElements?: number;
81
+ staticTextChildren?: number;
82
+ };
83
+ decisions?: readonly IdentityDecision[];
84
+ nameSimilarityTargets?: readonly string[];
85
+ }
86
+
87
+ export interface IdentityClassification {
88
+ state: IdentityState;
89
+ confidence: IdentityConfidence;
90
+ canonicalTarget?: string;
91
+ }
92
+
93
+ /**
94
+ * Classify already-resolved identity evidence. AST and filesystem concerns stay
95
+ * in the extractor; this function is deterministic and browser-safe.
96
+ */
97
+ export function classifyIdentity(input: ClassifyIdentityInput): IdentityClassification {
98
+ const { definition, renderRoot } = input;
99
+ if (definition.canonical) return { state: "canonical", confidence: "confirmed" };
100
+
101
+ const decision = input.decisions?.find(
102
+ (candidate) => candidate.componentKey === definition.componentKey
103
+ );
104
+ if (decision?.decision === "sanctioned-wrapper") {
105
+ return withTarget("variant", "confirmed", decision.canonicalTarget ?? rootTarget(input));
106
+ }
107
+ if (decision?.decision === "rejected-wrapper") {
108
+ return withTarget("shadow", "confirmed", decision.canonicalTarget ?? shadowTarget(input));
109
+ }
110
+
111
+ if (renderRoot.kind === "canonical") {
112
+ if (
113
+ (input.usageCounts?.nestedElements ?? 0) > 0 ||
114
+ (input.usageCounts?.staticTextChildren ?? 0) > 0
115
+ ) {
116
+ return withTarget("composite", "confirmed", renderRoot.target.componentKey);
117
+ }
118
+ const confidence = strongOverlap(input.propSurfaceOverlap) ? "likely" : "review";
119
+ return withTarget("variant", confidence, renderRoot.target.componentKey);
120
+ }
121
+
122
+ if (renderRoot.kind === "intrinsic") {
123
+ return classifyIntrinsic(input, renderRoot);
124
+ }
125
+
126
+ if (renderRoot.kind === "mixed") {
127
+ if (renderRoot.canonicalTargets.length > 0) {
128
+ const target = bestCanonicalReference(
129
+ definition.exportName,
130
+ renderRoot.canonicalTargets
131
+ )?.componentKey;
132
+ const fullyResolved =
133
+ renderRoot.externalTargets.length === 0 && renderRoot.unresolvedNames.length === 0;
134
+ return withTarget("composite", fullyResolved ? "confirmed" : "likely", target);
135
+ }
136
+
137
+ if (
138
+ renderRoot.externalTargets.length > 0 &&
139
+ renderRoot.localTargets.length === 0 &&
140
+ renderRoot.unresolvedNames.length === 0
141
+ ) {
142
+ return { state: "external", confidence: "likely" };
143
+ }
144
+
145
+ return withTarget("unresolved", "review", rootTarget(input));
146
+ }
147
+
148
+ if (renderRoot.kind === "external") {
149
+ return { state: "external", confidence: "confirmed" };
150
+ }
151
+
152
+ return withTarget("unresolved", "review", rootTarget(input));
153
+ }
154
+
155
+ function classifyIntrinsic(
156
+ input: ClassifyIdentityInput,
157
+ root: ResolvedIntrinsicIdentityRoot
158
+ ): IdentityClassification {
159
+ const target = bestCanonicalReference(input.definition.exportName, root.canonicalTargets);
160
+ if (!target || (!root.interactive && !root.hasRole)) {
161
+ return withTarget("unresolved", "review", rootTarget(input));
162
+ }
163
+
164
+ let confidence: IdentityConfidence =
165
+ root.precisionTier === "exact-html" || root.precisionTier === "role-reimpl"
166
+ ? "confirmed"
167
+ : "likely";
168
+ if (
169
+ confidence === "likely" &&
170
+ input.stylingOwnership !== "layout-only" &&
171
+ (strongOverlap(input.propSurfaceOverlap) || input.stylingOwnership === "visual-role")
172
+ ) {
173
+ confidence = "confirmed";
174
+ }
175
+
176
+ return { state: "shadow", confidence, canonicalTarget: target.role };
177
+ }
178
+
179
+ function rootTarget(input: ClassifyIdentityInput): string | undefined {
180
+ const { renderRoot } = input;
181
+ if (renderRoot.kind === "canonical") return renderRoot.target.componentKey;
182
+ if (renderRoot.kind === "intrinsic") {
183
+ return bestCanonicalReference(input.definition.exportName, renderRoot.canonicalTargets)?.role;
184
+ }
185
+ if (renderRoot.kind === "mixed") {
186
+ return bestCanonicalReference(input.definition.exportName, renderRoot.canonicalTargets)
187
+ ?.componentKey;
188
+ }
189
+ return bestMessageTarget(input.definition.exportName, input.nameSimilarityTargets ?? []);
190
+ }
191
+
192
+ function shadowTarget(input: ClassifyIdentityInput): string | undefined {
193
+ const { renderRoot } = input;
194
+ if (renderRoot.kind === "canonical") return renderRoot.target.role;
195
+ if (renderRoot.kind === "intrinsic") {
196
+ return bestCanonicalReference(input.definition.exportName, renderRoot.canonicalTargets)?.role;
197
+ }
198
+ if (renderRoot.kind === "mixed") {
199
+ return bestCanonicalReference(input.definition.exportName, renderRoot.canonicalTargets)?.role;
200
+ }
201
+ return bestMessageTarget(input.definition.exportName, input.nameSimilarityTargets ?? []);
202
+ }
203
+
204
+ function bestCanonicalReference(
205
+ definitionName: string,
206
+ candidates: readonly CanonicalIdentityReference[]
207
+ ): CanonicalIdentityReference | undefined {
208
+ return [...candidates].sort(
209
+ (left, right) =>
210
+ nameSimilarityScore(definitionName, right.role) -
211
+ nameSimilarityScore(definitionName, left.role) ||
212
+ left.componentKey.localeCompare(right.componentKey)
213
+ )[0];
214
+ }
215
+
216
+ function bestMessageTarget(
217
+ definitionName: string,
218
+ candidates: readonly string[]
219
+ ): string | undefined {
220
+ return [...candidates].sort(
221
+ (left, right) =>
222
+ nameSimilarityScore(definitionName, right) - nameSimilarityScore(definitionName, left) ||
223
+ left.localeCompare(right)
224
+ )[0];
225
+ }
226
+
227
+ function nameSimilarityScore(definitionName: string, target: string): number {
228
+ const definition = normalizeName(definitionName);
229
+ const candidate = normalizeName(target);
230
+ if (!candidate) return 0;
231
+ if (definition === candidate) return 3;
232
+ if (definition.endsWith(candidate) || definition.startsWith(candidate)) return 2;
233
+ if (definition.includes(candidate) || candidate.includes(definition)) return 1;
234
+ return 0;
235
+ }
236
+
237
+ function normalizeName(value: string): string {
238
+ return (
239
+ value
240
+ .split(/[.#/]/)
241
+ .at(-1)
242
+ ?.replace(/[^a-z0-9]/gi, "")
243
+ .toLowerCase() ?? ""
244
+ );
245
+ }
246
+
247
+ function strongOverlap(overlap: number | undefined): boolean {
248
+ return overlap !== undefined && overlap >= 0.7;
249
+ }
250
+
251
+ function withTarget(
252
+ state: IdentityState,
253
+ confidence: IdentityConfidence,
254
+ canonicalTarget: string | undefined
255
+ ): IdentityClassification {
256
+ return canonicalTarget ? { state, confidence, canonicalTarget } : { state, confidence };
257
+ }
@@ -0,0 +1,32 @@
1
+ import { describe, expect, it } from "vitest";
2
+
3
+ import { buildComponentKey } from "./component-key.js";
4
+
5
+ describe("buildComponentKey", () => {
6
+ it("normalizes clone-independent repository paths", () => {
7
+ expect(
8
+ buildComponentKey({
9
+ repoRelativeFile: "./packages/ui/src/../src/Button.tsx",
10
+ exportName: " Button ",
11
+ })
12
+ ).toBe("packages/ui/src/Button.tsx#Button");
13
+ });
14
+
15
+ it("normalizes Windows separators to the same key", () => {
16
+ const posix = buildComponentKey({
17
+ repoRelativeFile: "packages/ui/src/Button.tsx",
18
+ exportName: "Button",
19
+ });
20
+ const windows = buildComponentKey({
21
+ repoRelativeFile: ".\\packages\\ui\\src\\Button.tsx",
22
+ exportName: "Button",
23
+ });
24
+
25
+ expect(windows).toBe(posix);
26
+ });
27
+
28
+ it("rejects empty identity parts", () => {
29
+ expect(() => buildComponentKey({ repoRelativeFile: "", exportName: "Button" })).toThrow();
30
+ expect(() => buildComponentKey({ repoRelativeFile: "Button.tsx", exportName: " " })).toThrow();
31
+ });
32
+ });
@@ -0,0 +1,35 @@
1
+ export interface ComponentKeyInput {
2
+ repoRelativeFile: string;
3
+ exportName: string;
4
+ }
5
+
6
+ /**
7
+ * Mint a clone- and platform-portable identity for one component definition.
8
+ */
9
+ export function buildComponentKey({ repoRelativeFile, exportName }: ComponentKeyInput): string {
10
+ const file = normalizeRepoRelativeFile(repoRelativeFile);
11
+ const name = exportName.trim();
12
+ if (!file) throw new Error("repoRelativeFile must not be empty");
13
+ if (!name) throw new Error("exportName must not be empty");
14
+ return `${file}#${name}`;
15
+ }
16
+
17
+ function normalizeRepoRelativeFile(file: string): string {
18
+ const slashNormalized = file
19
+ .trim()
20
+ .replace(/\\/g, "/")
21
+ .replace(/^\.\/+/, "");
22
+ const absolute = slashNormalized.startsWith("/");
23
+ const parts: string[] = [];
24
+
25
+ for (const part of slashNormalized.split("/")) {
26
+ if (!part || part === ".") continue;
27
+ if (part === ".." && parts.length > 0 && parts.at(-1) !== "..") {
28
+ parts.pop();
29
+ continue;
30
+ }
31
+ parts.push(part);
32
+ }
33
+
34
+ return `${absolute ? "/" : ""}${parts.join("/")}`;
35
+ }
@@ -0,0 +1,39 @@
1
+ import type { FactIndex, UsageNodeFact } from "../facts/index.js";
2
+
3
+ export interface ComponentIdentityUsage {
4
+ usageCount: number;
5
+ /** Number of distinct source files containing a usage. */
6
+ blastRadius: number;
7
+ nodes: UsageNodeFact[];
8
+ }
9
+
10
+ /** Join resolved component usages to their usage nodes once for rules and DX surfaces. */
11
+ export function indexComponentIdentityUsage(
12
+ ix: FactIndex
13
+ ): ReadonlyMap<string, ComponentIdentityUsage> {
14
+ const nodesById = new Map(ix.byKind("usage_node").map((node) => [node.id, node]));
15
+ const nodesByComponent = new Map<string, Map<string, UsageNodeFact>>();
16
+
17
+ for (const usage of ix.byKind("usage_component")) {
18
+ const node = nodesById.get(usage.nodeId);
19
+ if (!node) continue;
20
+ const componentKey = String(usage.componentId);
21
+ const nodes = nodesByComponent.get(componentKey) ?? new Map<string, UsageNodeFact>();
22
+ nodes.set(node.id, node);
23
+ nodesByComponent.set(componentKey, nodes);
24
+ }
25
+
26
+ return new Map(
27
+ [...nodesByComponent].map(([componentKey, nodesByIdForComponent]) => {
28
+ const nodes = [...nodesByIdForComponent.values()];
29
+ return [
30
+ componentKey,
31
+ {
32
+ usageCount: nodes.length,
33
+ blastRadius: new Set(nodes.map((node) => node.file)).size,
34
+ nodes,
35
+ },
36
+ ];
37
+ })
38
+ );
39
+ }
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,
@@ -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,
@@ -627,6 +635,7 @@ export {
627
635
  ruleComponentsUnknownProp,
628
636
  ruleComponentsForbiddenPropValue,
629
637
  ruleComponentsPreferLibrary,
638
+ ruleComponentsShadowComponent,
630
639
  rulePropsInvalidValue,
631
640
  ruleJsxPreferredImportPath,
632
641
  ruleJsxPreferredComponent,
@@ -644,6 +653,7 @@ export {
644
653
  ruleTokensRequireDualFallback,
645
654
  ruleA11yRequiredAccessibleName,
646
655
  } from "./rules/index.js";
656
+ export { indexComponentIdentityUsage, type ComponentIdentityUsage } from "./identity/usage.js";
647
657
  export { nearestSignedScaleValue } from "./rules/utils.js";
648
658
  export {
649
659
  buildSpacingTokenLookup,
@@ -715,6 +725,24 @@ export type {
715
725
  GovernanceIntegrityVerdict,
716
726
  } from "./governance-integrity.js";
717
727
 
728
+ export {
729
+ canonicalDirectionConflictsTarget,
730
+ projectCanonicalDirectionConflicts,
731
+ } from "./canonical-direction.js";
732
+ export type { CanonicalDirectionConflict } from "./canonical-direction.js";
733
+
734
+ export { canonicalBridgeContractMappings } from "./canonical-bridge.js";
735
+
736
+ export {
737
+ compileEffectiveGovernanceInputs,
738
+ EFFECTIVE_GOVERNANCE_INPUTS_SCHEMA,
739
+ } from "./effective-governance-inputs.js";
740
+ export type {
741
+ CompileEffectiveGovernanceInputsOptions,
742
+ EffectiveGovernanceInputs,
743
+ EffectiveGovernanceInputsSourceState,
744
+ } from "./effective-governance-inputs.js";
745
+
718
746
  // Fact IR + index (Phase 3 of the governance compiler refactor)
719
747
  export {
720
748
  componentId,
@@ -726,6 +754,8 @@ export {
726
754
  matchesGlob,
727
755
  makeComponentMetadataFact,
728
756
  makeComponentCapabilityFact,
757
+ makeComponentDefinitionFact,
758
+ makeComponentIdentityFact,
729
759
  makeCanonicalCandidateFact,
730
760
  makeCanonicalMappingFact,
731
761
  makeCanonicalReplacementFact,
@@ -836,6 +866,20 @@ export {
836
866
  type RawHtmlCanonicalMatch,
837
867
  type RawHtmlPrecisionTier,
838
868
  } from "./raw-html-canonical.js";
869
+ export { buildComponentKey, type ComponentKeyInput } from "./identity/component-key.js";
870
+ export {
871
+ classifyIdentity,
872
+ type CanonicalIdentityReference,
873
+ type ClassifyIdentityInput,
874
+ type ExternalIdentityReference,
875
+ type IdentityClassification,
876
+ type IdentityConfidence,
877
+ type IdentityDecision,
878
+ type IdentityState,
879
+ type LocalIdentityReference,
880
+ type ResolvedIdentityRenderRoot,
881
+ type ResolvedIntrinsicIdentityRoot,
882
+ } from "./identity/classify.js";
839
883
  export type {
840
884
  ComponentId,
841
885
  FactId,
@@ -848,6 +892,10 @@ export type {
848
892
  CanonicalMappingFact,
849
893
  CanonicalReplacementFact,
850
894
  ComponentLevelFact,
895
+ ComponentDefinitionFact,
896
+ ComponentDefinitionRenderRoot,
897
+ ComponentIdentityFact,
898
+ IdentityFact,
851
899
  PolicyFact,
852
900
  UsageFact,
853
901
  ComponentMetadataFact,