Files
Jyotisha/frontend/tests/rectification-targeted-collect-cards-20260913.test.ts
T
Jesse_ChenandClaude Fable 5.1 dc732825d8 fix(rectification): 有题就接着问,题问完才出卡;引导窗口不再硬贴领域
出卡时机只看题源有没有空:撤回「门槛达标就短路采集线」的写法,同时
按 D2 保住「题源全空就按现行规则出卡」——门槛只在还有题可问时挡住
出卡,precision_gate_met 改成只上报(新挂在决策与公开投影上),不再
单独决定时机。引导窗口题在无领域轨道上改问开放题,一个时间窗只问一
次;录入卡提交的是「YYYY 年 M 月,<领域>方面有一件事」,不再是题干
的三选一列表。记忆化 golden 只补一个新键并冻结墙钟。离线回放改成注
入真值方向的边界事件,另跑一组反方向对照。Skill 10.0.28。

BUG-747~752

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JUei7K13cYxLHE3Axe4A45
2026-09-16 12:16:55 +00:00

251 lines
9.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import assert from "node:assert/strict";
import test from "node:test";
import { applyProbeOutcome } from "../src/lib/rectification-agentic/core/apply-probe-outcome.ts";
import type { ConflictProbe } from "../src/lib/rectification-agentic/core/types.ts";
import { RECTIFICATION_USER_COPY } from "../src/lib/rectification-agentic/user-copy.ts";
import {
SPLIT_ENDPOINT_PHRASE,
TARGETED_EXISTENCE_PROMPT,
pendingTargetedYearDomain,
targetedCollectExhausted,
targetedCollectPool,
} from "../src/lib/rectification-agentic/v9/collection-question-pool.ts";
import { rangeDeliveryForSnapshot } from "../src/lib/rectification-agentic/v9/divergence-panel.ts";
import {
buildMethodFollowupPlan,
targetedCollectFollowup,
tieBreakPersonalityAvailable,
tieBreakPersonalityFollowup,
} from "../src/lib/rectification-agentic/v9/method-followup.ts";
const TIMES = ["04:48", "04:53", "05:06", "05:07"] as const;
const LAYERS = ["d9", "d10", "d4", "d12", "d2", "d11"];
const EVIDENCE = [
{
status: "confirmed",
domain: "education",
datePrecision: "month",
occurredFrom: "2016-09-01",
occurredTo: "2016-09-30",
eventKind: "education_start",
summary: "上大学",
},
{
status: "confirmed",
domain: "career",
datePrecision: "month",
occurredFrom: "2020-04-01",
occurredTo: null,
eventKind: "career_entry",
summary: "入职实习",
},
] as const;
function declined(domain: string) {
return {
target_domain: domain,
status: "declined" as const,
intent: "collect_method_evidence",
questionId: `collect:targeted:${domain}`,
target_kind: `targeted:${domain}`,
};
}
test("A resolves existence then asks the year; B/C close only that line", () => {
const first = targetedCollectFollowup(LAYERS, EVIDENCE, [], ["04:48", "05:07"], 4);
assert.equal(first?.collection_key, "collect:targeted:relationship");
assert.equal(first?.choice_frame?.scoring, false);
// 原值: "结过婚或订过婚吗?"
// 新值: TARGETED_EXISTENCE_PROMPT.relationship(问整个领域、任何时间)
// 原因: D5/T4b——窄题干答「没有」不得当成整条线没有(BUG-741 / BUG-751
assert.equal(first?.choice_frame?.prompt, TARGETED_EXISTENCE_PROMPT.relationship);
assert.match(first?.spoken_prompt ?? "", /现在还剩 04:4805:07 里 4 个候选/);
assert.doesNotMatch(first?.spoken_prompt ?? "", SPLIT_ENDPOINT_PHRASE);
const afterYes = targetedCollectFollowup(
LAYERS,
EVIDENCE,
[{ ...declined("relationship"), status: "resolved", questionId: "collect:targeted:relationship" }],
["04:48", "05:07"],
4,
);
assert.equal(pendingTargetedYearDomain([{
...declined("relationship"),
status: "resolved",
}], EVIDENCE), "relationship");
assert.equal(afterYes?.collection_key, "collect:targeted:relationship:year");
assert.equal(afterYes?.choice_frame, null);
assert.equal(afterYes?.spoken_prompt, "大概哪年几月?");
const afterNo = targetedCollectPool(
LAYERS,
EVIDENCE,
[declined("relationship")],
["04:48", "05:07"],
4,
);
assert.equal(afterNo[0]?.domain, "relocation");
// 原值: 拒答 4 条线即耗尽
// 新值: 七条线都要拒答才耗尽
// 原因: D5——remainingTargetedDomains 不再按分盘层剔除,七个 CollectKind
// 按 COLLECT_KIND_ORDER 全部轮到(BUG-751
assert.equal(targetedCollectExhausted(LAYERS, EVIDENCE, [
declined("relationship"),
declined("relocation"),
declined("family"),
declined("finance"),
], ["04:48", "05:07"], 4), false);
assert.equal(targetedCollectExhausted(LAYERS, EVIDENCE, [
declined("education"),
declined("career"),
declined("relocation"),
declined("relationship"),
declined("family"),
declined("finance"),
declined("health_pressure"),
], ["04:48", "05:07"], 4), true);
});
test("personality D9/D10 stay out of the scoring pool until the range-card opt-in", () => {
const ledger = [
...EVIDENCE,
{
status: "confirmed",
domain: "relationship",
datePrecision: "month",
occurredFrom: "2024-05-01",
occurredTo: null,
eventKind: "relationship_start",
summary: "确定关系",
},
{
status: "confirmed",
domain: "family",
datePrecision: "month",
occurredFrom: "2016-05-01",
occurredTo: null,
eventKind: "family_event",
summary: "家里添丁",
},
];
const d9 = {
probeId: "contrast:varga.d9.巨蟹座/狮子座",
candidateSetVersion: "04:48-05:07",
question: "亲密关系里更接近下面哪一种相处方式?",
expectedOutcomes: [
{ outcomeId: "yes", supportsCandidateIds: ["04:53"], conflictsCandidateIds: ["05:06"] },
{ outcomeId: "weak_yes", supportsCandidateIds: ["05:06"], conflictsCandidateIds: ["04:53"] },
],
candidateSplitHash: "varga.d9.巨蟹座/狮子座",
informationGain: 1.4,
sourceFeatures: [{ technique: "D9", calculationResultId: null }],
domain: "relationship",
year: null,
semanticKey: "varga.d9.巨蟹座/狮子座",
choiceKind: "varga_style" as const,
styleOptions: [
{ label: "相处里更在意照顾对方的感受", answerClass: "yes" as const, sign: "巨蟹座" },
{ label: "习惯带头,也不排斥站到台前", answerClass: "weak_yes" as const, sign: "狮子座" },
],
};
const planInput = {
evidence: ledger,
declinedTopics: ["relocation", "finance"].map(declined),
sessionOutcome: "discriminate_candidates" as const,
candidatesSeparated: false,
remainingLayers: LAYERS,
remainingSplitTimes: ["04:48", "05:07"] as const,
remainingCandidateCount: 4,
topCandidateTimes: [...TIMES],
contrastPacket: {
candidateSetVersion: "04:48-05:07",
vargaDifferences: [],
probes: [d9],
},
};
const auto = buildMethodFollowupPlan(planInput);
assert.notEqual(auto.next_followup?.choice_kind, "varga_style");
const opted = tieBreakPersonalityFollowup(planInput);
assert.equal(opted?.choice_kind, "varga_style");
assert.match(opted?.semantic_key ?? "", /varga\.d9/);
assert.equal(opted?.tie_break_round, true);
const d10: ConflictProbe = {
id: "probe:varga.d10.狮子座/处女座",
semantic_key: "varga.d10.狮子座/处女座",
candidate_split_hash: "04:48-05:07:varga.d10",
domain: "career",
year: 0,
question: "平时做事,你更接近下面哪一种?",
candidate_ids: [...TIMES],
expected_outcomes: [
{ answer_class: "yes", supports: ["04:53"], conflicts: ["05:06"] },
{ answer_class: "weak_yes", supports: ["05:06"], conflicts: ["04:53"] },
{ answer_class: "no", supports: [], conflicts: [] },
{ answer_class: "unsure", supports: [], conflicts: [] },
],
information_gain: 0.9,
source: "varga_contrast",
choice_kind: "varga_style",
style_options: [
{ label: "相处里更在意照顾对方的感受", answer_class: "yes", sign: "巨蟹座" },
{ label: "习惯带头,也不排斥站到台前", answer_class: "weak_yes", sign: "狮子座" },
],
};
const scored = applyProbeOutcome(
{ "04:48": 10, "04:53": 12, "05:06": 11, "05:07": 10 },
d10,
"yes",
);
assert.equal(scored.kind, "tie_break");
assert.equal(scored.deltas["04:53"], 1);
assert.equal(scored.eliminated_ids.length, 0);
const delivery = rangeDeliveryForSnapshot({
candidates: TIMES.map((time, index) => ({
candidateId: `00000000-0000-4000-8000-${String(index + 1).padStart(12, "0")}`,
time,
})),
representativeTime: "04:53",
credibleRange: ["04:48", "05:07"],
evidence: ledger,
declinedTopics: ["relocation", "finance"].map(declined),
window_scan: {
scanned: true,
d9_lagna_count: 2,
d10_lagna_count: 2,
d9_candidates_differ: true,
d10_candidates_differ: true,
d4_candidates_differ: true,
d12_candidates_differ: true,
d2_candidates_differ: true,
d11_candidates_differ: true,
},
tieBreakAvailable: tieBreakPersonalityAvailable(planInput),
});
// 原值: narrow_hint 含「不限领域」的自由文本邀请
// 新值: 引导句「现在还剩 HH:MM–HH:MM 里 N 个候选…」,且禁自由文本邀请
// 原因: D3——门槛未达时系统点名逐题问,不再邀请随便打字(BUG-740 / BUG-751
assert.doesNotMatch(delivery.narrow_hint ?? "", /不限领域/);
assert.doesNotMatch(delivery.narrow_hint ?? "", /问完了/);
assert.match(delivery.narrow_hint ?? "", /现在还剩 .+ 里 \d+ 个候选/);
assert.doesNotMatch(delivery.narrow_hint ?? "", SPLIT_ENDPOINT_PHRASE);
assert.equal(delivery.tie_break_available, true);
const incompleteScan = rangeDeliveryForSnapshot({
candidates: TIMES.map((time, index) => ({
candidateId: `00000000-0000-4000-8000-${String(index + 1).padStart(12, "0")}`,
time,
})),
representativeTime: "04:53",
credibleRange: ["04:48", "05:07"],
window_scan: {
d9_candidates_differ: true,
d10_candidates_differ: false,
},
});
// 原值: truewindow_scan.d9_candidates_differ 为真就显示入口)
// 新值: false(没有未问的 D9/D10 varga_style 时不显示)
// 原因: BUG-666 入口与 tie-break 路由共用「还有未答参考题」判定
assert.equal(incompleteScan.tie_break_available, false);
assert.equal(RECTIFICATION_USER_COPY.rangeDeliveryTieBreakEntry, "再答两道参考题微调排序");
});