Files
Agent-System/.doc/system-design/2026-09-20-module-development-standards.md
WeiCHEN 381b56dafc refactor(docgov): converge shared lifecycle vocab into contracts
Introduce internal/contracts/vocabulary for state/role/error sentinels,
alias through docgov, and retarget hot production consumers so
coordinator no longer imports docgov for vocabulary-only use.
2026-09-22 21:44:57 +08:00

151 lines
10 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
title: "模块开发规范:通信契约、接口与故障隔离"
status: active
created: 2026-09-20
updated: 2026-09-20
---
# 模块开发规范:通信契约、接口与故障隔离
> 本文件是开发者入口,不是新的架构 SSOT。
> 决策仍以 [模块化单体 ADR](../adr/2026-09-14-modular-monolith-decoupling.md) 为准。
> 机器强制仍以 [module-boundaries.yaml](./module-boundaries.yaml) 与 `./scripts/arch-check.sh --mode block` 为准。
> 影响面守则不在此重复,见 [partial-deferred 计划 §2](../remediation/2026-09-16-partial-deferred-completion-plan.md)。
2026-09-20 确认:继续模块化单体。模块之间用进程内窄端口或 PostgreSQL 领域事件通信。不新增通用 IPC/RPC,不拆微服务。跨进程只允许两类既有边界:HTTP(Web↔server、server↔PI Runner)和 `cmd/events` 经 PG lease 消费。
## 1. 问题必须停在本模块
一次故障只允许表现为下面之一,不允许连带改第二个领域包才能编译或才能回滚数据:
| 故障位置 | 允许看见的影响 | 禁止 |
|----------|----------------|------|
| 领域不变量 | 本包返回 typed error;同一 UnitOfWork 回滚 | 别的领域包 import 本包实现后自行改表 |
| 用例编排 | 组合根换一个端口实现 | handler 直接 new 领域服务或拿 `*pgxpool.Pool` 写别人的表 |
| 异步副作用 | 该消费者 delivery 重试、死信或 quarantine | 一个消费者失败卡住其他消费者,或在持锁时调用 LLM/HTTP |
| 对外协议 | server 把领域错误映射成固定错误信封 | 把 SQL、路径、prompt、正文放进 HTTP message |
静态底线已经在门禁里:领域实现互不 import(消费端口和稳定词汇除外)、每张表一个写者、每个事件名有生产者/消费者 allowlist。
## 2. 三种通信契约
只允许这三种。新增第四种(包括模块间 gRPC、进程内全局 bus 单例、跨包直接调未导出帮助函数)先写 ADR。
### 2.1 进程内窄端口
消费者拥有接口,组合根注入实现。参照 `internal/app/chatflow/ports.go`:接口只表达这一个用例要的能力。
| 规则 | 说明 |
|------|------|
| 谁定义 | 调用方包,不放在被调用的领域包里“顺便导出” |
| 宽度 | 一个用例一组方法。三个以上消费者语义相同时才提升为公共端口 |
| 禁止万能接口 | 不得出现聚合所有模块方法的 `Store` / `Engine` 门面 |
| 禁止传输细节 | 端口结构体不带 JSON tag,不返回 `http.Response`、SQL 行、`pgx.Tx` |
| 身份 | `Principal` 只来自已认证会话或服务身份,不从请求体覆盖 UserID/ProjectID |
| 装配 | 只有 `internal/app` 与 `cmd/*` 构造实现。`internal/server` 只拿用例或端口 |
### 2.2 领域事件(跨进程总线)
生产路径是 PostgreSQL outbox + `cmd/events` worker(`FOR UPDATE SKIP LOCKED`),不是内存 `MemoryBus`。`MemoryBus` 只用于 dispatcher 特征测试。
契约只有一处运行时副本:`internal/events/registry.go` 的 `StaticContracts()`,并且必须与 YAML 的 event 段 parity。未登记的事件名或消费者在构建期失败。`fav.artifact_staged` 保持 `Enabled: false`,直到独立 FAV 计划放量。
信封至少包含:`event_id`、`event_name`、`schema_version`、`environment`、`project_id`、aggregate 标识与版本、`actor_ref`、`correlation_id` / `causation_id`、`idempotency_key`、`payload_digest`、`occurred_at`。
事件不得携带 secret、token、prompt、正文或绝对路径。大 payload 先冻结为不可变引用,事件只留 digest。
投递是至少一次。消费者用唯一键或 inbox receipt 保证对外只看见一次结果。副作用与 receipt 能进同一数据库事务的必须同事务。永久失败进 dead letter,不得阻塞其他订阅者。
### 2.3 对外 wire DTO
HTTP/SSE 的请求响应只存在于 `internal/server`(及 `internal/stream` 的传输类型)。领域包不 import server。
错误信封沿用已合入的 typed envelope:`code` / `message` / `request_id` / 可选 `details`。`message` 用固定安全文案;`details` 只允许该 code 的公开字段。授权拒绝不得泄露资源是否存在。
PI Runner 继续 typed-result-v1。typed 字段存在但校验失败必须 fail closed,不得降级成旧解析。
## 3. 输入与输出
### 3.1 新增或修改端口时的 checklist
- [ ] 接口放在消费者用例包,方法名是业务动作而不是表操作。
- [ ] 入参是该动作的命令/查询结构;出参是结果或本用例的 typed error。
- [ ] 无 JSON tag,无 `pgx` 类型,无物理路径,无另一领域包的具体 struct(稳定词汇除外)。
- [ ] 错误值留在领域包或用例包;server 只做 `errors.Is` 到 HTTP code 的映射,映射表集中在一处,不散落在每个 handler。
- [ ] 组合根测试用假实现替换端口,不 import 对方的 SQL。
### 3.2 新增事件时的 checklist
- [ ] `event_name` 形如 `<aggregate>.<fact>`,表达已发生的事实,不是命令。
- [ ] `schema_version` 从 1 起;破坏性字段变更升版本,未知 major fail closed。
- [ ] payload 字段写入 `StaticContracts().PayloadFields`,并同步 YAML allowlist。
- [ ] 生产者、消费者包名写入同一条契约;运行时注册必须是静态清单的子集。
- [ ] 领域提交、envelope、delivery 行在同一个 UnitOfWork。缺 `environment` 或 `project_id` 时 fail closed,禁止写死 `"dev"` / `0`。
- [ ] 消费者有幂等键;重复投递测试证明只有一个可见结果。
- [ ] `./scripts/arch-check.sh --mode block` 与 registry parity 测试通过。
### 3.3 稳定词汇
无行为的枚举和值对象才可以进共享词汇。带解析、哈希、路径拼接的函数留在所属模块(现例:`internal/doccontent`),禁止塞进 vocabulary。
跨包状态/角色/哨兵错误使用 `internal/contracts/vocabulary`(批次 C 决策,见 [short-term](../ops/short-term/2026-09-22-docgov-vocabulary-convergence.md))。不要新增对 `docgov.State*` / `docgov.Role*` / `docgov.Err*` 的跨包引用;`docgov` 仅保留领域别名与结构化错误类型。
## 4. 故障隔离红线
1. **表唯一写者**。`INSERT/UPDATE/DELETE` 只能出现在 YAML `table_owners` 指定的包。跨表原子性由用例打开的 UnitOfWork 提供,不是每个模块各开事务。
2. **解包事务的包有名单**。只有登记过的属主可以从 `uow.TxScope` 取出 `pgx.Tx`。server 与 chat 不在此列。
3. **领域实现互不 import**。需要对方能力时定义端口,由 `internal/app` 注入。用子包绕开编译环但保留反向 import,视为未解耦。
4. **持锁期间不做外部 IO**。LLM、PI Runner、对象存储、HTTP、SSE、大文件复制放在提交之后,由 outbox 驱动。
5. **事件消费者 fail closed**。授权失败、环境不符、schema 未知 → quarantine,不当成瞬时错误重试。FAV 消费者保持禁用。
6. **HTTP 不修投影**。GET 不得写数据库或覆盖 head。修复是独立命令并写审计。
7. **单文件变更面**。新生产 Go 文件不超过 800 行。已有超过 1000 行的文件(见审计热点 5)只允许在对应整改批次里拆,不借无关功能 PR 顺手重排。
## 5. 新增功能怎么落
按顺序做。跳过门禁视为未完成,即使功能在本地可用。
### 5.1 新模块
1. 在 short-term 写明唯一写入所有权(表、文件或无)。
2. 在 `module-boundaries.yaml` 的 `imports` 里只放允许依赖的包。默认可依赖 config、稳定词汇、本模块 repository。不可依赖另一个领域实现。
3. 组合根负责构造。server 若需要该能力,只能通过已有用例端口。
4. FEATURE-MATRIX 加行,mutex 与 CURRENT 认领一致。
### 5.2 新表
1. Goose 迁移只 additive。
2. YAML `table_owners` 登记唯一写者。
3. 跨表不变量写在用例的一个 UnitOfWork 里,并写明锁顺序:project/workflow → 聚合根 → version/turn → task run → outbox。
4. 不允许 server handler 出现该表的 SQL。
### 5.3 新事件或新消费者
走 §3.2。消费者状态是 `off | shadow | live`,按环境 + 消费者配置,不靠单个进程的环境变量偷偷打开。shadow 只写对账,不写业务副作用。FAV 不在此流程里放量。
### 5.4 改已有模块
1. 先看 [耦合审计](../audit/2026-09-20-module-coupling-audit.md) 该包是否已有 P1/P2。有则优先缩小依赖,而不是再增加一个具体类型字段。
2. 影响面守则(partial-deferred §2.2)仍然有效:深模块、禁止新增跨模块 import、schema additive、一能力一 flag、事件双登记、改声明范围要留 before/after。
3. 例外若必须保留,写入 YAML `exceptions` 时带 owner、原因、范围、到期日和移除里程碑。禁止无到期日的例外。本轮不新增例外来掩盖热点 1–4。
## 6. 与现有文档的分工
| 问题 | 读这里 | 不要在本文件找 |
|------|--------|----------------|
| 为什么是模块化单体 | ADR | 实施波次 |
| 事务、锁、FAV、授权模型 | [架构设计](./2026-09-14-module-decoupling-architecture.md) | 今日残留热点 |
| 事件状态机与 Track E/S/P | [Phase 2 计划](../remediation/2026-09-15-module-decoupling-ipc-phase2-remediation-plan.md) | 新批次 A–G |
| 改功能时如何收敛影响面 | [partial-deferred §2](../remediation/2026-09-16-partial-deferred-completion-plan.md) | 端口字段清单 |
| 今天还有哪些耦合 | [耦合审计](../audit/2026-09-20-module-coupling-audit.md) | 发布门禁 |
| 接下来哪一批先改 | [分批整改](../remediation/2026-09-20-module-decoupling-batch-plan.md) | 代码 |
## 7. 提交前自检
```bash
./scripts/arch-check.sh --mode block
go vet ./...
go test -short ./...
```
改了事件契约时,额外确认 `TestRegistryParityWithYAML` 在跑到的包里通过。普通工程文档变更按 [验证契约](../ops/2026-09-21-validation-contract.md) 检查差异、链接和状态;不运行无关 Go/PI 套件。运行模板、提示词与代码契约变更按受影响范围运行上述门禁,防止索引或生成物被误改。