3f1998bdb6
Coverage complete only unlocks discrimination. A 34/33/33 window plus an occupation note must ask a D9/D10 contrast probe instead of offering a stale winner card. Co-authored-by: Cursor <cursoragent@cursor.com>
231 lines
8.4 KiB
TypeScript
231 lines
8.4 KiB
TypeScript
import assert from "node:assert/strict";
|
|
import test from "node:test";
|
|
|
|
import {
|
|
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("D9/D10 sign differences synthesize a contrast probe when engine probes are empty", () => {
|
|
const packet = buildCandidateContrastPacket({
|
|
candidateSetVersion: "set-a",
|
|
calculationResultId: "22222222-2222-4222-8222-222222222222",
|
|
vargaDifferences: [
|
|
{ layer: "d9", signs: ["天秤", "天蝎", "射手"] },
|
|
{ layer: "d10", signs: ["巨蟹", "狮子", "处女"] },
|
|
],
|
|
});
|
|
const probe = selectDiscriminatorProbe(packet);
|
|
assert.ok(probe);
|
|
assert.ok(probe.expectedOutcomes.length >= 2);
|
|
assert.equal(probe.sourceFeatures[0]?.calculationResultId, "22222222-2222-4222-8222-222222222222");
|
|
assert.match(probe.question, /职业前事|事业盘/);
|
|
});
|
|
|
|
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, /当前推荐/);
|
|
});
|