Move the reviewed durable-event, HTTP envelope, and PI typed-result designs off the docs/ prefix so Track E can start from latest dev. Co-authored-by: Cursor <cursoragent@cursor.com>
23 KiB
title, status, created, updated, type, owner
| title | status | created | updated | type | owner |
|---|---|---|---|---|---|
| 模块解耦 Phase 2:持久事件交付与后续解耦路线 | planned | 2026-09-15 | 2026-09-15 | refactor | TBD |
模块解耦 Phase 2:持久事件交付与后续解耦路线
决策摘要
Phase 2 的核心交付只包含持久事件链:修正事件生产、补齐 PostgreSQL 投递实现、用无副作用 shadow 对账替换闭包桥,并以授权用户刷新/重启后的正文恢复作为停走门。
结构解耦(D4–D7)是独立轨道;HTTP 错误信封与 PI Runner typed result 各自单独立项。三条轨道分别验收,不能互相阻塞关闭。
本计划不拆微服务,不引入外部消息基础设施,不合入 FAV 分支,不删除历史数据,也不授权实施、合并或部署。
背景与纠偏
- 上位约束:模块化单体解耦 ADR、架构设计、Phase 1 计划。
- 复核结论:Phase 2 方案审阅。
- 现状调研:IPC/RPC 现状评估。
internal/events/pg.go目前只有信封写入与 SELECT 型ClaimIDs,没有原子 lease、ack/fail、重试、dead-letter 的生产闭环。internal/events/dispatcher_test.go只证明MemoryBus语义,不能作为 PG 运行证据。docgov仅单向依赖docrevision的 Markdown canonical/hash 工具,不存在生产双向 import 环。module-boundaries.yaml已有event_producers/event_consumers;缺口是映射不完整、运行时注册未与静态清单校验。docflow.Commands在部分生产 HTTP 路径临时构造,事件 sink 未稳定注入;其 envelope 仍写入Environment:"dev"、ProjectID:0。未先修生产入口就开始 shadow,会得到虚假的零差异。
目标与边界
Track E:持久事件交付(本计划核心)
目标:让 notify 与 projection 副作用在重复投递、进程崩溃、租约过期和重启后达到“至少一次投递、恰好一个可见结果”,并能从数据库追查每次 attempt。
包含 D1、D2、D3、D10、D11。Track E 完成后即可单独关闭,不等待结构或协议轨道。
Track S:结构解耦(后续独立 go/no-go)
目标:移除 bizdev locator,收敛 stageflow/chat/intent 依赖,并消除 docgov → docrevision 的剩余工具依赖。包含 D4、D5、D6、D7a、D7b。
Track S 只有在 Track E 复盘后重新认领 mutex 才能开始;每个单元独立关闭。
Track P:外部协议(另立 remediation)
- P1:HTTP/SSE 错误信封。
- P2:PI Runner typed-result-v1 rollout。
D8、D9 在本文件保留设计约束和立项入口,不属于 Track E/S 的 Definition of Done。独立方案见 HTTP/SSE 错误信封 与 PI typed-result-v1。
明确排除
- U4/U5 的 path/sql_write 例外清零及其既有 owner。
feature/final-artifact-vault的审计、合入、subscriber 激活与真实副作用验收。本计划只预留 FAV event schema 和 disabled allowlist 条目。- 全量 IPC/RPC、服务拆分、分布式事务协调器。
- 破坏性历史清理。旧事件只允许分类、隔离、修复或明确过期;任何删除另走审批计划。
需求与可追踪性
| ID | 要求 | 交付单元 | 核心验收 |
|---|---|---|---|
| E1 | PG delivery 状态机可生产运行 | D1 | 真实 PG 多 worker、崩溃点、fencing 测试 |
| E2 | 至少一次投递、一个可见结果 | D1/D2 | 副作用幂等键/事务 receipt;重复投递无重复可见结果 |
| E3 | 每次 attempt 可审计 | D1 | 不可变 attempt 表,日志不代替数据库审计 |
| E4 | 事件作用域真实且授权闭合 | D3/D11 | env/project/service principal/资源复核全部 fail closed |
| E5 | 闭包桥安全下线 | D2 | shadow 覆盖门通过;逐消费者切换;一窗口后删 API |
| E6 | 静态契约与运行注册一致 | D10 | allowlist 已有段补全;构建时 registry parity 门禁 |
| E7 | 历史脏数据不会误消费 | D11 | dry-run 清单;repair/quarantine/expire 决策可审计 |
| E8 | 用户刷新恢复 | D2 | 授权 UI 刷新/重启正文一致,无 projection-missing |
| S1 | bizdev 不再充当 locator | D4 | 8 个 getter 最终同批删除;无新调用方 |
| S2 | 用例类型归属清楚 | D5 | coordinator 不依赖 bizdev DTO |
| S3 | 剩余工具依赖消除 | D6 | docgov/docrevision 互不 import;canonical golden 一致 |
| S4 | intent/chat 可独立迁移 | D7a/D7b | 两条独立分支、mutex、验收与回滚 |
关键技术决策
- 语义不写“exactly once delivery”。 Dispatcher 提供 at-least-once delivery。消费者用唯一键、事务 inbox/receipt、CAS 或 no-replace 提交保证 exactly-one visible outcome。
- shadow 不执行业务副作用。 旧闭包仍是唯一真实写路径;shadow 只计算规范化 intended outcome,并写对账记录。禁止“闭包与消费者双写后比较”。
- 独立 worker 进程。 事件消费落在
cmd/events,与 HTTP 进程分开扩缩容;internal/app不重复启动 dispatcher。该选择沿用 Phase 1 U6,避免两种拓扑并存。 - 先证明事件源,再开始计时。 三条文档链必须在同一 UoW 写入领域变化、envelope 和目标 delivery。PG 测试证明生产入口确实产生事件后,shadow 窗口才有效。
- 每个消费者独立灰度。 状态为
off|shadow|live,按 environment + consumer 配置。notify、workflow advance、projection 分开切换;FAV 保持 off。 - 静态 allowlist 与运行 registry 同源。
module-boundaries.yaml现有映射补全后生成或校验只读 registry manifest;生产禁止未登记的动态注册。 - 历史事件先盘点。 作用域为 0、环境错误、未知 consumer、序列不连续或 payload schema 未知的旧记录不得自动 live 消费。
- 结构与协议不绑核心关闭。 Track E、S、P 分别有状态、停走门、测试报告和回滚点。
目标拓扑
flowchart LR
HTTP[server/use case] -->|domain change + envelope + deliveries / one UoW| PG[(PostgreSQL)]
WK[cmd/events] -->|atomic lease| PG
WK --> N[notify]
WK --> A[workflow advance]
WK --> P[projection]
N -->|idempotency key| PG
A -->|transactional inbox| PG
P -->|expected head + digest| WS[workspace projection]
F[FAV contract] -. disabled .-> WK
数据模型与并发协议
Delivery 状态机
pending -> leased -> acked
|-> pending transient failure / lease expiry
|-> dead max attempts or explicit permanent failure
|-> quarantine scope, schema, integrity or ordering violation
- Claim 必须在一个短事务内
WITH candidates AS (SELECT ... FOR UPDATE SKIP LOCKED LIMIT ...) UPDATE ... FROM candidates ... RETURNING,同时写lease_owner、递增fencing_token、lease_until、attempt。现有 SELECT-onlyClaimIDs不满足此条件。 - Handler 在 claim 事务外执行。Fencing token 必须随 consumer command 传到最终副作用提交点;数据库副作用在同一事务校验 token,projection marker 使用 expected version + 单调 token。Ack/fail 也必须匹配
event_id + consumer + lease_owner + fencing_token。旧 token 的副作用和 ack/fail 都必须失败。 - 长任务按受保护的 token 续租;heartbeat 失败立即取消 handler。停机先停止 claim,再等待 inflight 或安全释放租约。
- 同一 consumer + aggregate 同时最多一个 inflight;不同 aggregate 可并行。不能把领域版本号直接假设为无缺口事件序号。
- D11 必须在以下两种模型中选一并写 ADR/迁移:独立无缺口 event sequence;或 consumer checkpoint + 首事件 bootstrap。未决前禁止启用 gap 阻塞。
副作用原子性
| 消费者 | 可见结果契约 | 崩溃窗口处理 |
|---|---|---|
| notify | event_id + recipient + notification_kind 唯一键 |
通知写入与 inbox 同一 PG 事务;重放命中唯一键即视为成功 |
| workflow advance | event_id + transition 唯一 receipt;重新读取当前状态和权限 |
领域转换、outbox、inbox 同一 UoW;状态已前进时按 receipt 判定 |
| projection | document_id + source_snapshot_id + digest no-replace;expected head |
文件先写临时对象并 fsync,再原子提交 marker;重放核对 digest |
| FAV | 本计划不激活 | 由 FAV 审计定义 source-event/digest no-replace 契约 |
副作用完成后、delivery ack 前必须注入崩溃。恢复后可重复调用 handler,但最终只有一个可见结果。
审计表
Additive migration 新增不可变 event_delivery_attempts,至少记录:event、consumer、attempt、lease owner、fencing token、started/finished、outcome、typed error code、correlation ID。event_deliveries 只保存当前游标和状态。
attempt、dead-letter、shadow reconciliation 只存 ID、digest、typed code 和字段 allowlist 生成的摘要。禁止保存原始 payload、SQL error、绝对路径、token、prompt 或正文。
权限模型
| 动作 | 主体 | 必须校验 | 禁止 |
|---|---|---|---|
| 普通消费 | 每个 consumer 独立 service principal | environment、project、consumer、action;副作用前重读资源归属和当前状态 | 继承原用户会话、跨项目查询、共享万能 principal |
| shadow | ops service principal | 只写 reconciliation;不持有业务写权限 | 调用真实通知/推进/投影写端口 |
| replay dry-run | 具备事件运维 capability 的管理员 | env/project/consumer/event 范围、reason、approval ref | 无范围全库扫描、原始 payload 回显 |
| replay apply | 同一操作者能力 + 未过期 plan digest | apply 参数必须与 dry-run digest 完全相同 | 现场改范围;绕过资源当前授权 |
| 多项目/批量 replay | 两人批准 | operator 与 approver 分离;不可变审计 | 单人直接执行 |
worker 数据库角色只授予其拥有表和调用端口所需权限。Replay 是应用用例,不提供裸 SQL 运维入口。授权失败、资源已转移、project 不存在或 env 不匹配一律 quarantine,不重试为 transient。
分波计划
E0A:生产者、契约与旧数据门(D3、D10、D11)
进入条件:认领 events/app/arch mutex;dispatcher 保持 off。
交付:
- 组合根构造唯一
docflow.Commands,向 server 注入 UoW、PGSink、environment与 project resolver;禁止 HTTP handler 临时漏注入。 - candidate-ready、downstream-impact、approved 三条链在领域提交同一 UoW 内写 envelope 和目标 delivery。
- 修正
Environment/ProjectID,缺值 fail closed;先清点历史0/dev记录。 - 补全现有 event allowlist,校验生产者、消费者、payload schema、运行 registry parity。
- 生成历史事件 dry-run manifest,逐条分类为 safe、repair、quarantine、expire;不删数据。
- 决定连续性模型,审计
(aggregate_type, aggregate_id, aggregate_version)唯一约束是否允许同一版本多个事件;必要时 additive migration。
退出门:真实 PG 测试证明每条生产链产生正确 envelope/delivery;历史清单有审批结论;未登记事件和动态 consumer 构建失败。
E0B:PG dispatcher 基座(D1)
进入条件:E0A 全部通过;旧数据不会进入 live claim。
交付:
- 完成原子 lease、heartbeat、fenced ack/fail、backoff、dead/quarantine 和 attempt audit。
- 新建
cmd/events;每个环境单独容器、配置和 DB 连接预算。HTTP Runtime 不启动 worker。 - 增加 readiness(DB、registry、schema)、liveness、queue age、oldest pending、claim/ack/fail/fenced、attempt latency、dead/quarantine 指标。
- 默认从 Phase 1 基线起步:batch 50、lease 30s、heartbeat 10s、max attempts 12、backoff ceiling 15m;压测报告才能调整。
- 以 1/2/4 worker 验证无双 lease、跨 aggregate 并行、同 aggregate 串行、连接池有界和公平性。
退出门:PG 集成与 race 测试通过;在副作用后/ack 前、续租前后、停机 drain 等崩溃点均可恢复;资源预算和 p50/p95/p99 基线归档。
E1A:notify shadow 与切换(D2-notify)
- 旧闭包产生 canonical outcome;shadow handler 产生 intended outcome。两者经同一规范化器比较收件人、类型、去重键、payload digest。
- E0A 根据经作用域校验和审批的前 7 天有效事件基线为每个 event type 预登记最小样本 N,N 不低于 30;低流量分支补确定性 replay fixture。窗口不少于 3 个完整日,并覆盖成功、授权拒绝、重复和 permanent failure。
- 任一未解释 diff、越权、原始 payload 泄露或样本不足均重置窗口。
off|shadow|live是带 revision 的集群级受控状态,不是单实例环境变量。先部署同时理解新旧模式的兼容代码,drain 并确认旧实例全部退出,再原子切换权威写者。- 先把 notify 置 live;闭包 notify 路径置 off。保留显式回滚开关一个发布窗口。
退出门:重复投递只有一个通知;重启不丢;correlation ID 可查 producer→attempt→receipt;回滚演练通过。
E1B:workflow advance shadow 与切换(D2-advance)
批准事务必须发布 typed approved event。Consumer 在副作用前重读 document/project/workflow 状态和 service principal 权限。按 E1A 同样的 coverage gate 切换,随后移除批准 hook 注册。
退出门:并发批准只推进一次;陈旧或跨项目事件 quarantine;阶段推进产生的下游事件保持 causation/correlation;回滚演练通过。
E1C:projection 恢复纵切(D2-projection)
在副作用前、副作用后/ack 前、lease 过期后三个位置终止 worker并重启。授权用户通过真实页面刷新和重新打开文档,正文必须与数据库 head/snapshot digest 一致,不出现 projection missing,不产生重复版本。未授权用户仍得到非泄露式拒绝。
退出门:真实 PG + browser + worker restart 报告通过;按 correlation ID 能还原完整链路。FAV 仍为 off。
E2:核心收口
- 删除
OnOutbox =、AddOnApprovedHook(等生产注册点。 - 保留 API 一个发布窗口后,再独立 contract 提交删除字段/方法本体。
- Track E 的 release blocker 关闭;Track S/P 保持各自状态,不拖延此关闭。
实施单元明细
D1:PG dispatcher 与 attempt audit
主要文件:deploy/postgres/goose/<next>_event_dispatcher.sql、internal/events/pg.go、internal/events/dispatcher.go、cmd/events/main.go、环境 compose/config。
必须交付 E0B 的状态机、进程拓扑、连接预算、指标和真实 PG 故障注入。MemoryBus 测试只作快速 characterization,不计生产验收。
D2:闭包迁移
主要文件:internal/docrevision/service.go、internal/docgov/engine.go、internal/app/app.go、internal/app/docnotify、projection owner。
拆成 notify、workflow advance、projection 三个 short-term。Shadow 只写 reconciliation;每个消费者都有独立 off|shadow|live、停走门和回滚演练。最后一个 live 窗口结束后才能删除 hook API。
D3:事件生产与作用域
主要文件:internal/app/docflow/commands.go、server 注入点、internal/taskrun/finalize.go。
修复生产入口漏注入和硬编码。事件构造必须从已授权服务端上下文与持久记录取得 environment/project;客户端字段不可信。FAV staging 调用不属于 Track E;FAV disabled 时,生产链不得写 staging、artifact 或 publication 记录。Fav.StageAgentCommit(ctx, 0, ...) 的真实 project scope 修复随独立 FAV 审计实施。
D10:事件契约与 registry parity
主要文件:.doc/system-design/module-boundaries.yaml、internal/archtest、registry manifest 生成/校验代码。
不是“新增 allowlist 段”,而是补全已有段并把 payload schema/version、合法 producer、合法 consumer、enabled state 一并校验。运行 registry 必须是静态清单的子集;FAV 条目标记 disabled。
D11:历史数据与序列安全
主要文件:additive migration、只读 inventory/replay use case、审计报告。
交付历史清单、序列模型、约束审计、repair/quarantine/expire 规则。修复必须保留 old/new digest、operator、reason、approval;禁止原地无审计改值。
D4:bizdev locator 最终移除
先加 no-new-callers 门禁,再分批把调用方迁移到消费者端口。准备工作可分批;8 个 getter 的最终删除必须在一个 contract 提交完成。规则只禁止领域/用例 API 暴露、返回或转发 pool/tx;允许 app/cmd 组合根向表属主构造器注入 pool,也允许登记属主从 opaque TxScope 解出 tx。
D5:stageflow 类型归属
stageflow 定义 use-case request/result,并在 app 边界适配 bizdev 领域类型。行为方法不能机械复制成 DTO。用 mapping golden 和 characterization 锁定语义;coordinator 最终不 import bizdev。
D6:移除单向 canonical 工具依赖
当前不是循环。把 Markdown canonicalization 与 SHA 作为一个内聚的共享行为模块(建议 internal/doccontent)提取,提供 golden vectors。禁止把带行为的 canonicalizer 塞入 vocabulary。完成后 docgov/docrevision 互不 import。
D6 不依赖 D4/D5,只受 docgov/docrevision mutex 与 U4/U5 排期约束。
D7a:intent 与 bizdev 解绑
为 intent 注入最小只读消费者端口;单独分支、characterization、mutex 和验收。不得等待 chat 迁移。
D7b:chat 编排迁入 chatflow
逐用例迁移,保留 handler characterization。只有 D4 提供所需端口后才开始;禁止“一次搬空包”。每批收紧一条 import 规则并可独立回滚。
D8:HTTP/SSE 错误信封立项约束
另建 remediation。公开 wire 至少为 code/message/request_id/details?;message 使用固定安全文案,details 按 code 维护公开字段 allowlist。不得回传 raw error、SQL、路径、凭证、prompt、正文或跨租户 ID。授权拒绝保持资源存在性不泄露。全路由、SSE 与前端迁移各有 golden/negative test。
D9:PI typed-result-v1 rollout 立项约束
另建 remediation,不称“协议 v2”。只有 typed 字段完全缺失且握手明确声明旧 Runner 时才允许 legacy;typed 字段存在但 schema/digest/path/run_id/size 校验失败必须 fail closed,禁止降级。真实 PI E2E 覆盖取消、超时、未知 schema、超限输出、secret/prompt 注入与 downgrade attack。
迁移、部署与回滚
| 阶段 | 部署顺序 | 可回滚动作 | 禁止 |
|---|---|---|---|
| expand | additive migration → producer 修复 → registry | worker 保持 off;回滚应用不删表 | 旧数据直接 live claim |
| shadow | worker shadow → reconciliation | consumer 置 off;保留记录 | 执行业务副作用;清空 diff |
| switch | 部署兼容代码 → drain 旧实例 → 记录 backlog 水位 → 原子切换单 consumer live/闭包 off | 暂停 claim,核对已执行 receipt 与 backlog,再 live off + 对应闭包 on;只在兼容窗口内 | 滚动部署时同时开启两条真实写路径 |
| contract | 一发布窗口后删 hook API | 通过新提交恢复,不回滚 schema | 删除 delivery/attempt 历史 |
回滚期间禁止同时 replay。恢复 live 前必须重新做 backlog dry-run,并确认旧 lease 已失效。Migration 只 additive;任何表/列删除另立计划。
分支、mutex 与交付件
| 单元 | mutex | 分支示例 | 独立报告 |
|---|---|---|---|
| D3/D10/D11 | events + app + arch | fix/event-production-scope-contract |
E0A |
| D1 | events + deploy | feature/events-dispatcher-pg |
E0B |
| D2-notify | events + docnotify | fix/event-notify-cutover |
E1A |
| D2-advance | events + workflow | fix/event-workflow-cutover |
E1B |
| D2-projection | events + docgov | fix/event-projection-recovery |
E1C |
| D4 | bizdev | fix/bizdev-locator-removal |
S1 |
| D5 | stageflow + coordinator | fix/stageflow-type-ownership |
S2 |
| D6 | docgov + docrevision | fix/doccontent-canonical-boundary |
S3 |
| D7a | intent | fix/intent-bizdev-boundary |
S4a |
| D7b | chat + chatflow | fix/chatflow-orchestration-boundary |
S4b |
每行先建 short-term 并登记 CURRENT。合入 dev 与共享容器验证必须占集成槽位。
验收与停走门
Track E Definition of Done
- E1–E8 均有证据链接;E0A/E0B/E1A/E1B/E1C 各有独立 test report。
- PG 状态机、attempt audit、1/2/4 worker、race、崩溃点、fencing、顺序、背压和连接预算通过。
- notify/workflow/projection 分别完成 shadow coverage、切换和回滚演练;FAV 保持 disabled。
- 生产注册代码无
OnOutbox =、无AddOnApprovedHook(;不存在 HTTP 与 worker 双重接线。 - 授权 UI 在三类 worker 重启点刷新/重开正文一致;未授权与跨项目负例通过。
- replay dry-run/apply digest、service principal、字段脱敏、双人批量审批负例通过。
- arch-check block、
go test -race目标包、go test -short ./...、PG 集成、browser/restart 全绿。 - CURRENT、FEATURE-MATRIX、gap-matrix 只关闭事件可靠性 blocker,不把 Track S/P 写成已完成。
Track S 单元 DoD
D4–D7b 各自满足需求表中的 S1–S4、独立测试报告和 arch rule 收紧即可关闭;无需等待其他结构单元。
Track P 立项门
D8、D9 各有独立 remediation、owner、范围、兼容矩阵、negative tests 和真实验收计划。本 Phase 2 不要求其实现完成。
风险与控制
| 风险 | 控制 |
|---|---|
| shadow 因无事件源虚假通过 | E0A 先证明每条生产入口写 envelope/delivery,之后才开始计时 |
| 领域版本缺号永久阻塞 | D11 明确 event sequence 或 checkpoint/bootstrap,不把 version 当连续事件号 |
| 副作用成功但 ack 丢失 | 每消费者幂等契约;真实崩溃注入;fenced ack |
| 重试覆盖历史 | immutable attempts;delivery 只存当前状态 |
| 脏旧事件跨环境/项目执行 | inventory + quarantine;资源当前授权复核;service principal 最小权限 |
| worker 抢光连接 | 独立 pool 预算、inflight 上限、queue/backpressure 指标、压测后调参 |
| poison event 阻塞 aggregate | quarantine 需显式审批;不静默跳号;其他 aggregate 继续 |
| shadow/replay 泄密 | schema 字段 allowlist、digest 摘要、无 raw payload/error |
| FAV 拖住核心 | 只预留 disabled 契约;激活归独立 FAV 计划 |
| 结构改造扩大爆炸半径 | no-new-callers 先行;小批迁移;最终 getter 原子删除 |