About 30 fixed rectification lines lose the receipt tone: no 「已记录。」 openings (收到 when the range holds, 好 when it moves), 麻烦 instead of 请, 时间 instead of 候选 in what the user reads, and no 「新建对话」 after adoption. Numbers, clocks, ranges and the honesty boundaries are unchanged. CHOICE_ACK_RE also recognises the new acknowledgements, so a model sentence that repeats one is still dropped; the delivery guard matches the delivery sentence by containment and does not depend on the opening. Full suite 4973 / fail 24, identical to 61d6f248; build keeps / Static. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017eEAG8HD3mm8gsKXgk8uU8
104 lines
8.0 KiB
TypeScript
104 lines
8.0 KiB
TypeScript
import assert from "node:assert/strict";
|
|
import test from "node:test";
|
|
|
|
import { candidateForAdoptedMinute, postAdoptCheckVerdict, readoptOffer } from "../src/lib/rectification-agentic/v9/post-adopt-verdict.ts";
|
|
import { attachOfferResultToTurns } from "../src/lib/rectification-agentic/v9/turn-question.ts";
|
|
import type { SegmentSummary } from "../src/lib/rectification-agentic/core/segment-summary.ts";
|
|
|
|
// BUG-1172 (product 10-01 option A): after a post-adopt check the reply says whether the event
|
|
// fits the adopted time, and whether the chart verdicts moved — not only 「前事核对到这里」.
|
|
const candidate = (time: string, range: [string, string], clusterTimes?: string[]) => ({
|
|
id: time, time, cluster_range: range, ...(clusterTimes ? { cluster_times: clusterTimes } : {}),
|
|
prior_score: 10, posterior_score: 10, probability: 0.5, status: "active", rank: 1, strong_conflict_count: 0,
|
|
}) as never;
|
|
const CANDIDATES = [candidate("04:53", ["04:48", "04:57"], ["04:53", "04:55"]), candidate("05:03", ["05:00", "05:07"])];
|
|
const probe = {
|
|
id: "probe:education.2016.09.known_event_quality:x", semantic_key: "education.2016.09.known_event_quality",
|
|
candidate_split_hash: "x", domain: "education", year: 2016, question: "2016", candidate_ids: ["04:53", "05:03"],
|
|
information_gain: 0.4, source: "known_event_quality",
|
|
expected_outcomes: [
|
|
{ answer_class: "yes", supports: ["04:53"], conflicts: ["05:03"] },
|
|
{ answer_class: "no", supports: ["05:03"], conflicts: ["04:53"] },
|
|
{ answer_class: "weak_yes", supports: [], conflicts: [] },
|
|
],
|
|
} as never;
|
|
const summary = (tier: string, sign: number, adoptionTime: string | null = null): SegmentSummary => ({
|
|
targets: ["D1", "D9"], window_minutes: 21, scan_complete: true, no_rectification_needed: false,
|
|
adoption_minute: adoptionTime ? { offset: 0, time: adoptionTime, date: "1997-08-08", signs: {} } : null,
|
|
charts: [
|
|
{ chart: "D1", sign: 3, tier: "certain", alternatives: [], tied: false },
|
|
{ chart: "D9", sign, tier, alternatives: [], tied: false },
|
|
],
|
|
}) as never;
|
|
|
|
test("the adopted minute maps to the candidate whose cluster holds it, else the nearest", () => {
|
|
assert.equal(candidateForAdoptedMinute("04:55:00", CANDIDATES)?.time, "04:53");
|
|
assert.equal(candidateForAdoptedMinute("05:01", CANDIDATES)?.time, "05:03");
|
|
assert.equal(candidateForAdoptedMinute("05:30", CANDIDATES)?.time, "05:03", "outside every cluster: nearest");
|
|
});
|
|
|
|
test("verdict: fits, does not fit, cannot tell, and unsure", () => {
|
|
const base = { acceptedTime: "04:55:00", probe, candidates: CANDIDATES };
|
|
assert.equal(postAdoptCheckVerdict({ ...base, answerClass: "yes" }), "这件事和 04:55 对得上。");
|
|
// Minute product (no chart summary): the card is where a re-pick happens.
|
|
assert.equal(postAdoptCheckVerdict({ ...base, answerClass: "no" }),
|
|
// 原值: 这件事和 04:55 对不上;如果别的经历也对不上,可以回到上面的卡片改选。 / 新值: 这件事和 04:55 对不上。要是别的经历也对不上,可以回到上面的卡片换一个时间。 / 原因: BUG-1258:校正固定文案改为知心姐姐口吻(产品 2026-10-07 认可样稿)
|
|
"这件事和 04:55 对不上。要是别的经历也对不上,可以回到上面的卡片换一个时间。");
|
|
assert.equal(postAdoptCheckVerdict({ ...base, answerClass: "weak_yes" }), "这件事分不出 04:55 和别的时间。");
|
|
// 原值: 这件事记不清,不算。 / 新值: 这件记不清也没关系,不算进去。 / 原因: BUG-1258:校正固定文案改为知心姐姐口吻(产品 2026-10-07 认可样稿)
|
|
assert.equal(postAdoptCheckVerdict({ ...base, answerClass: "unsure" }), "这件记不清也没关系,不算进去。");
|
|
assert.equal(postAdoptCheckVerdict({ ...base, answerClass: null }), null);
|
|
assert.equal(postAdoptCheckVerdict({ ...base, acceptedTime: null, answerClass: "yes" }), null);
|
|
assert.equal(postAdoptCheckVerdict({ ...base, probe: null, answerClass: "yes" }), null, "no probe, no claim");
|
|
});
|
|
|
|
test("segment product says whether the chart verdicts moved", () => {
|
|
const base = { acceptedTime: "04:55", probe, candidates: CANDIDATES, answerClass: "yes" as const };
|
|
assert.equal(postAdoptCheckVerdict({ ...base, segmentBefore: summary("credible", 7), segmentAfter: summary("credible", 7) }),
|
|
"这件事和 04:55 对得上。盘型结论不变。");
|
|
// BUG-1173 — 原值: 「…盘型结论有变化:D1 上升:巨蟹(确定);D9 上升:天秤(倾向,备选也要看)。」 / 新值: 同一句
|
|
// 再接「你采用的 04:55 仍在其中,不用改。」 / 原因: 产品 10-01 选 A,变化时要说清采用的时间还在不在新结论里。
|
|
const minutes = [
|
|
{ offset: 7, time: "04:55", signs: { D1: 3, D9: 6 } },
|
|
{ offset: 14, time: "05:02", signs: { D1: 3, D9: 7 } },
|
|
];
|
|
const moved = postAdoptCheckVerdict({ ...base, segmentBefore: summary("credible", 7), segmentAfter: summary("tentative", 6), segmentMinutes: minutes });
|
|
assert.equal(moved, "这件事和 04:55 对得上。盘型结论有变化:D1 上升:巨蟹(确定);D9 上升:天秤(倾向,备选也要看)。你采用的 04:55 仍在其中,不用改。");
|
|
const left = postAdoptCheckVerdict({ ...base, answerClass: "no", segmentBefore: summary("credible", 6), segmentAfter: summary("credible", 7, "05:02"), segmentMinutes: minutes });
|
|
assert.equal(left, "这件事和 04:55 对不上。盘型结论有变化:D1 上升:巨蟹(确定);D9 上升:天蝎(较可信)。你采用的 04:55 的 D9 上升是天秤,和新的结论不一致;新的推荐时间是 05:02,可以点下面的「改用 05:02」。");
|
|
const noMinute = postAdoptCheckVerdict({ ...base, answerClass: "no", segmentBefore: summary("credible", 6), segmentAfter: summary("credible", 7), segmentMinutes: minutes });
|
|
assert.equal(noMinute, "这件事和 04:55 对不上。盘型结论有变化:D1 上升:巨蟹(确定);D9 上升:天蝎(较可信)。你采用的 04:55 的 D9 上升是天秤,和新的结论不一致,但现在还推荐不出单一时间,先按 04:55 排盘。");
|
|
for (const text of [moved!, left!, noMinute!]) assert.doesNotMatch(text, /确认|精确|相对支持|领先|落后/);
|
|
});
|
|
|
|
test("a clash that does not move the verdict says so instead of inviting a re-pick", () => {
|
|
const base = { acceptedTime: "04:55", probe, candidates: CANDIDATES, answerClass: "no" as const };
|
|
assert.equal(postAdoptCheckVerdict({ ...base, segmentBefore: summary("credible", 7), segmentAfter: summary("credible", 7) }),
|
|
"这件事和 04:55 对不上。一件事还不足以改变盘型结论,先按 04:55 排盘。");
|
|
});
|
|
|
|
test("re-offer only when the adopted minute left the verdict and a new minute exists", () => {
|
|
const minutes = [
|
|
{ offset: 7, time: "04:55", signs: { D1: 3, D9: 6 } },
|
|
{ offset: 14, time: "05:02", signs: { D1: 3, D9: 7 } },
|
|
];
|
|
assert.equal(readoptOffer("04:55:00", summary("credible", 7, "05:02"), minutes), "05:02");
|
|
assert.equal(readoptOffer("04:55", summary("credible", 6, "05:02"), minutes), null, "still fits: no re-offer");
|
|
assert.equal(readoptOffer("04:55", summary("credible", 7, null), minutes), null, "no server minute: no re-offer");
|
|
assert.equal(readoptOffer("04:55", summary("indistinct", 7, "05:02"), minutes), null, "an indistinct chart does not count");
|
|
assert.equal(readoptOffer(null, summary("credible", 7, "05:02"), minutes), null);
|
|
});
|
|
|
|
test("the card hangs under the latest reply only when a re-adopt is offered", () => {
|
|
const turns = [
|
|
{ id: "delivery", role: "assistant", question: null },
|
|
{ id: "adopt", role: "assistant", question: { kind: "reverse_verify", status: "answered", answer_option: "C" } },
|
|
{ id: "verdict", role: "assistant", question: null },
|
|
] as never[];
|
|
const owner = (readopt: boolean) => attachOfferResultToTurns(turns, { resultId: "r1", canAdopt: true, acceptedTime: "04:55", readopt })
|
|
.find((turn) => (turn as { offer_result_id: string | null }).offer_result_id) as { id: string } | undefined;
|
|
assert.equal(owner(false)?.id, "delivery");
|
|
assert.equal(owner(true)?.id, "verdict");
|
|
});
|
|
|