fix(rectification): invite another dated event instead of saying the pool is closed (BUG-689)
When the seven collect lines are closed, the range card still appears, but the copy asks for one more exact-day event of any kind instead of saying the questions are finished. A new dated event after delivery stales the snapshot so the session does not stay delivered.
This commit is contained in:
@@ -1,5 +1,9 @@
|
||||
# 印度占星 Skill 更新日志
|
||||
|
||||
## 2026-09-14 — 并列时请再补一件带年月的经历,不再说「问完了」
|
||||
|
||||
生时校正里,固定采集线问完、几个候选仍然分不开时,范围卡照出,但会请你再说一件记得确切哪一天的事,不限领域(登记结婚那天、入职第一天、手术那天、孩子出生那天、拿到录取通知那天)。记不得哪一天的,有年月也行。补完会接着算。不再写「能问的都问完了」。Skill 版本不变。
|
||||
|
||||
## 2026-09-14 — 风格参考题问不出时仍会给出范围卡
|
||||
|
||||
生时校正里,带年月的题问完后仍会先问还没用过的风格参考题。如果这道题这一轮问不出来(比如候选在该分盘上只有一种上升星座),或已经问过一轮,会直接给出范围卡,不会停在没有题目的等待里。Skill 版本不变。
|
||||
|
||||
+17
-1
@@ -10722,6 +10722,22 @@
|
||||
- 修复:只有真正拿得到可渲染风格题时才 hold;exhausted、closed-ceiling、holdout 不可用的收口不 hold;同一 Case 最多 hold 一轮。恢复「恰好一条交付闸」和「参考题确认语不算出口」两条断言。
|
||||
- 验证:`frontend/tests/rectification-tiebreak-before-card-20260914.test.ts`、`rectification-exhaustion-exit-20260906.test.ts`、`rectification-probe-pool-exhausted-20260911.test.ts`。
|
||||
- 防复发:任何「延迟交付」的守卫都必须带出口,且不得作用于耗尽与收口路径。验收必须比对 `npm test` 失败数与基线。不得把参考题确认语算作出载体。
|
||||
- 相关记录:BUG-686、BUG-656、BUG-674、BUG-680、BUG-597
|
||||
- 相关记录:BUG-686、BUG-656、BUG-674、BUG-680、BUG-597、BUG-689
|
||||
- 复发自:BUG-686(风格题前置扩大到所有交付分支)
|
||||
- 修复版本:待发布
|
||||
|
||||
## BUG-689 | 七条采集线问完后只说「能问的都问完了」,用户不知道还能补经历
|
||||
|
||||
- 状态:resolved
|
||||
- 首次发现:2026-09-14
|
||||
- 最近更新:2026-09-14
|
||||
- 影响面:`rangeDeliveryCollectClosed`、`rangeNarrowHint`、范围交付卡、交付后补证据
|
||||
- 用户现象:结婚 / 收入 / 家人等固定线答过「没有/跳过」后,范围卡只写「能问的都问完了」。用户不知道还可以再说一件带年月的经历,也不知道补了会重算。真机并列约 26% / 26% / 20%。
|
||||
- 触发条件:定向补事七条线全部关闭或覆盖,随后交付区间;前两名相对可能性差 ≤ 3 个百分点。
|
||||
- 根因:BUG-687 把空池收口写成终局句。采集线关闭不等于校正结束:交付后 Case 仍是 `candidate_ready`,输入框不禁用,新的带年月证据会改账本指纹并让快照过期。缺的是邀请;过期快照若仍带着 `tied_first` 停止原因,缺口状态机会继续停在 `delivered`。
|
||||
- 修复:空池文案改为不限领域的补充邀请:先请记得确切哪一天的事,再退年月;举七条线之外的例子;不承诺定到分钟。并列且线已关闭时,邀请出现在交付卡正文,不躺在浅色说明里。快照因新证据过期时,决策不再把 `tied_first` 等耗尽停止原因带进采集,缺口才不会停在交付态。
|
||||
- 验证:`frontend/tests/rectification-open-collect-invite-20260914.test.ts`。
|
||||
- 防复发:采集线关闭不等于校正结束;交付卡必须给出不限领域的补充邀请,且补充必须可见地生效。不得把邀请写成「再说一件就能定到分钟」。不得把已经拒答的线再列一遍(BUG-687)。
|
||||
- 相关记录:BUG-687、BUG-646、BUG-651、BUG-653、BUG-688、BUG-590
|
||||
- 复发自:BUG-687(空池收口写成终局句)
|
||||
- 修复版本:待发布
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
# 进度 · 并列时请再补一件经历(2026-09-14)
|
||||
|
||||
## 范围
|
||||
|
||||
- 分支:`codex/rectification-open-collect-invite-20260914`(基于 `origin/staging` @ `62803c5d`)
|
||||
- 任务单:`docs/tasks/TASK-rectification-open-collect-invite-20260914.md`
|
||||
- BUG-689 resolved(复发自 BUG-687)
|
||||
- 未改打分、确认门、`MIN_ACCEPTANCE_EVENTS` / `MIN_ACCEPTANCE_DOMAINS`(§4b)、Skill 版本
|
||||
|
||||
## 完成
|
||||
|
||||
- T1:`rangeDeliveryCollectClosed` 改为不限领域邀请:先请确切哪一天(登记结婚那天、入职第一天、手术那天、孩子出生那天、拿到录取通知那天),再退年月(换专业、出国、打官司、创业、重病、亲人离世)。`rangeNarrowHint` 空池两处返回点沿用该常量。不再写「问完了」。
|
||||
- T2:前两名相对可能性差 ≤ 3 个百分点且采集线关闭时,邀请出现在交付卡正文(`rectification-range-delivery__invite`);浅色 caption 只留「现在还剩 …」。非并列或线仍开放时不显示该邀请。采用按钮仍在。
|
||||
- T3:交付态后新增一件 confirmed dated evidence → 账本指纹变化、`snapshotCurrent === false`、`decideFromDossier` 不再交付,也不再带着 `tied_first` 停在 `delivered`。未改 BUG-682 的「GET 回落 collect_evidence + tied_first 仍算出卡」。
|
||||
- D3:不得把已拒答线再写成「哪年结婚或订婚 / 哪年收入明显变过 / 家里哪年添丁」。D1b 允许用「登记结婚那天」作日精度例子。
|
||||
- 未走让步。
|
||||
|
||||
## 验收数字
|
||||
|
||||
- 新测 `rectification-open-collect-invite-20260914.test.ts` 3/3
|
||||
- 定向:`rectification-tiebreak-before-card-20260914.test.ts`、`rectification-targeted-collect-cards-20260913.test.ts`、`rectification-probe-pool-exhausted-20260911.test.ts`、`rectification-delivery-vs-collect-20260914.test.ts`、`rectification-surface-state.test.ts`、`agent-voice-copy-contract.test.ts`、`rectification-collection-question-pool.test.ts`、`rectification-convergence-budget.test.ts`、`rectification-range-delivery-20260907.test.ts` 全绿
|
||||
- `tsc --noEmit`:0 错
|
||||
- `npm run lint`:0 error(既有 warning 未顺手改)
|
||||
- 全量 `npm test`:3018 / pass 2850 / fail 154 / skip 14。失败条数与本机 Windows 基线同型(CRLF、Skill SHA、Docker/DB、无 symlink)。Linux 门禁基线 27 条,本轮未新增失败。
|
||||
- `next build`:本工作树 `frontend/node_modules` 是 junction,未跑;`/` 是否仍 `○ Static` 需在有真实 `node_modules` 的树上复核
|
||||
|
||||
## 环境缺口
|
||||
|
||||
- 无 staging 登录态、无 Chrome:真机清单见 `docs/testing/rectification-open-collect-invite-20260914.md`
|
||||
- `rectification-exhaustion-exit-20260906.test.ts` 本机因 Skill SHA(Windows CRLF / 无软链)无法导入,Linux 门禁会跑
|
||||
@@ -190,7 +190,7 @@
|
||||
|
||||
| `TASK-rectification-precision-adaptive-boundary-research-20260914.md` | `PROGRESS-rectification-precision-gate-research-20260914.md` | **研究单**:出题闸门 `MIN_BOUNDARY_DAYS=45`(刷新 30)不看证据精度,而出生时间每差 1 分钟大运边界只平移约 1.1 天——20 分钟窗内的候选边界全挤在三周内被整批丢掉,这就是「六题后出不来题」的算术原因。量闸门按年/月/日分档(G0–G4)的收益,并必测答错容忍度(±3/±7/±14 天偏移下真值是否被挤出) | 待执行 | `codex/rectification-precision-gate-research-20260914` |
|
||||
|
||||
| `TASK-rectification-open-collect-invite-20260914.md` | `PROGRESS-rectification-open-collect-invite-20260914.md` | **P0**:固定七条采集线问完后只说「能问的都问完了」,用户不知道还能补经历、也不知道补了有用;而两轮研究证明补带年月经历是唯一有效手段。产品拍板:交付卡照出 + 卡上给不限领域的补充邀请(举七条线之外的例子),补完必须可见生效(BUG-689) | 待执行 | `codex/rectification-open-collect-invite-20260914` |
|
||||
| `TASK-rectification-open-collect-invite-20260914.md` | `PROGRESS-rectification-open-collect-invite-20260914.md` | **P0**:固定七条采集线问完后只说「能问的都问完了」,用户不知道还能补经历、也不知道补了有用;而两轮研究证明补带年月经历是唯一有效手段。产品拍板:交付卡照出 + 卡上给不限领域的补充邀请(先要确切日期,再退年月;举七条线之外的例子),补完必须可见生效(BUG-689) | 待验收 | `codex/rectification-open-collect-invite-20260914` |
|
||||
|
||||
| `TASK-rectification-cluster-width-research-20260914.md` | `PROGRESS-rectification-cluster-width-research-20260914.md` | **研究单**:上一轮证明调权重改不动交付区间宽度——所有方案宽度中位数都等于整个搜索窗。先确认 sweep 的宽度口径是否含淘汰(M0),再画簇结构像(M1),最后量三个改法:放宽簇上限、按分差决定是否合并、交付区间改分位覆盖(M2)。真值覆盖率不得下降 | 待验收 | `codex/rectification-cluster-width-research-20260914` |
|
||||
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
# 并列后补一件带年月经历:真机清单(2026-09-14)
|
||||
|
||||
分支 `codex/rectification-open-collect-invite-20260914`。本环境无 staging 登录态。
|
||||
|
||||
## BUG-689
|
||||
|
||||
- [ ] 固定七条线(结婚 / 收入 / 搬家 / 家人 / 学业 / 健康 / 职业)都答过「没有/跳过」后,范围卡仍出现。
|
||||
- [ ] 前两名相对可能性相差不超过约 3 个百分点时,卡的正文(不是浅色小字)写「这两分钟按现有信息分不开」,并请再说一件记得确切哪一天的事、不限领域;例子里至少有登记结婚那天、入职第一天、手术那天、孩子出生那天、拿到录取通知那天中的两个。记不得哪一天时,才退到年月。
|
||||
- [ ] 正文里不再出现「能问的都问完了」,也不再把已经说过没有的线写成「哪年结婚或订婚 / 哪年收入 / 家里哪年添丁」。
|
||||
- [ ] 「更像这个」和「新建对话按此时间排盘」的出口仍可用,邀请不挡住采用。
|
||||
- [ ] 并列出卡后,在输入框补一件带日期的经历(能到日更好,年月也可以):账本应记下这件事,范围或候选排序应有可见变化,或出现新的一问。不得停在「问完了」且没有任何反应。
|
||||
@@ -18,4 +18,4 @@
|
||||
## BUG-687
|
||||
|
||||
- [ ] 范围卡文案不再出现已经答过「没有/跳过」的线(结婚、收入、添丁等)。
|
||||
- [ ] 能问的线都关闭时,写「能问的都问完了」,不暗示还有没问的线。
|
||||
- [ ] 能问的线都关闭时,不再列出已经拒答的线;后续邀请口径见 `rectification-open-collect-invite-20260914.md`(BUG-689)。
|
||||
|
||||
+2
-2
@@ -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 (or “能问的都问完了”); 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 “能问的都问完了”); 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 |
|
||||
@@ -253,7 +253,7 @@ The birth-time rectification session is the consultation transcript plus a house
|
||||
- **Accessibility:** native radio inputs remain focusable, every conditional field has a persistent label, status text uses live regions, and the complete flow is keyboard operable.
|
||||
- **Motion:** source-dependent fields enter with the existing 180ms opacity/vertical reveal; reduced-motion removes the translation.
|
||||
- **Life-event evidence:** after deterministic questionnaire completion, render three structured event rows by default and allow up to six. Each row uses a domain select, a precision select, and a matching year/month/day control; free-form descriptions are not part of scoring.
|
||||
- **Candidate result:** keep the reported range, candidate interval, and active-time status visually separate. Delivery shows one range card: title “目前范围 HH:MM–HH:MM(对照了 N 件经历)”, a caption “还能再收窄:如果记得 …” under the title, up to three compare columns (time, relative likelihood, D9/D10/nakshatra traits, event-fit counts, next-12-month windows, “更像这个”), and the representative-minute boundary. Collect copy “现在还剩 HH:MM–HH:MM 里 N 个候选” uses the same `credible_range` as the timeline “目前范围”, not the first/last active candidate minute. An eight-method report sits in a closed `<details>` fold. Support numbers stay on the house board. Low confidence keeps evidence editing open; medium offers save or add evidence; high uses a separate confirmation action and never labels a column as the true birth time.
|
||||
- **Candidate result:** keep the reported range, candidate interval, and active-time status visually separate. Delivery shows one range card: title “目前范围 HH:MM–HH:MM(对照了 N 件经历)”, a caption “还能再收窄:如果记得 …” under the title while collect lines remain open, up to three compare columns (time, relative likelihood, D9/D10/nakshatra traits, event-fit counts, next-12-month windows, “更像这个”), and the representative-minute boundary. When the top two columns are within 3 percentage points and the seven collect lines are closed, the card body (not the caption) invites one more dated event of any kind, with examples outside those lines. Collect copy “现在还剩 HH:MM–HH:MM 里 N 个候选” uses the same `credible_range` as the timeline “目前范围”, not the first/last active candidate minute. An eight-method report sits in a closed `<details>` fold. Support numbers stay on the house board. Low confidence keeps evidence editing open; medium offers save or add evidence; high uses a separate confirmation action and never labels a column as the true birth time.
|
||||
- **Evidence accessibility:** every row keeps visible labels, validation errors use live regions, add/remove controls retain 44px targets, and scoring/confirmation loading states disable duplicate submission without hiding the existing evidence.
|
||||
- **One-question guide:** the guided journey renders only the persisted `nextAction` and one server-selected question. A deterministic question is visible immediately; Agent wording may replace it without changing the question identity, domain, precision request, progress, or permissions. The composer explicitly permits an approximate year. Spoken collect does not render skip chips; typing 「没有」 still declines the domain and 「记不清」 still skips it. Stop on spoken collect is not a composer button: `CHOICE_STOP_LABEL` (“先这样,先看当前范围”) stays on choice cards, and generating turns keep “停止回答”. The readonly range line is a status sentence, not a stop control. Discriminator cards fold “为什么问这题” under the stem. Hovering or selecting an option does not reveal an `answer_impact` time line. The method sentence (`vargaSentence`) lives in the expanded activity timeline, not in the spoken bubble. The composer has no `rectification-step-state` status sentence and no `rectification-composer-meta`.
|
||||
- **Draft review:** natural-language answers become one inline review card. The evidence domain is read-only and uses its Chinese label; precision controls which exact year, month, or day input is available. Incomplete drafts keep edit and skip paths visible, while confirmation is disabled until the structured date is valid. Status and errors use polite or assertive live regions without clearing the persisted journey.
|
||||
|
||||
@@ -45,7 +45,8 @@ Jyotisha 的可见文案是产品的一部分。正确性红线(真实性、
|
||||
| ## 统一参数与原始结构(用户刚问「我婚姻怎么样」) | 先用几句口语答婚姻方向,再进入统一参数与原始结构。 | 骨架仍在,开场先回人。 |
|
||||
| 生成中输入框变灰,回车没反应 | 生成中仍可打字;回车后显示「已排队,回答结束后发出」,可撤回 | 对标主流对话产品,焦点不丢。 |
|
||||
| 停止后出现红色告警条 | 已停止,已生成的内容保留;本次不会扣点。 | 停止是用户动作,不是出错。 |
|
||||
| 也可以再说一件你记得大概时间的事。 / 还差带月份的经历,领域不限。 | 训练门开后给区间卡,并写「如果还记得……范围还能再收一截」。材料不够时只写记下了哪几件、再来一件不是这类的、至少两个具体例子;输入框占位「再说一件带年月的事」。 | 永远给结果或精确缺口;不说「领域不限」「做不了」。 |
|
||||
| 也可以再说一件你记得大概时间的事。 / 还差带月份的经历,领域不限。 | 训练门开后给区间卡,并写「如果还记得……范围还能再收一截」。材料不够时只写记下了哪几件、再来一件不是这类的、至少两个具体例子;输入框占位「再说一件带年月的事」。 | 训练门未开时永远给结果或精确缺口;不说「领域不限」「做不了」。交付后采集线关完的邀请见下一行。 |
|
||||
| 能问的都问完了 | 这两分钟按现有信息分不开。你要是还记得确切哪一天的事,不限领域,说出来我接着算——登记结婚那天、入职第一天、手术那天、孩子出生那天、拿到录取通知那天都成。记不得哪一天的,有年月也行,比如换专业、出国、打官司、创业。 | 固定七条线关完不等于校正结束。并列时先请确切日期,再退年月;「不限领域」只出现在交付卡邀请里。不承诺「说了确切日期就能定到分钟」。 |
|
||||
| 口述采集没有停止按钮,范围小字不可点 | 选择题卡内有「先这样,先看当前范围」;生成中有「停止回答」。口述采集只有输入框,没有「没有」「记不清」芯片,也不挂在输入框上方。范围小字是状态句,不可点。 | 想跳过可以打字「没有」或「记不清」。 |
|
||||
| 只报一件后立刻按家人/学业/财务轮着问,题干带「2020 年前后」 | 记下后问「还有吗?比如……」(只列还没提过的具体事)。答「没有了」后才从用户说过的年份追问,例如「2020 年毕业后第一份工作大概哪年开始?」 | 先等用户说完;年份只来自用户,不用生日推。 |
|
||||
| 点选后旁白写「04:31–04:39 这段领先,05:00–05:07 这段落后」 | 已记录,范围收到 04:31–04:39。 | 旁白只说范围变没变。领先/落后留在卡内「答了会怎样」,不进气泡。 |
|
||||
|
||||
@@ -3390,6 +3390,12 @@ input:not([type="radio"]):not([type="checkbox"]):not([class^="ant-"]):not([class
|
||||
.rectification-range-delivery__tie-break:disabled {
|
||||
cursor: default;
|
||||
}
|
||||
.rectification-range-delivery__invite {
|
||||
margin: 0;
|
||||
color: var(--color-ink);
|
||||
font-size: var(--type-body-md);
|
||||
line-height: 1.65;
|
||||
}
|
||||
.rectification-range-delivery__boundary {
|
||||
margin: 0;
|
||||
color: var(--color-ink-secondary);
|
||||
|
||||
@@ -4,7 +4,9 @@ import type { RectificationCandidateResult } from "@/lib/rectification-candidate
|
||||
import {
|
||||
RECTIFICATION_USER_COPY,
|
||||
REPRESENTATIVE_MINUTE_DISCLAIMER,
|
||||
rangeDeliveryCaptionWithoutClosedInvite,
|
||||
rangeDeliveryEventCopy,
|
||||
rangeDeliveryShowsOpenCollectInvite,
|
||||
} from "@/lib/rectification-agentic/user-copy";
|
||||
import type { RangeDeliveryProjection } from "@/lib/rectification-agentic/v9/divergence-panel";
|
||||
import { RectificationVerificationReport } from "@/components/rectification-verification-report";
|
||||
@@ -40,14 +42,28 @@ export function RectificationRangeDelivery({
|
||||
const markdown = delivery?.verification_markdown
|
||||
?? result.verificationReportMarkdown;
|
||||
const sharedTraits = delivery?.shared_traits ?? [];
|
||||
const showOpenCollectInvite = rangeDeliveryShowsOpenCollectInvite({
|
||||
hint: delivery?.narrow_hint,
|
||||
columns,
|
||||
});
|
||||
const caption = showOpenCollectInvite || delivery?.narrow_hint?.includes(
|
||||
RECTIFICATION_USER_COPY.rangeDeliveryCollectClosed,
|
||||
)
|
||||
? rangeDeliveryCaptionWithoutClosedInvite(delivery?.narrow_hint)
|
||||
: delivery?.narrow_hint ?? null;
|
||||
|
||||
return (
|
||||
<section className="rectification-candidates rectification-range-delivery" aria-label="生时校正区间">
|
||||
<div className="rectification-candidates-heading">
|
||||
<strong>{title}</strong>
|
||||
</div>
|
||||
{delivery?.narrow_hint ? (
|
||||
<p className="rectification-range-delivery__narrow">{delivery.narrow_hint}</p>
|
||||
{caption ? (
|
||||
<p className="rectification-range-delivery__narrow">{caption}</p>
|
||||
) : null}
|
||||
{showOpenCollectInvite ? (
|
||||
<p className="rectification-range-delivery__invite">
|
||||
{RECTIFICATION_USER_COPY.rangeDeliveryCollectClosed}
|
||||
</p>
|
||||
) : null}
|
||||
{delivery?.tie_break_available && onTieBreak && !readonly ? (
|
||||
<button
|
||||
|
||||
@@ -342,9 +342,11 @@ export function decideRectification(input: DecideRectificationInput): Rectificat
|
||||
|
||||
if (input.snapshotCurrent === false) {
|
||||
if (probe && !userStopped && input.trainingGateOpen !== false) {
|
||||
return discriminateOrExhaust(input, separation, holdout, range, probe, rangeDeliveryCapability, stopReason);
|
||||
return discriminateOrExhaust(input, separation, holdout, range, probe, rangeDeliveryCapability);
|
||||
}
|
||||
return collect(separation, holdout, range, probe, capability, stopReason);
|
||||
// New dated evidence makes the snapshot stale. Do not carry exhaustion
|
||||
// stop reasons into collect: those would keep the UI in the delivered gap.
|
||||
return collect(separation, holdout, range, probe, capability);
|
||||
}
|
||||
if (
|
||||
stopClass?.kind === "exhausted"
|
||||
|
||||
@@ -112,6 +112,25 @@ export const USER_COLLECT_QUESTION_RETRY: Readonly<Record<string, string>> = {
|
||||
health_pressure: "身体或压力这边再问一次:哪年生病、受伤,或特别难熬?",
|
||||
};
|
||||
|
||||
/** Exact-day examples asked first after the seven targeted-collect lines close. */
|
||||
export const RANGE_DELIVERY_OPEN_COLLECT_DAY_EXAMPLES = [
|
||||
"登记结婚那天",
|
||||
"入职第一天",
|
||||
"手术那天",
|
||||
"孩子出生那天",
|
||||
"拿到录取通知那天",
|
||||
] as const;
|
||||
|
||||
/** Year-month examples outside the seven targeted-collect lines. */
|
||||
export const RANGE_DELIVERY_OPEN_COLLECT_EXAMPLES = [
|
||||
"换专业",
|
||||
"出国",
|
||||
"打官司",
|
||||
"创业",
|
||||
"重病",
|
||||
"亲人离世",
|
||||
] as const;
|
||||
|
||||
export const RECTIFICATION_USER_COPY = {
|
||||
genericCollectQuestion: GENERIC_COLLECT_QUESTION,
|
||||
collectQuestionByDomain: USER_COLLECT_QUESTION,
|
||||
@@ -149,10 +168,39 @@ export const RECTIFICATION_USER_COPY = {
|
||||
rangeDeliveryTieBreakEntry: "再答两道参考题微调排序",
|
||||
rangeDeliveryTieBreakAck: "只微调排序,不改目前范围。",
|
||||
rangeDeliveryTieBreakUsed: "这两分钟按现有信息分不开,参考题已经用过。",
|
||||
rangeDeliveryCollectClosed: "能问的都问完了",
|
||||
rangeDeliveryCollectClosed: `这两分钟按现有信息分不开。你要是还记得确切哪一天的事,不限领域,说出来我接着算——${RANGE_DELIVERY_OPEN_COLLECT_DAY_EXAMPLES.join("、")}都成。记不得哪一天的,有年月也行,比如${RANGE_DELIVERY_OPEN_COLLECT_EXAMPLES.join("、")}。`,
|
||||
verificationReportSummary: "查看验证报告",
|
||||
} as const;
|
||||
|
||||
/** Top-two relative-likelihood gap, in percentage points, that counts as a tie on the range card. */
|
||||
export const RANGE_DELIVERY_TIE_PERCENT = 3;
|
||||
|
||||
export function rangeDeliveryTopTwoTied(
|
||||
columns: readonly { probability_percent: number }[],
|
||||
): boolean {
|
||||
if (columns.length < 2) return false;
|
||||
return Math.abs(columns[0].probability_percent - columns[1].probability_percent)
|
||||
<= RANGE_DELIVERY_TIE_PERCENT;
|
||||
}
|
||||
|
||||
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 stripped = hint.replaceAll(invite, "").replace(/[。.]\s*$/u, "").trim();
|
||||
return stripped.length > 0 ? stripped : null;
|
||||
}
|
||||
|
||||
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));
|
||||
}
|
||||
|
||||
export const RANGE_DELIVERY_DOMAIN_LABEL: Readonly<Record<string, string>> = {
|
||||
education: "学业",
|
||||
career: "事业",
|
||||
|
||||
@@ -0,0 +1,385 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
import { createElement } from "react";
|
||||
import { renderToStaticMarkup } from "react-dom/server";
|
||||
|
||||
import { candidateSetId } from "../src/lib/rectification-agentic/core/build-state.ts";
|
||||
import { INFERENCE_ALGORITHM_VERSION, type InferenceCandidate, type InferenceState } from "../src/lib/rectification-agentic/core/types.ts";
|
||||
import {
|
||||
RANGE_DELIVERY_OPEN_COLLECT_DAY_EXAMPLES,
|
||||
RANGE_DELIVERY_OPEN_COLLECT_EXAMPLES,
|
||||
RANGE_DELIVERY_TIE_PERCENT,
|
||||
RECTIFICATION_USER_COPY,
|
||||
rangeDeliveryShowsOpenCollectInvite,
|
||||
} from "../src/lib/rectification-agentic/user-copy.ts";
|
||||
import {
|
||||
COLLECT_KIND_ORDER,
|
||||
rangeNarrowHint,
|
||||
} from "../src/lib/rectification-agentic/v9/collection-question-pool.ts";
|
||||
import {
|
||||
decideFromDossier,
|
||||
scoreableSnapshotCurrentFromDossier,
|
||||
type DecisionDossier,
|
||||
} from "../src/lib/rectification-agentic/v9/decision-from-dossier.ts";
|
||||
import { previousInferenceFromReceipt } from "../src/lib/rectification-agentic/v9/inference-adapter.ts";
|
||||
import { buildRangeDelivery } from "../src/lib/rectification-agentic/v9/divergence-panel.ts";
|
||||
import { evidenceLedgerFingerprint } from "../src/lib/rectification-agentic/v9/tool-service.ts";
|
||||
import { RectificationRangeDelivery } from "../src/components/rectification-range-delivery.tsx";
|
||||
import type { RectificationCandidateResult } from "../src/lib/rectification-candidate-result.ts";
|
||||
import { rectificationQuestionGapState } from "../src/lib/rectification-surface-state.ts";
|
||||
|
||||
const ID_A = "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaa1";
|
||||
const ID_B = "bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbb2";
|
||||
const ID_C = "cccccccc-cccc-4ccc-8ccc-ccccccccccc3";
|
||||
|
||||
const CLOSED_LAYERS = ["d9", "d10", "d4", "d5", "d7", "d2", "d30"] as const;
|
||||
|
||||
const DATED_EVIDENCE = [
|
||||
{
|
||||
id: "evidence-education",
|
||||
status: "confirmed",
|
||||
domain: "education",
|
||||
datePrecision: "month",
|
||||
occurredFrom: "2016-09-01",
|
||||
occurredTo: null,
|
||||
eventKind: "education_start",
|
||||
summary: "上大学",
|
||||
},
|
||||
{
|
||||
id: "evidence-career",
|
||||
status: "confirmed",
|
||||
domain: "career",
|
||||
datePrecision: "month",
|
||||
occurredFrom: "2020-04-01",
|
||||
occurredTo: null,
|
||||
eventKind: "career_entry",
|
||||
summary: "入职",
|
||||
},
|
||||
{
|
||||
id: "evidence-relocation",
|
||||
status: "confirmed",
|
||||
domain: "relocation",
|
||||
datePrecision: "month",
|
||||
occurredFrom: "2018-07-01",
|
||||
occurredTo: null,
|
||||
eventKind: "relocation",
|
||||
summary: "搬家",
|
||||
},
|
||||
] as const;
|
||||
|
||||
function declineAllTargeted() {
|
||||
return COLLECT_KIND_ORDER.map((domain) => ({
|
||||
questionId: `collect:targeted:${domain}`,
|
||||
status: "declined",
|
||||
target_kind: `targeted:${domain}`,
|
||||
intent: "collect_method_evidence",
|
||||
target_domain: domain,
|
||||
}));
|
||||
}
|
||||
|
||||
function cand(
|
||||
time: string,
|
||||
probability: number,
|
||||
range: readonly [string, string] = ["04:48", "05:07"],
|
||||
): InferenceCandidate {
|
||||
return {
|
||||
id: time,
|
||||
time,
|
||||
cluster_range: range,
|
||||
prior_score: probability * 100,
|
||||
posterior_score: probability * 100,
|
||||
probability,
|
||||
status: "active",
|
||||
rank: 1,
|
||||
strong_conflict_count: 0,
|
||||
};
|
||||
}
|
||||
|
||||
function inference(overrides: Partial<InferenceState> = {}): InferenceState {
|
||||
return {
|
||||
algorithm_version: INFERENCE_ALGORITHM_VERSION,
|
||||
candidate_set_id: "04:48-05:07:04:50,04:52,04:56",
|
||||
revision: 1,
|
||||
phase: "discrimination",
|
||||
result_status: "credible_range",
|
||||
range_start: "04:48",
|
||||
range_end: "05:07",
|
||||
candidates: [
|
||||
cand("04:50", 0.26),
|
||||
cand("04:52", 0.26),
|
||||
cand("04:56", 0.20),
|
||||
],
|
||||
events: [
|
||||
{ id: "e1", domain: "career", year: 2020, precision: "month", usage: "training" },
|
||||
{ id: "e2", domain: "education", year: 2016, precision: "month", usage: "training" },
|
||||
{ id: "e3", domain: "relocation", year: 2018, precision: "month", usage: "training" },
|
||||
],
|
||||
probes: [],
|
||||
answered_probes: [],
|
||||
rounds: [],
|
||||
entropy: 1,
|
||||
representative_time: "04:50",
|
||||
credible_range: ["04:48", "05:07"],
|
||||
refresh_count: 1,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
function deliveryResult(
|
||||
delivery: ReturnType<typeof buildRangeDelivery>,
|
||||
): RectificationCandidateResult {
|
||||
return {
|
||||
resultId: "result-open-collect",
|
||||
candidates: delivery.columns.map((column, index) => ({
|
||||
candidateId: column.candidate_id,
|
||||
rank: index + 1,
|
||||
time: column.time,
|
||||
relativeSupport: column.probability_percent,
|
||||
tiedMinuteCount: 1,
|
||||
})),
|
||||
overallConfidence: "medium",
|
||||
selectionAllowed: true,
|
||||
canAdopt: true,
|
||||
confirmationAllowed: false,
|
||||
decisionReceipt: null,
|
||||
representativeTime: delivery.representative_time,
|
||||
selectedTime: null,
|
||||
selectionKind: null,
|
||||
houseTable: null,
|
||||
houseTablesByTime: {},
|
||||
natalRecast: null,
|
||||
techniqueAudit: [],
|
||||
windowTransitions: [],
|
||||
eventDashaLedger: [],
|
||||
dashaAgreement: null,
|
||||
lagnaContrast: null,
|
||||
nakshatraBoundary: null,
|
||||
precisionStage: null,
|
||||
oosBlindPrompts: [],
|
||||
confirmationGate: { confirmation_allowed: false } as RectificationCandidateResult["confirmationGate"],
|
||||
validated: false,
|
||||
completionStatus: null,
|
||||
sessionOutcome: "adopt_representative",
|
||||
credibleRange: delivery.range,
|
||||
rangeDelivery: delivery,
|
||||
verificationReportMarkdown: delivery.verification_markdown,
|
||||
};
|
||||
}
|
||||
|
||||
function renderCard(delivery: ReturnType<typeof buildRangeDelivery>): string {
|
||||
return renderToStaticMarkup(createElement(RectificationRangeDelivery, {
|
||||
result: deliveryResult(delivery),
|
||||
acceptingCandidateId: null,
|
||||
readonly: false,
|
||||
onAccept: () => undefined,
|
||||
}));
|
||||
}
|
||||
|
||||
function gapFor(decision: { sessionOutcome: string; stopReason?: string | null }): string {
|
||||
return rectificationQuestionGapState({
|
||||
liveQuestionVisible: false,
|
||||
questionMissing: true,
|
||||
questionLoadFailed: false,
|
||||
collectWaiting: false,
|
||||
busy: false,
|
||||
readonly: false,
|
||||
regenerating: false,
|
||||
snapshotLoaded: true,
|
||||
resumableCase: true,
|
||||
retryAttempts: 0,
|
||||
sessionOutcome: decision.sessionOutcome,
|
||||
stopReason: decision.stopReason ?? null,
|
||||
});
|
||||
}
|
||||
|
||||
test("closed collect copy invites another dated event instead of saying the pool is finished", () => {
|
||||
const copy = rangeNarrowHint(
|
||||
CLOSED_LAYERS,
|
||||
DATED_EVIDENCE,
|
||||
declineAllTargeted(),
|
||||
["04:48", "05:07"],
|
||||
5,
|
||||
["04:48", "05:07"],
|
||||
);
|
||||
assert.match(copy, /现在还剩 04:48–05:07 里 5 个候选/);
|
||||
assert.doesNotMatch(copy, /问完了|都问完|没有更多/);
|
||||
assert.match(copy, /不限领域/);
|
||||
assert.match(copy, /确切哪一天/);
|
||||
assert.match(copy, /有年月也行/);
|
||||
assert.match(copy, /接着算/);
|
||||
assert.doesNotMatch(copy, /定到分钟|精确到分钟/);
|
||||
const dayHits = RANGE_DELIVERY_OPEN_COLLECT_DAY_EXAMPLES.filter((item) => copy.includes(item));
|
||||
assert.ok(dayHits.length >= 2, dayHits.join(","));
|
||||
const exampleHits = RANGE_DELIVERY_OPEN_COLLECT_EXAMPLES.filter((item) => copy.includes(item));
|
||||
assert.ok(exampleHits.length >= 2, exampleHits.join(","));
|
||||
// 原值: /结婚|收入|添丁/(空池不得再列已拒答的线)
|
||||
// 新值: 仍不得写「哪年结婚或订婚」这类还开着的线;D1b 允许用「登记结婚那天」作日精度例子
|
||||
// 原因: BUG-687 不得把已拒答的线再问一遍;BUG-689 D1b 优先要确切日期
|
||||
assert.doesNotMatch(copy, /哪年结婚或订婚|哪年收入明显变过|家里哪年添丁/);
|
||||
assert.ok(copy.includes(RECTIFICATION_USER_COPY.rangeDeliveryCollectClosed));
|
||||
});
|
||||
|
||||
test("the range card puts the closed-pool invite in the body only when the top two are tied", () => {
|
||||
const publicCandidates = [
|
||||
{ candidateId: ID_A, time: "04:50" },
|
||||
{ candidateId: ID_B, time: "04:52" },
|
||||
{ candidateId: ID_C, time: "04:56" },
|
||||
];
|
||||
const tied = buildRangeDelivery({
|
||||
inference: inference(),
|
||||
publicCandidates,
|
||||
credibleRange: ["04:48", "05:07"],
|
||||
representativeTime: "04:50",
|
||||
evidence: DATED_EVIDENCE,
|
||||
declinedTopics: declineAllTargeted(),
|
||||
});
|
||||
assert.equal(
|
||||
Math.abs((tied.columns[0]?.probability_percent ?? 0) - (tied.columns[1]?.probability_percent ?? 0))
|
||||
<= RANGE_DELIVERY_TIE_PERCENT,
|
||||
true,
|
||||
);
|
||||
assert.equal(rangeDeliveryShowsOpenCollectInvite({
|
||||
hint: tied.narrow_hint,
|
||||
columns: tied.columns,
|
||||
}), true);
|
||||
const tiedHtml = renderCard(tied);
|
||||
assert.match(tiedHtml, /rectification-range-delivery__invite/);
|
||||
assert.match(tiedHtml, /不限领域/);
|
||||
assert.match(tiedHtml, /确切哪一天/);
|
||||
assert.match(tiedHtml, /换专业/);
|
||||
assert.match(tiedHtml, /创业/);
|
||||
assert.match(tiedHtml, /更像这个/);
|
||||
assert.doesNotMatch(tiedHtml, /问完了/);
|
||||
const caption = tiedHtml.match(
|
||||
/class="rectification-range-delivery__narrow"[^>]*>([^<]*)</,
|
||||
)?.[1] ?? "";
|
||||
assert.doesNotMatch(caption, /不限领域/);
|
||||
|
||||
const clearLead = buildRangeDelivery({
|
||||
inference: inference({
|
||||
candidates: [
|
||||
cand("04:50", 0.70),
|
||||
cand("04:52", 0.20),
|
||||
cand("04:56", 0.10),
|
||||
],
|
||||
}),
|
||||
publicCandidates,
|
||||
credibleRange: ["04:48", "05:07"],
|
||||
representativeTime: "04:50",
|
||||
evidence: DATED_EVIDENCE,
|
||||
declinedTopics: declineAllTargeted(),
|
||||
});
|
||||
assert.equal(rangeDeliveryShowsOpenCollectInvite({
|
||||
hint: clearLead.narrow_hint,
|
||||
columns: clearLead.columns,
|
||||
}), false);
|
||||
const leadHtml = renderCard(clearLead);
|
||||
assert.doesNotMatch(leadHtml, /rectification-range-delivery__invite/);
|
||||
assert.doesNotMatch(leadHtml, /不限领域/);
|
||||
assert.match(leadHtml, /更像这个/);
|
||||
|
||||
const stillOpen = buildRangeDelivery({
|
||||
inference: inference({
|
||||
transitions: [{ layer: "d9", at: "04:52" }],
|
||||
}),
|
||||
publicCandidates,
|
||||
credibleRange: ["04:48", "05:07"],
|
||||
representativeTime: "04:50",
|
||||
evidence: DATED_EVIDENCE,
|
||||
declinedTopics: [],
|
||||
});
|
||||
assert.match(stillOpen.narrow_hint ?? "", /还能再收窄/);
|
||||
assert.equal(rangeDeliveryShowsOpenCollectInvite({
|
||||
hint: stillOpen.narrow_hint,
|
||||
columns: stillOpen.columns,
|
||||
}), false);
|
||||
const openHtml = renderCard(stillOpen);
|
||||
assert.doesNotMatch(openHtml, /rectification-range-delivery__invite/);
|
||||
assert.doesNotMatch(openHtml, /不限领域/);
|
||||
});
|
||||
|
||||
test("a new dated event after delivery stales the snapshot and leaves the delivered gap", () => {
|
||||
const rows = DATED_EVIDENCE.map((item) => ({ ...item }));
|
||||
const fingerprint = evidenceLedgerFingerprint(rows as never);
|
||||
const times = ["04:50", "04:52", "04:56"] as const;
|
||||
const state = inference({
|
||||
candidate_set_id: candidateSetId("04:48", "05:07", [...times]),
|
||||
refresh_count: 1,
|
||||
refresh_attempts: [{
|
||||
candidate_set_id: candidateSetId("04:48", "05:07", [...times]),
|
||||
answer_count: 0,
|
||||
result: "no_new_probes",
|
||||
at: "2026-09-14T00:00:00.000Z",
|
||||
}],
|
||||
});
|
||||
const dossier: DecisionDossier = {
|
||||
evidence: rows,
|
||||
conversationSummary: {
|
||||
activeFocus: null,
|
||||
declinedSkippedTopics: declineAllTargeted(),
|
||||
},
|
||||
latestResult: {
|
||||
resultId: "55555555-5555-4555-8555-555555555555",
|
||||
selectionAllowed: true,
|
||||
confirmationAllowed: false,
|
||||
evidenceLedgerFingerprint: fingerprint,
|
||||
candidates: times.map((time, index) => ({
|
||||
candidateId: [ID_A, ID_B, ID_C][index],
|
||||
time,
|
||||
rank: index + 1,
|
||||
relativeSupport: time === "04:56" ? 20 : 26,
|
||||
posterior_score: time === "04:56" ? 20 : 26,
|
||||
})),
|
||||
representativeTime: "04:50",
|
||||
decisionReceipt: {
|
||||
accept_allowed: true,
|
||||
acceptance_allowed: true,
|
||||
propose_allowed: true,
|
||||
selection_allowed: true,
|
||||
confirmation_allowed: false,
|
||||
inference_state: state,
|
||||
},
|
||||
},
|
||||
case: {
|
||||
acceptedTime: null,
|
||||
status: "candidate_ready",
|
||||
candidateRange: { start_time: "04:45", end_time: "05:15" },
|
||||
},
|
||||
};
|
||||
const delivered = decideFromDossier(dossier, { birthDate: "1997-08-08" });
|
||||
assert.ok(
|
||||
delivered.nextAction === "offer_provisional_range"
|
||||
|| delivered.nextAction === "ready_to_adopt"
|
||||
|| delivered.nextAction === "complete_with_range",
|
||||
delivered.nextAction,
|
||||
);
|
||||
assert.equal(gapFor(delivered), "delivered");
|
||||
|
||||
const nextEvidence = [
|
||||
...rows,
|
||||
{
|
||||
id: "evidence-lawsuit",
|
||||
status: "confirmed",
|
||||
domain: "other",
|
||||
datePrecision: "month",
|
||||
occurredFrom: "2019-03-01",
|
||||
occurredTo: null,
|
||||
eventKind: "legal_case",
|
||||
summary: "打官司",
|
||||
},
|
||||
];
|
||||
const nextDossier: DecisionDossier = { ...dossier, evidence: nextEvidence };
|
||||
const inferenceAfter = previousInferenceFromReceipt(nextDossier.latestResult?.decisionReceipt ?? null);
|
||||
assert.equal(scoreableSnapshotCurrentFromDossier(dossier, undefined, inferenceAfter), true);
|
||||
assert.equal(scoreableSnapshotCurrentFromDossier(nextDossier, undefined, inferenceAfter), false);
|
||||
const after = decideFromDossier(nextDossier, { birthDate: "1997-08-08" });
|
||||
assert.equal(
|
||||
after.nextAction === "complete_with_range"
|
||||
|| after.nextAction === "offer_provisional_range"
|
||||
|| after.nextAction === "ready_to_adopt",
|
||||
false,
|
||||
after.nextAction,
|
||||
);
|
||||
assert.notEqual(after.stopReason, "tied_first");
|
||||
assert.notEqual(gapFor(after), "delivered");
|
||||
});
|
||||
@@ -843,10 +843,11 @@ test("T4: exhausted refresh and declined targeted collect titles the card 目前
|
||||
assert.match(title, /^目前范围 04:48–05:07(对照了 4 件经历)$/);
|
||||
assert.doesNotMatch(title, /这次给出|最终|结束| · /);
|
||||
assert.match(delivery.narrow_hint ?? "", /现在还剩 04:48–05:07 里 6 个候选/);
|
||||
// 原值: /还能再收窄:如果记得/(忽略全部拒答,仍列出可收窄的线)
|
||||
// 新值: 全部定向补事已拒答,收口「能问的都问完了」
|
||||
// 原因: BUG-687
|
||||
assert.match(delivery.narrow_hint ?? "", /能问的都问完了/);
|
||||
// 原值: 全部定向补事已拒答,收口「能问的都问完了」
|
||||
// 新值: 收口改为不限领域的补充邀请
|
||||
// 原因: BUG-689 采集线关闭不等于校正结束
|
||||
assert.match(delivery.narrow_hint ?? "", /不限领域/);
|
||||
assert.doesNotMatch(delivery.narrow_hint ?? "", /问完了/);
|
||||
assert.doesNotMatch(delivery.narrow_hint ?? "", /还能再收窄:如果记得/);
|
||||
assert.doesNotMatch(delivery.narrow_hint ?? "", /能把 04:48 和 05:07 分开/);
|
||||
assert.doesNotMatch(delivery.narrow_hint ?? "", /这次给出|最终/);
|
||||
|
||||
@@ -205,10 +205,11 @@ test("personality D9/D10 stay out of the scoring pool until the range-card opt-i
|
||||
},
|
||||
tieBreakAvailable: tieBreakPersonalityAvailable(planInput),
|
||||
});
|
||||
// 原值: /还能再收窄/(rangeNarrowHint 忽略拒答,仍列出搬家/财务)
|
||||
// 新值: 拒答后不再列出那些线;此 fixture 剩余线都已覆盖或拒答,收口「能问的都问完了」
|
||||
// 原因: BUG-687 交付文案不得列出已关闭的线
|
||||
assert.match(delivery.narrow_hint ?? "", /能问的都问完了/);
|
||||
// 原值: 拒答后收口「能问的都问完了」
|
||||
// 新值: 仍不列出已关闭的线;收口改为不限领域的补充邀请
|
||||
// 原因: BUG-687 不得列出已关闭的线;BUG-689 采集线关闭不等于校正结束
|
||||
assert.match(delivery.narrow_hint ?? "", /不限领域/);
|
||||
assert.doesNotMatch(delivery.narrow_hint ?? "", /问完了/);
|
||||
assert.doesNotMatch(delivery.narrow_hint ?? "", /搬家|收入/);
|
||||
assert.doesNotMatch(delivery.narrow_hint ?? "", SPLIT_ENDPOINT_PHRASE);
|
||||
assert.equal(delivery.tie_break_available, true);
|
||||
|
||||
@@ -360,10 +360,13 @@ test("range copy omits declined lines and closes when none remain", () => {
|
||||
assert.match(openCopy, /收入/);
|
||||
assert.match(openCopy, /添丁/);
|
||||
const copy = rangeNarrowHint(layers, evidence, declined, ["04:48", "05:07"], 5, ["04:48", "05:07"]);
|
||||
assert.doesNotMatch(copy, /结婚/);
|
||||
assert.doesNotMatch(copy, /收入/);
|
||||
assert.doesNotMatch(copy, /添丁/);
|
||||
assert.match(copy, /能问的都问完了/);
|
||||
// 原值: /结婚|收入|添丁/ 且 /能问的都问完了/
|
||||
// 新值: 不得再写「哪年结婚或订婚」这类还开着的线;收口改为不限领域、先要确切日期
|
||||
// 原因: BUG-687 不得把已拒答的线再问一遍;BUG-689 D1b
|
||||
assert.doesNotMatch(copy, /哪年结婚或订婚|哪年收入明显变过|家里哪年添丁/);
|
||||
assert.doesNotMatch(copy, /问完了/);
|
||||
assert.match(copy, /不限领域/);
|
||||
assert.match(copy, /确切哪一天/);
|
||||
});
|
||||
|
||||
test("GET availability and the POST gate share one input builder", () => {
|
||||
|
||||
Reference in New Issue
Block a user