@usefragments/core 1.3.0 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/dist/{chunk-MLRDNSSA.js → chunk-HXGE3O2F.js} +39 -1
  2. package/dist/chunk-HXGE3O2F.js.map +1 -0
  3. package/dist/codes/index.d.ts +1 -1
  4. package/dist/codes/index.js +1 -1
  5. package/dist/compiled-types/index.d.ts +1 -1
  6. package/dist/generate/index.d.ts +1 -1
  7. package/dist/{governance-DYSirwJu.d.ts → governance-BLsyk56o.d.ts} +21 -0
  8. package/dist/index.d.ts +324 -82
  9. package/dist/index.js +507 -120
  10. package/dist/index.js.map +1 -1
  11. package/dist/react-types.d.ts +1 -1
  12. package/dist/test-utils.d.ts +1 -1
  13. package/package.json +1 -1
  14. package/src/codes/codes.ts +9 -0
  15. package/src/conform.ts +5 -0
  16. package/src/constants.ts +3 -0
  17. package/src/facts/builders.ts +46 -2
  18. package/src/facts/facts.test.ts +30 -0
  19. package/src/facts/index.ts +6 -0
  20. package/src/facts/types.ts +75 -2
  21. package/src/governance.test.ts +26 -0
  22. package/src/identity/classify.test.ts +185 -0
  23. package/src/identity/classify.ts +257 -0
  24. package/src/identity/component-key.test.ts +32 -0
  25. package/src/identity/component-key.ts +35 -0
  26. package/src/identity/usage.ts +39 -0
  27. package/src/index.ts +25 -0
  28. package/src/rules/components-prefer-library.test.ts +196 -0
  29. package/src/rules/components-prefer-library.ts +68 -1
  30. package/src/rules/components-shadow-component.test.ts +145 -0
  31. package/src/rules/components-shadow-component.ts +81 -0
  32. package/src/rules/fix-availability.ts +1 -0
  33. package/src/rules/index.ts +7 -0
  34. package/src/rules/styles-no-raw-color.test.ts +213 -0
  35. package/src/rules/styles-no-raw-color.ts +125 -0
  36. package/src/rules/taxonomy.test.ts +16 -1
  37. package/src/rules/tiers.ts +7 -5
  38. package/src/schema.ts +21 -2
  39. package/src/token-types.ts +12 -0
  40. package/src/tokens/categories.ts +54 -24
  41. package/src/tokens/scss.test.ts +94 -0
  42. package/src/tokens/scss.ts +11 -4
  43. package/src/types.ts +23 -0
  44. package/dist/chunk-MLRDNSSA.js.map +0 -1
@@ -1,5 +1,5 @@
1
1
  import { ComponentType, ReactNode, JSX } from 'react';
2
- export { F as FragmentDefinition, a as FragmentDefinitionV2 } from './governance-DYSirwJu.js';
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';
@@ -1,4 +1,4 @@
1
- import { C as CompiledBlock, b as CompiledFragment } from './governance-DYSirwJu.js';
1
+ import { C as CompiledBlock, b as CompiledFragment } from './governance-BLsyk56o.js';
2
2
  import 'zod';
3
3
  import 'react';
4
4
  import './topology/index.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@usefragments/core",
3
- "version": "1.3.0",
3
+ "version": "1.5.0",
4
4
  "license": "MIT",
5
5
  "description": "Core types, schemas, and runtime API for Fragments component definitions",
6
6
  "author": "Conan McNicholl",
@@ -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
 
@@ -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 {
@@ -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");
@@ -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,
@@ -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
- valueKind: "static" | "number" | "css-variable";
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 Fact = ComponentLevelFact | PolicyFact | UsageFact;
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
 
@@ -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
+ });