feat(rectification): read the adopted segment result's unreliable charts for reports and chat (BUG-1138, T1)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017eEAG8HD3mm8gsKXgk8uU8
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
44cdd9c9b9
commit
35b89bdf21
@@ -0,0 +1,119 @@
|
||||
import { parseSegmentSummary, TARGET_CHARTS, type SegmentSummary, type TargetChart } from "./rectification-agentic/core/segment-summary.ts";
|
||||
|
||||
/**
|
||||
* Which divisional charts a segment-v1 adoption could not determine (BUG-1138/1139).
|
||||
*
|
||||
* The rectification delivery card already says "D9 cannot be determined" for a
|
||||
* wide window; reports and chat used to read every chart from the saved minute
|
||||
* anyway. This module reads the adopted result's own segment summary so they can
|
||||
* say the same thing. Anything else (older adoptions, unrectified profiles, a
|
||||
* missing or malformed result) returns null and leaves callers unchanged.
|
||||
*/
|
||||
|
||||
export const UNRELIABLE_CHART_TIERS = new Set(["blocked", "indistinct"]);
|
||||
export const CASE_DOSSIER_RPC = "get_agentic_rectification_case_dossier";
|
||||
|
||||
export type SegmentProvenance = Readonly<{ caseId: string; resultId: string }>;
|
||||
|
||||
export type AdoptedChartTiers = Readonly<{
|
||||
source: "segment-v1";
|
||||
/** "adopted": the summary of the adopted result; "latest": a newer result of the same Case. */
|
||||
resultMatch: "adopted" | "latest";
|
||||
unreliable: readonly TargetChart[];
|
||||
}>;
|
||||
|
||||
type RpcResponse = PromiseLike<{ data: unknown; error: unknown }>;
|
||||
export type AdoptedChartTierClient = Readonly<{ rpc: (fn: string, args: Record<string, unknown>) => RpcResponse }>;
|
||||
export type AdoptedChartTierWarn = (payload: Readonly<{ event: "adopted_chart_tiers_unavailable"; reason: string }>) => void;
|
||||
|
||||
const uuidPattern = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
||||
|
||||
function record(value: unknown): Record<string, unknown> | null {
|
||||
return value !== null && typeof value === "object" && !Array.isArray(value) ? value as Record<string, unknown> : null;
|
||||
}
|
||||
|
||||
/** Only the segment-v1 adoption contract carries a result whose summary describes the saved minute. */
|
||||
export function segmentProvenance(value: unknown): SegmentProvenance | null {
|
||||
const row = record(value);
|
||||
if (!row || row.contract !== "segment-v1") return null;
|
||||
const caseId = typeof row.case_id === "string" ? row.case_id : "";
|
||||
const resultId = typeof row.result_id === "string" ? row.result_id : "";
|
||||
return uuidPattern.test(caseId) && uuidPattern.test(resultId) ? { caseId, resultId } : null;
|
||||
}
|
||||
|
||||
export function unreliableChartsFromSummary(summary: SegmentSummary): TargetChart[] {
|
||||
return TARGET_CHARTS.filter((chart) => summary.charts.some((row) => row.chart === chart && UNRELIABLE_CHART_TIERS.has(row.tier)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Prefer the adopted result. A later result of the same Case (answers after
|
||||
* adoption) carries strictly more evidence about the same window, so its tiers
|
||||
* are used instead; a different Case is never read.
|
||||
*/
|
||||
export function chartTiersFromDossier(dossier: unknown, provenance: SegmentProvenance): AdoptedChartTiers | "invalid" {
|
||||
const root = record(dossier);
|
||||
const caseRow = record(root?.case);
|
||||
const latest = record(root?.latest_result);
|
||||
const caseId = typeof caseRow?.case_id === "string" ? caseRow.case_id : typeof caseRow?.id === "string" ? caseRow.id : null;
|
||||
if (!root || !latest || (caseId !== null && caseId !== provenance.caseId)) return "invalid";
|
||||
const resultId = typeof latest.result_id === "string" ? latest.result_id : null;
|
||||
if (!resultId) return "invalid";
|
||||
const receipt = record(latest.decision_receipt);
|
||||
const state = record(receipt?.inference_state);
|
||||
const summary = parseSegmentSummary(state?.segment_summary);
|
||||
if (!summary) return "invalid";
|
||||
return {
|
||||
source: "segment-v1",
|
||||
resultMatch: resultId === provenance.resultId ? "adopted" : "latest",
|
||||
unreliable: unreliableChartsFromSummary(summary),
|
||||
};
|
||||
}
|
||||
|
||||
const CACHE_TTL_MS = 5 * 60 * 1000;
|
||||
const CACHE_LIMIT = 500;
|
||||
const cache = new Map<string, { at: number; value: AdoptedChartTiers }>();
|
||||
|
||||
export function clearAdoptedChartTierCache(): void {
|
||||
cache.clear();
|
||||
}
|
||||
|
||||
function defaultWarn(payload: Readonly<{ event: string; reason: string }>): void {
|
||||
console.warn(JSON.stringify(payload));
|
||||
}
|
||||
|
||||
function errorCode(error: unknown): string {
|
||||
if (error && typeof error === "object" && "code" in error && typeof error.code === "string" && error.code.trim()) return error.code.trim();
|
||||
if (error instanceof Error && error.name.trim()) return error.name;
|
||||
return "unknown";
|
||||
}
|
||||
|
||||
export async function loadAdoptedChartTiers(
|
||||
client: AdoptedChartTierClient,
|
||||
input: Readonly<{ userId: string; provenance: unknown }>,
|
||||
warn: AdoptedChartTierWarn = defaultWarn,
|
||||
now: () => number = Date.now,
|
||||
): Promise<AdoptedChartTiers | null> {
|
||||
const provenance = segmentProvenance(input.provenance);
|
||||
if (!provenance) return null;
|
||||
const key = `${input.userId}|${provenance.caseId}|${provenance.resultId}`;
|
||||
const hit = cache.get(key);
|
||||
if (hit && now() - hit.at < CACHE_TTL_MS) return hit.value;
|
||||
try {
|
||||
const result = await client.rpc(CASE_DOSSIER_RPC, { p_user_id: input.userId, p_case_id: provenance.caseId });
|
||||
if (result.error) {
|
||||
warn({ event: "adopted_chart_tiers_unavailable", reason: errorCode(result.error) });
|
||||
return null;
|
||||
}
|
||||
const tiers = chartTiersFromDossier(result.data, provenance);
|
||||
if (tiers === "invalid") {
|
||||
warn({ event: "adopted_chart_tiers_unavailable", reason: "invalid_segment_summary" });
|
||||
return null;
|
||||
}
|
||||
if (cache.size >= CACHE_LIMIT) cache.delete(cache.keys().next().value!);
|
||||
cache.set(key, { at: now(), value: tiers });
|
||||
return tiers;
|
||||
} catch (error) {
|
||||
warn({ event: "adopted_chart_tiers_unavailable", reason: errorCode(error) });
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { readFileSync } from "node:fs";
|
||||
import test from "node:test";
|
||||
import { buildSegmentSummary, type SegmentMinute } from "../src/lib/rectification-agentic/core/segment-summary.ts";
|
||||
import {
|
||||
CASE_DOSSIER_RPC,
|
||||
chartTiersFromDossier,
|
||||
clearAdoptedChartTierCache,
|
||||
loadAdoptedChartTiers,
|
||||
segmentProvenance,
|
||||
} from "../src/lib/rectification-adopted-chart-tiers.ts";
|
||||
|
||||
// Real native scan of a public Rodden-AA case (31-minute window), not a hand-built shape.
|
||||
const golden = JSON.parse(readFileSync(new URL("./fixtures/varga-api-response.golden.json", import.meta.url), "utf8"));
|
||||
const realMinutes = golden.scan.minutes as SegmentMinute[];
|
||||
|
||||
const userId = "11111111-1111-4111-8111-111111111111";
|
||||
const caseId = "22222222-2222-4222-8222-222222222222";
|
||||
const resultId = "33333333-3333-4333-8333-333333333333";
|
||||
const newerResultId = "44444444-4444-4444-8444-444444444444";
|
||||
const segmentV1 = { contract: "segment-v1", case_id: caseId, result_id: resultId, candidate_time: "09:05" };
|
||||
|
||||
// Real 31-minute scan: D1 is one sign (certain); every candidate weighs on the
|
||||
// first D9/D10 segments so the posterior picks them.
|
||||
function realSummary() {
|
||||
const candidates = realMinutes.filter((_, index) => index % 2 === 0).map((minute) => ({ time: minute.time, score: minute.offset <= 4 ? 12 : 1 }));
|
||||
return buildSegmentSummary({ minutes: realMinutes, candidates, windowMinutes: realMinutes.length, scanComplete: true });
|
||||
}
|
||||
|
||||
// Explicitly fictional ±60 control: a 121-minute window makes D9/D10 blocked by width alone.
|
||||
function wideSummary() {
|
||||
const minutes: SegmentMinute[] = Array.from({ length: 121 }, (_, offset) => ({
|
||||
offset, date: "2000-06-14", time: `${String(8 + Math.floor(offset / 60)).padStart(2, "0")}:${String(offset % 60).padStart(2, "0")}`,
|
||||
signs: { D1: 4, D9: Math.floor(offset / 13) % 12, D10: Math.floor(offset / 12) % 12 },
|
||||
}));
|
||||
return buildSegmentSummary({ minutes, candidates: [{ time: minutes[60]!.time, score: 10 }], windowMinutes: 121, scanComplete: true });
|
||||
}
|
||||
|
||||
function dossier(summary: unknown, id = resultId, owningCase = caseId) {
|
||||
return { case: { case_id: owningCase }, latest_result: { result_id: id, decision_receipt: { inference_state: { segment_summary: summary } } } };
|
||||
}
|
||||
|
||||
function client(data: unknown, error: unknown = null) {
|
||||
const calls: { fn: string; args: Record<string, unknown> }[] = [];
|
||||
return {
|
||||
calls,
|
||||
rpc: async (fn: string, args: Record<string, unknown>) => { calls.push({ fn, args }); return { data, error }; },
|
||||
};
|
||||
}
|
||||
|
||||
test("provenance: only segment-v1 with uuid case/result is read", () => {
|
||||
assert.deepEqual(segmentProvenance(segmentV1), { caseId, resultId });
|
||||
for (const value of [null, {}, { contract: "candidate-v2", case_id: caseId, result_id: resultId }, { ...segmentV1, case_id: "x" }, "segment-v1"]) {
|
||||
assert.equal(segmentProvenance(value), null);
|
||||
}
|
||||
});
|
||||
|
||||
test("five inputs: segment-v1 / legacy adoption / unrectified / missing result / corrupt summary", async () => {
|
||||
clearAdoptedChartTierCache();
|
||||
const warnings: string[] = [];
|
||||
const warn = (payload: { reason: string }) => { warnings.push(payload.reason); };
|
||||
|
||||
// 1. segment-v1 with a wide window: D9 and D10 blocked, D1 determined.
|
||||
const wide = client(dossier(wideSummary()));
|
||||
assert.deepEqual(await loadAdoptedChartTiers(wide, { userId, provenance: segmentV1 }, warn),
|
||||
{ source: "segment-v1", resultMatch: "adopted", unreliable: ["D9", "D10"] });
|
||||
assert.deepEqual(wide.calls, [{ fn: CASE_DOSSIER_RPC, args: { p_user_id: userId, p_case_id: caseId } }]);
|
||||
|
||||
// 2. Legacy (candidate-minute) adoption and 3. unrectified profile: no RPC, null.
|
||||
const untouched = client(dossier(wideSummary()));
|
||||
assert.equal(await loadAdoptedChartTiers(untouched, { userId, provenance: { contract: "candidate-v2", case_id: caseId, result_id: resultId } }, warn), null);
|
||||
assert.equal(await loadAdoptedChartTiers(untouched, { userId, provenance: null }, warn), null);
|
||||
assert.equal(untouched.calls.length, 0);
|
||||
|
||||
// 4. Result missing (RPC error, or a dossier without a latest result): null and a warning.
|
||||
clearAdoptedChartTierCache();
|
||||
assert.equal(await loadAdoptedChartTiers(client(null, { code: "P0001" }), { userId, provenance: segmentV1 }, warn), null);
|
||||
assert.equal(await loadAdoptedChartTiers(client({ case: { case_id: caseId } }), { userId, provenance: segmentV1 }, warn), null);
|
||||
|
||||
// 5. Corrupt summary (tier inconsistent with its own shares): null and a warning.
|
||||
const corrupt = JSON.parse(JSON.stringify(wideSummary()));
|
||||
corrupt.charts[1].tier = "credible";
|
||||
assert.equal(await loadAdoptedChartTiers(client(dossier(corrupt)), { userId, provenance: segmentV1 }, warn), null);
|
||||
assert.deepEqual(warnings, ["P0001", "invalid_segment_summary", "invalid_segment_summary"]);
|
||||
});
|
||||
|
||||
test("real narrow scan: certain D1 and credible D9/D10 are not flagged", () => {
|
||||
const summary = realSummary();
|
||||
const tiers = Object.fromEntries(summary.charts.map((chart) => [chart.chart, chart.tier]));
|
||||
assert.equal(tiers.D1, "certain");
|
||||
const result = chartTiersFromDossier(dossier(summary), { caseId, resultId });
|
||||
assert.notEqual(result, "invalid");
|
||||
if (result === "invalid") return;
|
||||
assert.deepEqual(result.unreliable, summary.charts.filter((chart) => chart.tier === "blocked" || chart.tier === "indistinct").map((chart) => chart.chart));
|
||||
assert.ok(!result.unreliable.includes("D1"));
|
||||
});
|
||||
|
||||
test("a newer result of the same Case is read; another Case never is", () => {
|
||||
const newer = chartTiersFromDossier(dossier(wideSummary(), newerResultId), { caseId, resultId });
|
||||
assert.deepEqual(newer, { source: "segment-v1", resultMatch: "latest", unreliable: ["D9", "D10"] });
|
||||
assert.equal(chartTiersFromDossier(dossier(wideSummary(), resultId, "55555555-5555-4555-8555-555555555555"), { caseId, resultId }), "invalid");
|
||||
});
|
||||
|
||||
test("results are cached per adopted result for five minutes", async () => {
|
||||
clearAdoptedChartTierCache();
|
||||
const reader = client(dossier(wideSummary()));
|
||||
let now = 1_000;
|
||||
await loadAdoptedChartTiers(reader, { userId, provenance: segmentV1 }, () => {}, () => now);
|
||||
now += 60_000;
|
||||
await loadAdoptedChartTiers(reader, { userId, provenance: segmentV1 }, () => {}, () => now);
|
||||
assert.equal(reader.calls.length, 1);
|
||||
now += 5 * 60_000;
|
||||
await loadAdoptedChartTiers(reader, { userId, provenance: segmentV1 }, () => {}, () => now);
|
||||
assert.equal(reader.calls.length, 2);
|
||||
});
|
||||
Reference in New Issue
Block a user