Files
workbuddy_skills/product-planning/scripts/check_terms.py
T
admin 8160f91559 技能库同步:另一台机的 2026-10-16 包 + element-picker 入包 + 工具归拢到技能级 assets/
product-planning(同步 26 改 65 新)
- 目录约定:docs/pm/<项目>/ ⇒ <工作区>/项目/<产品slug>/ 为产品根,下分 产品规划/ 与 产品原型/{designs,pages}
- ④段骨架六节 ⇒ 七节(新增「原型地址」;后两节改「页面导航」「原型状态取值」)
- ③段新增真机组件库;结案「源站只有暗色主题」,tokens.css(亮色)标为不可用于生产
- ③段改版规则反转为「一个产品只用一套原型、只留当前态」;②段新增 产品规划模板.md(表头唯一定义处)
- scripts:产品根定位重写(含 .registry/current-project 指针),新增 check_terms.py 与 selftest_product_root.py

session-mechanism(同步 3 改 2 新)
- references/06-技能优化规程.md 新增 §5「要求落在生成时看得见的面上」,总表 16 ⇒ 17 条;SKILL.md 同步
- scripts/collabctl.py 取回 _find_board() 候选定位(旧版写死 scripts/board.py,而看板已搬进 boards/会话看板/ ⇒ 旧版本就是坏的)

element-picker(新装)
- 按用户口径作「独立功能加入 product-planning」,非独立全局技能
- 修掉它自身三处路径漂移(技能根写法 ⇒ 同目录;README 两处旧落点;剪贴板示例文案与代码对齐)

工具归拢到技能级 assets/
- diagram-design / design-capture / element-picker / video-capture 从各段 assets/ 移到技能包 assets/
- 新增红线:本包有两处 assets/(技能级工具箱 vs 段内资产),引工具一律写「技能级 assets/<工具名>/」
- 路径口径由两类改三类(技能级工具箱 / 本段自带资产 / 同根技能)

不入库(磁盘保留)
- design-system-tiaoyue 整个目录(用户 2026-10-16 令):git rm -r --cached + .gitignore,理由=14 MB 且可 CDP 重采;
  已在 ③段 SKILL.md 注明「取仓后这一层是空的属正常,用自带套需另取或重采」
- element-picker 的运行期存档 marks/(服务自己会建目录)
2026-10-16 23:13:33 +08:00

