@usefragments/core 2.0.1 → 2.1.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/codes/index.d.ts +1 -1
- package/dist/compiled-types/index.d.ts +1 -1
- package/dist/generate/index.d.ts +1 -1
- package/dist/{index-C8bcXVav.d.ts → index-xn5CNdf4.d.ts} +451 -451
- package/dist/index.d.ts +1438 -1426
- package/dist/index.js +5074 -4583
- package/dist/index.js.map +1 -1
- package/dist/manifest.d.ts +24 -24
- package/dist/preview-runtime.d.ts +1 -1
- package/dist/react-types.d.ts +1 -1
- package/dist/registry.d.ts +380 -380
- package/dist/schemas/index.d.ts +1 -1
- package/dist/{governance-hOPXGbbs.d.ts → source-identity-DvOBz2yJ.d.ts} +675 -610
- package/dist/storyAdapter.d.ts +1 -1
- package/dist/test-utils.d.ts +1 -1
- package/package.json +1 -1
- package/src/conform.ts +10 -0
- package/src/contract/index.ts +24 -0
- package/src/contract/preimage.ts +17 -3
- package/src/contract/source-identity.test.ts +167 -0
- package/src/contract/source-identity.ts +303 -0
- package/src/evaluation/canonical-facts-trust.test.ts +398 -0
- package/src/evaluation/evaluate.ts +84 -11
- package/src/evaluation/evaluation-v2-receipt-v1.test.ts +28 -2
- package/src/evaluation/index.ts +7 -1
- package/src/evaluation/receipt.ts +17 -2
- package/src/evaluation/types.ts +19 -4
- package/src/index.ts +29 -0
- package/src/rules/components-prefer-library-classnames.test.ts +354 -0
- package/src/rules/components-prefer-library.test.ts +246 -0
- package/src/rules/components-prefer-library.ts +340 -61
|
@@ -0,0 +1,398 @@
|
|
|
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 {
|
|
17
|
+
CONTRACT_PREIMAGE_SCHEMA,
|
|
18
|
+
CONTRACT_PREIMAGE_SCHEMA_V2,
|
|
19
|
+
type ContractPreimage,
|
|
20
|
+
} from "../contract/preimage.js";
|
|
21
|
+
import { g } from "../governance.js";
|
|
22
|
+
import { runRules } from "../rules/index.js";
|
|
23
|
+
import { evaluate } from "./evaluate.js";
|
|
24
|
+
import {
|
|
25
|
+
EVALUATOR_VERSION_V2,
|
|
26
|
+
SOURCE_IDENTITY_EVALUATOR_VERSION_V2,
|
|
27
|
+
LEGACY_EVALUATOR_VERSION_V2,
|
|
28
|
+
} from "./types.js";
|
|
29
|
+
import type { EvaluationInput, EvaluationInputV2 } from "./types.js";
|
|
30
|
+
|
|
31
|
+
const preimage = {
|
|
32
|
+
schema: CONTRACT_PREIMAGE_SCHEMA,
|
|
33
|
+
domains: {
|
|
34
|
+
components: "1".repeat(64),
|
|
35
|
+
tokens: "2".repeat(64),
|
|
36
|
+
canonicalMap: "3".repeat(64),
|
|
37
|
+
policy: "4".repeat(64),
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
function input(facts: FactIndex, overrides: Partial<EvaluationInput> = {}): EvaluationInput {
|
|
42
|
+
return {
|
|
43
|
+
contract: { state: "pinned", fcid: contractHash(preimage), preimage },
|
|
44
|
+
scope: { kind: "full", coverage: { filesScanned: 2 } },
|
|
45
|
+
evidence: { kind: "facts", facts },
|
|
46
|
+
policy: { failOnWarnings: false },
|
|
47
|
+
baseline: { identityVerified: true, findingStates: {} },
|
|
48
|
+
trust: { source: "verified", mappings: {} },
|
|
49
|
+
integrity: { governance: "healthy" },
|
|
50
|
+
evaluatedAt: 1_788_000_000_000,
|
|
51
|
+
...overrides,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function shadow(confidence: "confirmed" | "likely" | "review" = "confirmed") {
|
|
56
|
+
const facts = new FactIndex();
|
|
57
|
+
facts.addMany(
|
|
58
|
+
compileGlobalGovernanceFacts({
|
|
59
|
+
rules: { "components/shadow-component": { enabled: true, severity: "error" } },
|
|
60
|
+
})
|
|
61
|
+
);
|
|
62
|
+
const definition = makeComponentDefinitionFact({
|
|
63
|
+
file: "src/SaveButton.tsx",
|
|
64
|
+
exportName: "SaveButton",
|
|
65
|
+
componentKey: "src/SaveButton.tsx#SaveButton",
|
|
66
|
+
renderRoot: { resolution: "intrinsic", tag: "button", interactive: true },
|
|
67
|
+
propSurface: ["className", "type"],
|
|
68
|
+
});
|
|
69
|
+
const usage = makeUsageNodeFact({
|
|
70
|
+
file: definition.file,
|
|
71
|
+
nodePath: "0",
|
|
72
|
+
element: "button",
|
|
73
|
+
location: { file: definition.file, line: 3, column: 1 },
|
|
74
|
+
});
|
|
75
|
+
facts.addMany([
|
|
76
|
+
definition,
|
|
77
|
+
usage,
|
|
78
|
+
makeComponentIdentityFact({
|
|
79
|
+
componentKey: definition.componentKey,
|
|
80
|
+
state: "shadow",
|
|
81
|
+
confidence,
|
|
82
|
+
canonicalTarget: "Button",
|
|
83
|
+
evidence: [definition.id, usage.id],
|
|
84
|
+
}),
|
|
85
|
+
]);
|
|
86
|
+
return facts;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
describe("canonical facts supply mapping evidence", () => {
|
|
90
|
+
it("blocks a confirmed shadow using the same facts that establish its canonical target", () => {
|
|
91
|
+
const result = evaluate(input(shadow()));
|
|
92
|
+
expect(result.verdict).toBe("block");
|
|
93
|
+
expect(result.counts).toMatchObject({ blocked: 1, pendingCloudTrust: 0 });
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it.each(["likely", "review"] as const)("keeps %s shadows from earning a block", (confidence) => {
|
|
97
|
+
expect(evaluate(input(shadow(confidence))).counts.blocked).toBe(0);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it("retains explicit negative and unknown mapping judgments", () => {
|
|
101
|
+
const facts = shadow();
|
|
102
|
+
const fingerprint = runRules(facts)[0]!.fingerprint;
|
|
103
|
+
for (const mapping of ["unconfirmed", "unknown"] as const) {
|
|
104
|
+
const result = evaluate(
|
|
105
|
+
input(facts, { trust: { source: "verified", mappings: { [fingerprint]: mapping } } })
|
|
106
|
+
);
|
|
107
|
+
expect(result.counts.blocked).toBe(0);
|
|
108
|
+
expect(result.verdict).toBe(mapping === "unknown" ? "indeterminate" : "pass");
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it("still requires source verification and a verified baseline", () => {
|
|
113
|
+
expect(
|
|
114
|
+
evaluate(input(shadow(), { trust: { source: "unverified", mappings: {} } })).counts.blocked
|
|
115
|
+
).toBe(0);
|
|
116
|
+
expect(evaluate(input(shadow(), { baseline: { identityVerified: false } })).verdict).toBe(
|
|
117
|
+
"indeterminate"
|
|
118
|
+
);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it("preserves accepted debt and approved waivers", () => {
|
|
122
|
+
const facts = shadow();
|
|
123
|
+
const fingerprint = runRules(facts)[0]!.fingerprint;
|
|
124
|
+
expect(
|
|
125
|
+
evaluate(
|
|
126
|
+
input(facts, {
|
|
127
|
+
baseline: { identityVerified: true, findingStates: { [fingerprint]: "existing" } },
|
|
128
|
+
})
|
|
129
|
+
).verdict
|
|
130
|
+
).toBe("pass");
|
|
131
|
+
expect(evaluate(input(facts, { waivers: [{ fingerprint }] })).verdict).toBe("pass");
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it("does not grant trust to an arbitrary findings-only report", () => {
|
|
135
|
+
const finding = runRules(shadow())[0]!;
|
|
136
|
+
const result = evaluate(
|
|
137
|
+
input(new FactIndex(), {
|
|
138
|
+
evidence: {
|
|
139
|
+
kind: "findings",
|
|
140
|
+
findings: [
|
|
141
|
+
{ fingerprint: finding.fingerprint, ruleId: finding.ruleId, severity: "error" },
|
|
142
|
+
],
|
|
143
|
+
},
|
|
144
|
+
})
|
|
145
|
+
);
|
|
146
|
+
expect(result.verdict).toBe("indeterminate");
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it("blocks an undefined token against the contract vocabulary", () => {
|
|
150
|
+
const facts = new FactIndex();
|
|
151
|
+
facts.addMany(
|
|
152
|
+
compileGlobalGovernanceFacts({
|
|
153
|
+
styles: [g.styles.cssVars().mustBeDefined({ severity: "error" })],
|
|
154
|
+
})
|
|
155
|
+
);
|
|
156
|
+
facts.addMany([
|
|
157
|
+
makeContractTokenFact({ name: "--color-text" }),
|
|
158
|
+
makeStyleDeclarationFact({
|
|
159
|
+
file: "src/proof.css",
|
|
160
|
+
selector: ".save-proof",
|
|
161
|
+
declarationPath: "0",
|
|
162
|
+
property: "color",
|
|
163
|
+
value: "var(--color-proof-undefined)",
|
|
164
|
+
location: { file: "src/proof.css", line: 1, column: 1 },
|
|
165
|
+
}),
|
|
166
|
+
]);
|
|
167
|
+
const result = evaluate(input(facts));
|
|
168
|
+
expect(result.findings).toHaveLength(1);
|
|
169
|
+
expect(result.verdict).toBe("block");
|
|
170
|
+
});
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it.each([
|
|
174
|
+
{ severity: "error", element: "button", verdict: "block" },
|
|
175
|
+
{ severity: "warn", element: "button", verdict: "pass" },
|
|
176
|
+
{ severity: "error", element: "Button", verdict: "pass" },
|
|
177
|
+
] as const)(
|
|
178
|
+
"preserves $severity/$element eligibility for canonical library substitutions",
|
|
179
|
+
({ severity, element, verdict }) => {
|
|
180
|
+
const facts = new FactIndex();
|
|
181
|
+
facts.addMany(
|
|
182
|
+
compileGlobalGovernanceFacts({
|
|
183
|
+
rules: {
|
|
184
|
+
"components/prefer-library": {
|
|
185
|
+
enabled: true,
|
|
186
|
+
severity,
|
|
187
|
+
options: {
|
|
188
|
+
canonicalSources: [{ kind: "npm", specifier: "@design/ui", include: ["Button"] }],
|
|
189
|
+
},
|
|
190
|
+
},
|
|
191
|
+
},
|
|
192
|
+
})
|
|
193
|
+
);
|
|
194
|
+
facts.addMany([
|
|
195
|
+
makeUsageNodeFact({
|
|
196
|
+
file: "src/App.tsx",
|
|
197
|
+
nodePath: "0",
|
|
198
|
+
element,
|
|
199
|
+
location: { file: "src/App.tsx", line: 2, column: 1 },
|
|
200
|
+
}),
|
|
201
|
+
makeUsageImportFact({
|
|
202
|
+
file: "src/App.tsx",
|
|
203
|
+
local: "Button",
|
|
204
|
+
imported: "Button",
|
|
205
|
+
source: "./Button",
|
|
206
|
+
location: { file: "src/App.tsx", line: 1, column: 1 },
|
|
207
|
+
}),
|
|
208
|
+
]);
|
|
209
|
+
const result = evaluate(input(facts));
|
|
210
|
+
expect(result.findings).toHaveLength(1);
|
|
211
|
+
expect(result.verdict).toBe(verdict);
|
|
212
|
+
expect(result.counts.pendingCloudTrust).toBe(0);
|
|
213
|
+
}
|
|
214
|
+
);
|
|
215
|
+
|
|
216
|
+
it.each(["error", "warn"] as const)(
|
|
217
|
+
"preserves %s policy for authored upstream token drift",
|
|
218
|
+
(severity) => {
|
|
219
|
+
const facts = new FactIndex();
|
|
220
|
+
facts.addMany(
|
|
221
|
+
compileGlobalGovernanceFacts({
|
|
222
|
+
rules: { "tokens/upstream-drift": { enabled: true, severity } },
|
|
223
|
+
})
|
|
224
|
+
);
|
|
225
|
+
facts.addMany([
|
|
226
|
+
makeTokenDefinitionFact({
|
|
227
|
+
name: "--brand",
|
|
228
|
+
value: "#111111",
|
|
229
|
+
category: "color",
|
|
230
|
+
role: "local",
|
|
231
|
+
authority: "authored",
|
|
232
|
+
sourceIdentity: "tokens.css",
|
|
233
|
+
location: { file: "tokens.css", line: 1, column: 1 },
|
|
234
|
+
}),
|
|
235
|
+
makeTokenDefinitionFact({
|
|
236
|
+
name: "--brand",
|
|
237
|
+
value: "#222222",
|
|
238
|
+
category: "color",
|
|
239
|
+
role: "upstream",
|
|
240
|
+
authority: "declared-package",
|
|
241
|
+
sourceIdentity: "@design/ui/fragments.json",
|
|
242
|
+
location: { file: "@design/ui/fragments.json", line: 1, column: 1 },
|
|
243
|
+
}),
|
|
244
|
+
]);
|
|
245
|
+
const result = evaluate(input(facts));
|
|
246
|
+
expect(result.findings).toHaveLength(1);
|
|
247
|
+
expect(result.verdict).toBe(severity === "error" ? "block" : "pass");
|
|
248
|
+
expect(result.counts.pendingCloudTrust).toBe(0);
|
|
249
|
+
}
|
|
250
|
+
);
|
|
251
|
+
|
|
252
|
+
function versionedInput(
|
|
253
|
+
evaluatorVersion: string,
|
|
254
|
+
contractPreimage: ContractPreimage = preimage
|
|
255
|
+
): EvaluationInputV2 {
|
|
256
|
+
const base = input(shadow());
|
|
257
|
+
const binding = {
|
|
258
|
+
provider: "github",
|
|
259
|
+
providerInstanceId: "github.com",
|
|
260
|
+
repositoryExternalId: "proof",
|
|
261
|
+
bindingId: "binding-proof",
|
|
262
|
+
};
|
|
263
|
+
const analysisPlan = buildAnalysisPlanV1({
|
|
264
|
+
binding,
|
|
265
|
+
source: { commitId: "proof-head", defaultBranch: "main", acquiredBy: "github_app" },
|
|
266
|
+
contract: { fcid: contractHash(contractPreimage), artifactDigest: "a".repeat(64) },
|
|
267
|
+
config: { path: "fragments.config.ts", digest: "b".repeat(64) },
|
|
268
|
+
profile: { id: "react-web-v1", version: "1", capabilities: ["tsx"] },
|
|
269
|
+
regions: [
|
|
270
|
+
{
|
|
271
|
+
regionId: "tsx",
|
|
272
|
+
pathPattern: "src/**/*.tsx",
|
|
273
|
+
kind: "component_source",
|
|
274
|
+
language: "typescript",
|
|
275
|
+
required: true,
|
|
276
|
+
requiredDialects: ["tsx"],
|
|
277
|
+
analyzer: { id: "react", interfaceVersion: "1", implementationVersion: "1" },
|
|
278
|
+
},
|
|
279
|
+
],
|
|
280
|
+
limits: {
|
|
281
|
+
maxSourceFiles: 1000,
|
|
282
|
+
maxSourceBytes: 10000000,
|
|
283
|
+
maxFileBytes: 1000000,
|
|
284
|
+
maxFacts: 10000,
|
|
285
|
+
maxRegions: 100,
|
|
286
|
+
maxDiagnostics: 1000,
|
|
287
|
+
maxDurationMs: 60000,
|
|
288
|
+
},
|
|
289
|
+
evaluatorVersion,
|
|
290
|
+
createdAt: "2026-09-08T00:00:00.000Z",
|
|
291
|
+
});
|
|
292
|
+
return {
|
|
293
|
+
schemaVersion: 2,
|
|
294
|
+
binding,
|
|
295
|
+
analysisPlan,
|
|
296
|
+
contract: { fcid: contractHash(contractPreimage), preimage: contractPreimage, active: true },
|
|
297
|
+
factSchemaVersion: "facts:v1",
|
|
298
|
+
facts: shadow(),
|
|
299
|
+
factsDigest: "c".repeat(64),
|
|
300
|
+
coverage: [
|
|
301
|
+
{ regionId: "tsx", state: "analyzed", fileCount: 2, byteCount: 400, diagnosticIds: [] },
|
|
302
|
+
],
|
|
303
|
+
policy: base.policy,
|
|
304
|
+
trust: base.trust,
|
|
305
|
+
baseline: {
|
|
306
|
+
identity: {
|
|
307
|
+
bindingDigest: repositoryBindingDigestV1(binding),
|
|
308
|
+
repositoryExternalId: binding.repositoryExternalId,
|
|
309
|
+
fcid: contractHash(contractPreimage),
|
|
310
|
+
sourceCommitId: "proof-base",
|
|
311
|
+
generationId: "baseline-proof",
|
|
312
|
+
},
|
|
313
|
+
findingStates: {},
|
|
314
|
+
},
|
|
315
|
+
waivers: [],
|
|
316
|
+
integrity: {
|
|
317
|
+
governance: "healthy",
|
|
318
|
+
inertConfigDiagnosticCount: 0,
|
|
319
|
+
profile: { state: "known", profileId: "react-web-v1", profileVersion: "1" },
|
|
320
|
+
facts: { state: "healthy", coalescedDuplicateCount: 0 },
|
|
321
|
+
},
|
|
322
|
+
evaluatedAt: base.evaluatedAt,
|
|
323
|
+
evaluatorVersion,
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
it("replays old captured semantics with the original input and result digests", () => {
|
|
328
|
+
const result = evaluate(versionedInput(LEGACY_EVALUATOR_VERSION_V2));
|
|
329
|
+
expect(result.verdict).toBe("indeterminate");
|
|
330
|
+
// Captured by running the pre-change evaluator from commit 90eeb5c.
|
|
331
|
+
expect(result.inputDigest).toBe(
|
|
332
|
+
"af1097a55bd36b726d35a70570589a4512e16e69b1ef1156f7d94a47e7d93919"
|
|
333
|
+
);
|
|
334
|
+
expect(result.resultDigest).toBe(
|
|
335
|
+
"e0395a9149a5de73e28b3295d3dc94833895286d7ec70371ab54bd392fa72954"
|
|
336
|
+
);
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
it("gives corrected semantics a distinct plan, input and result identity", () => {
|
|
340
|
+
const legacy = versionedInput(LEGACY_EVALUATOR_VERSION_V2);
|
|
341
|
+
const current = versionedInput(EVALUATOR_VERSION_V2);
|
|
342
|
+
const previous = evaluate(legacy);
|
|
343
|
+
const result = evaluate(current);
|
|
344
|
+
expect(result.verdict).toBe("block");
|
|
345
|
+
expect(current.analysisPlan.digest).not.toBe(legacy.analysisPlan.digest);
|
|
346
|
+
expect(result.inputDigest).not.toBe(previous.inputDigest);
|
|
347
|
+
expect(result.resultDigest).not.toBe(previous.resultDigest);
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
it("rejects source-qualified contracts at the public legacy evaluator boundary", () => {
|
|
351
|
+
const qualified = { ...preimage, schema: CONTRACT_PREIMAGE_SCHEMA_V2 };
|
|
352
|
+
expect(() =>
|
|
353
|
+
evaluate(
|
|
354
|
+
input(new FactIndex(), {
|
|
355
|
+
contract: { state: "pinned", fcid: contractHash(qualified), preimage: qualified },
|
|
356
|
+
})
|
|
357
|
+
)
|
|
358
|
+
).toThrow("Source-qualified contracts require a V2 evaluation");
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
it("holds rather than passes an enforceable raw element with ambiguous approved targets", () => {
|
|
362
|
+
const facts = new FactIndex();
|
|
363
|
+
facts.addMany(
|
|
364
|
+
compileGlobalGovernanceFacts({
|
|
365
|
+
rules: {
|
|
366
|
+
"components/prefer-library": {
|
|
367
|
+
enabled: true,
|
|
368
|
+
severity: "error",
|
|
369
|
+
options: {
|
|
370
|
+
sourceIdentitySchema: "component-source:v1",
|
|
371
|
+
canonicalMappings: ["first", "second"].map((name) => ({
|
|
372
|
+
name: "Button",
|
|
373
|
+
canonical: "Button",
|
|
374
|
+
htmlEquivalent: "button",
|
|
375
|
+
importPath: `@acme/${name}`,
|
|
376
|
+
source: { kind: "package", importPath: `@acme/${name}`, exportName: "Button" },
|
|
377
|
+
})),
|
|
378
|
+
},
|
|
379
|
+
},
|
|
380
|
+
},
|
|
381
|
+
})
|
|
382
|
+
);
|
|
383
|
+
facts.add(
|
|
384
|
+
makeUsageNodeFact({
|
|
385
|
+
file: "src/App.tsx",
|
|
386
|
+
nodePath: "0",
|
|
387
|
+
element: "button",
|
|
388
|
+
location: { file: "src/App.tsx", line: 1, column: 1 },
|
|
389
|
+
})
|
|
390
|
+
);
|
|
391
|
+
const configured = versionedInput(SOURCE_IDENTITY_EVALUATOR_VERSION_V2, {
|
|
392
|
+
...preimage,
|
|
393
|
+
schema: CONTRACT_PREIMAGE_SCHEMA_V2,
|
|
394
|
+
});
|
|
395
|
+
const result = evaluate({ ...configured, facts });
|
|
396
|
+
expect(result.verdict).toBe("indeterminate");
|
|
397
|
+
expect(result.findings[0]?.state).toBe("pending_cloud_trust");
|
|
398
|
+
});
|
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
* `pending_cloud_trust`; all affirmative → `blocked`.
|
|
24
24
|
*/
|
|
25
25
|
|
|
26
|
+
import { CONTRACT_PREIMAGE_SCHEMA_V2 } from "../contract/preimage.js";
|
|
26
27
|
import { deriveCoverageSummaryV1 } from "../analysis-plan/index.js";
|
|
27
28
|
import {
|
|
28
29
|
contractHash,
|
|
@@ -32,6 +33,7 @@ import {
|
|
|
32
33
|
} from "../contract/hash.js";
|
|
33
34
|
import { EVIDENCE_ORDER, canBlock, type EvidenceGrade } from "../evidence.js";
|
|
34
35
|
import { runRules } from "../rules/index.js";
|
|
36
|
+
import type { Finding } from "../rules/types.js";
|
|
35
37
|
import { configuredFindingSeverity, readRuleConfig } from "../rules/rule-config.js";
|
|
36
38
|
import { severityLevel } from "../severity.js";
|
|
37
39
|
import { tierFor } from "../rules/tiers.js";
|
|
@@ -41,6 +43,7 @@ import type {
|
|
|
41
43
|
EvaluationFindingInput,
|
|
42
44
|
EvaluationInput,
|
|
43
45
|
EvaluationInputV2,
|
|
46
|
+
EvaluationMappingTrust,
|
|
44
47
|
EvaluationReason,
|
|
45
48
|
EvaluationReasonV2,
|
|
46
49
|
EvaluationResult,
|
|
@@ -48,7 +51,12 @@ import type {
|
|
|
48
51
|
EvaluationVerdict,
|
|
49
52
|
EvaluationWaiver,
|
|
50
53
|
} from "./types.js";
|
|
51
|
-
import {
|
|
54
|
+
import {
|
|
55
|
+
EVALUATOR_VERSION,
|
|
56
|
+
EVALUATOR_VERSION_V2,
|
|
57
|
+
SOURCE_IDENTITY_EVALUATOR_VERSION_V2,
|
|
58
|
+
LEGACY_EVALUATOR_VERSION_V2,
|
|
59
|
+
} from "./types.js";
|
|
52
60
|
import { evaluationInputV2Schema, evaluationResultV2Schema } from "./receipt.js";
|
|
53
61
|
|
|
54
62
|
type TrustFactState = "affirmative" | "negative" | "unknown";
|
|
@@ -90,7 +98,16 @@ export function evaluate(input: EvaluationInput): EvaluationResult;
|
|
|
90
98
|
export function evaluate(
|
|
91
99
|
input: EvaluationInput | EvaluationInputV2
|
|
92
100
|
): EvaluationResult | EvaluationResultV2 {
|
|
93
|
-
|
|
101
|
+
if (isEvaluationInputV2(input)) return evaluateV2(input);
|
|
102
|
+
if (
|
|
103
|
+
input.contract.state === "pinned" &&
|
|
104
|
+
input.contract.preimage?.schema === CONTRACT_PREIMAGE_SCHEMA_V2
|
|
105
|
+
) {
|
|
106
|
+
throw new Error(
|
|
107
|
+
"Source-qualified contracts require a V2 evaluation with evaluation-kernel:v2.2"
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
return evaluateV1(input);
|
|
94
111
|
}
|
|
95
112
|
|
|
96
113
|
function isEvaluationInputV2(
|
|
@@ -99,7 +116,7 @@ function isEvaluationInputV2(
|
|
|
99
116
|
return "schemaVersion" in input && input.schemaVersion === 2;
|
|
100
117
|
}
|
|
101
118
|
|
|
102
|
-
function evaluateV1(input: EvaluationInput): EvaluationResult {
|
|
119
|
+
function evaluateV1(input: EvaluationInput, deriveCanonicalTrust = true): EvaluationResult {
|
|
103
120
|
const reasons = new Set<EvaluationReason>();
|
|
104
121
|
|
|
105
122
|
// ---- Contract identity (the kernel recomputes the FCID itself) ----------
|
|
@@ -163,7 +180,7 @@ function evaluateV1(input: EvaluationInput): EvaluationResult {
|
|
|
163
180
|
// is merged field-by-field — a synthetic row could fabricate gating
|
|
164
181
|
// evidence no input row carries — and reordering never changes the result
|
|
165
182
|
// because rows are canonically sorted before classification.
|
|
166
|
-
const evidence = evidenceFindings(input);
|
|
183
|
+
const evidence = evidenceFindings(input, deriveCanonicalTrust);
|
|
167
184
|
const findings = [...groupByFingerprint(evidence).values()].map((rows) => {
|
|
168
185
|
const classified = rows.map((row) => classifyFinding(row, input, activeWaivers));
|
|
169
186
|
return classified.reduce((hardest, candidate) =>
|
|
@@ -198,7 +215,7 @@ function evaluateV1(input: EvaluationInput): EvaluationResult {
|
|
|
198
215
|
gatingFindingsPresent: counts.blocked > 0,
|
|
199
216
|
};
|
|
200
217
|
|
|
201
|
-
const inputDigest = sha256Hex(canonicalPreimage(normalizeInputForDigest(input)));
|
|
218
|
+
const inputDigest = sha256Hex(canonicalPreimage(normalizeInputForDigest(input, evidence)));
|
|
202
219
|
const partial = {
|
|
203
220
|
evaluatorVersion: EVALUATOR_VERSION,
|
|
204
221
|
verdict,
|
|
@@ -238,7 +255,7 @@ const V2_INTEGRITY_REASONS = new Set<EvaluationReasonV2>([
|
|
|
238
255
|
]);
|
|
239
256
|
|
|
240
257
|
/**
|
|
241
|
-
*
|
|
258
|
+
* Supports the current first-party evaluator and exact replay of its predecessor. Unknown
|
|
242
259
|
* generations are never interpreted optimistically: evaluator/fact schema
|
|
243
260
|
* mismatches reject the call, while an unknown analysis profile remains a
|
|
244
261
|
* typed indeterminate result so coverage can explain the unsupported source.
|
|
@@ -247,7 +264,11 @@ const SUPPORTED_FACT_SCHEMA_VERSIONS_V2 = new Set(["facts:v1"]);
|
|
|
247
264
|
const SUPPORTED_REACT_WEB_PROFILE_VERSIONS_V1 = new Set(["1"]);
|
|
248
265
|
|
|
249
266
|
function evaluateV2(rawInput: EvaluationInputV2): EvaluationResultV2 {
|
|
250
|
-
if (
|
|
267
|
+
if (
|
|
268
|
+
rawInput.evaluatorVersion !== EVALUATOR_VERSION_V2 &&
|
|
269
|
+
rawInput.evaluatorVersion !== SOURCE_IDENTITY_EVALUATOR_VERSION_V2 &&
|
|
270
|
+
rawInput.evaluatorVersion !== LEGACY_EVALUATOR_VERSION_V2
|
|
271
|
+
) {
|
|
251
272
|
throw new Error(`Unsupported evaluator version: ${rawInput.evaluatorVersion}`);
|
|
252
273
|
}
|
|
253
274
|
if (!SUPPORTED_FACT_SCHEMA_VERSIONS_V2.has(rawInput.factSchemaVersion)) {
|
|
@@ -295,7 +316,12 @@ function evaluateV2(rawInput: EvaluationInputV2): EvaluationResultV2 {
|
|
|
295
316
|
},
|
|
296
317
|
evaluatedAt: input.evaluatedAt,
|
|
297
318
|
};
|
|
298
|
-
|
|
319
|
+
// Captured V2 inputs must replay byte-for-byte under their original semantics.
|
|
320
|
+
// New plans always select the current generation through EVALUATOR_VERSION_V2.
|
|
321
|
+
const compatibilityResult = evaluateV1(
|
|
322
|
+
compatibilityInput,
|
|
323
|
+
input.evaluatorVersion !== LEGACY_EVALUATOR_VERSION_V2
|
|
324
|
+
);
|
|
299
325
|
|
|
300
326
|
const requiredCoverage =
|
|
301
327
|
coverageSummary.requiredRegions === 0
|
|
@@ -407,7 +433,10 @@ function normalizeInputV2ForDigest(
|
|
|
407
433
|
// Evidence
|
|
408
434
|
// ---------------------------------------------------------------------------
|
|
409
435
|
|
|
410
|
-
function evidenceFindings(
|
|
436
|
+
function evidenceFindings(
|
|
437
|
+
input: EvaluationInput,
|
|
438
|
+
deriveCanonicalTrust: boolean
|
|
439
|
+
): EvaluationFindingInput[] {
|
|
411
440
|
if (input.evidence.kind === "findings") return [...input.evidence.findings];
|
|
412
441
|
// Facts mode: run the core rules engine inside the kernel (spec §1 — the
|
|
413
442
|
// kernel extends the `runRules(FactIndex)` seed). `runRules` is pure and
|
|
@@ -416,15 +445,59 @@ function evidenceFindings(input: EvaluationInput): EvaluationFindingInput[] {
|
|
|
416
445
|
// fact) is applied here exactly as the engine applies it to the findings it
|
|
417
446
|
// prints — otherwise a rule the config demoted to `info` gates the verdict.
|
|
418
447
|
const ruleConfig = readRuleConfig(input.evidence.facts);
|
|
448
|
+
const hasContractTokens = input.evidence.facts.byKind("contract_token").length > 0;
|
|
419
449
|
return runRules(input.evidence.facts).map((finding) => ({
|
|
420
450
|
fingerprint: finding.fingerprint,
|
|
421
451
|
ruleId: finding.ruleId,
|
|
422
452
|
severity: severityLevel(configuredFindingSeverity(finding, ruleConfig)),
|
|
423
453
|
advisory: finding.attributes?.advisory === true,
|
|
424
454
|
evidenceGrade: finding.evidenceGrade,
|
|
455
|
+
// An explicit trust decision still wins, including unknown/unconfirmed.
|
|
456
|
+
// Only findings derived here from canonical facts earn the fallback.
|
|
457
|
+
...(deriveCanonicalTrust
|
|
458
|
+
? {
|
|
459
|
+
mappingTrust:
|
|
460
|
+
input.trust?.mappings[finding.fingerprint] ??
|
|
461
|
+
canonicalFactMappingTrust(finding, hasContractTokens),
|
|
462
|
+
}
|
|
463
|
+
: {}),
|
|
425
464
|
}));
|
|
426
465
|
}
|
|
427
466
|
|
|
467
|
+
/** Rules already resolve the approved vocabulary when producing facts-mode
|
|
468
|
+
* findings. Preserve that evidence instead of guessing again from an import
|
|
469
|
+
* path (definition-level shadows have none). Findings-only reports cannot use
|
|
470
|
+
* this path; their caller must supply independently verified mapping trust. */
|
|
471
|
+
function canonicalFactMappingTrust(
|
|
472
|
+
finding: Finding,
|
|
473
|
+
hasContractTokens: boolean
|
|
474
|
+
): EvaluationMappingTrust | undefined {
|
|
475
|
+
switch (finding.ruleId) {
|
|
476
|
+
case "components/shadow-component":
|
|
477
|
+
return finding.attributes?.confidence === "confirmed" &&
|
|
478
|
+
typeof finding.attributes.canonicalTarget === "string" &&
|
|
479
|
+
finding.attributes.canonicalTarget.length > 0
|
|
480
|
+
? "confirmed"
|
|
481
|
+
: undefined;
|
|
482
|
+
case "components/prefer-library":
|
|
483
|
+
return typeof finding.attributes?.suggestedComponent === "string" &&
|
|
484
|
+
finding.attributes.suggestedComponent.length > 0
|
|
485
|
+
? "confirmed"
|
|
486
|
+
: undefined;
|
|
487
|
+
case "tokens/css-vars-must-be-defined":
|
|
488
|
+
return hasContractTokens ? "confirmed" : undefined;
|
|
489
|
+
case "tokens/upstream-drift":
|
|
490
|
+
// This rule only emits for one unambiguous authored local/upstream pair.
|
|
491
|
+
return finding.evidence.some(
|
|
492
|
+
({ fact }) => fact.kind === "token_definition" && "role" in fact && fact.role === "upstream"
|
|
493
|
+
)
|
|
494
|
+
? "confirmed"
|
|
495
|
+
: undefined;
|
|
496
|
+
default:
|
|
497
|
+
return undefined;
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
|
|
428
501
|
/** Hardest state first: an earned block outranks a pending hold outranks an
|
|
429
502
|
* advisory downgrade. */
|
|
430
503
|
const STATE_RANK = { blocked: 0, pending_cloud_trust: 1, advisory: 2 } as const;
|
|
@@ -640,12 +713,12 @@ function mappingFact(
|
|
|
640
713
|
* DERIVED findings (a `FactIndex` is a class instance, not a canonical value);
|
|
641
714
|
* two inputs that judge identically digest identically.
|
|
642
715
|
*/
|
|
643
|
-
function normalizeInputForDigest(input: EvaluationInput) {
|
|
716
|
+
function normalizeInputForDigest(input: EvaluationInput, evidence: EvaluationFindingInput[]) {
|
|
644
717
|
return {
|
|
645
718
|
schema: "evaluation-input:v1",
|
|
646
719
|
contract: input.contract,
|
|
647
720
|
scope: input.scope,
|
|
648
|
-
evidence: canonicalRows(
|
|
721
|
+
evidence: canonicalRows(evidence),
|
|
649
722
|
policy: input.policy,
|
|
650
723
|
baseline: input.baseline,
|
|
651
724
|
adoption: input.adoption,
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import { describe, expect, it } from "vitest";
|
|
2
2
|
import { buildAnalysisPlanV1, type CoverageRegionV1 } from "../analysis-plan/index.js";
|
|
3
3
|
import { contractHash } from "../contract/hash.js";
|
|
4
|
-
import { projectContractPreimage } from "../contract/preimage.js";
|
|
4
|
+
import { CONTRACT_PREIMAGE_SCHEMA_V2, projectContractPreimage } from "../contract/preimage.js";
|
|
5
5
|
import { evaluationReceiptIdFromDigest } from "../domain-ids.js";
|
|
6
6
|
import { FactIndex } from "../facts/fact-index.js";
|
|
7
7
|
import { makeTokenDefinitionFact } from "../facts/builders.js";
|
|
8
8
|
import { repositoryBindingDigestV1 } from "../repository-binding.js";
|
|
9
9
|
import {
|
|
10
10
|
EVALUATOR_VERSION_V2,
|
|
11
|
+
SOURCE_IDENTITY_EVALUATOR_VERSION_V2,
|
|
11
12
|
evaluate,
|
|
12
13
|
evaluationInputDigestV2,
|
|
13
14
|
evaluationResultDigestV2,
|
|
@@ -685,7 +686,7 @@ describe("evaluate V2", () => {
|
|
|
685
686
|
});
|
|
686
687
|
|
|
687
688
|
it("rejects evaluator and fact-schema versions it cannot interpret", () => {
|
|
688
|
-
expect(EVALUATOR_VERSION_V2).toBe("evaluation-kernel:v2");
|
|
689
|
+
expect(EVALUATOR_VERSION_V2).toBe("evaluation-kernel:v2.1");
|
|
689
690
|
expect(() =>
|
|
690
691
|
evaluate({ ...evaluationInput(), evaluatorVersion: "evaluation-kernel:v3" })
|
|
691
692
|
).toThrow(/Unsupported evaluator version/u);
|
|
@@ -770,3 +771,28 @@ describe("EvaluationReceiptV1", () => {
|
|
|
770
771
|
).toThrow();
|
|
771
772
|
});
|
|
772
773
|
});
|
|
774
|
+
|
|
775
|
+
describe("source-qualified receipt generation", () => {
|
|
776
|
+
it("requires the new evaluator and reproduces its input digest", () => {
|
|
777
|
+
const input = evaluationInput();
|
|
778
|
+
const preimage = { ...input.contract.preimage, schema: CONTRACT_PREIMAGE_SCHEMA_V2 };
|
|
779
|
+
const fcid = contractHash(preimage);
|
|
780
|
+
const { digest: _digest, ...oldPlan } = input.analysisPlan;
|
|
781
|
+
const forVersion = (version: string): EvaluationInputV2 => ({
|
|
782
|
+
...input,
|
|
783
|
+
evaluatorVersion: version,
|
|
784
|
+
contract: { ...input.contract, preimage, fcid },
|
|
785
|
+
analysisPlan: buildAnalysisPlanV1({
|
|
786
|
+
...oldPlan,
|
|
787
|
+
contract: { ...oldPlan.contract, fcid },
|
|
788
|
+
evaluatorVersion: version,
|
|
789
|
+
}),
|
|
790
|
+
});
|
|
791
|
+
for (const oldVersion of ["evaluation-kernel:v2", EVALUATOR_VERSION_V2])
|
|
792
|
+
expect(() => evaluate(forVersion(oldVersion))).toThrow(/Source-qualified/);
|
|
793
|
+
const result = evaluate(forVersion(SOURCE_IDENTITY_EVALUATOR_VERSION_V2));
|
|
794
|
+
expect(result.verdict).toBe("pass");
|
|
795
|
+
expect(result.integrity.fcidVerified).toBe(true);
|
|
796
|
+
expect(evaluate(forVersion(SOURCE_IDENTITY_EVALUATOR_VERSION_V2))).toEqual(result);
|
|
797
|
+
});
|
|
798
|
+
});
|
package/src/evaluation/index.ts
CHANGED
|
@@ -4,7 +4,13 @@
|
|
|
4
4
|
* `apps/cloud/src/lib/govern/evaluation-adapter.ts`.
|
|
5
5
|
*/
|
|
6
6
|
export { evaluate, evaluationInputDigestV2, evaluationResultDigestV2 } from "./evaluate.js";
|
|
7
|
-
export {
|
|
7
|
+
export {
|
|
8
|
+
EVALUATOR_VERSION,
|
|
9
|
+
EVALUATOR_VERSION_V2,
|
|
10
|
+
SOURCE_IDENTITY_EVALUATOR_VERSION_V2,
|
|
11
|
+
evaluatorVersionForContractSchema,
|
|
12
|
+
LEGACY_EVALUATOR_VERSION_V2,
|
|
13
|
+
} from "./types.js";
|
|
8
14
|
export {
|
|
9
15
|
EVALUATION_MAX_FINDINGS_V2,
|
|
10
16
|
EVALUATION_MAX_MAP_ENTRIES_V2,
|