a0ce55f066
Showing a choice card is no longer treated as completion. Distinguish probes require real candidate groups, holdout stays out of scoring, and ordinary sessions can finish with a credible range instead of an exact-minute gate. Co-authored-by: Cursor <cursoragent@cursor.com>
136 lines
5.0 KiB
TypeScript
136 lines
5.0 KiB
TypeScript
import assert from "node:assert/strict";
|
|
import test from "node:test";
|
|
|
|
import { applyProbeOutcome } from "../src/lib/rectification-agentic/core/apply-probe-outcome.ts";
|
|
import { buildInferenceState } from "../src/lib/rectification-agentic/core/build-state.ts";
|
|
import { distinguishContractErrors } from "../src/lib/rectification-agentic/core/distinguish-contract.ts";
|
|
import { probeFromEngine } from "../src/lib/rectification-agentic/core/probes-from-engine.ts";
|
|
import { parseDiscriminatingEventProbes } from "../src/lib/rectification-agentic/v9/refinement-packet.ts";
|
|
|
|
test("CI forbids distinguish probes with empty mapping or non-positive gain", () => {
|
|
assert.deepEqual(distinguishContractErrors({
|
|
role: "distinguish",
|
|
information_gain: 0,
|
|
candidate_ids: ["05:00", "05:20"],
|
|
expected_outcomes: [
|
|
{ answer_class: "yes", supports: ["05:00"], conflicts: ["05:20"] },
|
|
{ answer_class: "no", supports: ["05:20"], conflicts: ["05:00"] },
|
|
],
|
|
}), ["distinguish_non_positive_information_gain"]);
|
|
assert.deepEqual(distinguishContractErrors({
|
|
role: "distinguish",
|
|
information_gain: 0.4,
|
|
candidate_ids: [],
|
|
expected_outcomes: [
|
|
{ answer_class: "yes", supports: [], conflicts: [] },
|
|
{ answer_class: "no", supports: [], conflicts: [] },
|
|
],
|
|
}), ["distinguish_empty_candidate_ids"]);
|
|
assert.deepEqual(distinguishContractErrors({
|
|
role: "distinguish",
|
|
information_gain: 0.4,
|
|
candidate_ids: ["05:00", "05:20"],
|
|
expected_outcomes: [],
|
|
}), ["distinguish_empty_expected_outcomes"]);
|
|
});
|
|
|
|
test("receipt parser drops invalid distinguish probes and known_event_quality", () => {
|
|
const parsed = parseDiscriminatingEventProbes([
|
|
{
|
|
year: 2016,
|
|
year_label: "2016 年前后",
|
|
domain: "education",
|
|
event_family: "学业变化",
|
|
source: "known_event_quality",
|
|
tracks: ["vimshottari", "narayana"],
|
|
tracks_agree: true,
|
|
unique_minute_claim: false,
|
|
user_meaning: "clarification only",
|
|
role: "distinguish",
|
|
information_gain: 0,
|
|
},
|
|
{
|
|
year: 2018,
|
|
year_label: "2018 年前后",
|
|
domain: "career",
|
|
event_family: "职责变化",
|
|
source: "dasha_activation",
|
|
tracks: ["vimshottari", "narayana"],
|
|
tracks_agree: true,
|
|
unique_minute_claim: false,
|
|
user_meaning: "engine locked year and family",
|
|
role: "distinguish",
|
|
information_gain: 0.4,
|
|
candidate_ids: ["05:00", "05:20"],
|
|
expected_outcomes: [
|
|
{ answer_class: "yes", supports: ["05:00"], conflicts: ["05:20"] },
|
|
{ answer_class: "no", supports: ["05:20"], conflicts: ["05:00"] },
|
|
],
|
|
},
|
|
]);
|
|
assert.equal(parsed.length, 1);
|
|
assert.equal(parsed[0]?.domain, "career");
|
|
assert.equal(parsed[0]?.source, "dasha_activation");
|
|
assert.ok((parsed[0]?.information_gain ?? 0) > 0);
|
|
});
|
|
|
|
test("randomized hidden mutated answers change posterior only when mapped", () => {
|
|
const probe = probeFromEngine({
|
|
year: 2018,
|
|
year_label: "2018 年前后",
|
|
domain: "career",
|
|
event_family: "职责变化",
|
|
source: "dasha_activation",
|
|
tracks: ["vimshottari", "narayana"],
|
|
tracks_agree: true,
|
|
unique_minute_claim: false,
|
|
user_meaning: "engine locked year and family",
|
|
role: "distinguish",
|
|
information_gain: 0.4,
|
|
semantic_key: "career.2018",
|
|
candidate_split_hash: "set:career:2018",
|
|
candidate_ids: ["05:00", "05:20"],
|
|
expected_outcomes: [
|
|
{ answer_class: "yes", supports: ["05:00"], conflicts: ["05:20"] },
|
|
{ answer_class: "no", supports: ["05:20"], conflicts: ["05:00"] },
|
|
{ answer_class: "unsure", supports: [], conflicts: [] },
|
|
],
|
|
});
|
|
assert.ok(probe);
|
|
const scores = { "05:00": 10, "05:20": 10 };
|
|
const yes = applyProbeOutcome(scores, probe, "yes");
|
|
assert.equal(yes.kind, "informative");
|
|
assert.notDeepEqual(yes.scores, scores);
|
|
assert.ok(yes.deltas["05:00"] !== 0);
|
|
const mutated = applyProbeOutcome(scores, probe, "unsure");
|
|
assert.equal(mutated.kind, "low_information");
|
|
assert.deepEqual(mutated.scores, scores);
|
|
const state = buildInferenceState({
|
|
range_start: "05:00",
|
|
range_end: "05:20",
|
|
candidates: [
|
|
{ id: "05:00", time: "05:00", relative_support: 10 },
|
|
{ id: "05:20", time: "05:20", relative_support: 10 },
|
|
],
|
|
events: [
|
|
{ id: "e1", domain: "education", year: 2016, precision: "month" },
|
|
{ id: "e2", domain: "career", year: 2018, precision: "year" },
|
|
{ id: "e3", domain: "family", year: 2020, precision: "year" },
|
|
],
|
|
probes: [probe],
|
|
answered_probes: [{
|
|
probe_id: probe.id,
|
|
semantic_key: probe.semantic_key,
|
|
candidate_split_hash: probe.candidate_split_hash,
|
|
answer_class: "unsure",
|
|
classified_from: "choice",
|
|
}],
|
|
});
|
|
assert.equal(state.last_inference_round?.kind, "low_information");
|
|
assert.deepEqual(state.last_inference_round?.scores_before, state.last_inference_round?.scores_after);
|
|
assert.equal(
|
|
state.rounds.filter((item) => item.kind === "informative").length,
|
|
0,
|
|
);
|
|
});
|