@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.
- package/dist/{chunk-CBSNXFOD.js → chunk-AOG4FTV6.js} +146 -6
- package/dist/chunk-AOG4FTV6.js.map +1 -0
- package/dist/{chunk-57QDBEHQ.js → chunk-ZHS52OT4.js} +3 -9
- package/dist/chunk-ZHS52OT4.js.map +1 -0
- package/dist/codes/index.d.ts +2 -2
- package/dist/codes/index.js +2 -2
- package/dist/compiled-types/index.d.ts +1 -1
- package/dist/generate/index.d.ts +1 -1
- package/dist/{governance-DxdJV6lx.d.ts → governance-B88uR3Zq.d.ts} +229 -1
- package/dist/{index-0lmh0Lbo.d.ts → index-DbkPE46t.d.ts} +48 -8
- package/dist/index.d.ts +779 -158
- package/dist/index.js +674 -127
- package/dist/index.js.map +1 -1
- package/dist/react-types.d.ts +1 -1
- package/dist/registry.d.ts +18 -18
- package/dist/schemas/index.d.ts +1 -1
- package/dist/schemas/index.js +1 -1
- package/dist/test-utils.d.ts +1 -1
- package/package.json +1 -1
- package/src/agent-format.test.ts +1 -0
- package/src/canonical-bridge.ts +46 -0
- package/src/canonical-direction.ts +118 -0
- package/src/codes/codes.ts +18 -0
- package/src/conform.ts +5 -0
- package/src/contract/preimage.test.ts +40 -0
- package/src/contract/preimage.ts +20 -0
- package/src/effective-governance-inputs.test.ts +52 -0
- package/src/effective-governance-inputs.ts +106 -0
- package/src/facts/builders.ts +70 -2
- package/src/facts/compile.ts +20 -2
- package/src/facts/fact-index.ts +4 -0
- package/src/facts/facts.test.ts +30 -0
- package/src/facts/index.ts +6 -0
- package/src/facts/types.ts +86 -7
- package/src/governance-integrity.test.ts +22 -0
- package/src/governance-integrity.ts +10 -4
- package/src/governance.test.ts +93 -0
- package/src/governance.ts +76 -0
- package/src/identity/classify.test.ts +185 -0
- package/src/identity/classify.ts +257 -0
- package/src/identity/component-key.test.ts +32 -0
- package/src/identity/component-key.ts +35 -0
- package/src/identity/usage.ts +39 -0
- package/src/index.ts +48 -0
- package/src/rules/__tests__/fix-emission-invariant.test.ts +35 -4
- package/src/rules/components-prefer-library.test.ts +151 -0
- package/src/rules/components-prefer-library.ts +46 -4
- package/src/rules/components-shadow-component.test.ts +145 -0
- package/src/rules/components-shadow-component.ts +81 -0
- package/src/rules/fix-availability.ts +2 -0
- package/src/rules/index.ts +14 -0
- package/src/rules/jsx-preferred-import-path.ts +45 -9
- package/src/rules/rules.test.ts +202 -6
- package/src/rules/spacing-resolution.ts +2 -2
- package/src/rules/styles-no-raw-color.test.ts +6 -1
- package/src/rules/styles-no-raw-color.ts +3 -2
- package/src/rules/styles-no-raw-dimensions.ts +5 -7
- package/src/rules/styles-no-raw-spacing.ts +5 -2
- package/src/rules/styles-no-raw-typography.ts +7 -4
- package/src/rules/taxonomy.test.ts +19 -1
- package/src/rules/tiers.ts +9 -5
- package/src/rules/token-candidates.ts +36 -0
- package/src/rules/tokens-require-dual-fallback.ts +2 -1
- package/src/rules/tokens-upstream-drift.test.ts +111 -0
- package/src/rules/tokens-upstream-drift.ts +57 -0
- package/src/rules/utils.ts +22 -14
- package/src/schema.ts +28 -0
- package/src/schemas/index.ts +5 -8
- package/src/token-types.ts +71 -1
- package/src/types.ts +18 -0
- package/dist/chunk-57QDBEHQ.js.map +0 -1
- 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,
|