fix(web): stop adopting tied rectification candidates after method coverage
Independent Staging Quality Gate / validate (push) Has been cancelled
Independent Staging Quality Gate / publish (push) Has been cancelled

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>
This commit is contained in:
Jesse_Chen
2026-08-24 12:19:36 +08:00
parent 3b785b821d
commit 3f1998bdb6
19 changed files with 1345 additions and 80 deletions
@@ -44,12 +44,17 @@ test("low-confidence near ties never receive a recommendation badge", () => {
assert.equal(isRecommendedRectificationCandidate(result, result.candidates[0]!), false);
});
test("only an unselected representative receives a recommendation badge", () => {
test("only an unselected separated representative receives a recommendation badge", () => {
const result = parseRectificationCandidateResult({
...camelCaseSnapshot,
overallConfidence: "medium",
confirmationAllowed: false,
representativeTime: "05:07",
candidates: [
{ candidateId: CANDIDATE_ID, rank: 1, time: "05:07", relativeSupport: 62, tiedMinuteCount: 1 },
{ candidateId: SECOND_CANDIDATE_ID, rank: 2, time: "05:08", relativeSupport: 22, tiedMinuteCount: 1 },
{ candidateId: THIRD_CANDIDATE_ID, rank: 3, time: "05:09", relativeSupport: 16, tiedMinuteCount: 1 },
],
});
assert.ok(result);
@@ -374,7 +374,7 @@ test("distinguish follow-up copy forbids competing-chart ranking", () => {
assert.match(plan.next_followup?.user_prompt_hint ?? "", /不得发明年份/);
});
test("GET A/B card stays hidden once representative time can be offered", () => {
test("GET A/B card stays visible after coverage when candidates remain tied", () => {
const card = projectRectificationChoiceCard({
evidence: [{
status: "confirmed",
@@ -411,6 +411,11 @@ test("GET A/B card stays hidden once representative time can be offered", () =>
eventProbes: [MOVE_PROBE],
selectionAllowed: true,
proposeAllowed: true,
candidateScores: [
{ time: "05:00", score: 34 },
{ time: "05:01", score: 33 },
{ time: "05:02", score: 33 },
],
activeFocus: {
intent: "distinguish_candidates",
targetDomain: "relocation",
@@ -418,6 +423,54 @@ test("GET A/B card stays hidden once representative time can be offered", () =>
expectedAnswerSchema: { choice: SAMPLE_COPY },
},
});
assert.ok(card);
assert.equal(card.prompt, SAMPLE_COPY.prompt);
});
test("GET A/B card stays hidden once candidates are separated and ready to offer", () => {
const card = projectRectificationChoiceCard({
evidence: [{
status: "confirmed",
domain: "education",
datePrecision: "year",
occurredFrom: "2016-01-01",
occurredTo: null,
}, {
status: "confirmed",
domain: "relationship",
datePrecision: "year",
occurredFrom: "2018-01-01",
occurredTo: null,
}, {
status: "confirmed",
domain: "career",
datePrecision: "year",
occurredFrom: "2019-01-01",
occurredTo: null,
}, {
status: "confirmed",
domain: "family",
datePrecision: "year",
occurredFrom: "2020-01-01",
occurredTo: null,
}, {
status: "draft",
domain: "occupation",
datePrecision: "unknown",
occurredFrom: null,
occurredTo: null,
eventKind: "occupation_note",
}],
eventProbes: [MOVE_PROBE],
selectionAllowed: true,
proposeAllowed: true,
holdoutValidation: "passed",
candidateScores: [
{ time: "05:00", score: 62 },
{ time: "05:01", score: 22 },
{ time: "05:02", score: 16 },
],
});
assert.equal(card, null);
});
@@ -0,0 +1,230 @@
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, /当前推荐/);
});
+205 -12
View File
@@ -812,21 +812,27 @@ test("precision stage lagna_frame waits for uncovered career before asking anoth
assert.doesNotMatch(JSON.stringify(plan), UNIQUE_MINUTE_COPY);
});
test("lagna_frame after classic coverage does not keep blocking representative time cards", () => {
test("lagna_frame after classic coverage still discriminates when candidates are tied", () => {
const plan = buildMethodFollowupPlan({
evidence: CLASSIC_COVERAGE,
precisionStage: "lagna_frame",
});
assert.equal(plan.next_followup?.source, "precision_stage");
assert.equal(plan.next_followup?.ask_theme, "dated_event");
assert.equal(isOfferBlockingFollowup(plan.next_followup, plan.methods), false);
assert.equal(isOfferBlockingFollowup(plan.next_followup, plan.methods), true);
assert.equal(isOfferBlockingFollowup(plan.next_followup, plan.methods, { separated: true }), false);
assert.equal(conversationalSessionOutcome({
selectionAllowed: true,
proposeAllowed: true,
confirmationAllowed: false,
nextFollowup: plan.next_followup,
methods: plan.methods,
}), "adopt_representative");
candidateScores: [
{ time: "05:00", score: 34 },
{ time: "05:01", score: 33 },
{ time: "05:02", score: 33 },
],
}), "discriminate_candidates");
});
test("precision stage d4 asks home change not family, and d5 asks education", () => {
@@ -1007,7 +1013,7 @@ test("career evidence does not cover occupation; occupation still blocks until a
}), "collect_evidence");
});
test("draft occupation_note without a date covers occupation and unblocks offering", () => {
test("draft occupation_note without a date covers occupation and does not adopt a tie", () => {
const plan = buildMethodFollowupPlan({
evidence: [
{ status: "confirmed", domain: "education", datePrecision: "year", occurredFrom: "2016-01-01", occurredTo: null },
@@ -1027,12 +1033,17 @@ test("draft occupation_note without a date covers occupation and unblocks offeri
});
assert.equal(plan.methods.find((item) => item.method_id === "occupation")?.status, "covered");
assert.notEqual(plan.next_followup?.method_id, "occupation");
assert.equal(conversationalSessionOutcome({
assert.notEqual(conversationalSessionOutcome({
selectionAllowed: true,
proposeAllowed: true,
confirmationAllowed: false,
nextFollowup: plan.next_followup,
methods: plan.methods,
candidateScores: [
{ time: "05:00", score: 34 },
{ time: "05:01", score: 33 },
{ time: "05:02", score: 33 },
],
}), "adopt_representative");
});
@@ -1059,15 +1070,76 @@ test("stale occupation collect focus does not keep interviewing after occupation
targetKind: "occupation_note",
},
});
assert.notEqual(plan.next_followup?.method_id, "active_focus");
assert.notEqual(plan.next_followup?.method_id, "occupation");
assert.notEqual(conversationalSessionOutcome({
selectionAllowed: true,
proposeAllowed: true,
confirmationAllowed: false,
nextFollowup: plan.next_followup,
methods: plan.methods,
candidateScores: [
{ time: "05:00", score: 34 },
{ time: "05:01", score: 33 },
{ time: "05:02", score: 33 },
],
}), "adopt_representative");
});
test("D9/D10 contrast after occupation coverage asks a discriminator, not adopt", () => {
const plan = buildMethodFollowupPlan({
evidence: CLASSIC_COVERAGE.filter((item) => item.domain !== "horary"),
contrastPacket: {
candidateSetVersion: "05:00-05:02",
vargaDifferences: [
{ layer: "d9", signs: ["天秤", "天蝎", "射手"] },
{ layer: "d10", signs: ["巨蟹", "狮子", "处女"] },
],
probes: [{
probeId: "contrast:varga.d10.巨蟹|狮子|处女",
candidateSetVersion: "05:00-05:02",
question: "当前几个候选在事业盘上还分得开。请核对一段还没用进评分的职业前事。",
expectedOutcomes: [
{ outcomeId: "supports_巨蟹", supportsCandidateIds: ["巨蟹"], conflictsCandidateIds: ["狮子", "处女"] },
{ outcomeId: "supports_狮子", supportsCandidateIds: ["狮子"], conflictsCandidateIds: ["巨蟹", "处女"] },
{ outcomeId: "supports_处女", supportsCandidateIds: ["处女"], conflictsCandidateIds: ["巨蟹", "狮子"] },
],
candidateSplitHash: "varga.d10.巨蟹|狮子|处女",
informationGain: 0.12,
sourceFeatures: [{ technique: "D10", calculationResultId: RESULT_ID }],
domain: "career",
year: null,
semanticKey: "varga.d10.巨蟹|狮子|处女",
}],
},
});
assert.equal(plan.next_followup?.intent, "distinguish_candidates");
assert.match(plan.next_followup?.user_prompt_hint ?? "", /职业前事|事业盘/);
assert.equal(conversationalSessionOutcome({
selectionAllowed: true,
proposeAllowed: true,
confirmationAllowed: false,
nextFollowup: plan.next_followup,
methods: plan.methods,
}), "adopt_representative");
discriminatorProbe: {
probeId: "contrast:varga.d10",
candidateSetVersion: "05:00-05:02",
question: "核对一段还没用进评分的职业前事",
expectedOutcomes: [
{ outcomeId: "a", supportsCandidateIds: ["05:00"], conflictsCandidateIds: ["05:01"] },
{ outcomeId: "b", supportsCandidateIds: ["05:01"], conflictsCandidateIds: ["05:00"] },
],
candidateSplitHash: "varga.d10",
informationGain: 0.12,
sourceFeatures: [{ technique: "D10", calculationResultId: RESULT_ID }],
domain: "career",
year: null,
semanticKey: "varga.d10",
},
candidateScores: [
{ time: "05:00", score: 34 },
{ time: "05:01", score: 33 },
{ time: "05:02", score: 33 },
],
}), "discriminate_candidates");
});
@@ -1125,10 +1197,10 @@ test("high information_gain leftover probe still blocks offering after coverage"
confirmationAllowed: false,
nextFollowup: plan.next_followup,
methods: plan.methods,
}), "collect_evidence");
}), "discriminate_candidates");
});
test("event_probe does not block offering once blocking methods are covered", () => {
test("event_probe still discriminates after coverage when candidates remain tied", () => {
const plan = buildMethodFollowupPlan({
evidence: CLASSIC_COVERAGE.filter((item) => item.domain !== "horary"),
eventProbes: [{
@@ -1144,14 +1216,20 @@ test("event_probe does not block offering once blocking methods are covered", ()
role: "reverse_verify",
}],
});
assert.equal(isOfferBlockingFollowup(plan.next_followup, plan.methods), false);
assert.equal(plan.next_followup?.source, "event_probe");
assert.equal(isOfferBlockingFollowup(plan.next_followup, plan.methods), true);
assert.equal(conversationalSessionOutcome({
selectionAllowed: true,
proposeAllowed: true,
confirmationAllowed: false,
nextFollowup: plan.next_followup,
methods: plan.methods,
}), "adopt_representative");
candidateScores: [
{ time: "05:00", score: 34 },
{ time: "05:01", score: 33 },
{ time: "05:02", score: 33 },
],
}), "discriminate_candidates");
});
test("accepted time reverse-verifies an uncovered year in a covered domain", () => {
@@ -1206,6 +1284,12 @@ test("declining occupation covers the method; declining horary is skipped_by_pol
confirmationAllowed: false,
nextFollowup: plan.next_followup,
methods: plan.methods,
candidateScores: [
{ time: "05:00", score: 62 },
{ time: "05:01", score: 22 },
{ time: "05:02", score: 16 },
],
holdoutValidation: "passed",
}), "adopt_representative");
});
@@ -1214,12 +1298,19 @@ test("horary follow-up does not block propose once occupation is covered", () =>
evidence: CLASSIC_COVERAGE.filter((item) => item.domain !== "horary"),
});
assert.equal(plan.next_followup?.method_id, "horary");
assert.equal(isOfferBlockingFollowup(plan.next_followup, plan.methods), false);
assert.equal(conversationalSessionOutcome({
selectionAllowed: true,
proposeAllowed: true,
confirmationAllowed: false,
nextFollowup: plan.next_followup,
methods: plan.methods,
candidateScores: [
{ time: "05:00", score: 62 },
{ time: "05:01", score: 22 },
{ time: "05:02", score: 16 },
],
holdoutValidation: "passed",
}), "adopt_representative");
});
@@ -1275,6 +1366,7 @@ test("user stop with selection_allowed may offer the escape hatch", async () =>
latestResult: candidateSnapshotFixture({
selectionAllowed: true,
representativeTime: "04:48",
evidenceLedgerFingerprint: null,
candidates: [
{ candidate_id: CANDIDATE_ID, rank: 1, time: "04:48", relative_support: 58, tied_minute_count: 1 },
{ candidate_id: SECOND_CANDIDATE_ID, rank: 2, time: "04:49", relative_support: 42, tied_minute_count: 2 },
@@ -1306,3 +1398,104 @@ test("user stop with selection_allowed may offer the escape hatch", async () =>
);
});
test("offer-candidates refuses a 34/33/33 tie after method coverage", async () => {
const accounting = fakeAccounting({
...receiptHandlers,
get_agentic_rectification_case_dossier: () => dossierFixture({
evidence: [
{ ...educationEvidence, domain: "education" },
{
id: "44444444-4444-4444-8444-444444444442",
source_turn_id: TURN_ID,
subject: "self",
event_kind: "relationship_start",
domain: "relationship",
occurred_from: "2018-01-01",
occurred_to: null,
date_precision: "year",
summary: "感情变化",
status: "confirmed",
supersedes_evidence_id: null,
created_at: "2026-08-12T10:00:07.000Z",
},
{
id: "44444444-4444-4444-8444-444444444443",
source_turn_id: TURN_ID,
subject: "self",
event_kind: "career_entry",
domain: "career",
occurred_from: "2019-01-01",
occurred_to: null,
date_precision: "year",
summary: "工作变化",
status: "confirmed",
supersedes_evidence_id: null,
created_at: "2026-08-12T10:00:08.000Z",
},
{
id: "44444444-4444-4444-8444-444444444446",
source_turn_id: TURN_ID,
subject: "self",
event_kind: "family_event",
domain: "family",
occurred_from: "2020-01-01",
occurred_to: null,
date_precision: "year",
summary: "家人变化",
status: "confirmed",
supersedes_evidence_id: null,
created_at: "2026-08-12T10:00:09.000Z",
},
{
id: "44444444-4444-4444-8444-444444444445",
source_turn_id: TURN_ID,
subject: "self",
event_kind: "occupation_note",
domain: "occupation",
occurred_from: null,
occurred_to: null,
date_precision: "unknown",
summary: "长期一直是程序员",
status: "confirmed",
supersedes_evidence_id: null,
created_at: "2026-08-12T10:00:10.000Z",
},
],
latestResult: candidateSnapshotFixture({
selectionAllowed: true,
representativeTime: "05:00",
evidenceLedgerFingerprint: null,
decisionReceipt: {
propose_allowed: true,
window_scan: {
scanned: true,
d9_lagna_count: 3,
d10_lagna_count: 3,
d9_candidates_differ: true,
d10_candidates_differ: true,
d9_sign_names: ["天秤", "天蝎", "射手"],
d10_sign_names: ["巨蟹", "狮子", "处女"],
},
},
candidates: [
{ candidate_id: CANDIDATE_ID, rank: 1, time: "05:00", relative_support: 34, tied_minute_count: 1 },
{ candidate_id: SECOND_CANDIDATE_ID, rank: 2, time: "05:01", relative_support: 33, tied_minute_count: 1 },
{ candidate_id: THIRD_CANDIDATE_ID, rank: 3, time: "05:02", relative_support: 33, tied_minute_count: 1 },
],
}),
}),
});
const tools = createRectificationV9Tools({
userId: USER_ID,
caseId: CASE_ID,
turnId: TURN_ID,
accounting: accounting.client as never,
});
await assert.rejects(
() => (tools["rectification-offer-candidates"] as unknown as {
execute(input: unknown): Promise<unknown>;
}).execute({ caseId: CASE_ID }),
(error: unknown) => error instanceof RectificationToolServiceError && error.code === "offer_not_allowed",
);
});
@@ -71,7 +71,9 @@ test("system prompt carries only high-priority boundaries, never the method copy
assert.match(prompt, /工具执行过程保持静默/);
assert.match(prompt, /思考过程必须用简体中文/);
assert.match(prompt, /skill_verification_report/);
assert.match(prompt, /精度阶段追问不挡出牌/);
assert.match(prompt, /ask_candidate_discriminator/);
assert.match(prompt, /方法覆盖已齐只进入候选区分/);
assert.doesNotMatch(prompt, /方法覆盖已齐且 propose_allowed 时本轮 adopt/);
assert.match(prompt, /不得询问外貌、体质、胎记或疤痕/);
assert.match(prompt, /expectedAnswerSchema.choice/);
assert.match(prompt, /不要写 choice/);
@@ -226,6 +226,8 @@ export function candidateSnapshotFixture(overrides: {
selectedTime?: string | null;
selectionKind?: string | null;
candidates?: unknown[];
evidenceLedgerFingerprint?: string | null;
decisionReceipt?: Record<string, unknown>;
} = {}) {
return {
result_id: RESULT_ID,
@@ -244,12 +246,15 @@ export function candidateSnapshotFixture(overrides: {
confirmation_allowed: overrides.confirmationAllowed ?? false,
representative_candidate_id: overrides.representativeTime ? CANDIDATE_ID : null,
overall_confidence: "medium",
...overrides.decisionReceipt,
},
execution_ledger: [{ method: "d1-rashi", status: "executed" }],
representative_time: overrides.representativeTime ?? null,
selected_time: overrides.selectedTime ?? null,
selection_kind: overrides.selectionKind ?? null,
evidence_ledger_fingerprint: "b".repeat(64),
evidence_ledger_fingerprint: overrides.evidenceLedgerFingerprint === undefined
? "b".repeat(64)
: overrides.evidenceLedgerFingerprint,
candidate_range_fingerprint: "c".repeat(64),
skill_version: "9.0.0",
algorithm_version: "rectification-v5",