Files
Jyotisha/docs/tasks/PROGRESS-rectification-precision-gate-guided-collect-fix-20260916.md
T
jesse-ux 05cf20fb83
Independent Staging Quality Gate / validate (push) Successful in 14m41s
Independent Staging Quality Gate / publish (push) Successful in 40m10s
fix(ci): capability audit 丢掉 Next.js 路由组,公开 URL 不变
Gitea run 2697 唯一失败:侧栏把 /chart /ephemeris /reports 移进 app/(secondary)/ 后,_scan_app_routes 把文件夹名写进 app_routes。扫描时丢掉括号段。BUG-753。
2026-09-16 21:09:24 +08:00

23 KiB
Raw Blame History

进度 · 出卡精度门槛 + 引导式补经历(验收修复单,2026-09-16)

  • 任务书:docs/tasks/TASK-rectification-precision-gate-guided-collect-fix-20260916.mdF1F7 + 决策 D6
  • 基线:origin/staging @ a396bbe0(第二轮已 rebase;上一版基线 302ff085,代码基线 cfb41daf
  • 分支 / 工作树:codex/rectification-precision-gate-guided-collect-fix-20260916 / .worktrees/rectification-precision-gate-guided-collect-fix-20260916
  • BUG747753。开工时最大号是 743;实现期间另一会话把侧栏统一那一单合入 staging,占掉了 744~746,所以本单整体后移到 747 起。rebase 后 docs/BUG_HISTORY.md 里 743 → 744 → … → 752 连续。合入后 Gitea run 2697 仍红一条 capability audit,补 BUG-753。
  • 开工检索(AGENTS §5):BUG-733、654、656、740743 已逐条读完;BUG-733 的防复发条本轮补了一句(见下)。

0.2 合入后跟进(BUG-753Gitea run 2697

dc732825 把 F1 修绿(797 passed),但 run 26971 failedtest_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 笔:d9d34723fb77c86522edabbca396bbe0)。

  • 自动合并frontend/DESIGN.mdfrontend/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 编号 747752 不变744746 已被侧栏轮占用)。

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 B0.17 %
frontend/src/app/page.tsx 1836 行 1835 行(−1,未增长)
Skill 10.0.27 10.0.28sha256 f0bb8295…cffe

1. F1 · 记忆化 goldenBUG-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_probesdate 换成 _FrozenDatetoday() == 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.precisionGateMetpublicNextAction().precision_gate_methelper 路径不传 flag 时为 null),只上报、不决定时机。

撤回的三处短路(全部回到原判断):

  1. coverageBlocks 分支的 const narrowingOpen = stillNeedNarrowing(input) && !mayDeliverOnPrecision(input)
  2. !separation.sufficient 分支的 if (stillNeedNarrowing(input) && !mayDeliverOnPrecision(input))
  3. if (input.datedMethodCollectOpen === true && !input.userStopped && !mayDeliverOnPrecision(input))

保留门槛的五个最终交付分支:tied_first 耗尽、!sufficient 的 exhausted / adopt_representative / offer、顶层 exhausted。

v9/decision-from-dossier.tsnarrowingExhaustionguidedCollectExhausted 改为 refreshDone && guidedCollectExhausted(...),即引导池空但刷新没试过不算问完。

新增三条单测(rectification-precision-gate-20260916.test.ts):

单测 断言
empty guided pool with refresh untried does not deliver refreshExhausted: false + 池空 + 门槛达标 → ask_fact_collectioncanOfferRange=falsemayDeliverOnPrecision 直接返回 false
gate met but the targeted line is still open keeps asking precisionGateMet: true + targetedCollectExhausted: falseask_fact_collection
every line asked but the gate unmet still delivers under the existing rules 池空 + 门槛未达 → canOfferRange=truesessionOutcomeAllowsDelivery=true,且 precisionGateMet 仍报 false(D2:题源空了就按现行规则出卡,不加标注)
the precision gate is reported on a collect exit too 采集出口上 decision.precisionGateMet === falsepublicNextAction().precision_gate_met === falsehelper 路径(不传 flag)为 null

2.2 十六条基线红(任务书 §2 F2 表)

