fix(rectification): offer/compare model views drop audit-only fields (R1, BUG-593 side path)
T5 of TASK-rectification-grounding-20260927. - rectification-offer-candidates returned the full projection (~100 KB on the public AA case: *_pre_inference, 31 KB contrast packet, probe lists). It now returns the compare stripping (offerModelProjection); 100,012 → 28,799 bytes. - agentVisibleLatestProjection also drops engine_indistinguishable_width_minutes (audit-only since BUG-593), keeps the verification Markdown once (skill_verification_report; range_delivery.verification_markdown was a byte-identical copy) and shows the slim candidate list. - Receipt fingerprints stay over the full payloads; the case API projection (latestResultToolProjection) is unchanged. Contract tests on a real local engine response (public AA chart). 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
5b9fbe5b36
commit
1ed3e55579
@@ -114,7 +114,7 @@ import {
|
||||
publicEvidenceItemStatus,
|
||||
resolveEvidenceQuote,
|
||||
} from "@/lib/rectification-agentic/v9/evidence-quote";
|
||||
import { projectTurnDecision } from "@/lib/rectification-agentic/v9/turn-decision";
|
||||
import { modelVisibleInference, projectTurnDecision } from "@/lib/rectification-agentic/v9/turn-decision";
|
||||
import {
|
||||
contrastPacketFromLatestResult,
|
||||
decideFromDossier,
|
||||
@@ -624,8 +624,16 @@ function agentVisibleLatestProjection(
|
||||
representative_time_pre_inference: _preTime,
|
||||
house_table_pre_inference: _preHouse,
|
||||
natal_recast_pre_inference: _preNatal,
|
||||
// R1 (TASK-rectification-grounding-20260927): the engine's own width is
|
||||
// audit-only (BUG-593); the report width is skill_verification_report.
|
||||
engine_indistinguishable_width_minutes: _engineWidth,
|
||||
...rest
|
||||
} = projection;
|
||||
// R1: the verification Markdown is kept once, in skill_verification_report;
|
||||
// range_delivery carried a byte-identical copy.
|
||||
const rangeDelivery = rest.range_delivery && typeof rest.range_delivery === "object" && !Array.isArray(rest.range_delivery)
|
||||
? (({ verification_markdown: _markdown, ...delivery }) => delivery)(rest.range_delivery as Record<string, unknown>)
|
||||
: rest.range_delivery;
|
||||
const currentQuestion = extras.openQuestion
|
||||
? extras.openQuestion.unrenderable === true
|
||||
? {
|
||||
@@ -647,13 +655,15 @@ function agentVisibleLatestProjection(
|
||||
probe_id: extras.openQuestion.probe_id,
|
||||
}
|
||||
: null;
|
||||
// BUG-1057: the same slim candidate list the read-case shows.
|
||||
const compactInference = inference && typeof inference === "object" && !Array.isArray(inference)
|
||||
? inference as Record<string, unknown>
|
||||
? modelVisibleInference(inference as Record<string, unknown>)
|
||||
: null;
|
||||
const renderableChoice = extras.openQuestion?.kind === "choice"
|
||||
&& extras.openQuestion.unrenderable !== true;
|
||||
return {
|
||||
...rest,
|
||||
...(rangeDelivery !== undefined ? { range_delivery: rangeDelivery } : {}),
|
||||
current_question: currentQuestion,
|
||||
current_probe: null,
|
||||
inference_state: renderableChoice && compactInference
|
||||
@@ -675,6 +685,17 @@ function agentVisibleLatestProjection(
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* What rectification-offer-candidates hands the model (R1): the compare
|
||||
* stripping applied to the offer payload. No open question rides on an offer.
|
||||
*/
|
||||
export function offerModelProjection(payload: Record<string, unknown>): Record<string, unknown> {
|
||||
return agentVisibleLatestProjection(payload, {
|
||||
openQuestion: null,
|
||||
focusStatus: "not_applicable",
|
||||
});
|
||||
}
|
||||
|
||||
function followupPlanForParsed(
|
||||
parsed: DossierForTools,
|
||||
latest: DossierForTools["latestResult"],
|
||||
@@ -2202,7 +2223,11 @@ export function createRectificationV9Tools(ctx: RectificationV9Context) {
|
||||
inputFingerprint,
|
||||
resultFingerprint: hashResult(payload),
|
||||
});
|
||||
return payload;
|
||||
// R1 (BUG-593 side path): the model gets the same stripped projection
|
||||
// compare returns — no *_pre_inference, contrast packet, probes,
|
||||
// duplicate report Markdown or engine width. The receipt fingerprint
|
||||
// above stays over the full payload.
|
||||
return offerModelProjection(payload);
|
||||
} catch (error) {
|
||||
await failReceipt("rectification-offer-candidates", "candidates.updated", error, { inputFingerprint });
|
||||
throw error;
|
||||
|
||||
@@ -0,0 +1,142 @@
|
||||
/**
|
||||
* TASK-rectification-grounding-20260927 T5 (R1, BUG-593 side path): what
|
||||
* compare and offer hand the model. Fixture: a real local engine response for
|
||||
* a public AA chart (9 candidates), AGENTS §7.4.
|
||||
*
|
||||
* - offer used to return the full projection (~100 KB): `*_pre_inference`,
|
||||
* the contrast packet (31 KB) and the probe lists. It now gets the compare
|
||||
* stripping.
|
||||
* - the verification Markdown is kept once (skill_verification_report);
|
||||
* range_delivery carried a byte-identical copy.
|
||||
* - `engine_indistinguishable_width_minutes` is audit-only (BUG-593) and no
|
||||
* longer reaches the model.
|
||||
* - batch returns the post-rescore range and whether this turn delivers.
|
||||
* The full projection used by the case API and the stored receipts is
|
||||
* unchanged.
|
||||
*/
|
||||
import assert from "node:assert/strict";
|
||||
import { readFileSync } from "node:fs";
|
||||
import test from "node:test";
|
||||
import {
|
||||
CASE_ID,
|
||||
TURN_ID,
|
||||
USER_ID,
|
||||
dossierFixture,
|
||||
fakeAccounting,
|
||||
receiptHandlers,
|
||||
} from "./rectification-v9-test-support.ts";
|
||||
import {
|
||||
AA_EVIDENCE_ROWS,
|
||||
AA_RANGE,
|
||||
AA_TURNS,
|
||||
buildGoldenLatest,
|
||||
setFocusHandler,
|
||||
stubGoldenFetch,
|
||||
} from "./rectification-grounding-support.ts";
|
||||
import {
|
||||
batchRangeAfterRescore,
|
||||
createRectificationV9Tools,
|
||||
latestResultToolProjection,
|
||||
offerModelProjection,
|
||||
} from "../src/mastra/rectification-v9-tools.ts";
|
||||
import { parseV9CaseDossier } from "../src/lib/rectification-agentic/v9/tool-service.ts";
|
||||
import { decideFromDossier } from "../src/lib/rectification-agentic/v9/decision-from-dossier.ts";
|
||||
|
||||
const bytes = (value: unknown) => Buffer.byteLength(JSON.stringify(value), "utf8");
|
||||
|
||||
const MODEL_HIDDEN_KEYS = [
|
||||
"candidate_contrast_packet",
|
||||
"discriminating_event_probes",
|
||||
"event_clarification_probes",
|
||||
"evidence_collection_probes",
|
||||
"candidate_contrast_opportunities",
|
||||
"dasha_agreement_pre_inference",
|
||||
"representative_time_pre_inference",
|
||||
"house_table_pre_inference",
|
||||
"natal_recast_pre_inference",
|
||||
"engine_indistinguishable_width_minutes",
|
||||
] as const;
|
||||
|
||||
function assertModelView(view: Record<string, unknown>) {
|
||||
for (const key of MODEL_HIDDEN_KEYS) assert.equal(key in view, false, key);
|
||||
const report = view.skill_verification_report as { markdown?: string } | undefined;
|
||||
assert.ok(report?.markdown && report.markdown.length > 200, "report Markdown kept once");
|
||||
const delivery = view.range_delivery as Record<string, unknown> | undefined;
|
||||
if (delivery) assert.equal("verification_markdown" in delivery, false);
|
||||
const needle = JSON.stringify(report.markdown.slice(0, 80)).slice(1, -1);
|
||||
assert.equal(JSON.stringify(view).split(needle).length - 1, 1, "Markdown appears exactly once");
|
||||
const inference = view.inference_state as { candidates?: Array<Record<string, unknown>> } | null;
|
||||
for (const candidate of inference?.candidates ?? []) {
|
||||
assert.deepEqual(Object.keys(candidate), ["time", "score", "status", "cluster_range"]);
|
||||
}
|
||||
}
|
||||
|
||||
test("R1: offer's model view drops pre-inference, packet, probes, duplicate Markdown and engine width", async () => {
|
||||
const { latest } = await buildGoldenLatest();
|
||||
const parsed = parseV9CaseDossier(dossierFixture({ evidence: AA_EVIDENCE_ROWS, latestResult: latest, turns: AA_TURNS as never, candidateRange: AA_RANGE as never }))!;
|
||||
const decision = decideFromDossier(parsed as never);
|
||||
const full = latestResultToolProjection(parsed.latestResult as never, decision as never) as Record<string, unknown>;
|
||||
// The full projection (case API, receipts) keeps its audit fields.
|
||||
assert.ok("candidate_contrast_packet" in full);
|
||||
assert.ok("engine_indistinguishable_width_minutes" in full);
|
||||
const view = offerModelProjection({ ...full, candidate_snapshot: { is_current: true } });
|
||||
assertModelView(view);
|
||||
// Conclusion fields the delivery turn needs are still there.
|
||||
for (const key of ["candidates", "representative_time", "credible_range", "session_outcome", "can_adopt", "event_fit_rate", "range_delivery"]) {
|
||||
assert.ok(key in view, key);
|
||||
}
|
||||
console.log(JSON.stringify({ scope: "R1 offer model view", before_bytes: bytes(full), after_bytes: bytes(view) }));
|
||||
assert.ok(bytes(view) < bytes(full) / 3);
|
||||
});
|
||||
|
||||
test("R1: compare's model view (real tool, golden engine response) keeps the Markdown once and no engine width", async () => {
|
||||
const { latest, compute } = await buildGoldenLatest();
|
||||
const restore = stubGoldenFetch();
|
||||
try {
|
||||
const accounting = fakeAccounting({
|
||||
...receiptHandlers,
|
||||
get_agentic_rectification_case_dossier: () => dossierFixture({
|
||||
evidence: AA_EVIDENCE_ROWS,
|
||||
latestResult: latest,
|
||||
turns: AA_TURNS as never,
|
||||
candidateRange: AA_RANGE as never,
|
||||
}),
|
||||
get_agentic_rectification_case_compute: () => compute,
|
||||
set_agentic_rectification_conversation_focus: setFocusHandler,
|
||||
refresh_agentic_rectification_vedastro_validation: () => ({ result_id: latest.result_id, decision_receipt: latest.decision_receipt }),
|
||||
}, { fallback: () => null });
|
||||
const tools = createRectificationV9Tools({
|
||||
userId: USER_ID,
|
||||
caseId: CASE_ID,
|
||||
turnId: TURN_ID,
|
||||
userMessage: "2000 年拿了奖",
|
||||
accounting: accounting.client as never,
|
||||
}) as unknown as Record<string, { execute(input: unknown): Promise<Record<string, unknown>> }>;
|
||||
await tools["rectification-read-case"].execute({ caseId: CASE_ID });
|
||||
const compare = await tools["rectification-compare-candidates"].execute({ caseId: CASE_ID });
|
||||
assertModelView(compare);
|
||||
console.log(JSON.stringify({ scope: "R1 compare model view", after_bytes: bytes(compare) }));
|
||||
} finally {
|
||||
restore();
|
||||
}
|
||||
});
|
||||
|
||||
test("T1/R3: batch range_after_rescore names the post-rescore range and whether this turn delivers", async () => {
|
||||
const { latest } = await buildGoldenLatest();
|
||||
const parsed = parseV9CaseDossier(dossierFixture({ evidence: AA_EVIDENCE_ROWS, latestResult: latest, candidateRange: AA_RANGE as never }))!;
|
||||
const decision = decideFromDossier(parsed as never);
|
||||
const withQuestion = batchRangeAfterRescore(decision as never, latest.decision_receipt, { question_id: "q" });
|
||||
assert.deepEqual(withQuestion?.credible_range, decision.credibleRange);
|
||||
assert.equal(withQuestion?.representative_time, decision.representativeTime ?? null);
|
||||
assert.equal(withQuestion?.delivers_range_this_turn, false);
|
||||
const deliverable = { ...decision, canAdopt: true, sessionOutcome: "adopt_representative" };
|
||||
assert.equal(batchRangeAfterRescore(deliverable as never, latest.decision_receipt, null)?.delivers_range_this_turn, true);
|
||||
assert.equal(batchRangeAfterRescore(null, latest.decision_receipt, null), null);
|
||||
const tools = readFileSync(new URL("../src/mastra/rectification-v9-tools.ts", import.meta.url), "utf8");
|
||||
// The receipt keeps hashing the pre-existing shape; only the returned value grows.
|
||||
const batchReceipt = tools.indexOf("executedMethods: [...rescore.executedMethods],");
|
||||
const batchReturn = tools.indexOf("range_after_rescore: rescore.rangeAfter");
|
||||
assert.ok(batchReceipt > 0 && batchReturn > batchReceipt);
|
||||
assert.match(tools.slice(batchReceipt - 120, batchReceipt), /resultFingerprint: hashResult\(projection\)/);
|
||||
assert.match(tools, /return offerModelProjection\(payload\);/);
|
||||
});
|
||||
Reference in New Issue
Block a user