docs(rectification): rewrite convergence brief after upstream comparison
Independent Staging Quality Gate / validate (push) Successful in 9m29s
Independent Staging Quality Gate / publish (push) Successful in 20m8s

Comparing against the upstream skill repo overturned v1's premise. The
three methodology files are byte-identical across both repos, and the
production v5 scorer imports the same upstream event engine the v4 ranker
does, so swapping engines is off the table and v1's three-way bake-off is
dropped.

What productization actually dropped is the stopping mechanism: upstream
terminates on a fixed 8-question bank and a one-shot adjudication, while
the production decision path discriminates as long as a probe exists. The
round budget is computed but never read by that path, and the "exhausted"
branch it needs already exists. Tasks now wire that budget in, restore the
upstream next-step summary, and keep the report readable from turn zero.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0155nFCgCHtoA7jhSDGmZmMu
This commit is contained in:
Jesse_Chen
2026-08-30 19:35:52 +00:00
parent 7db2dd2de0
commit db6716e76c
+186 -172
View File
@@ -1,101 +1,125 @@
# 任务书 · 生时校正收敛重构(2026-08-30
# 任务书 · 生时校正收敛重构(2026-08-30v2
基线:`origin/staging` @ `e7226bbf`
基线:`origin/staging` @ `7db2dd2d`
**下面所有行号只是线索,请按选择器/函数名定位**,后续提交可能让行号偏移。
**下面所有行号只是线索,请按选择器/函数名定位**,后续提交可能让行号偏移。标注为「上游」的行号来自对 `/workspace/yinduzhanxing` 的一次调研,动手前请自行复核。
> **v2 说明**:v1 的判断是「线上打分器可能不如本地方法学,需要三方盲测决定是否换引擎」。对上游仓库 `/workspace/yinduzhanxing` 做完比对后,这个判断被推翻了。换引擎的路线已排除,本版的落地点比 v1 小一个数量级。v1 的任务 0(三方盲测)作废。
## 为什么要做
本轮的依据是对着代码和标定数据做的一次审计。先摆事实。
### 事实 1 · 方法学没有丢,也没有落后
### 事实 1 · 仓库里有三套互不相干的校正系统,被评测的那套没人用
`/workspace/Jyotisha` 与上游 `/workspace/yinduzhanxing` 同源(共同初始提交 `a5bbba28`2026-04-20),分叉于 `825a9ea5`2026-07-17)。分叉后上游 808 个提交、本仓 1,275 个提交,**互相同步 0 次**。
| | 系统 | 谁在跑 | 打分由谁做 | 评测状态 |
| --- | --- | --- | --- | --- |
| **A** | `skills/jyotish-vedic-astrology/.../references/birth-time-rectification-{advanced,decision-tree,cases}.md`(约 15.5k 字符) | 本地 Agent(合伙人的用法) | **模型自己推理** | **从未评测** |
| **B** | `_compute_rectification_v5_score``scripts/jyotish_api_server.py`),前端经 `/api/rectification/v5/score` 调用 | **线上生产** | 服务端 v5 打分器 | **从未评测** |
| **C** | `scripts/minute_rectification_fact_ranker_v4.py` | 只有评测脚本引用 | 服务端 v4 打分器 | 测过,见下 |
`references/rectification_sealed_holdout.v1.json` 的那组数字——`top_1_rate 0.15``top_3_rate 0.25``mean_absolute_minute_error 6.95``confirmation_coverage_rate 0.0``status not_ready``source_audit_status invalidated_after_replay`——**测的是 C**。而且文件自己写明:
但生时校正方法学的三份文件 `cmp` 逐字节相同:
```
current_tree_scorer.matches_metrics_scorer = false
current_tree_scorer.official_eval_trial_count = 0
IDENTICAL birth-time-rectification-advanced.md
IDENTICAL birth-time-rectification-decision-tree.md
IDENTICAL birth-time-rectification-cases.md
```
`grep` 确认 `fact_ranker_v4` 只被 `minute_rectification_fact_blind_eval_v4.py` / `minute_rectification_development_eval.py` / `conversational_minute_rectification_replay.py` 引用,**没有被 `mcp_server.py` 或前端引用过**。
`yinduzhanxing/references/` `Jyotisha/skills/jyotish-vedic-astrology/versions/6.9.14/references/`
生产 skill`skills/jyotish-birth-time-rectification/versions/10.0.9/SKILL.md`)第 29 行明确规定"全部计算与持久化只走服务端工具"、"候选范围与评分"以服务器为唯一权威——**B 链路的模型被禁止参与打分**。而 A 链路的模型是自己做方法学推理的。这是两种根本不同的架构,不是同一系统的两个版本。
上游六周里在校正上新增的是治理、契约与复现文档,不是方法学本身。**「本地版本更新」不成立。**
**所以:线上这套(B)的真实能力,目前没有任何数据。**
### 事实 2 · 三套打分链路是同一算法家族
### 事实 2 · 标定数据总量极小
- `Jyotisha/scripts/rectification/scoring_service.py:11` 直接 `from scripts.active_rectification_event_engine import ...` —— 线上 v5 调用的就是上游那个事件引擎。
- `Jyotisha/scripts/minute_rectification_fact_ranker_v4.py:16` import 同一个 `DOMAIN_CONFIG`
- 两边共享同一套权重:Vimshottari MD/AD/PD = `2.0 / 1.5 / 0.75`Narayana MD/AD = `2.0 / 1.0`
| 数据集 | 案例数 | 状态 |
| --- | ---: | --- |
| `minute_rectification_holdout_v3` | 20 | 源审计后作废 |
| `minute_rectification_holdout_v4_intake` | 4 | `exposed_awaiting_human_rereview``blind_holdout_eligible_case_count = 0` |
| `minute_rectification_development_v1` | 3 | 开发集 |
因此 `references/rectification_sealed_holdout.v1.json` 那组数字(`top_1_rate 0.15``mean_absolute_minute_error 6.95``confirmation_coverage_rate 0.0`)虽然测的是 v4,**对线上 v5 是弱但真实的先验,不能当作无关数据**。上游也没有更好的打分器可换。
v4 的准入门槛写在 `minimum_gate` 里:`public_aa_cases: 20`,且 `production_tuning_allowed: false`。也就是说**现在没有任何一个可用于盲测的冻结 holdout**。
### 事实 3 · 真正丢掉的是「停下来」的机制
### 事实 3 · 访谈不收敛,是因为终止条件不可达
上游三条链路每一条都天然终止:
近 40 个校正提交里最大的一类是停滞:
| 上游链路 | 终止方式 |
| --- | --- |
| `scripts/active_rectification_questions.py` | `QUESTION_TEMPLATES` 是**写死的 8 题**分 3 轮;问完 `workflow_status``dated_event_review_required`(上游 :537 |
| `scripts/active_rectification_events.py` | `adjudicate_candidate_rows()` **一次性批量裁决**(上游 :212),算完直接给 low/medium/high |
| `jyotish-app/rectification-engine.js` | 浏览器内一次算完给置信度标签,无追问 |
```
prevent silent collect focus stalls
prevent collect focus dead-end after choice answers
close non-converging range offer without an exit
keep collection denials from stalling the interview
stop occupation coverage from locking questions and the range exit
BUG-028 收到具体经历后仍重复泛问 / BUG-029 提前结束 / BUG-236 长期停留在建立记录
且上游**从不承诺分钟**`can_apply` 默认 `False``truth_status: not_birth_time_truth` 写死在代码里。
Jyotisha 把这套替换成了服务端逐轮动态生成 discriminator probe 的**无界循环**。
### 事实 4 · 无限访谈的机械解释
生产判定 `frontend/src/lib/rectification-agentic/core/rectification-decision.ts:144-149`
```ts
if (!separation.sufficient) {
if (probe && !userStopped) return discriminate(...); // 只要有探针就继续问
return completeWithRange(..., "offer");
}
```
`confirmation-gate.ts` 是 fail-closed 的:holdout 没 ready 就永不发唯一分钟确认。于是访谈只能继续问。**这些 stall 不是各自独立的 bug,是"终止条件是确认唯一分钟、而该条件当前不可达"这一个结构性矛盾的下游症状。**
**这条路径里没有任何轮次计数器。** 同时:
### 事实 4 · 一条消息有四个作者
- `confirmation_coverage_rate: 0.0` —— sealed holdout 的 20 例里**零例**达到确认门,即门几乎不会开。
- probe 的 `semantic_key``domain.year.month` 构造,随候选集每轮重新生成,实际不会耗尽。
服务端决定问什么(`src/lib/rectification-agentic/v9/method-followup.ts`,2,036 行)→ 模型写正文 → 服务端把题干接在正文之后 → UI 再渲染选择卡
**门永远不开 + 探针永不耗尽 = 无限访谈。** 这是全部 stall 类 bug 的根因
于是 `src/mastra/agentic-rectification.ts` 的系统提示第 4 条要求模型判断自己处在 `choice` / `collect_spoken` / 无持久化问题 等状态中的哪一种,然后区别行动。模型做不到,所以有了 `src/lib/rectification-agentic/v9/spoken-answer.ts`:**33 条内部标识符正则 + 68 条中文过程话术正则,共 101 条模式**,在事后擦模型漏进用户可见文本的内心戏。
### 事实 5 · 8 轮上限算了,但没人拿它当终止条件
每一条正则都是一个曾经发生过的 bug。
不要误以为它是死代码。生产链路确实在跑它:
### 事实 5 · 有一条与线上结论冲突的一手观察
```
mastra/rectification-v9-tools.ts:945 → buildCaseInferenceState
→ v9/inference-adapter.ts:248 → buildInferenceState
→ core/build-state.ts:162 → evaluateConvergence
→ core/convergence-evaluator.ts:54 maxRounds = state.max_rounds ?? DEFAULT_MAX_DISCRIMINATION_ROUNDS
→ core/types.ts:10 DEFAULT_MAX_DISCRIMINATION_ROUNDS = 8
```
产品侧报告:**A 链路(本地 Agent 直接执行 skill 方法学)能收敛到准确的出生时间。**
`max_rounds` 没有被传入,取默认 8,算出的 `result_status` 也写进了 state。**但 `rectification-decision.ts` 的判定不读它。**
这条观察不能被忽略,也不能直接采信,因为两件事同时成立:
所以修法**不是**「启用死代码」(去改 `convergence-evaluator.ts` 改了也没用),而是**把轮次预算接进 `rectification-decision.ts` 的判定输入**。
- 它**可能是真的且极其重要**。如果模型执行方法学的效果好于手写的 v5 打分器,那么生产(B)建在了错误的引擎上,而这恰好能解释访谈为什么永远不收敛——B 到不了终点,整个对话引擎是在为它打补丁。
- 它**目前不可证伪**。本地运行几乎肯定不是盲测:跑的人知道答案,或案例的声明窗口本就很窄。`holdout_v3` 被判 `invalidated_after_replay``v4_intake` 状态是 `exposed_awaiting_human_rereview`,说明这个仓库已经吃过一次"结果被曝光后作废"的亏。
而且终止分支已经存在:`rectification-decision.ts:158` 就有 `completeWithRange(separation, holdout, range, "exhausted")``:336``kind` 枚举也已支持 `"exhausted"`。**代码路径是现成的,只是没人触发。**
**这不是一个靠讨论能解决的分歧,是一个靠对照实验能解决的问题。** 任务 0 因此改为三方盲测对照,在拿到结果之前,本任务书不预设哪条链路更好
另有一套完整但闲置的停机策略:`frontend/src/lib/birth-time-dynamic-stop-policy.ts:43-48`(高置信 / 答满 10 条 safety cap / 平台期 2 轮 / 零信息增益 / 重复分区),属旧链路,V9 不走它
### 事实 6 · 上游同样没有盲测证据
`yinduzhanxing/references/real_case_calibration/timing_rectification_*_2026_07_23.json`
```
blind_replay_allowed = false
ready_case_count = 0 (minimum_case_count = 3)
reviewer_2_confirmed_count = 0
status = blocked_until_human_labels
production_tuning_allowed = false
progress.md:1611 — "zero cases ready for blind replay"
```
`birth-time-rectification-cases.md` 里的奥巴马/居里夫人案例,是**拿已知出生时间回验事件**并自评「吻合度 95%」——知道答案的复盘,不是校正测试。
**两边都没有盲测数据。产品侧「本地能收敛」的观察,最可能是定义差异**:上游问完固定题数就给候选区间并停下,那个体验就是「收敛」;按代码它给的一定是区间 + low/medium/high 标签,不是确定的分钟。
### 结论
**任务 0 之前不做任何架构结论。** 三条候选路线,由任务 0 的数据决定:
产品目标定为:**交付收窄后的区间,不交付唯一分钟。** 唯一分钟保留为 sealed holdout 通过后才开启的路径。
1. 若 A 显著优于 B → 生产改用模型执行方法学,B 降级为校验器。这会让任务 1–3 的形态大改,但方向明确
2. 若 B 与 A 相当或更好 → 保留 B,按本任务书的任务 1–3 重构收敛与交互。
3. 若两者都达不到"稳定收窄区间" → 停止交互层投入,回到方法学与打分本身。
无论哪条路线,**产品目标都应从"确认唯一分钟"改为"交付收窄后的区间"**——因为 `confirmation-gate.ts` 的 fail-closed 依赖 sealed holdout,而 holdout 的准入门槛(20 例 AA 盲测案例)在任务 4 完成前不可能满足。唯一分钟保留为 holdout 通过后才开启的路径。
要做的是**把上游那套「问完就停、停下来时坦白交代」的产品契约接回来**,不是重写收敛逻辑,更不是换打分引擎
---
## 硬红线
1. **任务 0 是门控** 三条链路(A 本地方法学 / B 线上 v5 / C v4 基线)的真实指标必须先在同一盲测协议下测出来。**在拿到对照数据之前,不得改动任何打分逻辑、不得改动收敛阈值、也不得把生产切换到任一链路。**「合伙人说本地能收敛」是待验证的假设,不是可以直接依据的结论;同样,「holdout 显示 15%」测的是 C,不能用来否定 A 或 B
2. **诚实性不可让渡。** 不得为了让访谈"看起来能收敛"而放宽 `confirmation-gate.ts` 的任何 blocker,不得降低 `SEALED_MINUTE_HOLDOUT` 门槛,不得在 `confirmation_allowed=false` 时宣称唯一分钟。这套 fail-closed 机制是本产品最值钱的资产之一,它现在正在正确工作。
3. **不得用 holdout 调参。** `minute_rectification_holdout_v4_intake.json``production_tuning_allowed``false``boundary` 字段写明晋级需要新版本、通过源审计、且打分身份在盲测前冻结。任何"跑一下看看效果再调"都属于污染,一旦发生该数据集即作废。开发集用于调参,holdout 只用于一次性验证
4. **收敛函数是唯一判据。** 任务 1 落地后,任何模块不得再单方面决定访谈是否继续。所有约束以输入形式喂给它
5. **区间只能变窄或不变,不得变宽。** 这是收敛的单调性不变量,必须有属性测试守住
6. **状态机留在 Postgres 函数里**,沿用既有模式`agentic_rectification_*` 的 security definer 函数)。不得把状态机搬进应用层。
1. **任务 0 是门控,但它只是一次信息收集,不许写代码。** 在拿到上游访谈手册与一份真实会话记录之前,不得改动 `rectification-decision.ts` 的判定
2. **诚实性不可让渡。** 不得为了让访谈看起来能收敛而放宽 `confirmation-gate.ts` 的任何 blocker,不得降低 `SEALED_MINUTE_HOLDOUT` 门槛,不得在 `confirmation_allowed=false` 时宣称唯一分钟。这套 fail-closed 机制现在正在正确工作。
3. **超预算的出口必须是「交付区间」,不是「失败」。**`completeWithRange(..., "exhausted")`,用户拿到候选区间与明确的边界说明。任何把超预算实现成报错、静默停止或空白页的做法都是错的
4. **不得用 holdout 调参。** `minute_rectification_holdout_v3``source_audit_status``invalidated_after_replay`,只能当开发集看趋势,**不得作为发布指标,不得写回 sealed 字段**。`v4_intake``production_tuning_allowed` 保持 `false`
5. **不要引入第五套打分器。** 不要移植 `jyotish-app/rectification-engine.js` 的 40/35/15/10 权重。它信号更弱(无 Narayana / Arudha / Ashtakavarga / Shadbala)且从未评测
6. **状态机留在 Postgres 函数里**,沿用既有模式。不得把状态机搬进应用层。
7. **不得修改既有测试断言** —— 除非该断言锁的正是本轮要改的缺陷本身;那种情况必须在断言上方注明原值与原因,并在 PROGRESS 单列。
8. 推 staging 前必须 `./node_modules/.bin/tsc --noEmit` 通过。**不要用 `npx tsc`**,本仓库环境下会装到空包 `tsc@2.0.4`
9. **数据库测试必须真跑。** `npm run test:db` 需要 Docker。**没有 Docker 就不要推** —— 把环境缺口写进 `BLOCKED.md` 并停下。
9. **数据库测试必须真跑。** `npm run test:db` 需要 Docker。没有 Docker 就不要推 —— 写进 `BLOCKED.md` 并停下。
10. 不得改 `.gitea/workflows/**`。不得在有未提交改动的工作树上切分支。不得自行把 staging 提升到 main。
让步顺序:**诚实性不回退 > 数据不损坏 > 功能与测试不回归 > 可验证的收敛改进 > 代码整洁 > 成本**。
@@ -108,103 +132,81 @@ git worktree add -b codex/rectification-convergence-20260830 \
../.worktrees/rectification-convergence-20260830 origin/staging
```
`pre_work_error_ledger.md`,跑 `scripts/pre_work_check.py`,读 `frontend/AGENTS.md`。改前在 `docs/BUG_HISTORY.md` 检索 `rectification` / 生时校正 相关记录(至少 30 条,务必读完停滞类那几条)。
`pre_work_error_ledger.md`,跑 `scripts/pre_work_check.py`,读 `frontend/AGENTS.md`。改前在 `docs/BUG_HISTORY.md` 检索校正相关记录(30 条以上,停滞类那几条务必读完)。
**先读这些再动手:**
- `references/rectification_sealed_holdout.v1.json` —— 打分器现状的唯一权威
- `scripts/minute_rectification_fact_ranker_v4.py` —— 打分器实现
- `scripts/minute_rectification_fact_blind_eval_v4.py` —— 盲测入口(`--manifest` 参数)
- `src/lib/rectification-agentic/v9/confirmation-gate.ts` —— fail-closed 的确认闸门
- `src/lib/rectification-agentic/v9/candidate-plateau.ts` —— `indistinguishableWidthMinutes`,本轮要把它从否决器改成目标
- `src/lib/rectification-agentic/v9/{decision-from-dossier,turn-decision,server-focus,method-followup}.ts` —— 当前分散的六处决策
- `src/lib/rectification-agentic/v9/spoken-answer.ts` —— 101 条清洗正则,任务 2 要整个删掉
- `src/mastra/agentic-rectification.ts:61-68` —— 系统提示,任务 2 要重写第 4 条
- `frontend/src/lib/rectification-agentic/core/rectification-decision.ts` —— 生产判定,本轮主战场
- `frontend/src/lib/rectification-agentic/core/convergence-evaluator.ts` + `core/build-state.ts:162` —— 已在跑但没被消费的轮次判定
- `frontend/src/lib/birth-time-dynamic-stop-policy.ts:43-48` —— 现成但闲置的停机策略
- `frontend/src/lib/rectification-agentic/v9/spoken-answer.ts` —— 101 条清洗正则,任务 2 要删
- `frontend/src/mastra/agentic-rectification.ts:61-68` —— 系统提示,任务 2 要重写第 4 条
- 上游 `/workspace/yinduzhanxing/scripts/active_rectification_events.py:115-179` —— 任务 3 要移植的 `build_candidate_result_summary()`
- 上游 `/workspace/yinduzhanxing/jyotish-app/rectification-collaboration.js` —— 四阶段状态机,参考它有多简单
---
## 任务 0P0,门控)· 三方盲测对照
### 目的
在同一批案例、同一套盲测协议下,测出 A(本地 Agent 执行方法学)、B(线上 v5 打分器)、C(v4 fact ranker,作为历史基线)三条链路的真实能力。**本任务的产出直接决定后续架构走向,任务 1–5 在它出数前一律不得开工。**
### 盲测协议(不可简化)
1. **案例集**`minute_rectification_holdout_v3` 的 20 例。v3 作为 sealed holdout 已作废,因此本轮结果**只能作为开发期对照,不得写入 `rectification_sealed_holdout.v1.json` 的 sealed 字段,不得对外宣称为盲测认证结果**。产出物落在新的对照报告文件里。
2. **真实出生时间必须对三条链路全程屏蔽。** 执行者(含人和 Agent)在出结果前不得接触答案。A 链路尤其要注意:**由不知道答案的人或自动化脚本驱动,不得由了解案例的人手动引导**。这是本任务唯一真正的技术难点,也是它值得做的原因。
3. **三条链路吃完全相同的输入**:同一份出生日期、地点、声明窗口、事件列表。不得给 A 更多上下文。
4. **打分身份先冻结**:记录三条链路各自的实现 `sha256`B 取 `jyotish_api_server.py` 的相关函数,C 取 `minute_rectification_fact_ranker_v4.py`A 取三份 reference md 的哈希),冻结后再跑。
5. 每条链路每例只跑一次,**不得挑最好的一次**。失败或超时如实记为失败。
### 要报的指标
对三条链路各出一份,**区间指标是重点**,唯一分钟命中率是次要的:
| 指标 | 说明 |
| --- | --- |
| `mean_absolute_minute_error` | 与真实时间的平均偏差 |
| `p50` / `p90` 区间宽度 | 交付物的实际精度 |
| 收窄曲线 | 证据条数 N=1..8 时的区间宽度,**这是任务 1 阈值的唯一依据** |
| `top_1_rate` / `top_3_rate` | 与历史基线可比 |
| 完成率 | 有多少例根本没收敛/超时/报错 |
| 单例成本 | A 链路的 token 成本可能显著高于 B,这直接影响 `TASK-billing-pricing` 的定价 |
### 验收
- 三条链路的对照表,同一批 20 例,同一协议。
- 盲测隔离有可复核的证据(谁跑的、答案何时揭晓、脚本或流程记录)。
- 各链路实现 `sha256` 已记录。
- **没有任何 holdout 文件被调参污染**;`v4_intake``production_tuning_allowed` 仍为 `false`
- PROGRESS 里给出明确的路线建议(结论章节的三选一),并说明依据。
### 停止条件
- 若无法建立可信的盲测隔离(例如 A 链路只能由知情者手动驱动)→ **停下**,写进 `BLOCKED.md`。一个不盲的对照结果比没有结果更危险,因为它会被当成决策依据。
- 若三条链路的收窄曲线都不随证据增加而下降 → **停下**,问题在方法学与打分,不在交互层,任务 1–3 全部作废重议。
## 任务 1(P0)· 收敛判据收敛成一个纯函数
**前置:本任务仅在任务 0 选定路线 2(保留 B)或路线 1(改用 A)后开工,且收敛阈值必须取自任务 0 的收窄曲线。**
## 任务 0P0,门控,不写代码)· 拿到上游访谈手册与一份真实会话
### 事实
"该继续问 / 该出牌 / 该收摊"目前散在至少六处:`decision-from-dossier.ts``turn-decision.ts``confirmation-gate.ts``candidate-plateau.ts``method-followup.ts``server-focus.ts`。每一处都能单方面返回空把流程卡住。`stop occupation coverage from locking questions and the range exit` 就是两个模块互相顶死的产物
调研发现一个**仓库外**的独立 skill 包:`~/.workbuddy/skills/jyotish-birth-time-rectification/`,含 `references/interview_playbook.md`(访谈手册)与 `references/evidence_thresholds.md`。它不在 `yinduzhanxing` 仓库里,只有 `yinduzhanxing/tests/test_birth_time_rectification_skill_contract.py:7` 引用了这个路径
产品侧「本地 Agent 能收敛」的观察,如果另有机制,最可能就在这两份文件里。
### 要做什么
1. 新建 `src/lib/rectification-agentic/core/convergence.ts`,导出一个**纯函数**
1. 向上游维护者索取 `interview_playbook.md``evidence_thresholds.md`,放进 `references/upstream/` 并在 PROGRESS 记录来源与获取日期。
2. 索取**一次真实本地校正会话的完整记录**,要能回答三个问题:
- 一共问了几轮?
- 最后交付的是一个分钟,还是一个区间?区间多宽?
- `confidence` / `can_apply` 是什么值?
3. 把答案写进 PROGRESS,并据此确认或推翻这条判断:**「上游的收敛 = 问完固定题数后给出候选区间并停止」。**
```
converge(候选集, 证据账本, 已问问题, 门闸状态) →
| { kind: "ASK", question, expectedGainMinutes }
| { kind: "NARROW", range, widthMinutes }
| { kind: "SETTLE", range, widthMinutes, confidence }
| { kind: "EXHAUSTED", reason, bestRange }
| { kind: "BLOCKED", reason }
```
### 验收
**没有第六种返回,不得返回 null/undefined。** confirmation gate、plateau、方法覆盖、holdout 状态全部作为**输入参数**传入,不得在函数内部再去读全局或查库
- 两份文件在仓库里,或明确记录「无法获取」及原因
- 会话记录的三个问题有答案。
- PROGRESS 给出结论:上游的终止机制是什么,Jyotisha 要复制的具体是哪一条。
2. 六处现有决策改为调用它。**本任务内行为保持等价** —— 先建立唯一判据,不改判断结果,风险最低。行为要变的部分留到任务 3。
3. `SETTLE` 的触发条件用任务 0 的实测宽度曲线定,不得用任务书里的任何数字。
### 如果拿不到
### 属性测试(这是本任务的核心交付,不是附属品)
**不阻塞任务 13。** 任务 1 的修法不依赖这份手册——轮次预算该接还是要接。但拿不到这件事要写进 `BLOCKED.md`,因为它是「本地能收敛」这条观察唯一可能的另一种解释。
`frontend/tests/` 下新增,至少覆盖:
---
- **全域非空**:对任意合法输入组合,`converge` 必返回五种之一。用随机生成的候选集/证据组合跑,不是几个手写用例。
- **单调性**:追加一条证据后,`widthMinutes` 只能变小或不变,**永不变大**(红线 5)。
- **可终止**:从任意状态出发,反复喂 `ASK` 返回的问题的答案,必须在有限步内到达 `SETTLE` / `EXHAUSTED` / `BLOCKED`。**不存在无限 ASK 循环。**
- **拒答不卡死**:用户对每一个 `ASK` 都拒答时,必须到达 `EXHAUSTED`,不得停在 `ASK`
## 任务 1(P0)· 把轮次预算接进生产判定
这四条属性一次性锁死未来所有 stall 类 bug,比再补 20 个 case 测试有效得多。
### 事实
见事实 4、5。`rectification-decision.ts:144-149``!separation.sufficient` 分支只要有 probe 且用户没喊停就无限 `discriminate`。轮次判定在别处算好了但没人读。
### 要做什么
1.`rectification-decision.ts` 的判定输入增加**预算三件套**,参数一律取自已有常量,**不要新造数字**:
- 轮次上限 —— `core/types.ts:10``DEFAULT_MAX_DISCRIMINATION_ROUNDS = 8`
- 平台期 —— `references/rectification_policy.v1.json``maxPlateauRounds`(当前只有 `scripts/rectification_policy.py` 读了,前端没用)
- 安全帽 —— `birth-time-dynamic-stop-policy.ts:45``effectiveAnswerCount >= 10`
2.`!separation.sufficient` 分支的 `discriminate(...)` **之前**插入预算检查。超预算时走**已存在的** `completeWithRange(separation, holdout, range, "exhausted")`:158)。
3. **不要去改 `convergence-evaluator.ts`。** 它算得没错,问题是没人消费。要么把 `result_status` 传进判定输入,要么在判定层独立计数——二选一,在 PROGRESS 说明选了哪个及理由。
4. 轮次计数必须**服务端持久化**,不得靠对话历史推断。沿用既有的 receipt / turn 计数来源。
### 属性测试(本任务的核心交付)
`frontend/tests/` 新增:
- **可终止**:从任意状态出发,反复喂 `discriminate` 返回的 probe 的答案,必须在 ≤ 预算轮数内到达某个 `completeWithRange` 分支。**不存在无限 discriminate 循环。**
- **拒答不卡死**:用户对每一个 probe 都拒答时必须到达终态,不得停在 `discriminate`
- **超预算即交付**:超预算时返回的是 `"exhausted"` 且带候选区间,不是错误或空结果。
- **单调性**:追加证据后区间宽度只能变小或不变,永不变大。
这四条一次性锁死未来所有 stall 类 bug。
### 验收
- 四条属性测试全绿。
- 既有校正端到端测试全绿(本任务行为等价)
- 六处决策点不再各自返回空
- 停滞类既有测试(`rectification-collect-stall.test.ts``rectification-range-offer-deadend.test.ts` 等)全绿
- 端到端:一个用户即使一直不给有效证据,也会在预算内拿到区间并结束
---
@@ -212,50 +214,58 @@ converge(候选集, 证据账本, 已问问题, 门闸状态) →
### 事实
见"事实 4"。当前 `CurrentQuestionKind = "choice" | "collect_spoken"``turn-decision.ts:33`)两套所有权规则,加上"无持久化问题时模型自己问",共三种模式,靠系统提示区分,靠 101 条正则兜底
一条消息目前有**四个作者**:服务端决定问什么(`v9/method-followup.ts`,2,036 行)→ 模型写正文 → 服务端把题干接在正文之后 → UI 再渲染选择卡
于是 `src/mastra/agentic-rectification.ts:66` 要求模型判断自己处在 `choice` / `collect_spoken` / 无持久化问题 三种状态中的哪一种再区别行动。模型做不到,所以有了 `v9/spoken-answer.ts`:**33 条内部标识符正则 + 68 条中文过程话术正则,共 101 条**,事后擦模型漏出的内心戏。每一条正则都是一个曾经发生过的 bug。
### 要做什么
1. **模型永远不提问。** 它在一轮里的唯一职责针对用户刚说的内容写**一句**确认/承接。系统提示第 4 条整条重写,删掉所有关于题干归属的条件分支。
2. **问题永远由服务端渲染,永远出现在同一个问题区。** `choice``collect_spoken` 合并一个"问题槽":有选项就可点,没选项就是输入框加提示。数据模型不再区分 kindUI 不再有两条渲染路径。
3. **不再有"服务端把题干接在模型正文之后"的拼接。** 一条消息 = 模型的一句确认(流式)+ 服务端问题区(结构化渲染),两者物理分离,各自唯一作者。
4. **删 `src/lib/rectification-agentic/v9/spoken-answer.ts` 的 101 条清洗正则。** 模型的输出面窄到不需要清洗。如果删完发现仍需清洗,说明第 1 步没做干净,回去改第 1 步,**不要把正则加回来**。
5. 服务端未产出问题时问题区为空——这是一个**可断言、可监控**的显式状态,不是静默停滞。加一条服务端告警`ASK` 之外的返回没有对应终态投影时上报
1. **模型永远不提问。** 一轮里的唯一职责针对用户刚说的内容写**一句**确认/承接。系统提示第 4 条整条重写,删掉所有关于题干归属的条件分支。
2. **问题永远由服务端渲染,永远出现在同一个问题区。** `choice``collect_spoken` 合并一个问题槽:有选项就可点,没选项就是输入框加提示。数据模型不再区分 kind,UI 不再有两条渲染路径。
3. **取消「服务端把题干接在模型正文之后的拼接。** 一条消息 = 模型的一句确认(流式)+ 服务端问题区(结构化渲染),两者物理分离,各自唯一作者。
4. **删 `spoken-answer.ts` 的 101 条清洗正则。** 如果删完发现仍需清洗,说明第 1 步没做干净,回去改第 1 步,**不要把正则加回来**。
5. 服务端未产出问题时问题区为空 —— 这是可断言、可监控的显式状态,不是静默停滞。加服务端告警。
### 验收
- 用户可见文本不可能出现内部标识符或过程话术,且**不是靠正则保证**,是靠模型输出面收窄保证
- 端到端连续 10 轮问答,问题始终出现在问题区,位置一致,不重复、不消失。
- 停滞类既有测试(`rectification-collect-stall.test.ts` 等)全绿。
- 用户可见文本不可能出现内部标识符或过程话术,且**不是靠正则保证**,是靠模型输出面收窄保证。
- 端到端连续 10 轮,问题始终在问题区,位置一致,不重复、不消失。
- `spoken-answer.ts` 的两条大正则不再存在于代码库。
---
## 任务 3(P0)· 交付物从第 0 轮就存在
## 任务 3P0)· 恢复「停下来时怎么交代」的契约,交付物从第 0 轮就存在
### 事实
"何时算交付完成"之所以难,是因为交付物本身没定义。BUG-031(未收敛仍永久扣费)就是这个缺口的直接后果。
上游 `active_rectification_events.py:115-179``build_candidate_result_summary()`,产出四个面向用户的 next-step code
```
collect_at_least_five_events
resolve_candidate_tie_or_narrow_window
narrow_window_before_minute_claim
do_not_apply_as_birth_time_truth
```
**Jyotisha 把这整段删了。** 于是访谈终止时没有话术锚点,只能继续问。
上游的降级阈值 Jyotisha 其实完整继承在 `references/rectification_policy.v1.json`(4 事件 / 3 域 / 5 分钟宽度 / 20% margin)——**判据没丢,丢的是「达不到时说什么」。**
### 要做什么
1. 定义**校正报告**投影,随时可读:
- 结论区间 `[HH:MM, HH:MM]`代表分钟(明确标注代表性,不是唯一解)
- 置信度:基于 N 条证据的吻合率
- 逐条证据:事件 → 支持哪个候选 → 用的哪个方法(dasha / D9 / D10 / D12…)
- 被排除的候选与排除理由
- 局限声明(沿用 `confirmation-gate` 的 blocker 文案,不要另写一套)
2. **第 0 轮就存在**:证据 0 条时区间 = 用户声明的出生窗口。每答一题重算。
3. 用户任何时刻可查看、可导出、可主动结束。结束即交付当前版本,不算失败。
4. UI 给出区间收窄进度:`±120 分钟 → ±45 → ±18 → ±7`。这是用户为 ¥198 买到的东西的可视化。
5. 计费与交付对齐:只要产生过至少一版报告,就是有效交付;`EXHAUSTED` 不再是失败态。**具体扣费金额由 `TASK-billing-pricing-20260830.md` 的任务 3 定,本任务只负责让"交付"这个事实可判定。**
1. 把上游的 `build_candidate_result_summary()` 恢复到 `Jyotisha/scripts/active_rectification_events.py`,让 `scripts/rectification/api_service.py``score_candidates()` 返回体带上 `next_step_codes``stability.label`
2. 定义**校正报告**投影,随时可读:结论区间 + 代表分钟(明确标注代表性、非唯一解)、置信度、逐条证据(事件 → 支持哪个候选 → 用哪个方法)、被排除的候选与理由、局限声明(复用 `confirmation-gate` 的 blocker 文案,不要另写一套)。
3. **第 0 轮就存在**:证据 0 条时区间 = 用户声明的出生窗口。每答一题重算。
4. 用户任何时刻可查看、可导出、可主动结束。结束即交付当前版本,**不算失败**。
5. UI 给出区间收窄进度(`±120 → ±45 → ±18 → ±7`)。这是用户为这笔钱买到的东西的可视化。
6. 计费与交付对齐:产生过至少一版报告即为有效交付,`exhausted` 不是失败态。**具体金额由 `TASK-billing-pricing-20260830.md` 决定,本任务不写任何金额。**
### 验收
-开的 case 在零证据时即可读出一份报告(区间 = 声明窗口)
- 每轮答题后区间单调收窄或不变,与任务 1 的单调性不变量一致
- 用户主动结束 → 拿到报告,状态是完成不是失败。
- 与计费任务书不冲突:本任务不写任何金额
- 新 case 在零证据时即可读出一份报告。
- 每轮答题后区间单调收窄或不变。
- 用户主动结束或超预算 → 拿到报告,状态是完成不是失败。
- `next_step_codes` 出现在服务端返回体里,并被终止话术使用
---
@@ -263,50 +273,54 @@ converge(候选集, 证据账本, 已问问题, 门闸状态) →
### 事实
`blind_holdout_eligible_case_count = 0`。v4 门槛要 20 例 AA 级公开案例,现在有 4 例且已曝光。**这是产品能不能承诺唯一分钟的唯一瓶颈,且它是数据问题不是工程问题**
两边都没有盲测数据(事实 6)。Jyotisha 的 `v4_intake` `blind_holdout_eligible_case_count = 0`,上游 `ready_case_count = 0`这是产品能不能承诺唯一分钟的唯一瓶颈,**且它是数据问题不是工程问题**
上游停在这里六周了,指望它自己解决不现实。
### 要做什么
做一个**免费的"验证我们"入口**面向**已经知道自己准确出生时间**(有出生证明/医院记录)的用户
做一个**免费的验证我们入口**面向已经知道自己准确出生时间(有出生证明/医院记录)的用户
1. 用户提供出生日期、地点、以及事件;**准确时间单独收集并对打分链路屏蔽**,全程不得进入候选生成或打分。
1. 用户提供出生日期、地点、事件;**准确时间单独收集并对打分链路屏蔽**,全程不得进入候选生成或打分。
2. 系统盲跑校正,出结果后再与真实时间比对,把偏差当场展示给用户。
3. 每一例都是一条合格标定数据,用户拿到一次免费的、可验证的能力演示。
3. 每一例都是一条合格标定数据,用户同时拿到一次免费的、可验证的能力演示。
一个入口同时解决三件事:标定数据来源、最有说服力的社会证明("我们在 N 个有出生证明的案例上盲测过,中位偏差 X 分钟")、以及获客内容。
一个入口同时解决三件事:标定数据来源、最有说服力的社会证明(我们在 N 个有出生证明的案例上盲测过,中位偏差 X 分钟)、获客内容。
### 硬约束
- **真实时间必须在架构上不可能泄漏进打分链路**,不是靠约定。写测试锁死。
- 入口收集的案例默认进 `intake` 队列,**晋级为冻结 holdout 必须走既有流程**:新版本号、通过源审计、打分身份在盲测前冻结`minimum_gate``boundary` 字段已写明规则,照办)
- **真实时间必须在架构上不可能泄漏进打分链路**,不是靠约定。写负向测试锁死。
- 案例默认进 intake 队列,晋级为冻结 holdout 必须走既有流程:新版本号、通过源审计、打分身份在盲测前冻结。
- 免费入口要有独立的滥用限制,不得复用咨询的公平使用配额。
- 与上游共享数据前先对齐口径,避免两边各建一套互不承认的标定集。
### 验收
- 真实时间泄漏的负向测试存在且通过。
- 新案例正确落入 intake 队列,不会被自动当成 holdout。
- 用户侧能看到"预测 vs 真实"的偏差对比。
- 用户侧能看到预测 vs 真实的偏差对比。
---
## 任务 5P2)· 清理
## 任务 5P2)· 清理与同步机制
**任务 13 全部上线并稳定运行两周后才做。** 提前做会让回滚变难。
**任务 1–3 上线并稳定运行两周后才做。**
1. **两代数据模型并存**`birth_time_rectification_*` 17 张(旧)+ `agentic_rectification_*` 16 张(新),共 31 张。逐张确认旧表是否仍被 RPC 写入,确认无引用的归档删除。这是 50 个校正迁移的主要来源之一。
2. **删掉永不执行的方法分支**`method-followup.ts` 注释写明第 5 项(外貌体质)与第 6 项(胎记疤痕)`skipped; never asked`代码里留着不问的分支只会误导后来者。
3. **36 个 v9 模块并成 8 个**`convergence` / `probe-catalog` / `scoring` / `evidence` / `case` / `narration` / `delivery` / `billing`
4. 每删一张表、每并一个模块单独一个提交,便于二分回滚
1. **两代数据模型并存**`birth_time_rectification_*` 17 张 + `agentic_rectification_*` 16 张 = 31 张,是 50 个校正迁移的主要来源。逐张确认旧表是否仍被 RPC 写入,无引用的归档删除。
2. **删掉永不执行的分支**`method-followup.ts` 注释写明第 5 项(外貌体质)与第 6 项(胎记疤痕)`skipped; never asked`
3. **`MIN_SEPARATION_LEAD` 与上游口径对齐**`core/candidate-separation.ts:7` 当前是绝对分差 8;上游 `active_rectification_events.py:256` 用的是相对 margin(< 10% 降级)。改成相对量后用 `minute_rectification_holdout_v3` 的 20 例看 `not_separated` 比例变化 —— **只看趋势,不作发布指标**(红线 4
4. **建立与上游的同步机制。** 六周零同步是本轮很多问题的背景。约定一个节奏(例如每月一次),至少同步 `references/``scripts/active_rectification_*`、方法学 md。这不是代码任务,是流程约定,写进 `AGENTS.md`
5. 每删一张表、每并一个模块单独一个提交,便于二分回滚。
### 不要动
打分引擎与 Swiss Ephemeris 计算、证据账本、Postgres 里的状态机、sealed holdout 机制。这些是对的。
打分引擎与 Swiss Ephemeris 计算、证据账本、Postgres 里的状态机、sealed holdout 机制`rectification_policy.v1.json` 的阈值。这些是对的。
---
## 交付
- PROGRESS 写进 `PROGRESS-rectification-convergence-20260830.md`,任务 0 的实测数据单列成表
- PROGRESS 写进 `PROGRESS-rectification-convergence-20260830.md`,任务 0 的会话记录结论单列
- 每个任务一个提交。
- 推 staging 前:`./node_modules/.bin/tsc --noEmit` 通过、`npm run lint` 无新增 error、`npm run test:db` 在 Docker 下 fail=0、`npm run build` 通过。
- 全量 `./node_modules/.bin/tsx --test tests/*.test.ts` 的失败清单与基线逐条比对,确认无新增。