// BUG-1181: the dated candidate-window contract belongs to scoring generation 9 and // every later one. The bump to scoring-10 (functional benefic / malefic profile v2) // must not turn new results into legacy same-day results anywhere in the stack. import assert from "node:assert/strict"; import { readFileSync, readdirSync } from "node:fs"; import test from "node:test"; import { FIRST_DATED_SCORING_GENERATION, isDatedScoringAlgorithmVersion } from "../src/lib/rectification-agentic/core/candidate-window.ts"; import { v9EngineVersion } from "../src/lib/rectification-agentic/v9/engine-client.ts"; import { resultIdentityView } from "../src/lib/rectification-agentic/v9/result-identity.ts"; import { parseV9CaseDossier } from "../src/lib/rectification-agentic/v9/tool-service.ts"; import { dossierFixture } from "./rectification-v9-test-support.ts"; test("dated contract is a generation threshold, not one literal", () => { assert.equal(FIRST_DATED_SCORING_GENERATION, 9); for (const version of ["rectification-v5-matrix-scoring-9", "rectification-v5-matrix-scoring-10", "rectification-v5-matrix-scoring-11", "rectification-v5-matrix-scoring-100"]) { assert.equal(isDatedScoringAlgorithmVersion(version), true, version); } for (const version of ["rectification-v5-matrix-scoring-8", "rectification-v5-matrix-scoring-1", "rectification-v5-matrix-scoring-09", "rectification-v5", "rectification-v5-matrix-scoring-10 ", "x-rectification-v5-matrix-scoring-10", "", null, undefined, 10]) { assert.equal(isDatedScoringAlgorithmVersion(version), false, String(version)); } }); test("receipt default and Python identity agree on scoring-10", (t) => { const saved = process.env.RECTIFICATION_ENGINE_VERSION; delete process.env.RECTIFICATION_ENGINE_VERSION; t.after(() => { if (saved === undefined) delete process.env.RECTIFICATION_ENGINE_VERSION; else process.env.RECTIFICATION_ENGINE_VERSION = saved; }); assert.equal(v9EngineVersion(), "rectification-v5-matrix-scoring-10"); const python = readFileSync(new URL("../../scripts/rectification/scoring_service.py", import.meta.url), "utf8"); assert.match(python, /^ALGORITHM_VERSION = "rectification-v5-matrix-scoring-10"$/m); }); test("no source file compares the algorithm identity with a scoring-N literal", () => { const roots = ["../src/lib/", "../src/mastra/", "../src/app/"]; const hits: string[] = []; const walk = (url: URL) => { for (const entry of readdirSync(url, { withFileTypes: true })) { const child = new URL(entry.name + (entry.isDirectory() ? "/" : ""), url); if (entry.isDirectory()) walk(child); else if (/\.(ts|tsx)$/.test(entry.name)) { const text = readFileSync(child, "utf8"); for (const line of text.split("\n")) if (/[!=]==?\s*"rectification-v5-matrix-scoring-\d+"/.test(line)) hits.push(`${child.pathname}: ${line.trim()}`); } } }; for (const root of roots) walk(new URL(root, import.meta.url)); assert.deepEqual(hits, []); }); test("SQL guard compares the generation too (migration 20261002010000)", () => { const sql = readFileSync(new URL("../supabase/migrations/20261002010000_rectification_dated_algorithm_generation.sql", import.meta.url), "utf8"); assert.match(sql, /create or replace function public\.validate_dated_rectification_candidate\(/); assert.match(sql, /coalesce\(p_result\.algorithm_version, ''\) ~ '\^rectification-v5-matrix-scoring-\(9\|\[1-9\]\[0-9\]\+\)\$'/); assert.doesNotMatch(sql, /algorithm_version = 'rectification-v5-matrix-scoring-9'/); // Only the function body changes: no table, column, grant or data statements. assert.doesNotMatch(sql, /^\s*(alter|drop|insert|update|delete|grant|revoke|create table|create index)\b/im); }); test("a scoring-9 history opens read-only under scoring-10 and offers a recompare (BUG-621 / BUG-984 pattern)", () => { // Real native scoring-9 dated golden as the persisted latest result; only the live identity differs. const historical = JSON.parse(readFileSync(new URL("./fixtures/rectification-midnight-date-anchor.delivery.native.json", import.meta.url), "utf8")).response; assert.equal(historical.algorithm_version, "rectification-v5-matrix-scoring-9"); const latest = { ...historical, candidates: historical.candidate_decisions }; const dossier = parseV9CaseDossier(dossierFixture({ stage: "minute", latestResult: latest })); assert.ok(dossier); assert.equal(dossier.latestResult?.algorithmVersion, "rectification-v5-matrix-scoring-9", "history keeps its own identity"); const policyVersion = historical.decision_policy_version; const underTen = resultIdentityView(dossier, { algorithmVersion: "rectification-v5-matrix-scoring-10", policyVersion }); assert.deepEqual([underTen.read_only, underTen.can_recompare, underTen.algorithm_version], [true, true, "rectification-v5-matrix-scoring-9"]); const underNine = resultIdentityView(dossier, { algorithmVersion: "rectification-v5-matrix-scoring-9", policyVersion }); assert.deepEqual([underNine.read_only, underNine.can_recompare], [false, false]); // Its candidates still parse as dated (generation >= 9), so the opened history keeps civil dates. assert.ok(dossier.latestResult!.candidates.every((candidate) => typeof candidate.candidate_date === "string")); });