From d5623b9fcb4021834ff1e5eb54d8c9d2aa1d5292 Mon Sep 17 00:00:00 2001 From: Tianyu Liu Date: Mon, 24 Aug 2026 18:07:45 +0200 Subject: [PATCH] M8-R10: document meter lifecycle recovery runbook --- docs/design/m8-warmtelink-energy.md | 30 ++++++++++++++++++++ docs/meter-epochs.md | 43 +++++++++++++++++++++++++++++ 2 files changed, 73 insertions(+) diff --git a/docs/design/m8-warmtelink-energy.md b/docs/design/m8-warmtelink-energy.md index 1329898..7d4265a 100644 --- a/docs/design/m8-warmtelink-energy.md +++ b/docs/design/m8-warmtelink-energy.md @@ -1211,3 +1211,33 @@ M8 收尾的前置条件。agent 不得执行、记录为已执行,或以 mock - [x] 最终报告完整交付 §12 九步及用户证据模板;真实 serial/HA walkthrough 由用户在交付后自行验收,不能在报告中伪称 agent 已执行。 - [x] 文档、Roadmap 和 milestone 状态反映真实完成度;没有删除用户数据、没有真实 secret/设备身份, 没有未经授权的 push/tag。 + +## 14. Post-M8 lifecycle repair(M8-R08~R10) + +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-R08:Close、Unbind、原子 Transfer 与 API 契约 | done | PASS(第 6 轮) | `M8-R08-impl-*`、`M8-R08-review-6.md` | +| M8-R09:Meter 管理 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 审计状态,不得伪称历史已干净收口。 diff --git a/docs/meter-epochs.md b/docs/meter-epochs.md index 6839a05..ab7530c 100644 --- a/docs/meter-epochs.md +++ b/docs/meter-epochs.md @@ -114,6 +114,49 @@ M8 将“协议连接”和物理 Meter 分开:`MeterSource` 产生稳定 chan 部署与回滚串口 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。