Files
workbuddy_skills/product-planning/scripts/check_naming.py
T
admin f64a172b5a product-planning:用备份里的新版④段装回(四段恢复),替换 10-14 删掉的旧版
- 来源:E:/技能备份/product-planning/references/stage-proto-doc/(4 份,与备份逐字一致,md5 逐个核过)。
- 新版与旧版的差别:新版定位改成「解释与记录**现有**原型,不重新规划产品、不重新设计界面」;
  交**两份产物**(原型内嵌说明区 + 独立产品文档 designs/<项目>/产品文档.html);
  内容按「需求列表 → 用户场景 → 操作流程 → 功能」四块 + 附录「原型页面地址」铺;
  配 R/S/F 编号体系与三份方法论(doc-area-spec / doc-language-spec 322 行 / guided-tour)。
  旧版那套(状态轴、三段必填、逐步骤演示)不再使用。
- 总入口 SKILL.md 同步改回四段:description、「四段结构」表(④子步=4a 场景盘点 / 4b 说明区 /
  4c 演示引导(可选))、交接口加 ③→④、产物落哪加④行、进入方式加回两行、只读清单加④条、
  执行规则与反面清单(加回两条:③别顺手写说明区/产品文档、④别重新规划产品);段名链恢复
  「产品 / 功能 / 界面 / 说明」;留痕位改写为④段的完整沿革(10-14 删旧版 → 10-15 装新版)。
- ③段 stage-delivery:「与④段的界」改回(③造物、④解释与记录),两处约束改回「说明区与独立产品文档归④段」,
  反面清单加回一条。①②段的段名对称链与越界条目改回四段。
- 闸门 check_naming.py:STAGES 恢复 stage-proto-doc(圈号数组回 ①②③④、交接口回三条边、
  子步例外列表回 4b/4c),DEPRECATED 去掉④段名(段名与子步名仍在使用)。
- 补入配套留痕:references/_留痕/stage-proto-doc-SKILL-20261014-替换前.md(旧版 SKILL 存档,转 LF)。
- 验收:check_naming.py --ws E:/ProgramData/AIProject/contentm_agent = 通过 2063 / 失败 0 / 提示 27。
2026-10-15 07:58:14 +08:00

