Product decision 2026-10-02 (TASK-upstream-sync5 R1): a time range is offered
only with at least 4 dated, primary-scoreable events covering 3 domains,
counted on all of them (training + reserved holdout). Was 3 training events /
2 domains in three TS copies and the Python acceptance gate while the policy
file already said 4/3.
- One definition: references/rectification_policy.v1.json
(minConfirmationEvents / minConfirmationDomains). TS core/types MIN_DATED_*,
rectification-decision MIN_STANDALONE_*, evidence-model MIN_ACCEPTANCE_*,
the convergence evaluator and the post-inference trainingGateOpen all read
it; Python decision_policy MIN_ACCEPTANCE_* alias MIN_CONFIRMATION_*.
- Python receipt counts all scoreable events / domains for event_quality and
domain_diversity; decision policy identity v3 -> v4 (candidate UUIDs carry
it). Candidate scores unchanged (77 v5 cases A/B identical), so the
algorithm stays rectification-v5-matrix-scoring-10.
- Memoization golden v3 written by write_golden; v2 frozen by sha256 with a
test that its scores equal v3 and only the receipt policy moved.
- Collect gap copy names the exact gap ("再来两件……其中至少一件不是……")
instead of always "再来一件"; VOICE.md updated. Legacy life-events form copy
4/3 as well.
- 30 frontend test files, 4 Python tests: fixtures extended to the same
scenario at 4/3, or assertions changed with 原值/新值/原因 notes.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N4f2nya58RoRu4yEmJgRGE
221 lines
7.4 KiB
TypeScript
221 lines
7.4 KiB
TypeScript
import assert from "node:assert/strict";
|
||
import test from "node:test";
|
||
|
||
import {
|
||
decideRectification,
|
||
publicCanAdopt,
|
||
rangeMatchesOpeningWindow,
|
||
sessionOutcomeAllowsDelivery,
|
||
} from "../src/lib/rectification-agentic/core/rectification-decision.ts";
|
||
import {
|
||
RECTIFICATION_DELIVERED_COPY,
|
||
RECTIFICATION_QUESTION_RETRY_LIMIT,
|
||
RECTIFICATION_QUESTION_UNAVAILABLE_COPY,
|
||
interviewDeliveredGap,
|
||
rectificationQuestionGapState,
|
||
} from "../src/lib/rectification-surface-state.ts";
|
||
import { OPEN_ENGINE_CAPABILITY_CEILING } from "./rectification-v9-test-support.ts";
|
||
import type { CandidateDiscriminatorProbe } from "../src/lib/rectification-agentic/core/candidate-contrast-packet.ts";
|
||
|
||
const DATED_PROBE: CandidateDiscriminatorProbe = {
|
||
probeId: "probe-2023-career",
|
||
candidateSetVersion: "set-1",
|
||
question: "2023 年 5 月前后,你有没有入职、换工作,或者当时手上的职责明显变重?",
|
||
expectedOutcomes: [
|
||
{
|
||
outcomeId: "yes",
|
||
supportsCandidateIds: ["05:06"],
|
||
conflictsCandidateIds: ["05:08", "05:14"],
|
||
},
|
||
{
|
||
outcomeId: "no",
|
||
supportsCandidateIds: ["05:08", "05:14"],
|
||
conflictsCandidateIds: ["05:06"],
|
||
},
|
||
],
|
||
candidateSplitHash: "05:06|05:08|05:14",
|
||
informationGain: 0.4,
|
||
sourceFeatures: [{ technique: "test", calculationResultId: null }],
|
||
domain: "career",
|
||
year: 2023,
|
||
semanticKey: "career.2023.entry",
|
||
};
|
||
|
||
const TIED_THREE = [
|
||
{ time: "05:06", score: 13 },
|
||
{ time: "05:08", score: 13 },
|
||
{ time: "05:14", score: 13 },
|
||
{ time: "05:03", score: 12 },
|
||
{ time: "04:59", score: 11 },
|
||
{ time: "04:46", score: 10 },
|
||
{ time: "04:53", score: 10 },
|
||
{ time: "05:15", score: 10 },
|
||
{ time: "04:50", score: 8 },
|
||
] as const;
|
||
|
||
const OPENING = ["04:45", "05:15"] as const;
|
||
|
||
function tiedInput(overrides: Partial<Parameters<typeof decideRectification>[0]> = {}) {
|
||
return {
|
||
engineCeiling: OPEN_ENGINE_CAPABILITY_CEILING,
|
||
methodCoverageAll: false,
|
||
trainingGateOpen: true,
|
||
candidateScores: TIED_THREE.map((item) => ({ time: item.time, score: item.score })),
|
||
holdoutValidation: "unavailable" as const,
|
||
// R1(BUG-1193):补第 4 件 / 第 3 域,保持原场景。
|
||
datedEventCount: 4,
|
||
datedDomainCount: 3,
|
||
inferenceCredibleRange: OPENING,
|
||
openingCandidateRange: OPENING,
|
||
...overrides,
|
||
};
|
||
}
|
||
|
||
test("tied first place with a dated probe keeps collecting instead of delivering", () => {
|
||
const decision = decideRectification(tiedInput({
|
||
discriminatorProbe: DATED_PROBE,
|
||
targetedCollectExhausted: true,
|
||
refreshExhausted: false,
|
||
}));
|
||
assert.notEqual(decision.nextAction, "complete_with_range");
|
||
assert.equal(sessionOutcomeAllowsDelivery(decision.sessionOutcome), false);
|
||
assert.equal(publicCanAdopt(decision), false);
|
||
assert.equal(decision.canAdopt, false);
|
||
assert.equal(decision.nextAction, "ask_candidate_discriminator");
|
||
});
|
||
|
||
test("tied first place with no probe still collects while targeted questions remain", () => {
|
||
const decision = decideRectification(tiedInput({
|
||
discriminatorProbe: null,
|
||
targetedCollectExhausted: false,
|
||
refreshExhausted: true,
|
||
}));
|
||
assert.notEqual(decision.nextAction, "complete_with_range");
|
||
assert.equal(sessionOutcomeAllowsDelivery(decision.sessionOutcome), false);
|
||
assert.equal(publicCanAdopt(decision), false);
|
||
assert.equal(decision.nextAction, "ask_fact_collection");
|
||
});
|
||
|
||
test("tied first place with no probe and targeted collect exhausted delivers a range", () => {
|
||
const decision = decideRectification(tiedInput({
|
||
discriminatorProbe: null,
|
||
targetedCollectExhausted: true,
|
||
refreshExhausted: false,
|
||
}));
|
||
assert.equal(decision.stopReason, "tied_first");
|
||
assert.equal(decision.nextAction, "complete_with_range");
|
||
assert.equal(decision.canOfferRange, true);
|
||
assert.equal(sessionOutcomeAllowsDelivery(decision.sessionOutcome), true);
|
||
});
|
||
|
||
test("a live first-place tie with remaining probes does not announce adopt_representative", () => {
|
||
const decision = decideRectification(tiedInput({
|
||
discriminatorProbe: DATED_PROBE,
|
||
// R1(BUG-1193):补第 4 件 / 第 3 域,保持原场景。
|
||
datedEventCount: 4,
|
||
targetedCollectExhausted: false,
|
||
refreshExhausted: false,
|
||
}));
|
||
assert.notEqual(decision.sessionOutcome, "adopt_representative");
|
||
assert.equal(publicCanAdopt(decision), false);
|
||
assert.equal(rangeMatchesOpeningWindow(decision.credibleRange, OPENING), true);
|
||
});
|
||
|
||
test("an unnarrowed delivered range withholds adopt", () => {
|
||
const unnarrowed = decideRectification(tiedInput({
|
||
discriminatorProbe: null,
|
||
targetedCollectExhausted: true,
|
||
inferenceCredibleRange: OPENING,
|
||
openingCandidateRange: OPENING,
|
||
}));
|
||
assert.equal(sessionOutcomeAllowsDelivery(unnarrowed.sessionOutcome), true);
|
||
assert.equal(unnarrowed.sessionOutcome === "provisional_range"
|
||
|| unnarrowed.sessionOutcome === "completed_with_range", true);
|
||
assert.equal(unnarrowed.canAdopt, false);
|
||
assert.equal(publicCanAdopt(unnarrowed), false);
|
||
assert.equal(unnarrowed.canOfferRange, true);
|
||
|
||
const narrowed = decideRectification(tiedInput({
|
||
discriminatorProbe: null,
|
||
targetedCollectExhausted: true,
|
||
inferenceCredibleRange: ["04:48", "05:07"],
|
||
openingCandidateRange: OPENING,
|
||
}));
|
||
assert.equal(sessionOutcomeAllowsDelivery(narrowed.sessionOutcome), true);
|
||
assert.equal(narrowed.canAdopt, true);
|
||
assert.equal(narrowed.sessionOutcome, "adopt_representative");
|
||
});
|
||
|
||
test("a delivered dead choice is the delivered gap, not unavailable repair", () => {
|
||
const warnings: string[] = [];
|
||
const original = console.warn;
|
||
console.warn = (value: unknown) => {
|
||
warnings.push(String(value));
|
||
};
|
||
try {
|
||
assert.equal(
|
||
interviewDeliveredGap({
|
||
questionMissing: false,
|
||
deadChoice: true,
|
||
sessionOutcome: "completed_with_range",
|
||
stopReason: "tied_first",
|
||
questionId: "distinguish:career.2023.entry",
|
||
hasChoiceCard: false,
|
||
}),
|
||
true,
|
||
);
|
||
assert.equal(
|
||
rectificationQuestionGapState({
|
||
liveQuestionVisible: false,
|
||
questionMissing: false,
|
||
questionLoadFailed: true,
|
||
deadChoice: true,
|
||
questionId: "distinguish:career.2023.entry",
|
||
hasChoiceCard: false,
|
||
busy: false,
|
||
readonly: false,
|
||
snapshotLoaded: true,
|
||
resumableCase: true,
|
||
retryAttempts: RECTIFICATION_QUESTION_RETRY_LIMIT,
|
||
sessionOutcome: "completed_with_range",
|
||
stopReason: "tied_first",
|
||
}),
|
||
"delivered",
|
||
);
|
||
assert.ok(warnings.some((line) => line.includes("rectification_delivered_state_dead_choice")));
|
||
} finally {
|
||
console.warn = original;
|
||
}
|
||
assert.equal(RECTIFICATION_DELIVERED_COPY.includes("没有拿到下一个问题"), false);
|
||
});
|
||
|
||
test("a non-delivery dead choice stays on the unavailable repair path", () => {
|
||
assert.equal(
|
||
interviewDeliveredGap({
|
||
questionMissing: false,
|
||
deadChoice: true,
|
||
sessionOutcome: "collect_evidence",
|
||
hasChoiceCard: false,
|
||
}),
|
||
false,
|
||
);
|
||
assert.equal(
|
||
rectificationQuestionGapState({
|
||
liveQuestionVisible: false,
|
||
questionMissing: false,
|
||
questionLoadFailed: true,
|
||
deadChoice: true,
|
||
questionPersisted: false,
|
||
collectWaiting: false,
|
||
busy: false,
|
||
readonly: false,
|
||
snapshotLoaded: true,
|
||
resumableCase: true,
|
||
retryAttempts: RECTIFICATION_QUESTION_RETRY_LIMIT,
|
||
sessionOutcome: "collect_evidence",
|
||
}),
|
||
"unavailable",
|
||
);
|
||
assert.equal(RECTIFICATION_QUESTION_UNAVAILABLE_COPY, "没有拿到下一个问题。");
|
||
});
|