fix(rectification): remove static interview fallbacks
Independent Staging Quality Gate / validate (push) Successful in 10m50s
Independent Staging Quality Gate / publish (push) Successful in 13m29s

This commit is contained in:
Jesse_Chen
2026-08-27 16:40:47 +08:00
parent 5f855c610c
commit f3327235ea
43 changed files with 1798 additions and 509 deletions
+229 -28
View File
@@ -2,7 +2,7 @@ import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import test from "node:test";
import { buildMethodFollowupPlan, buildNextUserAction, conversationalSessionOutcome, isOfferBlockingFollowup, latestUserStoppedCollecting } from "../src/lib/rectification-agentic/v9/method-followup.ts";
import { buildMethodFollowupPlan, buildNextUserAction, conversationalSessionOutcome, isOfferBlockingFollowup } from "../src/lib/rectification-agentic/v9/method-followup.ts";
import { trainingScoreableGate } from "../src/lib/rectification-agentic/v9/evidence-model.ts";
import {
askedKeysFromLedgerEvidence,
@@ -14,9 +14,16 @@ 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 { readVedastroMinuteSensitiveStatus } from "../src/lib/rectification-agentic/v9/confirmation-gate.ts";
import { authoritativeCandidateProjection } from "../src/lib/rectification-agentic/v9/inference-adapter.ts";
import { createRectificationV9Tools, latestResultToolProjection } from "../src/mastra/rectification-v9-tools.ts";
import { PUBLIC_RECTIFICATION_TOOLS } from "../src/lib/rectification-agentic/v9/public-receipt.ts";
import { RectificationToolServiceError } from "../src/lib/rectification-agentic/v9/tool-service.ts";
import {
candidateRangeFingerprint,
evidenceLedgerFingerprint,
parseV9CaseDossier,
RectificationToolServiceError,
} from "../src/lib/rectification-agentic/v9/tool-service.ts";
import {
resolveExactSkillPackage,
} from "../src/lib/skill-package-registry.ts";
@@ -42,6 +49,13 @@ const FAMILY_ID = "44444444-4444-4444-8444-444444444443";
const CAREER_ID = "44444444-4444-4444-8444-444444444444";
const UNIQUE_MINUTE_COPY = /±5 分钟确定性/;
const DYNAMIC_STYLE_OPTIONS = [
{ label: "明确发生且时间吻合", answer_class: "yes" as const },
{ label: "发生过但程度较弱", answer_class: "weak_yes" as const },
{ label: "明确没有发生", answer_class: "no" as const },
{ label: "不记得这段经历", answer_class: "unsure" as const },
];
const CLASSIC_COVERAGE = [
{ status: "confirmed", domain: "education", datePrecision: "year" as const, occurredFrom: "2016-01-01", occurredTo: null },
{ status: "confirmed", domain: "relationship", datePrecision: "year" as const, occurredFrom: "2018-01-01", occurredTo: null },
@@ -94,6 +108,7 @@ const CAREER_CONFLICT_PROBE = {
{ answer_class: "yes", supports: ["05:00"], conflicts: ["05:20"] },
{ answer_class: "no", supports: ["05:20"], conflicts: ["05:00"] },
],
style_options: DYNAMIC_STYLE_OPTIONS,
};
const EDUCATION_QUALITY_PROBE = {
@@ -111,6 +126,7 @@ const EDUCATION_QUALITY_PROBE = {
choice_kind: "event_quality" as const,
information_gain: 0,
semantic_key: "education.2016",
style_options: DYNAMIC_STYLE_OPTIONS,
};
const CAREER_QUALITY_PROBE = {
@@ -128,6 +144,7 @@ const CAREER_QUALITY_PROBE = {
choice_kind: "event_quality" as const,
information_gain: 0,
semantic_key: "career.2020",
style_options: DYNAMIC_STYLE_OPTIONS,
};
const ENGINE_SCORE = {
@@ -866,6 +883,172 @@ test("accepted batch evidence triggers server rescore without offering adoption"
}
});
test("cached candidates retry a failed VedAstro validation without recomputing ranking", async () => {
const validationResponse = {
status: "passed",
can_confirm_exact_minute: true,
event_validation: {
search_events_primary_supports_local_winner: true,
},
minute_sensitive_validation: {
status: "passed",
},
};
const restore = stubEngine(validationResponse);
try {
const compute = computeFixture();
const evidence = [educationEvidence];
const evidenceFingerprint = evidenceLedgerFingerprint([{
id: educationEvidence.id,
sourceTurnId: educationEvidence.source_turn_id,
subject: educationEvidence.subject,
eventKind: educationEvidence.event_kind,
domain: educationEvidence.domain,
occurredFrom: educationEvidence.occurred_from,
occurredTo: educationEvidence.occurred_to,
datePrecision: educationEvidence.date_precision,
summary: educationEvidence.summary,
status: educationEvidence.status,
supersedesEvidenceId: educationEvidence.supersedes_evidence_id,
createdAt: educationEvidence.created_at,
}]);
const rangeFingerprint = candidateRangeFingerprint(
compute.candidate_range,
compute.baseline_profile_fingerprint,
);
const cached = {
...candidateSnapshotFixture({
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"],
},
gates: {
exact_confirmation: {
external_validation_status: "failed",
vedastro_event_validation: {
status: "failed",
search_events_primary_supports_local_winner: false,
can_confirm_exact_minute: false,
failure: { code: "timeout" },
},
},
},
},
}),
candidate_range_fingerprint: rangeFingerprint,
};
const refreshedReceipt = {
...cached.decision_receipt,
gates: {
exact_confirmation: {
external_validation_status: "passed",
vedastro_event_validation: {
status: "passed",
search_events_primary_supports_local_winner: true,
can_confirm_exact_minute: true,
failure: null,
},
},
},
};
const parsedCached = parseV9CaseDossier(dossierFixture({ evidence, latestResult: cached }));
assert.equal(parsedCached?.latestResult?.selectionAllowed, true);
assert.equal(parsedCached?.latestResult?.evidenceLedgerFingerprint, evidenceFingerprint);
assert.equal(parsedCached?.latestResult?.candidateRangeFingerprint, rangeFingerprint);
assert.equal(readVedastroMinuteSensitiveStatus(parsedCached?.latestResult?.decisionReceipt), "failed");
assert.equal(authoritativeCandidateProjection(parsedCached!.latestResult!).candidates.length, 2);
const accounting = fakeAccounting({
...receiptHandlers,
get_agentic_rectification_case_dossier: () => dossierFixture({
evidence,
latestResult: cached,
}),
get_agentic_rectification_case_compute: () => compute,
refresh_agentic_rectification_vedastro_validation: () => ({
result_id: RESULT_ID,
decision_receipt: refreshedReceipt,
}),
set_agentic_rectification_conversation_focus: (_fn, args) => ({
focus: {
id: FOCUS_ID,
case_id: CASE_ID,
question_id: args.p_question_id,
intent: args.p_intent,
target_evidence_id: args.p_target_evidence_id,
target_domain: args.p_target_domain,
target_kind: args.p_target_kind,
expected_answer_schema: args.p_expected_answer_schema,
status: "active",
asked_at: "2026-08-27T00:00:00.000Z",
resolved_at: null,
},
idempotent: false,
}),
});
const tools = createRectificationV9Tools({
userId: USER_ID,
caseId: CASE_ID,
turnId: TURN_ID,
userMessage: "继续比较",
accounting: accounting.client as never,
});
const result = await (tools["rectification-compare-candidates"] as unknown as {
execute(input: unknown): Promise<{
cached: boolean;
candidates: Array<{ time: string; relativeSupport: number }>;
confirmation_gate: { blockers: Array<{ id: string; status: string }> };
}>;
}).execute({ caseId: CASE_ID });
const refreshCall = accounting.calls.find((call) =>
call.fn === "refresh_agentic_rectification_vedastro_validation"
);
assert.ok(refreshCall, JSON.stringify(accounting.calls.map((call) => call.fn)));
assert.deepEqual(refreshCall.args.p_validation, {
status: "passed",
search_events_primary_supports_local_winner: true,
can_confirm_exact_minute: true,
failure: null,
});
assert.equal(refreshCall.args.p_minute_sensitive_status, "passed");
assert.equal(
accounting.calls.some((call) => call.fn === "persist_agentic_rectification_candidate_v2"),
false,
);
assert.equal(result.cached, true);
assert.deepEqual(result.candidates.map((item) => [item.time, item.relativeSupport]), [
["05:02", 58],
["04:55", 42],
]);
assert.ok(result.confirmation_gate, JSON.stringify(result));
assert.equal(
result.confirmation_gate.blockers.find((gate) => gate.id === "vedastro_minute_sensitive")?.status,
"passed",
);
} finally {
restore();
}
});
test("evidence batch returns the persisted choice prompt as open_question", async () => {
const restore = stubEngine({
...ENGINE_SCORE,
@@ -892,6 +1075,7 @@ test("evidence batch returns the persisted choice prompt as open_question", asyn
{ answer_class: "yes", supports: ["04:50"], conflicts: ["05:20"] },
{ answer_class: "no", supports: ["05:20"], conflicts: ["04:50"] },
],
style_options: DYNAMIC_STYLE_OPTIONS,
choice_kind: "existence",
}],
},
@@ -1033,9 +1217,9 @@ test("rescore failure does not fail the evidence write", async () => {
assert.ok(result.rescore.error_code);
});
test("public tool surface stays at 13 and new cases bind 10.0.12", () => {
assert.equal(PUBLIC_RECTIFICATION_TOOLS.length, 13);
assert.equal(RECTIFICATION_SKILL_VERSION, "10.0.12");
test("public tool surface stays at 14 and new cases bind 10.0.13", () => {
assert.equal(PUBLIC_RECTIFICATION_TOOLS.length, 14);
assert.equal(RECTIFICATION_SKILL_VERSION, "10.0.13");
const deprecated = resolveExactSkillPackage(
"jyotish-birth-time-rectification",
"10.0.2",
@@ -1253,6 +1437,7 @@ test("accepted representative time reverse-verifies predicted events then hands
unique_minute_claim: false as const,
user_meaning: "年份锁定 2018 年前后。请写成一句自然语言,问是否入职或职责加重。",
role: "reverse_verify" as const,
style_options: DYNAMIC_STYLE_OPTIONS,
};
const plan = buildMethodFollowupPlan({
evidence: [{
@@ -1287,7 +1472,7 @@ test("accepted representative time reverse-verifies predicted events then hands
assert.equal(plan.next_followup?.method_id, "reverse_verify");
assert.equal(plan.next_followup?.domain, "career");
assert.equal(plan.next_followup?.choice_frame?.scoring, true);
assert.match(plan.next_followup?.choice_frame?.why ?? "", /按当前采用时间核对/);
assert.match(plan.next_followup?.choice_frame?.why ?? "", /2018 年前后/);
assert.equal(plan.deferred_followup, null);
assert.equal(plan.next_followup?.source, "reverse_verify");
const action = buildNextUserAction({
@@ -1770,18 +1955,7 @@ test("coverage-complete tie with no remaining split offers a provisional range",
assert.equal(action.id, "offer_provisional_range");
});
test("only explicit rectification exit language is a global user stop", () => {
assert.equal(latestUserStoppedCollecting([{ role: "user", text: "没有" }]), false);
assert.equal(latestUserStoppedCollecting([{ role: "user", text: "没有了" }]), false);
assert.equal(latestUserStoppedCollecting([{ role: "user", text: "想不起来了" }]), false);
assert.equal(latestUserStoppedCollecting([{ role: "user", text: "结束校正" }]), true);
assert.equal(latestUserStoppedCollecting([{ role: "user", text: "我不想继续了" }]), true);
assert.equal(latestUserStoppedCollecting([{ role: "user", text: "直接给结果吧" }]), true);
assert.equal(latestUserStoppedCollecting([{ role: "user", text: "就到这里" }]), true);
assert.equal(latestUserStoppedCollecting([{ role: "user", text: "我不是不想继续,只是想先补工作" }]), false);
assert.equal(latestUserStoppedCollecting([{ role: "user", text: "不要直接给结果,我还想补一件事" }]), false);
assert.equal(latestUserStoppedCollecting([{ role: "user", text: "还没到这里结束,我继续说" }]), false);
assert.equal(latestUserStoppedCollecting([{ role: "user", text: "还有一件升学" }]), false);
test("structured paused state ends evidence collection without parsing user copy", () => {
assert.equal(conversationalSessionOutcome({
selectionAllowed: true,
proposeAllowed: true,
@@ -2015,6 +2189,7 @@ test("accepted time reverse-verifies an uncovered year in a covered domain", ()
unique_minute_claim: false,
user_meaning: "年份锁定 2018 年前后。请写成一句自然语言,问是否入职或职责加重。",
role: "reverse_verify",
style_options: DYNAMIC_STYLE_OPTIONS,
}],
});
assert.equal(plan.next_followup?.source, "reverse_verify");
@@ -2112,19 +2287,12 @@ test("offer-candidates refuses while method coverage remains", async () => {
);
});
test("user stop with selection_allowed may offer the escape hatch", async () => {
test("paused case with selection_allowed may offer the escape hatch", async () => {
const accounting = fakeAccounting({
...receiptHandlers,
get_agentic_rectification_case_dossier: () => dossierFixture({
status: "paused",
evidence: [educationEvidence],
turns: [{
id: TURN_ID,
role: "user",
text: "结束校正",
status: "completed",
created_at: "2026-08-12T10:00:00.000Z",
completed_at: "2026-08-12T10:00:05.000Z",
}],
latestResult: candidateSnapshotFixture({
selectionAllowed: true,
representativeTime: "04:48",
@@ -2178,6 +2346,40 @@ test("user stop with selection_allowed may offer the escape hatch", async () =>
call.fn === "transition_agentic_rectification_case_status"
&& call.args.p_status === "candidate_ready"
),
false,
);
});
test("paused case resumes only when the Agent explicitly requests it", async () => {
let reads = 0;
const accounting = fakeAccounting({
...receiptHandlers,
get_agentic_rectification_case_dossier: () => dossierFixture({
status: reads++ === 0 ? "paused" : "collecting_evidence",
evidence: [],
}),
transition_agentic_rectification_case_status: (_fn, args) => ({
case_id: CASE_ID,
status: args.p_status,
idempotent: false,
}),
});
const tools = createRectificationV9Tools({
userId: USER_ID,
caseId: CASE_ID,
turnId: TURN_ID,
userMessage: "继续校正",
accounting: accounting.client as never,
});
const projection = await (tools["rectification-read-case"] as unknown as {
execute(input: unknown): Promise<{ status: string }>;
}).execute({ caseId: CASE_ID, resume: true });
assert.equal(projection.status, "collecting_evidence");
assert.equal(
accounting.calls.some((call) =>
call.fn === "transition_agentic_rectification_case_status"
&& call.args.p_status === "collecting_evidence"
),
true,
);
});
@@ -2287,4 +2489,3 @@ test("offer-candidates allows a 34/33/33 tie after method coverage when remainin
true,
);
});