fix(rectification): keep single-event opening on the existing collect path (BUG-604)

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Jesse_Chen
2026-09-09 14:43:25 +08:00
co-authored by Cursor
parent 16343355a6
commit 1453fb16ed
21 changed files with 150 additions and 38 deletions
+1 -1
View File
@@ -2,7 +2,7 @@
## 2026-09-09 — 生时校正开场一次可报多件,采集题可点「没有 / 记不清」,每轮只留一句话
开场先讲当前搜索窗口和核对做法,说明最后给区间和代表分钟、不给精确到秒,并请人想到几件说几件;会点出升学、第一份工作、搬家、恋爱结婚、家里的大事、生病受伤,仍不写具体年份、不要求先准备材料。一条消息里报三件只跑一次比较。口述采集题输入框上方出现「没有」「记不清」两个按钮:「没有」记下并跳过这方面,「记不清」记下这题先放着,本会话不再问该领域,采用后核对仍可能碰到。助手气泡不再写「本轮对照了…」方法句,那句改进行程记录的展开区;点选后旁白只说范围没变或收到哪一段;记下经历时只复述年-月和事件,不评价「很有帮助」。Skill 版本 10.0.19。
开场先讲当前搜索窗口和核对做法,说明最后给区间和代表分钟、不给精确到秒,并请人想到几件说几件;会点出升学、第一份工作、搬家、恋爱结婚、家里的大事、生病受伤,仍不写具体年份、不要求先准备材料。一条消息里报三件只跑一次比较。只报一件也按原来的逐领域采集往下走,不会追问「还有吗」;第一道逐领域题干末尾会加一次「想到别的也可以一起说」。口述采集题输入框上方出现「没有」「记不清」两个按钮:「没有」记下并跳过这方面,「记不清」记下这题先放着,本会话不再问该领域,采用后核对仍可能碰到。助手气泡不再写「本轮对照了…」方法句,那句改进行程记录的展开区;点选后旁白只说范围没变或收到哪一段;记下经历时只复述年-月和事件,不评价「很有帮助」。Skill 版本 10.0.19。
## 2026-09-09 — 生时校正对话上方常驻一条时间轴,看得见范围在收窄
@@ -5,13 +5,14 @@
任务书:`docs/tasks/TASK-rectification-conversation-economy-20260909.md`
基线:`origin/staging` @ `d9252516`(任务书写 aa499da1;本 worktree 从当时的 staging 头切出,含时间轴文档提交)
本单状态:**待验收**。产品提交 `aa7ccb30`;尚未 push,未合入 staging/main。
本单状态:**待验收**。产品提交 `aa7ccb30`;单事件开场补丁见本文件后文 SHA。尚未 push,未合入 staging/main。未合时间轴兄弟单。
未改:采用门、确认门、各道门槛、`page.tsx`、`use-conversation-scroll-anchor.ts`。未改时间轴兄弟单文件(`rectification-timeline-scale.ts` / `rectification-candidate-result.ts` / 时间轴测试 / DESIGN §10)。Skill 从 10.0.18 升到 **10.0.19**。
未改:采用门、确认门、各道门槛、`page.tsx`、`use-conversation-scroll-anchor.ts`。未改时间轴兄弟单文件(`rectification-timeline-scale.ts` / `rectification-candidate-result.ts` / 时间轴测试 / DESIGN §10)。Skill 停在 **10.0.19**(live + 快照同步改 OpeningPolicy,不升版)。
## 做了什么
- **BUG-604 / 决策 1** `buildOpeningBrief` 写入当前搜索窗口(`candidate_range` HH:MM–HH:MM)与 intake 来源、做法三句、六类领域。开场正文不合格则换成 `openingSpokenBody`。首题仍是 `collect:other:*`,题干 `GENERIC_COLLECT_QUESTION` =「先说你最容易想起的一两件,年月大概就行。」不写具体年份、不要求先准备材料。批量写入仍走 `rectification-record-evidence-batch`;已确认领域不再被 `nextDatedCollectFollowup` 追问。
- **BUG-604 追问补丁(`origin/staging` `54adb0d1`)** 邀请多件是降低门槛,不是要求。开场只报一件:batch 写一条、compare 一次、下一问仍是既有 `nextDatedCollectFollowup`(顺序不变)。不插「还有吗 / 还能想起别的吗」追问轮,也不重复开场邀请。第一道逐领域题干由服务端加一次 `FIRST_DATED_COLLECT_INVITE` =「想到别的也可以一起说。」(`invite_more_once`,只一次)。用户可见文案未出现「还有吗」。
- **BUG-605 / 决策 2** `collect_spoken` 且未 busy 时,`composer-wrap` 内两个 44px 次要按钮,类名 `rectification-collect-reply`(不复用 `rectification-step-state`,无示例骨架条)。「没有」= 打字「没有」= declined,回执「记下了,这方面先跳过。」「记不清」= skipped,回执「记下了,这题先放着。」精确这两句不调模型。skipped 本会话不再采集;采用后 `remainingReverseVerifyProbes` 仍可碰到 skipped(`includeSkipped: false`)。
- **BUG-606 / 决策 3** 气泡不再渲染 `vargaSentence`;展开的 `ConsultationRunTimeline` / `AgentActivityStatus` 保留。点选旁白只有「已记录,范围没变。」或「已记录,范围收到 / 变为 A–B。」证据轮 `stripQuestionSentences` 之后 `trimEvidenceTurnBody`:超过两句只留首句,去掉「很有帮助 / 很有价值 / 很有分量 / 特别有用」。
@@ -32,16 +33,19 @@
| 证据轮正文 | 2–4 句额度;模型三句原样落库 | 超过两句只留首句;去掉价值评价四词 | BUG-606(c) |
| Agent 提示 | 「正文只做承接(2-4 句)」;「接下来我们继续」 | 「正文只做承接」+「证据轮正文只写一句复述」;题干作下一段 | BUG-606 |
| 流式三句证据 | `answerText` 等于模型全文 | 落库首句,并以 `replace: true` 覆盖可见正文 | BUG-606 服务端裁剪 |
| 只报一件后的下一问 | 未单独锁;有插追问轮风险 | 下一问 = 既有 dated collect(education 后是 family);正文无「还有吗 / 别的吗」 | `54adb0d1`;邀请不是要求 |
| 第一道逐领域题干 | 等于 `USER_COLLECT_QUESTION.*` | 末尾一次接「想到别的也可以一起说。」;第二道起不再接 | 不是独立追问轮 |
| Skill 10.0.19 哈希 | `a21b33abbbe3a530d725b94719b57ff8417cfcb850c54b547d5c18e390ec3d2b` | `a68885d3cf2110ee456bff65210c60e6bef4da4ea45e2a790a5462fd6d7307c8` | OpeningPolicy 补单事件口径;版本不升 |
## 测试
- `frontend` `./node_modules/.bin/tsc --noEmit`:exit 0
- `npm run lint`:0 errors / 108 warnings(仓库基线 warning,无新 error)
- 任务书指定切片:`tests/rectification-*.test.ts` + `agent-voice-copy-contract.test.ts` + `skill-registry.test.ts`(排除 database):**1065 tests / 1065 pass / 0 fail**
- 任务书指定切片:`tests/rectification-*.test.ts` + `agent-voice-copy-contract.test.ts` + `skill-registry.test.ts`(排除 database):**1066 tests / 1066 pass / 0 fail**(含新增「single confirmed domain opening goes to dated collect without a chase-more turn」)
- Docker `database-rectification-*.test.ts`:本机未跑,记 blocked
- 浏览器真人走查:browser MCP 可用但无打开的页、无登录态的 `collect_spoken` 会话,未点到「没有 / 记不清」。采集按钮与 44px、不复用 `rectification-step-state` 由 `rectification-spoken-collect.test.ts` 锁源码。
- 浏览器真人走查:browser MCP 可用但无打开的页、无登录态的 `collect_spoken` 会话,未点到「没有 / 记不清」。采集按钮与 44px、不复用 `rectification-step-state` 由 `rectification-spoken-collect.test.ts` 锁源码。本补丁只改题干后缀与路由,无新 UI。
Skill 10.0.19 SHA256:`a21b33abbbe3a530d725b94719b57ff8417cfcb850c54b547d5c18e390ec3d2b`。live 树与 `versions/10.0.19` 一致,registry 哈希与 `computeSkillPackageSha256` 一致。
Skill 10.0.19 SHA256:`a68885d3cf2110ee456bff65210c60e6bef4da4ea45e2a790a5462fd6d7307c8`。live 树与 `versions/10.0.19` 一致,registry 哈希与 `computeSkillPackageSha256` 一致。
## 偏离 / blocked
@@ -49,6 +53,6 @@ Skill 10.0.19 SHA256:`a21b33abbbe3a530d725b94719b57ff8417cfcb850c54b547d5c18e3
2. 本机主仓停在 `codex/minute-rectification-p0` 且有脏文件;未 checkout / reset / stash。本单只在本 worktree。
3. `move_agent_to_root` 对子代理不可用;全程用本 worktree 绝对路径。
4. `.venv` 是指向主仓的 symlink,不提交。
5. `origin/staging` 在切出后超前 2 个提交;按任务书不 merge、不 push。
6. BUG_HISTORY BUG-604/605/606 修复版本:`aa7ccb30`(产品提交;本条为回填 SHA 的文档提交,不 amend)。
5. `origin/staging` 在切出后超前(含 `54adb0d1` 任务书补丁、`d5972ef4` BUG-607)。只把 `54adb0d1` 的 §2.1 单事件口径摘进本 TASK;**不**取 BUG-607,不 merge staging,不 push。
6. BUG_HISTORY BUG-604/605/606 修复版本:`aa7ccb30`(产品提交)。单事件开场补丁是新提交,不 amend。
7. 点选旁白测试里,若下一问未能单独落库,服务端仍可能把采集题干接在「已记录,范围收到…」后面。断言改为前缀匹配,不把下一问挂载当成旁白模板的一部分。
@@ -19,7 +19,8 @@
### 2.1 BUG-604 开场(决策 1)
- `buildOpeningBrief` 改为给模型三个事实:当前搜索窗口(`candidate_range`,如 04:45–05:15)与来源(intake 声明的不确定档)、做法三句的要点、六类领域清单(升学、第一份工作、搬家、恋爱结婚、家里的大事、生病受伤)。正文模板由 Skill 规定为:一句窗口与做法、一句"最后给区间和代表分钟,不给精确到秒"、一句"想到几件说几件,有大概年月就行"+ 六类;仍不得写具体年份、不得要求先准备材料。首题 `spokenPrompt` 保持采集题身份(`collect:other:*`),题干改为"先说你最容易想起的一两件,年月大概就行"。
- 批量写入:`rectification-record-evidence-batch` 已支持多事件;确认一条消息含 3 件不同领域事件时 batch 一次写入、compare 只跑一次、后续逐领域采集只问缺口(`nextDatedCollectFollowup` 已按 `hasConfirmedDomain` 跳过)。
- 验收:opening 落库正文 ≤ 4 句、含六类中至少五类、不含年份;mock 一条三事件消息 → receipt 里 compare 一次、`evidence` 三条、下一问不再是这三个领域;`agent-voice-copy-contract` 收新句。SKILL.md L52 改口径,`conversation-strategy.md` OpeningPolicy 同步,Skill 10.0.19 快照与 registry。
- **只报一件也必须走通(产品 2026-09-09 追问)。** 邀请是降低门槛,不是要求。用户开场只说一件事时,流程与今天完全一致:batch 写一条、compare 一次、下一问是既有的逐领域采集题(`nextDatedCollectFollowup` 顺序不变);**不得**插入"还有吗 / 还能想起别的吗"这种追问轮,也不得因为只报了一件就重复开场邀请。逐领域采集题的题干末尾可以带一次"想到别的也可以一起说"(服务端固定句,只在第一道逐领域采集题上出现一次)。
- 验收:opening 落库正文 ≤ 4 句、含六类中至少五类、不含年份;mock 一条单事件消息 → compare 一次、下一问为逐领域采集题、正文不含"还有吗 / 别的吗";mock 一条三事件消息 → receipt 里 compare 一次、`evidence` 三条、下一问不再是这三个领域;`agent-voice-copy-contract` 收新句。SKILL.md L52 改口径,`conversation-strategy.md` OpeningPolicy 同步,Skill 10.0.19 快照与 registry。
### 2.2 BUG-605 「没有 / 记不清」(决策 2)
- `collect_spoken` 焦点激活且未 busy 时,`composer-wrap` 内输入框上方渲染两个按钮(44px,次要样式,与被删的「先这样」不同类名,不得复用 `rectification-step-state`)。「没有」POST 与打字"没有"相同的请求(复用 `applyRectificationChoice`/拒答分支,`collectDeclinedAck` 原句);「记不清」走 `resolve-focus status=skipped` 的服务端路径,回执"记下了,这题先放着。"(新文案对照 VOICE.md)。
@@ -158,13 +158,14 @@
资料:家人记得大概时间,钟点任意,范围「差不多准」。地点任意公开城市。虚构经历。
开场后用一条消息说三件不同领域、带年月的事(例如上学、第一份工作、搬家)。之后继续采集。
开场后用一条消息说三件不同领域、带年月的事(例如上学、第一份工作、搬家)。另走一遍只说一件。之后继续采集。
期望:
- 开场正文不超过四句,点出升学、第一份工作、搬家、恋爱结婚、家里的大事、生病受伤中至少五类,不含具体年份
- 这一条消息只触发一次候选比较,账本写入三件
- 下一问不再是已经说过的那三个领域
- 只报一件时也只比较一次,下一问是既有逐领域采集,正文没有「还有吗 / 还能想起别的吗」;第一道逐领域题干末尾可以有一次「想到别的也可以一起说」
- 口述采集输入框上方有「没有」「记不清」,没有「先这样」
- 每轮助手正文一句复述(可另接一句范围变化);气泡里没有「本轮对照了…」,也没有「很有帮助 / 很有价值 / 很有分量 / 特别有用」
+1
View File
@@ -46,6 +46,7 @@ Jyotisha 的可见文案是产品的一部分。正确性红线(真实性、
| 停止后出现红色告警条 | 已停止,已生成的内容保留;本次不会扣点。 | 停止是用户动作,不是出错。 |
| 也可以再说一件你记得大概时间的事。 | 能分开候选的问题已经问完,先按现有材料给你结果。 / 还差带月份的经历,领域不限。 | 采集问完后交付或说清门槛,不再开放采集。 |
| 口述采集没有停止按钮,范围小字不可点 | 选择题卡内有「先这样,先看当前范围」;生成中有「停止回答」。口述采集输入框上方是「没有」「记不清」两个 44px 次要按钮,不是先这样。范围小字是状态句,不可点。 | 没有=拒答,记不清=本会话先放着。 |
| 只报一件后追问「还有吗,还能想起别的吗」 | 下一问仍是既有逐领域采集。第一道题干末尾由服务端加一次「想到别的也可以一起说。」 | 邀请不是要求;不插追问轮,也不重复开场。 |
| 点选后旁白写「04:31–04:39 这段领先,05:00–05:07 这段落后」 | 已记录,范围收到 04:31–04:39。 | 旁白只说范围变没变。领先/落后留在卡内「答了会怎样」,不进气泡。 |
| 每轮只有技法清单,不知道现在第几步 | 输入框上方有「第 N 步 · 原因 · 下一步」。 | 用户始终看得见该做什么。 |
| 完全不清楚出生时间后劝退「生时校正以后需要时再做」 | 可以直接开始生时校正:先从你记得的经历比出大致时段。 | 不知道钟点时先比时段,不劝退、不给分钟卡。 |
@@ -30,6 +30,15 @@ export type CollectDomain =
export const GENERIC_COLLECT_QUESTION = "先说你最容易想起的一两件,年月大概就行。";
/** One-time suffix on the first dated-collect stem. Not a chase-more turn. */
export const FIRST_DATED_COLLECT_INVITE = "想到别的也可以一起说。";
export function withFirstDatedCollectInvite(stem: string): string {
const spoken = stem.trim();
if (!spoken || spoken.includes(FIRST_DATED_COLLECT_INVITE)) return spoken;
return `${spoken}${FIRST_DATED_COLLECT_INVITE}`;
}
/** Six everyday domains named in the opening body. Not years. */
export const OPENING_COLLECT_DOMAINS = [
"升学",
@@ -108,6 +117,7 @@ export const RECTIFICATION_USER_COPY = {
collectHandoff: "接下来我们继续。",
collectDeclinedAck: "记下了,这方面先跳过。",
collectSkippedAck: "记下了,这题先放着。",
firstDatedCollectInvite: FIRST_DATED_COLLECT_INVITE,
uncertaintyStop: "前面几道题你多半选了\"说不好\",再问下去也分不开,先停在这里。",
tiedFirstStop: "几个候选打成平手,问题已经分不开它们。",
probePoolExhaustedStop: "能分开候选的问题已经问完,先按现有材料给你结果。",
@@ -428,6 +438,7 @@ export function listUserVisibleCopy(): string[] {
RECTIFICATION_USER_COPY.collectHandoff,
RECTIFICATION_USER_COPY.collectDeclinedAck,
RECTIFICATION_USER_COPY.collectSkippedAck,
FIRST_DATED_COLLECT_INVITE,
GENERIC_COLLECT_QUESTION,
openingSpokenBody(["04:45", "05:15"]),
RECTIFICATION_USER_COPY.uncertaintyStop,
@@ -84,6 +84,7 @@ import {
GENERIC_COLLECT_QUESTION,
USER_COLLECT_QUESTION,
USER_COLLECT_QUESTION_RETRY,
withFirstDatedCollectInvite,
} from "../user-copy.ts";
export { GENERIC_COLLECT_QUESTION };
@@ -194,6 +195,7 @@ export type MethodFollowup = Readonly<{
selection_score?: number;
probe_id?: string;
collect_retry?: boolean;
invite_more_once?: boolean;
block_periods?: Readonly<Record<"A" | "B" | "C", BlockScanBlock>>;
widen_windows?: Readonly<Record<"A" | "B", import("./window-widen.ts").WidenWindowOption>>;
date_reliability_evidence_id?: string;
@@ -1211,7 +1213,12 @@ export function spokenFollowupForUser(
const mapped = followup.collect_retry === true
? (USER_COLLECT_QUESTION_RETRY[domain] ?? USER_COLLECT_QUESTION[domain])
: USER_COLLECT_QUESTION[domain];
return mapped ?? null;
if (!mapped) return null;
const dated = (DATED_COLLECT_ORDER as readonly string[]).includes(domain);
if (followup.invite_more_once && dated && followup.collect_retry !== true) {
return withFirstDatedCollectInvite(mapped);
}
return mapped;
}
export const DATED_COLLECT_ORDER = [
@@ -1260,15 +1267,29 @@ function datedCollectDomainBlocked(
|| answeredYes.has(domain);
}
export function datedCollectAlreadyAsked(
declined: ReadonlySet<string>,
topics: readonly Readonly<Record<string, unknown>>[] = [],
): boolean {
if (DATED_COLLECT_ORDER.some((domain) => declined.has(domain))) return true;
return DATED_COLLECT_ORDER.some((domain) => domainCollectFocusAsked(topics, domain));
}
export function nextDatedCollectFollowup(
evidence: readonly MethodFollowupEvidence[],
declined: ReadonlySet<string>,
answeredYes: ReadonlySet<string> = new Set(),
options?: { askedDatedCollect?: boolean },
): MethodFollowup | null {
for (const domain of DATED_COLLECT_ORDER) {
if (datedCollectDomainBlocked(domain, evidence, declined, answeredYes)) continue;
const next = datedCollectFollowup(domain, evidence);
if (next) return next;
if (next) {
return {
...next,
invite_more_once: options?.askedDatedCollect !== true,
};
}
}
return null;
}
@@ -1357,7 +1378,9 @@ export function exhaustionSpokenCollectFollowup(input: {
}): MethodFollowup | null {
const declined = declinedDomains(input.declinedTopics ?? []);
const answeredYes = domainsAnsweredYes(input.answeredProbes, input.eventProbes);
const dated = nextDatedCollectFollowup(input.evidence, declined, answeredYes);
const dated = nextDatedCollectFollowup(input.evidence, declined, answeredYes, {
askedDatedCollect: datedCollectAlreadyAsked(declined, input.declinedTopics ?? []),
});
if (dated) return dated;
if (
!declined.has("occupation")
@@ -1883,9 +1906,15 @@ export function buildMethodFollowupPlan(input: {
const collectRetry = base.intent === "collect_method_evidence"
&& typeof base.domain === "string"
&& domainCollectFocusAsked(askedRows, base.domain);
const inviteMoreOnce = base.intent === "collect_method_evidence"
&& typeof base.domain === "string"
&& (DATED_COLLECT_ORDER as readonly string[]).includes(base.domain)
&& !collectRetry
&& !datedCollectAlreadyAsked(declinedDomains(input.declinedTopics ?? []), askedRows);
return {
...base,
...(collectRetry ? { collect_retry: true } : {}),
...(inviteMoreOnce ? { invite_more_once: true } : { invite_more_once: false }),
choice_frame: attach
? buildChoiceFrame(base, {
observations: input.observations,
@@ -2512,7 +2541,12 @@ export function buildMethodFollowupPlan(input: {
&& precisionCard?.choice_frame
) {
next = precisionCard;
} else if ((datedCollect = nextDatedCollectFollowup(input.evidence, declined, answeredYes))) {
} else if ((datedCollect = nextDatedCollectFollowup(input.evidence, declined, answeredYes, {
askedDatedCollect: datedCollectAlreadyAsked(declined, [
...(input.closedCollectFocuses ?? []),
...(input.declinedTopics ?? []),
]),
}))) {
next = makeFollowup(datedCollect);
} else if (!occupationCovered) {
if (meetsAcceptanceEventQuality(input.evidence)) {
@@ -2688,7 +2722,12 @@ export function buildMethodFollowupPlan(input: {
next = null;
}
if (!next) {
const leftoverDated = nextDatedCollectFollowup(input.evidence, declined, answeredYes);
const leftoverDated = nextDatedCollectFollowup(input.evidence, declined, answeredYes, {
askedDatedCollect: datedCollectAlreadyAsked(declined, [
...(input.closedCollectFocuses ?? []),
...(input.declinedTopics ?? []),
]),
});
if (leftoverDated) {
next = makeFollowup(leftoverDated);
} else if (!occupationCovered) {
@@ -275,6 +275,8 @@ test("opening body lists domains without years and the stem no longer lists year
assert.equal(GENERIC_COLLECT_QUESTION, "先说你最容易想起的一两件,年月大概就行。");
assert.doesNotMatch(GENERIC_COLLECT_QUESTION, /比如/);
assert.ok(listUserVisibleCopy().includes(RECTIFICATION_USER_COPY.collectSkippedAck));
assert.ok(listUserVisibleCopy().includes(RECTIFICATION_USER_COPY.firstDatedCollectInvite));
assert.doesNotMatch(listUserVisibleCopy().join("\n"), /还有吗|还能想起别的吗/);
assert.ok(MACHINE_VOICE_LEXICON.includes("领先"));
assert.ok(MACHINE_VOICE_LEXICON.includes("落后"));
});
@@ -30,7 +30,7 @@ import {
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, ADOPT_OUTCOMES } from "../src/lib/rectification-agentic/core/rectification-decision.ts";
import { containsBoundarySemantics, RECTIFICATION_USER_COPY } from "../src/lib/rectification-agentic/user-copy.ts";
import { containsBoundarySemantics, RECTIFICATION_USER_COPY, withFirstDatedCollectInvite } from "../src/lib/rectification-agentic/user-copy.ts";
import {
evidenceLedgerFingerprint,
parseV9CaseDossier,
@@ -978,8 +978,13 @@ test("answering the last discriminator persists a year-locked family collect foc
const schema = setFocus.args.p_expected_answer_schema as { choice?: unknown; prompt?: string; collect?: boolean };
assert.equal(schema.choice, undefined);
assert.equal(schema.collect, true);
// 旧:prompt 含 2021 → 新:口语等于 USER_COLLECT_QUESTION.family → 决策 7 采集题不带年份前缀
assert.equal(schema.prompt, RECTIFICATION_USER_COPY.collectQuestionByDomain.family);
// 旧:prompt 等于 USER_COLLECT_QUESTION.family
// 新:第一道逐领域采集题干末尾加一次「想到别的也可以一起说」
// 原因:BUG-604;年份仍不进采集题干
assert.equal(
schema.prompt,
withFirstDatedCollectInvite(RECTIFICATION_USER_COPY.collectQuestionByDomain.family),
);
assert.match(schema.prompt ?? "", /家人|结婚|添丁|住院/);
assert.equal(applied.nextInterviewPersisted, true);
assert.equal(applied.nextChoiceReady, false);
@@ -28,7 +28,7 @@ import {
parseV9CaseDossier,
RectificationToolServiceError,
} from "../src/lib/rectification-agentic/v9/tool-service.ts";
import { GENERIC_COLLECT_QUESTION, USER_COLLECT_QUESTION } from "../src/lib/rectification-agentic/user-copy.ts";
import { GENERIC_COLLECT_QUESTION, USER_COLLECT_QUESTION, withFirstDatedCollectInvite } from "../src/lib/rectification-agentic/user-copy.ts";
import { turnQuestionKind } from "../src/lib/rectification-agentic/v9/turn-question.ts";
import { createRectificationV9Tools } from "../src/mastra/rectification-v9-tools.ts";
import {
@@ -186,7 +186,10 @@ test("family collect spoken stem has no year prefix while probe_year stays dated
});
assert.equal(plan.next_followup?.domain, "family");
assert.equal(plan.next_followup?.probe_year, 2021);
assert.equal(spokenFollowupForUser(plan.next_followup), USER_COLLECT_QUESTION.family);
// 旧:口语等于 USER_COLLECT_QUESTION.family
// 新:第一道逐领域采集题干末尾加一次「想到别的也可以一起说」
// 原因:BUG-604 只报一件也走既有采集,不插「还有吗」追问轮
assert.equal(spokenFollowupForUser(plan.next_followup), withFirstDatedCollectInvite(USER_COLLECT_QUESTION.family));
});
test("four scoreable events skip holdout for domains already in the ledger", () => {
@@ -582,13 +585,13 @@ test("career discriminator yes covers career collect without ledger career evide
});
assert.equal(unanswered.next_followup?.domain, "career");
assert.equal(unanswered.next_followup?.intent, "collect_method_evidence");
assert.equal(spokenFollowupForUser(unanswered.next_followup), USER_COLLECT_QUESTION.career);
assert.equal(spokenFollowupForUser(unanswered.next_followup), withFirstDatedCollectInvite(USER_COLLECT_QUESTION.career));
const yes = careerAnswerPlan("yes");
assert.notEqual(yes.next_followup?.domain, "career");
assert.equal(yes.next_followup?.domain, "education");
assert.equal(yes.next_followup?.intent, "collect_method_evidence");
assert.equal(spokenFollowupForUser(yes.next_followup), USER_COLLECT_QUESTION.education);
assert.equal(spokenFollowupForUser(yes.next_followup), withFirstDatedCollectInvite(USER_COLLECT_QUESTION.education));
assert.equal(
yes.methods.find((item) => item.method_id === "d10_career")?.status,
"covered",
@@ -600,17 +603,17 @@ test("career discriminator yes covers career collect without ledger career evide
const no = careerAnswerPlan("no");
assert.equal(no.next_followup?.domain, "career");
assert.equal(spokenFollowupForUser(no.next_followup), USER_COLLECT_QUESTION.career);
assert.equal(spokenFollowupForUser(no.next_followup), withFirstDatedCollectInvite(USER_COLLECT_QUESTION.career));
const unsure = careerAnswerPlan("unsure");
assert.equal(unsure.next_followup?.domain, "career");
assert.equal(spokenFollowupForUser(unsure.next_followup), USER_COLLECT_QUESTION.career);
assert.equal(spokenFollowupForUser(unsure.next_followup), withFirstDatedCollectInvite(USER_COLLECT_QUESTION.career));
});
test("career discriminator yes does not invent coverage from a semantic_key prefix", () => {
const plan = careerAnswerPlan("yes", { eventProbes: [] });
assert.equal(plan.next_followup?.domain, "career");
assert.equal(spokenFollowupForUser(plan.next_followup), USER_COLLECT_QUESTION.career);
assert.equal(spokenFollowupForUser(plan.next_followup), withFirstDatedCollectInvite(USER_COLLECT_QUESTION.career));
});
test("ledger-classified career probe answers do not cover career collect", () => {
@@ -623,7 +626,7 @@ test("ledger-classified career probe answers do not cover career collect", () =>
}],
});
assert.equal(plan.next_followup?.domain, "career");
assert.equal(spokenFollowupForUser(plan.next_followup), USER_COLLECT_QUESTION.career);
assert.equal(spokenFollowupForUser(plan.next_followup), withFirstDatedCollectInvite(USER_COLLECT_QUESTION.career));
});
const THIRD_CANDIDATE_ID = "88888888-8888-4888-8888-888888888883";
@@ -33,6 +33,7 @@ import { RECTIFICATION_SKILL_VERSION } from "../src/lib/rectification-agentic/v9
import {
RECTIFICATION_USER_COPY,
USER_COLLECT_QUESTION,
withFirstDatedCollectInvite,
} from "../src/lib/rectification-agentic/user-copy.ts";
import {
ATTEMPT_ID,
@@ -486,7 +487,10 @@ test("revision 5 with uncovered relatives asks the dated family collect, not a y
// 旧:year_label 含 2021 → 新:采集口语不带年份前缀,year_label 不进 followup
// 原因:决策 7;probe_year 仍锁定 dated 家庭采集,不出无年份 D12 卡
assert.equal(plan.next_followup?.year_label, undefined);
assert.equal(spokenFollowupForUser(plan.next_followup), USER_COLLECT_QUESTION.family);
// 旧:口语等于 USER_COLLECT_QUESTION.family
// 新:第一道逐领域采集题干末尾加一次「想到别的也可以一起说」
// 原因:BUG-604 单事件开场不插追问轮
assert.equal(spokenFollowupForUser(plan.next_followup), withFirstDatedCollectInvite(USER_COLLECT_QUESTION.family));
assert.equal(plan.dropped_probes.some((item) => (
item.semantic_key.startsWith("varga.d12") && item.reason === "yearless_ungrounded_contrast"
)), true);
@@ -673,7 +673,10 @@ test("nonterminal invariant 3: answer_choice response waits until incident fallb
// 保留语义:answer_choice 出口仍必须先把家人采集题干写入 focus/turn,再返回 HTTP。
assert.equal(decision.canAdopt, true);
assert.ok(currentQuestion?.prompt);
assert.equal(currentQuestion.prompt, "家里如果有结婚、添丁或住院这类事,记得大概哪年就行。");
// 旧:家人采集题干原句
// 新:第一道逐领域采集题干末尾加一次「想到别的也可以一起说」
// 原因:BUG-604 单事件开场不插追问轮
assert.equal(currentQuestion.prompt, "家里如果有结婚、添丁或住院这类事,记得大概哪年就行。想到别的也可以一起说。");
const setFocus = accounting.calls.find((call) => call.fn === "set_agentic_rectification_conversation_focus");
assert.ok(setFocus);
assert.equal(setFocus.args.p_intent, "collect_method_evidence");
@@ -13,7 +13,7 @@ import {
parseWindowScan,
} 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 { GENERIC_COLLECT_QUESTION, USER_COLLECT_QUESTION } from "../src/lib/rectification-agentic/user-copy.ts";
import { GENERIC_COLLECT_QUESTION, USER_COLLECT_QUESTION, withFirstDatedCollectInvite } from "../src/lib/rectification-agentic/user-copy.ts";
import { RECTIFICATION_SKILL_VERSION } from "../src/lib/rectification-agentic/v9/case-status.ts";
import { buildInferenceState } from "../src/lib/rectification-agentic/core/build-state.ts";
import { decideRectification } from "../src/lib/rectification-agentic/core/rectification-decision.ts";
@@ -2381,7 +2381,10 @@ test("family collect attaches the collection-probe year instead of a yearless D2
// 原因:决策 7,年份只用于计分与去重,不进采集题干
assert.equal(plan.next_followup?.year_label, undefined);
assert.doesNotMatch(plan.next_followup?.semantic_key ?? "", /varga\.d24/);
assert.equal(spokenFollowupForUser(plan.next_followup), USER_COLLECT_QUESTION.family);
// 旧:口语等于 USER_COLLECT_QUESTION.family
// 新:第一道逐领域采集题干末尾加一次「想到别的也可以一起说」
// 原因:BUG-604 单事件开场不插追问轮
assert.equal(spokenFollowupForUser(plan.next_followup), withFirstDatedCollectInvite(USER_COLLECT_QUESTION.family));
});
test("already-open yearless family card is abandoned instead of kept as a scoring frame", () => {
@@ -11,7 +11,10 @@ import {
buildNextUserAction,
spokenFollowupForUser,
} from "../src/lib/rectification-agentic/v9/method-followup.ts";
import { USER_COLLECT_QUESTION } from "../src/lib/rectification-agentic/user-copy.ts";
import {
USER_COLLECT_QUESTION,
withFirstDatedCollectInvite,
} from "../src/lib/rectification-agentic/user-copy.ts";
import {
RECTIFICATION_QUESTION_RETRY_LIMIT,
rectificationQuestionGapState,
@@ -204,6 +207,9 @@ test("family collect spoken stem equals USER_COLLECT_QUESTION.family while probe
assert.equal(plan.next_followup?.probe_year, 2021);
assert.equal(plan.next_followup?.choice_frame, null);
const spoken = spokenFollowupForUser(plan.next_followup);
assert.equal(spoken, USER_COLLECT_QUESTION.family);
// 旧:口语等于 USER_COLLECT_QUESTION.family
// 新:第一道逐领域采集题干末尾加一次「想到别的也可以一起说」
// 原因:BUG-604 只报一件也走既有采集,不插「还有吗」追问轮
assert.equal(spoken, withFirstDatedCollectInvite(USER_COLLECT_QUESTION.family));
assert.doesNotMatch(spoken ?? "", /^\d{4} 年前后,/);
});
@@ -14,6 +14,7 @@ import {
declinedCollectDomains,
nextDatedCollectFollowup,
reverseVerifyRemainingForAdopt,
spokenFollowupForUser,
spokenCollectFallbackFollowup,
type MethodFollowup,
type MethodFollowupEvidence,
@@ -482,6 +483,34 @@ test("skipped collect domains are not re-asked this session; reverse-verify may
assert.equal(remaining.some((probe) => probe.domain === "family"), true);
});
test("single confirmed domain opening goes to dated collect without a chase-more turn", () => {
const evidence: MethodFollowupEvidence[] = [
{ status: "confirmed", domain: "education", datePrecision: "month", occurredFrom: "2016-09-01", occurredTo: null },
];
const next = nextDatedCollectFollowup(evidence, new Set());
assert.ok(next);
assert.notEqual(next.domain, "education");
assert.equal(next.domain, "family");
const spoken = spokenFollowupForUser(next);
assert.ok(spoken);
assert.match(spoken, /家里/);
assert.match(spoken, /想到别的也可以一起说/);
assert.doesNotMatch(spoken, /还有吗|别的吗|还能想起别的吗/);
assert.doesNotMatch(spoken, /先说你最容易想起的一两件/);
const later = nextDatedCollectFollowup(evidence, new Set(), new Set(), { askedDatedCollect: true });
assert.equal(later?.domain, "family");
assert.doesNotMatch(spokenFollowupForUser(later) ?? "", /想到别的也可以一起说/);
const tools = readFileSync(new URL("../src/mastra/rectification-v9-tools.ts", import.meta.url), "utf8");
assert.match(tools, /recordV10EvidenceBatch/);
assert.match(tools, /result\.acceptedCount > 0[\s\S]*autoRescoreAfterEvidenceChange/);
const copy = readFileSync(new URL("../src/lib/rectification-agentic/user-copy.ts", import.meta.url), "utf8");
const agent = readFileSync(new URL("../src/mastra/agentic-rectification.ts", import.meta.url), "utf8");
const agentRun = readFileSync(new URL("../src/lib/rectification-agentic/v9/agent-run.ts", import.meta.url), "utf8");
assert.doesNotMatch(copy, /还有吗|还能想起别的吗/);
assert.doesNotMatch(agent, /还有吗|还能想起别的吗/);
assert.doesNotMatch(agentRun, /还有吗|还能想起别的吗/);
});
test("three confirmed domains from one message are not re-asked by dated collect", () => {
const evidence: MethodFollowupEvidence[] = [
{ status: "confirmed", domain: "education", datePrecision: "month", occurredFrom: "2016-09-01", occurredTo: null },
+1 -1
View File
@@ -86,7 +86,7 @@ test("checked-in registry verifies hashed product packages and leaves consult on
{
name: "jyotish-birth-time-rectification",
version: "10.0.19",
sha256: "a21b33abbbe3a530d725b94719b57ff8417cfcb850c54b547d5c18e390ec3d2b",
sha256: "a68885d3cf2110ee456bff65210c60e6bef4da4ea45e2a790a5462fd6d7307c8",
},
{
name: "jyotish-personal-report",
@@ -57,7 +57,7 @@ description: "生时校正专用 Skill(V10)。以服务器权威 Case、Conv
开场必须满足:
- 一条消息可以报多件;想到几件说几件,有大概年月即可。
- 一条消息可以报多件;想到几件说几件,有大概年月即可。只报一件也继续既有逐领域采集,不得追问「还有吗 / 还能想起别的吗」,也不得重复开场邀请。第一道逐领域题干末尾由服务端带一次「想到别的也可以一起说」。
- 允许模糊日期:可以先说大概年份、阶段或范围;如确有信息增益,后续再澄清,不诱导猜测月份或日期。
- 首题保持采集题身份(`collect:other:*`),题干写成「先说你最容易想起的一两件,年月大概就行」。
- 至多一个主问题且唯一来源:每轮当前问题只能由服务端建立 `ConversationFocus` 并通过界面问题槽呈现。Agent 回复正文只做承接与解释,不得自行提出、复述、改写或预告问题;正文内容不参与问题槽判定。
@@ -18,7 +18,7 @@ recent turns 不是权威记忆,不得依赖“上一条 assistant 问了什
首次开场只使用服务器 opening brief 中的 Case 状态、当前搜索窗口(intake 不确定档)、做法三句要点与六类领域清单,并自然满足:
- 三句模板:当前窗口与核对做法;「最后给区间和代表分钟,不给精确到秒」;「想到几件说几件,有大概年月就行」并点出升学、第一份工作、搬家、恋爱结婚、家里的大事、生病受伤。
- 一条消息可以报多件。不索要 10–15 条事件长表,不要一进场就出 A/B/C/D。
- 一条消息可以报多件。不索要 10–15 条事件长表,不要一进场就出 A/B/C/D。只报一件也走既有逐领域采集;不得追问「还有吗 / 还能想起别的吗」,也不得重复开场邀请。第一道逐领域题干末尾由服务端带一次「想到别的也可以一起说」。
- 接受“大概某年 / 那几年 / 某个阶段”等模糊日期,不诱导猜月份、日期或精确时点。
- 不得写具体年份,不得要求先准备材料。
- 首题 `collect:other:*` 题干写成「先说你最容易想起的一两件,年月大概就行」。
@@ -57,7 +57,7 @@ description: "生时校正专用 Skill(V10)。以服务器权威 Case、Conv
开场必须满足:
- 一条消息可以报多件;想到几件说几件,有大概年月即可。
- 一条消息可以报多件;想到几件说几件,有大概年月即可。只报一件也继续既有逐领域采集,不得追问「还有吗 / 还能想起别的吗」,也不得重复开场邀请。第一道逐领域题干末尾由服务端带一次「想到别的也可以一起说」。
- 允许模糊日期:可以先说大概年份、阶段或范围;如确有信息增益,后续再澄清,不诱导猜测月份或日期。
- 首题保持采集题身份(`collect:other:*`),题干写成「先说你最容易想起的一两件,年月大概就行」。
- 至多一个主问题且唯一来源:每轮当前问题只能由服务端建立 `ConversationFocus` 并通过界面问题槽呈现。Agent 回复正文只做承接与解释,不得自行提出、复述、改写或预告问题;正文内容不参与问题槽判定。
@@ -18,7 +18,7 @@ recent turns 不是权威记忆,不得依赖“上一条 assistant 问了什
首次开场只使用服务器 opening brief 中的 Case 状态、当前搜索窗口(intake 不确定档)、做法三句要点与六类领域清单,并自然满足:
- 三句模板:当前窗口与核对做法;「最后给区间和代表分钟,不给精确到秒」;「想到几件说几件,有大概年月就行」并点出升学、第一份工作、搬家、恋爱结婚、家里的大事、生病受伤。
- 一条消息可以报多件。不索要 10–15 条事件长表,不要一进场就出 A/B/C/D。
- 一条消息可以报多件。不索要 10–15 条事件长表,不要一进场就出 A/B/C/D。只报一件也走既有逐领域采集;不得追问「还有吗 / 还能想起别的吗」,也不得重复开场邀请。第一道逐领域题干末尾由服务端带一次「想到别的也可以一起说」。
- 接受“大概某年 / 那几年 / 某个阶段”等模糊日期,不诱导猜月份、日期或精确时点。
- 不得写具体年份,不得要求先准备材料。
- 首题 `collect:other:*` 题干写成「先说你最容易想起的一两件,年月大概就行」。
+1 -1
View File
@@ -164,7 +164,7 @@
{
"name": "jyotish-birth-time-rectification",
"version": "10.0.19",
"sha256": "a21b33abbbe3a530d725b94719b57ff8417cfcb850c54b547d5c18e390ec3d2b",
"sha256": "a68885d3cf2110ee456bff65210c60e6bef4da4ea45e2a790a5462fd6d7307c8",
"sourceCommit": null,
"packagePath": "skills/jyotish-birth-time-rectification/versions/10.0.19",
"status": "active"