196 lines
9.1 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 -*-
"""交付物**用词**闸门 —— 与 ③段 `scripts/check_naming.py` 同族:那个查「文本一致性」,这个查「用词」。
为什么需要它(不是"为了严谨",是实测过的复发事故):
「宿主」这个词在本工作区有两义 —— **被嵌入的目标产品**(product-planning 语境,本项目=tiaoyue)
与**那个程序**(机制语境,正式名 `WorkBuddy 本体`)。同形不同义 ⇒ 读者无从分辨。
既有规矩只把「内部词须 0」查在**原型三页 HTML**上,评审产物(`产品规划复盘-*/` 这类)不受管
⇒ 晦涩词在交付物里复发(实测一次 48 处)。本脚本把检查面扩到**所有交付物**。
三条检查(`--paths` 指到哪就只查哪,⛔ 不隐式全盘扫):
① **内部词表 0 命中** —— 除「引用」外一律 FAIL。被判为「引用」而放行的,降级为 HINT。
② **同一实体两种叫法同篇** —— 同一文件里两个名字都在用 ⇒ FAIL(一个东西只许一个名)。
③ **机制侧用词越界** —— 把 `WorkBuddy 本体` 写成「宿主」,且同句还有机制侧词 ⇒ FAIL。
判定「引用」的三条(满足任一即放行,只报 HINT):
· 该词被成对引号包住(`` ` `` / 「」 / 『』 / “” / "");
· 同句带处置标记(旧名 / 已废 / 已改名 / 改名 / 改成 / 删掉 / 删除 / 整节 / 废止 / 历史 / 曾是 /
原节名 / 待改 / 旧口径 / 未落地 / 并入 / 不一致);
· 同一行命中 **≥2 个内部词** ⇒ 判为「节序对照句」。
⚠️ 已知局限(如实写在这里,别当成没有):这三条是**文本启发式**,判不出「表格里把旧名当现状陈述」
这类用法 —— 那种句子会走第 3 条放行。⇒ 本脚本拦「新写的正文里误用旧名/内部词」,⛔ 不替代人工回读。
用法:
python scripts/check_terms.py --paths <交付物目录或文件> [--paths …]
python scripts/check_terms.py --paths D:/某工作区/交付物 --ext .md
退出码:0 = 全过(可能带 HINT);1 = 有 FAIL;2 = 用法/路径错。
"""
import argparse
import os
import re
import sys
try:
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
sys.stderr.reconfigure(encoding="utf-8", errors="replace")
except Exception:
pass
# ── ① 内部词表。加词前先确认「它真的会误导读者」,不是「看着不顺眼」──────────────
INTERNAL = {
"宿主": "内部词:指被嵌入的产品时写「目标产品」(首次出现写「目标产品(本项目=tiaoyue)」);"
"指那个程序时写「WorkBuddy 本体」",
"跨页与界面状态": "已废节名(2026-10-15 用户令改名为「页面导航」)",
"待确认事项与偏离单": "已删除的节(2026-10-15 用户令整节删除,⛔ 别补回来)",
"待确认与偏离": "已删除的节(同上)",
}
# ── ② 同一实体两种叫法。左=正式名,右=旧称/别名;两者同篇非引用出现 ⇒ FAIL ──────
ALIAS_PAIRS = [
("目标产品", "宿主", "被嵌入的那个产品(product-planning 语境)"),
("WorkBuddy 本体", "宿主", "那个程序(机制语境)"),
]
# ── ③ 机制侧标记。出现「宿主」且同句带这些词 ⇒ 越界(本轮旧词用的是机制义)────────
MECH_MARKERS = ("WorkBuddy 本体", "本体", "进程", "常驻", "守护", "会话机制", "机制侧",
"manifest", "锁", "钩子", "注入面")
# 「引用」放行用的处置标记
CITE_MARKERS = ("旧名", "已废", "已改名", "改名", "改成", "删掉", "删除", "整节", "废止",
"历史", "曾是", "原节名", "待改", "旧口径", "未落地", "并入", "不一致")
QUOTE_PAIRS = ("`", "「", "」", "『", "』")
# 扫描时跳过的目录(`assets/` 是采集来的存量资产,本工作区惯例豁免)
SKIP_DIRS = {"__pycache__", "_留痕", "归档", "assets", "node_modules", ".git", "vendor", "tmp"}
def _quoted(line, word):
"""该词在本行是否被成对引号包住(`` ` `` | 「」 | 『』 | "" | “”)。"""
for a, b in (("`", "`"), ("「", "」"), ("『", "』"), ('"', '"'), ("“", "”")):
if re.search(re.escape(a) + r"[^" + re.escape(a + b) + r"]*"
+ re.escape(word) + r"[^" + re.escape(a + b) + r"]*" + re.escape(b), line):
return True
return False
def _citations(line, hits):
"""本行里的这些命中,哪些算「引用」(⇒ 放行,只报 HINT)。"""
if any(m in line for m in CITE_MARKERS):
return True
if len(hits) >= 2: # 同一行 ≥2 个内部词 ⇒ 判为节序对照句
return True
return all(_quoted(line, w) for w in hits)
def collect(paths, exts):
files = []
for p in paths:
p = os.path.abspath(p)
if os.path.isfile(p):
files.append(p)
continue
if not os.path.isdir(p):
raise SystemExit("⛔ 路径不存在:%s" % p)
for dp, dn, fn in os.walk(p):
dn[:] = [d for d in dn if d not in SKIP_DIRS]
for f in fn:
if f.startswith("_superseded"):
continue
if os.path.splitext(f)[1].lower() in exts:
files.append(os.path.join(dp, f))
return sorted(set(files))
def check_file(path):
"""返回 (fails, hints, passes) 三个字符串列表。"""
fails, hints, passes = [], [], []
with open(path, encoding="utf-8", errors="ignore") as f:
text = f.read()
lines = text.split("\n")
n_cited = 0
# ① 内部词表 + ③ 机制侧越界
for i, line in enumerate(lines, 1):
hits = [w for w in INTERNAL if w in line]
if not hits:
continue
if _citations(line, hits):
n_cited += len(hits)
continue
for w in hits:
fails.append("① 内部词 `%s` @ 行 %d —— %s\n %s" % (w, i, INTERNAL[w], line.strip()[:110]))
if w == "宿主" and any(m in line for m in MECH_MARKERS):
fails.append("③ 机制侧越界:这一句把 `WorkBuddy 本体` 写成了「宿主」@ 行 %d\n %s"
% (i, line.strip()[:110]))
# ② 同一实体两种叫法同篇(只算**非引用**命中:引号里的「待改对象」不算在用旧名)
live = []
for a, b, what in ALIAS_PAIRS:
na = sum(1 for line in lines
if a in line and not _citations(line, [w for w in INTERNAL if w in line]))
nb = sum(1 for line in lines
if b in line and not _citations(line, [w for w in INTERNAL if w in line]))
live.append((a, b, what, na, nb))
for a, b, what, na, nb in live:
if na and nb:
fails.append("② 同一实体两种叫法同篇:`%s` × %d 行 与 `%s` × %d 行 同时出现 —— %s 只许一个名"
% (a, na, b, nb, what))
if n_cited:
hints.append("引用了 %d 处内部词/旧名(判为「待改对象」,放行)" % n_cited)
passes.append("① 内部词表(非引用命中=0)|② 别名同篇|③ 机制侧越界 — 均无 FAIL")
return fails, hints, passes
def main():
ap = argparse.ArgumentParser(description="交付物用词闸门(内部词 / 别名 / 机制侧越界)")
ap.add_argument("--paths", action="append", required=True,
help="交付物目录或文件,可重复;⛔ 不隐式全盘扫")
ap.add_argument("--ext", default=".md,.html",
help="扫哪些扩展名(逗号分隔,缺省 .md,.html)")
a = ap.parse_args()
exts = tuple(e if e.startswith(".") else "." + e for e in a.ext.split(",") if e.strip())
try:
files = collect(a.paths, exts)
except SystemExit as e:
print(e)
return 2
if not files:
print("⛔ 一个待查文件都没扫到(给了 %s,扩展名 %s)—— 检查路径与 --ext。" % (a.paths, exts))
return 2
all_f, all_h, n_pass = [], [], 0
print("待查 %d 份(扩展名 %s)\n" % (len(files), "/".join(exts)))
for p in files:
fails, hints, passes = check_file(p)
rel = p
if fails:
print("⛔ %s" % rel)
for x in fails:
print(" %s" % x)
else:
n_pass += len(passes)
print("[过] %s" % rel)
for x in hints:
print(" [提示] %s" % x)
all_f += fails
all_h += hints
print()
print("小计:文件 %d | 通过项 %d | FAIL %d | 提示 %d"
% (len(files), n_pass, len(all_f), len(all_h)))
if all_f:
print("⛔ 有用词 FAIL —— 交付前必须清掉(引号里的「待改对象」不在此列)。")
return 1
print("✅ 用词全过。")
if all_h:
print("ℹ️ 提示项是「引用旧名/内部词」,人工看一眼是否确实是待改对象。")
return 0
if __name__ == "__main__":
sys.exit(main())