Files
Jyotisha/frontend/tests/rectification-convergence-budget.test.ts
T
Jesse_Chen 9f011194f2
Independent Staging Quality Gate / validate (push) Successful in 13m0s
Independent Staging Quality Gate / publish (push) Successful in 10m58s
fix(rectification): centralize delivery authority
2026-09-01 01:13:04 +08:00

447 lines
14 KiB
TypeScript

import assert from "node:assert/strict";
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 InferenceState,
} 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: state ? { inference_state: state } : null,
},
case: { acceptedTime: null },
};
}
function inferenceState(answers: InferenceState["answered_probes"]): 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: [],
probes: [],
answered_probes: answers,
rounds: [],
entropy: 1,
representative_time: "05:00",
credible_range: ["05:00", "05:07"],
};
}
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");
assert.equal(decision.sessionOutcome, "completed_with_range");
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");
assert.equal(decision.sessionOutcome, "completed_with_range");
});
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");
assert.equal(decision.sessionOutcome, "completed_with_range");
assert.equal(decision.canOfferRange, true);
// 原值是 true;候选未分开且区分预算耗尽只能交付 review-only range,不能采用。
assert.equal(decision.canAdopt, 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");
assert.equal(decision.sessionOutcome, "completed_with_range");
assert.equal(decision.resultStatus, "completed_with_range");
assert.equal(decision.canOfferRange, true);
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),
];
const halfUncertain = inferenceState([
{
probe_id: "probe-1",
semantic_key: "career.2020.test",
candidate_split_hash: "split-1",
answer_class: "unsure",
classified_from: "choice",
},
{
probe_id: "probe-2",
semantic_key: "relationship.2021.test",
candidate_split_hash: "split-2",
answer_class: "yes",
classified_from: "choice",
},
]);
assert.equal(decideFromDossier(dossier(rows, halfUncertain)).stopReason, "user_uncertainty_too_high");
assert.equal(decideAfterInferenceChange({
dossier: dossier(rows),
state: halfUncertain,
userStopped: false,
}).stopReason, "user_uncertainty_too_high");
const belowHalf = inferenceState([
...halfUncertain.answered_probes,
{
probe_id: "probe-3",
semantic_key: "family.2022.test",
candidate_split_hash: "split-3",
answer_class: "no",
classified_from: "choice",
},
]);
assert.equal(decideAfterInferenceChange({
dossier: dossier(rows),
state: belowHalf,
userStopped: false,
}).stopReason ?? null, null);
const evidenceDoesNotDilute = inferenceState([
...halfUncertain.answered_probes,
...[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([
{
probe_id: "probe-1",
semantic_key: "career.2020.test",
candidate_split_hash: "split-1",
answer_class: "no",
classified_from: "declined",
},
halfUncertain.answered_probes[1],
]);
assert.equal(decideAfterInferenceChange({
dossier: dossier(rows),
state: halfDeclined,
userStopped: false,
}).stopReason, "user_uncertainty_too_high");
});
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");
});