Consolidate approved policies and add local lease, document CAS, commit, candidate and environment gates. Preserve conservative recovery and cleanup limits.
9.9 KiB
title, status, created, updated
| title | status | created | updated |
|---|---|---|---|
| 模块开发规范:通信契约、接口与故障隔离 | active | 2026-09-20 | 2026-09-20 |
模块开发规范:通信契约、接口与故障隔离
本文件是开发者入口,不是新的架构 SSOT。 决策仍以 模块化单体 ADR 为准。 机器强制仍以 module-boundaries.yaml 与
./scripts/arch-check.sh --mode block为准。 影响面守则不在此重复,见 partial-deferred 计划 §2。
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。
在批次 C 完成前,不要新增对 docgov.State* / docgov.Role* / docgov.Err* 的跨包引用。已有引用记在 耦合审计,按整改批次收敛。
4. 故障隔离红线
- 表唯一写者。
INSERT/UPDATE/DELETE只能出现在 YAMLtable_owners指定的包。跨表原子性由用例打开的 UnitOfWork 提供,不是每个模块各开事务。 - 解包事务的包有名单。只有登记过的属主可以从
uow.TxScope取出pgx.Tx。server 与 chat 不在此列。 - 领域实现互不 import。需要对方能力时定义端口,由
internal/app注入。用子包绕开编译环但保留反向 import,视为未解耦。 - 持锁期间不做外部 IO。LLM、PI Runner、对象存储、HTTP、SSE、大文件复制放在提交之后,由 outbox 驱动。
- 事件消费者 fail closed。授权失败、环境不符、schema 未知 → quarantine,不当成瞬时错误重试。FAV 消费者保持禁用。
- HTTP 不修投影。GET 不得写数据库或覆盖 head。修复是独立命令并写审计。
- 单文件变更面。新生产 Go 文件不超过 800 行。已有超过 1000 行的文件(见审计热点 5)只允许在对应整改批次里拆,不借无关功能 PR 顺手重排。
5. 新增功能怎么落
按顺序做。跳过门禁视为未完成,即使功能在本地可用。
5.1 新模块
- 在 short-term 写明唯一写入所有权(表、文件或无)。
- 在
module-boundaries.yaml的imports里只放允许依赖的包。默认可依赖 config、稳定词汇、本模块 repository。不可依赖另一个领域实现。 - 组合根负责构造。server 若需要该能力,只能通过已有用例端口。
- FEATURE-MATRIX 加行,mutex 与 CURRENT 认领一致。
5.2 新表
- Goose 迁移只 additive。
- YAML
table_owners登记唯一写者。 - 跨表不变量写在用例的一个 UnitOfWork 里,并写明锁顺序:project/workflow → 聚合根 → version/turn → task run → outbox。
- 不允许 server handler 出现该表的 SQL。
5.3 新事件或新消费者
走 §3.2。消费者状态是 off | shadow | live,按环境 + 消费者配置,不靠单个进程的环境变量偷偷打开。shadow 只写对账,不写业务副作用。FAV 不在此流程里放量。
5.4 改已有模块
- 先看 耦合审计 该包是否已有 P1/P2。有则优先缩小依赖,而不是再增加一个具体类型字段。
- 影响面守则(partial-deferred §2.2)仍然有效:深模块、禁止新增跨模块 import、schema additive、一能力一 flag、事件双登记、改声明范围要留 before/after。
- 例外若必须保留,写入 YAML
exceptions时带 owner、原因、范围、到期日和移除里程碑。禁止无到期日的例外。本轮不新增例外来掩盖热点 1–4。
6. 与现有文档的分工
| 问题 | 读这里 | 不要在本文件找 |
|---|---|---|
| 为什么是模块化单体 | ADR | 实施波次 |
| 事务、锁、FAV、授权模型 | 架构设计 | 今日残留热点 |
| 事件状态机与 Track E/S/P | Phase 2 计划 | 新批次 A–G |
| 改功能时如何收敛影响面 | partial-deferred §2 | 端口字段清单 |
| 今天还有哪些耦合 | 耦合审计 | 发布门禁 |
| 接下来哪一批先改 | 分批整改 | 代码 |
7. 提交前自检
./scripts/arch-check.sh --mode block
go vet ./...
go test -short ./...
改了事件契约时,额外确认 TestRegistryParityWithYAML 在跑到的包里通过。普通工程文档变更按 验证契约 检查差异、链接和状态;不运行无关 Go/PI 套件。运行模板、提示词与代码契约变更按受影响范围运行上述门禁,防止索引或生成物被误改。