# 本仓开发纪律(AGENTS.md) > 面向在本仓工作的 agent 与维护者。**改代码前先读 [`ARCHITECTURE.md`](./ARCHITECTURE.md)** —— 它是本仓的架构契约,不是说明书。 ## 0. 上位约束(唯一) > **所有开发动作必须从架构出发。** > 架构不出错,开发过程中出现一点问题也只是局部小问题;反之,架构错了,局部补得再好也是在错误的骨架上堆砌。 判据很具体:**动手前能说出这次改动落在哪个架构节点上**(功能码 / 模块 / 文件 / `.internal/arch/*.md`)。 说不出 = 还没做架构思考,十有八九是局部补丁。 ## 1. 三条不变式是硬约束(不是"尽量") | | 不变式 | 违规的样子 | |---|---|---| | **I1** | 单源:模型每条属性都能由「事件流 + 磁盘实况」复算 | 又加了一个手写账本文件;把 `runtime/` 里的值当真相读 | | **I2** | 渲染:地图 / `PROJECT.md` 标记区 / `ARCH-MODEL.md` 全部由模型生成 | 手改渲染物;给渲染物加需要人工维护的字段 | | **I3** | 可丢弃:删掉 `.internal/runtime/` → 治理零损失(**依赖图同办**) | 把任何"不能丢"的东西放进 `runtime/` | 改完任何东西,跑 `npm test`——三条不变式都有可机检的用例(**具体项数不写在这里**,那是会漂移的数字,跑一次即得)。 ## 2. 版本规则 > **每次更新一律 +0.0.1**,不因"加功能"跳中间位(lk 2026-09-10 定调)。 > **唯一例外:架构换代**才允许跳位,且必须在变更日志与 README §8 里写明"换代"二字。 已发生的例外:`0.8.6 → 0.9.0`(不是加功能,是换骨架:决策丢弃、事故事实 F1–F9 保留); `0.9.2 → 0.10.0`(换代:模型从「包含树」升级为「包含树 + 依赖图」—— 依赖边由 import 静态扫描派生,非登记;闸门六 → 七,新增影响面闸); `0.10.0 → 0.10.1`(**修 bug,不是换代**:计数闸曾把 `closed` 收口回执也当补丁数 ⇒ 每笔改动被计两次, 阈值 3 实际在 ~1.5 笔就触发 —— 第一性原理触发器退化成狼来了;另修 `.d.ts` 被当代码扫、退役锚点计数仍显示); `0.10.1 → 0.10.2`(**修 bug + 补只读信号,不是换代**:① `normalizeAnchor` 认不了**完整节点 id** —— `module:pn-m03` 被 `nodeId` 二次折叠成 `module:module:pn-m03` ⇒ 恒 NULL,而工具描述写着锚点可为节点 id, 照描述写反而全被锚点闸拒(描述与实现不符比拒绝更坏:它把人训练成猜别名);② `nav_graph mode=health` 增 **文件职责**一节:一个落点文件被几个不同架构节点登记 —— 只读上报,**不进闸门、不设行数红线**); `0.10.2 → 0.10.3`(**仓清理,不是换代、也不是功能**:删除三个**不进包**的死投影 —— `PROJECT.md` / `.internal/ARCH-MODEL.md` / `.internal/events.jsonl`。判因:它们产自 0.9 自举期,而**本仓不是治理根** (治理根是 `D:\FF`,事件流在那边)⇒ 永远不会被 `nav_render` 重算 ⇒ 不是投影,是伪装成投影的历史快照, 且携带假事实(主线仍写"安装 0.9.0"、引用已删的 `HANDOFF.md` / `core/legacy.js`)。三者都不在 `files` 内 ⇒ **包内容与 0.10.2 逐字节相同**;本版只为对齐 origin 与发行记录,**无运行时 delta**); `0.10.3 → 0.10.4`(**仓清理,不是换代、也不是功能**:`.gitignore` 收敛三条已失效的 `.internal` 强制入仓规则 —— 本仓非治理根,`PROJECT.md` / `.internal/ARCH-MODEL.md` / `.internal/events.jsonl` 已随死投影清理删除。**不进包** ⇒ 除版本号外包内容与 0.10.3 相同。**口径澄清**: 「每次更新」= **任何合并进 main 的改动**,含不进包的仓清理,**不设「太小不必升」的判断口子** —— `0.10.0–0.10.2` 的发布链缺档正是「靠判断决定要不要发布」的产物)。 ## 3. 发布链(装与重启是两条时间线) ```bash cd D:\FF\project-nav npm pack --cache .npm-cache # 产出 dsh-external-project-nav-.tgz ``` 1. **产物名含版本号 ⇒ 声明必须同步**:profile 的 `dependencies` 里那条 `file:...tgz` 要一起改。 历史上发生过"复制副本让插件看起来更新了、声明没改"的假绿事故。 2. **重启由用户手动执行**。装完不重启,线上仍是旧版 —— 别把"装好了"当"生效了"。 3. 核实生效:查 profile 安装副本的 SHA256、`--dump-config` 无报错、新会话工具清单出现预期工具。 4. **装完先跑独立校验**:`verify-install.ps1`(**只读,重启前就能跑**)。它验五件事: 声明面 / 实体面 / **树 = 包 = 装**(三方逐文件 SHA256)/ 架构面(能力锚点在位 + 已删机制不在位)/ **运行时面**(对安装实体做加载 + 真跑,`verify-runtime.mjs`)。 **有未通过项就先别重启。** > 为什么要有"运行时面"这一级:静态校验只能证明**字节对得上**。打包错误、缺依赖、导出被改名、 > 语法在目标 Node 上不合法 —— 静态全看不见。**静态对得上 ≠ 能加载。** > 脚本**版本无关**(版本从 `package.json` 读)—— 换代不再新增脚本,旧的不会留在仓里变尾巴。 > `npm pack` 在某些受限沙箱下会因 npm 缓存目录在工作区外而 `EPERM`; > 用 `--cache .npm-cache`(工作区内)即可,不是放宽安全策略,只是改路径。 ## 4. 本会话/本仓的已知边界(实测,别重复踩) > **内容准入见 §6**:本表只收**本仓 / 本机**的边界事实;跨任务纪律一律走 `pending/` → 深睡,不进契约。 | 边界 | 事实 | 应对 | |---|---|---| | 文件沙箱 | **策略随会话变,别照抄历史结论**:`workspace-write` 下可写根是 `D:\FF\project-nav`(不含 `D:\FF` 与 `~/.dsh`);`danger-full-access` 下**全都可写**(含 profile) | **先判当前策略,再决定要不要推给别人**:`danger-full-access` ⇒ 自己动手(2026-09-12 实测:profile 安装 + 自证可直接做);`workspace-write` ⇒ 才交给用户。**唯一永远只有用户能做的是重启** —— agent 不能重启承载自己的进程 | | `node --test` | 用管道 spawn 子进程 → 受限沙箱下 `spawn EPERM` | 用 `npm test`(直接执行测试文件,`node:test` 照跑) | | Node 直接 spawn | `child_process` 抓管道输出 → `EPERM` | 别在 Node 里 spawn 子进程;需要的字节转换在本 shell 内用 .NET API 做 | | PowerShell 写文件 | **PS 5.1 的 `Set-Content -Encoding utf8` 会按 GBK 误读 UTF-8 源文件并写坏中文** | **一律用编辑工具改文本**;确需 PS 批量处理时只用 `[System.IO.File]::ReadAllBytes/WriteAllBytes` 做字节级操作 | | PowerShell 读脚本 | **PS 5.1 读无 BOM 的 UTF-8 `.ps1` 会按 GBK 解码**,中文(尤其全角标点)会把字符串终结符吃掉 → 语法错 | 含非 ASCII 的 `.ps1` 必须存成 **UTF-8 with BOM**;而 `package.json` / `.js` / `.md` 必须**无 BOM**(node 不认 BOM) | | PS 5.1 语法子集 | 不支持 `??`、`?.`、三元 `? :`、`-Encoding utf8NoBOM` | 用 `if/else` 与显式变量分支;写 JSON 用 .NET `UTF8Encoding($false)` | | **profile 里不能跑 npm 装** | profile 含 `"@dsh-external/dsh-motion": "link:..."`,npm 10.9.4 不接受 `link:` 协议(`EUNSUPPORTEDPROTOCOL`);npm 在 reify 时会解析**整份** package.json,所以 `npm install` 与 `npm install --no-save` **都会**失败;又没有 `package-lock.json`(只有 `node_modules/.package-lock.json`)故不能 `npm ci` | 装本地 tarball 用 **`install.ps1`**(配套 `verify-install.ps1` + `verify-runtime.mjs`):直接 `tar` 展开到 `node_modules/@dsh-external/project-nav/`(npm 对本地包做的本就是这件事),**完全不经过 npm**;声明另行定点改写。⚠ 脚本**版本无关**(版本从 `package.json` 读),换代不再新增脚本 | | 别在空目录里跑 `npm install ` | npm 会**向上找最近的 `package.json`** —— 在仓内临时目录里跑,它就把依赖树装进**本仓** `node_modules`(已实测发生,且它不改 package.json,很容易误判为成功) | 试验要放在仓**外**的临时目录,或用 `--prefix`;试验后清掉生成的 `node_modules` | ## 5. 事故事实不可丢弃(F1–F9) 它们写在 [`ARCHITECTURE.md`](./ARCHITECTURE.md) §8,**是需求不是历史**。架构可以重画,这些事实不能改: 并发追加不丢(F1)· 破锁用 rename + token(F2)· 能删才不留假 STALE(F3/F8)· 落点解析三来源都不许丢(F4)· 装与重启两条时间线(F5)· 边界只收工作自包含的项目(F6/F7)· 只用追加或整体重写 + 读回校验(F9)。 ## 6. 治理面纪律(0.10.0 起) > **会因代码变更而"过期"的文档,不该是文档 —— 该是投影。**(ARCHITECTURE §2) - **文档准入**:手写面只有两份契约(`ARCHITECTURE.md` + 本文件)。新增任何"要维护"的**文档**前,先回答它为什么不是投影。 - **内容准入(2026-09-12 补 · 修的是一个只朝一边开的洞)**:两份契约只收**判据** —— 那些"只在架构换代 / 纪律变更时才改"的东西。 运行中发现的**一切教训**(事故、坑、操作纪律),不论多值得记住,都**不进契约**,走记忆通道: **当场写** `~/.dsh/suite/knowledge/pending/` → 深睡按门槛提炼成 `[原则]`(同主题 ≥3 条痕迹)→ 每轮注入热记忆。 - **判据是成本,不是口味**:本仓契约**进包** ⇒ 改它就要走"重打包 → 装 → 校验 → 重启"。 2026-09-12 曾把一条跨任务纪律误写进 §4,为此多付了 **3 轮发布链** —— 要跟着发布链走的东西,装不下跨任务教训。 - ⚠ **旧条为什么没拦住**:旧版只约束"新增**文档**",不约束"往已有契约里新增**内容**",而后者**不会触发那道闸门**。 不对称在于:`ARCHITECTURE.md §9` 一直有自己的内容排除条款("测试断言数不写在这里"),`AGENTS.md` 没有。 **没有排除条款的契约,看起来永远允许追加。** - ⚠ **为什么会被泛化**:本仓有四条正当纪律都在教"发现 → 写进权威文档"(§0 改了架构先改契约 · §2 换代须写变更日志 · 本条旧版 新增文档先自问 · ARCHITECTURE §9 新增文件须先改本文)。**它们的作用域都是"架构事实"**; 把同一条反射用到"任何值得记住的规则"上,就越过了作用域。 - `.internal/arch/*.md` 已从"资产"降级为**按需临时投影**:不盖 sha1 指纹、不做新鲜度机检。 要让它被按任务检索到,登记成普通 `artifact` 节点(`nav_node layer=artifact when=…`),走 `nav_graph mode=docs`。 - 旧账本迁移(0.9.x 的一次性动作)**已完成,迁移器已随 0.10.0 删除** —— 迁移后不存在第二个真相,也不可能被误跑第二次。 ## 7. 数据面的正确写法 ```gitignore !.internal/ .internal/* !.internal/events.jsonl # 唯一事实源:必须进版本控制 !.internal/ARCH-MODEL.md # 人类可读的模型快照,进仓以便 diff 审查 .internal/runtime/ # 可丢弃 .internal/legacy/ # 旧账本只读快照 ``` 整目录忽略 `.internal/` 是**错的**:事件流不进版本控制,新 clone 读不到任何决策,"决策可传播"就成了空话。