lili bc0f486bc2 chore(评审清欠): W-REAL R4 代码评审遗留 16+1 条对账 + 存活项修复
逐条读当前代码坐实(不凭文档推断):16 条 = 7 moot / 6 已修 / 1 存活待修 / 2 本次修;
另协调者追加第 17 条(ad-slot 契约漂移)本次修。对账表全文入总账 §5。

wg1 六项随 Node 退役 moot(生产便宜档=cheap-worker,幂等/max_tokens/callback 已在替代解决);
tier2 成本硬闸(item3)经配置控制面阶段一① soft_budget 演进已修(单 POST 内联续修 +
CircuitBreakerMiddleware 会话级 spent_rmb 累加 fail-closed);CSP/vue-i18n/validate_play_scene/
契约真空/服务态主链等 已修;gamedef v1 标注漂移随文件删除 moot。

本次动手修:
- G1 品牌门扫描面有洞(item8): docs-gate.py 加 walk_code 扫业务代码目录(模块根
  package.json/index.html + wg1/ 等源码),旧门只扫 .md 会放过玩家/管理台可见字面量;
  负向测试证真能拦回流。
- 品牌残留漏网(item8): game-admin 菜单标题退役品牌名 → 绘境运营(扩面后被 G1 逮出)。
- ad-slot 契约漂移(R4-17,跨端契约): ad-slot.schema.json provider enum 加 callback
  (additive)+ 对齐描述;与 api-schemas/ad.yaml 及 ad 模块 CallbackAdProvider(真实现
  PROVIDER="callback")对齐。DB VARCHAR 注释/DO Javadoc 仍列 csj/gdt/mock=doc-only
  (SQL 为 Flyway 版本档不改 checksum),留终审。
- docs-gate.sh 壳注释 六检→七检(失实修正)。

存活待修: item12 tier2 bootstrap X-User-Id 鉴权头口子 + 无重试退避(需真实 deps.py
鉴权契约,部署 smoke 接缝,非本机最小改动可完成)。

验证: docs-gate 七检全绿 + G1 负向测试(植入即拦/删除回绿) + ad-slot JSON 合法 +
docs-gate.py 编译 + CallbackAdProviderTest 印证 getProvider()="callback"。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 19:58:42 -07:00

