merge: sync origin/main (22bbf8ab) into docs/correct-report-smoke
Staging Backend Quality Gate / validate (push) Successful in 11m30s
Staging Backend Quality Gate / publish (push) Successful in 6m49s

Resolve conflicts:
- globals.css: keep origin/main disabled opacity 1 (BUG-140 parity)
- consultation-entrypoint.test.ts: keep origin/main sticky-hover + disabled rules
- BUG_HISTORY.md: keep origin/main BUG-139 (staging admin), renumber local
  Python prework fix to BUG-158
This commit is contained in:
Jesse
2026-08-10 17:59:37 +08:00
287 changed files with 13270 additions and 44903 deletions
@@ -1,383 +0,0 @@
# 生时校正 V4 重构设计 — 2026-07-26
## 1. 结论
生时校正不应继续作为“聊天接口里顺便跑一次模型和分钟扫描”的功能维护。V4 将它重构为独立、可恢复、可审计的证据工作流:
1. 先收集跨领域、带日期精度的人生事件;
2. 再对已经存在但日期较粗的事件做定向修订;
3. 后台 Worker 对冻结的计算口径和证据集合评分;
4. 只输出通过稳定性门槛的候选时间范围;
5. 用户主动保存后,才把该范围交给原咨询问题继续使用。
产品永久边界:**V4 不确认单分钟,不把峰值分钟显示为真实出生时间,不修改 `profiles.active_birth_time`。**
本文记录已经落地到本地可测试工作树的目标架构,而不是对旧实现的小修补。
## 2. 交互记录暴露的旧架构问题
用户提供的 Agent 交互记录说明,专业校时需要的不只是一个聊天框,而是一套持续数十轮仍保持一致的证据系统。旧 Web 流程无法稳定复现该过程,主要原因不是文案不够像 Agent,而是职责没有拆开。
### 2.1 预设年份问卷替代了真实证据
旧流程先给出宽年份选项,再不断要求把同一事件从年份段缩到年份、季度、月份。这样会产生三个问题:
- 问题本身暗示事件应该发生在哪个窗口,带来确认偏差;
- 用户已经给过事件后,系统仍可能把它当成下一条新事件;
- 为了进入评分而取区间中点,会制造不存在的日期精度。
V4 改为先接收用户自己声明的事件和日期,再把后续日期补充写成同一事件的新 revision。
### 2.2 对话、抽取、评分和结果表达在同一请求中耦合
旧流程把事件抽取、技术计算、叙事生成和下一题规划放在一个请求生命周期里。任一模型超时、跨语言 schema 漂移或计算耗时,都可能让已提交的经历看起来没有保存。
V4 的同步请求只负责持久化答案并创建 Job;Worker 异步完成抽取、评分、稳定性检查和下一题规划。用户提交成功后可以离开页面,稍后继续。
### 2.3 “第一名分钟”被误当成产品答案
分钟扫描必然会产生一个最高分,但最高分不等于真实出生分钟。旧交互即使同时声明低置信度,仍会把一个具体分钟写成“建议暂用时间”,视觉上压过候选范围和不确定性说明。
V4 将 `representativeTime` 限定为内部聚类数据。公开快照中的 `canConfirmExactMinute` 是字面量 `false`;UI 只显示 `startTime–endTime`,不显示峰值分钟。
### 2.4 没有稳定的事件身份和修订链
用户可能先说“某年发生”,后续补成“某年某月”,也可能纠正原先年份。覆盖旧记录会丢失审计信息;新建一条记录又会重复计分。
V4 使用 `eventId + revision`:
- `eventId` 表示同一人生事件;
- 每次补充创建 append-only revision;
- `supersedesRevisionId` 指向上一版本;
- 评分只读取每个 `eventId` 的最新 revision。
### 2.5 会话状态不足以承担业务状态
浏览器会话可能被删除、刷新或从另一设备继续。旧流程把关键进度附着在聊天消息上,难以保证幂等、恢复、并发提交、扣费和原问题 handoff。
V4 将 Case、Turn、Event Revision、Snapshot、Job 和 Handoff 独立持久化;聊天会话只是入口和展示容器,不再是生时校正业务真源。
## 3. 产品合同
### 3.1 输入
- 已保存出生日期;
- 用户声明的候选时间和不确定范围;
- 出生地点坐标与时区;
- 固定计算口径:Lahiri、Mean Node、一分钟步长;
- 用户主动提供的人生事件。
### 3.2 证据领域
| 领域 | 典型事件 | 评分状态 |
| --- | --- | --- |
| `education` | 升学、复读、毕业、转学 | scoreable |
| `relocation` | 搬家、长期迁居、离乡 | scoreable |
| `relationship` | 重要关系开始或结束 | scoreable |
| `career` | 入职、离职、转行、职责突变 | scoreable |
| `finance` | 收入、负债、资产明显变化 | scoreable |
| `health_pressure` | 疾病、手术、事故、长期压力起点 | scoreable |
| `family` | 家庭结构或亲属重大事件 | context-only |
| `other` | 其他明确、重要、可核对事件 | 按领域能力决定 |
家庭事件先保留为上下文,不应为了“凑够领域”接入没有可靠 scorer 的技术层。
### 3.3 输出
- 一个主要候选范围;
- 可选的次级候选范围;
- 支持该范围的事件;
- 冲突或区分力不足的事件;
- 稳定性门结果;
- 用户是否已主动保存该范围。
### 3.4 永久禁止
- 不确认单分钟;
- 不把 `representativeTime` 作为公开结论;
- 不自动保存候选范围;
- 不修改 `profiles.active_birth_time`;
- 不因聊天文案或模型失败丢弃已经持久化的答案;
- 不用区间中点伪造事件日期;
- 不把同一事件的日期补充重复计分。
## 4. 两阶段问题规划
### 4.1 阶段一:领域覆盖
按低回忆成本优先收集七个领域。每次只问一个开放问题,要求用户自己提供事件与尽可能准确的年月,不先展示系统猜测的年份窗口。
当前确定性顺序为:
```text
education → relocation → relationship → career → finance → health_pressure → family
```
规划器已经保留 `candidateSplitByDomain` 输入;未来只有当评分引擎能给出可解释的领域区分力时,才允许在不增加模型自由度的前提下动态排序。
### 4.2 阶段二:日期精度修订
完成领域覆盖后,对最新 revision 仍不是 `day` 精度的 scoreable 事件定向追问:
- 问题保存 `targetEventId`;
- Turn 保存 `questionTargetEventId`;
- Worker 用该 ID 将回答追加到原事件;
- 如果回答没有可解析日期,不创建伪 revision;
- 已经追问过或用户跳过的事件写入尝试集合,不循环追问。
如果所有可修订事件都已处理但仍未通过稳定性门,则询问新的、日期明确的重要事件;用户可以暂停或结束。
## 5. 状态机
### 5.1 Case 状态
```mermaid
stateDiagram-v2
[*] --> awaiting_answer: create case
awaiting_answer --> processing: submit answer
processing --> awaiting_answer: worker plans next question
processing --> range_ready: range passes gate
awaiting_answer --> paused: pause
paused --> awaiting_answer: resume
awaiting_answer --> abandoned: abandon
range_ready --> abandoned: abandon without save
range_ready --> range_ready: explicitly save accepted range
```
| 状态 | 含义 | `currentQuestion` |
| --- | --- | --- |
| `awaiting_answer` | 等待用户回答 | 必须非空 |
| `processing` | 答案已保存,后台处理中 | 空 |
| `range_ready` | 有候选范围;可能尚未保存 | 可空;稳定范围就绪时为空 |
| `paused` | 用户主动暂停 | 保留可恢复进度 |
| `abandoned` | 用户结束本次校正 | 空 |
关键不变量:证据不足时必须生成下一题,不能出现 `status=awaiting_answer` 且 `currentQuestion=null`。
### 5.2 Worker 阶段
```text
extracting_evidence
→ scoring_candidates
→ checking_robustness
→ planning_question
→ collecting_evidence | complete
```
Case 状态描述用户能做什么,Phase 描述系统正在做什么。两者不能混用。
### 5.3 Job 状态
```text
pending → processing → completed
└→ failed
pending/expired processing → processing by another worker
obsolete input → stale
```
Job 使用十分钟 lease。只有持有有效 lease 且输入 case version、证据 hash、计算口径 hash 仍匹配的 Worker 可以完成任务。
### 5.4 Handoff 状态
```text
pending → claimed → executing → consumed
```
过期的 `claimed/executing` lease 可恢复为 `pending`。Handoff 使用 `requestId`、`claimActionId` 和 settlement receipt 保证多设备重试不会重复执行或重复扣费。
## 6. 数据模型
### 6.1 Case
Case 冻结一次校正的业务口径:
- `calculationSpec`;
- `calculationSpecHash`;
- `evidenceSetHash`;
- `version`;
- `currentQuestion`;
- `latestSnapshot`;
- `acceptedRange`。
`acceptedRange` 在用户点击“保存这个范围”前始终为 `null`。
### 6.2 Turn
Turn 是用户回答的不可变输入记录:
- 本轮问题 ID、领域和目标事件 ID;
- 可见问题文本;
- 用户原始回答;
- `actionId`;
- 提交时的 case version。
失败重试时可以恢复同一个问题,不需要从聊天文本反向猜测当时问了什么。
### 6.3 Event 与 Event Revision
Event 提供稳定身份;Revision 保存领域、事件类型、摘要、原文、日期范围、精度和评分资格。
评分前执行 `latestEventRevisions()`,确保同一事件只使用最新版本。原始回答保留,技术评分不依赖经过润色的叙事文案。
### 6.4 Candidate Snapshot
每次评分都生成不可变 Snapshot:
- 输入证据 hash;
- 计算口径 hash;
- 算法版本;
- 全部分钟候选分数;
- 候选聚类;
- 稳定性结果;
- 决策门原因。
Snapshot 使刷新、复算和算法升级可审计,不用覆盖上一轮结果。
## 7. 候选聚类与稳定性门
### 7.1 聚类
当前算法取峰值分数的相对 `0.97` 以上候选,将相邻分钟合并为 cluster,并按峰值和 score mass 排序。
`representativeTime` 仅用于内部描述 cluster 峰值。公开结果使用 `startTime` 和 `endTime`。
### 7.2 可保存范围门槛
主要范围必须同时满足:
- 至少 5 个 scoreable 事件;
- 至少 3 个 scoreable 领域;
- cluster 宽度至少 2 分钟,拒绝单分钟结果;
- cluster 宽度不超过 15 分钟;
- 邻近分钟支持至少 2 分钟;
- leave-one-out 保留率至少 0.8;
- 日期敏感性保留率至少 0.8;
- 计算口径 hash 与建案时一致。
无论是否通过,`canConfirmExactMinute` 永远为 `false`。
## 8. 请求与后台执行边界
### 8.1 同步 API 负责
- 认证和输入校验;
- 幂等 action;
- optimistic case version 检查;
- 保存 Turn;
- 创建 pending Job;
- 返回 `202` 和可轮询 Job。
### 8.2 Worker 负责
- 抽取新事件或定向修订;
- 计算 evidence hash;
- 调用 Python 候选引擎;
- 聚类与稳定性门;
- 生成 Snapshot;
- 规划下一题;
- 原子完成 Job 和 Case 状态迁移。
非关键叙事模型不在 V4 完成链路上。确定性问题和业务状态在模型不可用时仍可继续。
## 9. 数据库不变量
迁移 `20260726020000_birth_time_rectification_v4.sql` 将关键规则下沉到 PostgreSQL:
1. 同一用户最多一个未结束、未接受范围的活动 Case;
2. `actionId` 幂等,同一 action 不能绑定不同问题;
3. `expectedCaseVersion` 不匹配时拒绝陈旧写入;
4. 未通过快照门或范围与最新主要 cluster 不一致时不能保存;
5. 有效 Worker lease 不能被抢占,过期 lease 可以接管;
6. Worker 只能完成仍匹配输入 hash 和 version 的 Job;
7. Handoff claim、begin、refund 和 settlement 可重试;
8. 一次 Handoff 最多产生一次成功扣费;
9. 取消的请求可以重新生成 request key;
10. 整个 V4 migration 不更新 `profiles.active_birth_time`。
## 10. UI 设计
### 10.1 首屏
必须同时告诉用户:
- 正在比较的声明候选边界;
- 当前流程先核对经历;
- 结果只会是候选范围,不是已确认分钟;
- 原咨询问题已保留。
### 10.2 处理中
提交后立即显示“回答已经保存,计算在后台继续”。允许用户离开页面,不用让浏览器请求一直等待。
### 10.3 日期修订
日期修订问题必须有可见 label 和输入框,不得只显示空卡片。问题明确允许“不记得/跳过”,避免为了通过流程而编造日期。
### 10.4 结果
只显示:
- 主要/次级候选范围;
- 支持经历;
- 冲突或区分力不足;
- 不确定性说明;
- “保存这个范围”操作。
不显示内部事件 ID、hash、技术 packet、模型错误、评分明细或峰值分钟。
### 10.5 保存与继续咨询
点击“保存这个范围”只写 `acceptedRange`。之后用户可以把该范围带回原问题;咨询服务将其作为“未验证候选范围”使用,而不是已确认出生时间。
## 11. 错误与恢复策略
- 输入不合法:同步返回稳定中文错误,不创建 Job;
- 陈旧版本:返回冲突,客户端刷新 Case;
- Worker 失败:Job 标记 failed,恢复原问题;
- Worker 崩溃:lease 到期后其他 Worker 接管;
- 证据不足:生成下一题,不伪装成技术异常;
- 用户暂停:保留 Case、事件和问题;
- 用户结束:Case 进入 abandoned,出生资料不改写;
- 页面刷新:从活动 Case、Job 和事件台账恢复;
- 多设备 handoff:由数据库 lease 和 settlement receipt 仲裁。
## 12. 已落地代码边界
```text
frontend/src/lib/rectification-v4/ 领域模型、规划、评分适配、Store、Worker
frontend/src/app/api/rectification/v4/ HTTP API
frontend/src/components/rectification-v4-panel.tsx
frontend/src/hooks/use-rectification-v4.ts
frontend/scripts/rectification-v4-worker.mts
scripts/active_rectification_events_v4.py
frontend/supabase/migrations/20260726020000_birth_time_rectification_v4.sql
frontend/tests/rectification-v4-*.test.ts
tests/test_active_rectification_events_v4.py
```
首页旧聊天入口继续保留外壳和历史兼容,但新的活动流程由 `RectificationV4Panel` 和 V4 API 驱动。
## 13. 本地验收结果
截至 2026-07-26:
- 前端 V4、handoff、replay、consultation continuation:33 个测试通过;
- Python 引擎:2 个测试通过;
- 目标 ESLint:通过;
- `npm run build -- --webpack`:通过,27 个页面生成成功;
- PostgreSQL:12 项迁移和并发/扣费不变量通过;
- 真实 PostgreSQL + Python Engine E2E:最终进入 `range_ready`,主要范围 `05:26–05:30`;
- E2E 后 `active_birth_time` 保持原值,`acceptedRange` 保持空值;
- 静态浏览器预览:日期修订输入框、范围保存按钮、不确认分钟文案和 390px 无横向溢出均通过。
本轮没有提交、推送或部署。由于当前本地认证浏览器与隔离 V4 数据库/服务环境没有安全地连在一起,尚未声称“真实登录态端到端 UI”已验收;发布前仍需在正确的本地或 staging 认证环境执行一次完整用户操作 smoke。
## 14. 发布前必须补齐
1. 应用迁移并核对 migration ledger;
2. 启动独立 Worker,确认部署环境包含数据库和 Python API 配置;
3. 用测试账户完成:建案 → 七领域 → 日期修订 → 范围就绪 → 主动保存 → 原问题 handoff;
4. 证明刷新和另一设备可恢复;
5. 证明重复提交、过期版本和 Worker 接管不重复事件、不重复扣费;
6. 再次核对 `profiles.active_birth_time` 未被 V4 路径更新;
7. 将验收绑定到精确部署 Git SHA,而不是只看 HTTP 200。
@@ -1,131 +0,0 @@
# 对话式生时纠正修改方案与真实回放门槛
日期:2026-07-22
## 结论先行
当前系统可以改成普通 session 式的一问一答,但不能把“交互更顺”描述成“已经能准确到分钟”。现有公开开发案例的分钟评分仍不稳定;产品入口还会丢弃健康、事故、家庭类证据,并且实际只把最近 6 条事件送入评分。
本方案把修改拆成两个互不混淆的目标:
1. 让用户通过自然语言持续补充经历,系统每轮只问一个最有区分力的问题。
2. 用开发集和独立冻结盲测证明分钟排序是否真的收敛;没有通过门槛时只返回候选范围和证据缺口。
## 已确认的问题
### 1. 首轮正文被技术回执合同绑架
`narrative-agent.ts` 当前要求首轮正文复述所有稳定层、敏感层、具体值和领域映射。模型未满足时,fallback 又会输出同一份 D 层清单。技术包本身可以保留,但不应该成为用户正文。
### 2. 自然语言证据域不完整
`evidence-extractor.ts` 只显式识别事业、学业、迁移、关系、家庭、财富。疾病、手术、事故和丧亲会落到 `other`;`route.ts` 又会排除 `family` 和 `other`,所以 D30、家庭与部分重大人生事件无法参与评分。
日期正则还被限制为 1900–2099 年。公开历史人物案例中的 18xx 年事件会被保存成“日期未知”,无法评分;这也意味着现有历史 AA 开发案例并没有真正通过当前产品的自然语言入口。
### 3. 事件数量合同互相矛盾
- 会话收敛层:3 条开始排序,8 条停止继续取证。
- 路由评分层:只保留最近 6 条。
- Jyotish Skill:至少 5 条 dated events 才适合定框;高置信度通常需要 10–15 条多领域事件。
3 条可以产生早期候选反馈,但不得升级成准确分钟。停止条件也不能仅由事件数量触发。
### 4. 当前问题选择仍偏“让用户交材料”
系统应从候选差异中选择一个信息增益最高、用户容易回忆的问题,例如“第一次正式工作是哪年哪月”,而不是一次要求用户提交多个领域的事件。
## 最小实施方案
### 阶段 A:修正文合同,不改评分器
建议改动:
- `frontend/src/lib/conversational-rectification/narrative-agent.ts`
- 首轮正文只要求:当前范围、尚未确认边界、证据进度、一条具体问题。
- 取消首轮必须公开全部 stable/sensitive layer values 的校验。
- fallback 同样只输出一条问题。
- 技术层继续完整保存在 technical packet / receipt 中,在 UI 中放入可折叠“计算依据”。
- 用户消息区、输入框、滚动和留白完全复用普通 session 布局,不保留卡片式问卷。
验收:首轮正文不出现 D1/D9/D10 技术 dump;每轮只有一个问号和一个待回答主题。
### 阶段 B:补齐事件入口
建议改动:
- 在持久化和技术包域中新增 `health_pressure`,不要把它降级成 `other`。
- 抽取器识别:确诊、住院、手术、事故、受伤、创伤、重大疾病、康复。
- 日期解析支持出生之后的合法公历年份,而不是写死 19xx/20xx;继续用 `asOfDate` 排除未来事件。
- 家庭事件不要整体丢弃;按父母、子女、生育、丧亲等映射到后端支持的主题证据,若评分器尚无对应合同则明确存储为 context-only,而不是静默消失。
- 移除 `.slice(-6)` 的隐式截断。建立一个共享常量;建议开发回放先测 5、8、10、12 条,再决定产品上限。
- UI 显示“已记录 / 可评分 / 需补日期 / 仅作背景”的数量,避免用户以为回答已评分。
验收:真实测试集中所有标记为可评分的自然语言回答都能保留正确日期、精度和领域;任何被排除的事件都有可见原因。
### 阶段 C:一次问一个高信息量问题
复用现有 `candidateDifferences`、`suggestedDomains` 和 fact difference opportunities:
1. 过滤已经问过、用户表示不记得、或缺少后端支持的主题。
2. 优先选择能把当前候选分成最均衡分区的事件领域。
3. 将领域转成生活化问题,并要求“年份/月份 + 发生了什么”。
4. 用户回答后重新排序,再选择下一题。
问题模板必须具体,例如:
> 你第一次明显的工作转折发生在什么时候?可以是入职、离职、换行业、升职或创业。直接告诉我大约哪一年哪一月和发生了什么;记不清月份也可以先说年份。
### 阶段 D:安全收敛
每轮都可以展示“当前领先范围”,但只有同时满足以下条件才允许提示确认:
- 至少 5 条可评分事件,且至少 3 个领域;高置信度目标使用 10–15 条。
- 真实候选形成唯一分钟或预先规定的窄范围。
- ±1/2/5 分钟邻近稳定性通过。
- leave-one-event-out 通过,不能由单一事件决定结果。
- 没有缺失的强制计算层。
- 独立冻结盲测达到发布指标。
未满足时输出:候选范围、支持事件、冲突事件、未评分事件和下一条问题,不允许写“准确时间为”。
## 测试集分层
### 对话开发回放集
文件:`references/real_case_calibration/conversational_rectification_development_v1.json`
当前 v1 是入口 smoke fixture,不是分钟收敛 benchmark:TypeScript 测试验证 fixture 中自然语言在现有抽取器下的日期、精度和领域;Python 回放使用同一 fixture 的人工 `expected_route_scoreable` 标签模拟现有路由过滤,再测试逐轮排名。它不是自然语言到评分的端到端回放,且每例不足 5 条可评分事件,不能用于判断 Skill 级收敛。
下一版 development benchmark 必须把每例扩充至至少 5 条、目标 10–15 条多领域事件,并直接调用抽取器和路由投影,覆盖记不清、拒答、否定、纠正、重复和一条消息多事件。它引用仓库已有公开 AA 开发案例,永久排除在 holdout 之外,可以用于修 bug 和调问题排序。
### 独立冻结 holdout v4
开发集不能证明发布准确率。正式验证需要另一位 reviewer 收集至少 20 个未参与调参的公开 AA 案例,在评分前完成:出生来源复核、事件来源复核、假分钟承诺、manifest hash、实现 hash 和真值密封。只允许盲回放一次;失败后整批降为 development,下一版重新收集 fresh holdout。
## 指标
对话入口:
- 日期和精度提取准确率。
- 领域提取准确率。
- 应评分事件保留率。
- 每轮问题数必须等于 1。
- 不能回答、记不清、纠正旧答案时仍能继续。
分钟能力:
- 3、5、8、10 条事件时的 Top-1 / Top-3。
- 逐轮真实分钟排名和误差轨迹。
- 唯一分钟率、邻近稳定性、leave-one-out。并列范围未破时 `predicted_time` 必须为空;如保留确定性 tie-break,只能单列为诊断 MAE,不能当成分钟能力。
- false confirmation rate 和 confirmation coverage 必须同时报告。
建议继续沿用当前发布门槛:Top-1 ≥ 60%,Top-3 ≥ 85%,MAE ≤ 2 分钟,false confirmation ≤ 5%,insufficient-evidence rejection ≥ 90%。confirmation coverage 不能为 0,否则只是“永不确认”而非可用收敛。
## 实施顺序
1. 先合并阶段 A 与入口可观测性,不改变“准确分钟”声明。
2. 用本开发回放集修阶段 B,确保事件没有在入口丢失。
3. 接入阶段 C 的单问题排序并做多轮 replay。
4. 当开发集达到稳定目标后,冻结实现并收集 fresh holdout v4。
5. 只有 holdout 通过后才开放分钟确认;否则产品始终停在候选范围。
+12
View File
@@ -251,3 +251,15 @@ Prevention: keep evidence-request, life-event, private-candidate, public-recap,
After accumulated historical evidence produced a very narrow winning segment, that segment could contain fewer than two linked samples or discriminating divisional themes. Packet construction treated this valid “not enough distinction yet” state as a dependency failure, so a later answer returned 503 even though scoring and the astrology service were healthy.
Prevention: classify insufficient candidate-range discrimination explicitly; when a newly narrowed segment cannot support the technical evidence contract, retain the prior candidate range, preserve scored evidence, clear the unconfirmed result, and continue conversational collection.
## ERR-102 | Gitea returned HTTP 502 during final remote synchronization check | active 2026-08-09
Two final `git fetch origin --prune` attempts against the configured primary Gitea remote failed before ref exchange with HTTP `502`. The last locally verified refs remain available, but this run cannot prove that they are still current and must not claim a completed remote synchronization or push.
Prevention: retry fetch and `git ls-remote` before any push or release action, compare the full `main`, `staging`, and migration-branch SHAs, and stop if Gitea remains unavailable. Do not substitute cached refs, the GitHub mirror, or a successful local commit for current Gitea synchronization evidence.
## ERR-103 | Release Gate runner 缺少 Docker Compose v2 导致 17 个集成测试级联失败 | mitigated 2026-08-09
手工 Release Gate Run `1638` 在 `xiaoxin` 上通过 Docker Engine 检查后进入完整测试,但该 runner 不支持 `docker compose`,`--project-name` 与 `--env-file` 被 Docker 顶层 CLI 判为 unknown flag,导致 17 个 PostgreSQL、备份、身份和权限集成测试级联失败。候选 staging SHA、旧生产、Supabase 与 DNS 未被改变。
Prevention: 将该门禁运行在已验证 Docker Compose v2 的 `manman-linux`,并在安装依赖前以 `docker compose version --short` 强制 v2;不得把 `docker version` 当作 Compose 能力证明。新 SHA 必须重新完成 staging gate、公网 staging 身份和手工 Release Gate,失败 run 不得授权生产 migration、deploy 或维护停写。
@@ -1,57 +0,0 @@
# User-reported birth-time flow issues — 2026-07-20
This ledger records the five reported product failures and the evidence needed
to close them. It contains no copied authentication header, cookie, token,
email, user UUID, or real birth record. The original plan described this file as
an existing modification target; it did not exist in this checkout, so Task 12
created it.
`verified-local` means deterministic contract tests and the equivalent local
PostgreSQL 14 workflow pass. It is deliberately not `closed`: closure also
requires an authenticated synthetic production smoke whose health Git SHA is
the tested deployment SHA.
| Issue | Reported failure | Current status | Local evidence | Production closure artifact |
| --- | --- | --- | --- | --- |
| ISSUE-BT-001 | A chat appeared impossible to delete, or a late response could recreate it. | verified-local | `20260720000000_chat_delete_and_dynamic_candidate_confirmation.sql`; `frontend/tests/chat-session-delete-contract.test.ts`; the PG14 full-flow test deletes a real RLS-owned chat as `authenticated` and proves the account case remains. | Authenticated synthetic delete plus account-case reload, tied to `/api/health` deployment SHA. |
| ISSUE-BT-002 | A new chat could not establish a fresh rectification interaction and unfinished progress was coupled to chat state. | verified-local | `20260720010000_conversational_rectification_schema.sql`; account-level resume in `frontend/tests/conversational-rectification-e2e.test.ts` across two route clients; Task 9 current-chat consent tests. | Authenticated new-device/new-chat resume smoke tied to the deployed SHA. |
| ISSUE-BT-003 | Confirming a candidate such as `17:15` surfaced `The string did not match the expected pattern`. | verified-local | Atomic v3 confirmation in `20260720030000_conversational_rectification_transitions.sql`; client retry/fallback and mismatched-then-exact confirmation in `frontend/tests/conversational-rectification-e2e.test.ts`; the PG14 full-flow test rejects `05:20`, confirms exact `05:21`, and proves the old time survives until commit. | Authenticated production exact-candidate confirmation with old-time preservation, plus transient deployment-error probe. |
| ISSUE-BT-004 | Choosing `都不符合` surfaced the same raw English pattern error. | verified-local | The actual orchestrator treats `都不符合` as a normal direction change; Task 12 E2E advances the durable turn and preserves the single fee; client maps terminal 502/non-JSON failures to stable Chinese copy. | Authenticated production `都不符合` action followed by reload/resume, tied to the deployed SHA. |
| ISSUE-BT-005 | Initialization used generic broad-year choices and lost the rich card/chat rectification analysis. | verified-local | Task 9 onboarding soft gate; v3 narrative grounding rejects broad-year questionnaires; Task 12 asserts candidate boundary, D1/D9/D10 layers, three domain rationales, free text, and year/month event request; PG14 proves future background persists without scoring and the legacy suite imports old unfinished work once with `migration_waived`. | Authenticated synthetic first-turn snapshot and one legacy import smoke tied to the deployed SHA. |
## Initial production diagnosis (preserved)
The original issue record captured these causes before implementation:
- Chat deletion lacked both an authenticated `DELETE` grant and an owner-only
RLS policy.
- `startNewChat()` created only a chat session; the birth profile and
rectification case were account-scoped, so a new chat could not start an
explicit re-rectification while preserving the active chart.
- `dynamic-choice-v2` emitted `request_candidate_confirmation`, but the client
sent the legacy `confirm_guided_candidate` mutation, which rejected dynamic
cases and left them in `confirming` without applying the candidate.
- The reported `都不符合` failure coincided with a deployment replacement in
which Caddy returned HTTP 502 while Docker could not resolve the temporarily
unavailable `web` service.
- WebKit can represent a non-JSON parse failure as a `DOMException` named
`SyntaxError`; the old transport recognized JavaScript `SyntaxError` only and
could expose `The string did not match the expected pattern.` instead of
stable Chinese product copy.
The original closure checklist required owner-only deletion, explicit
account-level re-rectification, atomic idempotent candidate confirmation,
deployment availability or bounded retry behavior, stable localized WebKit
errors, and authenticated end-to-end coverage for `都不符合`, confirmation,
re-rectification, and deletion. The table above maps each requirement to the
implemented local evidence and the remaining production proof.
## Release decision
All five issues remain `verified-local` until the production closure artifacts
above are attached. A public 200 response, an unverified browser session, or a
local in-memory test cannot change them to `closed`. The deployment sequence and
non-destructive rollback are defined in `deploy/README.md`. The executable
creation policy keeps rollout `smoke_only` for one unlogged synthetic account
until the smoke SHA matches the exact deployed revision; ordinary users cannot
incur a new rectification charge during that canary window.
@@ -1,205 +0,0 @@
# Web 生时纠正 vs 本地 Claude Code:差异诊断与改进方案
> 日期:2026-08-01
> 状态:诊断完成;改进方案第 4 节"完整 MVP"已实施(见第 7 节)
> 范围:`jyotish-vedic-astrology` skill 方法论层 vs Web `skills/birth-time-rectification` 受限产品层
## 结论摘要
Web(staging)上用户感受到的"生时纠正交互僵硬、问题像硬编码模板",**不是 bug,而是两种刻意不同的架构**:
- **本地 Claude Code**:LLM 是**主分析师**,走 `jyotish-vedic-astrology` skill 的完整方法论,可自由多轮提问、运行脚本、交叉验证,最终产出精确出生分钟。
- **Web(Mastra)**:LLM 是**被约束的叙述者**,走 `skills/birth-time-rectification/SKILL.md`(36 行受限证据工作流)。服务端 Python 引擎 + TS 状态机拥有全部计算(候选扫描/评分/诊断/事件 ID/策略门控),LLM 只能在服务端预建的问题机会里选一个、再渲染短中文回复,永不确认单一分钟。
staging 前端实际挂载的是 **v4 rectification** 入口(`RectificationV4Panel` → `/api/rectification/v4/*`)。"硬编码感"主要来自服务端模板问题生成器 `opportunity-builder.ts`,与 v4/v5 模式切换无关。
---
## 1. 两个系统的架构对比
| 维度 | 本地 Claude Code | Web(Mastra v4 rectification) |
|---|---|---|
| 使用的 skill | `jyotish-vedic-astrology`(仓库根 `SKILL.md`,712 行,版本 6.9.14) | `skills/birth-time-rectification/SKILL.md`(36 行) |
| skill 目录结构 | symlink 指向根目录 `SKILL.md` + `references/`(100+ 方法论文档)+ `scripts/`(`jyotish_engine.py` 37 子命令)+ `assets/` | 36 行 SKILL.md + 6 个契约文件在 `skills/birth-time-rectification/references/` + `assets/rectification-capability-matrix.json` |
| 方法论 | 8 大方法(Dasha+Transit、D9 Navamsa、D10 Dasamsa、六亲、外表体质、身体缺陷、职业判断、卜卦【AI 暂不支持】);五阶段流程(收集→±30min→±15min→事件验证→D9/D10 收口到 ±5min→报告);决策树权重 Dasha 40% / D9+D10 35% / 专题层 15% / Nakshatra Pada 10% | 受限证据工作流:服务端扫描候选时间簇→评分→生成高信息量机会;agent 每轮只问一个自然问题;输出候选区间而非确定时间 |
| LLM 角色 | 主分析师,自由推理 | 被约束的叙述者(reasoner 选机会 / renderer 渲染) |
| 计算归属 | LLM 驱动 + `scripts/` 脚本 + 外部 oracle(PyJHora/VedAstro/jyotishganit) | 服务端 Python 引擎 + TS 状态机全拥有 |
| 输出 | 验证后的出生分钟(±5min) | 候选区间(`profiles.active_birth_time` 永不直接写入) |
| 错误处理 | 交互式纠错 | 幂等重放(action receipt + fingerprint)、确定性回退 |
**两者的关系**:`jyotish-vedic-astrology` skill 内部同时定义了这两层——方法论层(`references/birth-time-rectification-advanced.md`)和受限产品工作流层(独立的 `skills/birth-time-rectification/`)。Web 端刻意只暴露受限产品层。
---
## 2. 为什么 Web 无法复刻本地交互(6 个根源)
### 2.1 权威模型相反(设计边界,不是 bug)
`skills/birth-time-rectification/SKILL.md` 硬边界原文:
> - The server owns candidate scanning, scores, diagnostics, event IDs, and policy gates.
> - The agent may select one server-provided opportunity or request one server-provided diagnostic.
> - Never invent candidate times, scores, event IDs, dates, techniques, or tool inputs.
> - Never confirm a single minute or write `profiles.active_birth_time`.
这是一整套产品决策:**计费**(`billing.ts` reserve/complete/release)、**不暴露内部分数**(用户只能看到"候选区间"而非权重/评分)、**可靠性**(服务端计算确定性可审计,LLM 只做叙述)、**truth-overlay 合规**(`references/oracle/rectification_technique_usage_audit_2026_07_19.json` 把 D9/D10/D60 等标为"敏感度证据不是证明")。本地 Claude Code 没有这些约束,所以能做完整方法论。
### 2.2 问题是服务端模板("硬编码感"最强处)
`frontend/src/lib/rectification-agent/opportunity-builder.ts`:
- **固定模板文案**:`prompt` 字段全部是写死的句子,例如——
- `clarify_event_subject`:"你刚才提到"X",这件事主要发生在你本人,还是家人或伴侣身上?"
- `refine_event_date`:"关于"X",你还记得更具体的月份或日期吗?不确定也可以只说大概范围。"
- `ask_new_event` 各领域:career/relationship/health_pressure 等各一句。
- **硬编码 utility 公式**(L24-30):`.35*expectedInformationGain + .20*dateSensitivity + .15*candidateSplitRelevance + .10*domainCoverageGain + .10*recallEase + .10*novelty + routingValue[kind] - repetitionPenalty - privacyCost`,其中 `routingValue` 也是写死的(L14-22)。
- reasoner(`reasoner-agent.ts`)只按 `opportunityId` 选一个机会,**从不用自己的话提问**。
> v3 对话式(`/api/birth-time-conversation`)的 `narrative-agent.ts` 已带 `freeConversation` 设置、允许 agent 自由措辞——但 v3 后端未接入当前 UI 面板。
### 2.3 每轮只问一个问题
skill turn strategy:"Ask one natural question only"。`reasoner-agent.ts` 的决策被 `rectificationDecisionSchema` 严格约束,`maxToolCalls` 默认 1、最多一次 `run_rectification_diagnostics` 工具调用,然后必须返回终态动作。本地 Claude Code 是自由多轮对话。
### 2.4 Mastra skill 懒加载
`@mastra/core`(v1.50.1)的 skill 机制:`skills: [skillPath]` 只把 skill **元数据**(name/description,`<available_skills>` 块)注入系统提示;完整 `SKILL.md` 要模型主动调 `skill` 工具才在对话中加载(`node_modules/@mastra/core/dist/` 的 `SkillsProcessor`)。deepseek 走 `structuredOutput` 路径时未必稳定触发 `skill` 工具 → LLM 实际可用的指令比预期少。
### 2.5 硬编码业务规则
- `references/rectification_policy.v1.json`:`minScoringEvents=1`、`minConfirmationEvents=4`、`minConfirmationDomains=3`、`maxExternalValidationWidthMinutes=15`、`maxConfirmationWidthMinutes=5`、`minConfirmationMarginPercent=20`、`maxPlateauRounds=2`。→ 必须凑够 ≥4 个事件、≥3 个领域,否则一直追问,造成"问卷感"。
- 时段区间(`orchestrator.ts` L565-594、`handler.ts` L426-451):early_morning/morning/afternoon/evening/late_night;不确定性(医院 ±2min、家庭 5/10/15、约估 15/30/60)。
- 正则模式(`orchestrator.ts` L132-139):方向切换词/不确定词/肯定否定词/相对日期词。
- 领域分类关键词表(`evidence-extractor.ts` L101-146)。
- 回退文案(`narrative-agent.ts` L639-651)。
- 模型 ID(`handler.ts` L831-832):`deepseek-v4-pro` / `deepseek-v4-flash`。
### 2.6 渲染约束
- reasoner/renderer 都强制 `structuredOutput` JSON(`reasoner-agent.ts` L124、`renderer-agent.ts` L62)。
- renderer 还要 `enforceServerQuestion`(L38-40、L63)把服务端预建的 `exactQuestion` 强制覆盖进输出——LLM 措辞被服务端文案顶替。
- 模型为 deepseek 系列(非 Claude),对话自然度与指令遵循不同。
---
## 3. staging 入口确认
| 项 | 结论 | 证据 |
|---|---|---|
| 前端 UI | **v4 rectification**:`ConversationalBirthTimeRectification` 只是 `RectificationV4Panel` 的别名 | `components/conversational-birth-time-rectification.tsx:20` |
| API 入口 | `/api/rectification/v4/*` | `lib/rectification-v4/client.ts`(cases / active / answer / revise / accept-range / pause/resume/abandon) |
| v4 流程内部 | `runBoundedReasoner`(reasoner-agent.ts)+ `renderPublicTurn`(renderer-agent.ts),两者 `skills: [rectificationSkillPath]`(受限 36 行 skill) | `reasoner-agent.ts:115`、`renderer-agent.ts:16` |
| 模型 | `RECTIFICATION_ORCHESTRATION_MODEL_ID` / `RECTIFICATION_NARRATION_MODEL_ID`(未设则默认目录) | `case-service.ts:46-47` |
| v5 agent vs v4 legacy | 由部署宿主 `.env.staging` 的 `RECTIFICATION_AGENT_V5_ENABLED` / `RECTIFICATION_AGENT_V5_CANARY_PERCENT` / `RECTIFICATION_AGENT_V5_SHADOW` 决定(`feature-policy.ts`),仓库不可见;**两种模式都走同一套受限 skill + 模板问题** | `lib/rectification-agent/feature-policy.ts:26-39` |
| v3 对话式 | 按 rollout audience(paused/smoke_only/public)门控,**未接入当前 UI 面板** | `deploy/configure-staging-rectification-rollout.sh`、`components/rectification-v4-panel.tsx`(无 v3 引用) |
| 部署副本 | Dockerfile 把 `SKILL.md`/`assets`/`references`/`scripts`/`skills` 拷进 `/app/`,symlink 保留 | `deploy/railway-web.Dockerfile:18-22` |
> 注:`/api/health` 只上报 v3 的 rollout 状态(`rollout.conversationalRectificationV3.creationAudience`),不包含 v5 agent 的开关值,因此 v5 模式是否在 staging 开启需查部署宿主的 `.env.staging`。
---
## 4. 改进方案(在"服务端拥有计算"护栏内)
按侵入性从低到高排列,均为**建议**(本次未实施)。任何方案都不得把内部分数/权重/事件 ID 暴露给用户,不得确认单一分钟。
### 4.1 即时注入 skill 指令(低侵入,收益高)
- **改动**:把 36 行 `skills/birth-time-rectification/SKILL.md` 直接内联进 reasoner/renderer 的 `instructions`(`reasoner-agent.ts:117`、`renderer-agent.ts:17`),保留 `skills: [skillPath]` 作为能力来源。
- **效果**:消除 Mastra skill 懒加载不确定性——模型每轮都确定拥有"turn strategy + public language + 硬边界"指令。
- **风险**:低。指令与 skill 内容一致,只是从懒加载改为常驻。
### 4.2 LLM 起草问题 + 服务端 grounded 校验(中侵入,消除"模板感"核心)
- **改动**:`opportunity-builder.ts` 保留"选哪个机会"的服务端决策(kind/targetEventId/domain/utility),但把 `prompt` 从"必须原样使用"改为"话题约束";reasoner 用自然语言起草问题文本;新增一个 grounding 校验(复用 `narrative-agent.ts` 的 grounding 思路)确认草稿:① 命中目标事件/领域 ② 不含内部分数/权重/事件 ID ③ 是单问。
- **效果**:问题随上下文自适应,消灭"你刚才提到X…"的模板感。
- **风险**:中。需要新增校验层与测试;reasoner 输出 schema 从"选 opportunityId"扩展为"选 opportunityId + 起草文本"。
### 4.3 自由对话回合(中侵入)
- **改动**:服务端没有待处理机会(`opportunities` 为空或全部低效用)时,允许 agent 走"自然回应"而非强制提问。可复用 v3 `narrative-agent.ts` 的 `freeConversation` / `questionsAreOptional` 提示词模式,让 renderer 生成 1-3 句自然中文 + 可选开放收尾。
- **效果**:不再每轮都是"选择题",更像本地对话。
- **风险**:中。需防止发散、防止确认未验证分钟;收敛判定仍由服务端掌控。
### 4.4 渲染放宽(中侵入)
- **改动**:renderer 从 `structuredOutput` JSON 改为自然中文文本输出 + 事后校验(`enforceServerQuestion` 保留为兜底,仅当需要明确问题时强制服务端文案)。
- **效果**:回复更自然,减少 JSON 式僵硬措辞。
- **风险**:中。需新的文本校验(主题、长度、泄密扫描)。
### 4.5 模型目录加入 Claude(低侵入,可选)
- **改动**:`frontend/src/mastra/model.ts` 的模型目录加入 Claude(如 `claude-sonnet-5`),`RECTIFICATION_NARRATION_MODEL_ID` 指向它。
- **效果**:叙事/对话质量显著提升(deepseek 在结构化约束下更易模板化)。
- **风险**:低,纯配置;需确认供应商密钥与成本。
---
## 5. 不应改动(设计边界)
以下为 `birth-time-rectification` skill 与产品契约的硬性约束,**任何改进都不得触碰**:
1. 服务端拥有候选扫描、评分、诊断、事件 ID、策略门控。
2. 永不确认单一分钟;永不直接写 `profiles.active_birth_time`。
3. 候选区间只有确定性稳定门通过才对用户可见(`canAcceptRange`)。
4. 计费幂等(billing reserve/complete/release + action receipt 指纹重放)。
5. truth-overlay 强制降级:`reference_only`/`blocked`/`partial` 技法不得作为确定性结论(`references/oracle/skill_truth_overlay_2026_07_19.json`)。
6. 不暴露内部分数、权重、领域标签、工具载荷、agent 轨迹。
改进目标是让**叙述/提问的自然度**贴近本地,而不是让 Web 复刻本地的方法论深度——那需要把整条计算链路搬进 LLM 上下文,与现有产品架构冲突。
---
## 6. 附:关键文件索引
| 文件 | 作用 |
|---|---|
| `skills/birth-time-rectification/SKILL.md` | Web 端受限 skill(36 行硬边界) |
| `SKILL.md`(仓库根) | 本地完整 skill(712 行方法论,symlink 到 skill 目录) |
| `frontend/src/lib/rectification-agent/opportunity-builder.ts` | 服务端模板问题生成器(硬编码根源) |
| `frontend/src/lib/rectification-agent/reasoner-agent.ts` | v4/v5 reasoner(选机会 + diagnostic 工具) |
| `frontend/src/lib/rectification-agent/renderer-agent.ts` | v4/v5 renderer(渲染公开回合 + enforceServerQuestion) |
| `frontend/src/lib/rectification-agent/feature-policy.ts` | v4_legacy / v5_shadow / v5_agent 选择 |
| `frontend/src/lib/rectification-v4/case-service.ts` | 建 case、deployment_mode、模型 ID |
| `frontend/src/lib/rectification-v4/supabase-store.ts` | 持久化 deployment_mode/agent_mode |
| `frontend/src/lib/conversational-rectification/narrative-agent.ts` | v3 叙事 agent(freeConversation 参考实现) |
| `frontend/src/app/api/birth-time-conversation/handler.ts` | v3 handler(deepseek 模型 ID、流式) |
| `references/rectification_policy.v1.json` | 收敛门槛硬编码 |
| `deploy/configure-staging-rectification-rollout.sh` | staging rollout(paused/smoke_only/public) |
| `frontend/supabase/migrations/20260728020000_*.sql` | v5 列(deployment_mode/agent_mode/model id/version) |
---
## 7. 已实施:Agentic 生时纠正 MVP(2026-08-01)
按用户决策"完全复刻本地方法论",实现了一个新的 **agentic 生时纠正**聊天流:LLM 挂载完整 `jyotish-vedic-astrology` skill,像本地 Claude Code 一样驱动方法论,通过引擎工具请求计算(而不是自己瞎算),自由多轮对话,最终经高 rigor 确认门 + 用户明确同意后写回 `profiles.active_birth_time`。
### 7.1 新增文件
| 文件 | 作用 |
|---|---|
| `frontend/src/mastra/rectification-tools.ts` | 7 个工具包 Python 引擎端点:`rectification-gate`(精度门)、`rectification-scan`(分钟敏感度扫描)、`rectification-score`(V5 矩阵评分)、`rectification-diagnostics`(鲁棒性诊断)、`rectification-candidate-features`(候选静态特征)、`rectification-confirm`(高 rigor 三引擎 parity 确认门)、`rectification-save-birth-time`(服务端双重校验后写 profile) |
| `frontend/src/mastra/agentic-rectification.ts` | agent 工厂:完整 skill + 工具 + 中文指令(方法论流程、truth overlay、保存门控) |
| `frontend/src/lib/rectification-agentic/session.ts` | 会话支持:加载 profile 出生字段、`applyConfirmedBirthTime` 调 service-role RPC 写回 |
| `frontend/src/app/api/rectification/agent/route.ts` | NDJSON 流式端点:认证 → profile → 计费 reserve → agent.stream → delta/done 事件 → settle |
| `frontend/src/components/rectification-agentic-chat.tsx` | 聊天面板:流式渲染、隐藏块解析(suggestions/title/保存哨兵)、错误处理 |
| `frontend/src/components/conversational-birth-time-rectification.tsx` | 入口智能切换:有进行中的 v4 case → v4 面板恢复;否则 → agentic 聊天 |
| `frontend/supabase/migrations/20260801000000_agentic_rectification_profile_write.sql` | `apply_agentic_rectification_birth_time` RPC(security definer,仅 service_role,含基线并发保护) |
| `frontend/tests/rectification-agentic-tools.test.ts` / `rectification-agentic-session.test.ts` | 12 个测试 |
### 7.2 安全门控(核心)
LLM 绝不能写任意分钟。`rectification-confirm` 只有在引擎高 rigor 门全过(≥4 事件、≥3 领域、宽度/边际阈值、三引擎 parity、外部 VedAstro 校验)返回 `confirmation_allowed=true` + 确认分钟时,才在会话闭包中设置 `confirmedGate`;`rectification-save-birth-time` 要求请求的时间**恰好等于**该确认分钟,才调用 RPC 写库。RPC 还带 `p_baseline_time` 并发保护(当前 active 时间必须仍是会话开始时的基线)。
### 7.3 验证
- `npx tsx --test tests/*.test.ts`:**1076 全通过**(含 12 个新测试)。
- `npx tsc --noEmit`:新文件零错误(仓库剩余 5 个为预先存在)。
- `npx eslint`:新文件零错误零警告。
### 7.4 待办/注意
- **引擎端点鉴权**:`rectification-save-birth-time` 走的 RPC 仅 service_role;引擎各 rectification 端点无需 token(与 `runConsultationWorkflow` 一致)。
- **计费**:按消息 reserve/complete/cancel 咨询点数(复用 `begin/complete/cancel_consultation_credit`)。
- **v4 保留**:有进行中 v4 case 时仍走 v4 面板恢复,不丢数据。
- **模型**:默认走当前模型目录;若想让叙事用 Claude,在 `LLM_MODELS_JSON` 加 Claude 项并把 `LLM_DEFAULT_MODEL_ID` 指过去即可。
- **部署**:新路由无需新环境变量(复用 `JYOTISH_API_BASE`、Supabase 密钥、模型目录);新迁移需在 staging 执行 `db:migrate`。