Keep the modular-monolith baseline and name the seven places where a fault still crosses a module boundary. Co-authored-by: Cursor <cursoragent@cursor.com>
9.3 KiB
title, status, created, updated, severity
| title | status | created | updated | severity |
|---|---|---|---|---|
| 模块耦合现状审计(规范与分批整改基线) | done | 2026-09-20 | 2026-09-20 | high |
模块耦合现状审计(规范与分批整改基线)
现象
系统已经是模块化单体,不是未治理的大泥球:module-boundaries.yaml + internal/archtest 已把 import 与事件登记清到零违规(baseline_counts.import=0、event=0,二者在 block_kinds),可靠事件由独立 cmd/events worker 经 PostgreSQL lease 消费。
故障仍会跨模块扩散的原因不在“缺总线”,而在四类残留:
- HTTP 适配层直接持有领域具体类型,而不是只调用用例端口。
chat、app子包与coordinator用子包拆分避开了 Go 编译环,逻辑上仍互相依赖。docgov的状态、角色和错误被多个模块当成本模块词汇使用。- 带到期日的 SQL/路径例外仍允许非属主写入或拼接工作区路径。
本审计只记录证据与分级,不改代码。整改批次见 分批计划。
根因分析
已有隔离基线(不要推倒)
| 机制 | 位置 | 现状 |
|---|---|---|
| 模块化单体 ADR | 2026-09-14-modular-monolith-decoupling | 不拆微服务;窄端口 + 组合根 + 领域事件 |
| 静态边界 | module-boundaries.yaml | import allowlist、表唯一写者、路径属主、事件生产者/消费者 |
| 架构门禁 | internal/archtest、scripts/arch-check.sh --mode block |
import/event 零容忍 |
| 进程内端口 | internal/app/*flow(例:internal/app/chatflow/ports.go) |
消费者拥有窄接口;组合根注入实现 |
| 跨进程事件 | internal/events + cmd/events |
PG outbox/inbox;dev :8088 / test :8089 / main :8090 |
| 事件契约 | internal/events/registry.go StaticContracts() |
11 个事件名、schema version、payload 字段 allowlist;与 YAML parity |
StaticContracts 现有事件:document.agent_candidate_ready、document.agent_version_committed、document.downstream_impact_detected、document.change_report_degraded、document.approved、taskrun.completed、taskrun.cancelled、fav.artifact_staged(Enabled: false)、intent.plan.awaiting_input、intent.plan.step_changed、intent.plan.completed。
热点 1(P1):server 上帝适配层
internal/server/server.go 的 Server(约 40–80 行)同时持有 *bizdev.Service、*coordinator.CoordinatorEngine、*docgov.GovernanceEngine、*docrevision.Service、*taskrun.Manager、*projectinstance.Service、*pgxpool.Pool 等具体类型。同结构体里只有少数字段是端口(chatflow.ChatLog、chatflow.ContextAssembler、intentflow.Service、intent.Recognizer)。
生产 import 覆盖约 30 个内部包(server.go 11–34 行)。handler 直接解释领域错误,例如 internal/server/handlers.go 对 docgov.ErrDocumentBusy 的分支(426、449 行)以及 docgov.RoleAuthor(702、1291 行)。
后果:领域包改一个错误或角色常量,HTTP 层必须跟着改;pool 还让适配层有机会绕过表属主写 SQL。
热点 2(P2):chat ↔ app ↔ coordinator 逻辑环
编译环被子包拆开了,依赖方向没有:
internal/chat/session.go11–13 行 importapp/chatflow、app/intentflow、coordinator。internal/chat/action_executor.go10–14 行 importapp/intentflow、app/stageflow、coordinator、docgov、taskrun,并持有具体引擎指针。internal/app/chatflow/ports.go又 importcoordinator、ctxmgr、projectinstance(AssembleRequest直接嵌coordinator.UserInfo)。
chat 既是领域会话,又反向依赖用例包。用例一变,会话执行器就会编译失败,问题不会停在单一模块。
热点 3(P3):docgov 词汇泄漏
生产代码(不含本包测试)直接使用 docgov 状态/角色/错误的位置包括:
internal/coordinator/engine.go:ErrDocumentBusy(955)、StateDrafting(1596、1598)。internal/bizdev/service.go:ErrDocumentLocked/ErrDocumentBusy(718)。internal/server/handlers.go、document_revision_handlers.go、auth_helpers.go:RoleAuthor、ErrDocumentBusy。internal/chat/action_executor.go:动作名字符串"docgov.review_approve"(58、150 行),以及docgov.StateDrafting(90)。
这些不是稳定共享词汇(internal/contracts/vocabulary 尚未承接它们),而是领域实现类型被当公共 API。
热点 4(P3):工作流定义与实例网
internal/workflowdef/catalog.goimportprojectworkflow。internal/workflowdef/validator_v2.goimportworkflow。internal/projectinstance生产文件(store.go、service.go、gate.go、types.go等)importworkflowdef。internal/workflow/runtime/orchestrator.goimportprojectinstance。
定义、目录、实例和运行时互相看得见对方的包。改一份工作流校验会同时碰到定义包和实例包。
热点 5(P4):巨型文件
非测试生产文件行数(2026-09-20 wc -l 口径,探索盘点):
| 行数 | 文件 |
|---|---|
| 1844 | internal/coordinator/engine.go |
| 1590 | internal/bizdev/service.go |
| 1395 | internal/server/handlers.go |
| 1291 | internal/projectinstance/store.go |
| 1245 | internal/intent/recognizer.go |
| 1235 | internal/projectworkflow/store.go |
| 1207 | internal/docgov/engine.go |
| 1172 | internal/docrevision/service.go |
| 1151 | internal/squad/workspace.go |
| 1028 | internal/freedoc/service.go |
单文件过大本身不是耦合,但它把多个用例绑在同一变更面上,评审和回滚半径变大。拆分应跟在解环之后,避免边搬边改依赖。
热点 6(P4):前端 API 层无统一客户端
web/src/api/ 约 37 个文件按域各写 fetch(),API_BASE 与鉴权头分散。错误信封、重试和 401 处理无法在一处收口,后端错误契约一变就会在多个 api 文件重复漏改。这是适配层问题,不是把前端改成 RPC。
热点 7(P2,有到期日):边界例外未清零
module-boundaries.yaml exceptions 共 21 条,baseline_counts 为 sql_write 5、path 16。
| 种类 | 条数 | 里程碑 | 到期 | 风险 |
|---|---|---|---|---|
sql_write |
4 | U3 | 2026-09-28 | 8 天后到期。docduty、docrevision 仍写 document_records / freeform_documents |
sql_write |
1 | U8 | 2026-10-26 | projectinstance 终止流程直接取消 task_runs |
path |
2 | U4 | 2026-09-28 | 同样 8 天后到期。server 与 docrevision 仍点名 output.md |
path |
14 | U5 | 2026-10-12 | agent、bizdev、coordinator、docgov、storage 仍拼接工作区路径或把 data/dev 写进提示 |
到期后若门禁把这些 kind 升为 block 而代码未迁走,构建会失败;若继续延期而不迁,非属主写入会继续让文档/任务故障跨包扩散。本轮不改 YAML,清零归整改批次 G。
严重度
整体 high:不会单独打穿已签发布门禁(production_ready 不因本审计回退),但 P1/P2 使“问题停在单一模块”尚未成立。
| 级别 | 热点 | 扩散方式 |
|---|---|---|
| P1 | server 具体类型装配 | 领域改动直接编译失败到 HTTP |
| P2 | chat/app/coordinator 逻辑环 | 会话、意图用例、协调引擎必须同改 |
| P2 | 2026-09-28 到期的 sql_write/path 例外 | 非属主写表或读工作区文件 |
| P3 | docgov 词汇泄漏 | 状态机常量变成跨包协议 |
| P3 | workflow 定义/实例网 | 校验与运行时同改 |
| P4 | 巨型文件、前端 fetch 分散 | 变更面大,不立刻产生跨表写入 |
关联整改
- Remediation: 2026-09-20-module-decoupling-batch-plan.md
- 开发规范: 2026-09-20-module-development-standards.md
- 上位约束(不重复制定): 架构、Phase 2、影响面守则 §2
证据
- 结构体与 import:
internal/server/server.go、internal/chat/session.go、internal/chat/action_executor.go、internal/app/chatflow/ports.go。 - 事件契约:
internal/events/registry.goStaticContracts。 - 例外账本:
module-boundaries.yamlexceptions与baseline_counts(import 0 / sql_write 5 / path 16 / event 0)。 - 盘点方式:2026-09-20 只读检索生产 import 与
docgov符号引用;行数来自当时文件长度,后续拆分后以wc -l重测为准。
开放问题
- U3/U4 例外在 2026-09-28 到期后,是先延长豁免并写原因,还是立刻占用
docgovmutex 做批次 G 的第一刀。本审计不代决,批次 G 开工前必须二选一,禁止静默改到期日。 internal/contracts/vocabulary在架构文档中已定义,代码侧尚未作为 docgov 状态/角色的唯一出口。批次 C 再决定是新建该包还是把稳定枚举留在消费者端口。