321 lines
14 KiB
Python

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""文档治理门(engineering-conventions §10.7 的可执行实现 · 2026-07-02 立)
七检,任一失败退出码非零;每检打印通过/命中明细:
G1 品牌不变量 活层禁退役品牌根(白名单:docs/ip、_archive、_recall、日期留痕档、行内 brand-ok 标)
G2 canonical `canonical: true` 的 topic 全仓唯一,且与 docs/architecture/README.md 注册表双向对账
G3 死链 活层 md 的 []() 相对链接与反引号内 docs/·.agents/ 仓内路径必须存在(git show 定位放行)
G4 入口卫生 AGENTS.md 禁 git clone / 全局安装 / 个人绝对路径 / localhost
G5 留痕隔离 策展层(AGENTS.md、docs/architecture、.agents)不得链接非 canonical 的留痕档(plans/brainstorms/dated specs;_index.md 与 canonical 目标豁免)
G6 设计档申报 docs/agent-specs/*-设计.md 必带 frontmatter topic/status/sot-impact
G7 计划谱系 新建 plan/设计档必带 frontmatter `上级:`(仓根相对路径单指针),沿链 6 跳内达 canonical SoT;死链/断链/成环即挡(engineering-conventions §10.8)
用法:python3 .agents/tools/docs-gate.py [仓库根,缺省=脚本上两级]
挂载:.githooks/pre-commit(git config core.hooksPath .githooks)、.gitea/workflows/docs-gate.yml、wave-close 第 8 步。
门③(doc↔code 兑现断言)不在本脚本:归生成主线 harness,规格见 engineering-conventions §10.7。
"""
import os
import re
import sys
# 退役品牌根的唯一字面量(活层散文一律用「退役品牌名」间接表述,勿再写出字面量)
BRAND_RETIRED = "造梦"
# G1 品牌不变量的代码扫描面(W-REAL R4 补:防品牌回流到模块根 package.json / index.html 与 wg1/ 等代码——
# 评审曾指出门只扫活层 md 会放过玩家/管理台可见的代码字面量)。业务代码目录 + 相关源码后缀;
# .agents 不入(门自身源码持有 BRAND_RETIRED 字面量,扫它会自噬)、docs 由 walk_md 覆盖。
BRAND_CODE_BASES = ["game-runtime", "game-studio", "game-admin", "wg1", "tier2",
"cheap-worker", "game-cloud", "contracts", "deploy", "spikes"]
BRAND_CODE_EXTS = (".json", ".html", ".ts", ".js", ".mjs", ".cjs", ".vue",
".py", ".java", ".yaml", ".yml")
# 依赖/构建产物/归档目录原地裁剪(既防误报也快)。
BRAND_PRUNE_DIRS = {"node_modules", "target", "dist", "build", ".git",
"_archive", "_recall", ".venv", "__pycache__"}
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}-")
MD_LINK = re.compile(r"\[[^\]]*\]\(([^)]+)\)")
BACKTICK = re.compile(r"`([^`\n]+)`")
FAILS = []
def rel(p):
return os.path.relpath(p, ROOT)
def walk_md(bases, exclude_archives=True):
out = []
for base in bases:
top = os.path.join(ROOT, base)
if os.path.isfile(top):
out.append(top)
continue
for dp, dns, fs in os.walk(top):
dp_rel = rel(dp) + "/"
if "node_modules" in dp_rel or "/target/" in dp_rel:
continue
if exclude_archives and ("_archive" in dp_rel or "_recall" in dp_rel):
continue
if dp_rel.startswith("docs/ip/"):
continue
for fn in fs:
if fn.endswith(".md"):
out.append(os.path.join(dp, fn))
return out
def walk_code(bases, exts):
"""遍历业务代码目录里指定后缀的源文件(供 G1 品牌扫描面防回流用;prune 掉依赖/构建/归档目录)。"""
out = []
for base in bases:
top = os.path.join(ROOT, base)
if not os.path.isdir(top):
continue
for dp, dns, fs in os.walk(top):
dns[:] = [d for d in dns if d not in BRAND_PRUNE_DIRS] # 原地裁剪不进依赖/构建目录
for fn in fs:
if fn.endswith(exts): # str.endswith 接受后缀元组
out.append(os.path.join(dp, fn))
return out
def read(fp):
try:
with open(fp, encoding="utf-8", errors="ignore") as f:
return f.read()
except OSError:
return ""
def frontmatter(fp):
"""返回 frontmatter 字段 dict(浅解析,只认顶层 `key: value` 行)。"""
lines = read(fp).splitlines()
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 check(name, problems):
if problems:
FAILS.append(name)
print(f"{name}: {len(problems)}")
for p in problems:
print(f" {p}")
else:
print(f"{name}")
# ---------- G1 品牌不变量 ----------
def g1():
problems = []
# ① 活层策展 md(AGENTS.md / docs / .agents):日期留痕档豁免。
for fp in walk_md(["AGENTS.md", "docs", ".agents"]):
if DATED.match(os.path.basename(fp)):
continue
for i, line in enumerate(read(fp).splitlines(), 1):
if BRAND_RETIRED in line and "brand-ok" not in line:
problems.append(f"{rel(fp)}:{i} {line.strip()[:60]}")
# ② 业务代码目录源文件(W-REAL R4 补扫描面):模块根 package.json / index.html 与 wg1/ 等代码防品牌回流。
for fp in walk_code(BRAND_CODE_BASES, BRAND_CODE_EXTS):
for i, line in enumerate(read(fp).splitlines(), 1):
if BRAND_RETIRED in line and "brand-ok" not in line:
problems.append(f"{rel(fp)}:{i} {line.strip()[:60]}")
check("G1 品牌不变量(活层 md + 业务代码无退役品牌根)", problems)
# ---------- G2 canonical 唯一性 + 注册表对账 ----------
def g2():
problems = []
canon = {} # topic -> [paths]
for fp in walk_md(["docs", ".agents"], exclude_archives=False):
fm = frontmatter(fp)
if fm.get("canonical") == "true":
if "_archive" in rel(fp) or "_recall" in rel(fp):
problems.append(f"归档层不得标 canonical:{rel(fp)}")
continue
topic = fm.get("topic", "")
if not topic:
problems.append(f"canonical 缺 topic:{rel(fp)}")
continue
canon.setdefault(topic, []).append(rel(fp))
for topic, paths in sorted(canon.items()):
if len(paths) > 1:
problems.append(f"topic「{topic}」有 {len(paths)} 份 canonical:{' | '.join(paths)}")
# 注册表对账
readme = os.path.join(ROOT, "docs/architecture/README.md")
reg = {} # topic -> path(相对仓根)
for row in read(readme).splitlines():
if not row.startswith("|") or "canonical" not in row:
continue
cells = [c.strip() for c in row.strip().strip("|").split("|")]
if len(cells) < 3 or cells[2] != "canonical":
continue
m = MD_LINK.search(cells[1])
if not m:
problems.append(f"注册表行缺链接:{row.strip()[:70]}")
continue
tgt = os.path.normpath(os.path.join(os.path.dirname(readme), m.group(1).split("#")[0]))
reg[cells[0]] = rel(tgt)
reg_topics, canon_topics = set(reg), set(canon)
for t in sorted(reg_topics - canon_topics):
problems.append(f"注册表有、frontmatter 无 canonical:「{t}」→ {reg.get(t)}")
for t in sorted(canon_topics - reg_topics):
problems.append(f"文档标了 canonical、注册表未登记:「{t}」({canon[t][0]})")
for t in sorted(reg_topics & canon_topics):
if reg[t] not in canon[t]:
problems.append(f"topic「{t}」注册表指 {reg[t]},frontmatter 在 {canon[t][0]}")
check("G2 canonical 唯一性 + 注册表对账", problems)
# ---------- G3 死链 ----------
def g3():
problems = []
for fp in walk_md(["AGENTS.md", "docs", ".agents"]):
txt = read(fp)
dirn = os.path.dirname(fp) or ROOT
for m in MD_LINK.finditer(txt):
t = m.group(1).strip()
if t.startswith(("http://", "https://", "#", "mailto:", "<", "@")) or "," in t:
continue
t = t.split("#")[0].split(" ")[0].strip()
if not t:
continue
tgt = os.path.normpath(os.path.join(dirn, t))
if not os.path.exists(tgt):
problems.append(f"{rel(fp)} -> {m.group(1)}")
for line in txt.splitlines():
if "git show" in line:
continue
for m in BACKTICK.finditer(line):
t = m.group(1).strip()
tok = t.split()[0] if t.split() else t
if not re.match(r"^(docs|\.agents)/", tok):
continue
if any(c in tok for c in "{}*<>$#") or "YYYY" in tok:
continue
cand = os.path.join(ROOT, tok.rstrip("/"))
if not (os.path.exists(cand) or os.path.exists(cand + ".md")):
problems.append(f"{rel(fp)} -> `{t}`(反引号仓内路径不存在)")
check("G3 死链(md 链接 + 反引号仓内路径)", problems)
# ---------- G4 入口卫生 ----------
def g4():
problems = []
patterns = ["git clone", "~/.claude", "install -g", "http://localhost", "http://127."]
for i, line in enumerate(read(os.path.join(ROOT, "AGENTS.md")).splitlines(), 1):
for p in patterns:
if p in line:
problems.append(f"AGENTS.md:{i} 含「{p}")
check("G4 入口卫生(AGENTS.md)", problems)
# ---------- G5 留痕隔离 ----------
def g5():
problems = []
trace_prefix = ("docs/plans/", "docs/brainstorms/", "docs/memorys/")
for fp in walk_md(["AGENTS.md", "docs/architecture", ".agents"]):
dirn = os.path.dirname(fp) or ROOT
for m in MD_LINK.finditer(read(fp)):
t = m.group(1).strip()
if t.startswith(("http://", "https://", "#", "mailto:", "<", "@")) or "," in t:
continue
t = t.split("#")[0].strip()
if not t:
continue
tgt = os.path.normpath(os.path.join(dirn, t))
tr = rel(tgt)
hit = tr.startswith(trace_prefix) or (
tr.startswith("docs/agent-specs/") and DATED.match(os.path.basename(tr)))
if not hit:
continue
if os.path.basename(tr) == "_index.md":
continue
if os.path.exists(tgt) and frontmatter(tgt).get("canonical") == "true":
continue # 链向 canonical 活档合法
problems.append(f"{rel(fp)} -> {tr}(策展层链非 canonical 留痕)")
check("G5 留痕隔离(策展层不链留痕档)", problems)
# ---------- G6 设计档申报 ----------
def g6():
problems = []
specs = os.path.join(ROOT, "docs/agent-specs")
if os.path.isdir(specs):
for fn in os.listdir(specs):
if fn.endswith("-设计.md"):
fm = frontmatter(os.path.join(specs, fn))
missing = [k for k in ("topic", "status", "sot-impact") if not fm.get(k)]
if missing:
problems.append(f"docs/agent-specs/{fn} 缺 frontmatter:{','.join(missing)}")
check("G6 设计档申报(topic/status/sot-impact)", problems)
# ---------- G7 计划谱系(单指针 上级:) ----------
G7_CUTOFF = "2026-07-03" # 生效日:文件名日期 >= 此日的新档必带 上级:;存量自愿(带了就验)
G7_MAX_HOPS = 6 # 谱系链跳数上限(计划树实际 3-4 级,6 留余量)
def g7():
"""计划/设计档谱系门:每档一行 `上级:`(仓根相对路径),沿链必达 canonical SoT。
只验证「单指针 + 链可达」这条最小事实(engineering-conventions §10.8);
全链/树/当前位置都是查询视图(.agents/tools/plan-tree.py),不在门内维护。
"""
problems = []
holders = [] # 所有带 上级: 的档(存量自愿带了也验)
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 not fn.endswith(suffix) or not DATED.match(fn):
continue
fp = os.path.join(d, fn)
fm = frontmatter(fp)
if fn[:10] >= G7_CUTOFF and not fm.get("上级"):
problems.append(f"{rel(fp)} 缺 frontmatter `上级:`(新档必须单指针挂进计划谱系,§10.8)")
elif fm.get("上级"):
holders.append(fp)
for fp in holders:
cur, seen = fp, set()
for _ in range(G7_MAX_HOPS + 1):
fm = frontmatter(cur)
if fm.get("canonical") == "true" and cur != fp:
break # 达 canonical SoT,链成立
parent = fm.get("上级")
if not parent:
problems.append(f"{rel(fp)} 谱系断链:{rel(cur)} 既非 canonical 也无 `上级:`(上级需先认领这条线)")
break
tgt = os.path.normpath(os.path.join(ROOT, parent))
if not os.path.isfile(tgt):
problems.append(f"{rel(fp)} 上级死链:{rel(cur)} -> {parent}")
break
if tgt in seen or tgt == cur:
problems.append(f"{rel(fp)} 谱系成环:{parent}")
break
seen.add(cur)
cur = tgt
else:
problems.append(f"{rel(fp)} 谱系超 {G7_MAX_HOPS} 跳未达 canonical SoT")
check("G7 计划谱系(`上级:` 单指针沿链达 canonical)", problems)
if __name__ == "__main__":
for fn in (g1, g2, g3, g4, g5, g6, g7):
fn()
if FAILS:
print(f"--- docs-gate 未过:{len(FAILS)} 检失败({''.join(FAILS)}) ---")
sys.exit(len(FAILS))
print("--- docs-gate 全绿 ---")