M8-R10: document meter lifecycle recovery runbook
frontend / frontend (push) Successful in 47s
pytest / test (push) Failing after 3m55s

This commit is contained in:
2026-08-24 18:37:46 +02:00
parent b1b6a309cb
commit d5623b9fcb
2 changed files with 73 additions and 0 deletions
+30
View File
@@ -1211,3 +1211,33 @@ M8 收尾的前置条件。agent 不得执行、记录为已执行,或以 mock
- [x] 最终报告完整交付 §12 九步及用户证据模板;真实 serial/HA walkthrough 由用户在交付后自行验收,不能在报告中伪称 agent 已执行。 - [x] 最终报告完整交付 §12 九步及用户证据模板;真实 serial/HA walkthrough 由用户在交付后自行验收,不能在报告中伪称 agent 已执行。
- [x] 文档、Roadmap 和 milestone 状态反映真实完成度;没有删除用户数据、没有真实 secret/设备身份, - [x] 文档、Roadmap 和 milestone 状态反映真实完成度;没有删除用户数据、没有真实 secret/设备身份,
没有未经授权的 push/tag。 没有未经授权的 push/tag。
## 14. Post-M8 lifecycle repairM8-R08R10
M8 交付后的 Meter lifecycle 修复链记录在本地 `review-notes/M8-meter-lifecycle-repair-plan.md`
它不新增 ORM / **数据库** schema 或 Alembic revision,不做启动自动修复、一次性数据脚本或历史删除;R08 虽然
更新了 API/Pydantic schema 及 OpenAPI/codegen,但没有变更 ORM 或数据库 schema。已有的 stranded binding 只能
由用户通过 UI 的原子 Transfer 恢复。
| 修复卡 | 当前状态 | 独立 review | 实现 / review 简报 |
| --- | --- | --- | --- |
| M8-R08Close、Unbind、原子 Transfer 与 API 契约 | done | PASS(第 6 轮) | `M8-R08-impl-*``M8-R08-review-6.md` |
| M8-R09Meter 管理 UI | done | PASS(第 12 轮) | `M8-R09-impl-*``M8-R09-review-12.md` |
| M8-R10:文档、最终报告与全量收尾 | done | PASS(第 2 轮) | `M8-R10-impl-1.md``M8-R10-review-2.md` |
R08 的 base commit 为 `c40ee65`(其自动化链包含 5 个 `fixup!`,最终 review 范围止于 `5a50b07`);R09
的 base commit 为 `5c08825`(包含 10 个 `fixup!`,最终 review 范围止于 `c4f4ff7`)。R10 已由第 2 轮
冷启动独立 Reviewer 判定 PASS 后标记 `done`;实现者的本地闸门结果没有被当作人工或 review 验收。
R08/R09 规定的用户操作为:Close active Meter、Unbind open binding、以单请求 same-Meter Transfer 切 source
以及把 closed previous Meter 上的 stranded DSMR binding 恢复到 active Meter。`meter_swap` 在唯一兼容 open
binding 时可自动 handoff;歧义一律 fail closed。恢复可选择晚于新 Meter 起点的时间并留下可见 gap,所有
lifecycle 写入与相应成本重算同事务,失败整笔回滚,HA discovery 只在 commit 后 best-effort republish。
stranded DSMR walkthrough 只能使用开发库中已经存在、由用户报告的 stranded row;不得通过 SQL、API、脚本、
migration 或直接改数据库制造前置状态。若隔离开发库没有这类历史行,应将该项记录为 `N/A/blocked`。Close、
Unbind、Transfer、Recovery 与 handoff 都会改写 Meter/binding 状态,walkthrough 必须将它们视为独立场景,使用
彼此独立且满足前置条件的 Meter/binding(或先恢复各自前置状态),不得按一个会破坏后续前置条件的连续故事操作。
按用户指定顺序,R10 必须先获得独立 Reviewer PASS;之后才由 Orchestrator 对未 push 的 fixup 执行 autosquash
并回填最终交付 SHA 与 commit 数。在此之前,报告只可陈述 pre-autosquash 审计状态,不得伪称历史已干净收口。
+43
View File
@@ -114,6 +114,49 @@ M8 将“协议连接”和物理 Meter 分开:`MeterSource` 产生稳定 chan
部署与回滚串口 source 参见 [`warmtelink-energy.md`](./warmtelink-energy.md);保留旧 source/binding/history 可使审计和重算可重复,不能通过删除历史来“修复”边界周期。 部署与回滚串口 source 参见 [`warmtelink-energy.md`](./warmtelink-energy.md);保留旧 source/binding/history 可使审计和重算可重复,不能通过删除历史来“修复”边界周期。
## 生命周期操作与 stranded binding 恢复
M8-R08/R09 为 Meter 与 binding 增加了显式的生命周期操作。所有时间都由前端按本地日期时间输入,再按既有
Principle-A 约定交给后端;未来时间会被拒绝。
- **Close Meter**:只能关闭 active Meter。该 Meter 与它的所有 open binding 在同一个 `ended_at`、同一
事务中关闭;关闭后该 commodity 没有 active Meter。
- **Unbind**:可对任意 open binding 执行,只写 binding 的 `ended_at`,绝不删除历史。已关闭 Meter 上残留的
open binding 也可在 UI 中解绑,默认关闭时间为该 Meter 的 `ended_at`
- **Transfer**:以单个原子请求切换 source channel,不采用浏览器端“先关闭再新建”的两步操作。同一 Meter
的 source switch 在同一 `effective_at` 关闭旧 binding、开启新 binding。失败时 binding、Meter 与受影响的
成本重算全部回滚,不会显示或留下部分成功。
新建或更新 binding 必须完整落在所属 Meter epoch 内:
`meter.started_at <= binding.started_at < binding.ended_at <= meter.ended_at`Meter 已关闭时);
open-ended binding 只允许属于 active Meter。Close、Unbind、Transfer 与声明 Meter 都从最早受影响边界重算到
当前时间;electricity 与 heating/hot-water 分别使用对应的成本引擎,重算失败时整笔生命周期变更回滚。数据库
提交成功后才会 best-effort 重新发布 HA discovery;发布失败不会伪装成持久化失败。
### 换表自动交接与人工恢复
声明 `reason=meter_swap` 的新 Meter 时,若未选择 channel 且旧 active Meter 恰好有一条唯一、单位兼容的
open binding,系统会在新 Meter 起点自动把该 channel 原子交接过去。存在多个候选或时间线歧义时,声明会
fail closed 并整体回滚。不是这种唯一自动交接的声明,也会在新边界关闭旧 Meter 的 open binding,避免产生新的
“closed Meter + open binding”。
旧版本或历史异常可能已经留下 stranded binding:前一块 closed Meter 仍有 open DSMR/WarmteLink binding
而当前 active Meter 没有 binding。不要修改数据库、不要跑 migration,也不会在启动时自动修复。请在
**Energy → Meters** 使用该 stranded binding 的 **Recover binding** 操作:选择当前 active Meter 与兼容
channel,提交一次 Transfer。来源必须是同 commodity、唯一且紧邻的前一块 closed Meter;旧 binding 固定在旧
Meter 的 `ended_at` 结束,新 binding 从选择的 `effective_at` 开始。默认是新 Meter 的 `started_at`;选择更晚
时间是允许的,但 UI 会提示这段明确的 unbound gap。非前序 Meter、单位不匹配、channel 在无关区间被占用或歧义
都会被拒绝,不会误关历史。
人工 walkthrough 只能使用开发库里**已经存在且由用户报告的** stranded row。不得为了演示通过 SQL、API、
脚本、migration 或直接改数据库制造这种历史异常;隔离开发库中没有该 row 时,记录此项为 `N/A/blocked` 即可。
Close、Unbind、same-Meter Transfer、Recover binding 与 meter-swap handoff 都会改变 Meter 或 binding 状态,
因此应作为彼此独立的验收场景:每个场景使用各自满足前置条件的 Meter/binding,或在执行前恢复独立前置状态,
不能把它们串成会互相破坏前提的单一故事。
上述恢复不新增 Alembic migration、不执行启动修复,也不删除 Meter、binding、reading 或成本历史;它只把用户
确认的时间线修正为可审计的闭区间。
## 非目标(本里程碑不做) ## 非目标(本里程碑不做)
- Gas 计费 strategy。 - Gas 计费 strategy。