@usefragments/core 2.0.0 → 2.0.2
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-BMPYIUZE.js → chunk-QPOKQ5H6.js} +8 -2
- package/dist/chunk-QPOKQ5H6.js.map +1 -0
- package/dist/codes/index.js +1 -1
- package/dist/index.d.ts +18 -4
- package/dist/index.js +158 -66
- package/dist/index.js.map +1 -1
- package/dist/schemas/index.js +1 -1
- package/package.json +1 -1
- package/src/canonical-source.ts +41 -0
- package/src/evaluation/canonical-facts-trust.test.ts +337 -0
- package/src/evaluation/evaluate.ts +67 -10
- package/src/evaluation/evaluation-v2-receipt-v1.test.ts +1 -1
- package/src/evaluation/index.ts +1 -1
- package/src/evaluation/types.ts +5 -3
- package/src/facts/builders.ts +4 -0
- package/src/facts/fact-index.ts +4 -0
- package/src/facts/facts.test.ts +70 -0
- package/src/facts/types.ts +4 -0
- package/src/identity/usage.test.ts +64 -0
- package/src/identity/usage.ts +7 -1
- package/src/index.ts +6 -0
- package/src/rules/components-prefer-library.ts +110 -33
- package/dist/chunk-BMPYIUZE.js.map +0 -1
package/dist/schemas/index.js
CHANGED
package/package.json
CHANGED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { CanonicalSource } from "./governance.js";
|
|
2
|
+
|
|
3
|
+
/** Export filters narrow source ownership; an empty include approves nothing. */
|
|
4
|
+
export function canonicalSourceIncludesExport(source: CanonicalSource, name: string): boolean {
|
|
5
|
+
const role = name.split(".").at(-1) ?? name;
|
|
6
|
+
const matches = (names: readonly string[]) => names.includes(name) || names.includes(role);
|
|
7
|
+
return (
|
|
8
|
+
!matches(source.exclude ?? []) && (source.include === undefined || matches(source.include))
|
|
9
|
+
);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/** Filesystem ownership only. Package import specifiers are not local paths. */
|
|
13
|
+
export function canonicalSourceContainsFile(source: CanonicalSource, file: string): boolean {
|
|
14
|
+
const root =
|
|
15
|
+
source.kind === "directory"
|
|
16
|
+
? source.path
|
|
17
|
+
: source.kind === "registry"
|
|
18
|
+
? source.installPath
|
|
19
|
+
: source.implementationPath;
|
|
20
|
+
if (!root) return false;
|
|
21
|
+
const normalize = (value: string) =>
|
|
22
|
+
value.replace(/\\/g, "/").replace(/^\.\//, "").replace(/\/$/, "");
|
|
23
|
+
const normalizedRoot = normalize(root);
|
|
24
|
+
const normalizedFile = normalize(file);
|
|
25
|
+
return (
|
|
26
|
+
normalizedRoot === "." ||
|
|
27
|
+
normalizedFile === normalizedRoot ||
|
|
28
|
+
normalizedFile.startsWith(`${normalizedRoot}/`)
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function isCanonicalSourceDefinition(
|
|
33
|
+
file: string,
|
|
34
|
+
exportName: string,
|
|
35
|
+
sources: readonly CanonicalSource[]
|
|
36
|
+
): boolean {
|
|
37
|
+
return sources.some(
|
|
38
|
+
(source) =>
|
|
39
|
+
canonicalSourceContainsFile(source, file) && canonicalSourceIncludesExport(source, exportName)
|
|
40
|
+
);
|
|
41
|
+
}
|
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { buildAnalysisPlanV1 } from "../analysis-plan/index.js";
|
|
3
|
+
import {
|
|
4
|
+
FactIndex,
|
|
5
|
+
compileGlobalGovernanceFacts,
|
|
6
|
+
makeComponentDefinitionFact,
|
|
7
|
+
makeComponentIdentityFact,
|
|
8
|
+
makeContractTokenFact,
|
|
9
|
+
makeStyleDeclarationFact,
|
|
10
|
+
makeUsageNodeFact,
|
|
11
|
+
makeUsageImportFact,
|
|
12
|
+
makeTokenDefinitionFact,
|
|
13
|
+
} from "../facts/index.js";
|
|
14
|
+
import { repositoryBindingDigestV1 } from "../repository-binding.js";
|
|
15
|
+
import { contractHash } from "../contract/hash.js";
|
|
16
|
+
import { CONTRACT_PREIMAGE_SCHEMA } from "../contract/preimage.js";
|
|
17
|
+
import { g } from "../governance.js";
|
|
18
|
+
import { runRules } from "../rules/index.js";
|
|
19
|
+
import { evaluate } from "./evaluate.js";
|
|
20
|
+
import { EVALUATOR_VERSION_V2, LEGACY_EVALUATOR_VERSION_V2 } from "./types.js";
|
|
21
|
+
import type { EvaluationInput, EvaluationInputV2 } from "./types.js";
|
|
22
|
+
|
|
23
|
+
const preimage = {
|
|
24
|
+
schema: CONTRACT_PREIMAGE_SCHEMA,
|
|
25
|
+
domains: {
|
|
26
|
+
components: "1".repeat(64),
|
|
27
|
+
tokens: "2".repeat(64),
|
|
28
|
+
canonicalMap: "3".repeat(64),
|
|
29
|
+
policy: "4".repeat(64),
|
|
30
|
+
},
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
function input(facts: FactIndex, overrides: Partial<EvaluationInput> = {}): EvaluationInput {
|
|
34
|
+
return {
|
|
35
|
+
contract: { state: "pinned", fcid: contractHash(preimage), preimage },
|
|
36
|
+
scope: { kind: "full", coverage: { filesScanned: 2 } },
|
|
37
|
+
evidence: { kind: "facts", facts },
|
|
38
|
+
policy: { failOnWarnings: false },
|
|
39
|
+
baseline: { identityVerified: true, findingStates: {} },
|
|
40
|
+
trust: { source: "verified", mappings: {} },
|
|
41
|
+
integrity: { governance: "healthy" },
|
|
42
|
+
evaluatedAt: 1_788_000_000_000,
|
|
43
|
+
...overrides,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function shadow(confidence: "confirmed" | "likely" | "review" = "confirmed") {
|
|
48
|
+
const facts = new FactIndex();
|
|
49
|
+
facts.addMany(
|
|
50
|
+
compileGlobalGovernanceFacts({
|
|
51
|
+
rules: { "components/shadow-component": { enabled: true, severity: "error" } },
|
|
52
|
+
})
|
|
53
|
+
);
|
|
54
|
+
const definition = makeComponentDefinitionFact({
|
|
55
|
+
file: "src/SaveButton.tsx",
|
|
56
|
+
exportName: "SaveButton",
|
|
57
|
+
componentKey: "src/SaveButton.tsx#SaveButton",
|
|
58
|
+
renderRoot: { resolution: "intrinsic", tag: "button", interactive: true },
|
|
59
|
+
propSurface: ["className", "type"],
|
|
60
|
+
});
|
|
61
|
+
const usage = makeUsageNodeFact({
|
|
62
|
+
file: definition.file,
|
|
63
|
+
nodePath: "0",
|
|
64
|
+
element: "button",
|
|
65
|
+
location: { file: definition.file, line: 3, column: 1 },
|
|
66
|
+
});
|
|
67
|
+
facts.addMany([
|
|
68
|
+
definition,
|
|
69
|
+
usage,
|
|
70
|
+
makeComponentIdentityFact({
|
|
71
|
+
componentKey: definition.componentKey,
|
|
72
|
+
state: "shadow",
|
|
73
|
+
confidence,
|
|
74
|
+
canonicalTarget: "Button",
|
|
75
|
+
evidence: [definition.id, usage.id],
|
|
76
|
+
}),
|
|
77
|
+
]);
|
|
78
|
+
return facts;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
describe("canonical facts supply mapping evidence", () => {
|
|
82
|
+
it("blocks a confirmed shadow using the same facts that establish its canonical target", () => {
|
|
83
|
+
const result = evaluate(input(shadow()));
|
|
84
|
+
expect(result.verdict).toBe("block");
|
|
85
|
+
expect(result.counts).toMatchObject({ blocked: 1, pendingCloudTrust: 0 });
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it.each(["likely", "review"] as const)("keeps %s shadows from earning a block", (confidence) => {
|
|
89
|
+
expect(evaluate(input(shadow(confidence))).counts.blocked).toBe(0);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it("retains explicit negative and unknown mapping judgments", () => {
|
|
93
|
+
const facts = shadow();
|
|
94
|
+
const fingerprint = runRules(facts)[0]!.fingerprint;
|
|
95
|
+
for (const mapping of ["unconfirmed", "unknown"] as const) {
|
|
96
|
+
const result = evaluate(
|
|
97
|
+
input(facts, { trust: { source: "verified", mappings: { [fingerprint]: mapping } } })
|
|
98
|
+
);
|
|
99
|
+
expect(result.counts.blocked).toBe(0);
|
|
100
|
+
expect(result.verdict).toBe(mapping === "unknown" ? "indeterminate" : "pass");
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it("still requires source verification and a verified baseline", () => {
|
|
105
|
+
expect(
|
|
106
|
+
evaluate(input(shadow(), { trust: { source: "unverified", mappings: {} } })).counts.blocked
|
|
107
|
+
).toBe(0);
|
|
108
|
+
expect(evaluate(input(shadow(), { baseline: { identityVerified: false } })).verdict).toBe(
|
|
109
|
+
"indeterminate"
|
|
110
|
+
);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it("preserves accepted debt and approved waivers", () => {
|
|
114
|
+
const facts = shadow();
|
|
115
|
+
const fingerprint = runRules(facts)[0]!.fingerprint;
|
|
116
|
+
expect(
|
|
117
|
+
evaluate(
|
|
118
|
+
input(facts, {
|
|
119
|
+
baseline: { identityVerified: true, findingStates: { [fingerprint]: "existing" } },
|
|
120
|
+
})
|
|
121
|
+
).verdict
|
|
122
|
+
).toBe("pass");
|
|
123
|
+
expect(evaluate(input(facts, { waivers: [{ fingerprint }] })).verdict).toBe("pass");
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it("does not grant trust to an arbitrary findings-only report", () => {
|
|
127
|
+
const finding = runRules(shadow())[0]!;
|
|
128
|
+
const result = evaluate(
|
|
129
|
+
input(new FactIndex(), {
|
|
130
|
+
evidence: {
|
|
131
|
+
kind: "findings",
|
|
132
|
+
findings: [
|
|
133
|
+
{ fingerprint: finding.fingerprint, ruleId: finding.ruleId, severity: "error" },
|
|
134
|
+
],
|
|
135
|
+
},
|
|
136
|
+
})
|
|
137
|
+
);
|
|
138
|
+
expect(result.verdict).toBe("indeterminate");
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it("blocks an undefined token against the contract vocabulary", () => {
|
|
142
|
+
const facts = new FactIndex();
|
|
143
|
+
facts.addMany(
|
|
144
|
+
compileGlobalGovernanceFacts({
|
|
145
|
+
styles: [g.styles.cssVars().mustBeDefined({ severity: "error" })],
|
|
146
|
+
})
|
|
147
|
+
);
|
|
148
|
+
facts.addMany([
|
|
149
|
+
makeContractTokenFact({ name: "--color-text" }),
|
|
150
|
+
makeStyleDeclarationFact({
|
|
151
|
+
file: "src/proof.css",
|
|
152
|
+
selector: ".save-proof",
|
|
153
|
+
declarationPath: "0",
|
|
154
|
+
property: "color",
|
|
155
|
+
value: "var(--color-proof-undefined)",
|
|
156
|
+
location: { file: "src/proof.css", line: 1, column: 1 },
|
|
157
|
+
}),
|
|
158
|
+
]);
|
|
159
|
+
const result = evaluate(input(facts));
|
|
160
|
+
expect(result.findings).toHaveLength(1);
|
|
161
|
+
expect(result.verdict).toBe("block");
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it.each([
|
|
166
|
+
{ severity: "error", element: "button", verdict: "block" },
|
|
167
|
+
{ severity: "warn", element: "button", verdict: "pass" },
|
|
168
|
+
{ severity: "error", element: "Button", verdict: "pass" },
|
|
169
|
+
] as const)(
|
|
170
|
+
"preserves $severity/$element eligibility for canonical library substitutions",
|
|
171
|
+
({ severity, element, verdict }) => {
|
|
172
|
+
const facts = new FactIndex();
|
|
173
|
+
facts.addMany(
|
|
174
|
+
compileGlobalGovernanceFacts({
|
|
175
|
+
rules: {
|
|
176
|
+
"components/prefer-library": {
|
|
177
|
+
enabled: true,
|
|
178
|
+
severity,
|
|
179
|
+
options: {
|
|
180
|
+
canonicalSources: [{ kind: "npm", specifier: "@design/ui", include: ["Button"] }],
|
|
181
|
+
},
|
|
182
|
+
},
|
|
183
|
+
},
|
|
184
|
+
})
|
|
185
|
+
);
|
|
186
|
+
facts.addMany([
|
|
187
|
+
makeUsageNodeFact({
|
|
188
|
+
file: "src/App.tsx",
|
|
189
|
+
nodePath: "0",
|
|
190
|
+
element,
|
|
191
|
+
location: { file: "src/App.tsx", line: 2, column: 1 },
|
|
192
|
+
}),
|
|
193
|
+
makeUsageImportFact({
|
|
194
|
+
file: "src/App.tsx",
|
|
195
|
+
local: "Button",
|
|
196
|
+
imported: "Button",
|
|
197
|
+
source: "./Button",
|
|
198
|
+
location: { file: "src/App.tsx", line: 1, column: 1 },
|
|
199
|
+
}),
|
|
200
|
+
]);
|
|
201
|
+
const result = evaluate(input(facts));
|
|
202
|
+
expect(result.findings).toHaveLength(1);
|
|
203
|
+
expect(result.verdict).toBe(verdict);
|
|
204
|
+
expect(result.counts.pendingCloudTrust).toBe(0);
|
|
205
|
+
}
|
|
206
|
+
);
|
|
207
|
+
|
|
208
|
+
it.each(["error", "warn"] as const)(
|
|
209
|
+
"preserves %s policy for authored upstream token drift",
|
|
210
|
+
(severity) => {
|
|
211
|
+
const facts = new FactIndex();
|
|
212
|
+
facts.addMany(
|
|
213
|
+
compileGlobalGovernanceFacts({
|
|
214
|
+
rules: { "tokens/upstream-drift": { enabled: true, severity } },
|
|
215
|
+
})
|
|
216
|
+
);
|
|
217
|
+
facts.addMany([
|
|
218
|
+
makeTokenDefinitionFact({
|
|
219
|
+
name: "--brand",
|
|
220
|
+
value: "#111111",
|
|
221
|
+
category: "color",
|
|
222
|
+
role: "local",
|
|
223
|
+
authority: "authored",
|
|
224
|
+
sourceIdentity: "tokens.css",
|
|
225
|
+
location: { file: "tokens.css", line: 1, column: 1 },
|
|
226
|
+
}),
|
|
227
|
+
makeTokenDefinitionFact({
|
|
228
|
+
name: "--brand",
|
|
229
|
+
value: "#222222",
|
|
230
|
+
category: "color",
|
|
231
|
+
role: "upstream",
|
|
232
|
+
authority: "declared-package",
|
|
233
|
+
sourceIdentity: "@design/ui/fragments.json",
|
|
234
|
+
location: { file: "@design/ui/fragments.json", line: 1, column: 1 },
|
|
235
|
+
}),
|
|
236
|
+
]);
|
|
237
|
+
const result = evaluate(input(facts));
|
|
238
|
+
expect(result.findings).toHaveLength(1);
|
|
239
|
+
expect(result.verdict).toBe(severity === "error" ? "block" : "pass");
|
|
240
|
+
expect(result.counts.pendingCloudTrust).toBe(0);
|
|
241
|
+
}
|
|
242
|
+
);
|
|
243
|
+
|
|
244
|
+
function versionedInput(evaluatorVersion: string): EvaluationInputV2 {
|
|
245
|
+
const base = input(shadow());
|
|
246
|
+
const binding = {
|
|
247
|
+
provider: "github",
|
|
248
|
+
providerInstanceId: "github.com",
|
|
249
|
+
repositoryExternalId: "proof",
|
|
250
|
+
bindingId: "binding-proof",
|
|
251
|
+
};
|
|
252
|
+
const analysisPlan = buildAnalysisPlanV1({
|
|
253
|
+
binding,
|
|
254
|
+
source: { commitId: "proof-head", defaultBranch: "main", acquiredBy: "github_app" },
|
|
255
|
+
contract: { fcid: contractHash(preimage), artifactDigest: "a".repeat(64) },
|
|
256
|
+
config: { path: "fragments.config.ts", digest: "b".repeat(64) },
|
|
257
|
+
profile: { id: "react-web-v1", version: "1", capabilities: ["tsx"] },
|
|
258
|
+
regions: [
|
|
259
|
+
{
|
|
260
|
+
regionId: "tsx",
|
|
261
|
+
pathPattern: "src/**/*.tsx",
|
|
262
|
+
kind: "component_source",
|
|
263
|
+
language: "typescript",
|
|
264
|
+
required: true,
|
|
265
|
+
requiredDialects: ["tsx"],
|
|
266
|
+
analyzer: { id: "react", interfaceVersion: "1", implementationVersion: "1" },
|
|
267
|
+
},
|
|
268
|
+
],
|
|
269
|
+
limits: {
|
|
270
|
+
maxSourceFiles: 1000,
|
|
271
|
+
maxSourceBytes: 10000000,
|
|
272
|
+
maxFileBytes: 1000000,
|
|
273
|
+
maxFacts: 10000,
|
|
274
|
+
maxRegions: 100,
|
|
275
|
+
maxDiagnostics: 1000,
|
|
276
|
+
maxDurationMs: 60000,
|
|
277
|
+
},
|
|
278
|
+
evaluatorVersion,
|
|
279
|
+
createdAt: "2026-09-08T00:00:00.000Z",
|
|
280
|
+
});
|
|
281
|
+
return {
|
|
282
|
+
schemaVersion: 2,
|
|
283
|
+
binding,
|
|
284
|
+
analysisPlan,
|
|
285
|
+
contract: { fcid: contractHash(preimage), preimage, active: true },
|
|
286
|
+
factSchemaVersion: "facts:v1",
|
|
287
|
+
facts: shadow(),
|
|
288
|
+
factsDigest: "c".repeat(64),
|
|
289
|
+
coverage: [
|
|
290
|
+
{ regionId: "tsx", state: "analyzed", fileCount: 2, byteCount: 400, diagnosticIds: [] },
|
|
291
|
+
],
|
|
292
|
+
policy: base.policy,
|
|
293
|
+
trust: base.trust,
|
|
294
|
+
baseline: {
|
|
295
|
+
identity: {
|
|
296
|
+
bindingDigest: repositoryBindingDigestV1(binding),
|
|
297
|
+
repositoryExternalId: binding.repositoryExternalId,
|
|
298
|
+
fcid: contractHash(preimage),
|
|
299
|
+
sourceCommitId: "proof-base",
|
|
300
|
+
generationId: "baseline-proof",
|
|
301
|
+
},
|
|
302
|
+
findingStates: {},
|
|
303
|
+
},
|
|
304
|
+
waivers: [],
|
|
305
|
+
integrity: {
|
|
306
|
+
governance: "healthy",
|
|
307
|
+
inertConfigDiagnosticCount: 0,
|
|
308
|
+
profile: { state: "known", profileId: "react-web-v1", profileVersion: "1" },
|
|
309
|
+
facts: { state: "healthy", coalescedDuplicateCount: 0 },
|
|
310
|
+
},
|
|
311
|
+
evaluatedAt: base.evaluatedAt,
|
|
312
|
+
evaluatorVersion,
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
it("replays old captured semantics with the original input and result digests", () => {
|
|
317
|
+
const result = evaluate(versionedInput(LEGACY_EVALUATOR_VERSION_V2));
|
|
318
|
+
expect(result.verdict).toBe("indeterminate");
|
|
319
|
+
// Captured by running the pre-change evaluator from commit 90eeb5c.
|
|
320
|
+
expect(result.inputDigest).toBe(
|
|
321
|
+
"af1097a55bd36b726d35a70570589a4512e16e69b1ef1156f7d94a47e7d93919"
|
|
322
|
+
);
|
|
323
|
+
expect(result.resultDigest).toBe(
|
|
324
|
+
"e0395a9149a5de73e28b3295d3dc94833895286d7ec70371ab54bd392fa72954"
|
|
325
|
+
);
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
it("gives corrected semantics a distinct plan, input and result identity", () => {
|
|
329
|
+
const legacy = versionedInput(LEGACY_EVALUATOR_VERSION_V2);
|
|
330
|
+
const current = versionedInput(EVALUATOR_VERSION_V2);
|
|
331
|
+
const previous = evaluate(legacy);
|
|
332
|
+
const result = evaluate(current);
|
|
333
|
+
expect(result.verdict).toBe("block");
|
|
334
|
+
expect(current.analysisPlan.digest).not.toBe(legacy.analysisPlan.digest);
|
|
335
|
+
expect(result.inputDigest).not.toBe(previous.inputDigest);
|
|
336
|
+
expect(result.resultDigest).not.toBe(previous.resultDigest);
|
|
337
|
+
});
|
|
@@ -32,6 +32,7 @@ import {
|
|
|
32
32
|
} from "../contract/hash.js";
|
|
33
33
|
import { EVIDENCE_ORDER, canBlock, type EvidenceGrade } from "../evidence.js";
|
|
34
34
|
import { runRules } from "../rules/index.js";
|
|
35
|
+
import type { Finding } from "../rules/types.js";
|
|
35
36
|
import { configuredFindingSeverity, readRuleConfig } from "../rules/rule-config.js";
|
|
36
37
|
import { severityLevel } from "../severity.js";
|
|
37
38
|
import { tierFor } from "../rules/tiers.js";
|
|
@@ -41,6 +42,7 @@ import type {
|
|
|
41
42
|
EvaluationFindingInput,
|
|
42
43
|
EvaluationInput,
|
|
43
44
|
EvaluationInputV2,
|
|
45
|
+
EvaluationMappingTrust,
|
|
44
46
|
EvaluationReason,
|
|
45
47
|
EvaluationReasonV2,
|
|
46
48
|
EvaluationResult,
|
|
@@ -48,7 +50,7 @@ import type {
|
|
|
48
50
|
EvaluationVerdict,
|
|
49
51
|
EvaluationWaiver,
|
|
50
52
|
} from "./types.js";
|
|
51
|
-
import { EVALUATOR_VERSION, EVALUATOR_VERSION_V2 } from "./types.js";
|
|
53
|
+
import { EVALUATOR_VERSION, EVALUATOR_VERSION_V2, LEGACY_EVALUATOR_VERSION_V2 } from "./types.js";
|
|
52
54
|
import { evaluationInputV2Schema, evaluationResultV2Schema } from "./receipt.js";
|
|
53
55
|
|
|
54
56
|
type TrustFactState = "affirmative" | "negative" | "unknown";
|
|
@@ -99,7 +101,7 @@ function isEvaluationInputV2(
|
|
|
99
101
|
return "schemaVersion" in input && input.schemaVersion === 2;
|
|
100
102
|
}
|
|
101
103
|
|
|
102
|
-
function evaluateV1(input: EvaluationInput): EvaluationResult {
|
|
104
|
+
function evaluateV1(input: EvaluationInput, deriveCanonicalTrust = true): EvaluationResult {
|
|
103
105
|
const reasons = new Set<EvaluationReason>();
|
|
104
106
|
|
|
105
107
|
// ---- Contract identity (the kernel recomputes the FCID itself) ----------
|
|
@@ -163,7 +165,7 @@ function evaluateV1(input: EvaluationInput): EvaluationResult {
|
|
|
163
165
|
// is merged field-by-field — a synthetic row could fabricate gating
|
|
164
166
|
// evidence no input row carries — and reordering never changes the result
|
|
165
167
|
// because rows are canonically sorted before classification.
|
|
166
|
-
const evidence = evidenceFindings(input);
|
|
168
|
+
const evidence = evidenceFindings(input, deriveCanonicalTrust);
|
|
167
169
|
const findings = [...groupByFingerprint(evidence).values()].map((rows) => {
|
|
168
170
|
const classified = rows.map((row) => classifyFinding(row, input, activeWaivers));
|
|
169
171
|
return classified.reduce((hardest, candidate) =>
|
|
@@ -198,7 +200,7 @@ function evaluateV1(input: EvaluationInput): EvaluationResult {
|
|
|
198
200
|
gatingFindingsPresent: counts.blocked > 0,
|
|
199
201
|
};
|
|
200
202
|
|
|
201
|
-
const inputDigest = sha256Hex(canonicalPreimage(normalizeInputForDigest(input)));
|
|
203
|
+
const inputDigest = sha256Hex(canonicalPreimage(normalizeInputForDigest(input, evidence)));
|
|
202
204
|
const partial = {
|
|
203
205
|
evaluatorVersion: EVALUATOR_VERSION,
|
|
204
206
|
verdict,
|
|
@@ -238,7 +240,7 @@ const V2_INTEGRITY_REASONS = new Set<EvaluationReasonV2>([
|
|
|
238
240
|
]);
|
|
239
241
|
|
|
240
242
|
/**
|
|
241
|
-
*
|
|
243
|
+
* Supports the current first-party evaluator and exact replay of its predecessor. Unknown
|
|
242
244
|
* generations are never interpreted optimistically: evaluator/fact schema
|
|
243
245
|
* mismatches reject the call, while an unknown analysis profile remains a
|
|
244
246
|
* typed indeterminate result so coverage can explain the unsupported source.
|
|
@@ -247,7 +249,10 @@ const SUPPORTED_FACT_SCHEMA_VERSIONS_V2 = new Set(["facts:v1"]);
|
|
|
247
249
|
const SUPPORTED_REACT_WEB_PROFILE_VERSIONS_V1 = new Set(["1"]);
|
|
248
250
|
|
|
249
251
|
function evaluateV2(rawInput: EvaluationInputV2): EvaluationResultV2 {
|
|
250
|
-
if (
|
|
252
|
+
if (
|
|
253
|
+
rawInput.evaluatorVersion !== EVALUATOR_VERSION_V2 &&
|
|
254
|
+
rawInput.evaluatorVersion !== LEGACY_EVALUATOR_VERSION_V2
|
|
255
|
+
) {
|
|
251
256
|
throw new Error(`Unsupported evaluator version: ${rawInput.evaluatorVersion}`);
|
|
252
257
|
}
|
|
253
258
|
if (!SUPPORTED_FACT_SCHEMA_VERSIONS_V2.has(rawInput.factSchemaVersion)) {
|
|
@@ -295,7 +300,12 @@ function evaluateV2(rawInput: EvaluationInputV2): EvaluationResultV2 {
|
|
|
295
300
|
},
|
|
296
301
|
evaluatedAt: input.evaluatedAt,
|
|
297
302
|
};
|
|
298
|
-
|
|
303
|
+
// Captured V2 inputs must replay byte-for-byte under their original semantics.
|
|
304
|
+
// New plans always select the current generation through EVALUATOR_VERSION_V2.
|
|
305
|
+
const compatibilityResult = evaluateV1(
|
|
306
|
+
compatibilityInput,
|
|
307
|
+
input.evaluatorVersion === EVALUATOR_VERSION_V2
|
|
308
|
+
);
|
|
299
309
|
|
|
300
310
|
const requiredCoverage =
|
|
301
311
|
coverageSummary.requiredRegions === 0
|
|
@@ -407,7 +417,10 @@ function normalizeInputV2ForDigest(
|
|
|
407
417
|
// Evidence
|
|
408
418
|
// ---------------------------------------------------------------------------
|
|
409
419
|
|
|
410
|
-
function evidenceFindings(
|
|
420
|
+
function evidenceFindings(
|
|
421
|
+
input: EvaluationInput,
|
|
422
|
+
deriveCanonicalTrust: boolean
|
|
423
|
+
): EvaluationFindingInput[] {
|
|
411
424
|
if (input.evidence.kind === "findings") return [...input.evidence.findings];
|
|
412
425
|
// Facts mode: run the core rules engine inside the kernel (spec §1 — the
|
|
413
426
|
// kernel extends the `runRules(FactIndex)` seed). `runRules` is pure and
|
|
@@ -416,15 +429,59 @@ function evidenceFindings(input: EvaluationInput): EvaluationFindingInput[] {
|
|
|
416
429
|
// fact) is applied here exactly as the engine applies it to the findings it
|
|
417
430
|
// prints — otherwise a rule the config demoted to `info` gates the verdict.
|
|
418
431
|
const ruleConfig = readRuleConfig(input.evidence.facts);
|
|
432
|
+
const hasContractTokens = input.evidence.facts.byKind("contract_token").length > 0;
|
|
419
433
|
return runRules(input.evidence.facts).map((finding) => ({
|
|
420
434
|
fingerprint: finding.fingerprint,
|
|
421
435
|
ruleId: finding.ruleId,
|
|
422
436
|
severity: severityLevel(configuredFindingSeverity(finding, ruleConfig)),
|
|
423
437
|
advisory: finding.attributes?.advisory === true,
|
|
424
438
|
evidenceGrade: finding.evidenceGrade,
|
|
439
|
+
// An explicit trust decision still wins, including unknown/unconfirmed.
|
|
440
|
+
// Only findings derived here from canonical facts earn the fallback.
|
|
441
|
+
...(deriveCanonicalTrust
|
|
442
|
+
? {
|
|
443
|
+
mappingTrust:
|
|
444
|
+
input.trust?.mappings[finding.fingerprint] ??
|
|
445
|
+
canonicalFactMappingTrust(finding, hasContractTokens),
|
|
446
|
+
}
|
|
447
|
+
: {}),
|
|
425
448
|
}));
|
|
426
449
|
}
|
|
427
450
|
|
|
451
|
+
/** Rules already resolve the approved vocabulary when producing facts-mode
|
|
452
|
+
* findings. Preserve that evidence instead of guessing again from an import
|
|
453
|
+
* path (definition-level shadows have none). Findings-only reports cannot use
|
|
454
|
+
* this path; their caller must supply independently verified mapping trust. */
|
|
455
|
+
function canonicalFactMappingTrust(
|
|
456
|
+
finding: Finding,
|
|
457
|
+
hasContractTokens: boolean
|
|
458
|
+
): EvaluationMappingTrust | undefined {
|
|
459
|
+
switch (finding.ruleId) {
|
|
460
|
+
case "components/shadow-component":
|
|
461
|
+
return finding.attributes?.confidence === "confirmed" &&
|
|
462
|
+
typeof finding.attributes.canonicalTarget === "string" &&
|
|
463
|
+
finding.attributes.canonicalTarget.length > 0
|
|
464
|
+
? "confirmed"
|
|
465
|
+
: undefined;
|
|
466
|
+
case "components/prefer-library":
|
|
467
|
+
return typeof finding.attributes?.suggestedComponent === "string" &&
|
|
468
|
+
finding.attributes.suggestedComponent.length > 0
|
|
469
|
+
? "confirmed"
|
|
470
|
+
: undefined;
|
|
471
|
+
case "tokens/css-vars-must-be-defined":
|
|
472
|
+
return hasContractTokens ? "confirmed" : undefined;
|
|
473
|
+
case "tokens/upstream-drift":
|
|
474
|
+
// This rule only emits for one unambiguous authored local/upstream pair.
|
|
475
|
+
return finding.evidence.some(
|
|
476
|
+
({ fact }) => fact.kind === "token_definition" && "role" in fact && fact.role === "upstream"
|
|
477
|
+
)
|
|
478
|
+
? "confirmed"
|
|
479
|
+
: undefined;
|
|
480
|
+
default:
|
|
481
|
+
return undefined;
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
|
|
428
485
|
/** Hardest state first: an earned block outranks a pending hold outranks an
|
|
429
486
|
* advisory downgrade. */
|
|
430
487
|
const STATE_RANK = { blocked: 0, pending_cloud_trust: 1, advisory: 2 } as const;
|
|
@@ -640,12 +697,12 @@ function mappingFact(
|
|
|
640
697
|
* DERIVED findings (a `FactIndex` is a class instance, not a canonical value);
|
|
641
698
|
* two inputs that judge identically digest identically.
|
|
642
699
|
*/
|
|
643
|
-
function normalizeInputForDigest(input: EvaluationInput) {
|
|
700
|
+
function normalizeInputForDigest(input: EvaluationInput, evidence: EvaluationFindingInput[]) {
|
|
644
701
|
return {
|
|
645
702
|
schema: "evaluation-input:v1",
|
|
646
703
|
contract: input.contract,
|
|
647
704
|
scope: input.scope,
|
|
648
|
-
evidence: canonicalRows(
|
|
705
|
+
evidence: canonicalRows(evidence),
|
|
649
706
|
policy: input.policy,
|
|
650
707
|
baseline: input.baseline,
|
|
651
708
|
adoption: input.adoption,
|
|
@@ -685,7 +685,7 @@ describe("evaluate V2", () => {
|
|
|
685
685
|
});
|
|
686
686
|
|
|
687
687
|
it("rejects evaluator and fact-schema versions it cannot interpret", () => {
|
|
688
|
-
expect(EVALUATOR_VERSION_V2).toBe("evaluation-kernel:v2");
|
|
688
|
+
expect(EVALUATOR_VERSION_V2).toBe("evaluation-kernel:v2.1");
|
|
689
689
|
expect(() =>
|
|
690
690
|
evaluate({ ...evaluationInput(), evaluatorVersion: "evaluation-kernel:v3" })
|
|
691
691
|
).toThrow(/Unsupported evaluator version/u);
|
package/src/evaluation/index.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* `apps/cloud/src/lib/govern/evaluation-adapter.ts`.
|
|
5
5
|
*/
|
|
6
6
|
export { evaluate, evaluationInputDigestV2, evaluationResultDigestV2 } from "./evaluate.js";
|
|
7
|
-
export { EVALUATOR_VERSION, EVALUATOR_VERSION_V2 } from "./types.js";
|
|
7
|
+
export { EVALUATOR_VERSION, EVALUATOR_VERSION_V2, LEGACY_EVALUATOR_VERSION_V2 } from "./types.js";
|
|
8
8
|
export {
|
|
9
9
|
EVALUATION_MAX_FINDINGS_V2,
|
|
10
10
|
EVALUATION_MAX_MAP_ENTRIES_V2,
|
package/src/evaluation/types.ts
CHANGED
|
@@ -26,9 +26,11 @@ import type { FactIndex } from "../facts/fact-index.js";
|
|
|
26
26
|
import type { RepositoryBindingKeyV1 } from "../repository-binding.js";
|
|
27
27
|
|
|
28
28
|
/** Bumped when kernel semantics change in a way that can flip a verdict. */
|
|
29
|
-
export const EVALUATOR_VERSION = "evaluation-kernel:v1" as const;
|
|
30
|
-
/**
|
|
31
|
-
export const EVALUATOR_VERSION_V2 = "evaluation-kernel:v2" as const;
|
|
29
|
+
export const EVALUATOR_VERSION = "evaluation-kernel:v1.1" as const;
|
|
30
|
+
/** Current facts-only evaluator. The input schema remains V2. */
|
|
31
|
+
export const EVALUATOR_VERSION_V2 = "evaluation-kernel:v2.1" as const;
|
|
32
|
+
/** Replay-only generation: its captured mapping judgments remain authoritative. */
|
|
33
|
+
export const LEGACY_EVALUATOR_VERSION_V2 = "evaluation-kernel:v2" as const;
|
|
32
34
|
|
|
33
35
|
/**
|
|
34
36
|
* Four-state per-finding model (spec §2). `candidate` is the derivation step —
|
package/src/facts/builders.ts
CHANGED
|
@@ -122,6 +122,7 @@ export function makeComponentDefinitionFact(input: {
|
|
|
122
122
|
componentKey: string;
|
|
123
123
|
renderRoot: ComponentDefinitionFact["renderRoot"];
|
|
124
124
|
propSurface: string[];
|
|
125
|
+
location?: FactLocation;
|
|
125
126
|
stylingChannel?: ComponentDefinitionFact["stylingChannel"];
|
|
126
127
|
}): ComponentDefinitionFact {
|
|
127
128
|
return {
|
|
@@ -134,6 +135,7 @@ export function makeComponentDefinitionFact(input: {
|
|
|
134
135
|
componentKey: input.componentKey,
|
|
135
136
|
renderRoot: input.renderRoot,
|
|
136
137
|
propSurface: [...input.propSurface],
|
|
138
|
+
...(input.location ? { location: { ...input.location } } : {}),
|
|
137
139
|
...(input.stylingChannel ? { stylingChannel: { ...input.stylingChannel } } : {}),
|
|
138
140
|
};
|
|
139
141
|
}
|
|
@@ -664,6 +666,7 @@ export function makeUsageNodeFact(input: {
|
|
|
664
666
|
export function makeUsageComponentFact(input: {
|
|
665
667
|
nodeId: FactId;
|
|
666
668
|
componentId: ComponentId;
|
|
669
|
+
definitionKey?: string;
|
|
667
670
|
}): UsageComponentFact {
|
|
668
671
|
return {
|
|
669
672
|
id: factId("usage_component", {
|
|
@@ -673,6 +676,7 @@ export function makeUsageComponentFact(input: {
|
|
|
673
676
|
kind: "usage_component",
|
|
674
677
|
nodeId: input.nodeId,
|
|
675
678
|
componentId: input.componentId,
|
|
679
|
+
...(input.definitionKey ? { definitionKey: input.definitionKey } : {}),
|
|
676
680
|
};
|
|
677
681
|
}
|
|
678
682
|
|
package/src/facts/fact-index.ts
CHANGED
|
@@ -87,6 +87,10 @@ function logicalFactForComparison(fact: Fact): Record<string, unknown> {
|
|
|
87
87
|
const { location: _location, sourceNames: _sourceNames, ...logicalFact } = fact;
|
|
88
88
|
return logicalFact;
|
|
89
89
|
}
|
|
90
|
+
if (fact.kind === "component_definition") {
|
|
91
|
+
const { location: _location, ...logicalFact } = fact;
|
|
92
|
+
return { ...logicalFact, componentId: logicalComponentId(fact.componentId) };
|
|
93
|
+
}
|
|
90
94
|
if (fact.kind === "jsx_import_path_preferred") {
|
|
91
95
|
return {
|
|
92
96
|
...fact,
|