Files
Jyotisha/frontend/tests/rectification-probe-replay-loss-20260908.test.ts
T
Jesse_ChenandCursor be71104313
Independent Staging Quality Gate / validate (push) Successful in 13m45s
Independent Staging Quality Gate / publish (push) Successful in 12m20s
fix(rectification): inherit answered-probe outcomes onto new minutes (BUG-594)
Rescored candidate sets were widening because new minutes stayed
neutral. Derive support/conflict from varga signs and dasha segments,
and announce range changes from facts rather than tool names.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-08 17:33:24 +08:00

544 lines
19 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import test from "node:test";
import {
buildCandidateContrastPacket,
vargaSignPartitionKey,
} from "../src/lib/rectification-agentic/core/candidate-contrast-packet.ts";
import {
buildInferenceState,
nextProbe,
replayInferenceState,
} from "../src/lib/rectification-agentic/core/build-state.ts";
import { applyProbeOutcome } from "../src/lib/rectification-agentic/core/apply-probe-outcome.ts";
import type { TransitionSignLookup } from "../src/lib/rectification-agentic/core/sign-from-transitions.ts";
import { selectHighestGainProbe } from "../src/lib/rectification-agentic/core/select-probe.ts";
import type { ConflictProbe, ProbeAnswer } from "../src/lib/rectification-agentic/core/types.ts";
import {
rangeChangedAfterEvidence,
rangeWidthMinutes,
withRangeChangedAfterEvidence,
} from "../src/lib/rectification-agentic/user-copy.ts";
const S1_TIMES = [
"04:50", "04:51", "04:52", "04:53", "04:54", "04:55", "04:56", "04:57", "05:13",
] as const;
const KEPT_TIMES = S1_TIMES.slice(0, 7);
const REPLACED = ["04:57", "05:13"] as const;
const NEW_TIMES = ["04:59", "05:14"] as const;
const S2_TIMES = [...KEPT_TIMES, ...NEW_TIMES] as const;
const EVENTS = [
{ id: "e-edu-2010", domain: "education", year: 2010, precision: "year" as const },
{ id: "e-family-2011", domain: "family", year: 2011, precision: "year" as const },
{ id: "e-finance-2012", domain: "finance", year: 2012, precision: "year" as const },
{ id: "e-health-2013", domain: "health_pressure", year: 2013, precision: "year" as const },
];
function clockProbe(input: {
id: string;
semanticKey: string;
domain: string;
year: number;
source: string;
choiceKind?: ConflictProbe["choice_kind"];
yesSupports: readonly string[];
yesConflicts: readonly string[];
}): ConflictProbe {
return {
id: input.id,
semantic_key: input.semanticKey,
candidate_split_hash: `${input.semanticKey}:${input.yesSupports.join(",")}`,
domain: input.domain,
year: input.year,
question: input.semanticKey,
candidate_ids: [...input.yesSupports, ...input.yesConflicts],
expected_outcomes: [
{ answer_class: "yes", supports: input.yesSupports, conflicts: input.yesConflicts },
{ answer_class: "weak_yes", supports: input.yesSupports, conflicts: input.yesConflicts },
{ answer_class: "no", supports: input.yesConflicts, conflicts: input.yesSupports },
{ answer_class: "unsure", supports: [], conflicts: [] },
],
information_gain: 0.4,
source: input.source,
...(input.choiceKind ? { choice_kind: input.choiceKind } : {}),
};
}
function answeredProbes(): ConflictProbe[] {
const yesSupports = [...KEPT_TIMES];
const yesConflicts = [...REPLACED];
return [
clockProbe({
id: "probe:varga.d9.old",
semanticKey: "varga.d9.04:50|04:51|04:52|04:53|04:54|04:55|04:56/04:57|05:13",
domain: "relationship",
year: 0,
source: "varga_contrast",
choiceKind: "varga_style",
yesSupports,
yesConflicts,
}),
clockProbe({
id: "probe:varga.d10.old",
semanticKey: "varga.d10.04:50|04:51|04:52|04:53|04:54|04:55|04:56/04:57|05:13",
domain: "career",
year: 0,
source: "varga_contrast",
choiceKind: "varga_style",
yesSupports,
yesConflicts,
}),
clockProbe({
id: "probe:career.2023",
semanticKey: "career.2023",
domain: "career",
year: 2023,
source: "dasha_boundary",
yesSupports,
yesConflicts,
}),
clockProbe({
id: "probe:career.2024",
semanticKey: "career.2024",
domain: "career",
year: 2024,
source: "dasha_boundary",
yesSupports,
yesConflicts,
}),
clockProbe({
id: "probe:relocation.2015",
semanticKey: "relocation.2015",
domain: "relocation",
year: 2015,
source: "dasha_boundary",
yesSupports,
yesConflicts,
}),
];
}
function engineCandidates(times: readonly string[]) {
return times.map((time) => ({ id: time, time, relative_support: 20 }));
}
function choiceAnswers(probes: readonly ConflictProbe[]): ProbeAnswer[] {
return probes.map((probe) => ({
probe_id: probe.id,
semantic_key: probe.semantic_key,
candidate_split_hash: probe.candidate_split_hash,
answer_class: "yes" as const,
classified_from: "choice" as const,
}));
}
function deltaFor(state: ReturnType<typeof buildInferenceState>, time: string): number {
const row = state.candidates.find((item) => item.time === time);
assert.ok(row, time);
return row.posterior_score - row.prior_score;
}
function width(range: readonly [string, string] | null | undefined): number {
if (!range) return Number.POSITIVE_INFINITY;
return rangeWidthMinutes(range[0], range[1]) ?? Number.POSITIVE_INFINITY;
}
function buildS1() {
const probes = answeredProbes();
return buildInferenceState({
range_start: "04:45",
range_end: "05:15",
candidates: engineCandidates(S1_TIMES),
events: EVENTS,
probes,
answered_probes: choiceAnswers(probes),
});
}
function rescoreWithoutLiveProbes(
previous: ReturnType<typeof buildInferenceState>,
times: readonly string[],
liveProbes: readonly ConflictProbe[] = [],
) {
return buildInferenceState({
range_start: "04:45",
range_end: "05:15",
candidates: engineCandidates(times),
events: EVENTS,
probes: liveProbes,
previous,
});
}
test("BUG-587 (a) a changed candidate set still replays five answered probes", () => {
const s1 = buildS1();
assert.equal(s1.answered_probes.length, 5);
assert.equal(s1.rounds.length, 5);
assert.ok(width(s1.credible_range) <= 8);
const s2 = rescoreWithoutLiveProbes(s1, S2_TIMES);
assert.equal(s2.rounds.length, 5);
assert.equal(s2.answered_probes.length, 5);
assert.equal(s2.probes.filter((item) => item.carried).length, 5);
for (const time of KEPT_TIMES) {
assert.equal(deltaFor(s2, time), deltaFor(s1, time), time);
}
// 04:59 sits between conflict minutes 04:57 and 05:13, so it inherits conflict.
assert.ok(deltaFor(s2, "04:59") < 0);
// 05:14 is past the last known minute and this fixture has no transitions.
assert.equal(deltaFor(s2, "05:14"), 0);
assert.ok(width(s2.credible_range) <= width(s1.credible_range) + NEW_TIMES.length);
});
test("BUG-587 (b) a second rescore still replays every answer", () => {
const s1 = buildS1();
const afterFinance = rescoreWithoutLiveProbes(s1, S2_TIMES);
const afterRelocation = rescoreWithoutLiveProbes(afterFinance, S2_TIMES);
assert.equal(afterRelocation.rounds.length, 5);
for (const time of KEPT_TIMES) {
assert.equal(deltaFor(afterRelocation, time), deltaFor(s1, time), time);
}
assert.ok(deltaFor(afterRelocation, "04:59") < 0);
assert.equal(deltaFor(afterRelocation, "05:14"), 0);
assert.equal(
afterRelocation.candidates.every((item) => item.posterior_score === item.prior_score),
false,
);
});
test("BUG-587 (c) varga semantic keys use the sign partition, not the minute list", () => {
const transitions = [
{ layer: "d9", at: "04:53", from_sign: "天秤座", to_sign: "天蝎座" },
{ layer: "d9", at: "04:57", from_sign: "天蝎座", to_sign: "射手座" },
];
const first = buildCandidateContrastPacket({
candidateSetVersion: "set-a",
candidateTimes: ["04:50", "04:53", "04:57"],
transitions,
});
const second = buildCandidateContrastPacket({
candidateSetVersion: "set-b",
candidateTimes: ["04:50", "04:52", "04:53", "04:57"],
transitions,
});
const firstKey = first.probes.find((item) => item.semanticKey.startsWith("varga.d9."))?.semanticKey;
const secondKey = second.probes.find((item) => item.semanticKey.startsWith("varga.d9."))?.semanticKey;
assert.equal(firstKey, "varga.d9.天秤座|天蝎座|射手座");
assert.equal(secondKey, firstKey);
assert.doesNotMatch(firstKey ?? "", /\d\d:\d\d/);
assert.equal(vargaSignPartitionKey("d9", ["天秤座", "天蝎座", "射手座"]), firstKey);
const s1 = buildS1();
const s2 = rescoreWithoutLiveProbes(s1, S2_TIMES);
const carried = s2.probes.find((item) => item.semantic_key.includes("varga.d9."));
assert.ok(carried?.carried);
assert.match(carried.semantic_key, /\d\d:\d\d/);
assert.equal(
s2.answered_probes.some((item) => item.semantic_key === carried.semantic_key),
true,
);
});
test("BUG-587 (d) nextProbe does not re-ask a carried probe", () => {
const s1 = buildS1();
const fresh: ConflictProbe = clockProbe({
id: "probe:education.2016",
semanticKey: "education.2016",
domain: "education",
year: 2016,
source: "dasha_boundary",
yesSupports: ["04:50", "04:51"],
yesConflicts: ["05:14"],
});
const s2 = rescoreWithoutLiveProbes(s1, S2_TIMES, [fresh]);
const selected = nextProbe(s2);
assert.equal(selected?.id, fresh.id);
assert.equal(s2.probes.some((item) => item.carried && item.id === selected?.id), false);
assert.equal(
selectHighestGainProbe(s2.probes, s2.answered_probes)?.id,
fresh.id,
);
for (const probe of s2.probes.filter((item) => item.carried)) {
assert.equal(selectHighestGainProbe([probe], s2.answered_probes), null);
}
});
test("BUG-588 evidence narration names a widened range after compare", () => {
const changed = withRangeChangedAfterEvidence(
"记下了。",
["04:50", "04:57"],
["04:47", "05:15"],
);
assert.match(changed, /范围从 04:5004:57 变为 04:4705:15。/);
assert.equal(
rangeChangedAfterEvidence(["04:50", "04:57"], ["04:47", "05:15"]),
"范围从 04:5004:57 变为 04:4705:15。",
);
assert.equal(
withRangeChangedAfterEvidence("记下了。", ["04:50", "04:57"], ["04:50", "04:57"]),
"记下了。",
);
assert.equal(rangeChangedAfterEvidence(["04:50", "04:57"], ["04:50", "04:57"]), null);
const agentRun = readFileSync(
new URL("../src/lib/rectification-agentic/v9/agent-run.ts", import.meta.url),
"utf8",
);
const askedFocusAt = agentRun.indexOf("const askedFocus = latestDossier.conversationSummary.activeFocus");
const settlement = askedFocusAt >= 0 ? agentRun.slice(askedFocusAt) : "";
const stripAt = settlement.indexOf("stripQuestionSentences");
const rangeAt = settlement.indexOf("withRangeChangedAfterEvidence");
assert.ok(stripAt >= 0 && rangeAt > stripAt);
assert.doesNotMatch(settlement, /toolsUsed\.has\("rectification-compare-candidates"\)/);
assert.match(settlement, /rangeAfterEvidence/);
assert.match(
withRangeChangedAfterEvidence("接下来我们继续。", ["04:47", "04:59"], ["04:47", "05:14"]),
/范围从 04:4704:59 变为 04:4705:14。/,
);
});
test("P3 selection cards stay gated by busy and a loaded snapshot", () => {
const chat = readFileSync(
new URL("../src/components/rectification-agentic-chat.tsx", import.meta.url),
"utf8",
);
const block = chat.slice(
chat.indexOf("const showSelectionCards"),
chat.indexOf("const showReadonlyRange"),
);
assert.match(block, /!busy/);
assert.match(block, /caseSnapshotLoaded/);
assert.match(chat, /startTransition\(/);
});
const ACCIDENT_EARLY = ["04:47", "04:51", "04:53", "04:55", "04:57", "04:59"] as const;
const ACCIDENT_LATE = ["05:00", "05:06", "05:15"] as const;
const ACCIDENT_OLD = [...ACCIDENT_EARLY, ...ACCIDENT_LATE] as const;
const ACCIDENT_NEW = [...ACCIDENT_EARLY, "04:48", "04:50", "05:14"] as const;
const ACCIDENT_TRANSITIONS: readonly TransitionSignLookup[] = [
{ layer: "d9", at: "05:00", from_sign: "天蝎座", to_sign: "射手座" },
{ layer: "d10", at: "05:00", from_sign: "巨蟹座", to_sign: "狮子座" },
];
function vargaStyleProbe(input: {
id: string;
layer: "d9" | "d10";
yesSign: string;
otherSign: string;
domain: string;
}): ConflictProbe {
const semanticKey = `varga.${input.layer}.${input.yesSign}|${input.otherSign}`;
return {
id: input.id,
semantic_key: semanticKey,
candidate_split_hash: `${semanticKey}:${ACCIDENT_EARLY.join(",")}`,
domain: input.domain,
year: 0,
question: semanticKey,
candidate_ids: [...ACCIDENT_OLD],
expected_outcomes: [
{ answer_class: "yes", supports: ACCIDENT_EARLY, conflicts: ACCIDENT_LATE },
{ answer_class: "weak_yes", supports: ACCIDENT_LATE, conflicts: ACCIDENT_EARLY },
{ answer_class: "no", supports: [], conflicts: [] },
{ answer_class: "unsure", supports: [], conflicts: [] },
],
information_gain: 0.4,
source: "varga_contrast",
choice_kind: "varga_style",
style_options: [
{ label: input.yesSign, answer_class: "yes", sign: input.yesSign },
{ label: input.otherSign, answer_class: "weak_yes", sign: input.otherSign },
],
};
}
function segmentProbe(input: {
id: string;
semanticKey: string;
domain: string;
year: number;
yesSupportsLate: boolean;
}): ConflictProbe {
const yesSupports = input.yesSupportsLate ? ACCIDENT_LATE : ACCIDENT_EARLY;
const yesConflicts = input.yesSupportsLate ? ACCIDENT_EARLY : ACCIDENT_LATE;
return clockProbe({
id: input.id,
semanticKey: input.semanticKey,
domain: input.domain,
year: input.year,
source: "dasha_boundary",
yesSupports,
yesConflicts,
});
}
function accidentProbes(): ConflictProbe[] {
return [
vargaStyleProbe({
id: "probe:varga.d9.scorpio",
layer: "d9",
yesSign: "天蝎座",
otherSign: "射手座",
domain: "relationship",
}),
vargaStyleProbe({
id: "probe:varga.d10.cancer",
layer: "d10",
yesSign: "巨蟹座",
otherSign: "狮子座",
domain: "career",
}),
segmentProbe({
id: "probe:career.2023.05",
semanticKey: "career.2023.05",
domain: "career",
year: 2023,
yesSupportsLate: true,
}),
segmentProbe({
id: "probe:career.2024.04",
semanticKey: "career.2024.04",
domain: "career",
year: 2024,
yesSupportsLate: true,
}),
segmentProbe({
id: "probe:relocation.2015.05",
semanticKey: "relocation.2015.05",
domain: "relocation",
year: 2015,
yesSupportsLate: false,
}),
segmentProbe({
id: "probe:family.2011",
semanticKey: "family.2011",
domain: "family",
year: 2011,
yesSupportsLate: false,
}),
];
}
function accidentAnswers(probes: readonly ConflictProbe[]): ProbeAnswer[] {
return probes.map((probe) => ({
probe_id: probe.id,
semantic_key: probe.semantic_key,
candidate_split_hash: probe.candidate_split_hash,
answer_class: probe.semantic_key === "career.2023.05" ? "no" as const : "yes" as const,
classified_from: "choice" as const,
}));
}
function buildAccidentState(
times: readonly string[],
previous?: ReturnType<typeof buildInferenceState> | null,
) {
const probes = previous ? [] : accidentProbes();
return buildInferenceState({
range_start: "04:45",
range_end: "05:15",
candidates: engineCandidates(times),
events: EVENTS,
probes,
previous,
answered_probes: previous ? [] : accidentAnswers(accidentProbes()),
transitions: ACCIDENT_TRANSITIONS,
transition_times: ACCIDENT_TRANSITIONS.map((item) => item.at),
});
}
test("BUG-594 (a) a new minute inherits varga signs and segment outcomes and is eliminated", () => {
const s1 = buildAccidentState(ACCIDENT_OLD);
assert.equal(s1.answered_probes.length, 6);
for (const time of ACCIDENT_LATE) {
const row = s1.candidates.find((item) => item.time === time);
assert.ok(row, time);
assert.ok(row.strong_conflict_count >= 3, time);
assert.equal(row.status, "eliminated", time);
}
assert.deepEqual(s1.credible_range, ["04:47", "04:59"]);
const s2 = buildAccidentState(ACCIDENT_NEW, s1);
const born = s2.candidates.find((item) => item.time === "05:14");
assert.ok(born);
assert.ok(born.strong_conflict_count >= 3);
assert.equal(born.status, "eliminated");
assert.deepEqual(s2.credible_range, ["04:47", "04:59"]);
assert.ok(s2.rounds.some((round) => (round.score_deltas?.["05:14"] ?? 0) !== 0));
const d9 = s2.probes.find((item) => item.semantic_key.startsWith("varga.d9."));
assert.equal(d9?.outcome_by_minute?.["05:14"], "conflict");
const career2024 = s2.probes.find((item) => item.semantic_key === "career.2024.04");
assert.equal(career2024?.outcome_by_minute?.["05:14"], "support");
const career2023 = s2.probes.find((item) => item.semantic_key === "career.2023.05");
assert.equal(career2023?.outcome_by_minute?.["05:14"], "conflict");
const replayed = replayInferenceState(s2, s2.answered_probes);
assert.equal(replayed.candidates.find((item) => item.time === "05:14")?.status, "eliminated");
});
test("BUG-594 (b) a new minute between two outcome sets stays neutral on that probe", () => {
const probe = clockProbe({
id: "probe:career.2023.05",
semanticKey: "career.2023.05",
domain: "career",
year: 2023,
source: "dasha_boundary",
yesSupports: ["04:47", "04:51", "04:53"],
yesConflicts: ["05:06", "05:15"],
});
const scores = Object.fromEntries(
["04:47", "04:51", "04:53", "05:00", "05:06", "05:15"].map((time) => [time, 20]),
);
const applied = applyProbeOutcome(scores, probe, "yes");
assert.equal(applied.deltas["05:00"], 0);
assert.ok((applied.deltas["04:47"] ?? 0) > 0);
assert.ok((applied.deltas["05:06"] ?? 0) < 0);
});
test("BUG-594 (c) a varga probe without transitions leaves a new minute neutral", () => {
const probe = vargaStyleProbe({
id: "probe:varga.d9.scorpio",
layer: "d9",
yesSign: "天蝎座",
otherSign: "射手座",
domain: "relationship",
});
const scores = Object.fromEntries([...ACCIDENT_OLD, "05:14"].map((time) => [time, 20]));
const applied = applyProbeOutcome(scores, probe, "yes");
assert.equal(applied.deltas["05:14"], 0);
const withSigns = applyProbeOutcome(scores, probe, "yes", {
transitions: ACCIDENT_TRANSITIONS,
});
assert.ok((withSigns.deltas["05:14"] ?? 0) < 0);
});
test("BUG-594 (d) successive covered new minutes do not widen the 13-minute accident range", () => {
let state = buildAccidentState(ACCIDENT_OLD);
assert.deepEqual(state.credible_range, ["04:47", "04:59"]);
assert.equal(width(state.credible_range), 12);
const rounds: readonly (readonly string[])[] = [
ACCIDENT_NEW,
[...ACCIDENT_EARLY, "04:49", "04:52", "05:14"],
[...ACCIDENT_EARLY, "04:48", "04:54", "05:13"],
];
for (const times of rounds) {
const previousWidth = width(state.credible_range);
const previousIds = new Set(state.candidates.map((item) => item.id));
state = buildAccidentState(times, state);
const uncovered = times.filter((time) => {
if (previousIds.has(time)) return false;
return state.probes.every((probe) => (
!probe.outcome_by_minute || probe.outcome_by_minute[time] === "neutral"
));
});
assert.ok(
width(state.credible_range) <= previousWidth + uncovered.length,
`${times.join(",")}: ${width(state.credible_range)} > ${previousWidth}+${uncovered.length}`,
);
}
assert.equal(width(state.credible_range), 12);
assert.deepEqual(state.credible_range, ["04:47", "04:59"]);
});