@usefragments/core 2.0.1 → 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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@usefragments/core",
3
- "version": "2.0.1",
3
+ "version": "2.0.2",
4
4
  "license": "MIT",
5
5
  "description": "Core types, schemas, and runtime API for Fragments component definitions",
6
6
  "author": "Conan McNicholl",
@@ -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
- * V1 supports one exact first-party profile/fact/evaluator generation. Unknown
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 (rawInput.evaluatorVersion !== EVALUATOR_VERSION_V2) {
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
- const compatibilityResult = evaluateV1(compatibilityInput);
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(input: EvaluationInput): EvaluationFindingInput[] {
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(evidenceFindings(input)),
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);
@@ -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,
@@ -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
- /** Authoritative facts-only evaluator introduced by Fragments V1 Brief 04. */
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/index.ts CHANGED
@@ -1129,6 +1129,7 @@ export {
1129
1129
  EVALUATION_RECEIPT_STRING_MAX_BYTES_V1,
1130
1130
  EVALUATOR_VERSION,
1131
1131
  EVALUATOR_VERSION_V2,
1132
+ LEGACY_EVALUATOR_VERSION_V2,
1132
1133
  effectiveEvaluationWaiverV2Schema,
1133
1134
  evaluate,
1134
1135
  evaluationInputDigestV2,