Files
Jyotisha/TASK-rectification-convergence-20260830.md
T
Jesse_Chen 30ef6afea7
Independent Staging Quality Gate / validate (push) Has been cancelled
Independent Staging Quality Gate / publish (push) Has been cancelled
docs(rectification): archive upstream playbook and add stop-rule task
The upstream interview playbook and evidence thresholds resolve task 0.
Upstream stops and reports when evidence is thin — fewer than three dated
events, fewer than two domains, or a tie — while this repo treats the same
kind of thresholds as a confirmation gate and keeps asking when they are
not met. Its label ladder has no confirmed rung at all, and every contract
test asserts candidate_range_not_birth_time_truth.

Task 1 capped the interview by round count, which is half of it. Task 6
adds the evidence-state stop rules, reframes exhausted as a normal
delivery, adopts the upstream label ladder and closing wording, and keeps
the 4/3 confirmation gate separate from the 3/2 delivery floor.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0155nFCgCHtoA7jhSDGmZmMu
2026-08-31 02:27:32 +00:00

24 KiB
Raw Blame History

任务书 · 生时校正收敛重构(2026-08-30v2

基线:origin/staging @ 7db2dd2d

下面所有行号只是线索,请按选择器/函数名定位,后续提交可能让行号偏移。标注为「上游」的行号来自对 /workspace/yinduzhanxing 的一次调研,动手前请自行复核。

v2 说明:v1 的判断是「线上打分器可能不如本地方法学,需要三方盲测决定是否换引擎」。对上游仓库 /workspace/yinduzhanxing 做完比对后,这个判断被推翻了。换引擎的路线已排除,本版的落地点比 v1 小一个数量级。v1 的任务 0(三方盲测)作废。

为什么要做

事实 1 · 方法学没有丢,也没有落后

/workspace/Jyotisha 与上游 /workspace/yinduzhanxing 同源(共同初始提交 a5bbba282026-04-20),分叉于 825a9ea52026-07-17)。分叉后上游 808 个提交、本仓 1,275 个提交,互相同步 0 次

但生时校正方法学的三份文件 cmp 逐字节相同:

IDENTICAL  birth-time-rectification-advanced.md
IDENTICAL  birth-time-rectification-decision-tree.md
IDENTICAL  birth-time-rectification-cases.md

yinduzhanxing/references/Jyotisha/skills/jyotish-vedic-astrology/versions/6.9.14/references/

上游六周里在校正上新增的是治理、契约与复现文档,不是方法学本身。「本地版本更新」不成立。

事实 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.75Narayana MD/AD = 2.0 / 1.0

因此 references/rectification_sealed_holdout.v1.json 那组数字(top_1_rate 0.15mean_absolute_minute_error 6.95confirmation_coverage_rate 0.0)虽然测的是 v4对线上 v5 是弱但真实的先验,不能当作无关数据。上游也没有更好的打分器可换。

事实 3 · 真正丢掉的是「停下来」的机制

上游三条链路每一条都天然终止:

