diff --git a/.doc/ops/research/2026-09-15-module-decoupling-ipc-assessment.md b/.doc/ops/research/2026-09-15-module-decoupling-ipc-assessment.md new file mode 100644 index 00000000..90be56f2 --- /dev/null +++ b/.doc/ops/research/2026-09-15-module-decoupling-ipc-assessment.md @@ -0,0 +1,143 @@ +--- +title: "模块解耦与 IPC/RPC 交互现状评估(耦合点清点与进一步解耦方案)" +status: done +created: 2026-09-15 +updated: 2026-09-15 +author: Cline +confidence: high +--- + +# 调研:模块解耦与 IPC/RPC 交互现状评估 + +## 结论 + +1. **系统走的是"模块化单体"路线,而不是全量 IPC/RPC 微服务**——这是 [ADR 2026-09-14](../../adr/2026-09-14-modular-monolith-decoupling.md) 的明确决策(非目标:不拆微服务、不引入分布式事务协调器)。模块间解耦靠三件套:**消费者拥有的 Go 端口接口 + 唯一组合根(`internal/app`)装配 + PostgreSQL outbox 事件**。真正的跨进程边界只有三条:前端↔server(HTTP/SSE)、Go↔PI Agent Runner 容器(HTTP + Bearer)、规划中的独立事件 dispatcher 进程。 +2. **调用契约已大部分"机器可执行地"约定**:`.doc/system-design/module-boundaries.yaml` + `internal/archtest` 四类静态规则(import / event 两类已在生产门禁 `--mode block` 下清零;path 16 条、sql_write 5 条为带 owner/到期日的例外账本),外加 `events.Envelope` schema 1、`taskrun.RunManifest` schema 1、SSE transport、幂等协议(idempotency key + request digest;ETag = `head_snapshot_id:head_revision_no`)。 +3. **残留的强耦合集中在五处**:bizdev 门面兼 service locator、docgov↔docrevision 双向 import、chat/coordinator 各依赖 ~10 个包、进程内闭包 hook 桥(崩溃即丢、无幂等)、durable dispatcher 未接线(事件写进了 outbox 但没有消费者 worker 在跑)。另有 PI Runner stdout/result.json 旧协议与 HTTP 错误信封未统一两项已声明的 deferred 债。 + +对用户目标的直接回答:**"彻底解耦、一处修改不扩散"已大体达成**(import 违规为 0、单一写者、契约带版本与 SHA),**但"所有交互都走 IPC/RPC"并非本仓库的方向也不建议照搬**;错误排查能力的短板不在进程边界,而在事件链路只写不消费、闭包桥无审计痕迹。 + +--- + +## 1. 通信边界清点(谁和谁怎么交互) + +| 边界 | 机制 | 契约位置 | 状态 | +|---|---|---|---| +| Web 前端 ↔ server | HTTP `/api/v1/*`(~110 路由)+ SSE | `internal/server`(wire DTO)、`internal/stream/transport/sse.go` | ✅ 已约定;错误信封统一 deferred | +| Go ↔ PI Agent Runner 容器 | HTTP + `Bearer RUNNER_AUTH_TOKEN`;`/health`、`/v1/run`、`/v1/cancel`、`/v1/search`(`deploy/pi-agent/runner.mjs:319-405`) | backend allowlist(`internal/taskrun/backend.go`:stub/pi-agent/cursor);结果经 stdout/result.json | ⚠️ 传输已约定;**结果协议仍是 stdout/result.json**(整改计划明确 deferred 不本轮替换) | +| 模块 ↔ 模块(进程内) | Go 端口接口(消费者所有)+ `internal/app` 组合根注入 | `module-boundaries.yaml` imports allowlist + archtest | ✅ 机器可执行;bizdev/coordinator/chat 例外见 §3 | +| 领域事件 | PG outbox + envelope + 逐消费者 delivery + fencing token | `internal/events/envelope.go`(schema 1、digest、secret 扫描)、`pg.go`、`replay.go` | ⚠️ **写侧已落地、消费侧未接线**(见 §3.4) | +| 终态产物 | RunManifest(canonical JSON + SHA-256)+ FAV | `internal/taskrun/manifest.go`(schema 1) | ✅ U8 已合入 dev;FAV 分支未整支 merge(KTD12) | + +## 2. 契约约定情况(输入/输出/协议是否"约好") + +**已约定且可执行/可校验:** + +- 静态边界:`scripts/arch-check.sh --mode block` 通过(`block_kinds: [import, event]`,baseline import=0、event=0);例外必须有 owner/milestone/expires。 +- 事件信封:`EnvelopeSchema=1`,字段齐全(event_id/aggregate_type/version/correlation/causation/idempotency_key/payload_digest/producer),`ValidateEnvelope` 拒绝伪造 producer、digest 不符、payload 携带 secret。 +- 投递协议:`Delivery`(pending/leased/acked/dead/quarantine)+ lease + fencing token + `MaxAttempts=12` + aggregate version gap 等待(`events/bus.go`,PG 版 `SKIP LOCKED` 同语义)。 +- 幂等协议:TaskRun `Claim(idempotency_key, request_digest)` 同 key 异 digest 返回 `IDEMPOTENCY_CONFLICT`;文档 ETag/expected head。 +- 持久 schema:RunManifest v1(member kinds 枚举 + canonical 排序 + SHA);`configs/schemas/change-report-v1.json`。 +- 后端选择:`ResolveBackend` 服务端 allowlist,调用方不可注入任意字符串;`StripSecrets` 出站脱敏。 + +**未约定 / 弱约定:** + +- PI Runner 结果解析(stdout/result.json)——deferred。 +- HTTP 错误信封统一——deferred。 +- 闭包 hook(`OnOutbox`、`AddOnApprovedHook`)无 schema、无版本、无审计记录。 +- docflow 发事件的 `Environment: "dev"` 与 `ProjectID: 0` 硬编码(`internal/app/docflow/commands.go:167`),未从 Principal/命令注入——协议字段存在但值来源失真(test/main 环境下事件环境标注错误)。 + +## 3. 强/弱耦合点清点(附证据) + +### 3.1 强耦合:bizdev 门面 + service locator(最大枢纽) + +- ~4.8k LOC,被 **12 个生产包 import(78 处引用)**;自身 import 18 个内部包。 +- `bizdev.Service` 暴露 `Pool()/CAS()/DocGov()/DocRevision()/ProjectWorkflow()/FreeformDoc()/KB()/ContextManager()`(`service.go:229-260`)——**service locator 反模式**:任何拿到 `*bizdev.Service` 的包都能直取 `*pgxpool.Pool` 和其他模块的具体实现,绕过所有端口抽象。 +- `app/stageflow` 已开始绞杀(只依赖 bizdev),但 `coordinator.StageTaskRunner` 仍以 `bizdev.TaskRunRequest/Result` 为词汇(`engine.go:85-88`),即传输类型与门面绑死。 +- 影响:改 bizdev 任何签名会波及 chat/coordinator/taskrun/projectinstance/intent 等全部下游——正是"一处修改多处出问题"的活样本。 + +### 3.2 强耦合:docgov ↔ docrevision 双向 import + +- `docgov` imports `docrevision`,`docrevision` imports `docgov`(boundaries.yaml 双向 allowlist)。两个"单一写者"模块互相拿到对方类型,表所有权边界在类型层面没有闭合。 + +### 3.3 强耦合:chat / coordinator 扇出过大 + +- `chat` 依赖 11 个包(bizdev、coordinator、ctxmgr、docgov、intent、projectinstance、replyexport、squad、stream、taskrun…);`coordinator` 依赖 10 个包。U10 已完成 chatlog/contextasm 端口化,但 `internal/chat` 本体尚未收编进 `app/chatflow`。 +- `ctxmgr` 被 5 个包共享(56 处引用),是共享可变上下文状态的汇聚点。 +- `intent`(纯规则模块)依赖 `bizdev`——与架构文档 §3 的模块表(intent 只准依赖 vocabulary)直接矛盾,属于基线豁免债。 + +### 3.4 强耦合:进程内闭包桥替代事件消费(可靠性缺口) + +- 生产发布者只有两处:`app/docflow`(AgentCommit)与 `taskrun/finalize.go`(`events.PGSink{}.Append`,consumers 声明为 projection/fav/notify)。 +- **没有任何生产代码构造/运行 PG dispatcher**(全仓 grep `events.New|Dispatcher` 无命中;`MemoryBus` 仅 characterization 用;U6 规划的 `cmd/events/main.go` 不存在)。`Runtime.Start` 只跑 reminder loop 并绑定闭包。 +- 消费侧仍是闭包:`docnotify.Bind` 直接改写 `docrevision.Service.OnOutbox` 字段;`docgov.AddOnApprovedHook → app.go:180 onWorkflowDocumentApproved` 在组合根内联编排 projectinstance + projectworkflow + notification。 +- 后果:进程崩溃/重启时 hook 注册丢失、无 delivery 记录、无重试/死信/replay——设计目标 5(事件在崩溃、重复投递下可追踪可重试)**未达成**;这也正是错误排查最痛的盲区(事件有表可查,hook 无迹可寻)。 + +### 3.5 弱耦合(已在例外账本内、有到期日) + +- **path 16 条**:bizdev(4)/coordinator(4)/docgov(2)/docrevision(2)/server(3)/storage(1) 直接拼 `output.md`、`data/dev`、`workspaces/` 物理路径;U4 到期 2026-09-28,U5 到期 2026-10-12。 +- **sql_write 5 条**:生产写越界,按属主里程碑清零。 +- **server 适配层**:102 文件 / 17,211 LOC,allowlist 28 个包——架构上允许(server 是唯一传输适配层),但 `chatflow_http` 等编排仍在 server 内,U12 传输收口未完全做完。 +- **uow.AsPG unwrap owner**:已有 archtest 规则约束谁能解出 `pgx.Tx`,方向正确。 + +## 4. 进一步解耦方案(按优先级) + +### P0 正确性/可排查性(先做,收益最大) + +1. **接线 durable dispatcher,替换闭包桥(U6 收尾)**: + - 新增 `cmd/events` worker(claim → deliver → ack/dead-letter,`SKIP LOCKED` + fencing token 已有实现 `internal/events/pg.go`); + - 在组合根把 `projection`/`notify`/`fav` 注册为真实消费者,逐个删除 `docrevision.OnOutbox` 与 `docgov.AddOnApprovedHook` 闭包桥(U6 Approach 第 7 步原文); + - 验收即架构文档 §9.2 行为门禁:worker 崩溃/lease 过期/重复投递后副作用恰好一次。 + - 这一步同时解决"崩溃丢通知"和"排查无审计痕迹"两个问题。 +2. **修 docflow 事件字段注入**:`Environment/ProjectID` 从 Principal 与命令上下文取值,禁止硬编码 `"dev"`/`0`;可加 archtest 规则(production 禁止 `Environment: "dev"` 字面量出现在 envelope 构造)。 + + +### P1 结构解耦(消除"一处改、多处坏") + +3. **绞杀 bizdev**:删除 `Service` 上的 `Pool()/CAS()/ContextManager()` 等 locator 访问器,组合根直接装配具体服务;`ExecuteTask/ExecuteStage/InspectWorkspace` 全面移交 `app/stageflow`;`coordinator.StageTaskRunner` 改用 stageflow 自有请求/结果类型,切断对 `bizdev.*` 传输类型的依赖。目标:bizdev 退化为纯领域包(docduty/stage_access 等)或拆散归零。 +4. **打破 docgov↔docrevision 循环**:共享类型提升到 `contracts/vocabulary`(仅无行为值对象)或改为消费者端口;随后在 boundaries.yaml 中互相移出 allowlist 并转 block。 +5. **收编 `internal/chat`**:把剩余编排并入 `app/chatflow`,chat 包依赖从 11 收敛到端口集合。 +6. **解除 `intent → bizdev`**:与架构表对齐,intent 只依赖 vocabulary(现为豁免债)。 + +### P2 契约完善(对外接口收口) + +7. **path/sql_write 例外按期清零转 block**(2026-09-28 / 2026-10-12 到期,owner 已登记);到期未清零的按 archtest 语义直接红。 +8. **HTTP 错误信封统一**(deferred 项单独立项):typed `VERSION_CONFLICT`/`IDEMPOTENCY_CONFLICT` 已有领域错误,补 wire 映射规范。 +9. **PI Runner 协议 v2**(deferred 项单独立项):以 RunManifest schema 为基础定义 typed result 契约 + JSON Schema 入 `configs/schemas/`,替换 stdout/result.json 解析;保留 `/v1/run|cancel|search|health` + Bearer 传输不变。 +10. **事件 producer/consumer allowlist 落地为 yaml**(架构文档 §9.1 已规划):event name → producer/consumer 映射进 `module-boundaries.yaml`,archtest 校验,防运行时动态注册旁路。 + +### 关于"要不要全面 IPC/RPC 化"的总体建议 + +- **不建议**把单体内的端口调用改成 RPC:ADR 与 KTD1–KTD12 的判断在当前规模下成立——进程内端口 + archtest 门禁 + 带版本契约已实现"改一处不扩散"(import 违规 0、单一写者、schema 版本 + SHA 校验),RPC 只会引入序列化、部分失败、延迟与运维成本。 +- **值得保持/推进的进程边界**恰好是三条已有/已规划的:PI Runner(已独立容器 + HTTP 契约)、durable dispatcher 独立进程(`cmd/events`,本次 P0-1)、FAV 发布 worker(U7 后续)。 +- 若未来确需拆服务,先决条件清单即 P2 三项:错误信封、PI 协议 schema 化、事件 allowlist——完成后每条进程边界都有显式契约。 + +## 证据 + +| 项 | 路径/命令 | 结果 | +|----|-----------|------| +| 架构决策 | `.doc/adr/2026-09-14-modular-monolith-decoupling.md`、`.doc/system-design/2026-09-14-module-decoupling-architecture.md` | 模块化单体;非目标含"不拆微服务" | +| 静态门禁 | `scripts/arch-check.sh` + `internal/archtest` | `--mode block` 通过;import/event=0;path=16、sql_write=5 例外 | +| 里程碑状态 | `.doc/status/FEATURE-MATRIX.md:30`、CURRENT | U5–U12 已合入 `dev@bf59cce`(schema 38);path/sql_write 例外保留 | +| PI Runner 协议 | `deploy/pi-agent/runner.mjs:81-89,319-405` | Bearer 鉴权 + 4 端点 | +| 事件信封/投递 | `internal/events/envelope.go`、`bus.go`、`pg.go` | schema 1 + fencing + inbox;无生产 dispatcher 装配点 | +| 闭包桥 | `internal/app/docnotify/subscriber.go:30-36`、`internal/docgov/engine.go:39-53`、`internal/app/app.go:170-208` | OnOutbox / AddOnApprovedHook 进程内直绑 | +| bizdev locator | `internal/bizdev/service.go:229-260` | Pool/CAS/DocGov 等 8 个访问器 | +| 扇出统计 | `grep` import 扫描(生产包) | bizdev 被 12 包 78 处引用;chat 11 包;coordinator 10 包;ctxmgr 5 包 56 处 | +| 双向依赖 | `module-boundaries.yaml` | docgov↔docrevision 互在 allowlist | +| 事件字段硬编码 | `internal/app/docflow/commands.go:160-177` | `Environment:"dev"`、`ProjectID:0` | +| RunManifest | `internal/taskrun/manifest.go` | schema 1 + canonical + SHA-256 | + +## 开放问题 + +- `projection`/`fav` 两个已声明的 outbox 消费者当前没有任何注册实现——dispatcher 接线时需先确认 projection subscriber(workspace 投影重建)归属哪个用例包。 +- FAV 分支 `feature/final-artifact-vault` 未整支合入(KTD12 要求先审后用);U7 降级读取依赖它,需单独排期审计。 +- `*pgxpool.Pool` 跨包传递(bizdev.Pool() 暴露)是否已被 archtest 覆盖未见显式规则;绞杀 bizdev 时应同步补规则,防止 locator 以新形式回潮。 + +## 建议动作 + +1. 立项 P0-1(durable dispatcher 接线 + 闭包桥下线)为下一个 short-term 任务,占 `events`/`app` mutex。 +2. P0-2 可作为小修复随 P0-1 同批提交。 +3. P1-3(bizdev 绞杀)建议拆为多支 short-term,先从删除 locator 访问器开始(编译器会列出全部受影响点,天然的工作清单)。 +4. P2 三项各走独立 remediation 计划(与既有 deferred 清单一致,避免与在途任务抢 mutex)。 +