fix(rectification): use candidate dates for cross-midnight dasha scoring
Add date-isolated caches and regression coverage, align scoring identity, and freeze full research reruns while preserving historical artifacts. Record unresolved cache/receipt identity and end-to-end acceptance gaps for branch review only. Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
@@ -949,8 +949,10 @@ export async function runV9Diagnostics(input: {
|
||||
};
|
||||
}
|
||||
|
||||
// Keep the receipt default aligned with Python's ALGORITHM_VERSION; persisted
|
||||
// receipts retain their original identity (this is not a Skill/open gate).
|
||||
export const v9EngineVersion = (): string =>
|
||||
process.env.RECTIFICATION_ENGINE_VERSION?.trim() || "rectification-v5";
|
||||
process.env.RECTIFICATION_ENGINE_VERSION?.trim() || "rectification-v5-matrix-scoring-8";
|
||||
|
||||
export type V9RangeReading = Readonly<{
|
||||
stableThemes: readonly string[];
|
||||
|
||||
+1428
File diff suppressed because it is too large
Load Diff
@@ -81,8 +81,11 @@ const diagnosticReport = JSON.parse(readFileSync(
|
||||
verified_minute_claim_allowed: boolean;
|
||||
};
|
||||
};
|
||||
// 原值:docs/research/sealed_holdout_rerun_2026_09_20.json
|
||||
// 新值:docs/research/sealed_holdout_rerun_cross_midnight_2026_09_20.json
|
||||
// 原因:BUG-981 修复后重跑另存报告;旧报告保留,确认门行为与阈值不变。
|
||||
const currentTreeRerun = JSON.parse(readFileSync(
|
||||
new URL("../../docs/research/sealed_holdout_rerun_2026_09_20.json", import.meta.url),
|
||||
new URL("../../docs/research/sealed_holdout_rerun_cross_midnight_2026_09_20.json", import.meta.url),
|
||||
"utf8",
|
||||
)) as {
|
||||
frozen_record: { implementation_sha256: string };
|
||||
@@ -204,7 +207,7 @@ test("sealed holdout aggregates match the v3 report and still block confirmation
|
||||
);
|
||||
assert.equal(
|
||||
productHoldout.current_tree_scorer.source_report,
|
||||
"docs/research/sealed_holdout_rerun_2026_09_20.json",
|
||||
"docs/research/sealed_holdout_rerun_cross_midnight_2026_09_20.json",
|
||||
);
|
||||
assert.equal(currentTreeRerun.official_valid_independent_blind, false);
|
||||
assert.equal(productHoldout.current_tree_scorer.matches_metrics_scorer, false);
|
||||
|
||||
@@ -0,0 +1,195 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { readFileSync } from "node:fs";
|
||||
import test from "node:test";
|
||||
|
||||
import {
|
||||
cachedEngineScoreIsReusable,
|
||||
liveEngineScoringIdentityFromEnv,
|
||||
readV9EngineScoringIdentity,
|
||||
v9EngineVersion,
|
||||
} from "../src/lib/rectification-agentic/v9/engine-client.ts";
|
||||
import { openRectificationCase } from "../src/lib/rectification-agentic/v9/case-service.ts";
|
||||
import { loadV9TurnReceipt } from "../src/lib/rectification-agentic/v9/tool-service.ts";
|
||||
import { resolveSkillPackageVersion } from "../src/lib/skill-package-registry.ts";
|
||||
import { createRectificationV9Tools } from "../src/mastra/rectification-v9-tools.ts";
|
||||
import {
|
||||
CASE_ID, SESSION_ID, TURN_ID, USER_ID, FOCUS_ID,
|
||||
computeFixture, dossierFixture, fakeAccounting, receiptHandlers,
|
||||
} from "./rectification-v9-test-support.ts";
|
||||
|
||||
// Real native-engine response, using explicitly fictional input, not a hand-built contract.
|
||||
const golden = JSON.parse(readFileSync(new URL(
|
||||
"./fixtures/rectification-engine-version-cross-midnight-golden.json", import.meta.url,
|
||||
), "utf8"));
|
||||
const CURRENT = "rectification-v5-matrix-scoring-8";
|
||||
const PREVIOUS = "rectification-v5-matrix-scoring-7";
|
||||
const envKeys = [
|
||||
"RECTIFICATION_ENGINE_VERSION", "RECTIFICATION_ALGORITHM_VERSION",
|
||||
"RECTIFICATION_DECISION_POLICY_VERSION",
|
||||
] as const;
|
||||
|
||||
function isolateIdentityEnv(t: test.TestContext) {
|
||||
const saved = envKeys.map((key) => [key, process.env[key]] as const);
|
||||
for (const key of envKeys) delete process.env[key];
|
||||
t.after(() => {
|
||||
for (const [key, value] of saved) {
|
||||
if (value === undefined) delete process.env[key];
|
||||
else process.env[key] = value;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
test("receipt default matches the cross-midnight native scoring version without changing Skill identity", (t) => {
|
||||
isolateIdentityEnv(t);
|
||||
assert.equal(v9EngineVersion(), CURRENT);
|
||||
for (const blank of ["", " "]) {
|
||||
process.env.RECTIFICATION_ENGINE_VERSION = blank;
|
||||
assert.equal(v9EngineVersion(), CURRENT);
|
||||
}
|
||||
assert.equal(golden.score.algorithm_version, CURRENT);
|
||||
assert.equal(golden.versions.algorithm_version, CURRENT);
|
||||
const python = readFileSync(new URL("../../scripts/rectification/scoring_service.py", import.meta.url), "utf8");
|
||||
assert.match(python, /ALGORITHM_VERSION = "rectification-v5-matrix-scoring-8"/);
|
||||
// A receipt default must not become an environment override for cache identity.
|
||||
assert.deepEqual(liveEngineScoringIdentityFromEnv({}), { algorithmVersion: null, policyVersion: null });
|
||||
});
|
||||
|
||||
test("explicit engine override still wins and remains a deployment prerequisite, not silently rewritten", (t) => {
|
||||
isolateIdentityEnv(t);
|
||||
process.env.RECTIFICATION_ENGINE_VERSION = " rectification-v5 ";
|
||||
assert.equal(v9EngineVersion(), "rectification-v5");
|
||||
assert.equal(liveEngineScoringIdentityFromEnv().algorithmVersion, null);
|
||||
process.env.RECTIFICATION_ENGINE_VERSION = ` ${PREVIOUS} `;
|
||||
assert.equal(v9EngineVersion(), PREVIOUS);
|
||||
// This existing cache-identity fallback predates this change; no new open gate is introduced.
|
||||
assert.equal(liveEngineScoringIdentityFromEnv().algorithmVersion, PREVIOUS);
|
||||
});
|
||||
|
||||
test("native versions invalidate matching-input old scores while same-version scores remain reusable", async (t) => {
|
||||
isolateIdentityEnv(t);
|
||||
t.mock.method(globalThis, "fetch", async (url: unknown) => {
|
||||
assert.ok(String(url).endsWith("/api/rectification/v5/versions"));
|
||||
return Response.json(golden.versions);
|
||||
});
|
||||
const live = await readV9EngineScoringIdentity();
|
||||
assert.equal(live.algorithmVersion, CURRENT);
|
||||
const fingerprints = { evidenceLedgerFingerprint: "e".repeat(64), candidateRangeFingerprint: "c".repeat(64) };
|
||||
const previous = { ...fingerprints, algorithmVersion: PREVIOUS, policyVersion: golden.versions.decision_policy_version };
|
||||
assert.equal(cachedEngineScoreIsReusable(previous, fingerprints, live), false);
|
||||
assert.equal(cachedEngineScoreIsReusable({ ...previous, algorithmVersion: CURRENT }, fingerprints, live), true);
|
||||
assert.equal(previous.algorithmVersion, PREVIOUS);
|
||||
});
|
||||
|
||||
test("policy-only or stale algorithm overrides bypass native versions under the existing cache policy", async (t) => {
|
||||
isolateIdentityEnv(t);
|
||||
let fetches = 0;
|
||||
t.mock.method(globalThis, "fetch", async () => {
|
||||
fetches += 1;
|
||||
return Response.json(golden.versions);
|
||||
});
|
||||
const fingerprints = { evidenceLedgerFingerprint: "e".repeat(64), candidateRangeFingerprint: "c".repeat(64) };
|
||||
const stored = { ...fingerprints, algorithmVersion: PREVIOUS, policyVersion: golden.versions.decision_policy_version };
|
||||
process.env.RECTIFICATION_DECISION_POLICY_VERSION = golden.versions.decision_policy_version;
|
||||
const policyOnly = await readV9EngineScoringIdentity();
|
||||
assert.equal(policyOnly.algorithmVersion, null);
|
||||
assert.equal(cachedEngineScoreIsReusable(stored, fingerprints, policyOnly), true);
|
||||
process.env.RECTIFICATION_ALGORITHM_VERSION = PREVIOUS;
|
||||
const staleOverride = await readV9EngineScoringIdentity();
|
||||
assert.equal(staleOverride.algorithmVersion, PREVIOUS);
|
||||
assert.equal(cachedEngineScoreIsReusable(stored, fingerprints, staleOverride), true);
|
||||
assert.equal(fetches, 0);
|
||||
});
|
||||
|
||||
test("unreachable versions retain the existing unknown-identity cache fallback, not proof of rescore", async (t) => {
|
||||
isolateIdentityEnv(t);
|
||||
t.mock.method(globalThis, "fetch", async () => { throw new Error("fixture versions unavailable"); });
|
||||
const live = await readV9EngineScoringIdentity();
|
||||
assert.deepEqual(live, { algorithmVersion: null, policyVersion: null });
|
||||
const fingerprints = { evidenceLedgerFingerprint: "e".repeat(64), candidateRangeFingerprint: "c".repeat(64) };
|
||||
assert.equal(cachedEngineScoreIsReusable({ ...fingerprints, algorithmVersion: PREVIOUS }, fingerprints, live), true);
|
||||
});
|
||||
|
||||
test("new Case scoring writes the new version on both started and completed receipts", async (t) => {
|
||||
isolateIdentityEnv(t);
|
||||
const request = golden.request;
|
||||
const candidateRange = { start_time: request.start_time, end_time: request.end_time };
|
||||
const evidence = request.events.map((event: Record<string, string>) => ({
|
||||
id: event.id, source_turn_id: TURN_ID, subject: "self", event_kind: event.event_kind,
|
||||
domain: event.domain, occurred_from: event.date_start, occurred_to: event.date_end,
|
||||
date_precision: event.precision, summary: event.summary, status: "confirmed",
|
||||
}));
|
||||
const calls: string[] = [];
|
||||
t.mock.method(globalThis, "fetch", async (url: unknown) => {
|
||||
const path = new URL(String(url)).pathname;
|
||||
calls.push(path);
|
||||
if (path.endsWith("/versions")) return Response.json(golden.versions);
|
||||
if (path.endsWith("/score") || path.endsWith("/diagnostics")) return Response.json(golden.score);
|
||||
throw new Error(`unexpected engine request ${path}`);
|
||||
});
|
||||
const accounting = fakeAccounting({
|
||||
...receiptHandlers,
|
||||
get_agentic_rectification_case_dossier: () => dossierFixture({ candidateRange, evidence, latestResult: null }),
|
||||
get_agentic_rectification_case_compute: () => ({
|
||||
...computeFixture({ baselineBirthSnapshot: {
|
||||
birth_date: request.birth_date, latitude: request.lat, longitude: request.lon,
|
||||
timezone_offset: request.tz, timezone_id: "Asia/Shanghai", birth_time_source: "approximate",
|
||||
} }), candidate_range: candidateRange,
|
||||
}),
|
||||
persist_agentic_rectification_candidate_v2: (_fn, args) => ({
|
||||
result_id: args.p_engine_result_id, cached: false, candidates: args.p_candidates,
|
||||
algorithm_version: args.p_algorithm_version, event_contract_version: args.p_event_contract_version,
|
||||
decision_policy_version: args.p_decision_policy_version, decision_receipt: args.p_decision_receipt,
|
||||
execution_ledger: args.p_execution_ledger, overall_confidence: golden.score.decision_receipt.overall_confidence,
|
||||
selection_allowed: golden.score.decision_receipt.selection_allowed, confirmation_allowed: false,
|
||||
representative_time: golden.score.decision_receipt.representative_time,
|
||||
}),
|
||||
set_agentic_rectification_conversation_focus: (_fn, args) => ({
|
||||
focus: { id: FOCUS_ID, case_id: CASE_ID, question_id: args.p_question_id,
|
||||
intent: args.p_intent, target_evidence_id: args.p_target_evidence_id,
|
||||
target_domain: args.p_target_domain, target_kind: args.p_target_kind,
|
||||
expected_answer_schema: args.p_expected_answer_schema, status: "active" }, idempotent: false,
|
||||
}),
|
||||
});
|
||||
const tools = createRectificationV9Tools({ userId: USER_ID, caseId: CASE_ID, turnId: TURN_ID, accounting: accounting.client as never });
|
||||
for (const name of ["rectification-compare-candidates", "rectification-read-diagnostics"] as const) {
|
||||
await (tools[name] as unknown as { execute(input: unknown): Promise<unknown> }).execute({ caseId: CASE_ID });
|
||||
const receipts = accounting.calls.filter((call) => call.fn === "insert_agentic_rectification_tool_receipt" && call.args.p_tool_name === name);
|
||||
assert.deepEqual(receipts.map((call) => [call.args.p_status, call.args.p_engine_version]), [
|
||||
["started", CURRENT], ["completed", CURRENT],
|
||||
]);
|
||||
}
|
||||
assert.ok(calls.includes("/api/rectification/v5/score"));
|
||||
const persisted = accounting.calls.find((call) => call.fn === "persist_agentic_rectification_candidate_v2");
|
||||
assert.equal(persisted?.args.p_algorithm_version, CURRENT);
|
||||
});
|
||||
|
||||
test("history opens with its bound Skill and reads old engine receipt values unchanged", async (t) => {
|
||||
isolateIdentityEnv(t);
|
||||
const bound = resolveSkillPackageVersion("jyotish-birth-time-rectification", "10.0.17");
|
||||
for (const oldVersion of ["rectification-v5", PREVIOUS]) {
|
||||
const historical = { turn_id: TURN_ID, engine_version: oldVersion, skill_name: bound.name, skill_version: bound.version, status: "completed" };
|
||||
const before = structuredClone(historical);
|
||||
const open = { disposition: "readonly", case_id: CASE_ID, session_id: SESSION_ID,
|
||||
status: "closed", should_start_opening: false, skill_version: bound.version };
|
||||
const accounting = fakeAccounting({
|
||||
open_agentic_rectification_case: () => open,
|
||||
get_agentic_rectification_skill_identity: () => ({ skill_name: bound.name, skill_version: bound.version, skill_sha256: bound.sha256, skill_source_commit: bound.sourceCommit }),
|
||||
open_agentic_rectification_case_v2: (_fn, args) => {
|
||||
assert.equal(args.p_skill_version, bound.version);
|
||||
assert.equal(args.p_skill_sha256, bound.sha256);
|
||||
assert.equal("p_engine_version" in args, false);
|
||||
return open;
|
||||
},
|
||||
get_agentic_rectification_turn_receipt: () => historical,
|
||||
});
|
||||
const opened = await openRectificationCase(accounting.client as never, USER_ID, {
|
||||
intent: "session", requestId: "aaaaaaaa-bbbb-4ccc-8ddd-eeeeeeeeeeee", sessionId: SESSION_ID,
|
||||
});
|
||||
assert.equal(opened.disposition, "readonly");
|
||||
const receipt = await loadV9TurnReceipt(accounting.client as never, USER_ID, CASE_ID, TURN_ID);
|
||||
assert.equal(v9EngineVersion(), CURRENT);
|
||||
assert.equal(receipt?.engineVersion, oldVersion);
|
||||
assert.deepEqual(historical, before);
|
||||
assert.equal(accounting.calls.some((call) => /insert|persist|upgrade/.test(call.fn)), false);
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user