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/(服务自己会建目录)
196 lines
9.1 KiB
Python
196 lines
9.1 KiB
Python
#!/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())
|