上游链路 终止方式
scripts/active_rectification_questions.py QUESTION_TEMPLATES写死的 8 题分 3 轮;问完 workflow_statusdated_event_review_required(上游 :537
scripts/active_rectification_events.py adjudicate_candidate_rows() 一次性批量裁决(上游 :212),算完直接给 low/medium/high
jyotish-app/rectification-engine.js 浏览器内一次算完给置信度标签,无追问

且上游从不承诺分钟can_apply 默认 Falsetruth_status: not_birth_time_truth 写死在代码里。

Jyotisha 把这套替换成了服务端逐轮动态生成 discriminator probe 的无界循环

事实 4 · 无限访谈的机械解释

生产判定 frontend/src/lib/rectification-agentic/core/rectification-decision.ts:144-149

if (!separation.sufficient) {
  if (probe && !userStopped) return discriminate(...);   // 只要有探针就继续问
  return completeWithRange(..., "offer");
}

这条路径里没有任何轮次计数器。 同时:

  • confirmation_coverage_rate: 0.0 —— sealed holdout 的 20 例里零例达到确认门,即门几乎不会开。
  • probe 的 semantic_keydomain.year.month 构造,随候选集每轮重新生成,实际不会耗尽。

门永远不开 + 探针永不耗尽 = 无限访谈。 这是全部 stall 类 bug 的根因。

事实 5 · 8 轮上限算了,但没人拿它当终止条件

不要误以为它是死代码。生产链路确实在跑它:

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

max_rounds 没有被传入,取默认 8,算出的 result_status 也写进了 state。rectification-decision.ts 的判定不读它。

所以修法不是「启用死代码」(去改 convergence-evaluator.ts 改了也没用),而是把轮次预算接进 rectification-decision.ts 的判定输入

而且终止分支已经存在:rectification-decision.ts:158 就有 completeWithRange(separation, holdout, range, "exhausted"):336kind 枚举也已支持 "exhausted"代码路径是现成的,只是没人触发。

另有一套完整但闲置的停机策略: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 标签,不是确定的分钟。

结论

产品目标定为:交付收窄后的区间,不交付唯一分钟。 唯一分钟保留为 sealed holdout 通过后才开启的路径。

要做的是把上游那套「问完就停、停下来时坦白交代」的产品契约接回来,不是重写收敛逻辑,更不是换打分引擎。


硬红线

  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_v3source_audit_statusinvalidated_after_replay,只能当开发集看趋势,不得作为发布指标,不得写回 sealed 字段v4_intakeproduction_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 并停下。
  10. 不得改 .gitea/workflows/**。不得在有未提交改动的工作树上切分支。不得自行把 staging 提升到 main。

让步顺序:诚实性不回退 > 数据不损坏 > 功能与测试不回归 > 可验证的收敛改进 > 代码整洁 > 成本

开工前置

git fetch origin --prune
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 检索校正相关记录(30 条以上,停滞类那几条务必读完)。

先读这些再动手:

  • 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 —— 四阶段状态机,参考它有多简单

任务 0(已完成 2026-08-31)· 上游访谈手册与阈值文档

三份文件已获取,归档于 references/upstream/(来源:上游维护者本地 skill 包 ~/.workbuddy/skills/jyotish-birth-time-rectification/,获取日期 2026-08-31):

  • references/upstream/interview_playbook.md
  • references/upstream/evidence_thresholds.md
  • references/upstream/test_birth_time_rectification_skill_contract.py

结论:上游的「收敛」= 从不试图确认分钟

1. 停止规则与本仓方向相反。

interview_playbook.md 的 Stop Rules 原文:

Stop and report the current interval when any is true:
- fewer than three dated events
- fewer than two domains
- tied first-place candidates
- user uncertainty is too high to map support/conflict reliably

即:证据不足 → 停下来,交付当前区间。

本仓 frontend/src/lib/birth-time-evidence.ts:188-189minConfirmationEvents(4) / minConfirmationDomains(3) 当作确认门,达不到 → 继续问

同一类阈值,上游当「可以开始交付的地板」,本仓当「可以下结论的天花板」。前者三轮内必达,后者按 sealed holdout 的 confirmation_coverage_rate: 0.0 是零达成。这是无限访谈的最终解释。

2. 上游的标签梯子没有「已确认」这一档。

evidence_thresholds.mdblocked → user_history_verification_required → manual_pattern_consensus → single_adapter_support → multi_adapter_consensus。顶端自注 "this is still not birth-time truth"standalone_core 模式天花板更低,只到 manual_pattern_consensus

test_birth_time_rectification_skill_contract.py每一个 case(含最高档 main_repository_enhanced)都断言:

assert receipt["claim_status"] == "candidate_range_not_birth_time_truth"

3. 提问节奏是硬的三轮。

第 1 轮 35 道 A/B/C/D 选择题;第 2 轮只要最强 domain 的带日期事件;第 3 轮只问能区分 top 候选的 1–3 个事件。手册明写 "Never collect a long autobiography before the first route decision."

4. 终止话术是写死的。

当前最优结果是候选时间段,而不是已经确认的唯一出生分钟。临时代表时间仅用于下一轮验证与比较。

对已完成任务的影响

任务 1 实现的 budgetExhausted()(轮次 8 / 答题 10 / 平台期 2)方向正确但只覆盖了一半——上游是证据状态型停止且三轮即止。差额由任务 6 补齐。


任务 1(P0)· 把轮次预算接进生产判定

事实

见事实 4、5。rectification-decision.ts:144-149!separation.sufficient 分支只要有 probe 且用户没喊停就无限 discriminate。轮次判定在别处算好了但没人读。

要做什么

  1. rectification-decision.ts 的判定输入增加预算三件套,参数一律取自已有常量,不要新造数字
    • 轮次上限 —— core/types.ts:10DEFAULT_MAX_DISCRIMINATION_ROUNDS = 8
    • 平台期 —— references/rectification_policy.v1.jsonmaxPlateauRounds(当前只有 scripts/rectification_policy.py 读了,前端没用)
    • 安全帽 —— birth-time-dynamic-stop-policy.ts:45effectiveAnswerCount >= 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.tsrectification-range-offer-deadend.test.ts 等)全绿。
  • 端到端:一个用户即使一直不给有效证据,也会在预算内拿到区间并结束。

任务 2(P0)· 提问权收归服务端,模型降级为一句确认

事实

一条消息目前有四个作者:服务端决定问什么(v9/method-followup.ts,2,036 行)→ 模型写正文 → 服务端把题干接在正文之后 → UI 再渲染选择卡。

于是 src/mastra/agentic-rectification.ts:66 要求模型判断自己处在 choice / collect_spoken / 无持久化问题 三种状态中的哪一种再区别行动。模型做不到,所以有了 v9/spoken-answer.ts33 条内部标识符正则 + 68 条中文过程话术正则,共 101 条,事后擦模型漏出的内心戏。每一条正则都是一个曾经发生过的 bug。

要做什么

  1. 模型永远不提问。 一轮里它的唯一职责是针对用户刚说的内容写一句确认/承接。系统提示第 4 条整条重写,删掉所有关于题干归属的条件分支。
  2. 问题永远由服务端渲染,永远出现在同一个问题区。 choicecollect_spoken 合并成一个「问题槽」:有选项就可点,没选项就是输入框加提示。数据模型不再区分 kind,UI 不再有两条渲染路径。
  3. 取消「服务端把题干接在模型正文之后」的拼接。 一条消息 = 模型的一句确认(流式)+ 服务端问题区(结构化渲染),两者物理分离,各自唯一作者。
  4. 删掉 spoken-answer.ts 的 101 条清洗正则。 如果删完发现仍需清洗,说明第 1 步没做干净,回去改第 1 步,不要把正则加回来
  5. 服务端未产出问题时问题区为空 —— 这是可断言、可监控的显式状态,不是静默停滞。加服务端告警。

验收

  • 用户可见文本不可能出现内部标识符或过程话术,且不是靠正则保证,是靠模型输出面收窄保证。
  • 端到端连续 10 轮,问题始终在问题区,位置一致,不重复、不消失。
  • spoken-answer.ts 的两条大正则不再存在于代码库。

任务 3(P0)· 恢复「停下来时怎么交代」的契约,交付物从第 0 轮就存在

事实

上游 active_rectification_events.py:115-179build_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. 把上游的 build_candidate_result_summary() 恢复到 Jyotisha/scripts/active_rectification_events.py,让 scripts/rectification/api_service.pyscore_candidates() 返回体带上 next_step_codesstability.label
  2. 定义校正报告投影,随时可读:结论区间 + 代表分钟(明确标注为代表性、非唯一解)、置信度、逐条证据(事件 → 支持哪个候选 → 用哪个方法)、被排除的候选与理由、局限声明(复用 confirmation-gate 的 blocker 文案,不要另写一套)。
  3. 第 0 轮就存在:证据 0 条时区间 = 用户声明的出生窗口。每答一题重算。
  4. 用户任何时刻可查看、可导出、可主动结束。结束即交付当前版本,不算失败
  5. UI 给出区间收窄进度(±120 → ±45 → ±18 → ±7)。这是用户为这笔钱买到的东西的可视化。
  6. 计费与交付对齐:产生过至少一版报告即为有效交付,exhausted 不是失败态。具体金额由 TASK-billing-pricing-20260830.md 决定,本任务不写任何金额。

验收

  • 新 case 在零证据时即可读出一份报告。
  • 每轮答题后区间单调收窄或不变。
  • 用户主动结束或超预算 → 拿到报告,状态是完成不是失败。
  • next_step_codes 出现在服务端返回体里,并被终止话术使用。

任务 4P1)· 解决标定数据饥荒

事实

两边都没有盲测数据(事实 6)。Jyotisha 的 v4_intake blind_holdout_eligible_case_count = 0,上游 ready_case_count = 0。这是产品能不能承诺唯一分钟的唯一瓶颈,且它是数据问题不是工程问题

上游停在这里六周了,指望它自己解决不现实。

要做什么

做一个免费的「验证我们」入口,面向已经知道自己准确出生时间(有出生证明/医院记录)的用户:

  1. 用户提供出生日期、地点、事件;准确时间单独收集并对打分链路屏蔽,全程不得进入候选生成或打分。
  2. 系统盲跑校正,出结果后再与真实时间比对,把偏差当场展示给用户。
  3. 每一例都是一条合格标定数据,用户同时拿到一次免费的、可验证的能力演示。

一个入口同时解决三件事:标定数据来源、最有说服力的社会证明(「我们在 N 个有出生证明的案例上盲测过,中位偏差 X 分钟」)、获客内容。

硬约束

  • 真实时间必须在架构上不可能泄漏进打分链路,不是靠约定。写负向测试锁死。
  • 案例默认进 intake 队列,晋级为冻结 holdout 必须走既有流程:新版本号、通过源审计、打分身份在盲测前冻结。
  • 免费入口要有独立的滥用限制,不得复用咨询的公平使用配额。
  • 与上游共享数据前先对齐口径,避免两边各建一套互不承认的标定集。

验收

  • 真实时间泄漏的负向测试存在且通过。
  • 新案例正确落入 intake 队列,不会被自动当成 holdout。
  • 用户侧能看到「预测 vs 真实」的偏差对比。

任务 5P2)· 清理与同步机制

任务 1–3 上线并稳定运行两周后才做。

  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 机制、rectification_policy.v1.json 的阈值。这些都是对的。


任务 6P0)· 接过上游的终止语义

事实

见任务 0 结论。任务 1 已接入轮次型预算,但上游的终止是证据状态型的,且 exhausted 在上游是正常终态而非兜底态。

要做什么

  1. 增加证据型停止规则,与既有轮次预算并列(任一命中即停并交付):

    • 带日期事件 < 3
    • 覆盖 domain < 2
    • 并列第一(已有 separation 可判)
    • 用户不确定度过高,无法可靠映射 support/conflict

    阈值取 references/upstream/evidence_thresholds.md 的 Minimum Standalone Gate3 事件 / 2 域),不要沿用本仓的 4/3——那组是确认门,语义不同,两者必须分开保留,不得互相覆盖。

  2. exhausted 从兜底态重新定位为正常终态。 改命名与全部用户可见文案:走到这里是「交付完成」,不是「很遗憾没能完成」。计费上它必须是有效交付(与 TASK-billing-pricing-20260830.md 对齐)。

  3. 引入上游的 label ladder,替换「确认唯一分钟」作为默认目标: blocked → user_history_verification_required → manual_pattern_consensus → single_adapter_support → multi_adapter_consensus 唯一分钟确认降级为 sealed holdout 通过后才开启的路径,不得作为访谈的默认终点。既有 confirmation-gate.ts 的 blocker 保持不变(红线 2),它现在正确地永不放行。

  4. 采用上游的终止话术,逐字使用任务 0 结论第 4 条那句。它是上游打磨出的合规表述,不要另写。

  5. 收紧提问节奏到三轮形态:第 1 轮 A/B/C/D 选择题批量出,第 2 轮只要最强 domain 的带日期事件,第 3 轮只问能区分 top 候选的 1–3 个。当前 8 轮预算作为外层熔断保留,不作为正常节奏。

验收

  • 证据型停止规则有单测:3 事件以下、2 域以下、并列第一各自触发终止并交付区间。
  • 4/3 确认门与 3/2 交付地板在代码中是两个独立常量,不互相覆盖,注释写明语义差别。
  • 用户可见文案中不存在把正常终态描述为失败的表述。
  • 默认路径下系统不再以「确认唯一分钟」为目标;相关文案与 label 均出自新梯子。
  • 既有校正测试与任务 1 的四条属性测试全绿。

交付

  • 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 的失败清单与基线逐条比对,确认无新增。