0c0df42679
One "一时说不好" no longer ends the interview. Plateau streaks use the same sample floor. Delivery copy states why the range stopped. Co-authored-by: Cursor <cursoragent@cursor.com>
600 lines
20 KiB
TypeScript
600 lines
20 KiB
TypeScript
import assert from "node:assert/strict";
|
|
import { readFileSync } from "node:fs";
|
|
import test from "node:test";
|
|
|
|
import { OPEN_ENGINE_CAPABILITY_CEILING } from "./rectification-v9-test-support.ts";
|
|
|
|
import { EFFECTIVE_ANSWER_SAFETY_CAP } from "../src/lib/birth-time-dynamic-stop-policy.ts";
|
|
import {
|
|
DEFAULT_MAX_DISCRIMINATION_ROUNDS,
|
|
INFERENCE_ALGORITHM_VERSION,
|
|
type ConflictProbe,
|
|
type InferenceState,
|
|
type RoundTrace,
|
|
} from "../src/lib/rectification-agentic/core/types.ts";
|
|
import {
|
|
MIN_STANDALONE_DATED_DOMAINS,
|
|
MIN_STANDALONE_DATED_EVENTS,
|
|
RECTIFICATION_TERMINATION_COPY,
|
|
decideRectification,
|
|
type EvidenceStopReason,
|
|
} from "../src/lib/rectification-agentic/core/rectification-decision.ts";
|
|
import { RECTIFICATION_POLICY } from "../src/lib/rectification-policy.ts";
|
|
import type { CandidateDiscriminatorProbe } from "../src/lib/rectification-agentic/core/candidate-contrast-packet.ts";
|
|
import { candidateSetId } from "../src/lib/rectification-agentic/core/build-state.ts";
|
|
import {
|
|
decideAfterInferenceChange,
|
|
decideFromDossier,
|
|
type DecisionDossier,
|
|
} from "../src/lib/rectification-agentic/v9/decision-from-dossier.ts";
|
|
import { rectificationLabel } from "../src/lib/rectification-agentic/v9/rectification-label.ts";
|
|
import { evidenceLedgerFingerprint } from "../src/lib/rectification-agentic/v9/tool-service.ts";
|
|
|
|
const PROBE: CandidateDiscriminatorProbe = {
|
|
probeId: "probe-1",
|
|
candidateSetVersion: "set-1",
|
|
question: "这件事更接近哪一种情况?",
|
|
expectedOutcomes: [
|
|
{
|
|
outcomeId: "yes",
|
|
supportsCandidateIds: ["05:00"],
|
|
conflictsCandidateIds: ["05:06", "05:07"],
|
|
},
|
|
{
|
|
outcomeId: "no",
|
|
supportsCandidateIds: ["05:06", "05:07"],
|
|
conflictsCandidateIds: ["05:00"],
|
|
},
|
|
],
|
|
candidateSplitHash: "05:00|05:06|05:07",
|
|
informationGain: 0.5,
|
|
sourceFeatures: [{ technique: "test", calculationResultId: null }],
|
|
domain: "career",
|
|
year: 2020,
|
|
semanticKey: "career.2020.test",
|
|
};
|
|
|
|
const BASE_INPUT = {
|
|
engineCeiling: OPEN_ENGINE_CAPABILITY_CEILING,
|
|
methodCoverageAll: true,
|
|
trainingGateOpen: true,
|
|
candidateScores: [
|
|
{ time: "05:00", score: 34 },
|
|
{ time: "05:06", score: 33 },
|
|
{ time: "05:07", score: 33 },
|
|
],
|
|
discriminatorProbe: PROBE,
|
|
holdoutValidation: "unavailable" as const,
|
|
};
|
|
|
|
const DOSSIER_CANDIDATES = BASE_INPUT.candidateScores.map((item, index) => ({
|
|
candidateId: `candidate-${index + 1}`,
|
|
time: item.time,
|
|
rank: index + 1,
|
|
relativeSupport: item.score,
|
|
}));
|
|
|
|
function evidence(domain: string, index: number, overrides: Partial<DecisionDossier["evidence"][number]> = {}) {
|
|
return {
|
|
id: `evidence-${index}`,
|
|
status: "confirmed",
|
|
domain,
|
|
datePrecision: "year",
|
|
occurredFrom: `${2010 + index}-01-01`,
|
|
occurredTo: null,
|
|
eventKind: `${domain}_event`,
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
function dossier(rows: DecisionDossier["evidence"], state?: InferenceState): DecisionDossier {
|
|
return {
|
|
evidence: rows,
|
|
conversationSummary: { activeFocus: null, declinedSkippedTopics: [] },
|
|
latestResult: {
|
|
candidates: DOSSIER_CANDIDATES,
|
|
representativeTime: "05:00",
|
|
evidenceLedgerFingerprint: evidenceLedgerFingerprint(rows as never),
|
|
decisionReceipt: {
|
|
acceptance_allowed: true,
|
|
selection_allowed: true,
|
|
propose_allowed: true,
|
|
confirmation_allowed: true,
|
|
...(state ? { inference_state: state } : {}),
|
|
},
|
|
},
|
|
case: { acceptedTime: null },
|
|
};
|
|
}
|
|
|
|
function inferenceState(
|
|
answers: InferenceState["answered_probes"],
|
|
extra: Partial<InferenceState> = {},
|
|
): InferenceState {
|
|
return {
|
|
algorithm_version: INFERENCE_ALGORITHM_VERSION,
|
|
candidate_set_id: candidateSetId("05:00", "05:07", DOSSIER_CANDIDATES.map((item) => item.time)),
|
|
revision: 1,
|
|
phase: "discrimination",
|
|
result_status: "discriminating",
|
|
range_start: "05:00",
|
|
range_end: "05:07",
|
|
candidates: DOSSIER_CANDIDATES.map((item) => ({
|
|
id: item.candidateId,
|
|
time: item.time,
|
|
cluster_range: [item.time, item.time],
|
|
prior_score: item.relativeSupport,
|
|
posterior_score: item.relativeSupport,
|
|
probability: item.relativeSupport / 100,
|
|
status: "active" as const,
|
|
rank: item.rank,
|
|
strong_conflict_count: 0,
|
|
})),
|
|
events: [
|
|
{ id: "e1", domain: "career", year: 2011, precision: "year", usage: "training" },
|
|
{ id: "e2", domain: "relationship", year: 2012, precision: "year", usage: "training" },
|
|
{ id: "e3", domain: "family", year: 2013, precision: "year", usage: "training" },
|
|
],
|
|
probes: [],
|
|
answered_probes: answers,
|
|
rounds: [],
|
|
entropy: 1,
|
|
representative_time: "05:00",
|
|
credible_range: ["05:00", "05:07"],
|
|
...extra,
|
|
};
|
|
}
|
|
|
|
function remainingProbe(id: string, year: number): ConflictProbe {
|
|
return {
|
|
id,
|
|
semantic_key: `finance.${year}.test`,
|
|
candidate_split_hash: `split-${id}`,
|
|
domain: "finance",
|
|
year,
|
|
question: `${year} 年前后收入有没有明显变化?`,
|
|
candidate_ids: ["05:00", "05:06", "05:07"],
|
|
expected_outcomes: [
|
|
{ answer_class: "yes", supports: ["05:00"], conflicts: ["05:06", "05:07"] },
|
|
{ answer_class: "no", supports: ["05:06", "05:07"], conflicts: ["05:00"] },
|
|
{ answer_class: "unsure", supports: [], conflicts: [] },
|
|
],
|
|
information_gain: 0.5,
|
|
source: "dasha_boundary",
|
|
};
|
|
}
|
|
|
|
function lowInformationRounds(count: number): RoundTrace[] {
|
|
return Array.from({ length: count }, (_, index) => ({
|
|
round: index + 1,
|
|
phase: "discrimination" as const,
|
|
probe_id: `probe-${index + 1}`,
|
|
scores_before: {},
|
|
scores_after: {},
|
|
entropy_before: 1,
|
|
entropy_after: 1,
|
|
eliminated_ids: [],
|
|
winner_id: null,
|
|
kind: "low_information" as const,
|
|
}));
|
|
}
|
|
|
|
function choiceAnswer(
|
|
probeId: string,
|
|
year: number,
|
|
answerClass: "yes" | "no" | "unsure",
|
|
classifiedFrom: "choice" | "declined" = "choice",
|
|
) {
|
|
return {
|
|
probe_id: probeId,
|
|
semantic_key: `career.${year}.test`,
|
|
candidate_split_hash: `split-${probeId}`,
|
|
answer_class: answerClass,
|
|
classified_from: classifiedFrom,
|
|
};
|
|
}
|
|
|
|
function decideWithBudget(budget: {
|
|
inferenceRounds?: number;
|
|
effectiveAnswerCount?: number;
|
|
plateauRounds?: number;
|
|
}) {
|
|
return decideRectification({ ...BASE_INPUT, ...budget });
|
|
}
|
|
|
|
test("every persisted discrimination budget terminates before asking another probe", () => {
|
|
for (const budget of [
|
|
{ inferenceRounds: DEFAULT_MAX_DISCRIMINATION_ROUNDS },
|
|
{ effectiveAnswerCount: EFFECTIVE_ANSWER_SAFETY_CAP },
|
|
{ plateauRounds: RECTIFICATION_POLICY.maxPlateauRounds },
|
|
]) {
|
|
const decision = decideWithBudget(budget);
|
|
assert.equal(decision.nextAction, "complete_with_range");
|
|
// 原断言 sessionOutcome=completed_with_range → 新断言 adopt_representative。
|
|
// 为什么:预算耗尽只结束提问;覆盖完成且引擎可出牌时应交付代表性采用,不挡在 review-only 区间。
|
|
assert.equal(decision.sessionOutcome, "adopt_representative");
|
|
assert.equal(decision.canAdopt, true);
|
|
assert.equal(decision.canConfirmExactMinute, false);
|
|
assert.notEqual(decision.nextAction, "ask_candidate_discriminator");
|
|
}
|
|
});
|
|
|
|
test("repeated declined or unsure answers reach the existing plateau terminal", () => {
|
|
const decision = decideWithBudget({
|
|
inferenceRounds: 0,
|
|
effectiveAnswerCount: RECTIFICATION_POLICY.maxPlateauRounds,
|
|
plateauRounds: RECTIFICATION_POLICY.maxPlateauRounds,
|
|
});
|
|
assert.equal(decision.nextAction, "complete_with_range");
|
|
// 原断言 sessionOutcome=completed_with_range → 新断言 adopt_representative。
|
|
assert.equal(decision.sessionOutcome, "adopt_representative");
|
|
assert.equal(decision.canAdopt, true);
|
|
assert.equal(decision.canConfirmExactMinute, false);
|
|
});
|
|
|
|
test("exhausted discrimination still delivers the credible candidate range", () => {
|
|
const decision = decideWithBudget({
|
|
inferenceRounds: DEFAULT_MAX_DISCRIMINATION_ROUNDS,
|
|
effectiveAnswerCount: 0,
|
|
plateauRounds: 0,
|
|
});
|
|
assert.equal(decision.resultStatus, "completed_with_range");
|
|
// 原断言 sessionOutcome=completed_with_range / canAdopt=false → 新断言 adopt_representative / canAdopt=true。
|
|
// 为什么:exhausted 只结束区分轮,不挡代表性采用;唯一分钟确认门仍关。
|
|
assert.equal(decision.sessionOutcome, "adopt_representative");
|
|
assert.equal(decision.canOfferRange, true);
|
|
assert.equal(decision.canAdopt, true);
|
|
assert.equal(decision.canConfirmExactMinute, false);
|
|
assert.deepEqual(decision.credibleRange, ["05:00", "05:07"]);
|
|
});
|
|
|
|
test("additional score evidence never widens the credible range", () => {
|
|
const before = decideRectification({
|
|
...BASE_INPUT,
|
|
candidateScores: [
|
|
{ time: "05:00", score: 34 },
|
|
{ time: "05:06", score: 33 },
|
|
{ time: "05:07", score: 33 },
|
|
],
|
|
inferenceRounds: DEFAULT_MAX_DISCRIMINATION_ROUNDS,
|
|
});
|
|
const after = decideRectification({
|
|
...BASE_INPUT,
|
|
candidateScores: [
|
|
{ time: "05:00", score: 42 },
|
|
{ time: "05:06", score: 33 },
|
|
{ time: "05:07", score: 33 },
|
|
],
|
|
inferenceRounds: DEFAULT_MAX_DISCRIMINATION_ROUNDS,
|
|
});
|
|
|
|
const width = (range: readonly [string, string] | null) => {
|
|
assert.ok(range);
|
|
const toMinutes = (time: string) => Number(time.slice(0, 2)) * 60 + Number(time.slice(3, 5));
|
|
return toMinutes(range[1]) - toMinutes(range[0]);
|
|
};
|
|
|
|
assert.ok(width(after.credibleRange) <= width(before.credibleRange));
|
|
});
|
|
|
|
test("insufficient standalone evidence keeps collecting", () => {
|
|
for (const item of [
|
|
{ reason: "insufficient_dated_events" as const, input: { datedEventCount: 2, datedDomainCount: 2 } },
|
|
{ reason: "insufficient_domains" as const, input: { datedEventCount: 3, datedDomainCount: 1 } },
|
|
]) {
|
|
const decision = decideRectification({ ...BASE_INPUT, ...item.input });
|
|
assert.equal(decision.nextAction, "ask_fact_collection");
|
|
assert.equal(decision.sessionOutcome, "collect_evidence");
|
|
assert.equal(decision.canOfferRange, false);
|
|
assert.equal(decision.stopReason, item.reason);
|
|
}
|
|
});
|
|
|
|
test("exhausted evidence-state stops complete with a usable range and the fixed termination copy", () => {
|
|
const cases: readonly Readonly<{
|
|
reason: EvidenceStopReason;
|
|
input: Partial<Parameters<typeof decideRectification>[0]>;
|
|
}>[] = [
|
|
{
|
|
reason: "tied_first",
|
|
input: {
|
|
datedEventCount: 3,
|
|
datedDomainCount: 2,
|
|
candidateScores: [
|
|
{ time: "05:00", score: 34 },
|
|
{ time: "05:06", score: 34 },
|
|
{ time: "05:07", score: 32 },
|
|
],
|
|
discriminatorProbe: null,
|
|
},
|
|
},
|
|
{
|
|
reason: "user_uncertainty_too_high",
|
|
input: { datedEventCount: 3, datedDomainCount: 2, userUncertaintyHigh: true },
|
|
},
|
|
];
|
|
|
|
for (const item of cases) {
|
|
const decision = decideRectification({ ...BASE_INPUT, ...item.input });
|
|
assert.equal(decision.nextAction, "complete_with_range");
|
|
// 原断言 sessionOutcome=completed_with_range → 新断言 adopt_representative。
|
|
assert.equal(decision.sessionOutcome, "adopt_representative");
|
|
assert.equal(decision.resultStatus, "completed_with_range");
|
|
assert.equal(decision.canOfferRange, true);
|
|
assert.equal(decision.canAdopt, true);
|
|
assert.equal(decision.canConfirmExactMinute, false);
|
|
assert.ok(decision.credibleRange);
|
|
assert.equal(decision.stopReason, item.reason);
|
|
assert.equal(decision.terminationCopy, RECTIFICATION_TERMINATION_COPY);
|
|
}
|
|
});
|
|
|
|
test("stale snapshots cannot complete or adopt an evidence-stop range", () => {
|
|
const withProbe = decideRectification({
|
|
...BASE_INPUT,
|
|
snapshotCurrent: false,
|
|
datedEventCount: 2,
|
|
datedDomainCount: 2,
|
|
});
|
|
assert.equal(withProbe.nextAction, "ask_candidate_discriminator");
|
|
assert.equal(withProbe.canAdopt, false);
|
|
assert.equal(withProbe.selectionAllowed, false);
|
|
|
|
const withoutProbe = decideRectification({
|
|
...BASE_INPUT,
|
|
discriminatorProbe: null,
|
|
snapshotCurrent: false,
|
|
datedEventCount: 2,
|
|
datedDomainCount: 2,
|
|
});
|
|
assert.equal(withoutProbe.nextAction, "ask_fact_collection");
|
|
assert.equal(withoutProbe.canAdopt, false);
|
|
assert.equal(withoutProbe.selectionAllowed, false);
|
|
});
|
|
|
|
test("an exact first-place tie is distinct from a narrow 34/33/33 lead", () => {
|
|
const narrowLead = decideRectification({
|
|
...BASE_INPUT,
|
|
datedEventCount: 3,
|
|
datedDomainCount: 2,
|
|
});
|
|
assert.equal(narrowLead.separation.tiedForFirst, false);
|
|
assert.equal(narrowLead.stopReason, undefined);
|
|
assert.equal(narrowLead.nextAction, "ask_candidate_discriminator");
|
|
|
|
const exactTie = decideRectification({
|
|
...BASE_INPUT,
|
|
datedEventCount: 3,
|
|
datedDomainCount: 2,
|
|
candidateScores: [
|
|
{ time: "05:00", score: 34 },
|
|
{ time: "05:06", score: 34 },
|
|
{ time: "05:07", score: 32 },
|
|
],
|
|
});
|
|
assert.equal(exactTie.separation.tiedForFirst, true);
|
|
assert.equal(exactTie.stopReason, "tied_first");
|
|
});
|
|
|
|
test("standalone delivery floor remains separate from exact-minute confirmation", () => {
|
|
assert.equal(MIN_STANDALONE_DATED_EVENTS, 3);
|
|
assert.equal(MIN_STANDALONE_DATED_DOMAINS, 2);
|
|
assert.equal(RECTIFICATION_POLICY.minConfirmationEvents, 4);
|
|
assert.equal(RECTIFICATION_POLICY.minConfirmationDomains, 3);
|
|
});
|
|
|
|
test("dossier wiring counts only confirmed dated primary events", () => {
|
|
const decision = decideFromDossier(dossier([
|
|
evidence("career", 1),
|
|
evidence("relationship", 2),
|
|
evidence("family", 3, { status: "draft" }),
|
|
evidence("education", 4, { datePrecision: "unknown", occurredFrom: null }),
|
|
evidence("occupation", 5, { eventKind: "occupation_note" }),
|
|
]));
|
|
assert.equal(decision.stopReason, "insufficient_dated_events");
|
|
|
|
const oneDomain = decideFromDossier(dossier([
|
|
evidence("career", 1),
|
|
evidence("career", 2),
|
|
evidence("career", 3),
|
|
]));
|
|
assert.equal(oneDomain.stopReason, "insufficient_domains");
|
|
});
|
|
|
|
test("dossier and post-inference decisions share the half-uncertain stop rule", () => {
|
|
const rows = [
|
|
evidence("career", 1),
|
|
evidence("relationship", 2),
|
|
evidence("family", 3),
|
|
evidence("education", 4),
|
|
];
|
|
const nextProbe = remainingProbe("probe-next", 2023);
|
|
const oneUnsure = inferenceState(
|
|
[choiceAnswer("probe-1", 2020, "unsure")],
|
|
{ probes: [remainingProbe("probe-1", 2020), nextProbe] },
|
|
);
|
|
const oneUnsureDecision = decideFromDossier(dossier(rows, oneUnsure));
|
|
// 旧:1 答 1 unsure 即停 → 新:不足 3 答不停 → 保留 达到下限后一半不确定仍停
|
|
assert.equal(oneUnsureDecision.stopReason ?? null, null);
|
|
assert.equal(oneUnsureDecision.nextAction, "ask_candidate_discriminator");
|
|
assert.equal(decideAfterInferenceChange({
|
|
dossier: dossier(rows),
|
|
state: oneUnsure,
|
|
userStopped: false,
|
|
}).nextAction, "ask_candidate_discriminator");
|
|
|
|
const halfUncertain = inferenceState([
|
|
choiceAnswer("probe-1", 2020, "unsure"),
|
|
choiceAnswer("probe-2", 2021, "yes"),
|
|
]);
|
|
assert.equal(decideFromDossier(dossier(rows, halfUncertain)).stopReason ?? null, null);
|
|
assert.equal(decideAfterInferenceChange({
|
|
dossier: dossier(rows),
|
|
state: halfUncertain,
|
|
userStopped: false,
|
|
}).stopReason ?? null, null);
|
|
|
|
const threeAnswersTwoUnsure = inferenceState([
|
|
choiceAnswer("probe-1", 2020, "unsure"),
|
|
choiceAnswer("probe-2", 2021, "yes"),
|
|
choiceAnswer("probe-3", 2022, "unsure"),
|
|
]);
|
|
assert.equal(decideFromDossier(dossier(rows, threeAnswersTwoUnsure)).stopReason, "user_uncertainty_too_high");
|
|
assert.equal(decideAfterInferenceChange({
|
|
dossier: dossier(rows),
|
|
state: threeAnswersTwoUnsure,
|
|
userStopped: false,
|
|
}).stopReason, "user_uncertainty_too_high");
|
|
|
|
const fourAnswersTwoUnsure = inferenceState([
|
|
choiceAnswer("probe-1", 2020, "unsure"),
|
|
choiceAnswer("probe-2", 2021, "yes"),
|
|
choiceAnswer("probe-3", 2022, "unsure"),
|
|
choiceAnswer("probe-4", 2023, "yes"),
|
|
]);
|
|
assert.equal(decideAfterInferenceChange({
|
|
dossier: dossier(rows),
|
|
state: fourAnswersTwoUnsure,
|
|
userStopped: false,
|
|
}).stopReason, "user_uncertainty_too_high");
|
|
|
|
const belowHalf = inferenceState([
|
|
choiceAnswer("probe-1", 2020, "unsure"),
|
|
choiceAnswer("probe-2", 2021, "yes"),
|
|
choiceAnswer("probe-3", 2022, "no"),
|
|
]);
|
|
assert.equal(decideAfterInferenceChange({
|
|
dossier: dossier(rows),
|
|
state: belowHalf,
|
|
userStopped: false,
|
|
}).stopReason ?? null, null);
|
|
|
|
const evidenceDoesNotDilute = inferenceState([
|
|
choiceAnswer("probe-1", 2020, "unsure"),
|
|
choiceAnswer("probe-2", 2021, "yes"),
|
|
choiceAnswer("probe-3", 2022, "unsure"),
|
|
...[1, 2, 3].map((index) => ({
|
|
probe_id: `evidence-${index}`,
|
|
semantic_key: `evidence.${index}`,
|
|
candidate_split_hash: `evidence-${index}`,
|
|
answer_class: "yes" as const,
|
|
classified_from: "evidence" as const,
|
|
})),
|
|
]);
|
|
assert.equal(decideAfterInferenceChange({
|
|
dossier: dossier(rows),
|
|
state: evidenceDoesNotDilute,
|
|
userStopped: false,
|
|
}).stopReason, "user_uncertainty_too_high");
|
|
|
|
const halfDeclined = inferenceState([
|
|
choiceAnswer("probe-1", 2020, "no", "declined"),
|
|
choiceAnswer("probe-2", 2021, "yes"),
|
|
choiceAnswer("probe-3", 2022, "unsure"),
|
|
]);
|
|
assert.equal(decideAfterInferenceChange({
|
|
dossier: dossier(rows),
|
|
state: halfDeclined,
|
|
userStopped: false,
|
|
}).stopReason, "user_uncertainty_too_high");
|
|
});
|
|
|
|
test("unsure plateau rounds wait for the uncertainty sample floor", () => {
|
|
const rows = [
|
|
evidence("career", 1),
|
|
evidence("relationship", 2),
|
|
evidence("family", 3),
|
|
evidence("education", 4),
|
|
];
|
|
const twoUnsure = inferenceState(
|
|
[choiceAnswer("probe-1", 2020, "unsure"), choiceAnswer("probe-2", 2021, "unsure")],
|
|
{
|
|
probes: [remainingProbe("probe-next", 2023)],
|
|
rounds: lowInformationRounds(2),
|
|
},
|
|
);
|
|
const early = decideAfterInferenceChange({
|
|
dossier: dossier(rows),
|
|
state: twoUnsure,
|
|
userStopped: false,
|
|
});
|
|
assert.equal(early.stopReason ?? null, null);
|
|
assert.notEqual(early.nextAction, "complete_with_range");
|
|
assert.notEqual(early.sessionOutcome, "adopt_representative");
|
|
|
|
const afterFloor = inferenceState(
|
|
[
|
|
choiceAnswer("probe-1", 2020, "yes"),
|
|
choiceAnswer("probe-2", 2021, "yes"),
|
|
choiceAnswer("probe-3", 2022, "yes"),
|
|
choiceAnswer("probe-4", 2023, "unsure"),
|
|
choiceAnswer("probe-5", 2024, "unsure"),
|
|
],
|
|
{
|
|
probes: [remainingProbe("probe-next", 2025)],
|
|
rounds: lowInformationRounds(2),
|
|
},
|
|
);
|
|
const plateaued = decideAfterInferenceChange({
|
|
dossier: dossier(rows),
|
|
state: afterFloor,
|
|
userStopped: false,
|
|
});
|
|
assert.equal(plateaued.nextAction, "complete_with_range");
|
|
assert.equal(plateaued.sessionOutcome, "adopt_representative");
|
|
});
|
|
|
|
test("minUncertaintyAnswers is defined once in policy json", () => {
|
|
assert.equal(RECTIFICATION_POLICY.minUncertaintyAnswers, 3);
|
|
assert.equal(RECTIFICATION_POLICY.maxPlateauRounds, 2);
|
|
const policy = readFileSync(
|
|
new URL("../../references/rectification_policy.v1.json", import.meta.url),
|
|
"utf8",
|
|
);
|
|
assert.equal([...policy.matchAll(/minUncertaintyAnswers/g)].length, 1);
|
|
});
|
|
|
|
test("rectification label ladder follows evidence state and explicit adapter support", () => {
|
|
const blocked = decideRectification({
|
|
engineCeiling: OPEN_ENGINE_CAPABILITY_CEILING,
|
|
methodCoverageAll: false,
|
|
trainingGateOpen: false,
|
|
candidateScores: [],
|
|
});
|
|
// 原值是 blocked;空候选现在被分类为证据不足,不能继续无原因地 fail-open。
|
|
assert.equal(rectificationLabel({ decision: blocked }), "user_history_verification_required");
|
|
|
|
const stopped = decideRectification({
|
|
...BASE_INPUT,
|
|
datedEventCount: 2,
|
|
datedDomainCount: 2,
|
|
});
|
|
assert.equal(rectificationLabel({ decision: stopped }), "user_history_verification_required");
|
|
|
|
const continuing = decideRectification({
|
|
...BASE_INPUT,
|
|
datedEventCount: 3,
|
|
datedDomainCount: 2,
|
|
});
|
|
assert.equal(rectificationLabel({ decision: continuing }), "manual_pattern_consensus");
|
|
assert.equal(rectificationLabel({
|
|
decision: continuing,
|
|
supportedAdapterCount: 1,
|
|
}), "single_adapter_support");
|
|
assert.equal(rectificationLabel({
|
|
decision: continuing,
|
|
supportedAdapterCount: 2,
|
|
}), "multi_adapter_consensus");
|
|
|
|
const tied = decideRectification({
|
|
...BASE_INPUT,
|
|
datedEventCount: 3,
|
|
datedDomainCount: 2,
|
|
candidateScores: [
|
|
{ time: "05:00", score: 34 },
|
|
{ time: "05:06", score: 34 },
|
|
],
|
|
});
|
|
assert.equal(rectificationLabel({ decision: tied }), "blocked");
|
|
});
|