@usefragments/core 1.8.0 → 1.9.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-UUREQ4HD.js → chunk-DGHZQTLH.js} +7 -1
- package/dist/{chunk-UUREQ4HD.js.map → chunk-DGHZQTLH.js.map} +1 -1
- package/dist/codes/index.d.ts +1 -1
- package/dist/codes/index.js +1 -1
- package/dist/{index-Cxk3SOQP.d.ts → index-_sxhUNqx.d.ts} +102 -12
- package/dist/index.d.ts +140 -132
- package/dist/index.js +23 -1
- package/dist/index.js.map +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/package.json +1 -1
- package/src/rules/finding.ts +22 -0
- package/src/rules/jsx-preferred-import-path.ts +18 -0
- package/src/rules/rules.test.ts +114 -0
- package/src/schemas/index.ts +6 -0
package/dist/registry.d.ts
CHANGED
|
@@ -215,7 +215,7 @@ declare const registryComponentSchema: z.ZodObject<{
|
|
|
215
215
|
componentId: string;
|
|
216
216
|
name: string;
|
|
217
217
|
publicRef: string;
|
|
218
|
-
tier: "
|
|
218
|
+
tier: "composition" | "core" | "custom" | "utility";
|
|
219
219
|
provenance: Record<string, unknown>;
|
|
220
220
|
examples: unknown[];
|
|
221
221
|
files: {
|
|
@@ -263,7 +263,7 @@ declare const registryComponentSchema: z.ZodObject<{
|
|
|
263
263
|
status?: string | undefined;
|
|
264
264
|
category?: string | undefined;
|
|
265
265
|
contract?: unknown;
|
|
266
|
-
tier?: "
|
|
266
|
+
tier?: "composition" | "core" | "custom" | "utility" | undefined;
|
|
267
267
|
provenance?: Record<string, unknown> | undefined;
|
|
268
268
|
examples?: unknown[] | undefined;
|
|
269
269
|
styleFiles?: string[] | undefined;
|
|
@@ -497,7 +497,7 @@ declare const registryManifestDraftSchema: z.ZodObject<{
|
|
|
497
497
|
componentId: string;
|
|
498
498
|
name: string;
|
|
499
499
|
publicRef: string;
|
|
500
|
-
tier: "
|
|
500
|
+
tier: "composition" | "core" | "custom" | "utility";
|
|
501
501
|
provenance: Record<string, unknown>;
|
|
502
502
|
examples: unknown[];
|
|
503
503
|
files: {
|
|
@@ -545,7 +545,7 @@ declare const registryManifestDraftSchema: z.ZodObject<{
|
|
|
545
545
|
status?: string | undefined;
|
|
546
546
|
category?: string | undefined;
|
|
547
547
|
contract?: unknown;
|
|
548
|
-
tier?: "
|
|
548
|
+
tier?: "composition" | "core" | "custom" | "utility" | undefined;
|
|
549
549
|
provenance?: Record<string, unknown> | undefined;
|
|
550
550
|
examples?: unknown[] | undefined;
|
|
551
551
|
styleFiles?: string[] | undefined;
|
|
@@ -610,7 +610,7 @@ declare const registryManifestDraftSchema: z.ZodObject<{
|
|
|
610
610
|
componentId: string;
|
|
611
611
|
name: string;
|
|
612
612
|
publicRef: string;
|
|
613
|
-
tier: "
|
|
613
|
+
tier: "composition" | "core" | "custom" | "utility";
|
|
614
614
|
provenance: Record<string, unknown>;
|
|
615
615
|
examples: unknown[];
|
|
616
616
|
files: {
|
|
@@ -712,7 +712,7 @@ declare const registryManifestDraftSchema: z.ZodObject<{
|
|
|
712
712
|
status?: string | undefined;
|
|
713
713
|
category?: string | undefined;
|
|
714
714
|
contract?: unknown;
|
|
715
|
-
tier?: "
|
|
715
|
+
tier?: "composition" | "core" | "custom" | "utility" | undefined;
|
|
716
716
|
provenance?: Record<string, unknown> | undefined;
|
|
717
717
|
examples?: unknown[] | undefined;
|
|
718
718
|
styleFiles?: string[] | undefined;
|
|
@@ -970,7 +970,7 @@ declare const registryManifestSchema: z.ZodObject<{
|
|
|
970
970
|
componentId: string;
|
|
971
971
|
name: string;
|
|
972
972
|
publicRef: string;
|
|
973
|
-
tier: "
|
|
973
|
+
tier: "composition" | "core" | "custom" | "utility";
|
|
974
974
|
provenance: Record<string, unknown>;
|
|
975
975
|
examples: unknown[];
|
|
976
976
|
files: {
|
|
@@ -1018,7 +1018,7 @@ declare const registryManifestSchema: z.ZodObject<{
|
|
|
1018
1018
|
status?: string | undefined;
|
|
1019
1019
|
category?: string | undefined;
|
|
1020
1020
|
contract?: unknown;
|
|
1021
|
-
tier?: "
|
|
1021
|
+
tier?: "composition" | "core" | "custom" | "utility" | undefined;
|
|
1022
1022
|
provenance?: Record<string, unknown> | undefined;
|
|
1023
1023
|
examples?: unknown[] | undefined;
|
|
1024
1024
|
styleFiles?: string[] | undefined;
|
|
@@ -1085,7 +1085,7 @@ declare const registryManifestSchema: z.ZodObject<{
|
|
|
1085
1085
|
componentId: string;
|
|
1086
1086
|
name: string;
|
|
1087
1087
|
publicRef: string;
|
|
1088
|
-
tier: "
|
|
1088
|
+
tier: "composition" | "core" | "custom" | "utility";
|
|
1089
1089
|
provenance: Record<string, unknown>;
|
|
1090
1090
|
examples: unknown[];
|
|
1091
1091
|
files: {
|
|
@@ -1188,7 +1188,7 @@ declare const registryManifestSchema: z.ZodObject<{
|
|
|
1188
1188
|
status?: string | undefined;
|
|
1189
1189
|
category?: string | undefined;
|
|
1190
1190
|
contract?: unknown;
|
|
1191
|
-
tier?: "
|
|
1191
|
+
tier?: "composition" | "core" | "custom" | "utility" | undefined;
|
|
1192
1192
|
provenance?: Record<string, unknown> | undefined;
|
|
1193
1193
|
examples?: unknown[] | undefined;
|
|
1194
1194
|
styleFiles?: string[] | undefined;
|
|
@@ -1449,7 +1449,7 @@ declare const registryArtifactSchema: z.ZodEffects<z.ZodObject<{
|
|
|
1449
1449
|
componentId: string;
|
|
1450
1450
|
name: string;
|
|
1451
1451
|
publicRef: string;
|
|
1452
|
-
tier: "
|
|
1452
|
+
tier: "composition" | "core" | "custom" | "utility";
|
|
1453
1453
|
provenance: Record<string, unknown>;
|
|
1454
1454
|
examples: unknown[];
|
|
1455
1455
|
files: {
|
|
@@ -1497,7 +1497,7 @@ declare const registryArtifactSchema: z.ZodEffects<z.ZodObject<{
|
|
|
1497
1497
|
status?: string | undefined;
|
|
1498
1498
|
category?: string | undefined;
|
|
1499
1499
|
contract?: unknown;
|
|
1500
|
-
tier?: "
|
|
1500
|
+
tier?: "composition" | "core" | "custom" | "utility" | undefined;
|
|
1501
1501
|
provenance?: Record<string, unknown> | undefined;
|
|
1502
1502
|
examples?: unknown[] | undefined;
|
|
1503
1503
|
styleFiles?: string[] | undefined;
|
|
@@ -1564,7 +1564,7 @@ declare const registryArtifactSchema: z.ZodEffects<z.ZodObject<{
|
|
|
1564
1564
|
componentId: string;
|
|
1565
1565
|
name: string;
|
|
1566
1566
|
publicRef: string;
|
|
1567
|
-
tier: "
|
|
1567
|
+
tier: "composition" | "core" | "custom" | "utility";
|
|
1568
1568
|
provenance: Record<string, unknown>;
|
|
1569
1569
|
examples: unknown[];
|
|
1570
1570
|
files: {
|
|
@@ -1667,7 +1667,7 @@ declare const registryArtifactSchema: z.ZodEffects<z.ZodObject<{
|
|
|
1667
1667
|
status?: string | undefined;
|
|
1668
1668
|
category?: string | undefined;
|
|
1669
1669
|
contract?: unknown;
|
|
1670
|
-
tier?: "
|
|
1670
|
+
tier?: "composition" | "core" | "custom" | "utility" | undefined;
|
|
1671
1671
|
provenance?: Record<string, unknown> | undefined;
|
|
1672
1672
|
examples?: unknown[] | undefined;
|
|
1673
1673
|
styleFiles?: string[] | undefined;
|
|
@@ -1772,7 +1772,7 @@ declare const registryArtifactSchema: z.ZodEffects<z.ZodObject<{
|
|
|
1772
1772
|
componentId: string;
|
|
1773
1773
|
name: string;
|
|
1774
1774
|
publicRef: string;
|
|
1775
|
-
tier: "
|
|
1775
|
+
tier: "composition" | "core" | "custom" | "utility";
|
|
1776
1776
|
provenance: Record<string, unknown>;
|
|
1777
1777
|
examples: unknown[];
|
|
1778
1778
|
files: {
|
|
@@ -1885,7 +1885,7 @@ declare const registryArtifactSchema: z.ZodEffects<z.ZodObject<{
|
|
|
1885
1885
|
status?: string | undefined;
|
|
1886
1886
|
category?: string | undefined;
|
|
1887
1887
|
contract?: unknown;
|
|
1888
|
-
tier?: "
|
|
1888
|
+
tier?: "composition" | "core" | "custom" | "utility" | undefined;
|
|
1889
1889
|
provenance?: Record<string, unknown> | undefined;
|
|
1890
1890
|
examples?: unknown[] | undefined;
|
|
1891
1891
|
styleFiles?: string[] | undefined;
|
|
@@ -1978,7 +1978,7 @@ declare const registryArtifactSchema: z.ZodEffects<z.ZodObject<{
|
|
|
1978
1978
|
componentId: string;
|
|
1979
1979
|
name: string;
|
|
1980
1980
|
publicRef: string;
|
|
1981
|
-
tier: "
|
|
1981
|
+
tier: "composition" | "core" | "custom" | "utility";
|
|
1982
1982
|
provenance: Record<string, unknown>;
|
|
1983
1983
|
examples: unknown[];
|
|
1984
1984
|
files: {
|
|
@@ -2091,7 +2091,7 @@ declare const registryArtifactSchema: z.ZodEffects<z.ZodObject<{
|
|
|
2091
2091
|
status?: string | undefined;
|
|
2092
2092
|
category?: string | undefined;
|
|
2093
2093
|
contract?: unknown;
|
|
2094
|
-
tier?: "
|
|
2094
|
+
tier?: "composition" | "core" | "custom" | "utility" | undefined;
|
|
2095
2095
|
provenance?: Record<string, unknown> | undefined;
|
|
2096
2096
|
examples?: unknown[] | undefined;
|
|
2097
2097
|
styleFiles?: string[] | undefined;
|
package/dist/schemas/index.d.ts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
import 'zod';
|
|
2
2
|
export { LegacySeverityLevel, Severity, SeverityLevel, legacySeverityLevelSchema, severityLevelSchema, severitySchema } from '../severity.js';
|
|
3
|
-
export { b as AGENT_FORMAT_SCHEMA_VERSION, c as AgentErrorEnvelope, A as AgentFormat, d as AgentOutput, Q as FactEvidence, R as FactLocation, F as Finding, a as FindingFix, f as FindingReplaceClassTokenFix, g as FindingReplaceComponentFix, h as FindingReplaceImportFix, i as FindingReplacePropValueFix, j as FindingReplaceStyleValueFix, k as Fix, G as GovernanceVerdict, l as GovernanceVerdictMetadata, S as SuppressionDirective, V as ValidatorResult, n as Violation, o as agentErrorEnvelopeSchema, T as agentFindingSchema, p as agentFormatSchema, q as agentIntegritySchema, r as agentOutputSchema, U as agentPlanSchema, t as factEvidenceSchema, u as factLocationSchema, v as findingFixSchema, w as findingReplaceClassTokenFixSchema, x as findingReplaceComponentFixSchema, y as findingReplaceImportFixSchema, z as findingReplacePropValueFixSchema, B as findingReplaceStyleValueFixSchema, C as findingSchema, D as fixSchema, H as governanceVerdictMetadataSchema, J as governanceVerdictSchema, K as normalizeFinding, L as normalizeSeverity, M as normalizeViolation, N as suppressionDirectiveSchema, O as validatorResultSchema, P as violationSchema } from '../index-
|
|
3
|
+
export { b as AGENT_FORMAT_SCHEMA_VERSION, c as AgentErrorEnvelope, A as AgentFormat, d as AgentOutput, Q as FactEvidence, R as FactLocation, F as Finding, a as FindingFix, f as FindingReplaceClassTokenFix, g as FindingReplaceComponentFix, h as FindingReplaceImportFix, i as FindingReplacePropValueFix, j as FindingReplaceStyleValueFix, k as Fix, G as GovernanceVerdict, l as GovernanceVerdictMetadata, S as SuppressionDirective, V as ValidatorResult, n as Violation, o as agentErrorEnvelopeSchema, T as agentFindingSchema, p as agentFormatSchema, q as agentIntegritySchema, r as agentOutputSchema, U as agentPlanSchema, t as factEvidenceSchema, u as factLocationSchema, v as findingFixSchema, w as findingReplaceClassTokenFixSchema, x as findingReplaceComponentFixSchema, y as findingReplaceImportFixSchema, z as findingReplacePropValueFixSchema, B as findingReplaceStyleValueFixSchema, C as findingSchema, D as fixSchema, H as governanceVerdictMetadataSchema, J as governanceVerdictSchema, K as normalizeFinding, L as normalizeSeverity, M as normalizeViolation, N as suppressionDirectiveSchema, O as validatorResultSchema, P as violationSchema } from '../index-_sxhUNqx.js';
|
package/dist/schemas/index.js
CHANGED
package/package.json
CHANGED
package/src/rules/finding.ts
CHANGED
|
@@ -28,17 +28,38 @@ export interface MakeFindingInput {
|
|
|
28
28
|
evidence: FactEvidence[];
|
|
29
29
|
evidenceGrade?: EvidenceGrade;
|
|
30
30
|
fingerprintIdentity: Record<string, unknown>;
|
|
31
|
+
/**
|
|
32
|
+
* Legacy identity tuple from before a fingerprint migration. When present,
|
|
33
|
+
* the finding also carries `previousFingerprint` (the legacy hash) so
|
|
34
|
+
* baseline classification can match records keyed on the old hash during
|
|
35
|
+
* the grace window. Position keys are permitted here — legacy identities
|
|
36
|
+
* are reproduced verbatim, contract violations included.
|
|
37
|
+
*/
|
|
38
|
+
previousFingerprintIdentity?: Record<string, unknown>;
|
|
31
39
|
fix?: FindingFix;
|
|
32
40
|
attributes?: Record<string, unknown>;
|
|
33
41
|
}
|
|
34
42
|
|
|
43
|
+
const POSITION_IDENTITY_KEYS = ["line", "column", "endLine", "endColumn", "offset", "location"];
|
|
44
|
+
|
|
35
45
|
export function makeFinding(input: MakeFindingInput): Finding {
|
|
36
46
|
if (input.evidence.length === 0) {
|
|
37
47
|
throw new Error(`makeFinding(${input.ruleId}): findings must carry at least one evidence fact`);
|
|
38
48
|
}
|
|
49
|
+
const positionKeys = POSITION_IDENTITY_KEYS.filter((key) => key in input.fingerprintIdentity);
|
|
50
|
+
if (positionKeys.length > 0) {
|
|
51
|
+
throw new Error(
|
|
52
|
+
`makeFinding(${input.ruleId}): fingerprintIdentity must not contain position keys ` +
|
|
53
|
+
`(${positionKeys.join(", ")}) — fingerprints hash what's wrong, never where it sits. ` +
|
|
54
|
+
`Use a source-order occurrence index for repeats of the same identity.`
|
|
55
|
+
);
|
|
56
|
+
}
|
|
39
57
|
const fingerprint = hash64Hex(
|
|
40
58
|
canonicalJson({ ruleId: input.ruleId, ...input.fingerprintIdentity })
|
|
41
59
|
);
|
|
60
|
+
const previousFingerprint = input.previousFingerprintIdentity
|
|
61
|
+
? hash64Hex(canonicalJson({ ruleId: input.ruleId, ...input.previousFingerprintIdentity }))
|
|
62
|
+
: undefined;
|
|
42
63
|
const code = byRuleId.get(input.ruleId);
|
|
43
64
|
return normalizeFinding({
|
|
44
65
|
ruleId: input.ruleId,
|
|
@@ -48,6 +69,7 @@ export function makeFinding(input: MakeFindingInput): Finding {
|
|
|
48
69
|
helpUrl: code?.explainUrl,
|
|
49
70
|
message: input.message,
|
|
50
71
|
fingerprint,
|
|
72
|
+
...(previousFingerprint !== undefined ? { previousFingerprint } : {}),
|
|
51
73
|
location: input.location,
|
|
52
74
|
evidence: input.evidence,
|
|
53
75
|
evidenceGrade: input.evidenceGrade ?? "source_backed",
|
|
@@ -13,6 +13,12 @@ export function ruleJsxPreferredImportPath(ix: FactIndex): Finding[] {
|
|
|
13
13
|
|
|
14
14
|
const findings: Finding[] = [];
|
|
15
15
|
const seen = new Set<string>();
|
|
16
|
+
// Source-order occurrence index per identity tuple: the fingerprint contract
|
|
17
|
+
// (finding.ts) forbids position, so repeats of the same offending import in
|
|
18
|
+
// one file are distinguished by ordinal, not line/column. Deleting the first
|
|
19
|
+
// occurrence promotes the second to its index — debt count stays truthful
|
|
20
|
+
// while edits elsewhere in the file cannot re-key anything.
|
|
21
|
+
const occurrences = new Map<string, number>();
|
|
16
22
|
|
|
17
23
|
for (const usage of ix.byKind("usage_import")) {
|
|
18
24
|
// A confirmed bridge supersedes any non-bridge preference over the same
|
|
@@ -41,6 +47,9 @@ export function ruleJsxPreferredImportPath(ix: FactIndex): Finding[] {
|
|
|
41
47
|
const key = `${policy.id}\0${usage.file}\0${usage.source}\0${usage.location.line}\0${usage.location.column}\0${importedIdentity}`;
|
|
42
48
|
if (seen.has(key)) continue;
|
|
43
49
|
seen.add(key);
|
|
50
|
+
const identityKey = `${usage.file}\0${usage.source}\0${policy.to}\0${importedIdentity}`;
|
|
51
|
+
const occurrence = occurrences.get(identityKey) ?? 0;
|
|
52
|
+
occurrences.set(identityKey, occurrence + 1);
|
|
44
53
|
|
|
45
54
|
findings.push(
|
|
46
55
|
makeFinding({
|
|
@@ -53,6 +62,15 @@ export function ruleJsxPreferredImportPath(ix: FactIndex): Finding[] {
|
|
|
53
62
|
location: usage.location,
|
|
54
63
|
evidence: ix.evidence([usage.id, policy.id]),
|
|
55
64
|
fingerprintIdentity: {
|
|
65
|
+
file: usage.file,
|
|
66
|
+
from: usage.source,
|
|
67
|
+
to: policy.to,
|
|
68
|
+
imported: policy.imported,
|
|
69
|
+
occurrence,
|
|
70
|
+
},
|
|
71
|
+
// Grace window (baseline v1 → v2): the pre-migration hash keyed on
|
|
72
|
+
// line/column. Drop with the v1 dual-match after one release.
|
|
73
|
+
previousFingerprintIdentity: {
|
|
56
74
|
file: usage.file,
|
|
57
75
|
from: usage.source,
|
|
58
76
|
to: policy.to,
|
package/src/rules/rules.test.ts
CHANGED
|
@@ -37,6 +37,7 @@ import {
|
|
|
37
37
|
} from "../index.js";
|
|
38
38
|
import type { CanonicalBridgeV1, Fact, FactId } from "../index.js";
|
|
39
39
|
import { RULE_VERSION as A11Y_REQUIRED_ACCESSIBLE_NAME_VERSION } from "./a11y-required-accessible-name.js";
|
|
40
|
+
import { makeFinding } from "./finding.js";
|
|
40
41
|
|
|
41
42
|
type ButtonProps = {
|
|
42
43
|
variant?: "primary" | "secondary" | "ghost" | "link";
|
|
@@ -438,6 +439,78 @@ describe("preferred JSX imports and components", () => {
|
|
|
438
439
|
});
|
|
439
440
|
});
|
|
440
441
|
|
|
442
|
+
it("keeps preferred-path fingerprints stable across line drift", () => {
|
|
443
|
+
const findingsAtLine = (line: number) => {
|
|
444
|
+
const ix = new FactIndex();
|
|
445
|
+
ix.addMany(
|
|
446
|
+
compileGlobalGovernanceFacts({
|
|
447
|
+
jsx: [g.jsx.importPath().prefer("@legacy/ui", "@usefragments/ui", { severity: "error" })],
|
|
448
|
+
})
|
|
449
|
+
);
|
|
450
|
+
ix.add(
|
|
451
|
+
makeUsageImportFact({
|
|
452
|
+
file: "apps/checkout/page.tsx",
|
|
453
|
+
local: "Button",
|
|
454
|
+
imported: "Button",
|
|
455
|
+
source: "@legacy/ui",
|
|
456
|
+
location: { file: "apps/checkout/page.tsx", line, column: 0 },
|
|
457
|
+
})
|
|
458
|
+
);
|
|
459
|
+
return ruleJsxPreferredImportPath(ix);
|
|
460
|
+
};
|
|
461
|
+
|
|
462
|
+
const before = findingsAtLine(1);
|
|
463
|
+
const after = findingsAtLine(9);
|
|
464
|
+
|
|
465
|
+
expect(after[0].fingerprint).toBe(before[0].fingerprint);
|
|
466
|
+
// Byte-pinned hashes: the legacy value is what a released v1 CLI recorded
|
|
467
|
+
// for this exact tuple (hash64Hex(canonicalJson({ruleId, file, from, to,
|
|
468
|
+
// line, column}))). If previousFingerprintIdentity ever drifts from the
|
|
469
|
+
// true v1 formula, real baselines mass-invalidate — this literal is the
|
|
470
|
+
// tripwire.
|
|
471
|
+
expect(before[0].fingerprint).toBe("4deee0ff7bb1810b");
|
|
472
|
+
expect(before[0].previousFingerprint).toBe("c04344ad7a96b651");
|
|
473
|
+
// The grace-window legacy hash still tracks position, so a v1 baseline
|
|
474
|
+
// recorded at either line matches exactly one of the two runs.
|
|
475
|
+
expect(after[0].previousFingerprint).not.toBe(before[0].previousFingerprint);
|
|
476
|
+
});
|
|
477
|
+
|
|
478
|
+
it("distinguishes repeated same-identity imports by source-order occurrence", () => {
|
|
479
|
+
// Two imports of the same specifier under different local aliases share the
|
|
480
|
+
// fingerprint identity tuple (local never enters the hash) — the ordinal
|
|
481
|
+
// tells them apart. Byte-identical duplicates collapse at the fact layer.
|
|
482
|
+
const findingsFor = (locals: readonly string[]) => {
|
|
483
|
+
const ix = new FactIndex();
|
|
484
|
+
ix.addMany(
|
|
485
|
+
compileGlobalGovernanceFacts({
|
|
486
|
+
jsx: [g.jsx.importPath().prefer("@legacy/ui", "@usefragments/ui", { severity: "error" })],
|
|
487
|
+
})
|
|
488
|
+
);
|
|
489
|
+
locals.forEach((local, index) => {
|
|
490
|
+
ix.add(
|
|
491
|
+
makeUsageImportFact({
|
|
492
|
+
file: "apps/checkout/page.tsx",
|
|
493
|
+
local,
|
|
494
|
+
imported: "Button",
|
|
495
|
+
source: "@legacy/ui",
|
|
496
|
+
location: { file: "apps/checkout/page.tsx", line: index * 10 + 1, column: 0 },
|
|
497
|
+
})
|
|
498
|
+
);
|
|
499
|
+
});
|
|
500
|
+
return ruleJsxPreferredImportPath(ix);
|
|
501
|
+
};
|
|
502
|
+
|
|
503
|
+
const both = findingsFor(["Button", "AliasedButton"]);
|
|
504
|
+
expect(both).toHaveLength(2);
|
|
505
|
+
expect(both[0].fingerprint).not.toBe(both[1].fingerprint);
|
|
506
|
+
|
|
507
|
+
// Deleting the first occurrence promotes the survivor to its ordinal, so
|
|
508
|
+
// the remaining debt matches a fingerprint the baseline already accepts.
|
|
509
|
+
const survivor = findingsFor(["AliasedButton"]);
|
|
510
|
+
expect(survivor).toHaveLength(1);
|
|
511
|
+
expect(survivor[0].fingerprint).toBe(both[0].fingerprint);
|
|
512
|
+
});
|
|
513
|
+
|
|
441
514
|
it("emits a deterministic component replacement for canonical component mappings", () => {
|
|
442
515
|
const legacyButtonId = makeComponentId("@legacy/ui", "LegacyButton");
|
|
443
516
|
const ix = new FactIndex();
|
|
@@ -4163,6 +4236,47 @@ describe("styles/no-raw-dimensions", () => {
|
|
|
4163
4236
|
});
|
|
4164
4237
|
});
|
|
4165
4238
|
|
|
4239
|
+
// ---------------------------------------------------------------------------
|
|
4240
|
+
// makeFinding fingerprint contract
|
|
4241
|
+
// ---------------------------------------------------------------------------
|
|
4242
|
+
|
|
4243
|
+
describe("makeFinding fingerprint contract", () => {
|
|
4244
|
+
const base = {
|
|
4245
|
+
ruleId: "styles/no-raw-color",
|
|
4246
|
+
ruleVersion: "1",
|
|
4247
|
+
severity: "serious" as const,
|
|
4248
|
+
message: "Raw color",
|
|
4249
|
+
location: { file: "src/App.tsx", line: 3, column: 1 },
|
|
4250
|
+
evidence: [{ factId: "fact-1" as FactId, fact: { kind: "style_declaration" } }],
|
|
4251
|
+
};
|
|
4252
|
+
|
|
4253
|
+
it("rejects position keys in fingerprintIdentity", () => {
|
|
4254
|
+
expect(() =>
|
|
4255
|
+
makeFinding({
|
|
4256
|
+
...base,
|
|
4257
|
+
fingerprintIdentity: { file: "src/App.tsx", value: "#fff", line: 3, column: 1 },
|
|
4258
|
+
})
|
|
4259
|
+
).toThrow(/position keys \(line, column\)/);
|
|
4260
|
+
});
|
|
4261
|
+
|
|
4262
|
+
it("hashes a legacy identity into previousFingerprint when provided", () => {
|
|
4263
|
+
const migrated = makeFinding({
|
|
4264
|
+
...base,
|
|
4265
|
+
fingerprintIdentity: { file: "src/App.tsx", value: "#fff", occurrence: 0 },
|
|
4266
|
+
previousFingerprintIdentity: { file: "src/App.tsx", value: "#fff", line: 3, column: 1 },
|
|
4267
|
+
});
|
|
4268
|
+
const plain = makeFinding({
|
|
4269
|
+
...base,
|
|
4270
|
+
fingerprintIdentity: { file: "src/App.tsx", value: "#fff", occurrence: 0 },
|
|
4271
|
+
});
|
|
4272
|
+
|
|
4273
|
+
expect(migrated.fingerprint).toBe(plain.fingerprint);
|
|
4274
|
+
expect(migrated.previousFingerprint).toBeDefined();
|
|
4275
|
+
expect(migrated.previousFingerprint).not.toBe(migrated.fingerprint);
|
|
4276
|
+
expect(plain.previousFingerprint).toBeUndefined();
|
|
4277
|
+
});
|
|
4278
|
+
});
|
|
4279
|
+
|
|
4166
4280
|
// Type witness so eslint/tsc don't trip on unused imports above.
|
|
4167
4281
|
const _typeWitness: Fact[] = [];
|
|
4168
4282
|
void _typeWitness;
|
package/src/schemas/index.ts
CHANGED
|
@@ -116,6 +116,12 @@ export const findingSchema = z.object({
|
|
|
116
116
|
helpUrl: z.string().url().optional(),
|
|
117
117
|
message: z.string(),
|
|
118
118
|
fingerprint: z.string(),
|
|
119
|
+
/**
|
|
120
|
+
* Fingerprint this finding carried before a fingerprint-identity migration.
|
|
121
|
+
* Emitted only by migrated rules during the grace window so baseline
|
|
122
|
+
* records keyed on the legacy hash keep matching instead of churning.
|
|
123
|
+
*/
|
|
124
|
+
previousFingerprint: z.string().optional(),
|
|
119
125
|
location: factLocationSchema,
|
|
120
126
|
evidence: z.array(factEvidenceSchema).min(1),
|
|
121
127
|
evidenceGrade: evidenceGradeSchema.optional(),
|