fix(rectification): say 这两分钟 only when two candidates remain (BUG-692)
Count-aware closed-pool invite, catch the five stale contract assertions, and mark M1b V1/V2 as not_measured.
This commit is contained in:
@@ -1,5 +1,9 @@
|
||||
# 印度占星 Skill 更新日志
|
||||
|
||||
## 2026-09-15 — 并列邀请按候选数说话,不再把 4 个候选叫成「这两分钟」
|
||||
|
||||
生时校正里,固定采集线问完后的补充邀请会按还剩几个候选来写:剩 2 个说「这两分钟」,多于 2 个说「这几个候选」,数不清楚时不说这半句。不再出现同一段话里「4 个候选」和「这两分钟」打架。Skill 版本不变。
|
||||
|
||||
## 2026-09-14 — 报上来的时间会标明是记录还是大概记得
|
||||
|
||||
生时校正里,医院记录称作「出生记录时间」,家人记得称作「你填的大概时间」,只知道时段称作「你给的时间段」,不再把后两类叫成「你的出生时间」。和目前范围比较时只说相差几分钟;有医院记录却落在范围外时,两者并列,不替你选边。Skill 版本不变。
|
||||
|
||||
@@ -10757,3 +10757,19 @@
|
||||
- 相关记录:BUG-587、BUG-621、BUG-689
|
||||
- 复发自:无
|
||||
- 修复版本:待发布
|
||||
|
||||
## BUG-692 | 交付邀请语写「这两分钟」却与同段「4 个候选」矛盾;5 条契约断言未跟上
|
||||
|
||||
- 状态:resolved
|
||||
- 首次发现:2026-09-15
|
||||
- 最近更新:2026-09-15
|
||||
- 影响面:`rangeDeliveryCollectClosed`、`rangeNarrowHint`、`exitAppendCalls`、`publicDecisionFields`、精度研究 M1b 判定
|
||||
- 用户现象:范围卡旁白先写「还剩 04:47–04:53 里 4 个候选」,紧接着写「这两分钟按现有信息分不开」。同时 `npm test` 从基线 27 条涨到 32 条:4 条 exhaustion-exit 因短语表不含新交付文案判成 0 条出口载体,1 条 decision-authority 深比较缺 `birth_time_source`。
|
||||
- 触发条件:固定七条采集线关闭后交付;剩余候选数 ≠ 2。或在含 BUG-689/690 的树上跑全量 `npm test`。
|
||||
- 根因:BUG-689 把 tie-break 的「这两分钟」硬编码进任意候选数的邀请语;BUG-689/690 改了用户可见文案和公开字段,但没同步两处锁契约的测试。staging 当时部署仍是 `cf972f40`,未影响真实用户。
|
||||
- 修复:邀请语按候选数出话(2 → 「这两分钟」;>2 → 「这几个候选」;未知 → 不说这半句)。`exitAppendCalls()` 加入「收到 … 这 N 分钟 / 按现有信息分不开 / 说出来我接着算」,保持 `assert.equal(gates.length, 1)`,不加「只微调排序」。公开字段深比较补 `birth_time_source`。M1b 的 V1/V2 从 `no_benefit` 勘误为 `not_measured`。
|
||||
- 验证:`frontend/tests/rectification-open-collect-invite-20260914.test.ts`、`rectification-exhaustion-exit-20260906.test.ts`、`rectification-decision-authority.test.ts`。
|
||||
- 防复发:文案里出现的数字必须来自同一份数据,不得硬编码;改用户可见文案或公开字段时,必须同步跑 `npm test` 并把失败数与基线比对。
|
||||
- 相关记录:BUG-689、BUG-690、BUG-688、BUG-687、BUG-587
|
||||
- 复发自:BUG-689(邀请语复用两候选句)、BUG-690(公开字段未进深比较)
|
||||
- 修复版本:待发布
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
## 结论
|
||||
|
||||
**无收益,不立实现单。** 线上 `MIN_BOUNDARY_DAYS=45` / 刷新 30 与等权分盘保持不动。真机卡住的 ±10 窗上,放宽闸门能把宽度从 15 收到 11,但头名从 0.80 掉到 0.75;宽窗上宽度反而变大。V1/V2 与基线相同,V3(D60)在 ±10 把头名降到 0.70。唯一看起来像过门的格子是 G1+V3 在 ±10(命中仍 0.80、宽度 15→11),宽窗上不成立,也没有对该组合做 ±7 记错,不够立实现单。
|
||||
**无收益,不立实现单。** 线上 `MIN_BOUNDARY_DAYS=45` / 刷新 30 与等权分盘保持不动。真机卡住的 ±10 窗上,放宽闸门能把宽度从 15 收到 11,但头名从 0.80 掉到 0.75;宽窗上宽度反而变大。V1/V2 与基线相同(**勘误:标为未测,见 M1b**),V3(D60)在 ±10 把头名降到 0.70。唯一看起来像过门的格子是 G1+V3 在 ±10(命中仍 0.80、宽度 15→11),宽窗上不成立,也没有对该组合做 ±7 记错,不够立实现单。
|
||||
|
||||
判定口径:命中不降 **且** 宽度下降才算有收益;只多出题而宽度变宽不算。±7 天记错挤出真值的档位一律不推荐。本网格覆盖率始终 20/20,M2 挤出全是 0。
|
||||
|
||||
@@ -18,8 +18,8 @@
|
||||
| G2 | no_benefit | ±10:no_benefit squeezed7=0;±30:no_benefit squeezed7=0;±60:no_benefit squeezed7=0 |
|
||||
| G3 | no_benefit | ±10:no_benefit squeezed7=0;±30:no_benefit squeezed7=0;±60:no_benefit squeezed7=0 |
|
||||
| G4 | no_benefit | ±10:no_benefit squeezed7=0;±30:no_benefit squeezed7=0;±60:no_benefit squeezed7=0 |
|
||||
| V1 | no_benefit | ±10:no_benefit;±30:no_benefit;±60:no_benefit |
|
||||
| V2 | no_benefit | ±10:no_benefit;±30:no_benefit;±60:no_benefit |
|
||||
| V1 | not_measured | 原判定 `no_benefit`;见 M1b 勘误 |
|
||||
| V2 | not_measured | 原判定 `no_benefit`;见 M1b 勘误 |
|
||||
| V3 | no_benefit | ±10:no_benefit;±30:no_benefit;±60:no_benefit |
|
||||
|
||||
过运与三大外部引擎未参与本单(纯出题闸门 / 分盘配权,不解释运势)。
|
||||
@@ -171,6 +171,10 @@ V0 生产等权;V1 `min(window/varga_minutes, 1)` 再除以 `2n`;V2 只计
|
||||
| G3+V3 | 0.4 | 1 | 64 | 0 | 6.2 |
|
||||
| G4+V3 | 0.4 | 1 | 64 | 0 | 6.25 |
|
||||
|
||||
### 勘误(2026-09-15,BUG-692)
|
||||
|
||||
总表原判定 V1 / V2 = `no_benefit`。上节已写明「V0 采集误走了生产计分、没有打上分盘标签」,且 V1 与 V2 在 ±10 / ±30 / ±60 上的头名命中、覆盖、宽度、并列、挤出与 V0 **完全相同**。V3 在 ±10 把头名从 0.80 降到 0.70,说明配权机制能改数字,因此 V1/V2 与 V0 相同更像是权重没真正打上,而不是「测过且无收益」。**V1/V2 的 `no_benefit` 作废,改标 `not_measured`。** V3 结论保留。上面数字表未改。重跑与否另立项。
|
||||
|
||||
## M2 · 答错容忍度(日精度事件日期偏移)
|
||||
|
||||
组 A 的日精度事件按例、按事件独立随机偏移(种子 20260914,排除 0)。±7 天出现真值挤出的档位不得推荐。
|
||||
|
||||
@@ -78,6 +78,7 @@
|
||||
| `docs/research/minute_resolution_2026_09_14.md` | 第一轮:R1–R5 打分尺度测量(**文末有勘误,务必读**) |
|
||||
| `docs/research/cluster_width_2026_09_14.md` | 第二轮:宽度口径校正、簇结构画像、W1–W3 |
|
||||
| `docs/research/holdout_v4_build_2026_09_14.md` | v4 封存基准的构建与标注协议 |
|
||||
| `docs/research/precision_gate_2026_09_14.md` | 出题闸门分档与 M1b 分盘配权。**勘误:M1b 的 V1/V2 标为未测**(与 V0 数字完全相同,且 V0 采集未打分盘标签);V3 结论保留 |
|
||||
| `scripts/research/minute_resolution_sweep.py` / `cluster_width_probe.py` | 两轮的测量脚本(离线,未接生产) |
|
||||
| `tests/test_minute_resolution_research.py` / `test_cluster_width_research.py` | 两轮脚本的回归测试 |
|
||||
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
# 进度 · 邀请文案按候选数出话 + 5 条断言跟上(2026-09-15)
|
||||
|
||||
## 范围
|
||||
|
||||
- 分支:`codex/rectification-invite-copy-fix-20260915`(基于 `origin/staging` @ `81f8183c`)
|
||||
- 任务单:`docs/tasks/TASK-rectification-invite-copy-and-assertions-fix-20260915.md`
|
||||
- BUG-692 resolved(复发自 BUG-689 / BUG-690)
|
||||
- 未改打分、判据、置信度、确认门、数据库、Skill 版本;未回退 BUG-688 的「恰好一条」与「不含只微调排序」
|
||||
|
||||
## 完成
|
||||
|
||||
- T2:`exitAppendCalls()` 短语表加入「收到 … 这 N 分钟 / 按现有信息分不开 / 说出来我接着算」。`assert.equal(gates.length, 1)` 仍在。公开字段深比较补 `birth_time_source`,保持 `deepEqual`。
|
||||
- T1:`rangeDeliveryCollectClosed(candidateCount)`。2 → 「这两分钟」;>2 → 「这几个候选」;未知 → 只说「按现有信息分不开」。`rangeNarrowHint` 两处空池返回点传入 `candidateCount`。交付卡从 hint 抽出同一句邀请,避免再硬编码。
|
||||
- T3:`precision_gate_2026_09_14.md` M1b 追加勘误,总表 V1/V2 改 `not_measured`(原 `no_benefit` 留在勘误里)。闭包索引注明 V1/V2 未测。V3 保留。
|
||||
- 未走让步。
|
||||
|
||||
## 断言三栏(既有断言)
|
||||
|
||||
| 位置 | 原值 | 新值 | 原因 |
|
||||
| --- | --- | --- | --- |
|
||||
| `exitAppendCalls()` 短语表 | `能问的都问完了` 等旧收口 | 加上「收到 … 这 N 分钟 / 按现有信息分不开 / 说出来我接着算」 | BUG-689 交付邀请就是出口载体;BUG-692 跟上。不加「只微调排序」 |
|
||||
| `last closed-ceiling…` 旁白匹配 | 同上旧闸句 | 同上新短语;`gates.length === 1` 不动 | 同上 |
|
||||
| `public decision fields…` | 深比较不含 `birth_time_source` | 含 `birth_time_source`,仍深比较 | BUG-690 公开字段;不得改成部分匹配 |
|
||||
|
||||
## 验收数字
|
||||
|
||||
- 新测:`closed-pool invite names two minutes only when two candidates remain` 通过;既有 open-collect-invite 3 条仍绿(现 4/4)
|
||||
- 定向:`rectification-decision-authority`(含 `public decision fields`)、`agent-voice-copy-contract`、`rectification-collection-question-pool`、`rectification-probe-pool-exhausted-20260911`、`rectification-tiebreak-before-card-20260914`、`rectification-birth-time-provenance-20260914` 全绿
|
||||
- `tsc --noEmit`:0 错
|
||||
- `npm run lint`:0 error(既有 warning 未顺手改)
|
||||
- 全量 `npm test`:3023 / pass 2856 / fail 153 / skip 14。失败条数相对本机 Windows 上一轮 154 少 1(`public decision fields` 已绿)。失败类型仍是 CRLF、Skill SHA、Docker/DB、无 symlink。Linux 门禁基线 27;本轮修的 5 条里,authority 已在本机绿,exhaustion-exit 四条因 Skill SHA 无法在本机导入,留给 Linux 门禁。
|
||||
- `next build`:本工作树 `frontend/node_modules` 是 junction,未跑
|
||||
|
||||
## 环境缺口
|
||||
|
||||
- 无 staging 登录态、无 Chrome:真机清单见 `docs/testing/rectification-invite-copy-fix-20260915.md`
|
||||
- `next build`:本工作树 `frontend/node_modules` 是 junction,未跑;`/` 是否仍 `○ Static` 需在有真实 `node_modules` 的树上复核
|
||||
@@ -187,7 +187,7 @@
|
||||
|
||||
| `TASK-rectification-tiebreak-hold-exit-fix-20260914.md` | `PROGRESS-rectification-tiebreak-hold-exit-20260914.md` | **P0 回归修复单**:BUG-686 的风格题前置把耗尽与收口路径的交付也压住了,`npm test` 由 27 红涨到 33(6 条「该交付却不交付」)。后续 `6fd7925a` 只改断言把它们改绿:4 条文案放宽可接受,2 条实质弱化(「恰好一条交付闸」退化成短路、参考题确认语被算作出口载体)必须恢复;行为层根因未动。hold 要带出口、不得作用于 exhausted/closed-ceiling,最多 hold 一次(BUG-688)。尚未部署,未影响线上 | 待验收 | `codex/rectification-tiebreak-hold-exit-20260914` |
|
||||
|
||||
| `TASK-rectification-invite-copy-and-assertions-fix-20260915.md` | `PROGRESS-rectification-invite-copy-fix-20260915.md` | **P0 验收修复单**:BUG-689 的邀请语硬编码「这两分钟」却与同段话的「4 个候选」矛盾;BUG-689/690 各自把 4 条与 1 条契约断言留红(`npm test` 27→32)。修文案按候选数出话、两处断言跟上、并给精度研究 M1b 的 V1/V2 追加「未测」勘误(BUG-692) | 待执行 | `codex/rectification-invite-copy-fix-20260915` |
|
||||
| `TASK-rectification-invite-copy-and-assertions-fix-20260915.md` | `PROGRESS-rectification-invite-copy-fix-20260915.md` | **P0 验收修复单**:BUG-689 的邀请语硬编码「这两分钟」却与同段话的「4 个候选」矛盾;BUG-689/690 各自把 4 条与 1 条契约断言留红(`npm test` 27→32)。修文案按候选数出话、两处断言跟上、并给精度研究 M1b 的 V1/V2 追加「未测」勘误(BUG-692) | 待验收 | `codex/rectification-invite-copy-fix-20260915` |
|
||||
|
||||
| `TASK-rectification-record-conflict-copy-20260914.md` | `PROGRESS-rectification-record-conflict-copy-20260914.md` | BUG-690 的 D4 收口:产品 2026-09-14 拍板**记录优先、分歧如实呈现**——默认仍按出生记录时间排盘(行为已成立),校正区间标成「证据倾向」,采用入口在冲突时改成「改用校正结果」;系统不得宣布记录错或校正无效(BUG-691) | 待执行 | `codex/rectification-record-conflict-copy-20260914` |
|
||||
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
# 邀请语与候选数一致:真机清单(2026-09-15)
|
||||
|
||||
分支 `codex/rectification-invite-copy-fix-20260915`。本环境无 staging 登录态。
|
||||
|
||||
## BUG-692
|
||||
|
||||
- [ ] 固定七条线都答过「没有/跳过」后,范围卡仍出现,正文仍请再说一件记得确切哪一天的事、不限领域。
|
||||
- [ ] 交付卡上「现在还剩 … 里 N 个候选」与后面的「这两分钟 / 这几个候选」必须一致:N=2 才说「这两分钟」;N>2 说「这几个候选」,不得再写「这两分钟」。
|
||||
- [ ] 邀请其余部分不变:不限领域、日期优先例子、年月例子;不再出现「能问的都问完了」。
|
||||
@@ -5,7 +5,7 @@
|
||||
## BUG-689
|
||||
|
||||
- [ ] 固定七条线(结婚 / 收入 / 搬家 / 家人 / 学业 / 健康 / 职业)都答过「没有/跳过」后,范围卡仍出现。
|
||||
- [ ] 前两名相对可能性相差不超过约 3 个百分点时,卡的正文(不是浅色小字)写「这两分钟按现有信息分不开」,并请再说一件记得确切哪一天的事、不限领域;例子里至少有登记结婚那天、入职第一天、手术那天、孩子出生那天、拿到录取通知那天中的两个。记不得哪一天时,才退到年月。
|
||||
- [ ] 前两名相对可能性相差不超过约 3 个百分点时,卡的正文(不是浅色小字)写「按现有信息分不开」,并请再说一件记得确切哪一天的事、不限领域;例子里至少有登记结婚那天、入职第一天、手术那天、孩子出生那天、拿到录取通知那天中的两个。记不得哪一天时,才退到年月。句首按还剩几个候选:「2 个」说「这两分钟」,多于 2 个说「这几个候选」,不得和「N 个候选」打架。
|
||||
- [ ] 正文里不再出现「能问的都问完了」,也不再把已经说过没有的线写成「哪年结婚或订婚 / 哪年收入 / 家里哪年添丁」。
|
||||
- [ ] 「更像这个」和「新建对话按此时间排盘」的出口仍可用,邀请不挡住采用。
|
||||
- [ ] 并列出卡后,在输入框补一件带日期的经历(能到日更好,年月也可以):账本应记下这件事,范围或候选排序应有可见变化,或出现新的一问。不得停在「问完了」且没有任何反应。
|
||||
|
||||
+1
-1
@@ -234,7 +234,7 @@ The birth-time rectification session is the consultation transcript plus a house
|
||||
| `question-gap`, delivered | no current question, or the current question is a dead choice card, and the session already delivered a range (`completed_with_range` / `provisional_range` / adopt outcomes, or `tied_first`); range card or range line plus an exit note. Never “没有拿到下一个问题。” | enabled |
|
||||
| `verified_idle` | one closing line `postAdoptVerifyDone` under the still-visible range card (same assistant column); no spinner, no reload | enabled |
|
||||
| `choice-pending` | the answered card (`data-selected` fill, a top row “正在记录…”) and the same live row from “正在记录本次选择…” through the follow-up turn | enabled (typing queues), stop visible |
|
||||
| `candidates` | one range-delivery card titled “目前范围 …(对照了 N 件经历)”, a caption under the title that only names still-open lines; when the top two relative likelihoods are within 3 points and collect lines are closed, a body-size invite to add one more event of any kind, asking for an exact day first then falling back to year-month (never “能问的都问完了”); optional “再答两道参考题微调排序” only when unused D9/D10 remain; if those were already asked and the top two are still within one point, a line “这两分钟按现有信息分不开,参考题已经用过。”; then up to three compare columns (highest posterior first; “更像这个” adopts); a closed “查看验证报告” fold. On a normal convergence, unused style questions are asked before this card. Exhausted and closed-ceiling exits still deliver the card if a style question cannot be rendered. | enabled |
|
||||
| `candidates` | one range-delivery card titled “目前范围 …(对照了 N 件经历)”, a caption under the title that only names still-open lines; when the top two relative likelihoods are within 3 points and collect lines are closed, a body-size invite to add one more event of any kind, asking for an exact day first then falling back to year-month (never “能问的都问完了”). The invite’s subject matches the remaining count in the same paragraph: 2 → “这两分钟”, more than 2 → “这几个候选”, unknown → omit that clause. Optional “再答两道参考题微调排序” only when unused D9/D10 remain; if those were already asked and the top two are still within one point, a line “这两分钟按现有信息分不开,参考题已经用过.”; then up to three compare columns (highest posterior first; “更像这个” adopts); a closed “查看验证报告” fold. On a normal convergence, unused style questions are asked before this card. Exhausted and closed-ceiling exits still deliver the card if a style question cannot be rendered. | enabled |
|
||||
| `adopting` | “正在采用 HH:MM…” through the follow-up turn | enabled (typing queues), stop visible |
|
||||
| `confirmed` | “已确认校正时间:HH:MM” | enabled |
|
||||
| `readonly` | “该校正已结束,只能查看历史。” and “再次校正” | disabled |
|
||||
|
||||
@@ -46,7 +46,7 @@ Jyotisha 的可见文案是产品的一部分。正确性红线(真实性、
|
||||
| 生成中输入框变灰,回车没反应 | 生成中仍可打字;回车后显示「已排队,回答结束后发出」,可撤回 | 对标主流对话产品,焦点不丢。 |
|
||||
| 停止后出现红色告警条 | 已停止,已生成的内容保留;本次不会扣点。 | 停止是用户动作,不是出错。 |
|
||||
| 也可以再说一件你记得大概时间的事。 / 还差带月份的经历,领域不限。 | 训练门开后给区间卡,并写「如果还记得……范围还能再收一截」。材料不够时只写记下了哪几件、再来一件不是这类的、至少两个具体例子;输入框占位「再说一件带年月的事」。 | 训练门未开时永远给结果或精确缺口;不说「领域不限」「做不了」。交付后采集线关完的邀请见下一行。 |
|
||||
| 能问的都问完了 | 这两分钟按现有信息分不开。你要是还记得确切哪一天的事,不限领域,说出来我接着算——登记结婚那天、入职第一天、手术那天、孩子出生那天、拿到录取通知那天都成。记不得哪一天的,有年月也行,比如换专业、出国、打官司、创业。 | 固定七条线关完不等于校正结束。并列时先请确切日期,再退年月;「不限领域」只出现在交付卡邀请里。不承诺「说了确切日期就能定到分钟」。 |
|
||||
| 能问的都问完了 | 还剩 2 个候选时:「这两分钟按现有信息分不开。」还剩 4 个时:「这几个候选按现有信息分不开。」候选数未知时只说「按现有信息分不开。」后面接:你要是还记得确切哪一天的事,不限领域,说出来我接着算——登记结婚那天、入职第一天、手术那天、孩子出生那天、拿到录取通知那天都成。记不得哪一天的,有年月也行,比如换专业、出国、打官司、创业。 | 固定七条线关完不等于校正结束。句首的「这两分钟 / 这几个候选」必须和同段话里的候选数一致,不得硬编码。并列时先请确切日期,再退年月;「不限领域」只出现在交付卡邀请里。不承诺「说了确切日期就能定到分钟」。 |
|
||||
| 填报出生时间 05:10 / 与你的出生时间相差 7 分钟 | 医院记录:「出生记录时间 05:10」。家人记得:「你填的大概时间 05:10」「与你填的大概时间相差 7 分钟」。只知道时段:「你给的时间段」。 | 推算值不得称作「你的出生时间」。医院记录与目前范围不一致时只并列差值,不站队。 |
|
||||
| 口述采集没有停止按钮,范围小字不可点 | 选择题卡内有「先这样,先看当前范围」;生成中有「停止回答」。口述采集只有输入框,没有「没有」「记不清」芯片,也不挂在输入框上方。范围小字是状态句,不可点。 | 想跳过可以打字「没有」或「记不清」。 |
|
||||
| 只报一件后立刻按家人/学业/财务轮着问,题干带「2020 年前后」 | 记下后问「还有吗?比如……」(只列还没提过的具体事)。答「没有了」后才从用户说过的年份追问,例如「2020 年毕业后第一份工作大概哪年开始?」 | 先等用户说完;年份只来自用户,不用生日推。 |
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
RECTIFICATION_USER_COPY,
|
||||
REPRESENTATIVE_MINUTE_DISCLAIMER,
|
||||
rangeDeliveryCaptionWithoutClosedInvite,
|
||||
rangeDeliveryClosedInviteFromHint,
|
||||
rangeDeliveryEventCopy,
|
||||
rangeDeliveryShowsOpenCollectInvite,
|
||||
} from "@/lib/rectification-agentic/user-copy";
|
||||
@@ -42,13 +43,12 @@ export function RectificationRangeDelivery({
|
||||
const markdown = delivery?.verification_markdown
|
||||
?? result.verificationReportMarkdown;
|
||||
const sharedTraits = delivery?.shared_traits ?? [];
|
||||
const closedInvite = rangeDeliveryClosedInviteFromHint(delivery?.narrow_hint);
|
||||
const showOpenCollectInvite = rangeDeliveryShowsOpenCollectInvite({
|
||||
hint: delivery?.narrow_hint,
|
||||
columns,
|
||||
});
|
||||
const caption = showOpenCollectInvite || delivery?.narrow_hint?.includes(
|
||||
RECTIFICATION_USER_COPY.rangeDeliveryCollectClosed,
|
||||
)
|
||||
const caption = showOpenCollectInvite || closedInvite
|
||||
? rangeDeliveryCaptionWithoutClosedInvite(delivery?.narrow_hint)
|
||||
: delivery?.narrow_hint ?? null;
|
||||
|
||||
@@ -60,9 +60,9 @@ export function RectificationRangeDelivery({
|
||||
{caption ? (
|
||||
<p className="rectification-range-delivery__narrow">{caption}</p>
|
||||
) : null}
|
||||
{showOpenCollectInvite ? (
|
||||
{showOpenCollectInvite && closedInvite ? (
|
||||
<p className="rectification-range-delivery__invite">
|
||||
{RECTIFICATION_USER_COPY.rangeDeliveryCollectClosed}
|
||||
{closedInvite}
|
||||
</p>
|
||||
) : null}
|
||||
{delivery?.provenance_line ? (
|
||||
|
||||
@@ -136,6 +136,34 @@ export const RANGE_DELIVERY_OPEN_COLLECT_EXAMPLES = [
|
||||
"亲人离世",
|
||||
] as const;
|
||||
|
||||
/** Stable invite stem after the count-dependent subject. */
|
||||
export const RANGE_DELIVERY_COLLECT_CLOSED_INVITE = `你要是还记得确切哪一天的事,不限领域,说出来我接着算——${RANGE_DELIVERY_OPEN_COLLECT_DAY_EXAMPLES.join("、")}都成。记不得哪一天的,有年月也行,比如${RANGE_DELIVERY_OPEN_COLLECT_EXAMPLES.join("、")}。`;
|
||||
|
||||
const CLOSED_INVITE_HEAD = /(?:这两分钟|这几个候选)?按现有信息分不开。$/u;
|
||||
|
||||
/** Count=2 → 「这两分钟」; >2 → 「这几个候选」; unknown / 0 / 1 → omit. */
|
||||
export function rangeDeliveryCollectClosedSubject(candidateCount?: number): string | null {
|
||||
if (candidateCount === 2) return "这两分钟";
|
||||
if (typeof candidateCount === "number" && Number.isFinite(candidateCount) && candidateCount > 2) {
|
||||
return "这几个候选";
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function rangeDeliveryCollectClosed(candidateCount?: number): string {
|
||||
const subject = rangeDeliveryCollectClosedSubject(candidateCount);
|
||||
const head = subject ? `${subject}按现有信息分不开。` : "按现有信息分不开。";
|
||||
return `${head}${RANGE_DELIVERY_COLLECT_CLOSED_INVITE}`;
|
||||
}
|
||||
|
||||
export function rangeDeliveryClosedInviteFromHint(hint: string | null | undefined): string | null {
|
||||
if (!hint) return null;
|
||||
const at = hint.indexOf(RANGE_DELIVERY_COLLECT_CLOSED_INVITE);
|
||||
if (at < 0) return null;
|
||||
const head = hint.slice(0, at).match(CLOSED_INVITE_HEAD);
|
||||
return `${head?.[0] ?? ""}${RANGE_DELIVERY_COLLECT_CLOSED_INVITE}`;
|
||||
}
|
||||
|
||||
export const RECTIFICATION_USER_COPY = {
|
||||
genericCollectQuestion: GENERIC_COLLECT_QUESTION,
|
||||
collectQuestionByDomain: USER_COLLECT_QUESTION,
|
||||
@@ -173,7 +201,6 @@ export const RECTIFICATION_USER_COPY = {
|
||||
rangeDeliveryTieBreakEntry: "再答两道参考题微调排序",
|
||||
rangeDeliveryTieBreakAck: "只微调排序,不改目前范围。",
|
||||
rangeDeliveryTieBreakUsed: "这两分钟按现有信息分不开,参考题已经用过。",
|
||||
rangeDeliveryCollectClosed: `这两分钟按现有信息分不开。你要是还记得确切哪一天的事,不限领域,说出来我接着算——${RANGE_DELIVERY_OPEN_COLLECT_DAY_EXAMPLES.join("、")}都成。记不得哪一天的,有年月也行,比如${RANGE_DELIVERY_OPEN_COLLECT_EXAMPLES.join("、")}。`,
|
||||
verificationReportSummary: "查看验证报告",
|
||||
} as const;
|
||||
|
||||
@@ -192,8 +219,8 @@ export function rangeDeliveryCaptionWithoutClosedInvite(
|
||||
hint: string | null | undefined,
|
||||
): string | null {
|
||||
if (!hint) return null;
|
||||
const invite = RECTIFICATION_USER_COPY.rangeDeliveryCollectClosed;
|
||||
if (!hint.includes(invite)) return hint;
|
||||
const invite = rangeDeliveryClosedInviteFromHint(hint);
|
||||
if (!invite) return hint;
|
||||
const stripped = hint.replaceAll(invite, "").replace(/[。.]\s*$/u, "").trim();
|
||||
return stripped.length > 0 ? stripped : null;
|
||||
}
|
||||
@@ -202,8 +229,10 @@ export function rangeDeliveryShowsOpenCollectInvite(input: {
|
||||
hint: string | null | undefined;
|
||||
columns: readonly { probability_percent: number }[];
|
||||
}): boolean {
|
||||
const invite = RECTIFICATION_USER_COPY.rangeDeliveryCollectClosed;
|
||||
return Boolean(input.hint?.includes(invite) && rangeDeliveryTopTwoTied(input.columns));
|
||||
return Boolean(
|
||||
rangeDeliveryClosedInviteFromHint(input.hint)
|
||||
&& rangeDeliveryTopTwoTied(input.columns),
|
||||
);
|
||||
}
|
||||
|
||||
export const RANGE_DELIVERY_DOMAIN_LABEL: Readonly<Record<string, string>> = {
|
||||
@@ -557,7 +586,9 @@ export function listUserVisibleCopy(): string[] {
|
||||
RECTIFICATION_USER_COPY.rangeDeliveryTieBreakEntry,
|
||||
RECTIFICATION_USER_COPY.rangeDeliveryTieBreakAck,
|
||||
RECTIFICATION_USER_COPY.rangeDeliveryTieBreakUsed,
|
||||
RECTIFICATION_USER_COPY.rangeDeliveryCollectClosed,
|
||||
rangeDeliveryCollectClosed(2),
|
||||
rangeDeliveryCollectClosed(4),
|
||||
rangeDeliveryCollectClosed(),
|
||||
RECTIFICATION_USER_COPY.verificationReportSummary,
|
||||
postAdoptVerifyDoneCopy("04:53", false),
|
||||
rangeChangedAfterEvidence(["04:50", "04:57"], ["04:47", "05:15"]) ?? "",
|
||||
|
||||
@@ -340,7 +340,7 @@ function adoptHostNarration(input: {
|
||||
input.dossier.evidence,
|
||||
input.dossier.conversationSummary.declinedSkippedTopics,
|
||||
catalog.remainingSplitTimes,
|
||||
undefined,
|
||||
catalog.remainingCandidateCount,
|
||||
catalog.remainingCredibleRange,
|
||||
);
|
||||
if (!hint || delivered.includes(hint)) return delivered;
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
* value, not a fixed domain rotation. Age-band years never enter prompts.
|
||||
*/
|
||||
|
||||
import { RECTIFICATION_USER_COPY, USER_COLLECT_QUESTION } from "../user-copy.ts";
|
||||
import { rangeDeliveryCollectClosed, USER_COLLECT_QUESTION } from "../user-copy.ts";
|
||||
import { canonicalCollectDomain } from "./domain-alias.ts";
|
||||
|
||||
export const COLLECT_KIND_ORDER = [
|
||||
@@ -844,9 +844,9 @@ export function rangeNarrowHint(
|
||||
if (open.length === 0) {
|
||||
const range = clockRangePair(credibleRange) ?? clockRangePair(splitTimes);
|
||||
if (range && (candidateCount ?? 0) > 0) {
|
||||
return `现在还剩 ${range[0]}–${range[1]} 里 ${candidateCount} 个候选。${RECTIFICATION_USER_COPY.rangeDeliveryCollectClosed}`;
|
||||
return `现在还剩 ${range[0]}–${range[1]} 里 ${candidateCount} 个候选。${rangeDeliveryCollectClosed(candidateCount)}`;
|
||||
}
|
||||
return RECTIFICATION_USER_COPY.rangeDeliveryCollectClosed;
|
||||
return rangeDeliveryCollectClosed(candidateCount);
|
||||
}
|
||||
const remaining = remainingCandidatesLine(
|
||||
splitTimes,
|
||||
|
||||
@@ -2,6 +2,7 @@ import assert from "node:assert/strict";
|
||||
import { readFileSync } from "node:fs";
|
||||
import test from "node:test";
|
||||
|
||||
import { normalizeBirthTimeSource } from "../src/lib/rectification-agentic/birth-time-provenance.ts";
|
||||
import {
|
||||
ADOPT_OUTCOMES,
|
||||
decideRectification,
|
||||
@@ -706,6 +707,10 @@ test("public decision fields are derived from decideRectification", () => {
|
||||
credible_range: decision.credibleRange,
|
||||
// 旧 公开字段无 stop_reason → 新 只读暴露停止原因 → 保留 采用/确认门字段语义不变
|
||||
stop_reason: decision.stopReason ?? null,
|
||||
// 原值: 公开字段集合不含 birth_time_source
|
||||
// 新值: 与 publicNextAction 同集,含 birth_time_source
|
||||
// 原因: BUG-690 投影带来源标签;BUG-692 深比较跟上,不得改成部分匹配
|
||||
birth_time_source: normalizeBirthTimeSource(decision.birthTimeSource),
|
||||
});
|
||||
const publicOverlay = overlayPublicDecision({ selectionAllowed: true }, decision);
|
||||
assert.equal(publicOverlay.selectionAllowed, false);
|
||||
|
||||
@@ -551,12 +551,15 @@ function assistantAppendCalls(calls: Array<{ fn: string; args: Record<string, un
|
||||
));
|
||||
}
|
||||
|
||||
// 原值: /就能开始筛|现在记下的是|范围还能再收一截|目前范围|当前范围|范围已经收到|能问的都问完了/
|
||||
// 新值: 加上「收到 … 这 N 分钟 / 按现有信息分不开 / 说出来我接着算」
|
||||
// 原因: BUG-689 把「能问的都问完了」换成交付邀请;BUG-692 短语表跟上。不含「只微调排序」(BUG-688)。
|
||||
const EXIT_CARRIER_COPY = /就能开始筛|现在记下的是|范围还能再收一截|目前范围|当前范围|范围已经收到|能问的都问完了|收到.{0,80}这 \d+ 分钟|按现有信息分不开|说出来我接着算/;
|
||||
|
||||
function exitAppendCalls(calls: Array<{ fn: string; args: Record<string, unknown> }>) {
|
||||
return assistantAppendCalls(calls).filter((item) => (
|
||||
GATE_SENTENCE.test(String(item.args.p_assistant_message))
|
||||
|| /就能开始筛|现在记下的是|范围还能再收一截|目前范围|当前范围|范围已经收到|能问的都问完了/.test(
|
||||
String(item.args.p_assistant_message),
|
||||
)
|
||||
|| EXIT_CARRIER_COPY.test(String(item.args.p_assistant_message))
|
||||
));
|
||||
}
|
||||
|
||||
@@ -698,12 +701,12 @@ test("closed ceiling with training open delivers a range and a concrete next-col
|
||||
// 原因: 产品撤回门槛句终态,训练门开必须交付(BUG-646)
|
||||
assert.doesNotMatch(persisted.hostNarration ?? "", GATE_SENTENCE);
|
||||
assert.doesNotMatch(persisted.hostNarration ?? "", /领域不限|做不了|材料不够|还差/);
|
||||
// 原值: /范围还能再收一截|目前范围|当前范围/
|
||||
// 新值: 旁白写「范围已经收到…能问的都问完了」;「目前范围」在卡标题上
|
||||
// 原因: BUG-687 拒答后的收口文案
|
||||
// 原值: /范围还能再收一截|目前范围|当前范围|范围已经收到|能问的都问完了/
|
||||
// 新值: 旁白也可写「收到 … 这 N 分钟 / 按现有信息分不开 / 说出来我接着算」
|
||||
// 原因: BUG-689 交付邀请;BUG-692 短语表跟上
|
||||
assert.match(
|
||||
persisted.hostNarration ?? "",
|
||||
/范围还能再收一截|目前范围|当前范围|范围已经收到|能问的都问完了/,
|
||||
EXIT_CARRIER_COPY,
|
||||
);
|
||||
|
||||
const repaired = await ensureNonTerminalTurnExit({
|
||||
@@ -755,12 +758,12 @@ test("inconsistent projection logs ranked_count 0 and still delivers a gate", as
|
||||
}).find((item) => item?.event === "rectification_exhaustion_collect");
|
||||
assert.equal(log?.ranked_count, 0);
|
||||
assert.doesNotMatch(persisted.hostNarration ?? "", /领域不限|做不了|材料不够/);
|
||||
// 原值: /范围还能再收一截|目前范围|就能开始筛|当前范围|排不出可比较的候选/
|
||||
// 新值: 旁白也可写「范围已经收到…能问的都问完了」
|
||||
// 原因: BUG-687
|
||||
// 原值: /范围还能再收一截|目前范围|就能开始筛|当前范围|排不出可比较的候选|范围已经收到|能问的都问完了/
|
||||
// 新值: 旁白也可写「收到 … 这 N 分钟 / 按现有信息分不开 / 说出来我接着算」
|
||||
// 原因: BUG-689 交付邀请;BUG-692 短语表跟上
|
||||
assert.match(
|
||||
persisted.hostNarration ?? "",
|
||||
/范围还能再收一截|目前范围|就能开始筛|当前范围|排不出可比较的候选|范围已经收到|能问的都问完了/,
|
||||
EXIT_CARRIER_COPY,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -858,21 +861,13 @@ test("last closed-ceiling card concatenates the gate into one append", async ()
|
||||
expectedRevision: 6,
|
||||
});
|
||||
const gates = exitAppendCalls(accounting.calls);
|
||||
// 原值: 恰好 1 条交付闸,旁白含「目前范围」
|
||||
// 新值: 恰好 1 条交付闸,旁白可含「范围已经收到 / 能问的都问完了」
|
||||
// 原因: BUG-688 恢复「恰好一条」;文案放宽来自 BUG-687
|
||||
// 原值: 恰好 1 条交付闸,旁白含「目前范围 / 范围已经收到 / 能问的都问完了」
|
||||
// 新值: 恰好 1 条交付闸,旁白可含「收到 … 这 N 分钟 / 按现有信息分不开 / 说出来我接着算」
|
||||
// 原因: BUG-688 恢复「恰好一条」;BUG-689 邀请文案;BUG-692 短语表跟上。不含「只微调排序」。
|
||||
assert.equal(gates.length, 1, JSON.stringify(gates.map((item) => item.args.p_assistant_message)));
|
||||
assert.doesNotMatch(applied.narration, /领域不限|做不了|材料不够|还差 \d+ 件/);
|
||||
assert.match(
|
||||
applied.narration,
|
||||
/范围还能再收一截|目前范围|就能开始筛|当前范围|范围已经收到|能问的都问完了/,
|
||||
);
|
||||
assert.equal(
|
||||
(String(gates[0]?.args.p_assistant_message).match(
|
||||
/范围还能再收一截|目前范围|就能开始筛|范围已经收到|能问的都问完了/g,
|
||||
) ?? []).length > 0,
|
||||
true,
|
||||
);
|
||||
assert.match(applied.narration, EXIT_CARRIER_COPY);
|
||||
assert.equal(EXIT_CARRIER_COPY.test(String(gates[0]?.args.p_assistant_message)), true);
|
||||
});
|
||||
|
||||
test("closed ceiling with a leftover discriminator persists the card not the gate", async () => {
|
||||
@@ -982,9 +977,9 @@ test("ensureNonTerminalTurnExit writes a host turn when exhausted with no carrie
|
||||
});
|
||||
assert.ok(repaired.hostNarration);
|
||||
assert.equal(repaired.terminalNote, true);
|
||||
// 原值: exitAppendCalls 只认「目前范围」等旧闸句,计 1 条
|
||||
// 新值: 收口句「能问的都问完了 / 范围已经收到」也算载体
|
||||
// 原因: BUG-687
|
||||
// 原值: 收口句「能问的都问完了 / 范围已经收到」也算载体
|
||||
// 新值: 「收到 … 这 N 分钟 / 按现有信息分不开 / 说出来我接着算」也算载体;仍恰好 1 条
|
||||
// 原因: BUG-689 邀请文案;BUG-692 短语表跟上。不含「只微调排序」。
|
||||
assert.equal(exitAppendCalls(accounting.calls).length, 1);
|
||||
});
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
RANGE_DELIVERY_OPEN_COLLECT_DAY_EXAMPLES,
|
||||
RANGE_DELIVERY_OPEN_COLLECT_EXAMPLES,
|
||||
RANGE_DELIVERY_TIE_PERCENT,
|
||||
RECTIFICATION_USER_COPY,
|
||||
rangeDeliveryCollectClosed,
|
||||
rangeDeliveryShowsOpenCollectInvite,
|
||||
} from "../src/lib/rectification-agentic/user-copy.ts";
|
||||
import {
|
||||
@@ -216,7 +216,53 @@ test("closed collect copy invites another dated event instead of saying the pool
|
||||
// 新值: 仍不得写「哪年结婚或订婚」这类还开着的线;D1b 允许用「登记结婚那天」作日精度例子
|
||||
// 原因: BUG-687 不得把已拒答的线再问一遍;BUG-689 D1b 优先要确切日期
|
||||
assert.doesNotMatch(copy, /哪年结婚或订婚|哪年收入明显变过|家里哪年添丁/);
|
||||
assert.ok(copy.includes(RECTIFICATION_USER_COPY.rangeDeliveryCollectClosed));
|
||||
assert.ok(copy.includes(rangeDeliveryCollectClosed(5)));
|
||||
assert.doesNotMatch(copy, /这两分钟/);
|
||||
assert.match(copy, /现在还剩 04:48–05:07 里 5 个候选。这几个候选按现有信息分不开/);
|
||||
});
|
||||
|
||||
test("closed-pool invite names two minutes only when two candidates remain", () => {
|
||||
const two = rangeNarrowHint(
|
||||
CLOSED_LAYERS,
|
||||
DATED_EVIDENCE,
|
||||
declineAllTargeted(),
|
||||
["04:48", "05:07"],
|
||||
2,
|
||||
["04:48", "05:07"],
|
||||
);
|
||||
assert.match(two, /现在还剩 04:48–05:07 里 2 个候选。这两分钟按现有信息分不开/);
|
||||
assert.doesNotMatch(two, /这几个候选/);
|
||||
assert.match(two, /不限领域/);
|
||||
assert.match(two, /确切哪一天/);
|
||||
assert.match(two, /有年月也行/);
|
||||
assert.match(two, /接着算/);
|
||||
const dayHits = RANGE_DELIVERY_OPEN_COLLECT_DAY_EXAMPLES.filter((item) => two.includes(item));
|
||||
assert.ok(dayHits.length >= 2, dayHits.join(","));
|
||||
const exampleHits = RANGE_DELIVERY_OPEN_COLLECT_EXAMPLES.filter((item) => two.includes(item));
|
||||
assert.ok(exampleHits.length >= 2, exampleHits.join(","));
|
||||
|
||||
const four = rangeNarrowHint(
|
||||
CLOSED_LAYERS,
|
||||
DATED_EVIDENCE,
|
||||
declineAllTargeted(),
|
||||
["04:47", "04:53"],
|
||||
4,
|
||||
["04:47", "04:53"],
|
||||
);
|
||||
assert.match(four, /现在还剩 04:47–04:53 里 4 个候选。这几个候选按现有信息分不开/);
|
||||
assert.doesNotMatch(four, /这两分钟/);
|
||||
assert.match(four, /不限领域/);
|
||||
assert.match(four, /登记结婚那天/);
|
||||
|
||||
const unknown = rangeNarrowHint(
|
||||
CLOSED_LAYERS,
|
||||
DATED_EVIDENCE,
|
||||
declineAllTargeted(),
|
||||
);
|
||||
assert.equal(unknown, rangeDeliveryCollectClosed());
|
||||
assert.doesNotMatch(unknown, /这两分钟|这几个候选/);
|
||||
assert.match(unknown, /^按现有信息分不开。/);
|
||||
assert.match(unknown, /不限领域/);
|
||||
});
|
||||
|
||||
test("the range card puts the closed-pool invite in the body only when the top two are tied", () => {
|
||||
@@ -249,6 +295,8 @@ test("the range card puts the closed-pool invite in the body only when the top t
|
||||
assert.match(tiedHtml, /换专业/);
|
||||
assert.match(tiedHtml, /创业/);
|
||||
assert.match(tiedHtml, /更像这个/);
|
||||
assert.match(tiedHtml, /这几个候选按现有信息分不开/);
|
||||
assert.doesNotMatch(tiedHtml, /这两分钟按现有信息分不开。/);
|
||||
assert.doesNotMatch(tiedHtml, /问完了/);
|
||||
const caption = tiedHtml.match(
|
||||
/class="rectification-range-delivery__narrow"[^>]*>([^<]*)</,
|
||||
|
||||
Reference in New Issue
Block a user