fix(rectification): close round A/2 tail gaps in tests, CI, and stale-score reuse
Window_scan assertions now match the public from_sign/to_sign contract, the staging quick gate runs the rectification Python suite, and compare-candidates rescores when stored policy lags the live engine identity. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -181,11 +181,9 @@ function engineBase(): string {
|
||||
return process.env.JYOTISH_API_BASE?.trim() || "http://127.0.0.1:5200";
|
||||
}
|
||||
|
||||
async function postEngine(path: string, body: unknown, timeoutMs = 60_000): Promise<Record<string, unknown>> {
|
||||
async function readEngineJson(path: string, init: RequestInit, timeoutMs: number): Promise<Record<string, unknown>> {
|
||||
const response = await fetch(`${engineBase()}${path}`, {
|
||||
method: "POST",
|
||||
headers: { "content-type": "application/json" },
|
||||
body: JSON.stringify(body),
|
||||
...init,
|
||||
signal: AbortSignal.timeout(timeoutMs),
|
||||
});
|
||||
const data = await response.json().catch(() => null);
|
||||
@@ -201,6 +199,87 @@ async function postEngine(path: string, body: unknown, timeoutMs = 60_000): Prom
|
||||
return data as Record<string, unknown>;
|
||||
}
|
||||
|
||||
async function postEngine(path: string, body: unknown, timeoutMs = 60_000): Promise<Record<string, unknown>> {
|
||||
return readEngineJson(path, {
|
||||
method: "POST",
|
||||
headers: { "content-type": "application/json" },
|
||||
body: JSON.stringify(body),
|
||||
}, timeoutMs);
|
||||
}
|
||||
|
||||
async function getEngine(path: string, timeoutMs = 5_000): Promise<Record<string, unknown>> {
|
||||
return readEngineJson(path, {
|
||||
method: "GET",
|
||||
headers: { accept: "application/json" },
|
||||
}, timeoutMs);
|
||||
}
|
||||
|
||||
/** Coarse product id is not a scoring identity; never treat it as live algorithm. */
|
||||
const COARSE_ENGINE_PRODUCT_ID = "rectification-v5";
|
||||
|
||||
export type LiveEngineScoringIdentity = Readonly<{
|
||||
algorithmVersion: string | null;
|
||||
policyVersion: string | null;
|
||||
}>;
|
||||
|
||||
export function liveEngineScoringIdentityFromEnv(
|
||||
env: NodeJS.ProcessEnv | Record<string, string | undefined> = process.env,
|
||||
): LiveEngineScoringIdentity {
|
||||
const engine = env.RECTIFICATION_ENGINE_VERSION?.trim() || "";
|
||||
const algorithm = env.RECTIFICATION_ALGORITHM_VERSION?.trim()
|
||||
|| (engine && engine !== COARSE_ENGINE_PRODUCT_ID ? engine : "");
|
||||
const policy = env.RECTIFICATION_DECISION_POLICY_VERSION?.trim() || "";
|
||||
return {
|
||||
algorithmVersion: algorithm || null,
|
||||
policyVersion: policy || null,
|
||||
};
|
||||
}
|
||||
|
||||
export function scoringIdentityFromEnginePayload(
|
||||
payload: Readonly<Record<string, unknown>> | null | undefined,
|
||||
): LiveEngineScoringIdentity {
|
||||
const algorithm = typeof payload?.algorithm_version === "string" ? payload.algorithm_version.trim() : "";
|
||||
const policy = typeof payload?.decision_policy_version === "string"
|
||||
? payload.decision_policy_version.trim()
|
||||
: "";
|
||||
return {
|
||||
algorithmVersion: algorithm || null,
|
||||
policyVersion: policy || null,
|
||||
};
|
||||
}
|
||||
|
||||
export function cachedEngineScoreIsReusable(
|
||||
stored: {
|
||||
evidenceLedgerFingerprint?: string | null;
|
||||
candidateRangeFingerprint?: string | null;
|
||||
algorithmVersion?: string | null;
|
||||
policyVersion?: string | null;
|
||||
} | null | undefined,
|
||||
fingerprints: { evidenceLedgerFingerprint: string; candidateRangeFingerprint: string },
|
||||
live: LiveEngineScoringIdentity,
|
||||
): boolean {
|
||||
if (!stored) return false;
|
||||
if (stored.evidenceLedgerFingerprint !== fingerprints.evidenceLedgerFingerprint) return false;
|
||||
if (stored.candidateRangeFingerprint !== fingerprints.candidateRangeFingerprint) return false;
|
||||
if (live.policyVersion && stored.policyVersion && stored.policyVersion !== live.policyVersion) {
|
||||
return false;
|
||||
}
|
||||
if (live.algorithmVersion && stored.algorithmVersion && stored.algorithmVersion !== live.algorithmVersion) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
export async function readV9EngineScoringIdentity(): Promise<LiveEngineScoringIdentity> {
|
||||
const fromEnv = liveEngineScoringIdentityFromEnv();
|
||||
if (fromEnv.policyVersion || fromEnv.algorithmVersion) return fromEnv;
|
||||
try {
|
||||
return scoringIdentityFromEnginePayload(await getEngine("/api/rectification/v5/versions"));
|
||||
} catch {
|
||||
return { algorithmVersion: null, policyVersion: null };
|
||||
}
|
||||
}
|
||||
|
||||
function readCandidates(
|
||||
value: unknown,
|
||||
range: { start_time: string; end_time: string },
|
||||
|
||||
@@ -132,6 +132,8 @@ import {
|
||||
runV9CandidateScore,
|
||||
runV9Diagnostics,
|
||||
runV9VedastroValidate,
|
||||
readV9EngineScoringIdentity,
|
||||
cachedEngineScoreIsReusable,
|
||||
toEngineEvents,
|
||||
v9EngineVersion,
|
||||
executedMethodsFromLedger,
|
||||
@@ -830,10 +832,14 @@ export function createRectificationV9Tools(ctx: RectificationV9Context) {
|
||||
);
|
||||
const events = toEngineEvents(scorableEvidence(dossier.evidence));
|
||||
const latest = dossier.latestResult;
|
||||
const liveIdentity = await readV9EngineScoringIdentity();
|
||||
if (
|
||||
latest
|
||||
&& latest.evidenceLedgerFingerprint === evidenceFingerprint
|
||||
&& latest.candidateRangeFingerprint === rangeFingerprint
|
||||
&& cachedEngineScoreIsReusable(
|
||||
latest,
|
||||
{ evidenceLedgerFingerprint: evidenceFingerprint, candidateRangeFingerprint: rangeFingerprint },
|
||||
liveIdentity,
|
||||
)
|
||||
) {
|
||||
const ledger = latest.executionLedger ?? [];
|
||||
const windowScan = windowScanFromDecisionReceipt(latest.decisionReceipt);
|
||||
|
||||
@@ -11,6 +11,10 @@ import {
|
||||
} from "../src/lib/rectification-agentic/core/snapshot-source.ts";
|
||||
import type { ConflictProbe } from "../src/lib/rectification-agentic/core/types.ts";
|
||||
import { contrastPacketFromLatestResult } from "../src/lib/rectification-agentic/v9/decision-from-dossier.ts";
|
||||
import {
|
||||
cachedEngineScoreIsReusable,
|
||||
liveEngineScoringIdentityFromEnv,
|
||||
} from "../src/lib/rectification-agentic/v9/engine-client.ts";
|
||||
import {
|
||||
parseDiscriminatingEventProbes,
|
||||
parseProspectiveProbes,
|
||||
@@ -84,6 +88,39 @@ test("scoring policy version changes stale stored snapshots", () => {
|
||||
assert.equal(storedSnapshotIsCurrent(current, current), true);
|
||||
});
|
||||
|
||||
test("matching fingerprints still rescore when stored policy lags the live engine", () => {
|
||||
const fingerprints = {
|
||||
evidenceLedgerFingerprint: "b".repeat(64),
|
||||
candidateRangeFingerprint: "c".repeat(64),
|
||||
};
|
||||
const stored = {
|
||||
...fingerprints,
|
||||
algorithmVersion: "rectification-v5-matrix-scoring-6",
|
||||
policyVersion: "rectification-candidate-policy-v2",
|
||||
};
|
||||
assert.equal(
|
||||
cachedEngineScoreIsReusable(stored, fingerprints, {
|
||||
algorithmVersion: "rectification-v5-matrix-scoring-6",
|
||||
policyVersion: "rectification-candidate-policy-v2",
|
||||
}),
|
||||
true,
|
||||
);
|
||||
assert.equal(
|
||||
cachedEngineScoreIsReusable(stored, fingerprints, {
|
||||
algorithmVersion: "rectification-v5-matrix-scoring-7",
|
||||
policyVersion: "rectification-candidate-policy-v3",
|
||||
}),
|
||||
false,
|
||||
);
|
||||
const fromEnv = liveEngineScoringIdentityFromEnv({
|
||||
RECTIFICATION_ENGINE_VERSION: "rectification-v5",
|
||||
RECTIFICATION_DECISION_POLICY_VERSION: "rectification-candidate-policy-v3",
|
||||
});
|
||||
assert.equal(fromEnv.algorithmVersion, null);
|
||||
assert.equal(fromEnv.policyVersion, "rectification-candidate-policy-v3");
|
||||
assert.equal(cachedEngineScoreIsReusable(stored, fingerprints, fromEnv), false);
|
||||
});
|
||||
|
||||
test("anchored known_event_quality distinguish probes stay in the public packet", () => {
|
||||
const parsed = parseDiscriminatingEventProbes([qualityProbe()]);
|
||||
assert.equal(parsed.length, 1);
|
||||
|
||||
@@ -692,7 +692,17 @@ test("compare-candidates reuses a matching fingerprint without calling the engin
|
||||
const evidenceFp = evidenceLedgerFingerprint(parsed.evidence);
|
||||
const rangeFp = candidateRangeFingerprint(parsed.case.candidateRange, compute.baselineProfileFingerprint);
|
||||
const previous = globalThis.fetch;
|
||||
globalThis.fetch = (async () => {
|
||||
globalThis.fetch = (async (input: RequestInfo | URL) => {
|
||||
const url = String(input);
|
||||
if (url.includes("/api/rectification/v5/versions")) {
|
||||
return {
|
||||
ok: true,
|
||||
json: async () => ({
|
||||
algorithm_version: "rectification-v5",
|
||||
decision_policy_version: "rectification-candidate-policy-v2",
|
||||
}),
|
||||
} as Response;
|
||||
}
|
||||
throw new Error("engine must not run on a matching fingerprint");
|
||||
}) as unknown as typeof fetch;
|
||||
try {
|
||||
@@ -729,6 +739,65 @@ test("compare-candidates reuses a matching fingerprint without calling the engin
|
||||
}
|
||||
});
|
||||
|
||||
test("compare-candidates rescores when stored policy lags the live engine", async () => {
|
||||
const rawDossier = dossierFixture();
|
||||
const parsed = parseV9CaseDossier(rawDossier);
|
||||
const compute = parseV9ComputeProjection(computeFixture());
|
||||
assert.ok(parsed);
|
||||
assert.ok(compute);
|
||||
assert.ok(parsed.case.candidateRange);
|
||||
const evidenceFp = evidenceLedgerFingerprint(parsed.evidence);
|
||||
const rangeFp = candidateRangeFingerprint(parsed.case.candidateRange, compute.baselineProfileFingerprint);
|
||||
const previous = globalThis.fetch;
|
||||
let scoreRequested = false;
|
||||
globalThis.fetch = (async (input: RequestInfo | URL) => {
|
||||
const url = String(input);
|
||||
if (url.includes("/api/rectification/v5/versions")) {
|
||||
return {
|
||||
ok: true,
|
||||
json: async () => ({
|
||||
algorithm_version: "rectification-v5-matrix-scoring-7",
|
||||
decision_policy_version: "rectification-candidate-policy-v3",
|
||||
}),
|
||||
} as Response;
|
||||
}
|
||||
if (url.includes("/api/rectification/v5/score")) {
|
||||
scoreRequested = true;
|
||||
}
|
||||
throw new Error("stale policy must rescore");
|
||||
}) as unknown as typeof fetch;
|
||||
try {
|
||||
const accounting = fakeAccounting({
|
||||
...receiptHandlers,
|
||||
get_agentic_rectification_case_dossier: () => dossierFixture({
|
||||
latestResult: {
|
||||
...candidateSnapshotFixture({ evidenceLedgerFingerprint: evidenceFp }),
|
||||
candidate_range_fingerprint: rangeFp,
|
||||
},
|
||||
}),
|
||||
get_agentic_rectification_case_compute: () => computeFixture(),
|
||||
persist_agentic_rectification_candidate_v2: () => {
|
||||
throw new Error("persist must not run until the live engine scores");
|
||||
},
|
||||
});
|
||||
const tools = createRectificationV9Tools({
|
||||
userId: USER_ID,
|
||||
caseId: CASE_ID,
|
||||
turnId: TURN_ID,
|
||||
accounting: accounting.client as never,
|
||||
});
|
||||
await assert.rejects(
|
||||
(tools["rectification-compare-candidates"] as unknown as {
|
||||
execute(input: unknown): Promise<unknown>;
|
||||
}).execute({ caseId: CASE_ID }),
|
||||
(error: unknown) => error instanceof Error && error.message.includes("stale policy must rescore"),
|
||||
);
|
||||
assert.equal(scoreRequested, true);
|
||||
} finally {
|
||||
globalThis.fetch = previous;
|
||||
}
|
||||
});
|
||||
|
||||
test("compare-candidates refuses to run without scorable evidence", async () => {
|
||||
const accounting = fakeAccounting({
|
||||
...receiptHandlers,
|
||||
|
||||
Reference in New Issue
Block a user