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", algorithm_version: "rectification-event-contract-v2", 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", }]; const before = trainingScoreableGate(education); assert.equal(before.trainingCount, 2); 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, })), ]); assert.equal(after.trainingCount, before.trainingCount + 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> : []; 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>; 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> : []; 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; }).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>; assert.equal(items.length, 1); assert.equal(items[0]?.event_kind, "occupation_note"); assert.equal(items[0]?.occurred_from, null); });