fix(rectification): read credible_range as the still-valid union
Write and read used different range definitions at the same lead of 8, so a real separation always fail-closed the candidate projection. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,219 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
|
||||
import { buildInferenceState, type EngineEventInput } from "../src/lib/rectification-agentic/core/build-state.ts";
|
||||
import { unionStillValidRange } from "../src/lib/rectification-agentic/core/credible-range.ts";
|
||||
import { decideFromDossier } from "../src/lib/rectification-agentic/v9/decision-from-dossier.ts";
|
||||
import {
|
||||
authoritativeCandidateProjection,
|
||||
buildCaseInferenceState,
|
||||
} from "../src/lib/rectification-agentic/v9/inference-adapter.ts";
|
||||
import { evidenceLedgerFingerprint } from "../src/lib/rectification-agentic/v9/tool-service.ts";
|
||||
|
||||
const HOLDOUT_EVENTS: readonly EngineEventInput[] = [
|
||||
{ id: "e-edu", domain: "education", year: 2016, precision: "year" },
|
||||
{ id: "e-career", domain: "career", year: 2018, precision: "year" },
|
||||
{ id: "e-rel", domain: "relationship", year: 2021, precision: "year" },
|
||||
{ id: "e-fam", domain: "family", year: 2023, precision: "year" },
|
||||
];
|
||||
|
||||
const COVERAGE_EVIDENCE = [
|
||||
{
|
||||
id: "e-edu",
|
||||
status: "confirmed",
|
||||
domain: "education",
|
||||
datePrecision: "year",
|
||||
occurredFrom: "2016-01-01",
|
||||
occurredTo: null,
|
||||
eventKind: "education_milestone",
|
||||
},
|
||||
{
|
||||
id: "e-career",
|
||||
status: "confirmed",
|
||||
domain: "career",
|
||||
datePrecision: "year",
|
||||
occurredFrom: "2018-01-01",
|
||||
occurredTo: null,
|
||||
eventKind: "career_entry",
|
||||
},
|
||||
{
|
||||
id: "e-rel",
|
||||
status: "confirmed",
|
||||
domain: "relationship",
|
||||
datePrecision: "year",
|
||||
occurredFrom: "2021-01-01",
|
||||
occurredTo: null,
|
||||
eventKind: "relationship_start",
|
||||
},
|
||||
{
|
||||
id: "e-fam",
|
||||
status: "confirmed",
|
||||
domain: "family",
|
||||
datePrecision: "year",
|
||||
occurredFrom: "2023-01-01",
|
||||
occurredTo: null,
|
||||
eventKind: "family_event",
|
||||
},
|
||||
{
|
||||
id: "e-occ",
|
||||
status: "confirmed",
|
||||
domain: "occupation",
|
||||
datePrecision: "unknown",
|
||||
occurredFrom: null,
|
||||
occurredTo: null,
|
||||
eventKind: "occupation_note",
|
||||
},
|
||||
] as const;
|
||||
|
||||
function producedSnapshot(
|
||||
rows: readonly Readonly<{ time: string; relativeSupport: number }>[],
|
||||
events: readonly EngineEventInput[] = [],
|
||||
) {
|
||||
const times = rows.map((item) => item.time).sort();
|
||||
const state = buildInferenceState({
|
||||
range_start: times[0]!,
|
||||
range_end: times[times.length - 1]!,
|
||||
candidates: rows.map((item) => ({
|
||||
id: item.time,
|
||||
time: item.time,
|
||||
relative_support: item.relativeSupport,
|
||||
})),
|
||||
events,
|
||||
probes: [],
|
||||
});
|
||||
const persisted = state.candidates.map((item) => ({
|
||||
candidateId: item.id,
|
||||
time: item.time,
|
||||
rank: item.rank,
|
||||
relativeSupport: Math.round(item.posterior_score),
|
||||
}));
|
||||
return {
|
||||
state,
|
||||
latest: {
|
||||
resultId: "55555555-5555-4555-8555-555555555555",
|
||||
candidates: persisted,
|
||||
representativeTime: state.representative_time,
|
||||
evidenceLedgerFingerprint: evidenceLedgerFingerprint(COVERAGE_EVIDENCE as never),
|
||||
decisionReceipt: { inference_state: state },
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function dossierFor(latest: ReturnType<typeof producedSnapshot>["latest"]) {
|
||||
return {
|
||||
evidence: COVERAGE_EVIDENCE,
|
||||
conversationSummary: { activeFocus: null, declinedSkippedTopics: [] },
|
||||
latestResult: latest,
|
||||
case: { acceptedTime: null },
|
||||
};
|
||||
}
|
||||
|
||||
test("producer 58/42 lead keeps both candidates and can close without opening the minute gate", () => {
|
||||
const { state, latest } = producedSnapshot([
|
||||
{ time: "05:02", relativeSupport: 58 },
|
||||
{ time: "04:55", relativeSupport: 42 },
|
||||
], HOLDOUT_EVENTS);
|
||||
assert.deepEqual(state.credible_range, unionStillValidRange(state.candidates));
|
||||
assert.notDeepEqual(state.credible_range, ["04:55", "05:02"]);
|
||||
|
||||
const projection = authoritativeCandidateProjection(latest);
|
||||
assert.equal(projection.consistent, true);
|
||||
assert.equal(projection.candidates.length, 2);
|
||||
assert.deepEqual(projection.credibleRange, state.credible_range);
|
||||
|
||||
const decision = decideFromDossier(dossierFor(latest));
|
||||
assert.equal(decision.separation.sufficient, true);
|
||||
assert.equal(decision.nextAction, "ask_holdout_validation");
|
||||
assert.equal(decision.canConfirmExactMinute, false);
|
||||
assert.notEqual(decision.sessionOutcome, "exact_minute_confirmed");
|
||||
});
|
||||
|
||||
test("producer 58/42 with no holdout is ready_to_adopt, not a unique minute", () => {
|
||||
const { latest } = producedSnapshot([
|
||||
{ time: "05:02", relativeSupport: 58 },
|
||||
{ time: "04:55", relativeSupport: 42 },
|
||||
]);
|
||||
const decision = decideFromDossier(dossierFor(latest));
|
||||
assert.equal(authoritativeCandidateProjection(latest).consistent, true);
|
||||
assert.equal(decision.separation.sufficient, true);
|
||||
assert.equal(decision.nextAction, "ready_to_adopt");
|
||||
assert.equal(decision.canConfirmExactMinute, false);
|
||||
assert.notEqual(decision.sessionOutcome, "exact_minute_confirmed");
|
||||
});
|
||||
|
||||
test("producer 50/30/20 lead keeps all three candidates and still-valid range", () => {
|
||||
const { state, latest } = producedSnapshot([
|
||||
{ time: "05:00", relativeSupport: 50 },
|
||||
{ time: "05:20", relativeSupport: 30 },
|
||||
{ time: "05:40", relativeSupport: 20 },
|
||||
]);
|
||||
const projection = authoritativeCandidateProjection(latest);
|
||||
assert.equal(projection.consistent, true);
|
||||
assert.equal(projection.candidates.length, 3);
|
||||
assert.deepEqual(projection.credibleRange, unionStillValidRange(state.candidates));
|
||||
assert.deepEqual(projection.credibleRange, state.credible_range);
|
||||
});
|
||||
|
||||
test("producer sole candidate stays consistent", () => {
|
||||
const { state, latest } = producedSnapshot([
|
||||
{ time: "05:02", relativeSupport: 100 },
|
||||
]);
|
||||
const projection = authoritativeCandidateProjection(latest);
|
||||
assert.equal(projection.consistent, true);
|
||||
assert.equal(projection.candidates.length, 1);
|
||||
assert.deepEqual(projection.credibleRange, unionStillValidRange(state.candidates));
|
||||
});
|
||||
|
||||
test("missing inference_state still fail-closes to an empty projection", () => {
|
||||
const projection = authoritativeCandidateProjection({
|
||||
candidates: [
|
||||
{ candidateId: "05:02", time: "05:02", relativeSupport: 58 },
|
||||
{ candidateId: "04:55", time: "04:55", relativeSupport: 42 },
|
||||
],
|
||||
decisionReceipt: {},
|
||||
});
|
||||
assert.equal(projection.consistent, false);
|
||||
assert.deepEqual(projection.candidates, []);
|
||||
assert.deepEqual(projection.scores, []);
|
||||
assert.equal(projection.representativeTime, null);
|
||||
assert.equal(projection.credibleRange, null);
|
||||
});
|
||||
|
||||
test("a corrupted receipt range outside the window still fail-closes", () => {
|
||||
const { state, latest } = producedSnapshot([
|
||||
{ time: "05:02", relativeSupport: 58 },
|
||||
{ time: "04:55", relativeSupport: 42 },
|
||||
]);
|
||||
const broken = {
|
||||
...latest,
|
||||
decisionReceipt: {
|
||||
inference_state: { ...state, credible_range: ["04:00", "04:10"] },
|
||||
},
|
||||
};
|
||||
const projection = authoritativeCandidateProjection(broken);
|
||||
assert.equal(projection.consistent, false);
|
||||
assert.deepEqual(projection.candidates, []);
|
||||
assert.equal(projection.credibleRange, null);
|
||||
});
|
||||
|
||||
test("buildCaseInferenceState receipt is the producer of the projected range", () => {
|
||||
const state = buildCaseInferenceState({
|
||||
range: { start_time: "04:55", end_time: "05:02" },
|
||||
candidates: [
|
||||
{ candidateId: "05:02", time: "05:02", relativeSupport: 58 },
|
||||
{ candidateId: "04:55", time: "04:55", relativeSupport: 42 },
|
||||
],
|
||||
evidence: [],
|
||||
probes: [],
|
||||
});
|
||||
const projection = authoritativeCandidateProjection({
|
||||
candidates: state.candidates.map((item) => ({
|
||||
candidateId: item.id,
|
||||
time: item.time,
|
||||
relativeSupport: Math.round(item.posterior_score),
|
||||
})),
|
||||
decisionReceipt: { inference_state: state },
|
||||
});
|
||||
assert.equal(projection.consistent, true);
|
||||
assert.deepEqual(projection.credibleRange, unionStillValidRange(state.candidates));
|
||||
});
|
||||
@@ -633,7 +633,7 @@ test("public candidate cards follow the inference ranking and hide an inconsiste
|
||||
assert.equal(inconsistent.selectionAllowed, false);
|
||||
assert.equal(inconsistent.can_adopt, false);
|
||||
|
||||
for (const credibleRange of [["05:00", "05:00"], ["05:07", "05:00"]] as const) {
|
||||
for (const credibleRange of [["04:00", "04:10"], ["05:07", "05:00"]] as const) {
|
||||
const rangeMismatch = overlayPublicDecision({
|
||||
...base,
|
||||
decisionReceipt: {
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
} from "../src/lib/rectification-agentic/v9/varga-observations.ts";
|
||||
import { WINDOW_SCAN_DISPLAY_LAYER_ORDER } from "../src/lib/rectification-agentic/v9/refinement-packet.ts";
|
||||
import { RECTIFICATION_SKILL_VERSION } from "../src/lib/rectification-agentic/v9/case-status.ts";
|
||||
import { buildInferenceState } from "../src/lib/rectification-agentic/core/build-state.ts";
|
||||
import { decideRectification } from "../src/lib/rectification-agentic/core/rectification-decision.ts";
|
||||
import { readVedastroMinuteSensitiveStatus } from "../src/lib/rectification-agentic/v9/confirmation-gate.ts";
|
||||
import { authoritativeCandidateProjection } from "../src/lib/rectification-agentic/v9/inference-adapter.ts";
|
||||
@@ -58,6 +59,23 @@ function collectDecision(candidates: readonly { time: string; relativeSupport?:
|
||||
});
|
||||
}
|
||||
|
||||
function producedInferenceState(
|
||||
candidates: readonly Readonly<{ id: string; time: string; relative_support: number }>[],
|
||||
) {
|
||||
const times = candidates.map((item) => item.time).sort();
|
||||
return buildInferenceState({
|
||||
range_start: times[0]!,
|
||||
range_end: times[times.length - 1]!,
|
||||
candidates: candidates.map((item) => ({
|
||||
id: item.id,
|
||||
time: item.time,
|
||||
relative_support: item.relative_support,
|
||||
})),
|
||||
events: [],
|
||||
probes: [],
|
||||
});
|
||||
}
|
||||
|
||||
const DYNAMIC_STYLE_OPTIONS = [
|
||||
{ label: "明确发生且时间吻合", answer_class: "yes" as const },
|
||||
{ label: "发生过但程度较弱", answer_class: "weak_yes" as const },
|
||||
@@ -1020,26 +1038,10 @@ test("cached candidates retry a failed VedAstro validation without recomputing r
|
||||
representativeTime: "05:02",
|
||||
evidenceLedgerFingerprint: evidenceFingerprint,
|
||||
decisionReceipt: {
|
||||
inference_state: {
|
||||
algorithm_version: "rectification-inference-v1",
|
||||
candidate_set_id: "04:55-05:02:04:55,05:02",
|
||||
revision: 1,
|
||||
phase: "discrimination",
|
||||
result_status: "credible_range",
|
||||
range_start: "04:55",
|
||||
range_end: "05:02",
|
||||
candidates: [
|
||||
{ id: CANDIDATE_ID, time: "05:02", cluster_range: ["05:02", "05:02"], prior_score: 58, posterior_score: 58, probability: 0.58, status: "active", rank: 1, strong_conflict_count: 0 },
|
||||
{ id: SECOND_CANDIDATE_ID, time: "04:55", cluster_range: ["04:55", "04:55"], prior_score: 42, posterior_score: 42, probability: 0.42, status: "active", rank: 2, strong_conflict_count: 0 },
|
||||
],
|
||||
events: [],
|
||||
probes: [],
|
||||
answered_probes: [],
|
||||
rounds: [],
|
||||
entropy: 0.98,
|
||||
representative_time: "05:02",
|
||||
credible_range: ["04:55", "05:02"],
|
||||
},
|
||||
inference_state: producedInferenceState([
|
||||
{ id: CANDIDATE_ID, time: "05:02", relative_support: 58 },
|
||||
{ id: SECOND_CANDIDATE_ID, time: "04:55", relative_support: 42 },
|
||||
]),
|
||||
gates: {
|
||||
exact_confirmation: {
|
||||
external_validation_status: "failed",
|
||||
@@ -1361,27 +1363,15 @@ test("public tool surface stays at 14 and new cases bind 10.0.13", () => {
|
||||
assert.match(tools, /current_probe: null/);
|
||||
});
|
||||
|
||||
test("Mastra hides active candidates when the receipt range excludes one of them", () => {
|
||||
test("Mastra hides active candidates when the receipt range is corrupted", () => {
|
||||
const candidates = [
|
||||
{ candidateId: CANDIDATE_ID, time: "05:00", rank: 1, relativeSupport: 20, tiedMinuteCount: 1 },
|
||||
{ candidateId: SECOND_CANDIDATE_ID, time: "05:07", rank: 2, relativeSupport: 15, tiedMinuteCount: 1 },
|
||||
];
|
||||
const inferenceState = {
|
||||
algorithm_version: "rectification-inference-v1",
|
||||
candidate_set_id: "05:00-05:07:05:00,05:07",
|
||||
revision: 2,
|
||||
phase: "discrimination",
|
||||
result_status: "credible_range",
|
||||
range_start: "05:00",
|
||||
range_end: "05:07",
|
||||
candidates: [
|
||||
{ id: CANDIDATE_ID, time: "05:00", cluster_range: ["05:00", "05:00"], prior_score: 20, posterior_score: 20, probability: 0.57, status: "active", rank: 1, strong_conflict_count: 0 },
|
||||
{ id: SECOND_CANDIDATE_ID, time: "05:07", cluster_range: ["05:07", "05:07"], prior_score: 15, posterior_score: 15, probability: 0.43, status: "active", rank: 2, strong_conflict_count: 0 },
|
||||
],
|
||||
events: [], probes: [], answered_probes: [], rounds: [], entropy: 0.98,
|
||||
representative_time: "05:00",
|
||||
credible_range: ["05:00", "05:07"],
|
||||
};
|
||||
const inferenceState = producedInferenceState([
|
||||
{ id: CANDIDATE_ID, time: "05:00", relative_support: 20 },
|
||||
{ id: SECOND_CANDIDATE_ID, time: "05:07", relative_support: 15 },
|
||||
]);
|
||||
const latest = {
|
||||
resultId: RESULT_ID,
|
||||
candidates,
|
||||
@@ -1408,7 +1398,7 @@ test("Mastra hides active candidates when the receipt range excludes one of them
|
||||
const invalid = latestResultToolProjection({
|
||||
...latest,
|
||||
decisionReceipt: {
|
||||
inference_state: { ...inferenceState, credible_range: ["05:00", "05:00"] },
|
||||
inference_state: { ...inferenceState, credible_range: ["04:00", "04:10"] },
|
||||
},
|
||||
}, session);
|
||||
assert.deepEqual(invalid.candidates, []);
|
||||
@@ -2810,26 +2800,10 @@ test("paused case with selection_allowed may offer the escape hatch", async () =
|
||||
{ candidate_id: SECOND_CANDIDATE_ID, rank: 2, time: "04:49", relative_support: 42, tied_minute_count: 2 },
|
||||
],
|
||||
decisionReceipt: {
|
||||
inference_state: {
|
||||
algorithm_version: "rectification-inference-v1",
|
||||
candidate_set_id: "04:48-04:49:04:48,04:49",
|
||||
revision: 1,
|
||||
phase: "discrimination",
|
||||
result_status: "credible_range",
|
||||
range_start: "04:48",
|
||||
range_end: "04:49",
|
||||
candidates: [
|
||||
{ id: CANDIDATE_ID, time: "04:48", cluster_range: ["04:48", "04:48"], prior_score: 58, posterior_score: 58, probability: 0.58, status: "active", rank: 1, strong_conflict_count: 0 },
|
||||
{ id: SECOND_CANDIDATE_ID, time: "04:49", cluster_range: ["04:49", "04:49"], prior_score: 42, posterior_score: 42, probability: 0.42, status: "active", rank: 2, strong_conflict_count: 0 },
|
||||
],
|
||||
events: [],
|
||||
probes: [],
|
||||
answered_probes: [],
|
||||
rounds: [],
|
||||
entropy: 0.98,
|
||||
representative_time: "04:48",
|
||||
credible_range: ["04:48", "04:49"],
|
||||
},
|
||||
inference_state: producedInferenceState([
|
||||
{ id: CANDIDATE_ID, time: "04:48", relative_support: 58 },
|
||||
{ id: SECOND_CANDIDATE_ID, time: "04:49", relative_support: 42 },
|
||||
]),
|
||||
},
|
||||
}),
|
||||
}),
|
||||
|
||||
Reference in New Issue
Block a user