fix(rectification): narrate the credible range and stop scoring periods by length (BUG-569–570)
Choice copy was comparing the search window, so every answer said the range had not changed. Block-scan summed raw scores, so longer afternoon windows won before any evidence difference. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -150,6 +150,13 @@ test("collect prompt skip no longer writes a second assistant turn", () => {
|
||||
assert.doesNotMatch(exit, /shouldPersistFocusPromptTurn/);
|
||||
});
|
||||
|
||||
test("probe explain tracks use the shared Vimshottari / Narayana labels", () => {
|
||||
const explain = readFileSync(new URL("../src/lib/rectification-agentic/v9/probe-explain.ts", import.meta.url), "utf8");
|
||||
const visible = listUserVisibleCopy().join("\n");
|
||||
assert.doesNotMatch(explain, /毗湿奴多利|那罗延/);
|
||||
assert.doesNotMatch(visible, /毗湿奴多利|那罗延/);
|
||||
});
|
||||
|
||||
test("E-2 display copy keeps engine meaning out of instruction tone", () => {
|
||||
const dasha = engineMeaningToDisplayCopy(
|
||||
"主限更偏向 05:00,分盘大运更偏向 04:54。冲突时不能按更高把握收口。",
|
||||
|
||||
@@ -28,6 +28,7 @@ import {
|
||||
isIncompleteRunBanner,
|
||||
} from "../src/lib/rectification-agentic/v9/run-diagnostic.ts";
|
||||
import { applyHoldoutAnswer, buildInferenceState } from "../src/lib/rectification-agentic/core/build-state.ts";
|
||||
import { applyChoiceWithoutEvidence } from "../src/lib/rectification-agentic/v9/inference-adapter.ts";
|
||||
import { RECTIFICATION_TERMINATION_COPY } from "../src/lib/rectification-agentic/core/rectification-decision.ts";
|
||||
import { containsBoundarySemantics, RECTIFICATION_USER_COPY } from "../src/lib/rectification-agentic/user-copy.ts";
|
||||
import { parseV9CaseDossier, RectificationToolServiceError } from "../src/lib/rectification-agentic/v9/tool-service.ts";
|
||||
@@ -193,6 +194,68 @@ function inferenceState() {
|
||||
});
|
||||
}
|
||||
|
||||
function rangeNarrationInference(leadSupport: number, trailSupport: number) {
|
||||
return buildInferenceState({
|
||||
range_start: "04:31",
|
||||
range_end: "05:07",
|
||||
candidates: [
|
||||
{ id: "04:31", time: "04:31", relative_support: leadSupport },
|
||||
{ id: "05:07", time: "05:07", relative_support: trailSupport },
|
||||
],
|
||||
events: [
|
||||
{ id: "e1", domain: "education", year: 2016, precision: "month" },
|
||||
{ id: "e2", domain: "career", year: 2018, precision: "year" },
|
||||
{ id: "e3", domain: "relationship", year: 2021, precision: "year" },
|
||||
{ id: "e4", domain: "family", year: 2023, precision: "year" },
|
||||
],
|
||||
probes: [{
|
||||
id: "p-cd",
|
||||
semantic_key: "career.2015",
|
||||
candidate_split_hash: "04:31|05:07",
|
||||
domain: "career",
|
||||
year: 2015,
|
||||
question: "2016 年前后有没有高考或重要考试发挥失常?",
|
||||
candidate_ids: ["04:31", "05:07"],
|
||||
expected_outcomes: [
|
||||
{ answer_class: "yes", supports: ["04:31"], conflicts: ["05:07"] },
|
||||
{ answer_class: "no", supports: ["05:07"], conflicts: ["04:31"] },
|
||||
{ answer_class: "unsure", supports: [], conflicts: [] },
|
||||
],
|
||||
information_gain: 0.4,
|
||||
source: "dasha_boundary",
|
||||
}],
|
||||
});
|
||||
}
|
||||
|
||||
function rangeNarrationDossier(inference: ReturnType<typeof buildInferenceState>) {
|
||||
return dossierFixture({
|
||||
latestResult: candidateSnapshotFixture({
|
||||
decisionReceipt: { inference_state: inference },
|
||||
}),
|
||||
conversationSummary: conversationSummaryFixture({
|
||||
activeFocus: activeFocusFixture({
|
||||
expectedAnswerSchema: {
|
||||
choice: {
|
||||
prompt: "2016 年前后,有没有明显高考或重要考试发挥失常?",
|
||||
option_a: "是,大概就在那段时间",
|
||||
option_b: "有类似,但年份不对或不够重大",
|
||||
option_c: "没有明显发生",
|
||||
option_d: "不记得 / 不确定",
|
||||
options: [
|
||||
{ key: "A", label: "是,大概就在那段时间", answer_class: "yes" },
|
||||
{ key: "B", label: "有类似,但年份不对或不够重大", answer_class: "weak_yes" },
|
||||
{ key: "C", label: "没有明显发生", answer_class: "no" },
|
||||
{ key: "D", label: "不记得 / 不确定", answer_class: "unsure" },
|
||||
],
|
||||
},
|
||||
probe_id: "p-cd",
|
||||
semantic_key: "career.2015",
|
||||
},
|
||||
}),
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
function choiceDossier() {
|
||||
const inference = inferenceState();
|
||||
const snapshot = candidateSnapshotFixture({
|
||||
@@ -1207,8 +1270,10 @@ test("a scoring choice narrates cluster movement and range change", () => {
|
||||
{ range: ["04:31", "04:39"], delta: 2 },
|
||||
{ range: ["05:00", "05:07"], delta: -2 },
|
||||
],
|
||||
rangeBefore: ["04:31", "05:07"],
|
||||
rangeAfter: ["04:31", "04:39"],
|
||||
// 旧:rangeBefore/rangeAfter 直喂两个不同搜索窗口 → 新:入参改名 credibleBefore/credibleAfter,仍测纯函数拼句
|
||||
// 原因:直喂拦不住 applyRectificationChoice 把 range_start/range_end 当成范围
|
||||
credibleBefore: ["04:31", "05:07"],
|
||||
credibleAfter: ["04:31", "04:39"],
|
||||
});
|
||||
assert.match(narrowed, /领先/);
|
||||
assert.match(narrowed, /落后/);
|
||||
@@ -1218,8 +1283,8 @@ test("a scoring choice narrates cluster movement and range change", () => {
|
||||
scoring: true,
|
||||
appliedInference: true,
|
||||
deltasByCluster: [{ range: ["04:31", "04:39"], delta: 1 }],
|
||||
rangeBefore: ["04:31", "04:39"],
|
||||
rangeAfter: ["04:31", "04:39"],
|
||||
credibleBefore: ["04:31", "04:39"],
|
||||
credibleAfter: ["04:31", "04:39"],
|
||||
});
|
||||
assert.match(unchanged, /范围没变/);
|
||||
const unsure = composeChoiceNarration({
|
||||
@@ -1227,12 +1292,80 @@ test("a scoring choice narrates cluster movement and range change", () => {
|
||||
scoring: true,
|
||||
appliedInference: true,
|
||||
answerClass: "unsure",
|
||||
rangeBefore: ["04:31", "04:39"],
|
||||
rangeAfter: ["04:31", "04:39"],
|
||||
credibleBefore: ["04:31", "04:39"],
|
||||
credibleAfter: ["04:31", "04:39"],
|
||||
});
|
||||
assert.equal(unsure, "已记录。这题先不计分,换一件事问。");
|
||||
});
|
||||
|
||||
test("answering A narrates the credible range even when the search window stays put", async () => {
|
||||
const previous = rangeNarrationInference(10, 3);
|
||||
assert.deepEqual([previous.range_start, previous.range_end], ["04:31", "05:07"]);
|
||||
assert.deepEqual(previous.credible_range, ["04:31", "05:07"]);
|
||||
const reduced = applyChoiceWithoutEvidence(previous, {
|
||||
choiceKey: "A",
|
||||
schema: {
|
||||
probe_id: "p-cd",
|
||||
semantic_key: "career.2015",
|
||||
options: [
|
||||
{ key: "A", label: "是,大概就在那段时间", answer_class: "yes" },
|
||||
],
|
||||
},
|
||||
});
|
||||
assert.equal(reduced.applied, true);
|
||||
assert.deepEqual([reduced.state.range_start, reduced.state.range_end], ["04:31", "05:07"]);
|
||||
assert.deepEqual(reduced.state.credible_range, ["04:31", "04:31"]);
|
||||
const applied = await applyRectificationChoice(persistChoiceAccounting(rangeNarrationDossier(previous)).client, {
|
||||
userId: USER_ID,
|
||||
caseId: CASE_ID,
|
||||
sessionId: SESSION_ID,
|
||||
actionId: ACTION_ID,
|
||||
action: CHOICE_ACTION,
|
||||
focusId: FOCUS_ID,
|
||||
questionId: QUESTION_ID,
|
||||
probeId: "p-cd",
|
||||
optionId: "A",
|
||||
expectedRevision: previous.revision,
|
||||
});
|
||||
assert.match(applied.narration, /范围从 04:31–05:07 收到 04:31–04:31/);
|
||||
assert.doesNotMatch(applied.narration, /范围没变/);
|
||||
const source = readFileSync(new URL("../src/lib/rectification-agentic/v9/answer-choice.ts", import.meta.url), "utf8");
|
||||
assert.match(source, /credibleBefore:\s*previous\.credible_range/);
|
||||
assert.match(source, /credibleAfter:\s*applied\.state\.credible_range/);
|
||||
assert.doesNotMatch(source, /rangeBefore:\s*\[previous\.range_start/);
|
||||
});
|
||||
|
||||
test("answering A says the range did not change when the credible range stays put", async () => {
|
||||
const previous = rangeNarrationInference(10, 10);
|
||||
assert.deepEqual(previous.credible_range, ["04:31", "05:07"]);
|
||||
const reduced = applyChoiceWithoutEvidence(previous, {
|
||||
choiceKey: "A",
|
||||
schema: {
|
||||
probe_id: "p-cd",
|
||||
semantic_key: "career.2015",
|
||||
options: [
|
||||
{ key: "A", label: "是,大概就在那段时间", answer_class: "yes" },
|
||||
],
|
||||
},
|
||||
});
|
||||
assert.deepEqual([reduced.state.range_start, reduced.state.range_end], ["04:31", "05:07"]);
|
||||
assert.deepEqual(reduced.state.credible_range, ["04:31", "05:07"]);
|
||||
const applied = await applyRectificationChoice(persistChoiceAccounting(rangeNarrationDossier(previous)).client, {
|
||||
userId: USER_ID,
|
||||
caseId: CASE_ID,
|
||||
sessionId: SESSION_ID,
|
||||
actionId: ACTION_ID,
|
||||
action: CHOICE_ACTION,
|
||||
focusId: FOCUS_ID,
|
||||
questionId: QUESTION_ID,
|
||||
probeId: "p-cd",
|
||||
optionId: "A",
|
||||
expectedRevision: previous.revision,
|
||||
});
|
||||
assert.match(applied.narration, /范围没变/);
|
||||
assert.doesNotMatch(applied.narration, /范围从 /);
|
||||
});
|
||||
|
||||
test("the public agent route treats structured choice as a non-model command", () => {
|
||||
const route = readFileSync(new URL("../src/app/api/rectification/agent/route.ts", import.meta.url), "utf8");
|
||||
const start = route.indexOf("if (isStructuredChoice)");
|
||||
|
||||
@@ -130,6 +130,9 @@ test("choice frames ask one biographical event from a server probe, not competin
|
||||
assert.equal(mergeChoiceCard(frame, null), null);
|
||||
assert.match(frame.why_user, /2016 年前后/);
|
||||
assert.match(frame.why_user, /分成两组/);
|
||||
assert.match(frame.why_user, /Vimshottari/);
|
||||
assert.match(frame.why_user, /Narayana/);
|
||||
assert.doesNotMatch(frame.why_user, /毗湿奴多利|那罗延/);
|
||||
assert.match(frame.answer_impact.A, /05:00/);
|
||||
assert.match(frame.answer_impact.A, /领先/);
|
||||
assert.equal(frame.answer_impact.D, "不计分,换一题");
|
||||
|
||||
Reference in New Issue
Block a user