新建 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>
95 lines
3.7 KiB
Python
95 lines
3.7 KiB
Python
#!/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()
|