449 lines
24 KiB
Python
Raw 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 -*-
"""四段流水线**命名一致性**自检(纯静态文本比对,不需要渲染、不需要外部依赖)。
只查一件事:**四段的段名 / 子步名,在总入口、各段 SKILL.md、各段 references 之间有没有漂。**
⛔ 本脚本**不查视觉、几何、配色** —— 那些判据归③段 §供给·工艺数值判据,本脚本一概不碰。
为什么需要它(不是"为了严谨",是实测过的两次真实事故):
- 2026-10-02:段名从「需求调研/功能规划/界面与交付」改成「产品需求/产品功能/界面交互」,
`SKILL.md` 改了,但 `references/grill-me.md` 的 H1 仍写「需求澄清(grill-me · 调研段)」。
- 同日:`stage-requirements/SKILL.md` 的子步表写「2b 功能流转」,正文 H2 却是「## 2b 状态机」;
`product-planning` 的④段子步列只写「4a 说明区/ 演示引导」,漏了 4b / 4c。
这三处**没有任何门禁会报错** —— 只能靠回读,而回读会漏。
用法:
python scripts/check_naming.py --ws <工作区> # 查技能包自身 + 该工作区的产出白名单
python scripts/check_naming.py --root <某个技能根> # 只查一份
退出码:0 = 全过;1 = 有 FAIL。
"""
import argparse
import os
import pathlib
import re
import sys
# ── 权威口径表。改段名/子步名时,先改这里,再改文档 ──────────────────────────
STAGES = [
{
"slug": "stage-discovery",
"cn": "产品需求",
"h1": "# ① 产品需求",
# 2026-10-06 用户定案:①段收为五子步五产出,编号 1a~1e,全部落 research/
# 旧「1a 需求定义 / 1b 需求取证 / 1c 产品定位与场景」三子步废止(见 DEPRECATED)
"subs": ["1a 需求文档", "1b 竞品分析", "1c 用户画像", "1d 产品策略", "1e 使用场景"],
},
{
"slug": "stage-requirements",
"cn": "产品功能",
"h1": "# ② 产品功能",
# 2026-10-06 用户定案:②段收窄为「只细化功能」,2b/2c 删除,三子步并为一个
# 2026-10-07 用户定案:②段一拆二 —— 产品功能 / 界面布局 两个子步两文档
"subs": ["2a 产品功能", "2b 界面布局"],
},
{
"slug": "stage-delivery",
"cn": "界面交互",
"h1": "# ③ 界面交互",
"subs": ["3a 视觉规范", "3b 原型", "3c GPT参考版", "3d 审查打磨"],
},
{
"slug": "stage-proto-doc",
"cn": "原型说明文档",
"h1": "# ④ 原型说明文档",
# 4b/4c 允许只在一处出现完整(总入口与段内都写全),但**两处都必须能读到**
"subs": ["4a 场景盘点", "4b 说明区", "4c 演示引导"],
},
]
# ④段沿革(2026-10-14 → 10-15):10-14 按用户令删掉旧版(做法=往原型 HTML 注入说明区注解层,实测不好用);
# 10-15 用备份里的新版装上(两份产物 + R/S/F 编号 + 四块框架),本表随之恢复该项。
# 已废止的旧名。出现在「废止理由块」里是正当的(那是留痕),
# 所以这里只做**提示级**检查(不计入 FAIL),人工确认上下文即可。
DEPRECATED = [
"需求调研", "功能规划", "需求与结构", "界面与交付",
"1a 需求澄清", "2a 需求压测", "2b 状态机", "2c 图示", "1b 调研", "1c 方案",
# 2026-10-06 ②段收窄:以下三个子步名与旧产出名一并废止
"2a 功能定义与压测", "2b 功能流转", "2c 功能图示",
"1c 产品定位与商业设计",
# 2026-10-06 ①段收为五子步:旧三子步名废止(产出改落 research/1a~1e)
"1a 需求定义", "1b 需求取证", "1c 产品定位与场景",
# 2026-10-06 ①段旧产出名废止:00/01 合并为 1a,其余按组号改子步号
"00-问题定义.md", "01-需求澄清.md",
"10-参考实装拆解.md", "11-竞品分析.md", "12-用户画像.md",
"strategy/30-产品策略.md", "strategy/31-使用场景.md",
# 2026-10-14 旧版④段的做法废止(状态轴 / 三段必填 / 逐步骤演示),段名与子步名**仍在使用**,
# 故不列入本表;新版的判据在段内 `references/doc-language-spec.md` 等三份。
]
# ⛔ 2026-10-06 用户定案:①段**产出白名单**——本段只许这五份,第 6 份须用户明确要求。
# 判据:扫 `docs/pm/<项目>/research/` 的实际文件名,出现白名单外的 `.md` 即 FAIL。
# ⛔ 用 os.walk 时**必须逐层拆开**:`research/` 与 `prd/` 是两个不同的白名单。
WHITELIST = {
"stage-discovery": {
"dir": "research",
"allow": ["1a-需求文档.md", "1b-竞品分析.md", "1c-用户画像.md",
"1d-产品策略.md", "1e-使用场景.md"],
},
# 🔴 2026-10-07 用户定案:「2 产品功能 和 界面布局不就是两个文档嘛」
# ⇒ ②段同样是白名单制:`prd/` 里**只许这两份**(⛔ 不许退回单份 `PRD.md`)。
"stage-requirements": {
"dir": "prd",
"allow": ["2a-产品功能.md", "2b-界面布局.md"],
},
}
# 已从磁盘移除的资产。文档里提到它们时,只允许出现在**说明性文字**里
# ("原主干 X 已不存在"这类),一旦出现在「必读表/调用指令」位置就是悬空引用。
REMOVED_ASSETS = ["ui-page-design"]
# ⛔ 已废止的目录形态。它们若出现在正文的「产出 / 只读 / 必读」位置 ⇒ 真残留(FAIL)。
# 2026-10-08 实测:`usage-scenario.md` 的产出行、`stage-proto-doc/SKILL.md` 的输入表、
# 总入口的「只读」清单,三处还指着已废的 `strategy/` 与 `ux/`,而当时没有任何门禁会报。
# 豁免:行内出现下面任一废止标记时视为正当留痕(说明"原来叫什么、现在改成什么")。
DEAD_PATHS = {
r"strategy/30-|strategy/31-|docs/pm/[^`\s]*/strategy/":
"已废的 ①段 strategy/ 目录(现全部落 research/1a~1e)",
r"ux/state-machine|ux/diagrams|docs/pm/[^`\s]*/ux/":
"已废的 ②段 ux/ 目录(状态流转已移进 prd/2a 的功能条目)",
}
DEAD_PATH_EXEMPT = ("旧", "已废", "已改名", "曾", "历史上", "移进", "移到", "改为",
"废止", "原 ", "原标题", "历史:")
# ⛔ 单一可信源登记表(2026-10-08 立)。每条 = 一个「会变的事实」的唯一定义处。
# 判据:除定义处外,其余任何 md(不含 `归档/`、`_留痕/`、`assets/`、`_superseded*`)
# 里出现该模式 ⇒ FAIL。`authority: None` 表示全包禁写(该措辞本身就不该再出现)。
# 为什么需要它:复述才是漂移的根因 —— 实测一个设定被复述约 40 处,
# 改一次动三份文件 17 处仍漏 13 处口径(2026-10-08 用户:「是不是 本身就是问题」)。
# ⚠️ 加新条目前先确认「这个事实真的会变」;不会变的(如历史沿革、用户原话留痕)别往里塞。
AUTHORITY = [
{"fact": "②③边界(②段管什么 / ③段管什么)",
"authority": "references/stage-requirements/SKILL.md",
"pattern": r"归②段([^)]*)\s*\|\s*归(?:③段|本段)(",
"why": "两侧对照表只许在②段 SKILL.md 的 §②—③ 交接口 定义一次"},
{"fact": "停线规则(哪几种情况停)",
"authority": "SKILL.md",
"pattern": r"只有[三四]种情况停",
"why": "⛔ 全包禁写 —— 口径只在本包 SKILL.md 的「执行规则」,各段一律引用"},
{"fact": "可选档骨架名单(营销页向那 8 个)",
"authority": "references/stage-delivery/references/layouts-tooling.md",
"pattern": r"hero\s*/\s*features|hero/features",
"why": "骨架名单与「为什么工具型不能套」只许在 layouts-tooling.md 开头写一次"},
{"fact": "说人话门槛数值",
"authority": "SKILL.md",
"pattern": r"45\s*/\s*50",
"why": "门槛只许在本包 SKILL.md 的「执行规则」内容准则条 定义一次"},
{"fact": "对比度门槛数值",
"authority": "references/stage-delivery/references/execution-runbook.md",
"pattern": r"4\.5:1",
"why": "数值只许登记在 runbook 的数值判据段"},
{"fact": "设计契约字段的展开清单",
"authority": "references/stage-delivery/references/execution-runbook.md",
"pattern": r"重复项的信息量(",
"why": "十二字段的逐条展开只许在 runbook §2.1"},
{"fact": "②—③ 定案用户原话",
"authority": "references/stage-requirements/SKILL.md",
"pattern": r"一会一个样子",
"why": "用户原话只许在②段 §②—③ 交接口 出现一次(可追溯性由那一处承担)"},
{"fact": "1b 两体例的定案用户原话",
"authority": "references/stage-discovery/references/competitor-analysis.md",
"pattern": r"有独立分析也有汇总分析",
"why": "只许在 1b 方法论里写一次,其余引 §0.4"},
{"fact": "grill-me 的 A / B 节火力点",
"authority": "references/stage-discovery/references/grill-me.md",
"pattern": r"做成什么样、哪些情况不成立、状态怎么流转",
"why": "火力点定义只许在 grill-me.md"},
{"fact": "选定套的组件文件名(默认档与可选档不同名)",
"authority": "references/stage-delivery/references/execution-runbook.md",
"pattern": r"\+\s*`?components\.html`?",
"why": "⛔ 照可选档(153 套)的命名去找默认档的文件会扑空 —— 默认档那套没有 components.html。"
"2026-10-08 实测:D1 步骤照这个名字找 ⇒ 版式结构与组件族整层漏掉 ⇒ 「用了它的样式、没有它的排版」。"
"只许在 runbook §2.2「D1 读什么」里作为反面留痕出现"},
]
# ⛔ 2026-10-02 用户定案**永久删除**的产出与方法论(防被加回来)
# 判据:命中位置若落在「已删除的产出」留痕块 / 废止说明里 → 正常,只报 HINT。
DEAD_OUTPUTS = {
"opportunity-solution-tree": "机会树(用户:完全没用,都是编的)",
"20-机会树.md": "机会树产物(用户:完全没用,都是编的)",
"value-proposition": "价值主张方法论(用户:不如改为使用场景)",
"strategy/31-价值主张.md": "价值主张产物(已改名为 31-使用场景.md)",
}
results = [] # (level, check, detail)
def add(level, check, detail):
results.append((level, check, detail))
def read(path):
if not os.path.isfile(path):
return None
with open(path, encoding="utf-8", errors="ignore") as f:
return f.read()
def frontmatter_ok(text):
"""frontmatter 是否闭合。判法:第 1 行是 ---,且其后 12 行内能找到闭合 ---。
⛔ 别用 text.split('---')[0] 取 frontmatter —— 那是**开标记之前**的片段,
逻辑写反,会把每个文件都误报成「无 frontmatter」。
"""
lines = text.split("\n")
if not lines or lines[0].strip() != "---":
return False
return any(l.strip() == "---" for l in lines[1:12])
def check_root(root, label):
root = os.path.abspath(root)
if not os.path.isdir(root):
add("FAIL", f"[{label}] 技能根存在", root)
return
add("PASS", f"[{label}] 技能根存在", root)
# 1) 每段:frontmatter 闭合 + H1 与权威表一致 + 每个子步都有 H2
# ⚠️ 2026-10-05 整合后:四段收进 references/<slug>/,不再是技能根下的平级目录。
# 本函数按「技能包 = 一个目录,SKILL.md 在最外层」的形态定位:
# <root>/SKILL.md ← 总入口(曾为 <root>/product-planning/SKILL.md)
# <root>/references/<slug>/SKILL.md ← 四段(曾为 <root>/<slug>/SKILL.md)
# ⛔ 不许改回平级探测:那会让整合形态静默退回「一目录套多技能」而门禁照样全绿。
for st in STAGES:
p = os.path.join(root, "references", st["slug"], "SKILL.md")
text = read(p)
if text is None:
add("FAIL", f"[{label}] references/{st['slug']}/SKILL.md 存在", p)
continue
rel = f"references/{st['slug']}/SKILL.md"
add("PASS" if frontmatter_ok(text) else "FAIL",
f"[{label}] {rel} frontmatter 闭合", "")
h1s = [l.rstrip() for l in text.split("\n") if l.startswith("# ") and "产品" in l or l.startswith("# ") and "界面" in l or l.startswith("# ") and "原型说明" in l]
if st["h1"] in text:
add("PASS", f"[{label}] {rel} H1 = `{st['h1']}`", "")
else:
add("FAIL", f"[{label}] {rel} H1 应为 `{st['h1']}`",
"实际 H1 行: " + " | ".join(l.strip() for l in text.split("\n") if l.startswith("# ")[:2]))
for sub in st["subs"]:
key = sub.split()[0] # "1a" / "3b" ...
has_h2 = any(l.startswith(f"## {key} ") for l in text.split("\n"))
add("PASS" if has_h2 else "FAIL",
f"[{label}] {rel} 有 H2 章节 `{sub}`",
"" if has_h2 else f"找不到 `## {key} ` 开头的章节 —— 子步表与正文不一致")
# 2) 总入口:段名四件套 + 子步列表完整 + 交接口表
# ⚠️ 2026-10-05 整合后总入口就是技能包自己的 SKILL.md(不再有 product-planning/ 子目录)
pp = os.path.join(root, "SKILL.md")
text = read(pp)
if text is None:
add("FAIL", f"[{label}] SKILL.md 存在", pp)
return
rel = "SKILL.md"
for st in STAGES:
num = STAGES.index(st) + 1
circled = "①②③④"[num - 1]
# 段名必须以「圈号 + 空格 + 段名」成对出现,避免只匹配到散落正文
ok = f"**{circled} {st['cn']}**" in text
add("PASS" if ok else "FAIL",
f"[{label}] {rel} 四段结构表含 `{circled} {st['cn']}`", "")
# 子步:总入口表里每个子步都要能被逐个读到
for st in STAGES:
for sub in st["subs"]:
if sub.split()[0] in ("3d", "4b", "4c"):
# 3d/4b/4c 在总入口可能与相邻子步合并书写,只要求"子步号 + 关键词"能被搜到
kw = sub.split(" ", 1)[1] if " " in sub else sub
ok = (sub in text) or (kw in text)
else:
ok = sub in text
add("PASS" if ok else "FAIL",
f"[{label}] {rel} 四段结构表子步列含 `{sub}`", "")
# 交接口表:三条边必须都在
for edge in ("① → ②", "② → ③", "③ → ④"):
add("PASS" if edge in text else "FAIL",
f"[{label}] {rel} 交接口表含 `{edge}`", "")
# 3) 悬空引用:已移除资产**不许出现在必读表的数据行里**
# ⛔ 只扫表格数据行(以 `|` 开头且含路径形态),不扫章节前的告示段 ——
# 告示里写"原必读表指向 X,该目录已不在磁盘上"是**正当留痕**,不是悬空引用。
# 判据形态:形如 `| **D0** | ...ui-page-design... | ... |` 才算。
runbook = read(os.path.join(root, "references", "stage-delivery", "references", "execution-runbook.md"))
if runbook:
m = re.search(r"## 1\. 必须加载的文件(.*?)(?=\n## )", runbook, re.S)
section = m.group(1) if m else ""
table_rows = [l for l in section.split("\n")
if l.strip().startswith("|") and "---" not in l]
for asset in REMOVED_ASSETS:
bad = [l.strip()[:90] for l in table_rows
if asset in l and "取代原" not in l]
add("FAIL" if bad else "PASS",
f"[{label}] runbook 必读表数据行未引用已移除资产 `{asset}`",
";".join(bad) if bad else "")
# 5) 单一可信源:会变的事实只许在定义处出现,别处出现即复述(复述必然漂)
for spec in AUTHORITY:
auth = (os.path.normpath(os.path.join(root, spec["authority"]))
if spec["authority"] else None)
bad = []
for dp, dn, fn in os.walk(root):
dn[:] = [d for d in dn
if d not in ("__pycache__", "_留痕", "归档", "assets")]
for f in fn:
if not f.endswith(".md") or f.startswith("_superseded"):
continue
p = os.path.join(dp, f)
if auth and os.path.normpath(p) == auth:
continue
tt = read(p)
if not tt:
continue
for i, l in enumerate(tt.split("\n"), 1):
if re.search(spec["pattern"], l) and not any(
w in l for w in ("已于", "历史", "废止", "留痕")):
bad.append(f"{os.path.relpath(p, root).replace(chr(92), '/')}#{i}")
add("FAIL" if bad else "PASS",
f"[{label}] 单一可信源:{spec['fact']}",
(f"⛔ 复述出现在 {';'.join(bad[:6])} —— {spec['why']}" if bad else ""))
# 4) 提示级:旧名 / 已删项出现在哪(供人工判上下文,不计入 FAIL)
# ⚠️ 2026-10-08 修 bug:原实现先跑了一个空循环(只把文件名读进 `t`、什么都不做),
# 紧接着的 DEPRECATED 判断又误用了那个上一轮残留的 `t`(该用本轮读到的 `tt`)
# ⇒ 旧名提示恒不触发,`strategy/30-产品策略.md` 这类真残留被静默放过(假绿)。
# 现合并成一个循环,一律用本轮读到的 `tt`。
for dp, dn, fn in os.walk(root):
dn[:] = [d for d in dn if d != "__pycache__"]
for f in fn:
if not f.endswith(".md"):
continue
p = os.path.join(dp, f)
tt = read(p)
if not tt:
continue
for key, why in DEAD_OUTPUTS.items():
if key in tt:
lines = [i for i, l in enumerate(tt.split(chr(10)), 1) if key in l]
add("HINT", f"[{label}] {os.path.relpath(p, root)} 出现已删除项 `{key}`",
f"{why}|行 {lines[:5]} —— 确认是否落在废止说明/留痕里(正当)")
for old in DEPRECATED:
if old in tt:
lines = [i for i, l in enumerate(tt.split("\n"), 1) if old in l]
add("HINT", f"[{label}] {os.path.relpath(p, root)} 出现旧名 `{old}`",
f"行 {lines[:6]} —— 确认是否落在「废止理由块」内(正当)还是真残留")
# 4c) 旧产物路径残留:已废目录形态出现在正文里就是真残留(告示/留痕里的除外)
# ⛔ 不要指望 DEPRECATED 的 HINT —— 它只提示不拦;路径错会让下游整段读空文件。
for dp, dn, fn in os.walk(root):
dn[:] = [d for d in dn if d not in ("__pycache__", "_留痕")]
for f in fn:
if not f.endswith(".md"):
continue
p = os.path.join(dp, f)
tt = read(p)
if not tt:
continue
for pat, why in DEAD_PATHS.items():
hits = [(i, l.strip()[:80]) for i, l in enumerate(tt.split("\n"), 1)
if re.search(pat, l) and not any(w in l for w in DEAD_PATH_EXEMPT)]
add("FAIL" if hits else "PASS",
f"[{label}] {os.path.relpath(p, root)} 无悬空旧路径 `{why}`",
";".join(f"行{i}: {l}" for i, l in hits[:4]) if hits else "")
def check_whitelist(ws, label):
"""①②段产出白名单:`docs/pm/<项目>/<dir>/` 里出现白名单外的 .md ⇒ FAIL。
⛔ 这是**产出层**门禁,与命名门禁不同源:命名管"文档里写的名字有没有漂",
本函数管"磁盘上到底多出了什么文件"。2026-10-06 事故:某轮自作主张多产了一份
《参考实装拆解》,用户反复看到"冒出来的不相关的文档" ⇒ 从此用机器判。
✅ 2026-10-07 扩到②段(`prd/` 只许 `2a-产品功能.md` / `2b-界面布局.md`)——
两段同为白名单制,⛔ 不出现"①段严、②段松"这种各说各话。
"""
import glob
for slug, rule in WHITELIST.items():
pat = os.path.join(ws, "docs", "pm", "*", rule["dir"])
for d in glob.glob(pat):
if not os.path.isdir(d):
continue
proj = os.path.basename(os.path.dirname(d))
extra = sorted(f for f in os.listdir(d)
if f.endswith(".md") and f not in rule["allow"])
rel = os.path.relpath(d, ws).replace("\\", "/")
seg = "①段" if slug == "stage-discovery" else "②段"
add("FAIL" if extra else "PASS",
f"[{label}] {seg}产出白名单 `{rel}`",
f"白名单外文件:{extra} —— ⛔ 该段只许 {len(rule['allow'])} 份({rule['allow']}),"
f"多出的须用户明确要求" if extra else "无白名单外文件")
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--root", action="append", default=None,
help="技能根目录,可重复;默认只查本技能包自身")
ap.add_argument("--ws", default=None,
help="工作区根(含 docs/pm 的那一层);默认从 cwd 向上找")
args = ap.parse_args()
# 本文件位于 <技能包>/scripts/ ⇒ 上溯 2 层 = 技能包(SKILL.md 就在这层)
skill_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
# 🔴🔴 2026-10-07 技能迁全局后的**必须修正**:
# 旧写法 `ws = os.path.dirname(skill_root)` 是按「技能包在工作区根」写的。
# 技能从工作区搬到技能根(`<技能根>/product-planning/`)之后,
# `ws` 会算成技能根 ⇒ **`docs/pm/*` 一个都扫不到**
# ⇒ `check_whitelist()` 静默扫零个目录、**恒绿通过**(假绿比报红更危险)。
# ⇒ 工作区一律**显式给**:`--ws <工作区>`;没给就向上找「含 docs/pm 的那一层」,
# 再找不到就**明确报 FAIL**,⛔ 绝不静默跳过。
roots = args.root or [skill_root]
ws = args.ws
if not ws:
_p = pathlib.Path.cwd()
for _ in range(6):
if (_p / "docs" / "pm").is_dir():
ws = str(_p)
break
_p = _p.parent
if not ws or not os.path.isdir(os.path.join(ws, "docs", "pm")):
add("FAIL", "找得到工作区(`docs/pm` 那一层)",
"⛔ 未找到 ⇒ 产出白名单**没被真正检查**。请显式传 `--ws <工作区>`。")
ws = ws or os.getcwd()
for r in roots:
check_root(r, os.path.basename(os.path.dirname(r.rstrip("/")) if r.endswith("skills") else r))
# 产出白名单只查一次(它是**磁盘状态**,与技能根有几份无关)
check_whitelist(ws, "产出白名单")
fails = [x for x in results if x[0] == "FAIL"]
hints = [x for x in results if x[0] == "HINT"]
passes = [x for x in results if x[0] == "PASS"]
for lvl, check, detail in results:
if lvl == "HINT":
print(f"[提示] {check}" + (f" —— {detail}" if detail else ""))
print()
for lvl, check, detail in fails:
print(f"[FAIL] {check}" + (f" —— {detail}" if detail else ""))
for lvl, check, detail in passes:
print(f"[过] {check}")
print()
print(f"小计:通过 {len(passes)} | 失败 {len(fails)} | 提示 {len(hints)}")
if fails:
print("⛔ 有 FAIL —— 上面的不一致必须修完再跑流水线。")
return 1
print("✅ 命名一致性全过。")
if hints:
print("ℹ️ 提示项需人工看一眼上下文(旧名出现在废止理由块里是正当留痕)。")
return 0
if __name__ == "__main__":
sys.exit(main())