Tag 归档¶
seal_tag.py 是 /gm-finalize 用来把一个已完成的 tag 变成 docs/tags/<Tag>/
下不可变归档的工具。通常你不需要手动运行它,唯一的例外是 backfill——用于给旧版本
GodotMaker 封存的归档补上索引文件。
封存后的归档长什么样¶
docs/tags/
├── README.md 所有已封存 tag 的父级索引
└── v0.1.0/
├── README.md 该 tag 的导航页
├── SUMMARY.md 有长度边界的检索摘要
├── CHANGELOG.md 由 /gm-finalize 写入的交付说明
├── GDD-snapshot.md
├── PLAN.md
├── STRUCTURE.md
├── DESIGN.md
├── SCENES.md
├── MEMORY.md
├── memory/ MEMORY.md 索引的架构与约束详情
├── evaluation-final.json
└── evidence/
├── manifest.json 每个归档文件的路径、类别、字节数、SHA-256
├── e2e/
└── screenshots/
平铺的文档路径(PLAN.md、STRUCTURE.md、SCENES.md 等)与旧版本完全一致,
所以任何已经在读 docs/tags/<Tag>/PLAN.md 的流程都不受影响。
归档如实反映 archive 运行那一刻的项目状态,删除也算:如果此时 memory/ 或
e2e/ 已经不存在,上一次运行留下的副本会被删除,而不是被带进新的快照。
怎么读归档¶
docs/tags/README.md—— 有哪些 tag,按版本排序,附发布日期、主题和来源 revision。docs/tags/<Tag>/SUMMARY.md—— 一屏之内讲清主题、交付的 mechanic、 变更的系统与场景、验证结论、已知限制,并链接到其余所有内容。docs/tags/<Tag>/README.md—— 逐文件的职责表,用来判断该打开哪份文档。- canonical 文档和
evidence/—— 内容完整但阅读成本高,只在摘要不够用时才打开。
SUMMARY.md 是检索入口,不是新的事实来源。它只复述 CHANGELOG.md、PLAN.md、
evaluation-final.json 和 final_report.json 中已确认的内容;Worker 的探索过程、
完整 Trace 和 MEMORY.md 不会进入摘要。摘要与 canonical 文档冲突时,
以 canonical 文档为准。
摘要只链接归档里真实存在的内容。项目没有 memory/ 目录、或回填的归档缺少某份
canonical 文档时,摘要里不会出现对应条目,而不是留下一个必定 404 的链接。
不可变语义¶
一个 tag 完全封存后,evidence/manifest.json 中会带上 "sealed": true。此后
archive 和 index 都会拒绝改动该目录并以 3 退出。这就是重复执行
/gm-finalize 不会静默改写历史的原因。
如果 manifest 缺失或写着 "sealed": false,说明是一次未完成的 finalize——
重新运行 archive 就是既定的恢复路径,不需要任何额外参数。--force 只用于
明确要求重新封存的场景。
失败时的写入顺序¶
有两条互相拉扯的规则:
- A.
docs/tags/README.md绝不能列出未封存的 tag。它是检索入口,而未封存的 归档仍然是随时可被覆盖的 partial snapshot。 - B. 失败的运行绝不能留下「已封存但内容不完整」的 tag,因为
archive和index都会拒绝改动已封存的 tag。
index 的写入顺序是:先写 SUMMARY.md 和该 tag 的 README.md,再用一次原子写入
提交 "sealed": true,最后才写父级索引——父级索引是纯派生状态,只根据磁盘上已经
提交的 manifest 渲染。
| 失败位置 | 结果 |
|---|---|
| seal 提交之前或提交本身 | tag 未封存,父级索引没被动过。重跑 index <Tag> 即可。 |
| 父级索引写入 | tag 已正确封存,索引只是漏了它。运行 reindex。 |
所有会改写归档的入口——archive、index --force、backfill --force——都会在替换
任何文件之前先认领该归档:如果它已封存就先把 tag 从父级索引里移除,再把 manifest
换成「进行中」标记,最后才开始写。因此写入中途失败留下的归档会明确显示为
「未封存、未收录」,而不是仍带着 sealed: true、但哈希已经对不上实际文件的状态。
如果失败的恰好是认领这一步,则什么都还没被改写,归档保持原样。
两条分支都不会让索引出现未封存的 tag,也都不会把已封存的归档锁死。backfill 用
同样的顺序:逐个目标封存,最后统一写派生索引;中途失败时先前的目标已封存、其余保持
未封存、索引只列实际封存的内容,直接重跑即可完成(已封存的目标会被跳过)。
所有生成文件都先写到同目录的临时文件再 rename 就位,因此写入中断不会把上一版文件截断。
/gm-finalize 的完成门禁校验同一组标记:归档必须有可解析的
evidence/manifest.json 且 "sealed" 严格为 true,父级索引必须已收录该 tag。
仅仅文件存在但未封存不算通过。
修复过期的父级索引¶
根据磁盘上已封存的归档重新生成 docs/tags/README.md,不改动任何归档。这是
index 自己无法收尾的那唯一一种失败的修复手段——seal 已经落盘、之后的索引刷新
没有成功。此时重跑 index 是错的:该 tag 确实已封存,只会返回 3。
子命令¶
| 命令 | 作用 |
|---|---|
python tools/seal_tag.py archive <Tag> |
把工作文档、memory/ 子目录和 e2e/ 证据复制到 docs/tags/<Tag>/,然后校验归档内 MEMORY.md 的链接。写入未封存的 manifest。 |
python tools/seal_tag.py index <Tag> |
生成 SUMMARY.md、该 tag 的 README.md、已封存的 manifest 和父级 docs/tags/README.md。这一步才算封存。 |
python tools/seal_tag.py backfill <Tag> / --all |
给旧版本封存的归档补上 README.md、SUMMARY.md 和 manifest;已封存的归档默认跳过,需要 --force 才会重新索引。 |
python tools/seal_tag.py reindex |
根据磁盘上已封存的归档重新生成 docs/tags/README.md,不改动任何归档。 |
python tools/seal_tag.py bundle <Tag> |
输出 /gm-finalize 写 CHANGELOG 所需的 JSON。 |
python tools/seal_tag.py reset |
清空 .godotmaker/stage.jsonl 并删除 metrics_current.jsonl。 |
退出码:0 成功 · 1 文件系统或运行时失败 · 2 缺少来源文件、归档内链接无法解析
或用法错误 · 3 该 tag 已封存。
MEMORY 链接校验¶
MEMORY.md 通过 memory/ 下的文件索引详细架构与约束。只归档索引而不归档这些文件,
等于冻结了一组失效链接,所以 archive 会一并复制 memory/,然后校验归档内的
MEMORY.md:
- 指向
memory/…但归档内没有对应文件、使用绝对路径、或越出归档目录的链接会 阻止封存(退出码 2)。修好根目录的MEMORY.md或补回缺失文件后重跑即可。 - 指向本来就不归档的项目文件(
src/player.gd、assets/…)的链接会记录到 manifest 的link_warnings,不阻塞流程。
HTML 注释和代码块中的链接会被忽略,因此 MEMORY.md 模板自带的示例条目不会
让第一次 finalize 失败。
回填旧归档¶
在这套结构出现之前创建的归档只有平铺文档,没有 README.md、SUMMARY.md 和
manifest,因此不会出现在父级索引里。执行一次回填即可:
回填只添加索引文件,绝不改写 canonical 文档——它会在前后分别计算这些文档的哈希,
一旦发现变化就报错退出。它也不会把今天的 memory/ 复制进历史 tag:那会把
当下的内容注入过去的快照。缺失的子目录会被记录为 manifest warning,该 tag 的
README 中完整性显示为 partial。
已经带有 sealed manifest 的归档会被跳过——重新索引会用本次运行解析出的 revision
覆盖它原本记录的封存 revision。确实需要重新索引时加 --force。
属于「正在进行中的 finalize」的归档同样会被跳过,且 --force 也不能覆盖这条:
它由 archive 写出但还没被 index 封存,外形和旧归档一模一样(有 PLAN.md、
未封存、没有 CHANGELOG.md),但用这些不完整的输入把它封存会让真正的 finalize
在 exit 3 上被锁死。正确做法是用 seal_tag.py index <Tag> 把它收尾。显式点名这样
的 tag 会直接报错,而不是静默跳过。
即使是复制到一半就中断的运行也一样。每个会改动归档的命令在第一次写入之前,都会把
manifest 换成标注 stage(archiving / indexing / backfilling)的「进行中」
标记,因此被中断的归档仍然可识别,不会退化成旧归档的形状。backfill 只会接续自己
中断的工作,不碰 finalize 的。
归档上已有的 warning——比如一次失败的 E2E 或截图复制——会被带进 backfill 写出的新
manifest,因此重写不会把一个 partial 归档悄悄标成 complete。反复跑 --force 会重新
推导出同样的条目,但不会堆积重复项。
对于实际被索引的归档,来源 revision 取自 git tag <Tag>(/gm-finalize 打的那个
commit),而不是今天的 HEAD;没有对应 git tag 的归档记录为 null,不会编造一个
revision。
/gm-finalize 永远不会自动执行 backfill。改写历史始终是用户明确发起的操作。