出卡时机只看题源有没有空:撤回「门槛达标就短路采集线」的写法,同时 按 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
200 lines
6.8 KiB
TypeScript
200 lines
6.8 KiB
TypeScript
import assert from "node:assert/strict";
|
|
import test from "node:test";
|
|
|
|
import { RECTIFICATION_POLICY } from "../src/lib/rectification-policy.ts";
|
|
import {
|
|
decideRectification,
|
|
mayDeliverOnPrecision,
|
|
publicCanAdopt,
|
|
publicNextAction,
|
|
sessionOutcomeAllowsDelivery,
|
|
} from "../src/lib/rectification-agentic/core/rectification-decision.ts";
|
|
import { computePrecisionGateMet } from "../src/lib/rectification-agentic/core/precision-gate.ts";
|
|
import { RANGE_DELIVERY_TIE_PERCENT } from "../src/lib/rectification-agentic/user-copy.ts";
|
|
import { OPEN_ENGINE_CAPABILITY_CEILING } from "./rectification-v9-test-support.ts";
|
|
|
|
const TIED_FIVE = [
|
|
{ time: "04:51", score: 13 },
|
|
{ time: "04:53", score: 13 },
|
|
{ time: "04:56", score: 13 },
|
|
{ time: "04:58", score: 13 },
|
|
{ time: "05:11", score: 13 },
|
|
] as const;
|
|
|
|
function input(overrides: Partial<Parameters<typeof decideRectification>[0]> = {}) {
|
|
return {
|
|
engineCeiling: OPEN_ENGINE_CAPABILITY_CEILING,
|
|
methodCoverageAll: true,
|
|
trainingGateOpen: true,
|
|
candidateScores: TIED_FIVE.map((item) => ({ time: item.time, score: item.score })),
|
|
holdoutValidation: "unavailable" as const,
|
|
datedEventCount: 5,
|
|
datedDomainCount: 3,
|
|
discriminatorProbe: null,
|
|
targetedCollectExhausted: true,
|
|
refreshExhausted: true,
|
|
inferenceCredibleRange: ["04:51", "05:11"] as const,
|
|
openingCandidateRange: ["04:45", "05:15"] as const,
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
test("policy defines deliveryMaxWidthMinutes once", () => {
|
|
assert.equal(RECTIFICATION_POLICY.deliveryMaxWidthMinutes, 10);
|
|
assert.equal(RANGE_DELIVERY_TIE_PERCENT, 3);
|
|
});
|
|
|
|
test("precision gate requires width, display gap, and no exact tie", () => {
|
|
assert.equal(computePrecisionGateMet({
|
|
range: ["04:51", "05:11"],
|
|
topTwoPercents: [26, 26],
|
|
tiedForFirst: true,
|
|
}), false);
|
|
assert.equal(computePrecisionGateMet({
|
|
range: ["04:51", "04:59"],
|
|
topTwoPercents: [40, 35],
|
|
tiedForFirst: false,
|
|
}), true);
|
|
assert.equal(computePrecisionGateMet({
|
|
range: ["04:51", "04:59"],
|
|
topTwoPercents: [26, 26],
|
|
tiedForFirst: true,
|
|
}), false);
|
|
});
|
|
|
|
test("20 minute tied range keeps collecting with a guided question", () => {
|
|
const decision = decideRectification(input({
|
|
precisionGateMet: false,
|
|
guidedCollectExhausted: false,
|
|
}));
|
|
assert.equal(decision.nextAction, "ask_fact_collection");
|
|
assert.equal(decision.canOfferRange, false);
|
|
assert.equal(sessionOutcomeAllowsDelivery(decision.sessionOutcome), false);
|
|
assert.equal(publicCanAdopt(decision), false);
|
|
});
|
|
|
|
test("8 minute range with a 5 point lead delivers once every line is asked", () => {
|
|
const decision = decideRectification(input({
|
|
candidateScores: [
|
|
{ time: "04:51", score: 20 },
|
|
{ time: "04:53", score: 15 },
|
|
{ time: "04:56", score: 10 },
|
|
],
|
|
inferenceCredibleRange: ["04:51", "04:59"],
|
|
precisionGateMet: true,
|
|
// 原值: guidedCollectExhausted: false
|
|
// 新值: guidedCollectExhausted: true
|
|
// 原因: D6——门槛只是必要条件,引导线没问完不出卡(BUG-751)
|
|
guidedCollectExhausted: true,
|
|
targetedCollectExhausted: true,
|
|
refreshExhausted: true,
|
|
}));
|
|
assert.equal(decision.canOfferRange, true);
|
|
assert.equal(sessionOutcomeAllowsDelivery(decision.sessionOutcome), true);
|
|
assert.equal(decision.precisionGateMet, true);
|
|
});
|
|
|
|
test("gate met but the targeted line is still open keeps asking", () => {
|
|
const decision = decideRectification(input({
|
|
candidateScores: [
|
|
{ time: "04:51", score: 20 },
|
|
{ time: "04:53", score: 15 },
|
|
{ time: "04:56", score: 10 },
|
|
],
|
|
inferenceCredibleRange: ["04:51", "04:59"],
|
|
precisionGateMet: true,
|
|
guidedCollectExhausted: false,
|
|
targetedCollectExhausted: false,
|
|
refreshExhausted: true,
|
|
}));
|
|
assert.equal(decision.nextAction, "ask_fact_collection");
|
|
assert.equal(decision.canOfferRange, false);
|
|
assert.equal(sessionOutcomeAllowsDelivery(decision.sessionOutcome), false);
|
|
});
|
|
|
|
test("empty guided pool with refresh untried does not deliver", () => {
|
|
const decision = decideRectification(input({
|
|
candidateScores: [
|
|
{ time: "04:51", score: 20 },
|
|
{ time: "04:53", score: 15 },
|
|
{ time: "04:56", score: 10 },
|
|
],
|
|
inferenceCredibleRange: ["04:51", "04:59"],
|
|
precisionGateMet: true,
|
|
guidedCollectExhausted: true,
|
|
targetedCollectExhausted: true,
|
|
refreshExhausted: false,
|
|
}));
|
|
assert.equal(decision.nextAction, "ask_fact_collection");
|
|
assert.equal(decision.canOfferRange, false);
|
|
assert.equal(
|
|
mayDeliverOnPrecision({
|
|
methodCoverageAll: true,
|
|
candidateScores: [],
|
|
engineCeiling: OPEN_ENGINE_CAPABILITY_CEILING,
|
|
precisionGateMet: true,
|
|
guidedCollectExhausted: true,
|
|
refreshExhausted: false,
|
|
targetedCollectExhausted: true,
|
|
}),
|
|
false,
|
|
);
|
|
});
|
|
|
|
test("8 minute exact tie keeps collecting", () => {
|
|
const decision = decideRectification(input({
|
|
candidateScores: [
|
|
{ time: "04:51", score: 13 },
|
|
{ time: "04:53", score: 13 },
|
|
],
|
|
inferenceCredibleRange: ["04:51", "04:59"],
|
|
precisionGateMet: false,
|
|
guidedCollectExhausted: false,
|
|
}));
|
|
assert.equal(decision.nextAction, "ask_fact_collection");
|
|
assert.equal(decision.canOfferRange, false);
|
|
});
|
|
|
|
test("every line asked but the gate unmet still delivers under the existing rules", () => {
|
|
// D2:题源都空了就是「用户确实补不出来」,卡片、采用按钮、三句正文不变,
|
|
// 也不加「未达门槛」标注。D6 只保证在还有题可问时不会提前出卡。
|
|
const decision = decideRectification(input({
|
|
precisionGateMet: false,
|
|
guidedCollectExhausted: true,
|
|
}));
|
|
assert.equal(decision.canOfferRange, true);
|
|
assert.equal(sessionOutcomeAllowsDelivery(decision.sessionOutcome), true);
|
|
// 门槛值照常上报,不参与出卡判断。
|
|
assert.equal(decision.precisionGateMet, false);
|
|
});
|
|
|
|
test("the precision gate is reported on a collect exit too", () => {
|
|
const collecting = decideRectification(input({
|
|
precisionGateMet: false,
|
|
guidedCollectExhausted: false,
|
|
}));
|
|
assert.equal(collecting.nextAction, "ask_fact_collection");
|
|
assert.equal(collecting.precisionGateMet, false);
|
|
assert.equal(publicNextAction(collecting).precision_gate_met, false);
|
|
const helper = decideRectification(input());
|
|
assert.equal(helper.precisionGateMet, null);
|
|
});
|
|
|
|
test("userStopped delivers even when the precision gate is unmet", () => {
|
|
const decision = decideRectification(input({
|
|
userStopped: true,
|
|
precisionGateMet: false,
|
|
guidedCollectExhausted: false,
|
|
}));
|
|
assert.equal(decision.canOfferRange, true);
|
|
assert.equal(sessionOutcomeAllowsDelivery(decision.sessionOutcome), true);
|
|
});
|
|
|
|
test("omitted precision flags keep the helper path delivering", () => {
|
|
assert.equal(mayDeliverOnPrecision({
|
|
methodCoverageAll: true,
|
|
candidateScores: [],
|
|
engineCeiling: OPEN_ENGINE_CAPABILITY_CEILING,
|
|
}), true);
|
|
});
|