@usefragments/core 1.5.2 → 1.6.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-BAHCOAVG.js → chunk-WVFNDPM4.js} +423 -189
- package/dist/chunk-WVFNDPM4.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-pKrfh517.d.ts → governance-DxFipN5V.d.ts} +609 -20
- package/dist/index.d.ts +638 -43
- package/dist/index.js +304 -37
- 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/__tests__/policy-exclude.test.ts +180 -0
- package/src/canonical-bridge.ts +69 -1
- package/src/canonical-direction.test.ts +118 -0
- package/src/canonical-direction.ts +43 -2
- package/src/codes/__tests__/codes.test.ts +14 -1
- package/src/codes/codes.ts +40 -0
- package/src/facts/builders.ts +35 -0
- package/src/facts/compile.ts +135 -21
- package/src/facts/fact-index.ts +15 -2
- package/src/facts/facts.test.ts +6 -2
- package/src/facts/index.ts +9 -6
- package/src/facts/types.ts +45 -9
- package/src/governance-integrity.test.ts +174 -3
- package/src/governance-integrity.ts +305 -5
- package/src/governance.ts +87 -1
- package/src/index.ts +38 -1
- package/src/policy-exclude.ts +113 -0
- package/src/rules/families.test.ts +69 -0
- package/src/rules/families.ts +52 -0
- package/src/rules/index.ts +6 -0
- package/src/rules/jsx-preferred-import-path.ts +29 -11
- package/src/rules/rules.test.ts +125 -1
- package/src/rules/styles-no-raw-color.ts +13 -4
- package/src/rules/styles-no-raw-dimensions.ts +13 -4
- package/src/rules/styles-no-raw-spacing.ts +12 -4
- package/src/rules/styles-no-raw-typography.ts +13 -4
- package/src/rules/utils.ts +39 -0
- package/dist/chunk-BAHCOAVG.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-DxFipN5V.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
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
compileGlobalGovernanceFacts,
|
|
5
|
+
describePolicyExclude,
|
|
6
|
+
detectUnmatchedPolicyExcludes,
|
|
7
|
+
governanceConfigSchema,
|
|
8
|
+
matchPolicyExclude,
|
|
9
|
+
normalizePolicyExcludes,
|
|
10
|
+
type GovernanceConfig,
|
|
11
|
+
} from "../index.js";
|
|
12
|
+
|
|
13
|
+
describe("policy exclude normalization", () => {
|
|
14
|
+
it("accepts bare globs and { glob, reason } records alike", () => {
|
|
15
|
+
expect(
|
|
16
|
+
normalizePolicyExcludes([
|
|
17
|
+
"src/legacy/**",
|
|
18
|
+
{ glob: "vendor/**", reason: "vendor drop, tracked in DS-611" },
|
|
19
|
+
])
|
|
20
|
+
).toEqual([
|
|
21
|
+
{ glob: "src/legacy/**" },
|
|
22
|
+
{ glob: "vendor/**", reason: "vendor drop, tracked in DS-611" },
|
|
23
|
+
]);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it("returns undefined when nothing usable is authored", () => {
|
|
27
|
+
expect(normalizePolicyExcludes(undefined)).toBeUndefined();
|
|
28
|
+
expect(normalizePolicyExcludes([])).toBeUndefined();
|
|
29
|
+
expect(normalizePolicyExcludes(["", { reason: "no glob" }, 7])).toBeUndefined();
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it("matches `**` across segments and `*` within one", () => {
|
|
33
|
+
const excludes = normalizePolicyExcludes(["src/legacy/vendor/**", "src/*.tsx"]);
|
|
34
|
+
|
|
35
|
+
expect(matchPolicyExclude(excludes, "src/legacy/vendor/deep/Widget.tsx")).toEqual({
|
|
36
|
+
glob: "src/legacy/vendor/**",
|
|
37
|
+
});
|
|
38
|
+
expect(matchPolicyExclude(excludes, "./src/App.tsx")).toEqual({ glob: "src/*.tsx" });
|
|
39
|
+
expect(matchPolicyExclude(excludes, "src\\App.tsx")).toEqual({ glob: "src/*.tsx" });
|
|
40
|
+
expect(matchPolicyExclude(excludes, "src/nested/App.tsx")).toBeUndefined();
|
|
41
|
+
expect(matchPolicyExclude(undefined, "src/App.tsx")).toBeUndefined();
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("describes an exclusion with its scope, glob, and reason", () => {
|
|
45
|
+
expect(
|
|
46
|
+
describePolicyExclude("style.rawColors.forbid", {
|
|
47
|
+
glob: "vendor/**",
|
|
48
|
+
reason: "vendor drop",
|
|
49
|
+
})
|
|
50
|
+
).toBe("style.rawColors.forbid exclude matched vendor/** — vendor drop");
|
|
51
|
+
expect(describePolicyExclude("styles/no-raw-color", { glob: "vendor/**" })).toBe(
|
|
52
|
+
"styles/no-raw-color exclude matched vendor/**"
|
|
53
|
+
);
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
describe("global record excludes compile onto policy facts", () => {
|
|
58
|
+
const policy: GovernanceConfig = {
|
|
59
|
+
styles: [
|
|
60
|
+
{
|
|
61
|
+
kind: "style.rawColors.forbid",
|
|
62
|
+
except: [],
|
|
63
|
+
prefer: "token",
|
|
64
|
+
severity: "warn",
|
|
65
|
+
exclude: [{ glob: "src/legacy/vendor/**", reason: "vendor drop" }],
|
|
66
|
+
},
|
|
67
|
+
],
|
|
68
|
+
jsx: [
|
|
69
|
+
{
|
|
70
|
+
kind: "jsx.importPath.prefer",
|
|
71
|
+
from: "@mui/material",
|
|
72
|
+
to: "@/components",
|
|
73
|
+
severity: "warn",
|
|
74
|
+
exclude: ["src/legacy/vendor/**"],
|
|
75
|
+
},
|
|
76
|
+
],
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
it("is accepted by the shipped schema", () => {
|
|
80
|
+
expect(() => governanceConfigSchema.parse(policy)).not.toThrow();
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it("threads the normalized excludes onto the compiled facts", () => {
|
|
84
|
+
const facts = compileGlobalGovernanceFacts(policy);
|
|
85
|
+
const color = facts.find((fact) => fact.kind === "style_raw_color_forbidden");
|
|
86
|
+
const importPath = facts.find((fact) => fact.kind === "jsx_import_path_preferred");
|
|
87
|
+
|
|
88
|
+
expect(color).toMatchObject({
|
|
89
|
+
exclude: [{ glob: "src/legacy/vendor/**", reason: "vendor drop" }],
|
|
90
|
+
});
|
|
91
|
+
expect(importPath).toMatchObject({ exclude: [{ glob: "src/legacy/vendor/**" }] });
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("keeps a user exclude alive when a preset record shares the same fact id", () => {
|
|
95
|
+
// A resolved policy is `[...presetRecords, ...userRecords]`, and the singleton style
|
|
96
|
+
// kinds are content-addressed on kind alone — so only one of the two survives
|
|
97
|
+
// indexing. The user's exemption must not be the casualty.
|
|
98
|
+
const facts = compileGlobalGovernanceFacts({
|
|
99
|
+
styles: [
|
|
100
|
+
{ kind: "style.rawColors.forbid", except: [], prefer: "token", severity: "warn" },
|
|
101
|
+
{
|
|
102
|
+
kind: "style.rawColors.forbid",
|
|
103
|
+
except: [],
|
|
104
|
+
prefer: "token",
|
|
105
|
+
severity: "warn",
|
|
106
|
+
exclude: ["src/legacy/vendor/**"],
|
|
107
|
+
},
|
|
108
|
+
],
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
const colorFacts = facts.filter((fact) => fact.kind === "style_raw_color_forbidden");
|
|
112
|
+
expect(colorFacts).toHaveLength(2);
|
|
113
|
+
// Both copies carry the union, so whichever the index keeps is the armed one.
|
|
114
|
+
for (const fact of colorFacts) {
|
|
115
|
+
expect(fact).toMatchObject({ exclude: [{ glob: "src/legacy/vendor/**" }] });
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it("leaves fact identity untouched so fingerprints do not move", () => {
|
|
120
|
+
const withExclude = compileGlobalGovernanceFacts(policy);
|
|
121
|
+
const withoutExclude = compileGlobalGovernanceFacts({
|
|
122
|
+
styles: [{ kind: "style.rawColors.forbid", except: [], prefer: "token", severity: "warn" }],
|
|
123
|
+
jsx: [
|
|
124
|
+
{
|
|
125
|
+
kind: "jsx.importPath.prefer",
|
|
126
|
+
from: "@mui/material",
|
|
127
|
+
to: "@/components",
|
|
128
|
+
severity: "warn",
|
|
129
|
+
},
|
|
130
|
+
],
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
expect(withExclude.map((fact) => fact.id)).toEqual(withoutExclude.map((fact) => fact.id));
|
|
134
|
+
// …and a policy with no excludes stays byte-identical to the pre-1.6.0 shape.
|
|
135
|
+
expect(withoutExclude.every((fact) => !("exclude" in fact))).toBe(true);
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
describe("detectUnmatchedPolicyExcludes", () => {
|
|
140
|
+
const policy: GovernanceConfig = {
|
|
141
|
+
styles: [
|
|
142
|
+
{
|
|
143
|
+
kind: "style.rawColors.forbid",
|
|
144
|
+
except: [],
|
|
145
|
+
prefer: "token",
|
|
146
|
+
severity: "warn",
|
|
147
|
+
exclude: ["src/legacy/vendor/**"],
|
|
148
|
+
},
|
|
149
|
+
],
|
|
150
|
+
rules: { "styles/no-raw-spacing": { exclude: ["src/deleted-area/**"] } },
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
it("stays silent when every glob covers a scanned file", () => {
|
|
154
|
+
expect(
|
|
155
|
+
detectUnmatchedPolicyExcludes(policy, [
|
|
156
|
+
"src/legacy/vendor/Widget.tsx",
|
|
157
|
+
"src/deleted-area/Old.tsx",
|
|
158
|
+
])
|
|
159
|
+
).toEqual([]);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it("names each glob that scoped nothing, on both authoring routes", () => {
|
|
163
|
+
const diagnostics = detectUnmatchedPolicyExcludes(policy, ["src/App.tsx"]);
|
|
164
|
+
|
|
165
|
+
expect(diagnostics).toMatchObject([
|
|
166
|
+
{
|
|
167
|
+
code: "FUI9006",
|
|
168
|
+
kind: "unmatched-exclude",
|
|
169
|
+
path: "govern.rules.styles/no-raw-spacing.exclude",
|
|
170
|
+
},
|
|
171
|
+
{ code: "FUI9006", kind: "unmatched-exclude", path: "govern.styles[0].exclude" },
|
|
172
|
+
]);
|
|
173
|
+
expect(diagnostics[0]?.message).toContain("src/deleted-area/**");
|
|
174
|
+
expect(diagnostics[1]?.message).toContain("style.rawColors.forbid");
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it("does not flag excludes when nothing was scanned at all", () => {
|
|
178
|
+
expect(detectUnmatchedPolicyExcludes(policy, [])).toEqual([]);
|
|
179
|
+
});
|
|
180
|
+
});
|
package/src/canonical-bridge.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ContractCanonicalMappingInput } from "./contract/preimage.js";
|
|
2
|
-
import type { CanonicalBridgeV1 } from "./governance.js";
|
|
2
|
+
import type { CanonicalBridgeV1, CanonicalSource } from "./governance.js";
|
|
3
3
|
import { ownedImportMatchesRoot } from "./package-identity-match.js";
|
|
4
4
|
|
|
5
5
|
export function canonicalBridgeUnderlyingKey(input: {
|
|
@@ -28,6 +28,74 @@ export function canonicalBridgeContractMappings(
|
|
|
28
28
|
}));
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
+
/**
|
|
32
|
+
* One authored bridge, projected into the two facts a consumer needs to honor
|
|
33
|
+
* it WITHOUT widening it: the npm canonical source its underlying export
|
|
34
|
+
* implies, and the local component the user actually sanctioned.
|
|
35
|
+
*
|
|
36
|
+
* The pairing is the point. A bridge says "`src/components/Button.tsx#Button`
|
|
37
|
+
* wraps `@mui/material#Button`" — a fact about ONE component. Reading only the
|
|
38
|
+
* source half turns it into "`@mui/material#Button` is canonical in this repo",
|
|
39
|
+
* which is a strictly wider claim: every component in the tree that happens to
|
|
40
|
+
* render that export inherits the sanction it was never given.
|
|
41
|
+
*/
|
|
42
|
+
export interface CanonicalBridgeIdentityBinding {
|
|
43
|
+
/** The npm canonical source this bridge's underlying export implies. */
|
|
44
|
+
source: Extract<CanonicalSource, { kind: "npm" }>;
|
|
45
|
+
/** The sanctioned local component — `<repo-relative-file>#<exportName>`. */
|
|
46
|
+
localComponentKey: string;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* The per-component bindings an authored bridge set implies — one definition,
|
|
51
|
+
* because every consumer of "which components did the user sanction, against
|
|
52
|
+
* which package export" must see the same set.
|
|
53
|
+
*
|
|
54
|
+
* Consumed by `classify`'s RENDER_ROOT_CANONICAL context, which gates the
|
|
55
|
+
* signal on `localComponentKey`: only the component the bridge names may claim
|
|
56
|
+
* the bridge's canonical, so a sanction stays as narrow as the user authored it.
|
|
57
|
+
*/
|
|
58
|
+
export function canonicalBridgeIdentityBindings(
|
|
59
|
+
bridges: readonly CanonicalBridgeV1[] | undefined
|
|
60
|
+
): CanonicalBridgeIdentityBinding[] {
|
|
61
|
+
return (bridges ?? []).map((bridge) => ({
|
|
62
|
+
source: {
|
|
63
|
+
kind: "npm",
|
|
64
|
+
specifier: bridge.underlying.packageName,
|
|
65
|
+
include: [bridge.underlying.exportName],
|
|
66
|
+
},
|
|
67
|
+
localComponentKey: bridge.local.componentKey,
|
|
68
|
+
}));
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* The npm canonical sources an authored bridge set implies — the source half of
|
|
73
|
+
* `canonicalBridgeIdentityBindings`, for consumers that resolve the whole tree
|
|
74
|
+
* at once and scope the result themselves.
|
|
75
|
+
*
|
|
76
|
+
* A bridge states that `underlying.packageName#exportName` IS the canonical
|
|
77
|
+
* thing a local component wraps, so identity resolution treats that export as
|
|
78
|
+
* declared, exactly as an explicit `govern.canonicalSources` npm entry would.
|
|
79
|
+
* Before this projection was shared, a repo whose decision lived only in
|
|
80
|
+
* `canonicalBridges` (hand-authored, or written by `identity sanction`)
|
|
81
|
+
* resolved render roots at `canonical` grade in the pipeline while the
|
|
82
|
+
* classifier saw no declaration at all — the authored fact reached one consumer
|
|
83
|
+
* and silently missed the other.
|
|
84
|
+
*
|
|
85
|
+
* Consumer: the scan pipeline's identity input
|
|
86
|
+
* (`buildCanonicalDirectionIdentityInput`), which resolves render roots for the
|
|
87
|
+
* whole definition set and reports the grade rather than acting on it.
|
|
88
|
+
* Anything that turns the grade into an AUTHORED decision must use
|
|
89
|
+
* `canonicalBridgeIdentityBindings` instead — this projection deliberately
|
|
90
|
+
* drops `local.componentKey`, so on its own it cannot tell a sanctioned wrapper
|
|
91
|
+
* from any other component that renders the same export.
|
|
92
|
+
*/
|
|
93
|
+
export function canonicalBridgeIdentitySources(
|
|
94
|
+
bridges: readonly CanonicalBridgeV1[] | undefined
|
|
95
|
+
): Array<Extract<CanonicalSource, { kind: "npm" }>> {
|
|
96
|
+
return canonicalBridgeIdentityBindings(bridges).map((binding) => binding.source);
|
|
97
|
+
}
|
|
98
|
+
|
|
31
99
|
export function canonicalBridgeMatchesUnderlyingImport(
|
|
32
100
|
bridge: CanonicalBridgeV1,
|
|
33
101
|
source: string,
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
|
|
3
|
+
import { FactIndex, makeComponentDefinitionFact } from "./facts/index.js";
|
|
4
|
+
import type { ComponentDefinitionRenderRoot } from "./facts/index.js";
|
|
5
|
+
import { projectCanonicalDirectionConflicts } from "./canonical-direction.js";
|
|
6
|
+
|
|
7
|
+
const NPM_SOURCE = {
|
|
8
|
+
kind: "npm",
|
|
9
|
+
specifier: "@mui/material",
|
|
10
|
+
include: ["Button", "TextField"],
|
|
11
|
+
} as const;
|
|
12
|
+
|
|
13
|
+
function wrapperDefinition(input: {
|
|
14
|
+
file: string;
|
|
15
|
+
exportName: string;
|
|
16
|
+
renderRoot: ComponentDefinitionRenderRoot;
|
|
17
|
+
exported?: boolean;
|
|
18
|
+
}) {
|
|
19
|
+
return makeComponentDefinitionFact({
|
|
20
|
+
file: input.file,
|
|
21
|
+
exportName: input.exportName,
|
|
22
|
+
exported: input.exported ?? true,
|
|
23
|
+
componentKey: `${input.file}#${input.exportName}`,
|
|
24
|
+
renderRoot: input.renderRoot,
|
|
25
|
+
propSurface: [],
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
describe("projectCanonicalDirectionConflicts", () => {
|
|
30
|
+
it("projects a conflict from a package-identity canonical key", () => {
|
|
31
|
+
const ix = new FactIndex();
|
|
32
|
+
ix.add(
|
|
33
|
+
wrapperDefinition({
|
|
34
|
+
file: "src/components/Button.tsx",
|
|
35
|
+
exportName: "Button",
|
|
36
|
+
renderRoot: {
|
|
37
|
+
resolution: "canonical",
|
|
38
|
+
canonical: "@mui/material#Button",
|
|
39
|
+
importSource: "@mui/material",
|
|
40
|
+
},
|
|
41
|
+
})
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
expect(projectCanonicalDirectionConflicts(ix, [NPM_SOURCE])).toMatchObject([
|
|
45
|
+
{
|
|
46
|
+
packageName: "@mui/material",
|
|
47
|
+
exportName: "Button",
|
|
48
|
+
wrapperComponentKey: "src/components/Button.tsx#Button",
|
|
49
|
+
state: "unresolved",
|
|
50
|
+
},
|
|
51
|
+
]);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
// A canonical package vendored inside the repo (a workspace package or the
|
|
55
|
+
// committed archetype fixture's fake-packages/) resolves the wrapper's render
|
|
56
|
+
// root to its local file identity. The original import specifier is still
|
|
57
|
+
// exact and source-backed — the conflict must not vanish when resolution
|
|
58
|
+
// goes local, or init detects an ambiguity the scan can never see.
|
|
59
|
+
it("projects a conflict through the import source when the canonical key resolved locally", () => {
|
|
60
|
+
const ix = new FactIndex();
|
|
61
|
+
ix.add(
|
|
62
|
+
wrapperDefinition({
|
|
63
|
+
file: "src/components/Button.tsx",
|
|
64
|
+
exportName: "Button",
|
|
65
|
+
renderRoot: {
|
|
66
|
+
resolution: "canonical",
|
|
67
|
+
canonical: "fake-packages/mui-material/index.js#Button",
|
|
68
|
+
importSource: "@mui/material",
|
|
69
|
+
},
|
|
70
|
+
})
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
expect(projectCanonicalDirectionConflicts(ix, [NPM_SOURCE])).toMatchObject([
|
|
74
|
+
{
|
|
75
|
+
packageName: "@mui/material",
|
|
76
|
+
exportName: "Button",
|
|
77
|
+
wrapperComponentKey: "src/components/Button.tsx#Button",
|
|
78
|
+
wrapperFile: "src/components/Button.tsx",
|
|
79
|
+
wrapperExportName: "Button",
|
|
80
|
+
state: "unresolved",
|
|
81
|
+
},
|
|
82
|
+
]);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it("ignores locally-resolved roots whose import source is not a canonical package", () => {
|
|
86
|
+
const ix = new FactIndex();
|
|
87
|
+
ix.add(
|
|
88
|
+
wrapperDefinition({
|
|
89
|
+
file: "src/components/Button.tsx",
|
|
90
|
+
exportName: "Button",
|
|
91
|
+
renderRoot: {
|
|
92
|
+
resolution: "canonical",
|
|
93
|
+
canonical: "packages/other-kit/index.ts#Button",
|
|
94
|
+
importSource: "@acme/other-kit",
|
|
95
|
+
},
|
|
96
|
+
})
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
expect(projectCanonicalDirectionConflicts(ix, [NPM_SOURCE])).toEqual([]);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it("honors the include list on the import-source fallback", () => {
|
|
103
|
+
const ix = new FactIndex();
|
|
104
|
+
ix.add(
|
|
105
|
+
wrapperDefinition({
|
|
106
|
+
file: "src/components/Chip.tsx",
|
|
107
|
+
exportName: "Chip",
|
|
108
|
+
renderRoot: {
|
|
109
|
+
resolution: "canonical",
|
|
110
|
+
canonical: "fake-packages/mui-material/index.js#Chip",
|
|
111
|
+
importSource: "@mui/material",
|
|
112
|
+
},
|
|
113
|
+
})
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
expect(projectCanonicalDirectionConflicts(ix, [NPM_SOURCE])).toEqual([]);
|
|
117
|
+
});
|
|
118
|
+
});
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import type { CanonicalSource } from "./governance.js";
|
|
2
|
-
import type { FactIndex } from "./facts/index.js";
|
|
2
|
+
import type { ComponentDefinitionRenderRoot, FactIndex } from "./facts/index.js";
|
|
3
3
|
import { ownedImportMatchesRoot } from "./package-identity-match.js";
|
|
4
4
|
|
|
5
|
+
type CanonicalRenderRoot = Extract<ComponentDefinitionRenderRoot, { resolution: "canonical" }>;
|
|
6
|
+
|
|
5
7
|
export interface CanonicalDirectionConflict {
|
|
6
8
|
packageName: string;
|
|
7
9
|
exportName: string;
|
|
@@ -32,7 +34,7 @@ export function projectCanonicalDirectionConflicts(
|
|
|
32
34
|
|
|
33
35
|
for (const definition of ix.byKind("component_definition")) {
|
|
34
36
|
if (!definition.exported || definition.renderRoot.resolution !== "canonical") continue;
|
|
35
|
-
const target = npmTarget(definition.renderRoot
|
|
37
|
+
const target = npmTarget(definition.renderRoot, npmSources);
|
|
36
38
|
if (!target) continue;
|
|
37
39
|
|
|
38
40
|
const identity = identityByComponent.get(definition.componentKey);
|
|
@@ -83,6 +85,16 @@ export function canonicalDirectionConflictsTarget(
|
|
|
83
85
|
}
|
|
84
86
|
|
|
85
87
|
function npmTarget(
|
|
88
|
+
renderRoot: CanonicalRenderRoot,
|
|
89
|
+
sources: readonly Extract<CanonicalSource, { kind: "npm" }>[]
|
|
90
|
+
): { packageName: string; exportName: string } | null {
|
|
91
|
+
return (
|
|
92
|
+
npmTargetFromCanonicalKey(renderRoot.canonical, sources) ??
|
|
93
|
+
npmTargetFromImportSource(renderRoot, sources)
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function npmTargetFromCanonicalKey(
|
|
86
98
|
canonicalTarget: string,
|
|
87
99
|
sources: readonly Extract<CanonicalSource, { kind: "npm" }>[]
|
|
88
100
|
): { packageName: string; exportName: string } | null {
|
|
@@ -108,6 +120,35 @@ function npmTarget(
|
|
|
108
120
|
return null;
|
|
109
121
|
}
|
|
110
122
|
|
|
123
|
+
/**
|
|
124
|
+
* A canonical package that also lives in the repo (a vendored fixture package
|
|
125
|
+
* or a workspace package whose package.json name matches the specifier)
|
|
126
|
+
* resolves to its local file identity, so the canonical key carries no package
|
|
127
|
+
* name. The original import specifier still does — and it is just as exact and
|
|
128
|
+
* source-backed, so the conflict must not vanish when resolution goes local
|
|
129
|
+
* (init's detector sees it; the scan must see the same set).
|
|
130
|
+
*/
|
|
131
|
+
function npmTargetFromImportSource(
|
|
132
|
+
renderRoot: CanonicalRenderRoot,
|
|
133
|
+
sources: readonly Extract<CanonicalSource, { kind: "npm" }>[]
|
|
134
|
+
): { packageName: string; exportName: string } | null {
|
|
135
|
+
const separator = renderRoot.canonical.lastIndexOf("#");
|
|
136
|
+
if (separator <= 0 || separator === renderRoot.canonical.length - 1) return null;
|
|
137
|
+
const exportName = renderRoot.canonical
|
|
138
|
+
.slice(separator + 1)
|
|
139
|
+
.split(".")
|
|
140
|
+
.at(-1);
|
|
141
|
+
if (!exportName || exportName === "default") return null;
|
|
142
|
+
|
|
143
|
+
for (const source of sources) {
|
|
144
|
+
if (!ownedImportMatchesRoot(renderRoot.importSource, source.specifier)) continue;
|
|
145
|
+
if (source.exclude?.includes(exportName)) continue;
|
|
146
|
+
if (source.include && !source.include.includes(exportName)) continue;
|
|
147
|
+
return { packageName: source.specifier, exportName };
|
|
148
|
+
}
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
151
|
+
|
|
111
152
|
function packageSubpath(importPath: string, packageName: string): string {
|
|
112
153
|
return importPath === packageName ? "" : importPath.slice(packageName.length + 1);
|
|
113
154
|
}
|
|
@@ -11,7 +11,20 @@ const __filename = fileURLToPath(import.meta.url);
|
|
|
11
11
|
const __dirname = path.dirname(__filename);
|
|
12
12
|
const registryPath = path.resolve(__dirname, "../../../../../docs/codes/registry.json");
|
|
13
13
|
|
|
14
|
-
const SUBSYSTEM_EMITTED_CODES = [
|
|
14
|
+
const SUBSYSTEM_EMITTED_CODES = [
|
|
15
|
+
"FUI9001",
|
|
16
|
+
"FUI9002",
|
|
17
|
+
"FUI9003",
|
|
18
|
+
"FUI9004",
|
|
19
|
+
"FUI9005",
|
|
20
|
+
"FUI9006",
|
|
21
|
+
"FUI9007",
|
|
22
|
+
"FUI9008",
|
|
23
|
+
// Emitted by the CLI scan layer, not the rule engine: the contract's token
|
|
24
|
+
// sources are read before any rule runs, and an unreadable one aborts the
|
|
25
|
+
// scan rather than producing a finding.
|
|
26
|
+
"FUI9009",
|
|
27
|
+
] as const;
|
|
15
28
|
|
|
16
29
|
const RESERVED_CODES = {
|
|
17
30
|
FUI1001: "Reserved for canonical primitive findings; no core rule emits it.",
|
package/src/codes/codes.ts
CHANGED
|
@@ -488,6 +488,46 @@ export const CODES = [
|
|
|
488
488
|
fixAvailable: false,
|
|
489
489
|
evidenceRequired: false,
|
|
490
490
|
}),
|
|
491
|
+
code({
|
|
492
|
+
code: "FUI9006",
|
|
493
|
+
ruleId: "config/unmatched-exclude",
|
|
494
|
+
category: "system",
|
|
495
|
+
defaultSeverity: "moderate",
|
|
496
|
+
title: "Rule exclude matched no scanned file",
|
|
497
|
+
lifecycle: "experimental",
|
|
498
|
+
fixAvailable: false,
|
|
499
|
+
evidenceRequired: false,
|
|
500
|
+
}),
|
|
501
|
+
code({
|
|
502
|
+
code: "FUI9007",
|
|
503
|
+
ruleId: "config/colliding-record",
|
|
504
|
+
category: "system",
|
|
505
|
+
defaultSeverity: "moderate",
|
|
506
|
+
title: "Governance record was dropped as a duplicate",
|
|
507
|
+
lifecycle: "experimental",
|
|
508
|
+
fixAvailable: false,
|
|
509
|
+
evidenceRequired: false,
|
|
510
|
+
}),
|
|
511
|
+
code({
|
|
512
|
+
code: "FUI9008",
|
|
513
|
+
ruleId: "config/overridden-record-severity",
|
|
514
|
+
category: "system",
|
|
515
|
+
defaultSeverity: "moderate",
|
|
516
|
+
title: "Rule override outranks a record's authored severity",
|
|
517
|
+
lifecycle: "experimental",
|
|
518
|
+
fixAvailable: false,
|
|
519
|
+
evidenceRequired: false,
|
|
520
|
+
}),
|
|
521
|
+
code({
|
|
522
|
+
code: "FUI9009",
|
|
523
|
+
ruleId: "contract/unreadable-token-source",
|
|
524
|
+
category: "system",
|
|
525
|
+
defaultSeverity: "critical",
|
|
526
|
+
title: "Contract token source cannot be read",
|
|
527
|
+
lifecycle: "experimental",
|
|
528
|
+
fixAvailable: false,
|
|
529
|
+
evidenceRequired: false,
|
|
530
|
+
}),
|
|
491
531
|
] as const satisfies readonly FuiCode[];
|
|
492
532
|
|
|
493
533
|
export const byCode: ReadonlyMap<string, FuiCode> = new Map(
|