lili 8e2d5d9e50 docs(governance): 计划谱系单指针 上级: 立约(§10.8)+ docs-gate 七检加 G7 + plan-tree 查询视图 + 存量断链回填
新建 plan/设计档 frontmatter 必带一行 上级:(仓根相对路径),沿链 6 跳内达 canonical SoT;
全链/树/当前位置都是查询结果(plan-tree.py)而非维护对象,_index 在飞板保持线级、不加逐叶登记。
G7 拦三种腐坏:缺字段 / 上级死链 / 链断(中途档既非 canonical 又无上级),成环与超跳同挡(均已植坏档实测命中)。
存量修复:统一执行计划基建线补认领配置控制面设计(治真断链、含 07-01 SCA 选型反转口径),
配置控制面设计与阶段〇/一① plan 补 上级:;AGENTS.md/.agents README/feature-design-doc 模板同步七检口径。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 11:45:32 -07:00

95 lines
3.7 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""计划谱系树(engineering-conventions §10.8 的查询视图 · 2026-07-02 立)
沿 frontmatter `上级:` 单指针把 plan/设计档拼成树打印,回答两个问题:
「这份计划在整个上线计划的哪个位置」「某条线当前推进到哪份文档」。
谱系的唯一事实 = 每档一行 `上级:`(docs-gate G7 验证);本工具只是查询视图,不维护任何状态。
用法:python3 .agents/tools/plan-tree.py [仓库根,缺省=脚本上两级]
"""
import os
import re
import sys
ROOT = os.path.abspath(sys.argv[1]) if len(sys.argv) > 1 else os.path.abspath(
os.path.join(os.path.dirname(__file__), "..", ".."))
DATED = re.compile(r"^\d{4}-\d{2}-\d{2}-")
def frontmatter(fp):
"""浅解析 frontmatter(与 docs-gate.py 同口径,只认顶层 `key: value` 行)。"""
try:
with open(fp, encoding="utf-8", errors="ignore") as f:
lines = f.read().splitlines()
except OSError:
return {}
if not lines or lines[0].strip() != "---":
return {}
fm = {}
for line in lines[1:60]:
if line.strip() == "---":
break
m = re.match(r"^([A-Za-z一-鿿][\w\-一-鿿]*):\s*(.*)$", line)
if m:
fm[m.group(1)] = m.group(2).split("#")[0].strip()
return fm
def main():
# 节点 = 全部 plan/设计档 + 被 `上级:` 指到的任何 md(如 docs/mvp 主计划)
nodes = {} # 仓根相对路径 -> frontmatter
for base, suffix in (("docs/plans", "-plan.md"), ("docs/agent-specs", "-设计.md")):
d = os.path.join(ROOT, base)
if not os.path.isdir(d):
continue
for fn in sorted(os.listdir(d)):
if fn.endswith(suffix) and DATED.match(fn):
nodes[f"{base}/{fn}"] = frontmatter(os.path.join(d, fn))
children, orphans = {}, [] # 上级 -> [子];无上级且无人引用的存量档
for p, fm in sorted(nodes.items()):
parent = fm.get("上级")
if parent and os.path.isfile(os.path.join(ROOT, parent)):
children.setdefault(parent, []).append(p)
elif not parent:
orphans.append(p)
# 根 = 被引用为上级、但自身不在(或不再向上指)的档;补读其 frontmatter 取状态
referenced = set(children)
for p in sorted(referenced):
if p not in nodes:
nodes[p] = frontmatter(os.path.join(ROOT, p))
roots = [p for p in sorted(referenced) if not nodes[p].get("上级") or nodes[p].get("上级") not in referenced.union(nodes)]
def label(p):
fm = nodes.get(p, {})
tags = []
if fm.get("canonical") == "true":
tags.append("canonical")
status = (fm.get("status") or fm.get("topic") or "").strip()
if status:
tags.append(status[:44] + ("" if len(status) > 44 else ""))
return f"{p}" + (f" {' · '.join(tags)}" if tags else "")
def show(p, prefix=""):
kids = children.get(p, [])
for i, k in enumerate(kids):
last = i == len(kids) - 1
print(prefix + ("└─ " if last else "├─ ") + label(k))
show(k, prefix + (" " if last else ""))
if not roots:
print("(还没有任何档挂谱系:给 plan/设计档 frontmatter 加 `上级:` 后再跑)")
for r in roots:
print(label(r))
show(r)
# 未挂谱系的存量留痕档只报数,不刷屏(§10.8:存量不强制回填)
if orphans:
print(f"\n未挂谱系的存量档 {len(orphans)} 份(留痕层,按需回填 `上级:`;明细加 --orphans 查看)")
if "--orphans" in sys.argv:
for p in orphans:
print(f" {p}")
if __name__ == "__main__":
main()