@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.
- package/dist/{chunk-MLRDNSSA.js → chunk-HXGE3O2F.js} +39 -1
- package/dist/chunk-HXGE3O2F.js.map +1 -0
- package/dist/codes/index.d.ts +1 -1
- package/dist/codes/index.js +1 -1
- package/dist/compiled-types/index.d.ts +1 -1
- package/dist/generate/index.d.ts +1 -1
- package/dist/{governance-DYSirwJu.d.ts → governance-BLsyk56o.d.ts} +21 -0
- package/dist/index.d.ts +324 -82
- package/dist/index.js +507 -120
- package/dist/index.js.map +1 -1
- package/dist/react-types.d.ts +1 -1
- package/dist/test-utils.d.ts +1 -1
- package/package.json +1 -1
- package/src/codes/codes.ts +9 -0
- package/src/conform.ts +5 -0
- package/src/constants.ts +3 -0
- package/src/facts/builders.ts +46 -2
- package/src/facts/facts.test.ts +30 -0
- package/src/facts/index.ts +6 -0
- package/src/facts/types.ts +75 -2
- package/src/governance.test.ts +26 -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 +25 -0
- package/src/rules/components-prefer-library.test.ts +196 -0
- package/src/rules/components-prefer-library.ts +68 -1
- 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 +1 -0
- package/src/rules/index.ts +7 -0
- package/src/rules/styles-no-raw-color.test.ts +213 -0
- package/src/rules/styles-no-raw-color.ts +125 -0
- package/src/rules/taxonomy.test.ts +16 -1
- package/src/rules/tiers.ts +7 -5
- package/src/schema.ts +21 -2
- package/src/token-types.ts +12 -0
- package/src/tokens/categories.ts +54 -24
- package/src/tokens/scss.test.ts +94 -0
- package/src/tokens/scss.ts +11 -4
- package/src/types.ts +23 -0
- package/dist/chunk-MLRDNSSA.js.map +0 -1
package/dist/react-types.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { ComponentType, ReactNode, JSX } from 'react';
|
|
2
|
-
export { F as FragmentDefinition, a as FragmentDefinitionV2 } from './governance-
|
|
2
|
+
export { F as FragmentDefinition, a as FragmentDefinitionV2 } from './governance-BLsyk56o.js';
|
|
3
3
|
import 'zod';
|
|
4
4
|
import './topology/index.js';
|
|
5
5
|
import './types-xJ2xyp_G.js';
|
package/dist/test-utils.d.ts
CHANGED
package/package.json
CHANGED
package/src/codes/codes.ts
CHANGED
|
@@ -108,6 +108,15 @@ export const CODES = [
|
|
|
108
108
|
fixAvailable: false,
|
|
109
109
|
evidenceRequired: true,
|
|
110
110
|
}),
|
|
111
|
+
ruleCode({
|
|
112
|
+
code: "FUI1007",
|
|
113
|
+
ruleId: "components/shadow-component",
|
|
114
|
+
category: "canonical-usage",
|
|
115
|
+
defaultSeverity: "moderate",
|
|
116
|
+
title: "Component shadows a canonical primitive",
|
|
117
|
+
lifecycle: "experimental",
|
|
118
|
+
evidenceRequired: true,
|
|
119
|
+
}),
|
|
111
120
|
code({
|
|
112
121
|
code: "FUI2001",
|
|
113
122
|
ruleId: "tokens/require-design-tokens",
|
package/src/conform.ts
CHANGED
|
@@ -20,6 +20,11 @@ export interface ConformInput {
|
|
|
20
20
|
filename?: string;
|
|
21
21
|
/** deterministic (default) applies safe edits; none returns findings only. */
|
|
22
22
|
apply?: "deterministic" | "none";
|
|
23
|
+
/** Explicit request to consolidate a known shadow into its canonical root. */
|
|
24
|
+
shadowConsolidation?: {
|
|
25
|
+
shadow: string;
|
|
26
|
+
canonicalTarget?: string;
|
|
27
|
+
};
|
|
23
28
|
}
|
|
24
29
|
|
|
25
30
|
export interface ConformLocation {
|
package/src/constants.ts
CHANGED
|
@@ -30,6 +30,9 @@ export const BRAND = {
|
|
|
30
30
|
/** CLI command name (e.g., "fragments") */
|
|
31
31
|
cliCommand: "fragments",
|
|
32
32
|
|
|
33
|
+
/** Full user-facing CLI invocation for copy printed to end users (e.g., "npx @usefragments/cli") */
|
|
34
|
+
cliInvocation: "npx @usefragments/cli",
|
|
35
|
+
|
|
33
36
|
/** Directory for storing fragments, registry, and cache */
|
|
34
37
|
dataDir: ".fragments",
|
|
35
38
|
|
package/src/facts/builders.ts
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
import type { GovernanceSeverity } from "../governance.js";
|
|
12
|
-
import { factId } from "./ids.js";
|
|
12
|
+
import { asComponentId, factId } from "./ids.js";
|
|
13
13
|
import type {
|
|
14
14
|
A11yNameRequiredFact,
|
|
15
15
|
CanonicalCandidateFact,
|
|
@@ -20,7 +20,9 @@ import type {
|
|
|
20
20
|
ClassNameLiteralFact,
|
|
21
21
|
ClassNameOrigin,
|
|
22
22
|
ComponentCapabilityFact,
|
|
23
|
+
ComponentDefinitionFact,
|
|
23
24
|
ComponentId,
|
|
25
|
+
ComponentIdentityFact,
|
|
24
26
|
ComponentMetadataFact,
|
|
25
27
|
FactLocation,
|
|
26
28
|
GovernanceRuleConfigFact,
|
|
@@ -99,6 +101,48 @@ export function makeComponentCapabilityFact(input: {
|
|
|
99
101
|
};
|
|
100
102
|
}
|
|
101
103
|
|
|
104
|
+
export function makeComponentDefinitionFact(input: {
|
|
105
|
+
file: string;
|
|
106
|
+
exportName: string;
|
|
107
|
+
componentKey: string;
|
|
108
|
+
renderRoot: ComponentDefinitionFact["renderRoot"];
|
|
109
|
+
propSurface: string[];
|
|
110
|
+
stylingChannel?: ComponentDefinitionFact["stylingChannel"];
|
|
111
|
+
}): ComponentDefinitionFact {
|
|
112
|
+
return {
|
|
113
|
+
id: factId("component_definition", { componentKey: input.componentKey }),
|
|
114
|
+
kind: "component_definition",
|
|
115
|
+
componentId: asComponentId(input.componentKey),
|
|
116
|
+
file: input.file,
|
|
117
|
+
exportName: input.exportName,
|
|
118
|
+
componentKey: input.componentKey,
|
|
119
|
+
renderRoot: input.renderRoot,
|
|
120
|
+
propSurface: [...input.propSurface],
|
|
121
|
+
...(input.stylingChannel ? { stylingChannel: { ...input.stylingChannel } } : {}),
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export function makeComponentIdentityFact(input: {
|
|
126
|
+
componentKey: string;
|
|
127
|
+
state: ComponentIdentityFact["state"];
|
|
128
|
+
confidence: ComponentIdentityFact["confidence"];
|
|
129
|
+
canonicalTarget?: string;
|
|
130
|
+
decisionId?: string;
|
|
131
|
+
evidence: FactId[];
|
|
132
|
+
}): ComponentIdentityFact {
|
|
133
|
+
return {
|
|
134
|
+
id: factId("component_identity", { componentKey: input.componentKey }),
|
|
135
|
+
kind: "component_identity",
|
|
136
|
+
componentId: asComponentId(input.componentKey),
|
|
137
|
+
componentKey: input.componentKey,
|
|
138
|
+
state: input.state,
|
|
139
|
+
confidence: input.confidence,
|
|
140
|
+
...(input.canonicalTarget ? { canonicalTarget: input.canonicalTarget } : {}),
|
|
141
|
+
...(input.decisionId ? { decisionId: input.decisionId } : {}),
|
|
142
|
+
evidence: [...input.evidence],
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
102
146
|
// ---------------------------------------------------------------------------
|
|
103
147
|
// Classifier facts
|
|
104
148
|
// ---------------------------------------------------------------------------
|
|
@@ -620,7 +664,7 @@ export function makeUsagePropResolvedFact(input: {
|
|
|
620
664
|
export function makeUsageInlineStyleFact(input: {
|
|
621
665
|
nodeId: FactId;
|
|
622
666
|
property: string;
|
|
623
|
-
valueKind: "static" | "number" | "css-variable";
|
|
667
|
+
valueKind: "static" | "number" | "css-variable" | "dynamic-raw";
|
|
624
668
|
value: string;
|
|
625
669
|
}): UsageInlineStyleFact {
|
|
626
670
|
return {
|
package/src/facts/facts.test.ts
CHANGED
|
@@ -10,6 +10,8 @@ import {
|
|
|
10
10
|
compileGlobalGovernanceFacts,
|
|
11
11
|
g,
|
|
12
12
|
makeTokenDefinitionFact,
|
|
13
|
+
makeComponentDefinitionFact,
|
|
14
|
+
makeComponentIdentityFact,
|
|
13
15
|
makeUsageNodeFact,
|
|
14
16
|
} from "../index.js";
|
|
15
17
|
import type { ComponentId, FactId, PropValueForbiddenFact } from "../index.js";
|
|
@@ -151,6 +153,34 @@ describe("fact IR — content-addressed IDs", () => {
|
|
|
151
153
|
expect(a).toBe(b);
|
|
152
154
|
});
|
|
153
155
|
|
|
156
|
+
it("keys definition and identity facts by the portable componentKey", () => {
|
|
157
|
+
const first = makeComponentDefinitionFact({
|
|
158
|
+
file: "src/a/Button.tsx",
|
|
159
|
+
exportName: "Button",
|
|
160
|
+
componentKey: "src/a/Button.tsx#Button",
|
|
161
|
+
renderRoot: { resolution: "intrinsic", tag: "button", interactive: true },
|
|
162
|
+
propSurface: ["disabled"],
|
|
163
|
+
});
|
|
164
|
+
const second = makeComponentDefinitionFact({
|
|
165
|
+
file: "src/b/Button.tsx",
|
|
166
|
+
exportName: "Button",
|
|
167
|
+
componentKey: "src/b/Button.tsx#Button",
|
|
168
|
+
renderRoot: { resolution: "intrinsic", tag: "button", interactive: true },
|
|
169
|
+
propSurface: ["disabled"],
|
|
170
|
+
});
|
|
171
|
+
const identity = makeComponentIdentityFact({
|
|
172
|
+
componentKey: first.componentKey,
|
|
173
|
+
state: "shadow",
|
|
174
|
+
confidence: "confirmed",
|
|
175
|
+
canonicalTarget: "Button",
|
|
176
|
+
evidence: [first.id],
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
expect(first.id).not.toBe(second.id);
|
|
180
|
+
expect(String(identity.componentId)).toBe(first.componentKey);
|
|
181
|
+
expect(identity.evidence).toEqual([first.id]);
|
|
182
|
+
});
|
|
183
|
+
|
|
154
184
|
it("keeps v1 IDs stable across owned package spellings only in typed identity fields", () => {
|
|
155
185
|
const legacyComponentId = asComponentId("@fragments-sdk/ui#Button");
|
|
156
186
|
const currentComponentId = asComponentId("@usefragments/ui#Button");
|
package/src/facts/index.ts
CHANGED
|
@@ -18,6 +18,10 @@ export type {
|
|
|
18
18
|
CanonicalMappingFact,
|
|
19
19
|
CanonicalReplacementFact,
|
|
20
20
|
ComponentLevelFact,
|
|
21
|
+
ComponentDefinitionFact,
|
|
22
|
+
ComponentDefinitionRenderRoot,
|
|
23
|
+
ComponentIdentityFact,
|
|
24
|
+
IdentityFact,
|
|
21
25
|
PolicyFact,
|
|
22
26
|
UsageFact,
|
|
23
27
|
ComponentMetadataFact,
|
|
@@ -73,6 +77,8 @@ export { componentId, asComponentId, factId, hash64Hex, canonicalJson } from "./
|
|
|
73
77
|
export {
|
|
74
78
|
makeComponentMetadataFact,
|
|
75
79
|
makeComponentCapabilityFact,
|
|
80
|
+
makeComponentDefinitionFact,
|
|
81
|
+
makeComponentIdentityFact,
|
|
76
82
|
makePropMetadataFact,
|
|
77
83
|
makePropValueAvoidedFact,
|
|
78
84
|
makePropValueForbiddenFact,
|
package/src/facts/types.ts
CHANGED
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
15
|
import type { GovernanceSeverity } from "../governance.js";
|
|
16
|
+
import type { IdentityConfidence, IdentityState } from "../identity/classify.js";
|
|
16
17
|
|
|
17
18
|
// ---------------------------------------------------------------------------
|
|
18
19
|
// Branded IDs
|
|
@@ -310,7 +311,20 @@ export interface UsageInlineStyleFact extends BaseFact {
|
|
|
310
311
|
kind: "usage_inline_style";
|
|
311
312
|
nodeId: FactId;
|
|
312
313
|
property: string;
|
|
313
|
-
|
|
314
|
+
/**
|
|
315
|
+
* How the extractor resolved the value:
|
|
316
|
+
* - `static` — a string literal (`"#f00"`, `"12px"`).
|
|
317
|
+
* - `number` — a numeric literal (React treats it as `px`).
|
|
318
|
+
* - `css-variable` — a `var(--token)` reference; rules treat it as a token.
|
|
319
|
+
* - `dynamic-raw` — a value constructed at runtime whose *static* fragments
|
|
320
|
+
* unambiguously begin a raw color literal (`` `#${red}` ``,
|
|
321
|
+
* `` `rgb(${r},${g},${b})` ``, `"#" + hex`). `value` carries a reconstructed
|
|
322
|
+
* skeleton with each interpolation rendered as `‹expr›`. Detect-only: a rule
|
|
323
|
+
* may flag it as a raw color but must NEVER attach a deterministic fix
|
|
324
|
+
* (the value is partly runtime). Only color-shaped dynamic values ever
|
|
325
|
+
* surface as facts — a non-color dynamic value stays opaque.
|
|
326
|
+
*/
|
|
327
|
+
valueKind: "static" | "number" | "css-variable" | "dynamic-raw";
|
|
314
328
|
value: string;
|
|
315
329
|
}
|
|
316
330
|
|
|
@@ -561,6 +575,63 @@ export interface CanonicalReplacementFact extends BaseFact {
|
|
|
561
575
|
location?: FactLocation;
|
|
562
576
|
}
|
|
563
577
|
|
|
578
|
+
// ---------------------------------------------------------------------------
|
|
579
|
+
// Component identity facts
|
|
580
|
+
// ---------------------------------------------------------------------------
|
|
581
|
+
|
|
582
|
+
export type ComponentDefinitionRenderRoot =
|
|
583
|
+
| {
|
|
584
|
+
resolution: "canonical";
|
|
585
|
+
canonical: string;
|
|
586
|
+
importSource: string;
|
|
587
|
+
}
|
|
588
|
+
| {
|
|
589
|
+
resolution: "intrinsic";
|
|
590
|
+
tag: string;
|
|
591
|
+
role?: string;
|
|
592
|
+
interactive?: boolean;
|
|
593
|
+
}
|
|
594
|
+
| {
|
|
595
|
+
resolution: "mixed";
|
|
596
|
+
canonicals: string[];
|
|
597
|
+
intrinsics: string[];
|
|
598
|
+
}
|
|
599
|
+
| {
|
|
600
|
+
resolution: "external";
|
|
601
|
+
importSource: string;
|
|
602
|
+
}
|
|
603
|
+
| {
|
|
604
|
+
resolution: "unresolved";
|
|
605
|
+
reason: string;
|
|
606
|
+
};
|
|
607
|
+
|
|
608
|
+
export interface ComponentDefinitionFact extends BaseFact {
|
|
609
|
+
kind: "component_definition";
|
|
610
|
+
/** Index-compatible projection of `componentKey`. */
|
|
611
|
+
componentId: ComponentId;
|
|
612
|
+
file: string;
|
|
613
|
+
exportName: string;
|
|
614
|
+
componentKey: string;
|
|
615
|
+
renderRoot: ComponentDefinitionRenderRoot;
|
|
616
|
+
propSurface: string[];
|
|
617
|
+
stylingChannel?: {
|
|
618
|
+
module: string;
|
|
619
|
+
ownsVisualRole: boolean;
|
|
620
|
+
};
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
export interface ComponentIdentityFact extends BaseFact {
|
|
624
|
+
kind: "component_identity";
|
|
625
|
+
/** Index-compatible projection of `componentKey`. */
|
|
626
|
+
componentId: ComponentId;
|
|
627
|
+
componentKey: string;
|
|
628
|
+
state: IdentityState;
|
|
629
|
+
confidence: IdentityConfidence;
|
|
630
|
+
canonicalTarget?: string;
|
|
631
|
+
decisionId?: string;
|
|
632
|
+
evidence: FactId[];
|
|
633
|
+
}
|
|
634
|
+
|
|
564
635
|
// ---------------------------------------------------------------------------
|
|
565
636
|
// Discriminated unions
|
|
566
637
|
// ---------------------------------------------------------------------------
|
|
@@ -609,7 +680,9 @@ export type UsageFact =
|
|
|
609
680
|
| StructuralConfidenceFact
|
|
610
681
|
| CanonicalReplacementFact;
|
|
611
682
|
|
|
612
|
-
export type
|
|
683
|
+
export type IdentityFact = ComponentDefinitionFact | ComponentIdentityFact;
|
|
684
|
+
|
|
685
|
+
export type Fact = ComponentLevelFact | PolicyFact | UsageFact | IdentityFact;
|
|
613
686
|
|
|
614
687
|
export type FactKind = Fact["kind"];
|
|
615
688
|
|
package/src/governance.test.ts
CHANGED
|
@@ -139,6 +139,32 @@ describe("governance DSL", () => {
|
|
|
139
139
|
});
|
|
140
140
|
});
|
|
141
141
|
|
|
142
|
+
it("preserves top-level component identity decisions", () => {
|
|
143
|
+
const config = defineConfig({
|
|
144
|
+
identity: {
|
|
145
|
+
decisions: [
|
|
146
|
+
{
|
|
147
|
+
component: "src/components/AppDialog.tsx#AppDialog",
|
|
148
|
+
kind: "sanction",
|
|
149
|
+
canonicalTarget: "Dialog",
|
|
150
|
+
reason: "Owns the application modal defaults.",
|
|
151
|
+
decisionId: "local:app-dialog",
|
|
152
|
+
},
|
|
153
|
+
],
|
|
154
|
+
},
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
expect(config.identity?.decisions).toEqual([
|
|
158
|
+
{
|
|
159
|
+
component: "src/components/AppDialog.tsx#AppDialog",
|
|
160
|
+
kind: "sanction",
|
|
161
|
+
canonicalTarget: "Dialog",
|
|
162
|
+
reason: "Owns the application modal defaults.",
|
|
163
|
+
decisionId: "local:app-dialog",
|
|
164
|
+
},
|
|
165
|
+
]);
|
|
166
|
+
});
|
|
167
|
+
|
|
142
168
|
it("rejects unsafe registry canonical source paths", () => {
|
|
143
169
|
expect(() =>
|
|
144
170
|
defineConfig({
|
|
@@ -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
|
+
});
|