AGENTS.md@internal/blueprint · git:20261026.7abd946 · 2026-10-26 · sha256 7a90032d1fd05ba8

AGENTS.md@internal/blueprint git:20261026.7abd946A

Immutable. This exact content never changes and is served at /api/v1/blob/7a90032d1fd05ba8.

# internal/blueprint — 仓库蓝图

> 范围:本目录内不可派生的约束。(原 `docs/repo-blueprint-design.md` v4 + §21 实施记录已不存在,
> 蓝图的规格由本文件持有。)全局摘要见仓库根 `AGENTS.md` §6.4。
> 注意:`section.go:1-11` 的包文档还写着"本包只做 R1 的骨架",与 R1–R6 已落地不符——那是注释过期,
> 不是功能缺失。

## 边界

蓝图的**存储与算法层**:零 LLM 扫描器派生结构事实、把 `agent_exit.blueprint[]` 提交的散文折成
原子条目落盘、渲染常驻索引。不负责装配与开关(`internal/app` + `config.BlueprintEnabled()`)、
不负责 prompt 与委派、`read_blueprint` 工具本体在 `internal/tools`。

## 硬约束

**1. 叶子包。** 只准 import 标准库 + `util` + `projectid`,由 `scripts/check_blueprint_leaf.sh` 守。
仓库读数(gitinfo)与 token 计数器(tokenutil)由装配层**做成参数传进来**:
`RepoToken`(`derive.go:47-51`)、`CountTokens`(`index.go:37-39`)、Git 三项(`fold.go:60-61`)。
启用判据**不在本包**,本包也不读环境变量(`section.go:9`,I8)。

**2. 派生的不落盘,落盘的不派生。** front-matter 的计数由 Render 从正文重算(`doc.go:29-30,106-110`);
derived 段不在 `AssertedSections`(`section.go:30-39`);模块边界由扫描器定,**模型不许造模块**
(`fold.go:246-249`)。结构层不落盘 ⇒ 模型没有任何途径写坏它。

**3. fold 是确定性纯函数。** `Merge` 不碰文件系统(`fold.go:101`);幂等靠"短 ID 由内容派生 +
段内按 `(where, claim)` 稳定排序 + 同 `module+field` 非空才覆盖"(`merge.go:12-17`,I20)。
排序漂走的后果不只是"字节变了",还会让 provider 端前缀缓存与人类 review 同时失效。

**4. 正文逐字没变就不算一次 fold(NoOp)**(`fold.go:160-166`),`Commit` 把"全被拒收"也归 NoOp
(`commit.go:48-56`)。理由:I20 的"字节不变"与 I18 的"没更新 ≠ 更新失败"会打架。

**5. 第五道校验不拒收,只标 stale。** 前四道在 `validate()`(`fold.go:213`),锚点复核只把状态标出来
(`:138-154`)。每个 `RejectReason` 都必须有一条能读出来的账(`:15-32`,I18)——新增拒收原因必须
同时有夹具,`TestEveryRejectionReasonIsAccounted`(`fold_test.go:320`)会盯。

**6. 散文路径校验判不了就弃权**(`prosepath.go`,I14)。曾经的形状判定把中文技术散文里的
`Name/Brief/Effect` 斜杠列表和写短的前缀路径全判成违规,一条拒丢一条 claim、全丢就是 NoOp ⇒
**蓝图文件永不出现**,而症状与"模型根本没提交"逐字相同(根 AGENTS.md §11 坑 42/43)。
存在性由 `where` 锚点管,那是第 5 条的活。

**7. 乐观 CAS,不用锁文件。** 读 → fold → 写前重读 → 变了就对着最新内容重放同一个 fold;
两次冲突返回 `ErrFoldConflict`(`commit.go:15-34`)。**调用方必须把它当"下次自然补上",
不是任务失败**——把降级写成 error,Director 就以为委派失败了,于是把已完成的工作重做一遍。
归档(`revisions/`)写不下去不拦本次提交(`commit.go:79-85`),但 GC 删孤儿时归档失败就**不删**(`gc.go:63-66`)。

