pietro-works/PACS
GitHub: pietro-works/PACS
一套面向 AI 辅助工程与团队协作的轻量级代码耦合标记约定,专门追踪那些不会产生任何报错却会导致功能静默失效的跨文件隐式依赖。
Stars: 0 | Forks: 0
P.A.C.S | Petrus Agentic Coupling System
没人会警告你哪块石头撑起了屋顶。但 PACS 会……
## 它是什么 PACS 是一种小型约定,用于标记代码库中那些会悄然失效的耦合。每个耦合都会获得一个稳定的代码(`PACS0001`、`PACS0002` 等),在 registry 中占据一行,并将该代码作为注释放置在它所管控的具体代码行上。当你在该行附近进行修改时,这个代码会引导你查看对应的记录行,其中列出了你现在必须修改的所有其他位置。 一个编号,一行记录,一条注释。这就是整个系统。 ## 它旨在解决的问题 有些 bug 会主动宣告自己的存在。构建变红、测试失败、应用抛出异常。这些都是“友善”的 bug,因为它们的失败本身就是一种强制约束。 PACS 则是为了应对另一种情况。你添加了一个单位类型,却忘记了 sprite 表,导致它渲染为空白。你重命名了消息上的一个字段,而接收端读取到的是 `undefined`,导致请求悄无声息地超时。你添加了一个音效却从未注册它,导致它永远在完美的静音中播放。没有任何报错。就像你剪断了一根电线,然后在一个你忘了存在的房间里灯灭了,直到几周后有人问起新功能为什么看起来坏了,你才会发现。 编译器看不到这种耦合。类型系统通常也做不到。测试只有在有人已经知道耦合存在时才能捕捉到它,而这种知识恰恰会在不同会话、重构以及下一位打开文件的人之间消散。 ## 为什么它有效 路标就设立在修改点。警告就在你即将剪断的电线上,而不是埋没在没人会重新打开的文档里。 这里只有一个事实来源。registry 中的记录行会列出所有的触点,因此“我是否把它们都找全了”会得到一个明确的答案,而不是一种感觉。 这些代码是稳定且只增不减的。`PACS0007` 在明年或在一个陌生人的克隆仓库中都代表同样的含义。在重构期间移动它时,标签会随代码一起迁移;该编号永远不会被复用于其他事物。 而且它刻意保持精简。只有那些承重的、会静默失效的耦合才能获得代码。如果某些东西会以明确的报错形式崩溃,它就什么也得不到,因为崩溃本身已经完成了提示工作。过度标记是这种理念走向消亡的原因,因此生成代码的门槛被有意提高,并且每一行都必须用自己的话来证明其存在的合理性,而不是复制粘贴下来的套话。 其强制机制是在每次修改结束时写下一行话:列出你实际打开的记录行,而不仅仅是你声称触碰过的代码。一次提交会以类似 `Opened PACS0001, PACS0014, confirmed touch-points in atlas.js and validate.js` (已打开 PACS0001、PACS0014,确认在 atlas.js 和 validate.js 中的触点)的话语作为结束。这一行就是完整的审计追踪,可以根据实际的代码变更进行核查,而不仅仅是口头声明。审阅者读到这一行,就能知道哪些“暗雷”已被纳入考量,而不必先在自己脑子里重新构建一遍耦合图。 ## 三个真实案例 以下案例来自同一个代码库,一个大约七千行的浏览器游戏。仅仅一次排查就发现了二十二个符合标准的耦合。这里是其中的三个,形态各不相同。 ### 被十几处读取的 registry 单位类型存放在一个常量中。在显而易见的位置添加第四种类型(比如 `drone`)很容易。陷阱在于,还有另外十几处地方是独立以类型作为键的,并且它们全都没有从源常量派生:sprite atlas、移动调度、验证器的 allowlist、构建菜单。如果漏掉了 atlas,drone 就会在整场比赛中保持隐形。如果漏掉了验证器,你下达的每一个指令在引擎看到它之前就会被丢弃。 ``` // PACS0001 — a new unit type needs matching edits at every keyed-by-type site // (sprite atlas, validator allowlist, movement dispatch, build cards) — AGENTS.md const UNITS = { worker: {...}, vehicle: {...}, triangle: {...} } ``` ``` PACS0001: anchor: "engine.js:UNITS" sync_with: - "atlas.js" - "validate.js" - "dispatch.js" - "build.js" fails_silently: "new type renders blank, or its orders get dropped before the engine runs" justification: "four independent keyed-by-type sites, none derived from UNITS, so nothing forces them to stay in sync" ``` ### 跨越进程边界的消息 页面通过 `postMessage` 将回合请求传递给浏览器扩展。该扩展按名称读取每个字段,没有使用展开语法。如果你在页面上将 `requestId` 重命名为 `reqId`,却忘记了扩展,该字段在接收端就会变成 `undefined`。没有任何异常抛出。四分钟后回合超时,看起来完全就像是模型未能给出回答——而这往往是你最不会想到去排查的地方。 ``` // PACS0011 — the extension reads these fields BY NAME (no spread); // rename here means rename there, or it arrives undefined across the boundary — AGENTS.md window.postMessage({ __bridge: 'req', requestId: id, bot, payload }, '*') ``` ``` PACS0011: anchor: "webtab-client.js:request_envelope" sync_with: - "extension/bridge.js" - "extension/background.js" fails_silently: "a renamed field crosses as undefined, the turn silently times out" justification: "the two sides read fields by name across a process boundary, so a rename on one side has no compiler link to the other" ``` ### 播向虚无的声音 每个音效在播放前都会在一个表中进行查找。如果你调用了 `sfx('meltdown')` 却从未将 `meltdown` 添加到该表中,查找操作就会返回空,并且函数会提前返回。你花了一下午时间谱写的警报声将在完全的寂静中播放,而你的第一反应肯定是猜测出现了静音 bug 或者是触发了 cooldown,而不会想到是缺少了某个 key。 ``` // PACS0014 — every sfx(name) needs a key here, or the cue silently no-ops (permanent silence) — AGENTS.md const SOUNDS = { chat: ..., coreHit: ..., meltdown: ... } ``` ``` PACS0014: anchor: "audio.js:SOUNDS" sync_with: - "every sfx() / play() caller" fails_silently: "an unregistered event plays nothing, no error, reads as a mute bug" justification: "the lookup fails closed with no error path, so a missing key and a muted cue are indistinguishable from outside" ``` 将几十条这样的记录堆叠起来,你就拥有了一张地图,标示出你的代码库可能会以哪些悄无声息的方式出错,并且就写在下一个人或下一个 agent 真正会撞上它的地方。 ## 采用它 在一个持久化的位置放置一个 registry 文件,比如 `AGENTS.md`。从那时起,在任何规划或涉及代码修改的工作开始之前,都必须完整读取该文件。当你遇到静默的耦合时,取下一个可用的编号,添加一行带有真实理由的 YAML 记录,并标记锚点。就这么简单。 在大多数代码中使用 `//`,在 CSS 或模板字符串中使用 `/* */`,在标记语言中使用 ``,在 shell 中使用 `#`。 ## 模块化 PACS 从零依赖开始。在最简单的情况下,它仅仅是 `AGENTS.md` 和 `SPEC.md`,直接由你的 agent 解析。 如果你想要自动化验证,可以在你的 workspace 或 CI 中运行 `bin/pacs check`,以捕捉死路径并执行自定义测试脚本。如果你使用 Claude Code 并且想要在工具执行前进行强制约束,`pacs-hooks/` 提供了可选的中间件,它会阻止文件编辑,直到 registry 被读取。 linter 只运行你配置的内容。如果你没有验证脚本,它只会检查 YAML schema 和标签。如果你不使用 Claude Code,你可以完全忽略 hooks 文件夹。 关于生成和检查的完整规则在 [SPEC.md](SPEC.md) 中。它的篇幅刚好能在一屏内显示完。 ## greencheck(每次输出的参考信号) 一次长会话可能会在读取一次 registry 到下一次提交之间产生许多次回复。为了弥补这一盲区,`AGENTS.md` 中包含了一条指令,告诉 agent:只要某次回复实际上参考了 registry,就要在回复的开头附上 `P.A.C.S ✅`。 这是一个明面上的断言。它不再是隐形地待在 context window 里,而是显现在对话记录中;当人类滚动浏览时,可以轻易发现:在一个显然无视了 registry 的修改旁边出现了一个绿勾,或者在一个显然本该查阅 registry 的修改旁边缺少了绿勾。一个你能抓到它说谎的信号,胜过一个你根本看不见的检查。 ## PACS 不是什么 它不是 [Andrej Karpathy 的 LLM Wiki](https://gist.github.com/karpathy/442a6bf555914893e9891c11519de94f) 的替代品,也无意成为其替代品。 LLM Wiki 是一个不断积累的知识库。一个 agent 会读取你的原始源码,并将它们编译成你可以查询的、相互链接的页面,从而让理解得以沉淀,而不是在每次遇到问题时都重新推导一遍。它是由你的 agent 编写并保持更新的图书馆,试图容纳所有值得了解的知识。 PACS 几乎不存储任何东西。它不解释你的系统,也不教你领域知识,更不会回答关于它们的问题。它只标记一小部分“暗雷”,然后拒绝向更多领域扩展。wiki 记住的是值得理解的内容;而 PACS 只记住那些会一言不发地伤害你的隐患,并且它把每一条多余的记录都视为累赘。 同时运行它们,两者并不冲突。wiki 是你的 agent 理解代码的方式。PACS 是你的 agent 修改代码且不使其悄然退化的方式。一个是整个领域的地图。另一个则是标示出其中三颗地雷的微小红 X。 ## 名称由来 PACS 代表 Petrus Agentic Coupling System,没错,它是以撰写它的人的名字命名的,而这个名字可以追溯到希腊语中的“岩石”。一个关于绝不能悄然发生偏移的承重部分的方法论,却以一个名字字面意思就是“岿然不动”的家伙来命名。你大可将此视为虚荣心,也可视为品牌营销,全凭你定。但至少它很准确。 Golem(魔像)的形象才是值得保留的部分。在古老的故事中,魔像会严格遵从刻在它身上的指令行事,不多一分,不少一毫,也从未有一次认为自己懂得更多。他在动手之前会读取完整的 registry,处理清单上的每一个触点,并准确地告诉你他修改了什么。这使得他,在状态好的时候,比那个与他同名(Petrus)的男人更像是一位严谨的贡献者。 ## 许可证 MIT。拿去用,重命名它,标记出你自己的地雷。标签:AI辅助开发, 云安全监控, 代码审查, 代码质量检查, 数据可视化, 数据管道, 文档结构分析, 软件工程, 静态分析