# project-nav 架构(v0.10.4 · 树 + 图) > **上位约束**:所有开发动作必须从架构出发。动手前能说出这次改动落在哪个架构节点;说不出 = 还没做架构思考,十有八九是局部补丁。 > > 本文是**架构契约**,不是说明文档。改架构先改它。生成物永不可手写——手写即被下次渲染覆盖(I2)。 --- ## 1. 一句话架构 > **一条 append-only 事件流(唯一事实源)→ 一个由它折叠出的架构模型(可丢弃缓存)→ 一层渲染投影。** > > **模型 = 包含树(谁属于谁)+ 依赖图(谁引用谁)。** > **依赖图由磁盘 import 派生——不登记、不手写、不会过期。** > > **闸门是对模型的查询;产出是模型的重渲染。** ## 2. 治理面最小化(第一性判据) > **会因代码变更而"过期"的文档,不该是文档 —— 该是投影。** 投影 = 零手写、随时重生、**没有"过期"这回事**(因为没人维护它)。于是手写面只剩两样: | 面 | 载体 | 何时改 | |---|---|---| | **契约** | `ARCHITECTURE.md` · `AGENTS.md` | 只在架构换代 / 纪律变更时 | | **投影** | `PROJECT.md` 标记区 · `.internal/ARCH-MODEL.md` · 地图 · **`nav_graph` 直出的一页** | 永不手写,随时重算 | **推论**:新增任何"要维护"的文档前,先回答它为什么不是投影;答不上来就让它变成投影,或不要它。 `.internal/arch/*.md` 曾是要维护的资产(sha1 指纹 + 新鲜度机检 + 行号重锚),现降级为**按需临时投影**:用完即弃,不登记、不盖指纹、不进健康检查。 ## 3. 三层数据面(PLANE) | 层 | 路径 | 生命周期 | 版本控制 | 谁能写 | |---|---|---|---|---| | **事件流** | `.internal/events.jsonl` | 永久 | **是** | 只追加,永不修改(唯一真相) | | **运行时** | `.internal/runtime/` | 短命 | 否 | 模型缓存 · 在途意图 · 锁 · 诊断(**含依赖图**) | | **投影** | `PROJECT.md` 标记区 · `.internal/ARCH-MODEL.md` · 地图 | 可再生 | 是 | 只有 `nav_render` | **不存在第四层**:任何新的长期状态先回答"它是事件,还是渲染?",两者都不是就不该存在。 **依赖图不是第四层**——它属于第二层的「磁盘实况」,与 STALE 探测、缺口扫描在同一位置计算。 ## 4. 三条不变式(可机检) - **I1 单源**:模型每个属性都能由 `事件流 + 磁盘实况` 复算。缓存无法自证与源一致时,它不是缓存,是第二个真相。 - **I2 渲染**:地图 / `PROJECT.md` 标记区 / 模型文档全部由模型纯函数生成。渲染物零手写;手写的只有两份契约与决策事件。 - **I3 可丢弃**:**删除 `runtime/` → 治理零损失**,下次调用重建(依赖图同办)。 ## 5. 事件模型(4 种 kind,唯一写入面) | kind | 语义 | |---|---| | `commit` | 一次改动意图(anchor + scope + `arch=`);`open` 记 scope 证据,`closed` 记收口结果 | | `decide` | 架构决策(ADR);`id = ADR-` | | `node` | 节点 upsert / 退役(级联) | | `set` | 主线向量 doing / next / notDoing / exitCondition | `seq` 由追加顺序分配、ID 由 seq 派生 ⇒ 并发追加不可能撞 ID。 **收口不依赖会话**:`open` 的 scope 证据 = 文件 `{size, mtimeMs, sha1}`;证据已变 ⇒ 下次任意工具调用自动收口;未变 ⇒ 意图继续在途(**在途 = 有人正在改,不是孤儿**)。比对**先判存在性、再比内容**——先比 sha1 会把"消失"误报成"被修改"。 ## 6. 七个闸门(全部是 `nav_commit` 内的模型查询) | 闸门 | 判据 | 强度 | |---|---|---| | **锚点闸** | 架构节点真实存在(或锚定 `.internal/arch/*.md`) | 拒 | | **范围闸** | 撞 `notDoing`?撞他人在途 scope? | 拒 / 告警 | | **主线闸** | scope 里的模块被 `doing/next` 引用? | 告警 | | **计数闸** | 同一锚点自上次决策以来**补丁数** ≥ 3?(补丁 = `open` 意图 + 无配对的 `closed` 记录;**收口回执不重复计数** —— 否则每笔改动被计两次,阈值 3 实际在 ~1.5 笔就触发) | 拒 | | **决策闸** | `arch=` 缺失? | 告警 | | **影响面闸** | scope 内节点的**下游**(谁引用我)未纳入 scope? | 告警 | | **完结闸** | 有该收而未收的意图? | 自动 + 报异常 | ## 7. 工具面(6 个,每个 = 模型上的一种操作) | 工具 | 操作 | |---|---| | `nav_graph` | **读**:落点 / 影响面 / 缺口 / 覆盖度 / 文档路由 / 健康 / 决策 / 地图 | | `nav_commit` | **写**:登记改动意图(锚点 + scope + `arch=`);开新笔时按证据自动收口 | | `nav_decide` | **写**:架构决策(挂节点;登记即重置该节点补丁计数) | | `nav_node` | **写**:节点 upsert / 退役并级联;文档工件注册同此入口 | | `nav_render` | **写**:重生成全部投影 | | `nav_set` | **写**:主线向量 | 工具数下降不是目标,是"闸门变查询、产出变渲染"的结果。 ## 8. 不可丢弃的事故事实(F1–F9 · 是需求,不是历史) | # | 事实 | 现架构如何满足 | |---|---|---| | F1 | 两会话并发写 → 索引被覆盖回退 | 事件流 append-only;模型缓存由 runtime 锁保护重写 | | F2 | 破锁竞态 `stat→unlink` 删掉别人新锁 | 破锁用 `rename + token 校验`;锁必须可重入 | | F3 | 假警报腐蚀信号 | 退役语义 + I1 单源 | | F4 | scope 解析曾丢路径 ⇒ 指纹恒空 | 落点 = 索引键 ∪ 字面量 ∪ glob 展开,带断言,解析不出就明说 | | F5 | 装与重启是两条时间线 | 发布链契约照旧(tgz → profile → 重启) | | F6 | Windows 可写根只有一个、不含 `~/.dsh` | **宿主侧约束**(0.9.2 起不在本插件内满足) | | F7 | 服务账户无 logon SID ⇒ 沙箱后端起不来 | **宿主侧约束**(同上) | | F8 | 索引"能增不能删"⇒ 永久假 STALE | 退役能力保留 | | F9 | 保存旧对象 ⇒ 内容静默不落盘 | 只用**追加**与**整体重写 + 读回校验** | ## 9. 文件落点 | 落点 | 职责 | |---|---| | `host/index.js` | Cordis 装配:6 工具注册(唯一 host 面) | | `core/paths.js` · `lock.js` | 基础层:路径契约与平面 · runtime 锁(F2) | | `core/log.js` · `scope.js` | IO 层:事件流追加/读取/校验(F9)· 落点解析三来源(F4)+ 证据指纹 + **import 静态扫描** | | `core/model.js` | 域层:折叠(I1)+ 磁盘实况(STALE / 缺口 / **依赖图**) | | `core/gates.js` | 查询层:七闸(纯函数) | | `core/commit.js` · `render.js` | 编排层:写入编排 + 按证据收口 · 投影渲染(I2) | | `core/format.js` | 表现层:**一切"说给模型看"的文本都在这里成形** | | `test/` | 领域行为 · 不变量 I1–I3 与 A1–A6 · 并发与锁 · host 装配面 | **依赖方向严格单向**:`paths ← {log, scope} ← model ← gates ← commit ← host`。 新增文件 = 架构变更,须先改本文。测试断言数**不写在这里**(那是会漂移的数字,跑 `npm test` 即得)。 ## 10. 文件职责压力(只读信号,不是闸门) **为什么不设行数红线**:行数是**代理指标**。长文件未必坏,短文件照样能混三个职责;而一旦把行数做成闸门, 它必然退化成"狼来了"——与 0.10.0 修掉的计数闸退化(收口回执被当补丁计数、阈值 3 实际 ~1.5 就触发)是同一类错。 **判据是结构,不是长度**。`filePressure(model)`(`core/model.js`)派生三个量: | 量 | 含义 | 由什么派生 | |---|---|---| | `ownerCount` | 这个文件被**几个不同架构节点**登记为落点 | 节点落点表(事件流) | | `din` / `dout` | 被多少文件 import / 自己 import 多少文件 | 依赖图(磁盘 import 扫描) | | `over` | `ownerCount ≥ 3` | 上述两者 | `license`:零手写、删 `runtime/` 可无损重建(I1/I3)。它只经 `nav_graph mode=health` **报告**,**不参与七闸、不拒绝任何写入**—— 拆不拆是架构判断(走 ADR),不是阈值判断。`0.10.2` 起可用;阈值 3 与计数闸同一量级。