分层的机制很简单:按目录叠加,离文件近的追加在后面。
但覆盖顺序必须写死成「子只能加、不能减」——能减的话硬规则就不硬了。
Demo 里同一份规则,两种设计下 data/ 那条的命运完全相反。
上一篇讲三段写什么,这一篇讲写在哪儿。 一个仓库只有根目录一份 AGENTS.md 是不够的:后端的规矩跟前端不一样, 共享目录的规矩跟业务目录也不一样。 但分层会带来一个新问题:两份规则打架时听谁的。 Demo 用同一份数据跑了两种覆盖设计,结果差在一条硬规则上。
一、根目录放通用规则
根目录那份管整个仓库,所以每一条都要能承受「所有目录都遵守」。 这是个很强的约束。 「组件文件名用大驼峰」写在根目录,后端目录也得遵守——显然不对。 一个判断方法:把这条规则套到最不相关的那个目录上,还成立吗? 不成立,就往下挪一层。 根目录适合放三类: 全仓通用的入口(跑、验)、全仓通用的禁区(不许加依赖)、全仓通用的硬规则(提交前必须验)。
二、子目录放局部规则
子目录那份管自己这一片,写「本目录特有」的东西。
| 目录 | 该写什么 |
|---|---|
src/api/AGENTS.md |
改接口要同步更新文档 |
src/web/AGENTS.md |
组件命名与目录约定 |
tests/AGENTS.md |
测试放哪儿、命名怎么起 |
子目录规则最容易写多。 写多了会变成「每个目录一份小文档」,没人维护,也没人读。 一个实用上限:子目录那份不超过五条。 还有一个隐蔽的好处:子目录规则是「按需加载」的。 Agent 只在碰到那个目录时才读它——所以你把局部细节放在子目录,等于给根目录减负。
三、谁覆盖谁
这是这一篇唯一需要记住的机制:按目录从外往里叠加,近的追加在后面。
Demo 里三个路径拿到三份不同的规则表:
src/api/handler.py 七条、src/web/Chat.jsx 六条、tasks.py 五条。
叠加本身没问题,问题在「同名条目」怎么办。
两种设计:
| 设计 | 同名条目 | 后果 |
|---|---|---|
| 子可覆盖 | 子目录的顶掉父的 | 一句「本目录例外」就废掉硬规则 |
| 子只能加 | 父的保留,冲突的标出来 | 硬规则一直硬 |
Demo 里两种设计跑出来是这样:
可覆盖那版,data/ 那一条变成了「本目录允许直接改 data/」;
只能加那版,原规则还在,新条目后面标了「与硬规则冲突,已忽略」。
为什么必须选后者?
因为前者的问题不是「这次改错了」,是「你不知道它改错了」。
任何人在子目录里写一句例外,你的硬规则就静默失效了。
顺带一个推论:软规则可以被子目录覆盖,硬规则不行。
「注释用中文」这种偏好,某个目录想用英文,随它;但「data/ 不许改」这种,谁也不能推翻。
四、动手 Demo:分层与覆盖:子目录能推翻根目录吗
这个 Demo 建三层规则文件,然后写一个合并器,算出某个路径最终听哪些规则。
同一份数据跑两遍,只改一个开关:子目录能不能覆盖父的同名条目。
看 data/ 那一条在两遍里变成了什么。
def _merge(root, subs, path, strict):
rules = list(root)
d = os.path.dirname(path)
while d: # 从文件所在目录一路往上
for topic, text, lvl in subs.get(d, []):
same = [i for i, x in enumerate(rules) if x[0] == topic]
if not same:
rules.append((topic, text, lvl))
elif strict and rules[same[0]][2] == u"hard":
rules.append((topic, text + u"(冲突,已忽略)", lvl))
elif not strict:
rules[same[0]] = (topic, text, lvl) # 覆盖
d = os.path.dirname(d)
return rules
实跑输出:
--------------------------------------------------------------------
分层与覆盖:子目录能推翻根目录吗
--------------------------------------------------------------------
① 三层规则文件
--------------------------------------------------------------------
| 文件 | 管哪儿 | 条数 |
|---|---|---|
| AGENTS.md | 整个仓库 | 5 |
| src/api/AGENTS.md | api 层 | 2 |
| src/web/AGENTS.md | web 层 | 2 |
② 某个文件到底听谁的
--------------------------------------------------------------------
src/api/handler.py -> 最终 7 条,来自 src/api
src/web/Chat.jsx -> 最终 6 条,来自 src/web
tasks.py -> 最终 5 条,来自 (根)
三个路径拿到三份不同的规则表:离得近的追加在后面。
这就是「分层」的全部机制——没有魔法,就是按目录叠加。
③ 覆盖顺序:两种设计,差在一条硬规则
--------------------------------------------------------------------
[子目录可覆盖] data -> 本目录允许直接改 data/
[子目录只能追加] data -> data/ 下不许改 / 本目录允许直接改 data/(与硬规则冲突,已忽略)
左边那版,api 目录里那条「允许直接改 data/」把根目录的硬规则顶掉了。
右边那版,它被追加进来但标了「与硬规则冲突」。
! 这就是为什么覆盖顺序要写死成「子只能加、不能减」。
能减的话,任何人在子目录里写一句「本目录例外」,你的硬规则就废了——而且你不会知道。
④ 谁离文件近,谁说了算
--------------------------------------------------------------------
| 问题 | 答案 | 为什么 |
|---|---|---|
| 子目录能加规则吗 | 能 | 局部规矩就得局部写 |
| 子目录能删父规则吗 | 不能 | 删了没人知道 |
| 同名条目冲突怎么办 | 硬规则赢 | 否则硬规则不硬 |
| 规则写在哪一层 | 离影响范围最近那层 | 太靠上会误伤别的目录 |
! 一个实用判断:这条规则只对某个目录成立,就写进那个目录;对整个仓库都成立,才写根目录。
写在根目录的每一条,都要能承受「所有目录都遵守」。
同一份输入,两行输出完全相反:
可覆盖那版 data/ 变成「本目录允许直接改」,只能加那版还是「不许改」。
差别只在一个 elif。
② 那一段值得多看一眼:三个路径、三份规则表。
tasks.py 在根目录,拿到五条;src/api/handler.py 拿到七条(根五条加局部两条)。
这就是「按需加载」的具体样子。
注意「来自 src/api」这个标注,它是排查问题的入口:规则不对时,你先要知道它读的是哪一份。
最后那张四行表,其实只有第二行是要背的:
子目录能删父规则吗——不能,删了没人知道。
30 秒实验
- 看一眼你仓库里有没有子目录级规则。 有的话,检查它有没有写「本目录例外」这种话。
- 把根目录的规则逐条套到最不相关的目录上。 套不上的,往下挪一层。
- 给子目录那份定个上限,五条。 超了说明该再分一层,或者该合并。
常见误区
- 「子目录应该能覆盖父规则,不然不灵活。」 灵活的是软偏好,硬规则不该灵活。 而且覆盖是静默的——你不知道它发生了。
- 「所有规则都写根目录最省事。」 省的是你的事,费的是它的注意力。 根目录四十条,它每条都要读,读多了会漏。
- 「分层太复杂,一份就够。」 一个目录一个团队的时候确实够。 两个团队改一个仓库,一份就不够了。
自测三问(点开对答案)
1. 分层的叠加顺序是什么? —— **按目录从外往里,离文件近的追加在后面。** **它不是「覆盖」,是「叠加」——除非你专门设计了覆盖。** 2. 为什么覆盖顺序要写死成「子只能加、不能减」? —— **因为能减的话,任何人在子目录写一句「本目录例外」,硬规则就静默失效。** **而你不会知道。** 3. 软规则和硬规则在覆盖上有什么区别? —— **软规则可以被子目录覆盖**(「注释用中文」在某目录想用英文,随它); **硬规则不行**(「`data/` 不许改」谁也不能推翻)。下一篇 6.3 讲另一种分层:团队规矩和个人偏好,一个进仓库,一个只在自己机器上。
- ☐ (本篇完,下一篇 6.3 团队规则与个人偏好)