Files

101 lines
6.3 KiB
Markdown
Raw Permalink Normal View History

# 代码分层与依赖方向
> **目的**:让"改一个模块"只需要读 **索引 + 相关模块**,而不是读全仓。
> **背景与实测数据**:`项目代码_分层范式与迭代风险评估_20260916.md`(含风险分档与"改一处读全仓"的量化评估)。
> **状态**:2026-09-16 立。当前**只是把规则写下来**——本仓**不需要重构**,需要的是别在扩张期把方向走丢。
---
## 1. 四层与依赖方向
```
① 入口层 src/cli.ts · src/web/server.ts · src/web/routes/* · src/web/middleware/*
② 领域层 src/domain/* ← 【新增,当前为空】
③ 能力层 src/supervisor · db · fs · worker · net · nginx
④ 基础层 src/config.ts · crypto.ts · isolation.ts · index.ts
```
### 依赖方向:`① → ② → ③ → ④` **单向,⛔ 不得回指**
| # | 规则 | 判据(怎么算违反) |
|---|---|---|
| **R1** | 上层可依赖下层;**下层不得 import 上层** | 在 `④` 的文件里出现 `from '../web/...'` 之类的**子目录 import** = 违反 |
| **R2** | 同层之间允许,但**新增同层依赖前先问一句"它到底属哪一层"** | 说不清的依赖,通常意味着**该有一条领域层接口** |
| **R3** | `net/*` 是**能力层的一员**(与 `supervisor`/`db`/`fs` 同级),⛔ **不是跨层的特权模块** | `net/*` 不得 import `web/*`;`web` 可以依赖 `net` |
| **R4** | `src/{config,crypto,isolation,index}.ts` **零业务依赖** | 这四个文件出现任何子目录 import = 违反(当前**成立**,见 §3) |
---
## 2. 三条工程纪律
**2.1 契约前置** —— 新增模块**先出接口与类型文件**,再写实现。
> 范例:`src/net/reachability.ts` + `src/net/rendezvous.ts`(覆盖网络 S0)。
> 做法 = **纯新增文件 + 零行为变化**,验收靠 `test/reachability.test.mjs` 把**现网真实数据写死为判据**(不靠肉眼)。
**2.2 模块头注释 = 索引** —— 每个模块目录的入口文件,头注释必须写明三件事:
> **职责 / 依赖谁 / 被谁依赖**。
> 本仓现有头注释质量已经很高(是"读索引就能定位"的基础),缺的只是把这三项**变成必须写的字段**。
> 效果:把"读全仓"降级为"读索引 + 读相关模块"。
**2.3 动手前的自查** —— 问两句:
> ① 这次改动涉及哪几层?② 有没有从下往上(下层 import 上层)的引用?
---
## 3. 现状实测(2026-09-16)
**规模**:`src` **58 个 .ts / 14,053 行**。`web` 占 40%(5,593 行)⇒ 入口层事实上承担了业务层职责。
**🔴 一处勘误**(早期统计有误,以此处为准):
> 曾有结论称「有 **3 处基础层反向依赖**(`(root)→web` 2 + `(root)→db` 1),趁早清掉」。
> **实测不成立** —— 它把 `src/cli.ts` 误算进了"基础层"。`cli.ts` 是**入口层①**,它 import `db`/`fs`/`web` 属 **① → ③ 的合法方向**。
> 证据:`grep -n "from '\./\(web\|db\|fs\|supervisor\|worker\|net\)/" src/*.ts` ⇒ **只命中 `cli.ts`**;`config.ts` / `crypto.ts` / `isolation.ts` / `index.ts` **零 import**。
> ⇒ **没有可清的反向依赖;R4 当前已经成立。**
**真正的成本不在反向依赖,在"边界模糊"**:
`web/routes/business-plugins.ts`(743 行)· `skills.ts`(687 行)把业务规则写在路由里,规则的另一半在 `supervisor/orchestrator.ts`(1,259 行)与 `db/repo.ts`(752 行)
⇒ **改一条规则要连读 3 个大文件 ≈ 2,500 行**。这才是该被 §4 第 1 条解决的。
---
## 4. 未做(按优先级)
| 优先级 | 事项 | 为什么排这个位置 |
|---|---|---|
| **P0(本文件)** | 把四层规则写下来 + 在 `README.md` 文档表登记 | 零代码风险;**扩张期一旦走丢,回头改的成本高得多** |
| **P1** | 补 `src/domain/*`:把 `web/routes` 的业务规则抽出来 | ⚠️ **行为敏感重构**(不是纯类型);影响面 **>10 文件** ⇒ **动工前先出清单** |
| **P2** | 按 §2.2 给各模块入口补「职责 / 依赖谁 / 被谁依赖」 | 批量改 >10 文件,同样先出清单 |
> **顺序建议**:P0 ✅ → 覆盖网络 S1–S4 → P1。
> P1 **不阻塞**覆盖网络;反过来,覆盖网络的新模块(`net/*`)正好是 §2.1「契约前置」的第一个样板。
---
## 5. 可执行判据(✅ 2026-09-16 补 —— 规则从此**不靠自觉**)
```bash
npm run check:layering # = node scripts/check-layering.mjs
```
- `scripts/check-layering.mjs` 静态扫描 `src/**/*.ts`,按 §1 四层判 **R1 / R3 / R4**;未归类的目录会被顶出来(逼你定层)。
- **ratchet(棘轮,不是豁免)**:存量违规登记在 `scripts/layering-baseline.json`,键 = 「源→目标」的**边**(不含行号 ⇒ 抗行号漂移)。
· **新增**违规 ⇒ **退出码 1**(⛔ 不得引入);· 基线内**已消除**项 ⇒ 打印提醒,`--update-baseline` 收紧(**只许减**)。
- 已挂进 **`npm run verify`**(`build` 之后、单测之前)。
### 5.1 首跑实测(2026-09-16 · 60 个 .ts)
| 层 | ①入口 | ②领域 | ③能力 | ④基础 | 未归类 |
|---|---|---|---|---|---|
| 文件数 | 24 | **0** | 32 | 4 | 0 |
**现存违规 5 条**,全部是 `③能力 → ①入口` 的反向依赖(= §3 那句「4 组双向依赖」的内核):
| 源 | 目标 | 修法方向 |
|---|---|---|
| `supervisor/proxy.ts` | `web/auth.ts`(`hashSessionToken` / `parseCookie`) | 纯函数 ⇒ **下沉到基础层** |
| `supervisor/proxy.ts` | `web/middleware/authn.ts`(`requireAuth`) | 会话校验是**能力**不是入口 ⇒ 下沉到能力层 |
| `fs/local-user-fs.ts` · `fs/remote-user-fs.ts` · `fs/workspace.ts` | `web/middleware/fs-guard.ts` | 路径守卫 = **领域规则** ⇒ 归 `src/domain/`(或 `src/fs/`) |
> ⚠️ **与 §3 勘误不矛盾**:§3 证伪的是「**基础层**(④)有 3 处反向依赖」⇒ 那部分确实 **0 处**;本节这 5 条是**能力层→入口层**(R1),是另一件事。
> ⚠️ 这 5 条**未整改**(命中 R7:行为敏感重构且影响 >10 文件 ⇒ 先出清单)⇒ 先建基线,让"**不新增**"立刻生效。
> 📌 **机制已兑现价值**:脚本首跑就抓出两件我肉眼漏掉的事 —— ① 完整的 R1 检查不是 0 违规(有 5 条);② 我自己漏定了 `src/nginx/` 的层归属。