@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.
@@ -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 { EVALUATOR_VERSION, EVALUATOR_VERSION_V2 } from "./types.js";
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
- return isEvaluationInputV2(input) ? evaluateV2(input) : evaluateV1(input);
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
- * V1 supports one exact first-party profile/fact/evaluator generation. Unknown
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 (rawInput.evaluatorVersion !== EVALUATOR_VERSION_V2) {
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
- const compatibilityResult = evaluateV1(compatibilityInput);
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(input: EvaluationInput): EvaluationFindingInput[] {
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(evidenceFindings(input)),
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
+ });
@@ -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 { EVALUATOR_VERSION, EVALUATOR_VERSION_V2 } from "./types.js";
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,