fix(rectification): ask relationship and career before dated collect
Independent Staging Quality Gate / validate (push) Successful in 10m6s
Independent Staging Quality Gate / publish (push) Has been cancelled

Move nextDatedCollectFollowup after family coverage so a new case still
asks D9 then D10. Dated third-event collect stays education → finance →
relocation → health. Admin input padding contract follows --space-3.

BUG-531 and BUG-532.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Jesse_Chen
2026-09-04 19:06:54 +08:00
parent 846b60642f
commit 0be51e65f5
12 changed files with 257 additions and 90 deletions
+4
View File
@@ -1,5 +1,9 @@
# 印度占星 Skill 更新日志
## 2026-09-04 — 新案例前两问恢复感情与事业
训练门未开时,新案例仍先问感情、再问事业、再问家人;缺第三件带年份的事,才按学业 → 财务 → 搬家 → 健康补采集。原生输入框间距合同测试跟上 `var(--space-3)`。Skill 版本仍是 10.0.14。
## 2026-09-04 — 采集阶段第三件事必须指向领域,盘外核对不得抢跑
可评分事件还不到 3 条时,不再进入盘外核对,也不再先问没有日期的职业或「随便哪件事」。下一问按家人 → 学业 → 财务 → 搬家 → 健康 → 事业 → 感情补带年份的事;采集题干必须写出领域。进度句只能用服务端给出的还差几件,没有数字就不说。Skill 版本仍是 10.0.14;采用门与确认门未改。
+33 -1
View File
@@ -8161,7 +8161,7 @@
- 修复:`DATED_COLLECT_ORDER`(家人 → 学业 → 财务 → 搬家 → 健康 → 事业 → 感情)由主流程与 `exhaustionSpokenCollectFollowup` 共用。训练门未开时,在方法轮转之前取下一件带年份采集题;职业与 `other` 泛问排在最后。`datedCollectFollowup``source``method_coverage`(采集,不是盘外核对)。
- 验证:新文件锁 education → finance → occupation → other;拒答路径落下 `USER_COLLECT_QUESTION.education``rectification-eight-method` 中训练门未开的下一问从感情改为家人(或财务),每条改断言附原值/新值/原因。4 条可评分事件时本分支不触发(`rectification-collect-stall``rectification-yearless-ungrounded` 的 OOS 结论不变)。
- 防复发:训练门未开时不得先问职业或无领域泛问。顺序只许有一处定义。
- 相关记录:BUG-426、BUG-442、BUG-472、BUG-527
- 相关记录:BUG-426、BUG-442、BUG-472、BUG-527、BUG-531
- 复发自:无
- 修复版本:待发布
@@ -8196,3 +8196,35 @@
- 相关记录:BUG-527
- 复发自:无
- 修复版本:待发布
## BUG-531 | 新案例第二问被家人抢占感情 / 事业
- 状态:resolved
- 首次发现:2026-09-04
- 最近更新:2026-09-04
- 影响面:`buildMethodFollowupPlan``nextDatedCollectFollowup` 的插入位置
- 用户现象:新案例已有 1 件事业(或 1 件感情)时,第二问变成家人,而不是感情 / 事业。
- 触发条件:训练门未开(前 3 件事几乎总是如此);`3847e9c9` 把 dated 补采集插在方法覆盖轮转之前。
- 根因:上游任务书把「缺第三件带年份的事」的顺序写成了训练门未开阶段的总顺序。执行方按字面把 `nextDatedCollectFollowup` 放在 `!dashaCovered` 之后、感情 / 事业 / 家人轮转之前。这是任务书顺序错位,不是执行方偏离。
- 修复:删掉轮转前分支。轮转里 `!familyCovered` 之后、`!occupationCovered` 之前,训练门未开且 `nextDatedCollectFollowup` 有结果时取它。此时感情 / 事业 / 家人已 covered 或 declined,补采集只会给出学业 → 财务 → 搬家 → 健康。`DATED_COLLECT_ORDER``exhaustionSpokenCollectFollowup` 不动。
- 验证:`rectification-collect-direction-20260904` 锁 1 事业 → `d9_relationship`、1 感情 → `d10_career`、事业+感情 → `relatives`、再拒答家人 → `d5_education`、再拒答学业 → `d2_finance`。家人答「没有」后焦点仍是学业。`3847e9c9` 改写的感情 / 事业断言回到 `45bdb63e`
- 防复发:dated 补采集不得插在感情 / 事业轮转之前。训练门未开时新案例前两问仍是 D9 / D10。
- 相关记录:BUG-528、BUG-463
- 复发自:BUG-528
- 修复版本:待发布
## BUG-532 | 原生 input 间距合同测试仍锁 12px
- 状态:resolved
- 首次发现:2026-09-04
- 最近更新:2026-09-04
- 影响面:`frontend/tests/admin-model-management-ui-contract.test.ts`;挡住 `backend-quality-gate``npm test --prefix frontend`
- 用户现象:无直接用户可见故障。`7ee7f825` 之后门禁因这一条失败,staging 部署停在更早的 SHA。
- 触发条件:`globals.css` 原生 input 为 `padding: 0 var(--space-3)`,合同测试仍匹配 `padding: 0 12px;`
- 根因:`7ee7f825``fix(ui): collapse product weights and radii onto the shared Button`)把间距收敛到 token,未改合同测试。本单开工时 `origin/staging` 上这条仍红,别处未修。
- 修复:测试接受 `padding: 0 var(--space-3);`。不改 CSS。
- 验证:`admin-model-management-ui-contract` 全过;全量失败清单只剩 Docker 并发争用,隔离重跑通过。
- 防复发:改 `globals.css` 原生控件尺寸时同步合同测试。不得为迁就测试把 token 改回裸像素。
- 相关记录:无
- 复发自:无
- 修复版本:待发布
@@ -0,0 +1,86 @@
# PROGRESS · 采集方向修复单(2026-09-04
工作树:`.worktrees/rectification-collect-direction-fix-20260904`
分支:`codex/rectification-collect-direction-fix-20260904`
基线:任务书写 `origin/staging` `3847e9c9`;开工时 tip 是纯文档 `cce919fe`。交付前再 `git fetch`,快进合入 `846b6064`BUG-526 任务书,未占用 BUG-531)。
任务书:`docs/tasks/TASK-rectification-collect-direction-fix-20260904.md`
未改 Python、未 bump Skill(仍 10.0.14)、未改 `holdoutFollowupFor` / `validateSpokenPrompt` / `collectionProgressFromReceipt` / `declinedDomains` / 采用确认门。推 `origin/staging`
| 任务 | 状态 | BUG |
| --- | --- | --- |
| 5.1 dated 分支移到轮转内(家人后、职业前) | 完成 | BUG-531 |
| 5.2 回退 17 条感情/事业断言 | 完成 | 随 BUG-531 |
| 5.3 门禁 CSS 合同测试 | 完成 | BUG-532 |
| 5.4 记录 | 完成 | — |
## 编号
开工时 `docs/BUG_HISTORY.md` 最大号 BUG-530。本单 **BUG-531532**`846b6064` 未占用。
## 实现要点
- 删掉 `buildMethodFollowupPlan` 里轮转前的 `if (!next && !meetsAcceptanceEventQuality) nextDatedCollectFollowup`
- 轮转内 `!familyCovered` 之后、`!occupationCovered` 之前:训练门未开且 `nextDatedCollectFollowup` 有结果则取它。`source` 仍是 `method_coverage`
- `DATED_COLLECT_ORDER``exhaustionSpokenCollectFollowup` 不动;`rectification-range-offer-deadend` 搬家在职业前保留。
- `3847e9c9` 把新案例第二问改成家人的断言回到 `45bdb63e`(感情 / 事业)。三栏:原值 = `3847e9c9`,新值 = `45bdb63e`,原因 = dated 分支移到家人之后。
- CSS 合同测试接受 `padding: 0 var(--space-3)`。这条是 `7ee7f825` 引入的,开工时 staging 仍红,别处未修。
## 偏离
- `rectification-question-ownership` / `rectification-v10-conversation-focus` 不能把 spokenPrompt 恢复成「哪年上的大学」:服务端下一问仍是采集,BUG-529 要求题干命中领域词。默认 1 件事业现在下一问是感情,改用 `USER_COLLECT_QUESTION.relationship`
- `three dated events with one holdout` 仍锁 `domain: finance`(家人拒答后 dated 补采集,不是轮转抢跑)。`45bdb63e` 未锁 domain;本单不删这条,因为行为与 `3847e9c9` 补第三件事一致。
- `collect_retry: true``USER_COLLECT_QUESTION_RETRY.relationship` 实际在 `rectification-walkthrough-polish`,已恢复。v10 文件里没有这两条断言。
## 测试
`tsc --noEmit`exit=0。
聚焦套件 `tsx --test tests/rectification-*.test.ts tests/admin-model-management-ui-contract.test.ts`
```
# tests 875
# pass 875
# fail 0
# cancelled 0
# skipped 0
# todo 0
exit=0
```
新断言在 `rectification-collect-direction-20260904.test.ts`9/9。
全量 `npm test`(本机有 Docker):
```
# tests 2656
# pass 2654
# fail 2
# cancelled 0
# skipped 0
# todo 0
exit=1
```
2 条失败均为 Docker 迁移争用(`database migration failed`):`database-personal-report-sections``rectification-v9-database``v9 agent api migration…``--test-concurrency=1` 隔离重跑:
```
# tests 8
# pass 8
# fail 0
# cancelled 0
# skipped 0
# todo 0
exit=0
```
`eslint` 本单文件 0 error`choice-card` / `walkthrough-polish` 各 1 条既有 unused 警告,未触碰。
未改 Python;未声称系统 `python3` 预检通过。
## 改动文件
- 计划:`frontend/src/lib/rectification-agentic/v9/method-followup.ts`
- 新断言:`frontend/tests/rectification-collect-direction-20260904.test.ts`
- 回退:eight-method、walkthrough-polish、question-ownership、choice-card、v10-conversation-focus
- 门禁:`frontend/tests/admin-model-management-ui-contract.test.ts`
- 文档:`docs/BUG_HISTORY.md``docs/tasks/README.md``CHANGELOG.md`、本文件
+1 -1
View File
@@ -48,7 +48,7 @@
| `TASK-rectification-adopt-narration-fix-20260904.md` | `PROGRESS-rectification-adopt-narration-fix-20260904.md` | 采用旁白 Agent 在点选入口从不运行(早退分支重算过期决策)+ 校验器/可观测/超时 | 已验收(5.1/5.2 实现通过;超时测试挂死事件循环,见修复单 2) | `13dded9f`BUG-521/522 |
| `TASK-rectification-adopt-narration-fix2-20260904.md` | `PROGRESS-rectification-adopt-narration-fix2-20260904.md` | 采用旁白超时用了 unref 的 `AbortSignal.timeout`,测试挂死取消同文件后三条用例,门禁 `npm test` 退出码 1 | 已验收(门禁通过,staging 已部署) | `45bdb63e`BUG-523 |
| `TASK-rectification-collect-direction-20260904.md` | `PROGRESS-rectification-collect-direction-20260904.md` | 可评分事件 2 条时盘外核对抢跑到刚拒答的家人领域,Agent 只能改写成不指向任何领域的泛问;缺第三件带年份的事却先问职业 | 已验收:5.1/5.3/5.4 通过;5.2 顺序错位(任务书责任)见修复单 | `codex/rectification-collect-direction-20260904`BUG-527530 |
| `TASK-rectification-collect-direction-fix-20260904.md` | `PROGRESS-rectification-collect-direction-fix-20260904.md` | dated 补采集分支插在方法轮转之前,新案例第二问被家人抢占感情/事业;`7ee7f825` CSS 与合同测试脱节挡住门禁 | 待执行 | `codex/rectification-collect-direction-fix-20260904`BUG-531532 |
| `TASK-rectification-collect-direction-fix-20260904.md` | `PROGRESS-rectification-collect-direction-fix-20260904.md` | dated 补采集分支插在方法轮转之前,新案例第二问被家人抢占感情/事业;`7ee7f825` CSS 与合同测试脱节挡住门禁 | 待验收 | `codex/rectification-collect-direction-fix-20260904`BUG-531532 |
| `TASK-rectification-ux-20260902.md` | `PROGRESS-rectification-ux-20260903.md` | 会话面空白假死与交互摩擦 | 已验收 | `d159f08e`(09-03 在新基线重做后合入,BUG-505509 |
### 聊天主链路与首页
@@ -1843,10 +1843,6 @@ export function buildMethodFollowupPlan(input: {
source: "method_coverage",
});
}
if (!next && !meetsAcceptanceEventQuality(input.evidence)) {
const dated = nextDatedCollectFollowup(input.evidence, declined);
if (dated) next = makeFollowup(dated);
}
if (!next && dashaCovered && coverageComplete && occupationCovered) {
const allowLowGainDiscriminator = !coverageComplete || !candidatesSeparated;
const yearless = yearlessDiscriminators[0];
@@ -1911,6 +1907,7 @@ export function buildMethodFollowupPlan(input: {
const candidate = followupFromRanked(ranked);
return candidate.choice_frame ? candidate : null;
};
let datedCollect: MethodFollowup | null = null;
if (!next) {
if (!relationshipCovered && !declined.has("relationship")) {
next = sameDomainYearlessCard("relationship") ?? makeFollowup({
@@ -1956,6 +1953,11 @@ export function buildMethodFollowupPlan(input: {
source: "method_coverage",
...collectionYearFields(familyCollect),
});
} else if (
!meetsAcceptanceEventQuality(input.evidence)
&& (datedCollect = nextDatedCollectFollowup(input.evidence, declined))
) {
next = makeFollowup(datedCollect);
} else if (!occupationCovered) {
if (meetsAcceptanceEventQuality(input.evidence)) {
for (const ranked of yearlessDiscriminators) {
@@ -115,9 +115,12 @@ test("model management renders localized labels without changing stored enum val
});
test("global native input sizing excludes Ant Design picker internals", () => {
// 原值: padding: 0 12px
// 新值: padding: 0 var(--space-3)
// 原因: 7ee7f825 把间距收敛到 token;合同测试跟随 CSS,不改业务代码。
assert.match(
globals,
/input:not\(\[type="radio"\]\):not\(\[type="checkbox"\]\):not\(\[class\^="ant-"\]\):not\(\[class\*=" ant-"\]\):not\(\.ant-picker input\), select[^\{]+\{ width: 100%; min-height: 44px; padding: 0 12px;/,
/input:not\(\[type="radio"\]\):not\(\[type="checkbox"\]\):not\(\[class\^="ant-"\]\):not\(\[class\*=" ant-"\]\):not\(\.ant-picker input\), select[^\{]+\{ width: 100%; min-height: 44px; padding: 0 var\(--space-3\);/,
);
assert.match(
globals,
@@ -605,10 +605,10 @@ test("GET hides a legacy known-event quality focus and resumes evidence collecti
expectedAnswerSchema: { choice: SAMPLE_COPY },
},
});
// 原值: d9_relationship
// 新值: relatives
// 原因: 丢弃遗留质量焦点后继续采集;一件学业未开训练门,下一问是家人。
assert.equal(plan.next_followup?.method_id, "relatives");
// 原值: relatives
// 新值: d9_relationship
// 原因: dated 分支移到家人之后,前两问恢复感情 / 事业
assert.equal(plan.next_followup?.method_id, "d9_relationship");
assert.equal(plan.next_followup?.choice_frame, null);
});
@@ -100,6 +100,43 @@ function collectPlan(
});
}
test("method rotation asks relationship and career before dated collect", () => {
const careerOnly = buildMethodFollowupPlan({
evidence: [dated("career", "2011", { eventKind: "career_entry" })],
holdoutValidation: "not_started",
});
assert.equal(careerOnly.next_followup?.method_id, "d9_relationship");
assert.equal(careerOnly.next_followup?.domain, "relationship");
const relationshipOnly = buildMethodFollowupPlan({
evidence: [dated("relationship", "2014", { eventKind: "relationship_start" })],
holdoutValidation: "not_started",
});
assert.equal(relationshipOnly.next_followup?.method_id, "d10_career");
assert.equal(relationshipOnly.next_followup?.domain, "career");
const both = buildMethodFollowupPlan({
evidence: TWO_SCOREABLE,
holdoutValidation: "not_started",
});
assert.equal(both.next_followup?.method_id, "relatives");
assert.equal(both.next_followup?.domain, "family");
const familyDeclined = collectPlan(TWO_SCOREABLE);
assert.equal(familyDeclined.next_followup?.method_id, "d5_education");
assert.equal(familyDeclined.next_followup?.domain, "education");
assert.notEqual(familyDeclined.next_followup?.domain, "occupation");
const educationDeclined = collectPlan(TWO_SCOREABLE, {
declinedTopics: [
...FAMILY_DECLINED,
{ target_domain: "education", status: "declined", intent: "collect_method_evidence" },
],
});
assert.equal(educationDeclined.next_followup?.method_id, "d2_finance");
assert.equal(educationDeclined.next_followup?.domain, "finance");
});
test("two scoreable events do not open OOS even when holdout prompts exist", () => {
assert.equal(meetsAcceptanceEventQuality(TWO_SCOREABLE), false);
const declined = new Set(["family"]);
@@ -54,6 +54,8 @@ const FAMILY_ID = "44444444-4444-4444-8444-444444444443";
const CAREER_ID = "44444444-4444-4444-8444-444444444444";
const UNIQUE_MINUTE_COPY = /±5 分钟确定性/;
// 本单回退 3847e9c9 把训练门未开的下一问改成家人的断言。见各断言旁三栏。
function collectDecision(candidates: readonly { time: string; relativeSupport?: number }[]) {
return decideRectification({
engineCeiling: OPEN_ENGINE_CAPABILITY_CEILING,
@@ -365,7 +367,7 @@ function stubEngine(response: unknown) {
};
}
test("eight-method routing asks family after dated education, not relocation", () => {
test("eight-method routing asks relationship after dated education, not relocation", () => {
const plan = buildMethodFollowupPlan({
evidence: [{
status: "confirmed",
@@ -375,11 +377,11 @@ test("eight-method routing asks family after dated education, not relocation", (
occurredTo: null,
}],
});
// 原值: d9_relationship / relationship
// 新值: relatives / family
// 原因: 训练门未开时主流程走 datedCollectOrder,家人在感情之前;SQL missing 的 relocation 仍不进轮转。
assert.equal(plan.next_followup?.method_id, "relatives");
assert.equal(plan.next_followup?.domain, "family");
// 原值: relatives / family
// 新值: d9_relationship / relationship
// 原因: dated 分支移到家人之后,前两问恢复感情 / 事业
assert.equal(plan.next_followup?.method_id, "d9_relationship");
assert.equal(plan.next_followup?.domain, "relationship");
assert.equal(plan.stop_domain_rotation, true);
assert.deepEqual([...plan.not_in_rotation], ["relocation"]);
assert.equal(plan.methods.find((item) => item.method_id === "appearance")?.status, "skipped_by_policy");
@@ -390,11 +392,11 @@ test("dasha conflict probe does not jump ahead of method rotation before accepta
evidence: [datedEvidence("education", "2016")],
eventProbes: [CAREER_CONFLICT_PROBE],
});
// 原值: method_coverage / d9_relationship
// 新值: method_coverage / relatives
// 原因: 训练门未开时先补家人采集,冲突探针仍不得抢跑。
// 原值: method_coverage / relatives
// 新值: method_coverage / d9_relationship
// 原因: dated 分支移到家人之后,前两问恢复感情 / 事业
assert.equal(plan.next_followup?.source, "method_coverage");
assert.equal(plan.next_followup?.method_id, "relatives");
assert.equal(plan.next_followup?.method_id, "d9_relationship");
assert.equal(plan.next_followup?.choice_frame, null);
assert.notEqual(plan.next_followup?.source, "event_probe");
});
@@ -404,11 +406,11 @@ test("known exam quality does not create a scoring card after one recorded event
evidence: [datedEvidence("education", "2016")],
eventProbes: [EDUCATION_QUALITY_PROBE, CAREER_CONFLICT_PROBE],
});
// 原值: method_coverage / d9_relationship
// 新值: method_coverage / relatives
// 原因: 一条学业记录未开训练门,先问家人而不是质量卡。
// 原值: method_coverage / relatives
// 新值: method_coverage / d9_relationship
// 原因: dated 分支移到家人之后,前两问恢复感情 / 事业
assert.equal(plan.next_followup?.source, "method_coverage");
assert.equal(plan.next_followup?.method_id, "relatives");
assert.equal(plan.next_followup?.method_id, "d9_relationship");
assert.equal(plan.next_followup?.choice_frame, null);
assert.notEqual(plan.next_followup?.choice_kind, "event_quality");
});
@@ -421,11 +423,11 @@ test("career known-event quality does not jump the adoption gate", () => {
],
eventProbes: [CAREER_QUALITY_PROBE, CAREER_CONFLICT_PROBE],
});
// 原值: method_coverage / d9_relationship
// 新值: method_coverage / relatives
// 原因: 两条事业记录仍不足训练门(领域数不够),先补家人。
// 原值: method_coverage / relatives
// 新值: method_coverage / d9_relationship
// 原因: dated 分支移到家人之后,前两问恢复感情 / 事业
assert.equal(plan.next_followup?.source, "method_coverage");
assert.equal(plan.next_followup?.method_id, "relatives");
assert.equal(plan.next_followup?.method_id, "d9_relationship");
assert.equal(plan.next_followup?.choice_frame, null);
assert.notEqual(plan.next_followup?.source, "event_probe");
assert.notEqual(plan.next_followup?.choice_kind, "event_quality");
@@ -436,11 +438,11 @@ test("encoded exam quality does not stamp another card and keeps method rotation
evidence: [datedEvidence("education", "2016", { summary: "2016年高考发挥异常" })],
eventProbes: [EDUCATION_QUALITY_PROBE, CAREER_CONFLICT_PROBE],
});
// 原值: method_coverage / d9_relationship
// 新值: method_coverage / relatives
// 原因: 编码质量探针仍不得在训练门前出卡;下一问是家人采集。
// 原值: method_coverage / relatives
// 新值: method_coverage / d9_relationship
// 原因: dated 分支移到家人之后,前两问恢复感情 / 事业
assert.equal(plan.next_followup?.source, "method_coverage");
assert.equal(plan.next_followup?.method_id, "relatives");
assert.equal(plan.next_followup?.method_id, "d9_relationship");
assert.equal(plan.next_followup?.choice_frame, null);
});
@@ -504,11 +506,11 @@ test("three scoreable events in one domain still rotate methods instead of rever
],
eventProbes: [CAREER_CONFLICT_PROBE],
});
// 原值: method_coverage / d9_relationship
// 新值: method_coverage / relatives
// 原因: 三件都在学业,领域数不足训练门,先补家人而不是反推。
// 原值: method_coverage / relatives
// 新值: method_coverage / d9_relationship
// 原因: dated 分支移到家人之后,前两问恢复感情 / 事业
assert.equal(plan.next_followup?.source, "method_coverage");
assert.equal(plan.next_followup?.method_id, "relatives");
assert.equal(plan.next_followup?.method_id, "d9_relationship");
assert.equal(plan.next_followup?.choice_frame, null);
});
@@ -528,11 +530,11 @@ test("occupation_note does not count toward reverse-inference event quality", ()
],
eventProbes: [CAREER_CONFLICT_PROBE],
});
// 原值: method_coverage / d9_relationship
// 新值: method_coverage / relatives
// 原因: occupation_note 不计训练门,下一问仍是家人采集。
// 原值: method_coverage / relatives
// 新值: method_coverage / d9_relationship
// 原因: dated 分支移到家人之后,前两问恢复感情 / 事业
assert.equal(plan.next_followup?.source, "method_coverage");
assert.equal(plan.next_followup?.method_id, "relatives");
assert.equal(plan.next_followup?.method_id, "d9_relationship");
assert.equal(plan.next_followup?.choice_frame, null);
});
@@ -558,10 +560,10 @@ test("age-band probe does not jump ahead of uncovered relationship", () => {
role: "reverse_verify",
}],
});
// 原值: d9_relationship
// 新值: relatives
// 原因: 年龄带探针不得抢跑;训练门未开时先问家人,不是感情。
assert.equal(plan.next_followup?.method_id, "relatives");
// 原值: relatives
// 新值: d9_relationship
// 原因: dated 分支移到家人之后,前两问恢复感情 / 事业
assert.equal(plan.next_followup?.method_id, "d9_relationship");
assert.equal(plan.next_followup?.source, "method_coverage");
assert.equal(plan.next_followup?.choice_frame, null);
});
@@ -618,10 +620,10 @@ test("user-stop action offers a range when candidates already exist", () => {
assert.equal(plan.methods.find((item) => item.method_id === "marks")?.status, "skipped_by_policy");
assert.equal(plan.methods.find((item) => item.method_id === "horary")?.status, "uncovered");
assert.equal(plan.methods.find((item) => item.method_id === "occupation")?.status, "uncovered");
// 原值: d9_relationship
// 新值: relatives
// 原因: 一件学业未开训练门,用户停下时仍在采集家人。
assert.equal(plan.next_followup?.method_id, "relatives");
// 原值: relatives
// 新值: d9_relationship
// 原因: dated 分支移到家人之后,前两问恢复感情 / 事业
assert.equal(plan.next_followup?.method_id, "d9_relationship");
assert.match(plan.next_followup?.user_prompt_hint ?? "", /自然语言/);
assert.doesNotMatch(plan.next_followup?.user_prompt_hint ?? "", /A\/B\/C\/D/);
assert.equal(plan.next_followup?.choice_frame, null);
@@ -647,10 +649,10 @@ test("selectionAllowed with remaining method follow-up keeps collecting and offe
nextFollowup: plan.next_followup,
workingTime: "05:07",
});
// 原值: d9_relationship
// 新值: relatives
// 原因: 训练门未开时继续采集家人,停下才给区间。
assert.equal(plan.next_followup?.method_id, "relatives");
// 原值: relatives
// 新值: d9_relationship
// 原因: dated 分支移到家人之后,前两问恢复感情 / 事业
assert.equal(plan.next_followup?.method_id, "d9_relationship");
assert.equal(action.id, "ask_method_followup");
assert.equal(action.on_user_stop.id, "offer_provisional_range");
});
@@ -667,10 +669,10 @@ test("adopt_representative defers method follow-up instead of asking this turn",
sessionOutcome: "adopt_representative",
});
assert.equal(plan.next_followup, null);
// 原值: d9_relationship
// 新值: relatives
// 原因: 采用轮仍 stash 下一采集题;训练门未开时那题是家人。
assert.equal(plan.deferred_followup?.method_id, "relatives");
// 原值: relatives
// 新值: d9_relationship
// 原因: dated 分支移到家人之后,前两问恢复感情 / 事业
assert.equal(plan.deferred_followup?.method_id, "d9_relationship");
assert.equal(plan.session_outcome, "adopt_representative");
});
@@ -699,11 +701,11 @@ test("declined relationship skips to career and leaves horary uncovered", () =>
}],
declinedTopics: [{ target_domain: "relationship", status: "declined" }],
});
// 原值: d10_career / career
// 新值: relatives / family
// 原因: 感情拒答只跳过感情;训练门未开时下一件带年份的是家人,不是事业
assert.equal(plan.next_followup?.method_id, "relatives");
assert.equal(plan.next_followup?.domain, "family");
// 原值: relatives / family
// 新值: d10_career / career
// 原因: dated 分支移到家人之后,前两问恢复感情 / 事业
assert.equal(plan.next_followup?.method_id, "d10_career");
assert.equal(plan.next_followup?.domain, "career");
assert.deepEqual([...plan.do_not_poll], []);
assert.equal(plan.methods.find((item) => item.method_id === "horary")?.status, "uncovered");
});
@@ -938,11 +940,11 @@ test("read-case follows method plan and keeps D9/D10 type tables when SQL missin
}>;
}).execute({ caseId: CASE_ID, projection: "full_diagnostics" });
assert.deepEqual(projection.conversation_summary.missing_evidence_categories, ["relocation", "health", "finance"]);
// 原值: d9_relationship / relationship
// 新值: d2_finance / finance
// 原因: 学业+家人+事业三条里 1 条 holdout,训练门未开;datedCollectOrder 里家人/学业已有记录,下一问是财务。仍不按 SQL missing 先问搬家。
assert.equal(projection.method_followup_plan.next_followup?.method_id, "d2_finance");
assert.equal(projection.method_followup_plan.next_followup?.domain, "finance");
// 原值: d2_finance / finance
// 新值: d9_relationship / relationship
// 原因: dated 分支移到家人之后,前两问恢复感情 / 事业
assert.equal(projection.method_followup_plan.next_followup?.method_id, "d9_relationship");
assert.equal(projection.method_followup_plan.next_followup?.domain, "relationship");
assert.equal(projection.method_followup_plan.deferred_followup, null);
assert.equal(projection.method_followup_plan.session_outcome, "collect_evidence");
assert.equal(
@@ -1626,10 +1628,10 @@ test("precision stage lagna_frame waits for uncovered career before asking anoth
],
precisionStage: "lagna_frame",
});
// 原值: d10_career
// 新值: relatives
// 原因: 学业+感情仍不足训练门,lagna_frame 不得先问另一件泛问;先补家人。
assert.equal(plan.next_followup?.method_id, "relatives");
// 原值: relatives
// 新值: d10_career
// 原因: dated 分支移到家人之后,前两问恢复感情 / 事业
assert.equal(plan.next_followup?.method_id, "d10_career");
assert.equal(plan.next_followup?.source, "method_coverage");
assert.equal(isOfferBlockingFollowup(plan.next_followup, plan.methods), true);
assert.doesNotMatch(JSON.stringify(plan), UNIQUE_MINUTE_COPY);
@@ -2726,9 +2728,9 @@ test("three dated events with one holdout keep collecting instead of discriminat
precisionStage: "lagna_frame",
eventProbes: [CAREER_CONFLICT_PROBE],
});
// 原值: method_coverage(方法轮转
// 原值: method_coverage / finance3847e9c9 新增 domain 锁
// 新值: method_coverage / finance
// 原因: 家人已拒答,学业/事业/感情已有记录,holdout 占用 1 条,训练门未开,补财务不是区分
// 原因: dated 分支在家人之后;家人已拒答,下一件带年份仍是财务不是轮转抢跑
assert.equal(plan.next_followup?.source, "method_coverage");
assert.equal(plan.next_followup?.intent, "collect_method_evidence");
assert.equal(plan.next_followup?.domain, "finance");
@@ -2800,10 +2802,10 @@ test("adjacent education year does not re-ask enrollment after a recorded start"
});
assert.notEqual(plan.next_followup?.domain, "education");
assert.notEqual(plan.next_followup?.source, "event_probe");
// 原值: d10_career
// 新值: relatives
// 原因: 邻近年不得再问入学;训练门未开时下一问是家人,不是事业
assert.equal(plan.next_followup?.method_id, "relatives");
// 原值: relatives
// 新值: d10_career
// 原因: dated 分支移到家人之后,前两问恢复感情 / 事业
assert.equal(plan.next_followup?.method_id, "d10_career");
assert.match(plan.next_followup?.user_prompt_hint ?? "", /2016 年入学/);
assert.match(plan.next_followup?.user_prompt_hint ?? "", /2024 年感情结束/);
assert.match(plan.next_followup?.user_prompt_hint ?? "", /不要再问这些事发生在哪一年/);
@@ -402,10 +402,10 @@ test("collect set-focus writes collect:true even if Agent sends choice", async (
caseId: CASE_ID,
questionId: followup ? stableFollowupQuestionId(followup) : "collect:relationship:collect_method_evidence",
intent: "collect_method_evidence",
// 原值: 「你还记得哪年上的大学吗?
// 新值: USER_COLLECT_QUESTION[domain](默认 dossier 一件事业 → 家人
// 原因: 采集题干必须命中服务端领域关键词,大学不命中家人
spokenPrompt: USER_COLLECT_QUESTION[followup?.domain ?? "family"] ?? USER_COLLECT_QUESTION.family,
// 原值: USER_COLLECT_QUESTION.family3847e9c945bdb63e 为「哪年上的大学」
// 新值: USER_COLLECT_QUESTION[followup.domain](默认 dossier 一件事业 → 感情
// 原因: dated 分支移到家人之后,前两问恢复感情;BUG-529 仍要求题干命中领域词,大学不命中感情
spokenPrompt: USER_COLLECT_QUESTION[followup?.domain ?? "relationship"] ?? USER_COLLECT_QUESTION.relationship,
expectedAnswerSchema: {
choice: {
prompt: "伪造采集",
@@ -140,10 +140,10 @@ test("set/resolve focus tools return safe projections and never infer the active
caseId: CASE_ID,
questionId: "career-month-question",
intent: "clarify_event_date",
// 原值: 「你还记得哪年上的大学吗?
// 新值: USER_COLLECT_QUESTION.family
// 原因: 默认 dossier 一件事业,训练门未开时下一问是家人采集;大学不命中家人关键词。
spokenPrompt: USER_COLLECT_QUESTION.family,
// 原值: USER_COLLECT_QUESTION.family3847e9c945bdb63e 为「哪年上的大学」
// 新值: USER_COLLECT_QUESTION.relationship
// 原因: dated 分支移到家人之后,前两问恢复感情;服务端下一问仍是采集,BUG-529 要求题干命中感情词。
spokenPrompt: USER_COLLECT_QUESTION.relationship,
}) as Record<string, unknown>;
assert.equal(setResult.ok, undefined);
assert.equal(setResult.focus_id, FOCUS_ID);
@@ -459,11 +459,12 @@ test("same-domain collect retry uses structured closed-focus state, not body mat
}],
sessionOutcome: "collect_evidence",
});
// 原值: relationship / collect_retry true
// 新值: family
// 原因: 一件事业未开训练门,datedCollectOrder 先问家人;已关闭的感情采集不挡家人。retry 文案仍由上面的 constructed followup 覆盖。
assert.equal(plan.next_followup?.domain, "family");
assert.equal(spokenFollowupForUser(plan.next_followup), USER_COLLECT_QUESTION.family);
// 原值: family
// 新值: relationship / collect_retry true / USER_COLLECT_QUESTION_RETRY.relationship
// 原因: dated 分支移到家人之后,前两问恢复感情 / 事业
assert.equal(plan.next_followup?.domain, "relationship");
assert.equal(plan.next_followup?.collect_retry, true);
assert.equal(spokenFollowupForUser(plan.next_followup), USER_COLLECT_QUESTION_RETRY.relationship);
const declined = buildMethodFollowupPlan({
evidence: [{
status: "confirmed",