Gitea run 2697 唯一失败:侧栏把 /chart /ephemeris /reports 移进 app/(secondary)/ 后,_scan_app_routes 把文件夹名写进 app_routes。扫描时丢掉括号段。BUG-753。
23 KiB
进度 · 出卡精度门槛 + 引导式补经历(验收修复单,2026-09-16)
- 任务书:
docs/tasks/TASK-rectification-precision-gate-guided-collect-fix-20260916.md(F1~F7 + 决策 D6) - 基线:
origin/staging@a396bbe0(第二轮已 rebase;上一版基线302ff085,代码基线cfb41daf) - 分支 / 工作树:
codex/rectification-precision-gate-guided-collect-fix-20260916/.worktrees/rectification-precision-gate-guided-collect-fix-20260916 - BUG:747~753。开工时最大号是 743;实现期间另一会话把侧栏统一那一单合入 staging,占掉了 744~746,所以本单整体后移到 747 起。rebase 后
docs/BUG_HISTORY.md里 743 → 744 → … → 752 连续。合入后 Gitea run2697仍红一条 capability audit,补 BUG-753。 - 开工检索(AGENTS §5):BUG-733、654、656、740~743 已逐条读完;BUG-733 的防复发条本轮补了一句(见下)。
0.2 合入后跟进(BUG-753,Gitea run 2697)
dc732825 把 F1 修绿(797 passed),但 run 2697 仍 1 failed:test_capability_audit_scans_registry_and_local_sources 扫到 (secondary)/chart 等。侧栏把四个次级页移进路由组后,_scan_app_routes 把文件夹名当成公开 URL。扫描时丢掉 (…) 段,期望列表仍是 chart / ephemeris / reports。本地 pytest 该条 + growth contract 8 passed。
0. rebase 到 a396bbe0
第二轮已 git rebase origin/staging(侧栏统一那 4 笔:d9d34723、fb77c865、22edabbc、a396bbe0)。
- 自动合并:
frontend/DESIGN.md、frontend/src/app/page.tsx。page.tsx 以对方的结构为准,只保留本单那一处rectificationDeclaration合并(declaredTime+birthDate一个对象展开)。 - 手工解冲突 3 个,都在文档、都是「两侧各自追加」:
CHANGELOG.md:两条都保留,本单那条在上(后写的在前)。docs/BUG_HISTORY.md:以 staging 版本为底,重新插入本单对 BUG-733 的那一行补注,再把本单的 BUG-747~752 整块追加到 BUG-746 之后。结果## BUG-共 739 条,末尾 743 → 744 → … → 752 连续。docs/tasks/README.md:侧栏单那一行取对方的(已验收状态),本单那一行取自己的;两行都在。
- BUG 编号 747~752 不变(744~746 已被侧栏轮占用)。
0.1 结论先行(全部在 a396bbe0 上重测;基线也在 a396bbe0 上用一次性 worktree 重跑)
| 项 | 基线 a396bbe0 |
本分支 |
|---|---|---|
tsc --noEmit |
0 错 | 0 错 |
npm run lint |
0 error / 115 warning | 0 error / 115 warning |
npm test |
3399 条 / 47 红 | 3405 条 / 31 红;31 红与基线逐条同名,0 条新红,基线 47 红里的 16 条全部转绿 |
pytest tests/test_rectification_*.py tests/test_event_probes_guided_windows.py |
— | 193 passed |
next build --webpack |
○ / |
○ /(/chart、/ephemeris 也仍是 Static) |
| 首屏 gzip | 583,765 | 582,800(−965 B,−0.17 %) |
frontend/src/app/page.tsx |
1836 行 | 1835 行(−1,未增长) |
| Skill | 10.0.27 | 10.0.28,sha256 f0bb8295…cffe |
1. F1 · 记忆化 golden(BUG-747)
tests/golden/rectification_engine_memoization_v1.json只追加guided_collect_windows一个键。git diff --stat=1 file changed, 62 insertions(+),git diff里只有一段+ "guided_collect_windows": [ … ],插在discriminating_event_probes之后(与 live receipt 的键序一致),CRLF 行尾保持不变。没有删除或修改任何既有行。- 未放宽
_assert_tiered_equal(键集合仍是严格相等),未调用write_golden()。 - 顺带修了一个同族隐患:
_golden_payload()现在把scripts.rectification.event_probes的date换成_FrozenDate(today() == FROZEN_TODAY = 2026-09-16)。窗口年份来自date.today(),不冻结的话这份 golden 会在跨年时自己变红。冻结后生成的值与本机实跑一致,所以 golden 内容没有因此改变。 docs/BUG_HISTORY.md的 BUG-733 条目补了一行「2026-09-16 因 receipt 新增键重生成,diff 仅此一键」。
2. F2 + F3 · 断言处置清单(BUG-748 / BUG-751)
先按 F3 改代码,再逐条看断言。没有删任何测试,没有 skip 任何测试。
2.1 代码改动(F3 + D6)
core/rectification-decision.ts
mayDeliverOnPrecision(input):
userStopped === true → true
precisionGateMet 与 guidedCollectExhausted 都缺省 → true(门槛不参与,helper 路径回到 317e9f18)
否则 guidedCollectExhausted === true
&& refreshExhausted !== false
&& targetedCollectExhausted !== false (= 所有题源都问完)
口径(D2 + D6,第二轮修正):门槛只在还有题可问时挡住出卡。题源全空就是 D2 说的「用户确实补不出来」,卡片、采用按钮、三句正文照现行规则给,不加「未达门槛」标注。
写全是 userStopped || allExhausted || (precisionGateMet && allExhausted),化简后就是 userStopped || allExhausted。
按 D2 + D6,10 分钟门槛不改变出卡时机,本轮实际生效的是题源变多(引导窗口题 + 跳过线重问 + 七条线全部轮到)、线问完才出。
precisionGateMet 保留计算,并新挂到 RectificationDecision.precisionGateMet 与 publicNextAction().precision_gate_met(helper 路径不传 flag 时为 null),只上报、不决定时机。
撤回的三处短路(全部回到原判断):
coverageBlocks分支的const narrowingOpen = stillNeedNarrowing(input) && !mayDeliverOnPrecision(input)!separation.sufficient分支的if (stillNeedNarrowing(input) && !mayDeliverOnPrecision(input))if (input.datedMethodCollectOpen === true && !input.userStopped && !mayDeliverOnPrecision(input))
保留门槛的五个最终交付分支:tied_first 耗尽、!sufficient 的 exhausted / adopt_representative / offer、顶层 exhausted。
v9/decision-from-dossier.ts 的 narrowingExhaustion:guidedCollectExhausted 改为 refreshDone && guidedCollectExhausted(...),即引导池空但刷新没试过不算问完。
新增三条单测(rectification-precision-gate-20260916.test.ts):
| 单测 | 断言 |
|---|---|
empty guided pool with refresh untried does not deliver |
refreshExhausted: false + 池空 + 门槛达标 → ask_fact_collection、canOfferRange=false;mayDeliverOnPrecision 直接返回 false |
gate met but the targeted line is still open keeps asking |
precisionGateMet: true + targetedCollectExhausted: false → ask_fact_collection |
every line asked but the gate unmet still delivers under the existing rules |
池空 + 门槛未达 → canOfferRange=true、sessionOutcomeAllowsDelivery=true,且 precisionGateMet 仍报 false(D2:题源空了就按现行规则出卡,不加标注) |
the precision gate is reported on a collect exit too |
采集出口上 decision.precisionGateMet === false、publicNextAction().precision_gate_met === false;helper 路径(不传 flag)为 null |
2.2 十六条基线红(任务书 §2 F2 表)
| # | 文件 :: 用例 | 处置 |
|---|---|---|
| 1 | adopt-flow-fix-20260903 :: collect-phase nonterminal exit… |
改断言(三栏):adopt_representative/offer_provisional_range/publicCanAdopt=true → collect_evidence/ask_fact_collection/false;随之 repaired.persisted false→true、repair 警告 false→true、store.focus null→非空。D1(13 分钟、14:13:13 并列)+ D6,本用例回到 BUG-648 之前的采集出口,与题名一致 |
| 2 | adopt-flow-fix-20260903 :: skipped opening collect other still yields education… |
改断言(三栏):plan.next_followup = null → 学业线 collect:targeted:education;下半段 educationPlan.next_followup.domain horary → education。D5 七条线全部轮到,题名要的「yields education」正是这一条 |
| 3 | adopt-narration-20260904 :: fourteen-probe case decides offer_provisional_range… |
仅改代码即恢复绿(F3 撤回短路后 helper 路径回到原语义),断言未动 |
| 4 | adopt-narration-20260904 :: family collect declined vs extra distinguish declined… |
改断言(三栏):`/搬家 |
| 5 | answer-choice :: answering the last discriminator persists a year-locked family collect focus… |
改断言(三栏):setFocus === undefined → 写入 collect:targeted:education;末条「没有 focus 带 p_asked_turn_id === TURN_ID」改成「写入的 focus 只能是这道新问题」——linkFocusAskedTurn 把新问题挂到本轮刚追加的 assistant turn,而夹具让这个 id 恰好等于 TURN_ID,旧断言在这个夹具里已无法区分两者 |
| 6 | choice-card :: family coverage without a renderable discriminator still collects the next dated domain |
改断言(三栏):plan.next_followup = null → 迁居线 collect:targeted:relocation。题名要的「still collects the next dated domain」正是这一条 |
| 7 | collect-direction-20260904 :: holdout remaining domain uses the server collect stem… |
改断言(三栏):choice_frame === null → choice_kind === "existence";USER_COLLECT_QUESTION.education → TARGETED_EXISTENCE_PROMPT.education,另加一条「不得是反向核对口吻」的断言,保住题名的原意 |
| 8 | collect-direction-20260904 :: four scoreable events stop dated collect… |
改断言(三栏):intent != collect_method_evidence → == collect_method_evidence 且 collection_key 必须是 `collect:(targeted |
| 9 | collect-direction-20260904 :: separated candidates stop leftover dated collect once the training gate is open |
改断言(三栏):sessionOutcome != collect_evidence → == collect_evidence、nextAction = ask_fact_collection、userAction.id = ask_method_followup |
| 10 | convergence-budget :: dossier and post-inference decisions share the half-uncertain stop rule |
改断言(三栏):stopReason = "probe_pool_exhausted" → null,并显式断言两条路径的 stopReason 与 nextAction 一致(题名要的「share the rule」) |
| 11 | decide-next-action :: separated candidates stay in collect while dated method collect is still open |
仅改代码即恢复绿,断言未动 |
| 12 | delivery-vs-collect-20260914 :: POST idle persist and GET decideFromDossier agree on a first-place tie |
改断言(三栏):getDelivers = true → false(D1 并列时门槛恒假);「POST 与 GET 同口径」那条不变 |
| 13 | superseded-focus :: duplicate_focus on the last collect ask does not emit delivery copy |
改断言(三栏):p_target_domain != relationship → 改断言写入的是 `collect:(targeted |
| 14 | targeted-collect-cards-20260913 :: A resolves existence then asks the year; B/C close only that line |
改断言(三栏):题干 结过婚或订过婚吗? → TARGETED_EXISTENCE_PROMPT.relationship(T4b 问整个领域);targetedCollectExhausted 从「拒答 4 条即耗尽」改成「4 条 → false、七条 → true」 |
| 15 | targeted-collect-cards-20260913 :: personality D9/D10 stay out of the scoring pool… |
改断言(三栏):narrow_hint 含 不限领域 → 不得含 不限领域,改为匹配引导句 现在还剩 … 里 N 个候选(D3 删自由文本邀请) |
| 16 | tied-first-fix-20260914 :: tied first place with no probe still collects while targeted questions remain |
仅改代码即恢复绿,断言未动(这正是 F3 说的 BUG-654 规则回来了) |
2.3 D5/D6 连锁打红的另外 25 条(任务书未列,本轮一并处置)
撤回 datedMethodCollectOpen / stillNeedNarrowing 两处短路之后,cfb41daf 的 T4b(remainingTargetedDomains 不再按分盘层剔除,七个 CollectKind 全部轮到)就重新挡住了交付——这是 D5 + D6 的预期结果,但 cfb41daf 用短路把它盖住了,所以这 25 条在基线上是绿的。全部按「原值 / 新值 / 原因」三栏改:
| 文件 | 条数 | 处置摘要 |
|---|---|---|
coverage-collect |
4 | offer_provisional_range / ready_to_adopt → ask_fact_collection + collect_evidence;canAdopt 仍是引擎能力位,保持 true |
provisional-adopt |
6 | 公开投影 can_adopt / session_outcome 翻到采集侧;canRenderRectificationSelectionCards true→false;buildNextUserAction adopt_representative→ask_method_followup;selectionAllowed true→false |
range-offer-deadend |
2 | ready_to_adopt→ask_fact_collection、decision.canAdopt true→false(零拆分探针被 drop 的断言保留) |
eight-method |
1 | rectification-offer-candidates 由「可走」改为 assert.rejects(/offer_not_allowed/) |
collect-direction-20260904 |
1 | 同上(offer-candidates 仍被挡) |
exhaustion-exit-20260906 |
1 | adopt_representative→collect_evidence、stopReason→null、旁白由三句交付改为引导句 |
occupation-coverage-exit |
1 | canOfferRange true→false、collect_evidence / ask_fact_collection |
holdout-renderable |
1 | offer/ready → ask_fact_collection,validated_range 断言保留 |
collect-stall |
1 | 只改夹具:补齐 education / career / relationship 三条拒答(D5 之后七条线都要轮到),断言全部保持原样(publicCanAdopt=true、persisted=false、不写 focus) |
adopt-flow-fix-20260903 |
1 | adopt_representative nonterminal exit…:第二轮修正口径后无需改动,夹具与断言均恢复原样 |
answer-choice |
1 | last structured choice…:ready_to_adopt / can_adopt=true → ask_fact_collection / false(这条的原因是 D5 的线没问完,不是门槛) |
decision-authority |
1 | 公开字段深比较补 precision_gate_met(BUG-692 要求不得改成部分匹配,所以补字段而不是放宽比较) |
probe-pool-exhausted-20260911 |
1 | T3 第二轮修正后恢复原样;T4 只把末条旁白正则补上引导句(persistNextInterviewIfIdle 自己重算决策,写的是采集侧区间旁白) |
open-collect-invite-20260914 |
0 | 第二轮修正口径后恢复原样,无改动 |
surface-contract |
1 | declaredTime={rectificationDeclaredTime} → {...rectificationDeclaration}(F7) |
ingest-p0 / delivery-report-facts / tie-break-entry-20260913 / v9-contracts |
4 | Skill 版本 10.0.27 → 10.0.28 |
另有 10 个文件只是 RECTIFICATION_SKILL_VERSION 常量断言随 bump 改数字(collect-stall、confirmation-gate、delivery-report-facts、eight-method、exhaustion-exit、ingest-p0、occupation-coverage-exit、range-offer-deadend、replay-20260911、spoken-collect、v9-agent、v9-contracts、v9-entry-routing、window-cluster-cap、yearless-probe-downgrade、skill-registry),每处都带一行三栏注释。
2.4 最终红清单
npm test fail=31,与基线 31 条逐条同名:无 Docker 的 database / deployment / staging 套件,以及 [eval] 路径别名那一组。
测试总数 3399 → 3405(+6,全部是新增;没有删除、没有 skip):精度门槛 5 条(引导池空但刷新未试不交付、门槛达标但定向线未问完继续问、题源全空即使门槛未达也照现行规则出卡、采集出口也上报门槛、helper 路径上报 null)+ F4 的 3 条 + F6 的 1 条,其中 2 条是把既有用例改写后新增的姊妹条。
3. F4 · 无领域轨道的窗口改成开放题(BUG-749)
scripts/rectification/event_probes.py:删掉fallback[unlayered_index % len(fallback)]轮询;vim/nara轨道的窗口domain = GUIDED_ANY_DOMAIN = "any";nara:d9/nara:d10保持relationship/career,该领域被拒答时也退回"any"。declined_domains现在只用于剔除例子与 d9/d10 的固定领域。- 前端:
GUIDED_ANY_DOMAIN/GuidedWindowDomain/normalizeGuidedWindowDomain/guidedAnyExamples/firstOpenCollectKind;guidedWindowPrompt(window, openDomains)对any写「YYYY 年 M 到 M 月之间,有没有什么事,比如<口语 ≤3 个>?」,口语从KIND_ORAL表取、剔除 declined 与已覆盖,没有按领域写 if;全部领域都关掉时退化成「有没有什么事?」。windowAlreadyAsked改成只比对(年, month_lo, month_hi),换个领域标签不重问。A 之后pendingGuidedYearWindow把any解析成第一个仍开放的领域,录入卡芯片默认落在它上面(resolveEntryDomain另有防御)。 - 单测:Python 7 条(真实引擎 20 分钟窗 golden 非空、无领域轨道 =
any、d9/d10 保留固定领域、拒答领域退回any、split 均衡排序、跨年拆分);前端 3 条(开放题干口语 ≤3、同一窗口只问一次、芯片默认第一开放领域)。
4. F5 · 离线回放改成注入真值方向(BUG-752)
scripts/research/guided_collect_holdout_replay.py 新增 window_boundary_dates():按 guided_collect_windows 自己的循环重算(轨道, index),拿回每个候选在该窗口上的边界日期。truth 组注入真值候选自己的那个月,opposite 组注入「剩余候选里离真值最远的那一个」的同一边界。三档半径都跑,minute_rectification_holdout_v4.json 20 例,351.6 s,错误 0,未用让步(没有只跑 ±10/±30)。
| 半径 | 方向 | 20 例达标 | 其中六题后本来就达标 | 靠引导件补上 | 达标件数中位 | 每例窗口数中位 |
|---|---|---|---|---|---|---|
| ±10 | truth | 3 | 3 | 0 | 0 | 6 |
| ±10 | opposite | 3 | 3 | 0 | 0 | 6 |
| ±30 | truth | 1 | 1 | 0 | 0 | 6 |
| ±30 | opposite | 1 | 1 | 0 | 0 | 6 |
| ±60 | truth | 0 | 0 | 0 | — | 6 |
| ±60 | opposite | 0 | 0 | 0 | — | 6 |
原始表 docs/research/guided_collect_holdout_2026_09_16.json(含每例的 injected,形如 19:24:2026-05-08,可核对注入确实落在不同候选的边界上)。
口径说明与观察:
- 上一轮「0/20」的写法不成立(注入的是窗口左端点),这一轮按真值边界注入之后结论仍然一样:没有一例是「六题后未达、补引导件后达标」。这次的数字可以作数。
- truth 与 opposite 两组数字完全相同,说明在这个 holdout 上,把事件放在真值的边界还是放在最远候选的边界,对 D1 门槛没有可测差别。
- 每例都有 6 个窗口(上限就是 6),所以不是「题不够」。
- 窗口年份集中在 2025–2026:排序先按 split 均衡、再按靠近今天,
lo = 出生年 + 5、hi = min(今年, 出生年+80)。真人补经历时这几年确实最容易记得,但对名人 holdout 来说这些年份多半没有可用事件。 - 这不是合入门槛,产品据此决定 10 分钟门槛是否要调,或者引导题源是否还要换一条路。
5. F6 · 录入卡提交文本(BUG-750)
formatEventDateEntryMessage → 「YYYY 年 M 月(D 日),<领域标签>方面有一件事」,标签取 RANGE_DELIVERY_DOMAIN_LABEL。单测断言两种日期形态与「不含『或』」。没有改成 set-focus / batch 的结构化写入(任务书给的是「或」二选一),走的仍是现有带年月证据路径。
6. F7 · page.tsx 与 golden fixture
page.tsx1838 → 1837:rectificationDeclaredTime常量改成rectificationDeclaration = { declaredTime, birthDate },JSX 里两行 prop 合成一行{...rectificationDeclaration}。 偏离:任务书写的是「把那一行挪进conversational-birth-time-rectification.tsx内部取档案」。做不到而且不划算——该组件没有档案来源:RectificationCaseSnapshotPayload的case段只有status / accepted_time / confirmed_time / candidate_range / stage / birth_time_source,没有 birth_date;要在组件里拿档案得新开一次/api/account读取或改服务端合同,为了省一行代码多一次网络请求并不合算。行数目标已达成,记在这里由验收方判断。tests/test_event_probes_guided_windows.py重写:新增_real_built(),用build_candidate_static_context对公开 1990-01-01 北京测试盘的 12:00–12:20(20 分钟窗,5 个采样分钟)跑真实引擎,断言窗口非空并逐字段校验;test_declined_domains_are_removed改成test_declined_domains_fall_back_to_open_windows,用真实引擎结果做,skipTest全部删除(文件里已无skipTest)。手造 static contexts 只留在排序与跨年两条纯结构断言里。
7. 记录
docs/BUG_HISTORY.md:BUG-747~752(本分支内连续;744~746 属侧栏统一单,见 §0),BUG-733 补一行。CHANGELOG.md:2026-09-16 「问完所有线才出卡,时间窗不再硬贴一个领域」,写明 Skill 10.0.28。frontend/DESIGN.md:guided-collect行改写(开放窗口题干、一窗一问、录入卡提交文本),candidates行写明门槛是必要条件而非充分条件。frontend/docs/VOICE.md:新增两行(开放窗口题干、录入卡提交文本),已对照既有口径。docs/tasks/README.md:本单状态板改「已实现,待验收」+ 分支名 + 进度记录名。- 真机清单沿用
docs/testing/rectification-guided-collect-20260916.md(六条),本轮没有新增浏览器级验收项。 - 未写入任何真实出生资料;示例只用公开名人 holdout 与 1990-01-01 测试盘。
8. 偏离与未做
- F7 的
birthDate没有挪进组件内部(理由见 §6),改用 props 合并把 page.tsx 降回 1837 行。 - (第一轮偏离,已修正) 第一轮按修复单 F3 §2 的字面实现了「门槛未达即使线问完也只进 collect」,与主单 D2「引导题源也空了就按现行规则出卡」冲突,会造出「没有题也没有卡」的死角。产品第二轮拍板以 D2 为准:门槛只在还有题可问时挡出卡,题源全空照现行规则出卡。当前实现是修正后的版本,本记录各处数字也是修正后重测的。
- F2 实际处置 41 条而非 16 条:任务书的 16 条表是在
cfb41daf上数的,那一版用!mayDeliverOnPrecision短路盖住了 D5 带来的「七条线全部轮到」,撤回短路后另有 25 条既有断言跟着变。全部按三栏改,清单见 §2.3。 run_quality_gate.py --profile quick的最后一级是npm test,本机无 Docker,那 31 条与基线同名的失败照旧;前面各级(JSON、capabilities、fragments、BPHS、CORE_PYTEST_TARGETS)通过。- 浏览器级验收(登录态 / Chrome)、staging 部署核对仍是环境缺口,留给
docs/testing/rectification-guided-collect-20260916.md。