chore(卫生): 闸域表口径对齐机器+删退役仪表盘工具+清基线 08b 记录(2026-10-18 用户点令三项)
①architecture.md 闸域表第三列与机器侧不一致已修:原写代码域触发含 `game/`、`tools/`、`artwork/`,而 worktree-guard 现行判定只有 `_p.startswith("game/")`(:149-151)、run_all_gates.sh 头注也只认触及 game/——`7bdf53e3` 改判时漏改的第二处。同时查出 roadmap.md:88「重闸」步仍带同一条旧口径与「改闸件本身→全量」旧条款(该义务已由 7bdf53e3 换成「闸件自身改动不占合回全量,质量义务=落批时变异验证」),一并换代。
②删 `tools/loop_dashboard.py`(529 行):roadmap 早在 2026-09-27 重整批就宣布它退役、职责交 06 与状态记录,全仓现行文档/闸序列/技能零调用(git grep 只命中自身);它的主要数据源之一 active.md 快照节已于本日废除,「台账健康」栏(撞号检测与下一编号建议)随编号方案一起失效。它唯一不重复的能力=引用版本与被引文档现行版本的落后比对,已按现存格式重立为 `doc-consistency-check` 技能第 10 项人工核对(比对面从已消失的 07「当前批次来源」行改到 06 的闭合凭据)。试跑该新件即命中真问题:06 有一条凭据写「元进度.md v1.0」而该文档现行已 v1.21,另有一条 character-design.md v1.9 取不到现行版本可核。
③删 `game/tests/.gates/baseline.json` 的「08b active.md 体量」记录(基线 13 闸→12),本批 diff 因此触及 game/,按域判据跑全量档(代码域+文档域)。附带 `tools/path_check.py` 去掉已失效的 active.md 跳过项并把注释里「排除台账类文件」的旧说法改成与实际扫描面一致的表述(实查该工具扫的只有 .gd/.tscn 与 design/art/*.md,从未跳过 *checklist*/*memory*)。
This commit is contained in:
1 parent
ea69670c39
commit
c167affae2
5 files changed
+9
-544
No files matched your search
@@ -195,8 +195,8 @@
|
||||
|
||||
| 域 | 闸 | 本批改动落在这些路径即属该域 |
|
||||
|---|---|---|
|
||||
| **代码域** | run_smoke、run_all、probe_gate `--click`、probe_gate `--full`、render_gate、static_scan、回放确定性对拍(条件件) | `game/`、`tools/`、`artwork/` |
|
||||
| **文档域** | gdd_lint、doc_link_audit、doc_term_audit、流水线闸 | `design/`、`roadmap.md`、`CLAUDE.md` |
|
||||
| **代码域** | run_smoke、run_all、probe_gate `--click`、probe_gate `--full`、render_gate、static_scan、回放确定性对拍(条件件) | **唯一触发=`game/`**(游戏工程代码、测试、探针、场景、数据表)。2026-10-18 用户裁「跨领域执法禁绝」(提交 `7bdf53e3`)后,原 `tools/`、`artwork/` 两前缀不再把改动拉进代码域 |
|
||||
| **文档域** | gdd_lint、doc_link_audit、doc_term_audit、流水线闸 | 一切非 `game/` 的路径:`design/`、根规则文件(`CLAUDE.md`/`roadmap.md`)、`tools/` 工具脚本、`.zcode/` 配置、`sim-data/`、`reports/` |
|
||||
| **两域都含** | repo_structure_audit(熵增闸:顶层目录/根文件/图像白名单/退役路径/design 子目录五查——文档批新增或挪动 `.md` 同样会绊它;纯文本扫描秒级) | — |
|
||||
|
||||
口径三条:**域划分的唯一出处是 `tools/run_all_gates.sh`**(`--code`/`--docs`/不带参数=全量),本表是它的现状登记、不是第二份清单;改闸件本身或一次改动跨两域 → 跑全量档(§6.2 共享件例外);合回闸按 merge diff 路径推出应验的域,再校验回执的 `domain` 字段与新增红。代码域跑前先过**素材齐盘预检**——`game/assets/art/**` 与一切 `*.import` 边车都不入 git(2026-09-29 裁定),worktree 冷检出必空盘,空盘态冒烟 V1 成片假红、渲染闸更是一段 golden 都不比。
|
||||
|
||||
@@ -33,10 +33,6 @@
|
||||
"exit": "0",
|
||||
"fp": "green"
|
||||
},
|
||||
"08b active.md 体量": {
|
||||
"exit": "0",
|
||||
"fp": "green"
|
||||
},
|
||||
"08c 流水线闸": {
|
||||
"exit": "0",
|
||||
"fp": "green"
|
||||
|
||||
+1
-1
@@ -85,7 +85,7 @@
|
||||
2. **T0 开工**:建 worktree + 基线复读记档(冒烟/探针/三闸/MC 对拍基准)+ `git status` 归属核对(并行遗留只标旗不碰)。
|
||||
3. **循环骨架**:一轮 = 一个任务卡——读卡 → 对事实源 → 实装 → 轻闸全绿 → 提交(代拍板随 commit 记录)→ 状态记录追加(查撞号)→ 下一轮。单 worktree 多点提交一次合并;轮内禁开第二个 worktree;基线每轮开工时点重测;断点续跑 = `git log` 对任务卡定轮次 + 重跑当轮轻闸。
|
||||
4. **轻闸**(每轮必跑,headless):`tools/run_all_gates.sh --quick`(迭代中段自检档)。
|
||||
5. **重闸**(批末 Tn):`tools/run_all_gates.sh` **按本批改动域跑**(只碰 `game/`、`tools/`、`artwork/` → `--code`;只碰 `design/` 与根规则文件 → `--docs`;跨两域或改闸件本身 → 不带参数全量),所选域全绿——闸件清单、顺序与域划分只在该脚本里定义,本文件与 CLAUDE.md/规约均不另列;MC 逐位对拍(涉核心)+ 素材批加机检断言另行。
|
||||
5. **重闸**(批末 Tn):`tools/run_all_gates.sh` **按本批改动域跑**(触及 `game/` → `--code`,这是代码域唯一触发;不触及 `game/` 的一切路径 → `--docs`;一次改动同批混触两域 → 不带参数全量),所选域全绿——闸件清单、顺序与域划分只在该脚本里定义,本文件与 CLAUDE.md/规约均不另列;改闸件自身(`tools/` 下机检脚本与闸壳)不占合回全量,质量义务=落批时的变异验证;涉核心的 MC 与基线逐位比对、素材批的机检断言另行。
|
||||
6. **人闸**(停机交付):真人目检清单 + 代拍板追认清单 + 异步终审触点(样张类)——本循环内唯一必经人审。
|
||||
7. **异常分级**:L1 自救(一次重试)→ L2 记录继续(连续 3 次同红灯升级)→ L3 停机写报告等用户。
|
||||
8. **批次完成后的五步登记**:①06 条目状态按 doc-conventions §12.1 推进(做完整条删除)②本文件顶部「当前位」三行换代,涉及里程碑移动时同步里程碑表 ③**实现批禁写本文件与 `design/`**(2026-10-17 用户裁:代码实现批只准写 `reports/`),该改哪一行写进 `reports/iter<N>-反哺点-<日期>.md`,由文档批执行 ④阶段变更改 stage.txt ⑤批末闸序列**按本批改动域**全绿留痕(回执 commit 对齐分支尖)。
|
||||
|
||||
@@ -1,529 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
r"""loop_dashboard.py — 迭代闭环仪表盘生成器
|
||||
|
||||
用法:
|
||||
python tools/loop_dashboard.py # 生成 sim-data/loop-dashboard.html + 控制台摘要
|
||||
python tools/loop_dashboard.py --open # 生成后用系统默认浏览器打开
|
||||
python tools/loop_dashboard.py --out <path>
|
||||
|
||||
定位: 项目开发循环 = 「完善文档 → 更新垂直切片游玩体验 → 再完善文档」。
|
||||
闭环状态分散在六处——active.md 快照节 / 台账 / GDD 版本史 / 06-待办清单 /
|
||||
07-体验验证清单 / git 在飞改动——本工具把六处聚合成一个只读视图,每次迭代
|
||||
收口后重跑一次即可看清: 走到哪了、什么在飞、什么待拍板、什么验证项已落后于文档改动。
|
||||
|
||||
只读工具: 不改任何被扫描的文件,产物只有仪表盘 HTML 本身。
|
||||
|
||||
解析约定(跟随仓库现行格式,格式变了这里要跟着改):
|
||||
- active.md 台账区 = 文件最后一个二级标题之后(新条目追加在文件末尾)
|
||||
- GDD 版本 = 「版本历史」表中版本号最大的一行(行按时间追加)
|
||||
- 06-待办清单条目 = 四级标题,状态 = 其下「**状态**:」行
|
||||
- 07-体验验证清单头部「当前批次来源」行内引用的 vX.Y 与 GDD 现行版本比对,
|
||||
得出「体验验证清单落后于文档改动」的失效面
|
||||
"""
|
||||
import argparse
|
||||
import html
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
from collections import Counter, OrderedDict
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
GDD_DIR = ROOT / "design" / "gdd"
|
||||
ACTIVE_MD = ROOT / "active.md"
|
||||
ROADMAP_MD = ROOT / "roadmap.md"
|
||||
TODO_MD = GDD_DIR / "06-待办清单.md"
|
||||
VERIFY_MD = GDD_DIR / "07-体验验证清单.md"
|
||||
STAGE_TXT = ROOT / "stage.txt"
|
||||
DEFAULT_OUT = ROOT / "sim-data" / "loop-dashboard.html"
|
||||
|
||||
LOOP_ORDER = ["顶层文档", "排雷循环", "战斗循环", "冒险循环", "表现与支撑"]
|
||||
|
||||
# 状态分类前缀 → 桶。解决即移除(2026-09-18 新口径)下「已解决/已裁决」残留 = 清理债。
|
||||
RESOLVED_PREFIX = ("已解决", "已裁决")
|
||||
PARTIAL_PREFIX = ("部分收口", "部分清偿", "已部分收口", "已部分清偿")
|
||||
OPEN_PREFIX = ("待办",)
|
||||
DECIDED_PREFIX = ("方向已拍板", "已拍板")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- git 侧
|
||||
|
||||
def git(args):
|
||||
try:
|
||||
return subprocess.run(
|
||||
["git", "-c", "core.quotepath=false"] + args,
|
||||
capture_output=True, text=True, encoding="utf-8", errors="replace",
|
||||
cwd=ROOT, check=True,
|
||||
).stdout
|
||||
except (subprocess.CalledProcessError, FileNotFoundError):
|
||||
return ""
|
||||
|
||||
|
||||
def classify_path(rel: str):
|
||||
rel = rel.replace("\\", "/")
|
||||
if rel.startswith("design/gdd/"):
|
||||
return "GDD 文档"
|
||||
if rel == "active.md" or rel == "roadmap.md":
|
||||
return "会话与路线"
|
||||
if rel.startswith("design/qa/") or rel.startswith("design/research/"):
|
||||
return "验证与调研"
|
||||
if rel.startswith("design/"):
|
||||
return "设计侧其他(registry/art/dev)"
|
||||
if rel.startswith("game/"):
|
||||
if rel.endswith(".gd"):
|
||||
return "游戏代码"
|
||||
if rel.endswith(".tres"):
|
||||
return "数据表"
|
||||
return "游戏工程其他"
|
||||
if rel.startswith("tools/"):
|
||||
return "工具"
|
||||
if rel.startswith("sim-data/"):
|
||||
return "数据产物"
|
||||
return "其他"
|
||||
|
||||
|
||||
def scan_git():
|
||||
dirty, untracked = [], []
|
||||
for line in git(["status", "--porcelain"]).splitlines():
|
||||
if not line.strip():
|
||||
continue
|
||||
state, path = line[:2], line[3:].strip().strip('"')
|
||||
if state.startswith("??"):
|
||||
untracked.append(path)
|
||||
else:
|
||||
dirty.append((state.strip() or "M", path))
|
||||
log = [l for l in git(["log", "--oneline", "-5"]).splitlines() if l.strip()]
|
||||
grouped = Counter(classify_path(p) for _, p in dirty)
|
||||
gdd_dirty = [p for s, p in dirty if classify_path(p) == "GDD 文档"]
|
||||
return {
|
||||
"dirty": dirty, "untracked": untracked, "log": log,
|
||||
"grouped": grouped, "gdd_dirty": gdd_dirty,
|
||||
}
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- GDD 清单
|
||||
|
||||
VERSION_ROW_RE = re.compile(r"^\|\s*v?(\d+)\.(\d+)\s*\|\s*(\d{4}-\d{2}-\d{2})")
|
||||
|
||||
|
||||
def parse_doc(path: Path):
|
||||
try:
|
||||
text = path.read_text(encoding="utf-8")
|
||||
except UnicodeDecodeError:
|
||||
text = path.read_text(encoding="utf-8", errors="replace")
|
||||
title = next((l.lstrip("# ").strip() for l in text.splitlines() if l.startswith("# ")), path.stem)
|
||||
rel = path.relative_to(ROOT).as_posix()
|
||||
parts = path.relative_to(GDD_DIR).parts
|
||||
group = parts[0] if len(parts) > 1 else "顶层文档"
|
||||
version, vdate = None, None
|
||||
in_hist = False
|
||||
for line in text.splitlines():
|
||||
if line.startswith("## "):
|
||||
in_hist = "版本历史" in line
|
||||
continue
|
||||
if in_hist:
|
||||
m = VERSION_ROW_RE.match(line)
|
||||
if m:
|
||||
v = (int(m.group(1)), int(m.group(2)))
|
||||
if version is None or v > version:
|
||||
version, vdate = v, m.group(3)
|
||||
mtime = datetime.fromtimestamp(path.stat().st_mtime).strftime("%m-%d %H:%M")
|
||||
return {
|
||||
"path": rel, "name": path.stem, "title": title, "group": group,
|
||||
"version": f"v{version[0]}.{version[1]}" if version else "—",
|
||||
"vdate": vdate or "—", "mtime": mtime,
|
||||
}
|
||||
|
||||
|
||||
def scan_gdd(git_info):
|
||||
dirty_set = {p.replace("\\", "/") for p in git_info["gdd_dirty"]}
|
||||
docs = [parse_doc(p) for p in sorted(GDD_DIR.rglob("*.md"))]
|
||||
for d in docs:
|
||||
d["dirty"] = d["path"] in dirty_set
|
||||
return docs
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- active.md
|
||||
|
||||
LEDGER_ENTRY_RE = re.compile(r"^(\d+)\.\s+\*\*")
|
||||
|
||||
|
||||
def scan_active():
|
||||
info = {"snapshot_lines": [], "updated_line": "", "next_line": "",
|
||||
"entries": [], "collisions": [], "next_number": None, "tail_head": ""}
|
||||
if not ACTIVE_MD.exists():
|
||||
info["error"] = "active.md 不存在"
|
||||
return info
|
||||
lines = ACTIVE_MD.read_text(encoding="utf-8").splitlines()
|
||||
# 快照节(标题带纪律后缀,按关键词匹配)
|
||||
in_snap = False
|
||||
for line in lines:
|
||||
if line.startswith("## "):
|
||||
in_snap = "当前状态快照" in line
|
||||
if in_snap:
|
||||
continue
|
||||
if in_snap and line.strip():
|
||||
info["snapshot_lines"].append(line.strip())
|
||||
for ln in info["snapshot_lines"]:
|
||||
if ln.startswith("- **更新于**"):
|
||||
info["updated_line"] = ln
|
||||
if ln.startswith("- **下一步顺序**"):
|
||||
info["next_line"] = ln
|
||||
# 台账区 = 最后一个二级标题之后
|
||||
last_h2 = max((i for i, l in enumerate(lines) if l.startswith("## ")), default=-1)
|
||||
tail = lines[last_h2 + 1:]
|
||||
info["tail_head"] = lines[last_h2].lstrip("# ").strip() if last_h2 >= 0 else ""
|
||||
numbers = []
|
||||
for line in tail:
|
||||
m = LEDGER_ENTRY_RE.match(line)
|
||||
if m:
|
||||
numbers.append(int(m.group(1)))
|
||||
info["entries"] = numbers
|
||||
seen = Counter(numbers)
|
||||
info["collisions"] = sorted(n for n, c in seen.items() if c > 1)
|
||||
if numbers:
|
||||
info["next_number"] = max(numbers) + 1
|
||||
return info
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- 06 待办清单
|
||||
|
||||
def scan_todo():
|
||||
items = []
|
||||
if not TODO_MD.exists():
|
||||
return items
|
||||
loop = system = ""
|
||||
cur = None
|
||||
for line in TODO_MD.read_text(encoding="utf-8").splitlines():
|
||||
if line.startswith("## "):
|
||||
loop, system = line.lstrip("# ").strip(), ""
|
||||
elif line.startswith("### "):
|
||||
system = line.lstrip("# ").strip()
|
||||
elif line.startswith("#### "):
|
||||
cur = {
|
||||
"loop": loop, "system": system,
|
||||
"title": line.lstrip("# ").strip(),
|
||||
"status": "",
|
||||
}
|
||||
items.append(cur)
|
||||
elif cur is not None and line.startswith("**状态**"):
|
||||
cur["status"] = line.replace("**状态**", "").lstrip(":: ").strip()
|
||||
cur = None
|
||||
for it in items:
|
||||
s = it["status"]
|
||||
if s.startswith(RESOLVED_PREFIX):
|
||||
it["bucket"] = "解决态残留"
|
||||
elif s.startswith(PARTIAL_PREFIX):
|
||||
it["bucket"] = "部分收口"
|
||||
elif s.startswith(OPEN_PREFIX):
|
||||
it["bucket"] = "待办"
|
||||
elif s.startswith(DECIDED_PREFIX):
|
||||
it["bucket"] = "拍板待执行"
|
||||
else:
|
||||
it["bucket"] = "其他"
|
||||
return items
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- 07 体验验证清单
|
||||
|
||||
VERSION_REF_RE = re.compile(r"(战斗结算|增益状态|地图战斗资源|敌人系统|行动经济|棋盘生成|排雷玩法|难度曲线与Boss|元进度|关卡节点地图|HUD框架|战斗表现|数据配置|存档序列化|音频|教学引导|设置与本地化|地形变异|冒险结构|魔法道具)\s*(v\d+\.\d+)")
|
||||
|
||||
|
||||
def scan_verify(docs):
|
||||
info = {"items": [], "obligations": [], "source_line": "", "refs": [], "error": None}
|
||||
if not VERIFY_MD.exists():
|
||||
info["error"] = "07-体验验证清单.md 不存在"
|
||||
return info
|
||||
text = VERIFY_MD.read_text(encoding="utf-8")
|
||||
lines = text.splitlines()
|
||||
section = ""
|
||||
for line in lines:
|
||||
if line.startswith("## "):
|
||||
section = line.lstrip("# ").strip()
|
||||
continue
|
||||
if line.startswith("### ") and "观测项" in section:
|
||||
info["items"].append(line.lstrip("# ").strip())
|
||||
elif line.startswith("- ") and "重测批义务" in section:
|
||||
info["obligations"].append(line[2:].strip())
|
||||
m = re.search(r"当前批次来源:(.+)", text)
|
||||
if m:
|
||||
info["source_line"] = m.group(1).strip()
|
||||
by_name = {d["name"]: d for d in docs}
|
||||
for name, ver in VERSION_REF_RE.findall(info["source_line"]):
|
||||
cur = by_name.get(name)
|
||||
info["refs"].append({
|
||||
"doc": name, "ref": ver, "current": cur["version"] if cur else "?",
|
||||
"stale": bool(cur and cur["version"] != "—" and cur["version"] != ver),
|
||||
})
|
||||
return info
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- 路线图
|
||||
|
||||
def scan_roadmap():
|
||||
steps = []
|
||||
if not ROADMAP_MD.exists():
|
||||
return steps
|
||||
in_route = False
|
||||
for line in ROADMAP_MD.read_text(encoding="utf-8").splitlines():
|
||||
if line.startswith("## "):
|
||||
in_route = "路线图" in line and "维护" not in line
|
||||
continue
|
||||
if in_route and re.match(r"^\d+\.\s", line):
|
||||
steps.append(line.strip())
|
||||
return steps
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- 控制台摘要
|
||||
|
||||
def print_summary(stage, git_info, docs, active, todos, verify, out_path):
|
||||
def p(s=""):
|
||||
print(s)
|
||||
p("═" * 62)
|
||||
p(f"迭代闭环仪表盘 · {datetime.now().strftime('%Y-%m-%d %H:%M')} · 阶段: {stage}")
|
||||
p("═" * 62)
|
||||
if active.get("updated_line"):
|
||||
p(active["updated_line"].lstrip("- "))
|
||||
g = git_info["grouped"]
|
||||
p(f"在飞改动: {len(git_info['dirty'])} 文件(" +
|
||||
" / ".join(f"{k} {v}" for k, v in g.most_common()) +
|
||||
f")+ 未跟踪 {len(git_info['untracked'])}")
|
||||
dirty_docs = [d for d in docs if d["dirty"]]
|
||||
versioned = [d for d in docs if d["version"] != "—"]
|
||||
newest = max(versioned, key=lambda d: d["vdate"], default=None)
|
||||
p(f"GDD: {len(docs)} 份,其中在飞 {len(dirty_docs)} 份"
|
||||
+ (f";最近改版 {newest['name']} {newest['version']}({newest['vdate']})" if newest else ""))
|
||||
buckets = Counter(t["bucket"] for t in todos)
|
||||
p(f"待办清单: {len(todos)} 条(待办 {buckets.get('待办', 0)} / 部分收口 {buckets.get('部分收口', 0)}"
|
||||
f" / 拍板待执行 {buckets.get('拍板待执行', 0)} / 解决态残留 {buckets.get('解决态残留', 0)}"
|
||||
f"←按「解决即移除」口径应移除)")
|
||||
p(f"体验验证: {len(verify['items'])} 项待真人观测")
|
||||
if active["collisions"]:
|
||||
p(f"台账: 最新编号 {active['entries'][-1]},下一编号建议 {active['next_number']}"
|
||||
f"(撞号: {', '.join(str(n) for n in active['collisions'])})")
|
||||
else:
|
||||
p(f"台账: 最新编号 {active['entries'][-1] if active['entries'] else '—'},下一编号建议 {active['next_number']}")
|
||||
stale = [r for r in verify["refs"] if r["stale"]]
|
||||
if stale:
|
||||
p("⚠ 07-体验验证清单引用版本已落后:")
|
||||
for r in stale:
|
||||
p(f" {r['doc']}: 07 引 {r['ref']} / 现行 {r['current']}")
|
||||
if active.get("next_line"):
|
||||
p(active["next_line"].lstrip("- "))
|
||||
p(f"已生成: {out_path.relative_to(ROOT).as_posix()}")
|
||||
p("═" * 62)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- HTML
|
||||
|
||||
CSS = """
|
||||
:root{--bg:#14161a;--card:#1d2026;--line:#2c3038;--tx:#d8dce3;--dim:#8b919c;
|
||||
--acc:#5b9dd9;--warn:#d9a05b;--bad:#d95b6a;--ok:#6fbf7f;}
|
||||
*{box-sizing:border-box}
|
||||
body{margin:0;background:var(--bg);color:var(--tx);
|
||||
font-family:"Segoe UI","Microsoft YaHei","PingFang SC",sans-serif;font-size:14px;line-height:1.6}
|
||||
main{max-width:1080px;margin:0 auto;padding:24px 20px 60px}
|
||||
h1{font-size:22px;margin:0 0 4px}
|
||||
h2{font-size:17px;margin:36px 0 10px;padding-bottom:6px;border-bottom:1px solid var(--line)}
|
||||
.meta{color:var(--dim);font-size:12.5px;margin-bottom:8px}
|
||||
.grid{display:grid;grid-template-columns:1fr 1fr;gap:14px;align-items:start}
|
||||
@media(max-width:800px){.grid{grid-template-columns:1fr}}
|
||||
.card{background:var(--card);border:1px solid var(--line);border-radius:8px;padding:14px 16px}
|
||||
.card h3{margin:0 0 8px;font-size:14px;color:var(--acc)}
|
||||
.card ul{margin:0;padding-left:18px}
|
||||
.card li{margin:3px 0}
|
||||
table{width:100%;border-collapse:collapse;background:var(--card);
|
||||
border:1px solid var(--line);border-radius:8px;overflow:hidden;font-size:13px}
|
||||
th,td{padding:6px 10px;text-align:left;border-bottom:1px solid var(--line);vertical-align:top}
|
||||
th{color:var(--dim);font-weight:600;background:#22252c}
|
||||
tr:last-child td{border-bottom:none}
|
||||
.dirty{color:var(--warn);font-weight:600}
|
||||
.tag{display:inline-block;padding:0 8px;border-radius:10px;font-size:12px;margin-right:6px}
|
||||
.tag.open{background:#2b3a4a;color:#9cc7ec}
|
||||
.tag.partial{background:#463d2b;color:#e0c08a}
|
||||
.tag.decided{background:#3a3350;color:#c3b3ec}
|
||||
.tag.resolved{background:#3c2b30;color:#e0999f}
|
||||
.stale{color:var(--bad);font-weight:600}
|
||||
.ok{color:var(--ok)}
|
||||
.dim{color:var(--dim)}
|
||||
footer{margin-top:40px;color:var(--dim);font-size:12px;border-top:1px solid var(--line);padding-top:12px}
|
||||
code{background:#262a31;padding:1px 5px;border-radius:4px;font-size:12.5px}
|
||||
"""
|
||||
|
||||
|
||||
def esc(s):
|
||||
return html.escape(str(s))
|
||||
|
||||
|
||||
def bucket_tag(bucket):
|
||||
cls = {"待办": "open", "部分收口": "partial", "拍板待执行": "decided", "解决态残留": "resolved"}.get(bucket, "partial")
|
||||
return f'<span class="tag {cls}">{esc(bucket)}</span>'
|
||||
|
||||
|
||||
def render_html(stage, git_info, docs, active, todos, verify, roadmap, out_path):
|
||||
now = datetime.now().strftime("%Y-%m-%d %H:%M")
|
||||
buckets = Counter(t["bucket"] for t in todos)
|
||||
dirty_docs = [d for d in docs if d["dirty"]]
|
||||
stale_refs = [r for r in verify["refs"] if r["stale"]]
|
||||
|
||||
# 快照节
|
||||
snap = "".join(f"<li>{esc(l.lstrip('- '))}</li>" for l in active["snapshot_lines"])
|
||||
# 近期提交
|
||||
commits = "".join(f"<li><code>{esc(l.split()[0])}</code> {esc(l[l.index(' ')+1:][:90])}</li>" for l in git_info["log"])
|
||||
# 在飞分组
|
||||
fly_rows = "".join(
|
||||
f"<tr><td>{esc(k)}</td><td>{v}</td></tr>" for k, v in git_info["grouped"].most_common())
|
||||
untracked = "".join(f"<li>{esc(u)}</li>" for u in git_info["untracked"]) or "<li class='dim'>无</li>"
|
||||
# GDD 清单
|
||||
doc_rows = []
|
||||
for group in LOOP_ORDER:
|
||||
group_docs = [d for d in docs if d["group"] == group]
|
||||
if not group_docs:
|
||||
continue
|
||||
doc_rows.append(
|
||||
f"<tr><td colspan='5' class='dim' style='background:#22252c'>{esc(group)}({len(group_docs)})</td></tr>")
|
||||
for d in sorted(group_docs, key=lambda x: x["mtime"], reverse=True):
|
||||
mark = ' <span class="dirty">在飞</span>' if d["dirty"] else ""
|
||||
doc_rows.append(
|
||||
f"<tr><td>{esc(d['name'])}{mark}</td>"
|
||||
f"<td>{esc(d['version'])}</td><td class='dim'>{esc(d['vdate'])}</td>"
|
||||
f"<td class='dim'>{esc(d['mtime'])}</td>"
|
||||
f"<td class='dim'>{esc(d['title'][:40])}</td></tr>")
|
||||
# 06 待办
|
||||
todo_by_loop = OrderedDict((g, []) for g in LOOP_ORDER)
|
||||
for t in todos:
|
||||
todo_by_loop.setdefault(t["loop"], []).append(t)
|
||||
todo_cards = []
|
||||
for loop, items in todo_by_loop.items():
|
||||
if not items:
|
||||
continue
|
||||
open_n = sum(1 for i in items if i["bucket"] in ("待办", "部分收口", "拍板待执行"))
|
||||
lis = "".join(
|
||||
f"<li>{bucket_tag(i['bucket'])}{esc(i['system'])} · {esc(i['title'][:46])}</li>"
|
||||
for i in items if i["bucket"] != "解决态残留")
|
||||
resolved = [i for i in items if i["bucket"] == "解决态残留"]
|
||||
res_note = (f"<p class='dim' style='margin:8px 0 0'>另有 {len(resolved)} 条解决态残留"
|
||||
f"(按「解决即移除」口径待清)</p>" if resolved else "")
|
||||
todo_cards.append(
|
||||
f"<div class='card'><h3>{esc(loop)} · 开放 {open_n}/{len(items)}</h3>"
|
||||
f"<ul>{lis}</ul>{res_note}</div>")
|
||||
resolved_all = [t for t in todos if t["bucket"] == "解决态残留"]
|
||||
# 循环级计数(四问卡片②的汇总表)
|
||||
loop_rows = ""
|
||||
for loop, items in todo_by_loop.items():
|
||||
if not items:
|
||||
continue
|
||||
open_n = sum(1 for i in items if i["bucket"] in ("待办", "部分收口", "拍板待执行"))
|
||||
loop_rows += f"<tr><td>{esc(loop)}</td><td>{open_n}</td><td>{len(items)}</td></tr>"
|
||||
# 07
|
||||
verify_items = "".join(f"<li>{esc(i)}</li>" for i in verify["items"]) or "<li class='dim'>—</li>"
|
||||
obligations = "".join(f"<li>{esc(o)}</li>" for o in verify["obligations"]) or "<li class='dim'>—</li>"
|
||||
ref_rows = ""
|
||||
for r in verify["refs"]:
|
||||
verdict = '<span class="stale">已落后</span>' if r["stale"] else '<span class="ok">一致</span>'
|
||||
ref_rows += (f"<tr><td>{esc(r['doc'])}</td><td>{esc(r['ref'])}</td><td>{esc(r['current'])}</td>"
|
||||
f"<td>{verdict}</td></tr>")
|
||||
if not ref_rows:
|
||||
ref_rows = "<tr><td colspan='4' class='dim'>07 头部未解析出文档版本引用</td></tr>"
|
||||
# 台账健康
|
||||
if active["entries"]:
|
||||
coll = (f"检出撞号 {', '.join(str(n) for n in active['collisions'])}(编号有效、历史不改写)"
|
||||
if active["collisions"] else "无撞号")
|
||||
ledger_html = (f"<p>台账区(「{esc(active['tail_head'])}」之后)最新编号 "
|
||||
f"<b>{active['entries'][-1]}</b>,下一编号建议 <b>{active['next_number']}</b>;{esc(coll)}。</p>")
|
||||
else:
|
||||
ledger_html = "<p class='dim'>未解析出台账条目</p>"
|
||||
# 路线图
|
||||
route = "".join(f"<li>{esc(s)}</li>" for s in roadmap) or "<li class='dim'>—</li>"
|
||||
|
||||
return f"""<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<title>迭代闭环仪表盘 · Dungeon Sweeper</title>
|
||||
<style>{CSS}</style>
|
||||
</head>
|
||||
<body>
|
||||
<main>
|
||||
<h1>迭代闭环仪表盘</h1>
|
||||
<div class="meta">阶段: {esc(stage)} · 生成于 {now} · 只读视图,重跑 <code>python tools/loop_dashboard.py</code> 刷新</div>
|
||||
|
||||
<h2>当前状态快照(active.md)</h2>
|
||||
<div class="card"><ul>{snap}</ul></div>
|
||||
|
||||
<h2>迭代闭环四问</h2>
|
||||
<div class="grid">
|
||||
<div class="card"><h3>① 什么在飞(git 未提交 {len(git_info['dirty'])} + 未跟踪 {len(git_info['untracked'])})</h3>
|
||||
<table><tr><th>分组</th><th>文件数</th></tr>{fly_rows}</table>
|
||||
<p class="dim" style="margin:8px 0 0">未跟踪: </p><ul>{untracked}</ul></div>
|
||||
<div class="card"><h3>② 什么待拍板 / 待执行(06 清单 {len(todos)} 条)</h3>
|
||||
<p>待办 {buckets.get('待办', 0)} · 部分收口 {buckets.get('部分收口', 0)} · 拍板待执行 {buckets.get('拍板待执行', 0)} ·
|
||||
<span class="stale">解决态残留 {buckets.get('解决态残留', 0)}</span>(按「解决即移除」口径待清)</p>
|
||||
<table><tr><th>循环</th><th>开放</th><th>总条数</th></tr>{"".join(loop_rows)}</table>
|
||||
<p class="dim" style="margin:8px 0 0">完整清单见下方「待办清单全目」专区。</p></div>
|
||||
<div class="card"><h3>③ 什么要人玩(07 体验验证 {len(verify['items'])} 项)</h3>
|
||||
<ul>{verify_items}</ul></div>
|
||||
<div class="card"><h3>④ 验证是否落后于文档(失效面检测)</h3>
|
||||
<p class="dim">07 头部批次来源: {esc(verify['source_line'][:120])}</p>
|
||||
<table><tr><th>文档</th><th>07 引用</th><th>现行版本</th><th>判定</th></tr>{ref_rows}</table>
|
||||
{'<p class="stale">存在落后引用——07 观测项基于旧行为,验收读数以重测批义务为准。</p>' if stale_refs else '<p class="ok">引用与现行版本一致。</p>'}
|
||||
<h3 style="margin-top:12px">重测批义务(07 原文)</h3>
|
||||
<ul>{obligations}</ul></div>
|
||||
</div>
|
||||
|
||||
<h2>GDD 文档新鲜度({len(docs)} 份,按改动时间)</h2>
|
||||
<table><tr><th>文档</th><th>版本</th><th>版本日期</th><th>文件改动</th><th>标题</th></tr>{"".join(doc_rows)}</table>
|
||||
|
||||
<h2>待办清单全目(按循环)</h2>
|
||||
<div class="grid">{"".join(todo_cards)}</div>
|
||||
{'<div class="card" style="margin-top:14px"><h3>解决态残留(按「解决即移除」口径应从 06 移除,去向归台账与 git 历史)</h3><ul>' + "".join(f"<li>{esc(t['system'])} · {esc(t['title'])}</li>" for t in resolved_all) + '</ul></div>' if resolved_all else ''}
|
||||
|
||||
<h2>路线图(roadmap.md)</h2>
|
||||
<div class="card"><ol>{route}</ol></div>
|
||||
|
||||
<h2>台账健康</h2>
|
||||
<div class="card">{ledger_html}</div>
|
||||
|
||||
<h2>近期提交</h2>
|
||||
<div class="card"><ul>{commits}</ul></div>
|
||||
|
||||
<footer>
|
||||
数据源: active.md · roadmap.md · stage.txt ·
|
||||
design/gdd/06-待办清单.md · design/gdd/07-体验验证清单.md · design/gdd/** · git status/log。
|
||||
生成工具 tools/loop_dashboard.py;六处信息源格式变更时同步改解析约定(见工具头注)。
|
||||
</footer>
|
||||
</main>
|
||||
</body>
|
||||
</html>"""
|
||||
|
||||
|
||||
def main():
|
||||
if hasattr(sys.stdout, "reconfigure"):
|
||||
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
|
||||
ap = argparse.ArgumentParser(description="迭代闭环仪表盘生成器")
|
||||
ap.add_argument("--out", default=str(DEFAULT_OUT))
|
||||
ap.add_argument("--open", action="store_true", help="生成后用默认浏览器打开")
|
||||
args = ap.parse_args()
|
||||
|
||||
stage = STAGE_TXT.read_text(encoding="utf-8").strip() if STAGE_TXT.exists() else "—"
|
||||
git_info = scan_git()
|
||||
docs = scan_gdd(git_info)
|
||||
active = scan_active()
|
||||
todos = scan_todo()
|
||||
verify = scan_verify(docs)
|
||||
roadmap = scan_roadmap()
|
||||
|
||||
out_path = Path(args.out)
|
||||
out_path.write_text(
|
||||
render_html(stage, git_info, docs, active, todos, verify, roadmap, out_path),
|
||||
encoding="utf-8")
|
||||
print_summary(stage, git_info, docs, active, todos, verify, out_path)
|
||||
if args.open and sys.platform == "win32":
|
||||
os.startfile(str(out_path)) # noqa: 仅 Windows 便捷入口
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
+6
-8
@@ -188,14 +188,14 @@ def main():
|
||||
print(" · %-46s ← %s" % (ref, where[0]))
|
||||
|
||||
print("\n=== 退役命名扫描 ===")
|
||||
# ⚠ 口径纪律:退役命名**只在代码与素材清单里是违规**;文档的版本历史/
|
||||
# 台账在「叙述这些名字已退役」时提到它们属正常,不是违规。
|
||||
# 故扫描面只含:代码(.gd/.tscn) + 素材清单(design/art/*.md),
|
||||
# 且排除版本历史/台账类文件(active.md / *checklist* / *memory* / .zcode)。
|
||||
# 2026-10-07 初版全仓扫 .md ⇒ active.md/记忆文件里的历史记述全被误判,已修。
|
||||
# ⚠ 口径纪律:退役命名**只在代码与素材清单里是违规**;文档的版本历史与叙述性文件
|
||||
# 提到这些名字属正常,不是违规。故扫描面只含:代码(.gd/.tscn) + 素材清单
|
||||
# (design/art/*.md,且文件名不含「版本历史」),其余一律不扫;
|
||||
# .git/.workbuddy/.zcode/archive 在目录层就排除。
|
||||
# 2026-10-07 初版全仓扫 .md 曾把历史记述全判成违规,故收窄到上述两面(当日还额外跳过
|
||||
# 根目录会话状态文件,该文件已于 2026-10-18 用户裁废除,跳过项一并删除)。
|
||||
hits = 0
|
||||
scan_exts = (".gd", ".tscn")
|
||||
skip_names = ("active.md",)
|
||||
for dirpath, dirnames, names in os.walk(repo):
|
||||
dirnames[:] = [d for d in dirnames
|
||||
if d not in (".git", ".godot", "archive", "node_modules",
|
||||
@@ -205,8 +205,6 @@ def main():
|
||||
rel = os.path.relpath(fp, repo)
|
||||
if any(s in rel for s in (".workbuddy", ".zcode")):
|
||||
continue
|
||||
if fn in skip_names:
|
||||
continue
|
||||
is_code = fn.endswith(scan_exts)
|
||||
is_manifest = (fn.endswith(".md")
|
||||
and os.sep + "art" + os.sep in fp
|
||||
|
||||
Reference in new issue
Block a user