Product decision 2026-10-02 (TASK-upstream-sync5 R1): a time range is offered
only with at least 4 dated, primary-scoreable events covering 3 domains,
counted on all of them (training + reserved holdout). Was 3 training events /
2 domains in three TS copies and the Python acceptance gate while the policy
file already said 4/3.
- One definition: references/rectification_policy.v1.json
(minConfirmationEvents / minConfirmationDomains). TS core/types MIN_DATED_*,
rectification-decision MIN_STANDALONE_*, evidence-model MIN_ACCEPTANCE_*,
the convergence evaluator and the post-inference trainingGateOpen all read
it; Python decision_policy MIN_ACCEPTANCE_* alias MIN_CONFIRMATION_*.
- Python receipt counts all scoreable events / domains for event_quality and
domain_diversity; decision policy identity v3 -> v4 (candidate UUIDs carry
it). Candidate scores unchanged (77 v5 cases A/B identical), so the
algorithm stays rectification-v5-matrix-scoring-10.
- Memoization golden v3 written by write_golden; v2 frozen by sha256 with a
test that its scores equal v3 and only the receipt policy moved.
- Collect gap copy names the exact gap ("再来两件……其中至少一件不是……")
instead of always "再来一件"; VOICE.md updated. Legacy life-events form copy
4/3 as well.
- 30 frontend test files, 4 Python tests: fixtures extended to the same
scenario at 4/3, or assertions changed with 原值/新值/原因 notes.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N4f2nya58RoRu4yEmJgRGE
461 lines
16 KiB
TypeScript
461 lines
16 KiB
TypeScript
import assert from "node:assert/strict";
|
||
import { readFileSync } from "node:fs";
|
||
import test from "node:test";
|
||
|
||
import {
|
||
applyOccupationCollectLedgerNorm,
|
||
isOccupationCollectFocus,
|
||
isPrimaryScoreableEvidence,
|
||
trainingScoreableGate,
|
||
type EvidenceKind,
|
||
} from "../src/lib/rectification-agentic/v9/evidence-model.ts";
|
||
import { createRectificationV9Tools } from "../src/mastra/rectification-v9-tools.ts";
|
||
import {
|
||
CASE_ID,
|
||
CANDIDATE_ID,
|
||
FOCUS_ID,
|
||
SECOND_CANDIDATE_ID,
|
||
TURN_ID,
|
||
USER_ID,
|
||
activeFocusFixture,
|
||
candidateSnapshotFixture,
|
||
computeFixture,
|
||
conversationSummaryFixture,
|
||
dossierFixture,
|
||
fakeAccounting,
|
||
receiptHandlers,
|
||
} from "./rectification-v9-test-support.ts";
|
||
|
||
const OCCUPATION_FOCUS = {
|
||
intent: "collect_method_evidence" as const,
|
||
targetDomain: "occupation",
|
||
targetKind: "occupation_note",
|
||
questionId: "collect:occupation:collect_method_evidence",
|
||
};
|
||
|
||
const NOTE_ID = "44444444-4444-4444-8444-444444444451";
|
||
const CAREER_ID = "44444444-4444-4444-8444-444444444452";
|
||
const EDUCATION_ID = "44444444-4444-4444-8444-444444444441";
|
||
|
||
const ENGINE_SCORE = {
|
||
success: true,
|
||
endpoint: "rectification_v5_score",
|
||
result_id: "e4fbf2e0-85dc-5b42-a5a3-34e5dd4b7e62",
|
||
// 原值 event-contract-v2;新值 rectification-v5;原因:此业务替身匹配 test-support 受控部署,事件合同不变。
|
||
algorithm_version: "rectification-v5",
|
||
event_contract_version: "rectification-event-contract-v2",
|
||
decision_policy_version: "rectification-candidate-policy-v2",
|
||
execution_ledger_version: "rectification-execution-ledger-v2",
|
||
candidate_decisions: [
|
||
{ candidate_id: CANDIDATE_ID, time: "04:50", rank: 1, relative_support: 57, tied_minute_count: 1 },
|
||
{ candidate_id: SECOND_CANDIDATE_ID, time: "04:51", rank: 2, relative_support: 25, tied_minute_count: 2 },
|
||
],
|
||
decision_receipt: {
|
||
receipt_version: "candidate-decision-receipt-v2",
|
||
contract_version: "v2",
|
||
event_contract_version: "rectification-event-contract-v2",
|
||
policy_version: "rectification-candidate-policy-v2",
|
||
decision_policy_version: "rectification-candidate-policy-v2",
|
||
display_allowed: true,
|
||
selection_allowed: true,
|
||
acceptance_allowed: true,
|
||
propose_allowed: true,
|
||
confirmation_allowed: false,
|
||
accept_allowed: true,
|
||
confirm_allowed: false,
|
||
representative_candidate_id: CANDIDATE_ID,
|
||
representative_time: "04:50",
|
||
overall_confidence: "high",
|
||
margin_percent: 42.5,
|
||
gates: {
|
||
event_quality: { scoreable_event_count: 3, minimum: 3 },
|
||
domain_diversity: { domains: ["education", "career"], count: 2, minimum: 2 },
|
||
},
|
||
},
|
||
execution_ledger: [
|
||
{ ledger_version: "rectification-execution-ledger-v2", stage: "technique_layer", method: "d1-rashi", status: "executed", source: "python-engine" },
|
||
],
|
||
diagnostics: {
|
||
window_scan: {
|
||
scanned: true,
|
||
confirmation_allowed: false,
|
||
unique_minute_claim: false,
|
||
d9_lagna_count: 2,
|
||
d10_lagna_count: 1,
|
||
d9_candidates_differ: true,
|
||
d10_candidates_differ: false,
|
||
d9_sign_names: ["白羊座", "天蝎"],
|
||
},
|
||
},
|
||
};
|
||
|
||
function stubEngine(response: unknown) {
|
||
const previous = globalThis.fetch;
|
||
globalThis.fetch = (async () => ({
|
||
ok: true,
|
||
status: 200,
|
||
json: async () => response,
|
||
})) as unknown as typeof fetch;
|
||
return () => {
|
||
globalThis.fetch = previous;
|
||
};
|
||
}
|
||
|
||
const educationEvidence = {
|
||
id: EDUCATION_ID,
|
||
source_turn_id: TURN_ID,
|
||
subject: "self",
|
||
event_kind: "education_start",
|
||
domain: "education",
|
||
occurred_from: "2016-09-01",
|
||
occurred_to: "2016-09-30",
|
||
date_precision: "month",
|
||
summary: "2016年9月上大学",
|
||
status: "confirmed",
|
||
supersedes_evidence_id: null,
|
||
created_at: "2026-09-11T00:00:00.000Z",
|
||
};
|
||
|
||
function datedOccupationInput(kind: EvidenceKind = "career_entry") {
|
||
return {
|
||
domain: "career" as const,
|
||
eventKind: kind,
|
||
datePrecision: "month" as const,
|
||
occurredFrom: "2024-04-01",
|
||
occurredTo: null,
|
||
summary: "2024年4月开始做程序员",
|
||
quote: "2024 年 4 月开始做程序员",
|
||
subject: "self" as const,
|
||
};
|
||
}
|
||
|
||
test("occupation collect focus is identified from question id, not body text", () => {
|
||
assert.equal(isOccupationCollectFocus(OCCUPATION_FOCUS), true);
|
||
assert.equal(isOccupationCollectFocus({
|
||
intent: "collect_method_evidence",
|
||
targetDomain: "career",
|
||
targetKind: "anchor:education_completion:2020",
|
||
questionId: "collect:anchor:education_completion:2020",
|
||
}), false);
|
||
});
|
||
|
||
test("dated occupation collect writes occupation_note plus a scoreable career_entry", () => {
|
||
const remapped = applyOccupationCollectLedgerNorm(OCCUPATION_FOCUS, [datedOccupationInput()]);
|
||
assert.equal(remapped.length, 2);
|
||
assert.equal(remapped[0]?.domain, "occupation");
|
||
assert.equal(remapped[0]?.eventKind, "occupation_note");
|
||
assert.equal(remapped[0]?.datePrecision, "unknown");
|
||
assert.equal(remapped[0]?.occurredFrom, null);
|
||
assert.equal(remapped[0]?.occurredTo, null);
|
||
assert.equal(remapped[1]?.domain, "career");
|
||
assert.equal(remapped[1]?.eventKind, "career_entry");
|
||
assert.equal(remapped[1]?.datePrecision, "month");
|
||
assert.equal(remapped[1]?.occurredFrom, "2024-04-01");
|
||
assert.equal(isPrimaryScoreableEvidence({
|
||
status: "confirmed",
|
||
domain: remapped[0]!.domain,
|
||
datePrecision: remapped[0]!.datePrecision,
|
||
occurredFrom: remapped[0]!.occurredFrom,
|
||
occurredTo: remapped[0]!.occurredTo,
|
||
eventKind: remapped[0]!.eventKind,
|
||
}), false);
|
||
assert.equal(isPrimaryScoreableEvidence({
|
||
status: "confirmed",
|
||
domain: remapped[1]!.domain,
|
||
datePrecision: remapped[1]!.datePrecision,
|
||
occurredFrom: remapped[1]!.occurredFrom,
|
||
occurredTo: remapped[1]!.occurredTo,
|
||
eventKind: remapped[1]!.eventKind,
|
||
}), true);
|
||
});
|
||
|
||
test("undated occupation collect still writes only occupation_note", () => {
|
||
const remapped = applyOccupationCollectLedgerNorm(OCCUPATION_FOCUS, [{
|
||
domain: "career",
|
||
eventKind: "career_entry" as const,
|
||
datePrecision: "unknown" as const,
|
||
occurredFrom: null,
|
||
occurredTo: null,
|
||
summary: "程序员",
|
||
}]);
|
||
assert.equal(remapped.length, 1);
|
||
assert.equal(remapped[0]?.eventKind, "occupation_note");
|
||
assert.equal(remapped[0]?.occurredFrom, null);
|
||
});
|
||
|
||
test("model career kinds other than career_entry are kept on the dated row", () => {
|
||
const remapped = applyOccupationCollectLedgerNorm(
|
||
OCCUPATION_FOCUS,
|
||
[datedOccupationInput("career_change")],
|
||
);
|
||
assert.equal(remapped[1]?.eventKind, "career_change");
|
||
});
|
||
|
||
test("unknown precision with leftover dates still writes only the note", () => {
|
||
const remapped = applyOccupationCollectLedgerNorm(OCCUPATION_FOCUS, [{
|
||
domain: "career",
|
||
eventKind: "career_entry" as const,
|
||
datePrecision: "unknown" as const,
|
||
occurredFrom: "2024-04-01",
|
||
occurredTo: null,
|
||
}]);
|
||
assert.equal(remapped.length, 1);
|
||
assert.equal(remapped[0]?.occurredFrom, null);
|
||
assert.equal(remapped[0]?.datePrecision, "unknown");
|
||
});
|
||
|
||
test("occupation-norm null dates are not restored by a ?? fallback in tools", () => {
|
||
const source = readFileSync(new URL("../src/mastra/rectification-v9-tools.ts", import.meta.url), "utf8");
|
||
assert.doesNotMatch(source, /normalized\?\.occurredFrom \?\?/);
|
||
assert.doesNotMatch(source, /normalized\?\.occurredTo \?\?/);
|
||
assert.doesNotMatch(source, /\?\? item\.occurredFrom/);
|
||
assert.doesNotMatch(source, /\?\? occurredFrom/);
|
||
assert.match(source, /occupationNormalizedLedgerRows/);
|
||
});
|
||
|
||
test("dated occupation collect raises the training gate by one career event", () => {
|
||
const education = [{
|
||
status: "confirmed" as const,
|
||
domain: "education",
|
||
datePrecision: "month" as const,
|
||
occurredFrom: "2016-09-01",
|
||
occurredTo: "2016-09-30",
|
||
eventKind: "education_start",
|
||
}, {
|
||
status: "confirmed" as const,
|
||
domain: "education",
|
||
datePrecision: "month" as const,
|
||
occurredFrom: "2020-06-01",
|
||
occurredTo: "2020-06-30",
|
||
eventKind: "education_completion",
|
||
}, {
|
||
// R1(BUG-1193):补第 3 件 / 第 2 域,让职业那一件仍是开门的最后一件(4 件 3 域)。
|
||
status: "confirmed" as const,
|
||
domain: "relocation",
|
||
datePrecision: "month" as const,
|
||
occurredFrom: "2018-07-01",
|
||
occurredTo: "2018-07-31",
|
||
eventKind: "relocation",
|
||
}];
|
||
const before = trainingScoreableGate(education);
|
||
// 原值: 2 / 新值: 3(补了一件搬家) / 原因: R1 生时校正交付门槛 4 件 3 域(BUG-1193)
|
||
assert.equal(before.trainingCount, 3);
|
||
assert.equal(before.open, false);
|
||
const remapped = applyOccupationCollectLedgerNorm(OCCUPATION_FOCUS, [datedOccupationInput()]);
|
||
const after = trainingScoreableGate([
|
||
...education,
|
||
...remapped.map((item) => ({
|
||
status: "confirmed" as const,
|
||
domain: item.domain,
|
||
datePrecision: item.datePrecision,
|
||
occurredFrom: item.occurredFrom,
|
||
occurredTo: item.occurredTo,
|
||
eventKind: item.eventKind,
|
||
})),
|
||
]);
|
||
// 原值: after.trainingCount === before.trainingCount + 1(3 件全训练)
|
||
// 新值: 训练 + holdout === before + 1,holdoutCount === 1(第 4 件起留 1 件 holdout,训练仍 3 件)
|
||
// 原因: R1 生时校正交付门槛 4 件 3 域(BUG-1193);门槛按训练 + holdout 计,职业那一件仍是开门的一件
|
||
assert.equal(after.trainingCount + after.holdoutCount, before.trainingCount + 1);
|
||
assert.equal(after.holdoutCount, 1);
|
||
assert.equal(after.trainingDomainCount, 2);
|
||
assert.equal(after.open, true);
|
||
});
|
||
|
||
test("record-evidence-batch writes note plus career_entry and rescores", async () => {
|
||
const restore = stubEngine(ENGINE_SCORE);
|
||
let collectFocusResolved = false;
|
||
try {
|
||
const quote = "2024 年 4 月开始做程序员";
|
||
const accounting = fakeAccounting({
|
||
...receiptHandlers,
|
||
get_agentic_rectification_case_dossier: () => dossierFixture({
|
||
evidence: [educationEvidence],
|
||
latestResult: null,
|
||
conversationSummary: conversationSummaryFixture({
|
||
activeFocus: collectFocusResolved
|
||
? null
|
||
: activeFocusFixture({
|
||
intent: "collect_method_evidence",
|
||
targetDomain: "occupation",
|
||
targetKind: "occupation_note",
|
||
questionId: OCCUPATION_FOCUS.questionId,
|
||
expectedAnswerSchema: { collect: true, prompt: "你平时主要做什么工作?" },
|
||
}),
|
||
}),
|
||
}),
|
||
get_agentic_rectification_case_compute: () => computeFixture(),
|
||
record_agentic_rectification_evidence_batch: (_fn, args) => {
|
||
const items = Array.isArray(args.p_items) ? args.p_items as Array<Record<string, unknown>> : [];
|
||
return {
|
||
items: items.map((item, index) => ({
|
||
index,
|
||
outcome: "accepted",
|
||
evidence_id: item.event_kind === "occupation_note" ? NOTE_ID : CAREER_ID,
|
||
status: "confirmed",
|
||
idempotent: false,
|
||
clarification_fields: [],
|
||
error_code: null,
|
||
})),
|
||
accepted_count: items.length,
|
||
needs_clarification_count: 0,
|
||
rejected_count: 0,
|
||
focus_id: FOCUS_ID,
|
||
};
|
||
},
|
||
resolve_agentic_rectification_conversation_focus: () => {
|
||
collectFocusResolved = true;
|
||
return {
|
||
focus_id: FOCUS_ID,
|
||
status: "resolved",
|
||
evidence_id: NOTE_ID,
|
||
idempotent: false,
|
||
};
|
||
},
|
||
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",
|
||
asked_at: "2026-09-11T00:00:00.000Z",
|
||
resolved_at: null,
|
||
asked_turn_id: args.p_asked_turn_id ?? null,
|
||
},
|
||
idempotent: false,
|
||
}),
|
||
persist_agentic_rectification_candidate_v2: () => ({
|
||
...candidateSnapshotFixture({
|
||
representativeTime: "04:50",
|
||
selectionAllowed: true,
|
||
confirmationAllowed: false,
|
||
decisionReceipt: {
|
||
gates: {
|
||
event_quality: { scoreable_event_count: 3, minimum: 3 },
|
||
domain_diversity: { domains: ["education", "career"], count: 2, minimum: 2 },
|
||
},
|
||
},
|
||
}),
|
||
cached: false,
|
||
}),
|
||
});
|
||
const tools = createRectificationV9Tools({
|
||
userId: USER_ID,
|
||
caseId: CASE_ID,
|
||
turnId: TURN_ID,
|
||
userMessage: quote,
|
||
accounting: accounting.client as never,
|
||
});
|
||
const result = await (tools["rectification-record-evidence-batch"] as unknown as {
|
||
execute(input: unknown): Promise<{
|
||
accepted_count: number;
|
||
accepted_recaps: Array<{ display_date_label?: string }>;
|
||
rescore: { status: string; executed_methods: string[] };
|
||
}>;
|
||
}).execute({
|
||
caseId: CASE_ID,
|
||
focusId: FOCUS_ID,
|
||
items: [{
|
||
quote,
|
||
proposedKind: "career_entry",
|
||
subject: "self",
|
||
domain: "career",
|
||
datePrecision: "month",
|
||
occurredFrom: "2024-04",
|
||
summary: "2024年4月开始做程序员",
|
||
}],
|
||
});
|
||
const write = accounting.calls.find((call) => call.fn === "record_agentic_rectification_evidence_batch");
|
||
const items = (write?.args.p_items ?? []) as Array<Record<string, unknown>>;
|
||
assert.equal(items.length, 2);
|
||
assert.equal(items[0]?.event_kind, "occupation_note");
|
||
assert.equal(items[0]?.domain, "occupation");
|
||
assert.equal(items[0]?.date_precision, "unknown");
|
||
assert.equal(items[0]?.occurred_from, null);
|
||
assert.equal(items[1]?.event_kind, "career_entry");
|
||
assert.equal(items[1]?.domain, "career");
|
||
assert.equal(items[1]?.date_precision, "month");
|
||
assert.equal(items[1]?.occurred_from, "2024-04-01");
|
||
assert.equal(result.accepted_count, 2);
|
||
assert.equal(result.accepted_recaps.some((item) => item.display_date_label === "2024-04"), true);
|
||
assert.ok(result.rescore.executed_methods.includes("d1-rashi"));
|
||
} finally {
|
||
restore();
|
||
}
|
||
});
|
||
|
||
test("undated occupation collect batch writes only the note", async () => {
|
||
const quote = "程序员";
|
||
const accounting = fakeAccounting({
|
||
...receiptHandlers,
|
||
get_agentic_rectification_case_dossier: () => dossierFixture({
|
||
evidence: [educationEvidence],
|
||
latestResult: null,
|
||
conversationSummary: conversationSummaryFixture({
|
||
activeFocus: activeFocusFixture({
|
||
intent: "collect_method_evidence",
|
||
targetDomain: "occupation",
|
||
targetKind: "occupation_note",
|
||
questionId: OCCUPATION_FOCUS.questionId,
|
||
expectedAnswerSchema: { collect: true, prompt: "你平时主要做什么工作?" },
|
||
}),
|
||
}),
|
||
}),
|
||
record_agentic_rectification_evidence_batch: (_fn, args) => {
|
||
const items = Array.isArray(args.p_items) ? args.p_items as Array<Record<string, unknown>> : [];
|
||
return {
|
||
items: items.map((item, index) => ({
|
||
index,
|
||
outcome: "accepted",
|
||
evidence_id: NOTE_ID,
|
||
status: "confirmed",
|
||
idempotent: false,
|
||
clarification_fields: [],
|
||
error_code: null,
|
||
})),
|
||
accepted_count: items.length,
|
||
needs_clarification_count: 0,
|
||
rejected_count: 0,
|
||
focus_id: FOCUS_ID,
|
||
};
|
||
},
|
||
resolve_agentic_rectification_conversation_focus: () => ({
|
||
focus_id: FOCUS_ID,
|
||
status: "resolved",
|
||
evidence_id: NOTE_ID,
|
||
idempotent: false,
|
||
}),
|
||
});
|
||
const tools = createRectificationV9Tools({
|
||
userId: USER_ID,
|
||
caseId: CASE_ID,
|
||
turnId: TURN_ID,
|
||
userMessage: quote,
|
||
accounting: accounting.client as never,
|
||
});
|
||
await (tools["rectification-record-evidence-batch"] as unknown as {
|
||
execute(input: unknown): Promise<unknown>;
|
||
}).execute({
|
||
caseId: CASE_ID,
|
||
focusId: FOCUS_ID,
|
||
items: [{
|
||
quote,
|
||
proposedKind: "occupation_note",
|
||
subject: "self",
|
||
domain: "occupation",
|
||
datePrecision: "unknown",
|
||
summary: "程序员",
|
||
}],
|
||
});
|
||
const write = accounting.calls.find((call) => call.fn === "record_agentic_rectification_evidence_batch");
|
||
const items = (write?.args.p_items ?? []) as Array<Record<string, unknown>>;
|
||
assert.equal(items.length, 1);
|
||
assert.equal(items[0]?.event_kind, "occupation_note");
|
||
assert.equal(items[0]?.occurred_from, null);
|
||
});
|