**8. stale 是粘滞的。** 没有后台进程会补写,一片过期之后只有下一次 Repo 真的覆盖它才修——
所以索引行必须写成**行动理由**(`index.go:227-231`),把"值不值得为新鲜度花一次委派"留给有任务
上下文的一方。指纹不符时保留散文(`freshness.go:192-219`):清空会把可用信息变成不可用。
可见集合 == 派生集合,改名/删除的旧片既渲染不出也读不到,90 天才从文件摘除(`gc.go:22`)。

**9. 两条根护栏。** 拒 `$HOME` / `/` / 非 git 且无 manifest(`guard.go:40-65`);**存储根不得落在
项目内**(`guard.go:66-89`,I7)——进了工作区就会被 `git status` 算成 dirty,而
`[git_checkpoint] stash_dirty_worktree` 用的是 `--include-untracked`,会把真源收进 stash。
拿不到 home 时报错而不是退回 CWD(`store.go:40-56`)。`projectID` 唯一来源 `projectid.Compute`(I12)。

**10. 快照不可变 + 唯一写者。** `atomic.Pointer`(`derive.go:10-16`、`scan_types.go:12`);
`Request` 永不阻塞(`derive.go:147`);扫描失败**不清空旧快照**(`:196-204`);成本模式只在第一次
取样时定(`:210-215`,150ms 分界自带到期条件 `:26-33`)。schema 写了就必须有人执行:
`MaxSubmitItems=8` 在 `Merge` 里强制(`fold.go:117-132,265-276`)。
默认常数与 `config` 的模板值靠 `internal/config/blueprint_defaults_test.go` 钉同源。
落盘只走 `util.AtomicWrite` 一处(`doc.go:155-167`)。

**11. fold 的账目必须经返回值离开本包,不许只留在日志里。**
`FoldResult` 是拒收账目在本包内的唯一载体,注释明写"调用方必须把它打出去或写进 rollout"
(`fold.go:79-95`),`Commit` 连 NoOp 也把账目带回(`commit.go:34,48-56`)。理由:`agent_exit` 是
终止点,拒收若只进 `slog`,Director 看到的仍是一次"成功的委派",模型也不知道自己白交了,
下一轮还会这么干(根 AGENTS.md §11 坑 43 的包内那一半)。账目压成一行给 Director 读的渲染
(`FormatForDirector` + 便签 `> [blueprint]` 尾注两处出口、"不进 conclusion")是**调用方**的契约,
见 `internal/agents/AGENTS.md`。

**12. 出处锚点三态,程序判不了的一律记 unverifiable、不谎称验过。**
`AnchorChecker.Check`(`freshness.go:86-126`):`where` 为空 → unanchored,没声明出处就不该被追究
(`:88-90`);`path:line` 行号越界 → stale(`:108-116`);`path:symbol` 走朴素词边界匹配,
**不依赖 codeseek**——索引在建或不可用是常态(`:118-124`);非路径形态(跨模块论断)state 保持 ok
但记进 Coverage 的 unverifiable 计数(`:93-99`,I19)。行数/存在性缓存的生命周期 == 一次 fold
(`freshness.go:64-76`)。

**13. 常驻注入只有形状②,purpose 这类不可验证散文永不进常驻。**
边是唯一无可替代的确定性事实,purpose 是唯一不可验证的散文;散文进上下文的入口只有
`read_blueprint` 与委派前缀(`index.go:10-11`、`prose.go:9-12`)。token 硬顶
`MaxTokens<=0 → DefaultIndexMaxTokens`(`index.go:86-88`);header 先占预算、截断计数写进
header,且选行必须**迭代**而不是两趟——`+N modules not shown` 那一行自己占 token,两趟写法会把
硬顶从内部顶穿(I3,`index.go:125-132`)。四态 stale/unannotated/TRUNC/oversize 说出来的唯一
地方是 `indexFlags`(`index.go:17,24-28,192-194`);静默少一片是蓝图最坏的失效。

