merge: 卫生批入库(分支 hygiene-20261018 尖 53ad8c9e)——architecture 闸域表与 roadmap 重闸步的触发路径口径对齐机器(代码域唯一触发=game/,7bdf53e3 漏改两处);删退役仪表盘 tools/loop_dashboard.py(529 行,其独有的版本落后比对按现存格式重立为 doc-consistency-check 第 10 项)+tools/path_check.py 去掉失效跳过项;06 登记 L3 屏壳探针被共享存档断点污染一条(工程直入·待写卡,判据三条在内)+配比预设条凭据版本复核注记;baseline.json 的 08b 记录本批不动(删它要触 game/ 且会撞上那两道假红),归该修复批顺带清。文档域五闸零新增红,回执=53ad8c9e。

This commit is contained in:
DKChan committed 2026-10-18 20:20:16 +08:00
commit 9ebd2a6430
5 files changed
+22 -541

No files matched your search

+2 -2
View File
@@ -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 都不比。
+13 -1
View File
@@ -207,7 +207,7 @@
#### 冒险前资源配比预设(构筑提案)(2026-09-18 登记,来源=会话提及,设计面=文档轨)
**现状**:2026-09-18 用户提案,方向评估通过,登记在案。核心想法=玩家在**开始冒险前**预设地图战斗资源的类型配比——预设的是**分布**(九型格面池的正向权重占比;2026-10-04 立项裁决批口径 gp 型入池成九型,正向权重合计 77),不是落位:装板摇号仍按专属随机流摇出各格类型身份,玩家定分布、摇号定落位(与既定随机性立场相容)。两分法:① **基础资源配比**——正向行权重经覆盖值通道在开局注入;② **关卡特殊资源**——不同关卡(board_biome 图主题)携带配比池之外的特殊资源与差异化分布。设计红利=危险格与安全格同池+极性矩阵使配比自带代价轴(调高某型权重,安全格正向与危险格负向同步变多),不是免费午餐;注入通道=[元进度](systems/跨局循环/元进度.md) v1.0 已定稿的「冒险开始参数面与覆盖值通道」,零新机制;②正是[棋盘生成](systems/排雷循环/棋盘生成.md) board_biome 上下文与[地图战斗资源](systems/战斗循环/地图战斗资源.md)差异化插槽的启用场景。**八项裁决已全裁(2026-10-04 用户令「拍板点全部使用推荐」)**:①预算=组合制(总量挪点为骨架〔总和恒 77〕+每型上下限包络+容量随营地解锁渐进〔gp 首发不可调/预设档 3 套/下限 >0〕);②主权=设计者定包络与默认、玩家包络内分配(验收=默认分布锚+极端构筑护栏带);③容量柜台=奖励格图鉴承接;④注入语义=差值向量(存相对默认偏移 ΣΔ=0,集合操作保留在解锁层作辅语义);⑤渠道边界=清图槽差异化先行+三句不混池口径;盘问轮四裁随落(合成序=表值→玩家配比差值→包络钳制→图级变异偏斜→同调硬币偏置;图鉴柜台逐型商品;冒险开始锁档;包络宽度随容量分级放宽)。全案与淘汰依据原载提案 §八/§九/§十(该提案文件已随吸收即死退役,git 历史可考;需求卡片冻结在档)。影响面/依赖/被依赖清单原载本条(排雷/战斗/冒险/表现四循环十余文档指针),重启时自 git 历史取全文本。**挂起(2026-10-05 用户令)**:移出 roadmap 迭代 14(⑥号卡撤销)——暂不做,本条保留为挂起待办;八项裁决与需求卡片全部冻结保留(需求卡片文件头已挂冻结横幅);重启须用户另行发令。
**现状**:2026-09-18 用户提案,方向评估通过,登记在案。核心想法=玩家在**开始冒险前**预设地图战斗资源的类型配比——预设的是**分布**(九型格面池的正向权重占比;2026-10-04 立项裁决批口径 gp 型入池成九型,正向权重合计 77),不是落位:装板摇号仍按专属随机流摇出各格类型身份,玩家定分布、摇号定落位(与既定随机性立场相容)。两分法:① **基础资源配比**——正向行权重经覆盖值通道在开局注入;② **关卡特殊资源**——不同关卡(board_biome 图主题)携带配比池之外的特殊资源与差异化分布。设计红利=危险格与安全格同池+极性矩阵使配比自带代价轴(调高某型权重,安全格正向与危险格负向同步变多),不是免费午餐;注入通道=[元进度](systems/跨局循环/元进度.md) v1.0 已定稿的「冒险开始参数面与覆盖值通道」,零新机制;②正是[棋盘生成](systems/排雷循环/棋盘生成.md) board_biome 上下文与[地图战斗资源](systems/战斗循环/地图战斗资源.md)差异化插槽的启用场景。**八项裁决已全裁(2026-10-04 用户令「拍板点全部使用推荐」)**:①预算=组合制(总量挪点为骨架〔总和恒 77〕+每型上下限包络+容量随营地解锁渐进〔gp 首发不可调/预设档 3 套/下限 >0〕);②主权=设计者定包络与默认、玩家包络内分配(验收=默认分布锚+极端构筑护栏带);③容量柜台=奖励格图鉴承接;④注入语义=差值向量(存相对默认偏移 ΣΔ=0,集合操作保留在解锁层作辅语义);⑤渠道边界=清图槽差异化先行+三句不混池口径;盘问轮四裁随落(合成序=表值→玩家配比差值→包络钳制→图级变异偏斜→同调硬币偏置;图鉴柜台逐型商品;冒险开始锁档;包络宽度随容量分级放宽)。全案与淘汰依据原载提案 §八/§九/§十(该提案文件已随吸收即死退役,git 历史可考;需求卡片冻结在档)。影响面/依赖/被依赖清单原载本条(排雷/战斗/冒险/表现四循环十余文档指针),重启时自 git 历史取全文本。**挂起(2026-10-05 用户令)**:移出 roadmap 迭代 14(⑥号卡撤销)——暂不做,本条保留为挂起待办;八项裁决与需求卡片全部冻结保留(需求卡片文件头已挂冻结横幅);重启须用户另行发令。**凭据版本复核(2026-10-18,文档体检第 10 项)**:本条两处写的 `元进度.md v1.0` 是定稿当时的版本号,该文档现行已 v1.21;经查「冒险开始参数面」与「覆盖值通道」在 v1.21 里原样在役(该文档 :26/:98/:100/:119 四处),v1.16 与 v1.18 两行只对注入节作追加说明、未推翻通道结论——闭合判据仍成立,凭据按历史记载保留不改号。
**归属**:[地图战斗资源](systems/战斗循环/地图战斗资源.md)修订批(基础配比的权重覆盖语义 + GDD 修订)与[元进度](systems/跨局循环/元进度.md)(解锁容量与注入生产者)共裁;呈现面归[HUD 框架](systems/表现与支撑/HUD框架.md)与[教学引导](systems/表现与支撑/教学引导.md)顺带。
@@ -610,6 +610,18 @@
**状态**:待写卡
---
#### L3 屏壳切换探针的判定被仓库外存档状态污染,跑批绿红不可复现(2026-10-18 登记,来源=审计发现,设计面=工程直入)
**现状**:2026-10-18 卫生批跑全量档时 03 探针 --click 与 04 探针 --full 双双「基线绿→现红」,唯一失效单元是 `game/smoke/screen_flow_probe.gd` 的第④步(菜单开新冒险进不去,⑤⑥⑦三条随之因前置不满足连带红)。根因已钉死且与本批及任何代码改动无关:`user://save.json`(主区与所有 worktree 共用同一份、不入 git)里现存一个 `breakpoint` 断点,而 `game/presentation/main.gd:476-482` 按「二轮修改批 A」的用户目检裁决,在 `save_manager.has_breakpoint()` 时弹实底确认框「已有未完冒险(第 N 关)——开新冒险将覆盖旧断点」并直接返回、不开冒险;探针第④步只用前缀匹配找「▶ 开始冒险」并按下、等 3 帧就断言进冒险屏,它隐含假设「存档里没有断点」。三条判据:主区那份未受本批影响的 master(`ea69670c`)单跑同一探针复现完全一样的 7 处;同一 worktree 同一代码把 save.json 临时移开再跑立刻 `PASS — 六步主流程+回退路径全绿`(跑完存档逐字节还原,sha1 `dd67444c35be` 与移开前一致);`game/**/*.gd` 自 10-17 那次绿回执 `7ffd1dc0` 之后零改动。断点来源=上一轮 `ui_click_probe` 或任一会话跑到中途留下的未完冒险(存档账目已累积到 `adventure_total=6`、`level_clear_counts={1:18,2:7,3:4}`)。后果是验证闸的绿红取决于「这台机器上谁最后跑过」,正是 test-conventions §8「判定可靠性优先于覆盖率」所管的同一类缺陷。修复方向两条,均待写卡时定:探针侧自救(第④步前先走弃断点通道,或在确认框上按「开新冒险」再断言)=改 `game/`;跑批侧隔离(给探针配独立 user 目录)=改 `tools/probe_gate.sh`,⚠ 此路有一条未验障碍——本日用 `--user-dir` 指到空目录跑该探针,Godot 挂住零输出、超时未出结论,走这条要先查明成因。证据已指认,按 §12.1 工程直入通道跳过设计轨。
**归属**:game/smoke/screen_flow_probe.gd(探针自救面)与 tools/probe_gate.sh(跑批隔离面)——测试基建
**状态**:待写卡
---
#### 计数器寿命域未统一(2026-10-18 登记,来源=会话提及,设计面=文档轨)
+1 -1
View File
@@ -86,7 +86,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 对齐分支尖)。
-529
View File
@@ -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
View File
@@ -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