# 文件 :: 用例 处置
1 adopt-flow-fix-20260903 :: collect-phase nonterminal exit… 改断言(三栏)adopt_representative/offer_provisional_range/publicCanAdopt=truecollect_evidence/ask_fact_collection/false;随之 repaired.persisted false→true、repair 警告 false→true、store.focus null→非空。D113 分钟、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 horaryeducation。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 === nullchoice_kind === "existence"USER_COLLECT_QUESTION.educationTARGETED_EXISTENCE_PROMPT.education,另加一条「不得是反向核对口吻」的断言,保住题名的原意
8 collect-direction-20260904 :: four scoreable events stop dated collect… 改断言(三栏)intent != collect_method_evidence== collect_method_evidencecollection_key 必须是 `collect:(targeted
9 collect-direction-20260904 :: separated candidates stop leftover dated collect once the training gate is open 改断言(三栏)sessionOutcome != collect_evidence== collect_evidencenextAction = ask_fact_collectionuserAction.id = ask_method_followup
10 convergence-budget :: dossier and post-inference decisions share the half-uncertain stop rule 改断言(三栏)stopReason = "probe_pool_exhausted"null,并显式断言两条路径的 stopReasonnextAction 一致(题名要的「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 = truefalse(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.relationshipT4b 问整个领域);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 的 T4bremainingTargetedDomains 不再按分盘层剔除,七个 CollectKind 全部轮到)就重新挡住了交付——这是 D5 + D6 的预期结果,但 cfb41daf 用短路把它盖住了,所以这 25 条在基线上是绿的。全部按「原值 / 新值 / 原因」三栏改:

文件 条数 处置摘要
coverage-collect 4 offer_provisional_range / ready_to_adoptask_fact_collection + collect_evidencecanAdopt 仍是引擎能力位,保持 true
provisional-adopt 6 公开投影 can_adopt / session_outcome 翻到采集侧;canRenderRectificationSelectionCards true→falsebuildNextUserAction adopt_representativeask_method_followupselectionAllowed true→false
range-offer-deadend 2 ready_to_adoptask_fact_collectiondecision.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_representativecollect_evidencestopReason→null、旁白由三句交付改为引导句
occupation-coverage-exit 1 canOfferRange true→false、collect_evidence / ask_fact_collection
holdout-renderable 1 offer/readyask_fact_collectionvalidated_range 断言保留
collect-stall 1 只改夹具:补齐 education / career / relationship 三条拒答(D5 之后七条线都要轮到),断言全部保持原样(publicCanAdopt=truepersisted=false、不写 focus
adopt-flow-fix-20260903 1 adopt_representative nonterminal exit…:第二轮修正口径后无需改动,夹具与断言均恢复原样
answer-choice 1 last structured choice…ready_to_adopt / can_adopt=trueask_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-stallconfirmation-gatedelivery-report-factseight-methodexhaustion-exitingest-p0occupation-coverage-exitrange-offer-deadendreplay-20260911spoken-collectv9-agentv9-contractsv9-entry-routingwindow-cluster-capyearless-probe-downgradeskill-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 / firstOpenCollectKindguidedWindowPrompt(window, openDomains)any 写「YYYY 年 M 到 M 月之间,有没有什么事,比如<口语 ≤3 个>?」,口语从 KIND_ORAL 表取、剔除 declined 与已覆盖,没有按领域写 if;全部领域都关掉时退化成「有没有什么事?」。windowAlreadyAsked 改成只比对(年, month_lo, month_hi),换个领域标签不重问。A 之后 pendingGuidedYearWindowany 解析成第一个仍开放的领域,录入卡芯片默认落在它上面(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 = 出生年 + 5hi = 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.tsx 1838 → 1837rectificationDeclaredTime 常量改成 rectificationDeclaration = { declaredTime, birthDate }JSX 里两行 prop 合成一行 {...rectificationDeclaration}偏离:任务书写的是「把那一行挪进 conversational-birth-time-rectification.tsx 内部取档案」。做不到而且不划算——该组件没有档案来源:RectificationCaseSnapshotPayloadcase 段只有 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:0012:2020 分钟窗,5 个采样分钟)跑真实引擎,断言窗口非空并逐字段校验;test_declined_domains_are_removed 改成 test_declined_domains_fall_back_to_open_windows,用真实引擎结果做,skipTest 全部删除(文件里已无 skipTest)。手造 static contexts 只留在排序与跨年两条纯结构断言里。

7. 记录

  • docs/BUG_HISTORY.md:BUG-747752(本分支内连续;744~746 属侧栏统一单,见 §0),BUG-733 补一行。
  • CHANGELOG.md2026-09-16 「问完所有线才出卡,时间窗不再硬贴一个领域」,写明 Skill 10.0.28。
  • frontend/DESIGN.mdguided-collect 行改写(开放窗口题干、一窗一问、录入卡提交文本),candidates 行写明门槛是必要条件而非充分条件。
  • frontend/docs/VOICE.md:新增两行(开放窗口题干、录入卡提交文本),已对照既有口径。
  • docs/tasks/README.md:本单状态板改「已实现,待验收」+ 分支名 + 进度记录名。
  • 真机清单沿用 docs/testing/rectification-guided-collect-20260916.md(六条),本轮没有新增浏览器级验收项。
  • 未写入任何真实出生资料;示例只用公开名人 holdout 与 1990-01-01 测试盘。

8. 偏离与未做

  1. F7 的 birthDate 没有挪进组件内部(理由见 §6),改用 props 合并把 page.tsx 降回 1837 行。
  2. (第一轮偏离,已修正) 第一轮按修复单 F3 §2 的字面实现了「门槛未达即使线问完也只进 collect」,与主单 D2「引导题源也空了就按现行规则出卡」冲突,会造出「没有题也没有卡」的死角。产品第二轮拍板以 D2 为准:门槛只在还有题可问时挡出卡,题源全空照现行规则出卡。当前实现是修正后的版本,本记录各处数字也是修正后重测的。
  3. F2 实际处置 41 条而非 16 条:任务书的 16 条表是在 cfb41daf 上数的,那一版用 !mayDeliverOnPrecision 短路盖住了 D5 带来的「七条线全部轮到」,撤回短路后另有 25 条既有断言跟着变。全部按三栏改,清单见 §2.3。
  4. run_quality_gate.py --profile quick 的最后一级是 npm test,本机无 Docker,那 31 条与基线同名的失败照旧;前面各级(JSON、capabilities、fragments、BPHS、CORE_PYTEST_TARGETS)通过。
  5. 浏览器级验收(登录态 / Chrome)、staging 部署核对仍是环境缺口,留给 docs/testing/rectification-guided-collect-20260916.md