出卡时机只看题源有没有空:撤回「门槛达标就短路采集线」的写法,同时 按 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
251 lines
9.3 KiB
TypeScript
251 lines
9.3 KiB
TypeScript
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:48–05: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,
|
||
},
|
||
});
|
||
// 原值: true(window_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, "再答两道参考题微调排序");
|
||
});
|