a658ef8d2b
Coverage-complete ties stayed in discrimination because whole-window D9/D24 follow-ups were treated as probes, and restated dates inserted duplicate evidence. Skip encoded remaining layers, ask leftover D4 or offer a provisional range, and dedupe dated rows by kind and date. Co-authored-by: Cursor <cursoragent@cursor.com>
316 lines
12 KiB
TypeScript
316 lines
12 KiB
TypeScript
import assert from "node:assert/strict";
|
|
import test from "node:test";
|
|
|
|
import {
|
|
askedKeysFromLedgerEvidence,
|
|
buildCandidateContrastPacket,
|
|
selectDiscriminatorProbe,
|
|
} from "../src/lib/rectification-agentic/core/candidate-contrast-packet.ts";
|
|
import { evaluateCandidateSeparation } from "../src/lib/rectification-agentic/core/candidate-separation.ts";
|
|
import { decideNextAction } from "../src/lib/rectification-agentic/core/decide-next-action.ts";
|
|
import {
|
|
classifySnapshotStaleReason,
|
|
scoreableSnapshotIsCurrent,
|
|
snapshotIsCurrent,
|
|
type CandidateSnapshotSource,
|
|
} from "../src/lib/rectification-agentic/core/snapshot-source.ts";
|
|
import { evidenceLedgerFingerprint } from "../src/lib/rectification-agentic/v9/tool-service.ts";
|
|
import { buildSkillVerificationReport } from "../src/lib/rectification-agentic/v9/skill-verification-report.ts";
|
|
|
|
const TIED = [
|
|
{ id: "c0", time: "05:00", score: 34 },
|
|
{ id: "c1", time: "05:01", score: 33 },
|
|
{ id: "c2", time: "05:02", score: 33 },
|
|
];
|
|
|
|
const SEPARATED = [
|
|
{ id: "c0", time: "05:00", score: 62 },
|
|
{ id: "c1", time: "05:01", score: 22 },
|
|
{ id: "c2", time: "05:02", score: 16 },
|
|
];
|
|
|
|
const CONTRAST_PROBE = selectDiscriminatorProbe(buildCandidateContrastPacket({
|
|
candidateSetVersion: "05:00-05:02:05:00,05:01,05:02",
|
|
calculationResultId: "11111111-1111-4111-8111-111111111111",
|
|
engineProbes: [{
|
|
semantic_key: "career.2018.dasha_activation",
|
|
candidate_split_hash: "career:2018:05:00|05:01",
|
|
domain: "career",
|
|
year: 2018,
|
|
user_meaning: "2018 年前后是否职责明显加重?",
|
|
information_gain: 0.21,
|
|
expected_outcomes: [
|
|
{ answer_class: "yes", supports: ["05:00"], conflicts: ["05:01", "05:02"] },
|
|
{ answer_class: "no", supports: ["05:01"], conflicts: ["05:00"] },
|
|
],
|
|
}],
|
|
vargaDifferences: [{ layer: "d10", signs: ["巨蟹", "狮子", "处女"] }],
|
|
}));
|
|
|
|
function source(overrides: Partial<CandidateSnapshotSource> = {}): CandidateSnapshotSource {
|
|
return {
|
|
birthProfileFingerprint: "birth-a",
|
|
scoreableEvidenceFingerprint: "score-a",
|
|
inferenceRevision: 3,
|
|
candidateSetVersion: "set-a",
|
|
scoringPolicyVersion: "policy-v2",
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
test("does not adopt when method coverage is complete but candidates remain tied", () => {
|
|
const next = decideNextAction({
|
|
methodCoverageAll: true,
|
|
proposeAllowed: true,
|
|
candidateScores: TIED,
|
|
discriminatorProbe: CONTRAST_PROBE,
|
|
});
|
|
assert.equal(next.type, "ask_candidate_discriminator");
|
|
assert.equal(next.separation.status, "not_separated");
|
|
assert.ok((next.probe?.expectedOutcomes.length ?? 0) >= 2);
|
|
});
|
|
|
|
test("tied candidates prefer a discriminator probe with at least two predicted outcomes", () => {
|
|
const next = decideNextAction({
|
|
methodCoverageAll: true,
|
|
proposeAllowed: true,
|
|
candidateScores: TIED,
|
|
discriminatorProbe: CONTRAST_PROBE,
|
|
});
|
|
assert.equal(next.type, "ask_candidate_discriminator");
|
|
assert.ok((next.probe?.expectedOutcomes.length ?? 0) >= 2);
|
|
});
|
|
|
|
test("holdout that has not passed cannot enter ready_to_adopt", () => {
|
|
const next = decideNextAction({
|
|
methodCoverageAll: true,
|
|
proposeAllowed: true,
|
|
candidateScores: SEPARATED,
|
|
holdoutValidation: "not_started",
|
|
});
|
|
assert.equal(next.type, "ask_holdout_validation");
|
|
assert.notEqual(next.type, "ready_to_adopt");
|
|
});
|
|
|
|
test("separated candidates with holdout passed are ready to adopt", () => {
|
|
const next = decideNextAction({
|
|
methodCoverageAll: true,
|
|
proposeAllowed: true,
|
|
candidateScores: SEPARATED,
|
|
holdoutValidation: "passed",
|
|
});
|
|
assert.equal(next.type, "ready_to_adopt");
|
|
});
|
|
|
|
test("missing method coverage stays in fact collection even if scores look separated", () => {
|
|
const next = decideNextAction({
|
|
methodCoverageAll: false,
|
|
proposeAllowed: true,
|
|
candidateScores: SEPARATED,
|
|
discriminatorProbe: CONTRAST_PROBE,
|
|
});
|
|
assert.equal(next.type, "ask_fact_collection");
|
|
});
|
|
|
|
test("whole-window D9/D10 signs do not synthesize a discriminator without remaining-minute splits", () => {
|
|
const packet = buildCandidateContrastPacket({
|
|
candidateSetVersion: "set-a",
|
|
calculationResultId: "22222222-2222-4222-8222-222222222222",
|
|
vargaDifferences: [
|
|
{ layer: "d9", signs: ["天秤", "天蝎", "射手"] },
|
|
{ layer: "d10", signs: ["巨蟹", "狮子", "处女"] },
|
|
],
|
|
});
|
|
assert.equal(selectDiscriminatorProbe(packet), null);
|
|
});
|
|
|
|
test("exam quality and occupation notes skip remaining D24/D10 and keep D4", () => {
|
|
const evidence = [
|
|
{ domain: "education", eventKind: "education_interruption", summary: "高考失利复读", status: "confirmed" },
|
|
{ domain: "occupation", eventKind: "occupation_note", summary: "互联网程序员 / 前端", status: "confirmed" },
|
|
];
|
|
const packet = buildCandidateContrastPacket({
|
|
candidateSetVersion: "05:00-05:04",
|
|
calculationResultId: "22222222-2222-4222-8222-222222222222",
|
|
candidateTimes: ["05:00", "05:03", "05:04"],
|
|
transitions: [
|
|
{ layer: "d4", at: "05:00" },
|
|
{ layer: "d4", at: "05:03" },
|
|
{ layer: "d10", at: "05:00" },
|
|
{ layer: "d10", at: "05:03" },
|
|
{ layer: "d24", at: "05:00" },
|
|
{ layer: "d24", at: "05:03" },
|
|
{ layer: "d9", at: "04:52" },
|
|
{ layer: "d9", at: "05:08" },
|
|
{ layer: "d5", at: "05:15" },
|
|
],
|
|
askedKeys: askedKeysFromLedgerEvidence(evidence),
|
|
});
|
|
const probe = selectDiscriminatorProbe(packet);
|
|
assert.ok(probe);
|
|
assert.match(probe.semanticKey, /varga\.d4/);
|
|
assert.equal(probe.domain, "relocation");
|
|
assert.match(probe.question, /搬家|离乡/);
|
|
});
|
|
|
|
test("encoded D24/D10/D4 remaining splits leave no discriminator", () => {
|
|
const evidence = [
|
|
{ domain: "education", eventKind: "education_interruption", summary: "高考失利复读", status: "confirmed" },
|
|
{ domain: "occupation", eventKind: "occupation_note", summary: "互联网程序员", status: "confirmed" },
|
|
{ domain: "relocation", eventKind: "home_change", summary: "搬家离乡", status: "confirmed" },
|
|
];
|
|
const packet = buildCandidateContrastPacket({
|
|
candidateSetVersion: "05:00-05:04",
|
|
candidateTimes: ["05:00", "05:03", "05:04"],
|
|
transitions: [
|
|
{ layer: "d4", at: "05:00" },
|
|
{ layer: "d4", at: "05:03" },
|
|
{ layer: "d10", at: "05:00" },
|
|
{ layer: "d24", at: "05:00" },
|
|
],
|
|
askedKeys: askedKeysFromLedgerEvidence(evidence),
|
|
});
|
|
assert.equal(selectDiscriminatorProbe(packet), null);
|
|
});
|
|
|
|
test("remaining-candidate D24 split beats window D10 signs", () => {
|
|
const packet = buildCandidateContrastPacket({
|
|
candidateSetVersion: "05:00-05:07",
|
|
calculationResultId: "22222222-2222-4222-8222-222222222222",
|
|
vargaDifferences: [
|
|
{ layer: "d10", signs: ["巨蟹座", "狮子座", "处女座"] },
|
|
],
|
|
candidateTimes: ["05:00", "05:06", "05:07"],
|
|
transitions: [
|
|
{ layer: "d10", at: "05:00" },
|
|
{ layer: "d10", at: "05:15" },
|
|
{ layer: "d24", at: "05:00" },
|
|
{ layer: "d24", at: "05:06" },
|
|
],
|
|
askedKeys: ["education.2016", "varga.d10"],
|
|
});
|
|
const probe = selectDiscriminatorProbe(packet);
|
|
assert.ok(probe);
|
|
assert.match(probe.semanticKey, /varga\.d24/);
|
|
assert.equal(probe.domain, "education");
|
|
assert.deepEqual(probe.expectedOutcomes[0]?.supportsCandidateIds, ["05:00"]);
|
|
assert.ok(probe.expectedOutcomes[0]?.conflictsCandidateIds.includes("05:06"));
|
|
assert.ok(probe.expectedOutcomes.every((row) => (
|
|
row.supportsCandidateIds.every((id) => id.includes(":"))
|
|
)));
|
|
});
|
|
|
|
test("user stop with selection_allowed offers a provisional range", () => {
|
|
const next = decideNextAction({
|
|
methodCoverageAll: true,
|
|
proposeAllowed: true,
|
|
selectionAllowed: true,
|
|
userStopped: true,
|
|
candidateScores: TIED,
|
|
discriminatorProbe: CONTRAST_PROBE,
|
|
});
|
|
assert.equal(next.type, "offer_provisional_range");
|
|
});
|
|
|
|
test("non-scoreable occupation note does not change the scoreable evidence fingerprint", () => {
|
|
const dated = [{
|
|
id: "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaa1",
|
|
status: "confirmed" as const,
|
|
eventKind: "business_start",
|
|
domain: "career",
|
|
occurredFrom: "2026-07-19",
|
|
occurredTo: null,
|
|
datePrecision: "day" as const,
|
|
summary: "注册公司",
|
|
}];
|
|
const withNote = [...dated, {
|
|
id: "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaa2",
|
|
status: "confirmed" as const,
|
|
eventKind: "occupation_note",
|
|
domain: "occupation",
|
|
occurredFrom: null,
|
|
occurredTo: null,
|
|
datePrecision: "unknown" as const,
|
|
summary: "长期一直是程序员",
|
|
}];
|
|
const before = evidenceLedgerFingerprint(dated as never);
|
|
const after = evidenceLedgerFingerprint(withNote as never);
|
|
assert.equal(after, before);
|
|
const snapshot = source({ scoreableEvidenceFingerprint: before });
|
|
const current = source({ scoreableEvidenceFingerprint: after, inferenceRevision: snapshot.inferenceRevision });
|
|
assert.equal(snapshotIsCurrent(snapshot, current), true);
|
|
assert.equal(scoreableSnapshotIsCurrent(snapshot, current), true);
|
|
});
|
|
|
|
test("scoreable occupation evidence requires a new inference revision", () => {
|
|
const before = source();
|
|
const afterScoreable = source({
|
|
scoreableEvidenceFingerprint: "score-b",
|
|
inferenceRevision: before.inferenceRevision + 1,
|
|
});
|
|
assert.equal(classifySnapshotStaleReason(before, afterScoreable), "scoreable_evidence_changed");
|
|
assert.equal(afterScoreable.inferenceRevision, before.inferenceRevision + 1);
|
|
});
|
|
|
|
test("candidate cards must be created from the current inference and scoreable revisions", () => {
|
|
const current = source({ inferenceRevision: 4 });
|
|
const card = source({ inferenceRevision: 3 });
|
|
assert.equal(classifySnapshotStaleReason(card, current), "inference_revision_changed");
|
|
const matching = source({ inferenceRevision: 4 });
|
|
assert.equal(snapshotIsCurrent(matching, current), true);
|
|
});
|
|
|
|
test("stale reasons distinguish birth profile from scoreable evidence", () => {
|
|
const current = source();
|
|
assert.equal(
|
|
classifySnapshotStaleReason(source({ birthProfileFingerprint: "birth-b" }), current),
|
|
"birth_profile_changed",
|
|
);
|
|
assert.equal(
|
|
classifySnapshotStaleReason(source({ scoreableEvidenceFingerprint: "score-b" }), current),
|
|
"scoreable_evidence_changed",
|
|
);
|
|
assert.equal(
|
|
classifySnapshotStaleReason(source({ candidateSetVersion: "set-b" }), current),
|
|
"candidate_set_superseded",
|
|
);
|
|
});
|
|
|
|
test("34/33/33 is a tie, not a recommended winner", () => {
|
|
const separation = evaluateCandidateSeparation(TIED);
|
|
assert.equal(separation.status, "not_separated");
|
|
assert.equal(separation.sufficient, false);
|
|
assert.deepEqual(separation.credibleRange, ["05:00", "05:01", "05:02"]);
|
|
assert.equal(separation.representativeTime, "05:00");
|
|
});
|
|
|
|
test("final report does not claim executed techniques without calculationResultId", () => {
|
|
const report = buildSkillVerificationReport({
|
|
representativeTime: "05:00",
|
|
widthMinutes: 3,
|
|
candidates: [
|
|
{ time: "05:00", rank: 1, relativeSupport: 34 },
|
|
{ time: "05:01", rank: 2, relativeSupport: 33 },
|
|
{ time: "05:02", rank: 3, relativeSupport: 33 },
|
|
],
|
|
separation: evaluateCandidateSeparation(TIED),
|
|
techniqueAuditTable: [
|
|
{ technique: "D9", status: "executed", note: "no id" },
|
|
{ technique: "D10", status: "executed", note: "has id", calculation_result_id: "33333333-3333-4333-8333-333333333333" },
|
|
],
|
|
eventFitRate: {
|
|
matched: 8,
|
|
total: 9,
|
|
percent: 89,
|
|
label: "8/9",
|
|
user_meaning: "这段时间窗对已收事件有一定解释力",
|
|
unique_minute_claim: false,
|
|
},
|
|
});
|
|
assert.match(report, /基本并列/);
|
|
assert.match(report, /事件拟合程度,不是候选区分程度/);
|
|
assert.match(report, /\| D9 \| input_covered \|/);
|
|
assert.match(report, /\| D10 \| executed \|/);
|
|
assert.doesNotMatch(report, /当前推荐/);
|
|
});
|