**14. 证据集由调用方算好传进来;"集为空"与"不在集内"是两条不同的拒收。**
`FoldRequest.Evidence` = 本次 run 真打开过的文件所属模块集,由 agents 侧用
`Snapshot.EvidenceModules` 算——校验用的还是派生集合本身(`fold.go:61-67`);
module 不在集内 → `RejectNoEvidence`(`fold.go:250-251`),整轮一条证据都没有 →
`RejectNoReadAtAll`(`:259`),不许因为"没证据"就放行。`EvidenceModules` 去重 + 排序(确定性)、
未命中静默丢弃(`scan_types.go:171-174`)。"入参侧 ∪ 出参侧"的采集口径与
`toolspec.Spec.ResultPaths`(`list_dir` 的裸文件名必须排除,否则证据集收到假路径、误伤一整片模块)
不在本包——见 `internal/agents` 与 `internal/toolspec` 的文档。

**15. `revisions/` 滚动 5 份,唯一存在理由是"删错了"必须有 Undo。**
孤儿老化会真删段落、fold 会真替换正文,两者都动在唯一的可写文件上;没有归档,
"上一次的正确说法"就是一次手滑就没回来的东西(`revisions.go:13-17`,`RevisionsKeep = 5` `:22`)。
文件名用定宽 UTC 时间戳——字典序 == 时间序,"留最新 5 份"就是排序后砍尾巴;随机后缀防两个
tab 同一秒各 fold 一次互相覆盖(`:19-20`)。失败只返回错误、绝不 panic,调用方只做 warn
(`:28-30`)——与硬约束 7 的"归档写不下去不拦本次提交"同源。

## 入口

`doc.go`(文档模型,以及两条 CAS 相关函数为何分开)、`fold.go`(校验与拒收账目)、
`derive.go`(派生快照)、`index.go`(形状②渲染与四态)、`scan.go`(扫描器入口)。

## 闸门

- `fold_test.go:128,159,185,239`:幂等 / 乱序输入产出同一份字节 / CAS 重放 / 放弃——
  改排序或 ID 派生即红。
- `fold_prose_test.go:25,40,60,71`:坑 42 的四种散文形状(含"与违规串同形的正常散文")。
- `freshness_test.go:103`:指纹不符时删散文即红。
  `TestAnchorVerificationIsThreeValued`(`:17`)与 `TestAnchorSymbolMatchIsWordBounded`(`:87`):
  钉第 12 条——锚点退回两态、或符号匹配退化成子串(`Foo` 命中 `FooBar`)即红。
- `fold_test.go:271`(`TestCommitRejectsModuleNeverReadInThisRun`)与
  `:303`(`TestCommitWithoutAnyEvidenceWritesNothing`):钉第 14 条——
  "证据集外放行"或"空集当没有证据约束"即红。
- `index_test.go:73,131,166`:四态必显、token 硬顶、前缀字节稳定(钉第 13 条)。
- `guard_test.go:51`、`gc_test.go:53`、`gc_test.go:165`(`TestSaveRevisionRollsToKeep`,钉第 15 条)、
  `store_test.go:36`、
  `prose_test.go:32`、`derive_test.go:65,128,239`、`doc_test.go:37,66`。
- `scan_golden_test.go:74,83`:43 片 / 138 条边的自扫 golden(逐条归因写在该测试头上的注释里;
  2026-10-22 随知识机制删除从 45/144 变来,2026-10-23 因删掉 `internal/tui/anim` 整包从 44 片变到 43 片,
  2026-10-26 因 `cappedBuffer` 上移成 `util.CappedBuffer` 从 137 条边变到 138)。
  **改扫描器前先跑 `gofmt -l internal/blueprint/testdata`**——`testdata/` 是 Go 工具链的盲区,
  `go build/vet/test` 都不看它(根 AGENTS.md §11 坑 41)。
- 端到端验收在**另一个包**:`internal/agents/e2e_blueprint_test.go:127 TestE2ERepoRunWritesBlueprintFile`
  (真实 Repo 循环 + 真实扫描器 + 真实 guard,断言就是"文件出现")。