技能库同步:另一台机的 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/(服务自己会建目录)
This commit is contained in:
admin committed 2026-10-16 23:13:33 +08:00
1 parent 749c0e3d5d
commit 8160f91559
336 files changed
+12821 -6621

No files matched your search

+22 -22
View File
@@ -13,7 +13,7 @@
这三处**没有任何门禁会报错** —— 只能靠回读,而回读会漏。
用法:
python scripts/check_naming.py --ws <工作区> # 查技能包自身 + 该工作区的产出白名单
python scripts/check_naming.py --ws <工作区或产品根> # 查技能包自身 + 该工作区的产出白名单
python scripts/check_naming.py --root <某个技能根> # 只查一份
退出码:0 = 全过;1 = 有 FAIL。
"""
@@ -79,7 +79,7 @@ DEPRECATED = [
]
# ⛔ 2026-10-06 用户定案:①段**产出白名单**——本段只许这五份,第 6 份须用户明确要求。
# 判据:扫 `docs/pm/<项目>/research/` 的实际文件名,出现白名单外的 `.md` 即 FAIL。
# 判据:扫 `产品规划/research/` 的实际文件名,出现白名单外的 `.md` 即 FAIL。
# ⛔ 用 os.walk 时**必须逐层拆开**:`research/` 与 `prd/` 是两个不同的白名单。
WHITELIST = {
"stage-discovery": {
@@ -104,9 +104,9 @@ REMOVED_ASSETS = ["ui-page-design"]
# 总入口的「只读」清单,三处还指着已废的 `strategy/` 与 `ux/`,而当时没有任何门禁会报。
# 豁免:行内出现下面任一废止标记时视为正当留痕(说明"原来叫什么、现在改成什么")。
DEAD_PATHS = {
r"strategy/30-|strategy/31-|docs/pm/[^`\s]*/strategy/":
r"strategy/30-|strategy/31-|产品规划/strategy/":
"已废的 ①段 strategy/ 目录(现全部落 research/1a~1e)",
r"ux/state-machine|ux/diagrams|docs/pm/[^`\s]*/ux/":
r"ux/state-machine|ux/diagrams|产品规划/ux/":
"已废的 ②段 ux/ 目录(状态流转已移进 prd/2a 的功能条目)",
}
DEAD_PATH_EXEMPT = ("旧", "已废", "已改名", "曾", "历史上", "移进", "移到", "改为",
@@ -318,7 +318,8 @@ def check_root(root, label):
# ⇒ 旧名提示恒不触发,`strategy/30-产品策略.md` 这类真残留被静默放过(假绿)。
# 现合并成一个循环,一律用本轮读到的 `tt`。
for dp, dn, fn in os.walk(root):
dn[:] = [d for d in dn if d != "__pycache__"]
dn[:] = [d for d in dn
if d not in ("__pycache__", "_留痕", "归档", "assets")]
for f in fn:
if not f.endswith(".md"):
continue
@@ -358,7 +359,7 @@ def check_root(root, label):
def check_whitelist(ws, label):
"""①②段产出白名单:`docs/pm/<项目>/<dir>/` 里出现白名单外的 .md ⇒ FAIL。
"""①②段产出白名单:`<产品根>/产品规划/<dir>/` 里出现白名单外的 .md ⇒ FAIL。
⛔ 这是**产出层**门禁,与命名门禁不同源:命名管"文档里写的名字有没有漂",
本函数管"磁盘上到底多出了什么文件"。2026-10-06 事故:某轮自作主张多产了一份
@@ -368,11 +369,10 @@ def check_whitelist(ws, label):
"""
import glob
for slug, rule in WHITELIST.items():
pat = os.path.join(ws, "docs", "pm", "*", rule["dir"])
pat = os.path.join(ws, "产品规划", 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("\\", "/")
@@ -388,31 +388,31 @@ def main():
ap.add_argument("--root", action="append", default=None,
help="技能根目录,可重复;默认只查本技能包自身")
ap.add_argument("--ws", default=None,
help="工作区根(含 docs/pm 的那一层);默认从 cwd 向上找")
help="工作区根或产品根(含 `产品规划` 与 `产品原型` 的那一层);默认从 cwd 向上找")
args = ap.parse_args()
# 本文件位于 <技能包>/scripts/ ⇒ 上溯 2 层 = 技能包(SKILL.md 就在这层)
skill_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
# 🔴 产品根定位**沿用③段 `_common.py` 的唯一一份实现**(⛔ 不在这里再抄一套逻辑:
# 两处实现迟早一处改一处漏,同族事故在 2026-10-06 的判据漂移里已经吃过一次)。
_sk_scripts = os.path.join(skill_root, "references", "stage-delivery", "scripts")
if _sk_scripts not in sys.path:
sys.path.insert(0, _sk_scripts)
import _common as C
# 🔴🔴 2026-10-07 技能迁全局后的**必须修正**:
# 旧写法 `ws = os.path.dirname(skill_root)` 是按「技能包在工作区根」写的。
# 技能从工作区搬到技能根(`<技能根>/product-planning/`)之后,
# `ws` 会算成技能根 ⇒ **`docs/pm/*` 一个都扫不到**
# `ws` 会算成技能根 ⇒ **`产品规划/*` 一个都扫不到**
# ⇒ `check_whitelist()` 静默扫零个目录、**恒绿通过**(假绿比报红更危险)。
# ⇒ 工作区一律**显式给**:`--ws <工作区>`;没给就向上找「含 docs/pm 的那一层」,
# ⇒ 工作区一律**显式给**:`--ws <工作区或产品根>`;没给就向上找「含 `产品规划` 与 `产品原型` 的产品根」,
# 再找不到就**明确报 FAIL**,⛔ 绝不静默跳过。
roots = args.root or [skill_root]
ws = args.ws
# `--ws` 收**工作区根或产品根都行**;没给就从 cwd 起认(含向上找)。
ws = C.find_product_root(args.ws) if args.ws else C.find_product_root(os.getcwd())
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()
add("FAIL", "找得到产品根(含 `产品规划` 与 `产品原型` 的那一层)",
"⛔ 未找到 ⇒ 产出白名单**没被真正检查**。请显式传 `--ws <工作区或产品根>`。")
ws = args.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))
+195
View File
@@ -0,0 +1,195 @@
#!/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())
@@ -0,0 +1,99 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""产品根定位自检:造临时夹具验 `_common.find_product_root()` 的三态行为。
为什么要有它:目录口径换成「工作区根 / 项目 / 产品根{产品规划, 产品原型{designs, pages}}」之后,
**认错产品根**会让产物落到别的产品目录下(事后极难拆干净)。
本脚本只造临时夹具、**绝不碰任何真实工作区**,跑完自动清理。
用法:
python scripts/selftest_product_root.py
退出码:0 = 全过;1 = 有用例没过。
"""
import importlib.util
import io
import os
import shutil
import sys
import tempfile
try:
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
except Exception:
pass
SK = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
COMMON = os.path.join(SK, "references", "stage-delivery", "scripts", "_common.py")
def load_common():
spec = importlib.util.spec_from_file_location("_sm_common", COMMON)
mod = importlib.util.module_from_spec(spec)
sys.modules["_sm_common"] = mod
spec.loader.exec_module(mod)
return mod
def mkproot(p):
"""按新结构造一个产品根:产品规划/ + 产品原型/{designs,pages}/"""
for d in ("产品规划",
os.path.join("产品原型", "designs"),
os.path.join("产品原型", "pages")):
os.makedirs(os.path.join(p, d), exist_ok=True)
def main():
C = load_common()
fix = tempfile.mkdtemp(prefix="proot-selftest-")
out = []
def build_single():
mkproot(os.path.join(fix, "ws1", "项目", "only"))
def build_multi(pointer=""):
for nm in ("a", "b"):
mkproot(os.path.join(fix, "ws2", "项目", nm))
reg = os.path.join(fix, "ws2", ".registry")
os.makedirs(reg, exist_ok=True)
if pointer:
io.open(os.path.join(reg, "current-project"), "w", encoding="utf-8").write(pointer)
cases = [
("单产品·给工作区根", build_single, os.path.join(fix, "ws1"),
os.path.join(fix, "ws1", "项目", "only")),
("单产品·给产品根本身", build_single, os.path.join(fix, "ws1", "项目", "only"),
os.path.join(fix, "ws1", "项目", "only")),
("单产品·从子目录向上认", build_single,
os.path.join(fix, "ws1", "项目", "only", "产品原型", "pages"),
os.path.join(fix, "ws1", "项目", "only")),
("多产品·无指针·不许猜", build_multi, os.path.join(fix, "ws2"), ""),
("多产品·有指针·认指针", lambda: build_multi("b"), os.path.join(fix, "ws2"),
os.path.join(fix, "ws2", "项目", "b")),
]
try:
nfail = 0
for name, build, arg, expect in cases:
shutil.rmtree(fix, ignore_errors=True)
os.makedirs(fix, exist_ok=True)
build()
got = C.find_product_root(arg)
ok = os.path.normcase(os.path.abspath(got)) == os.path.normcase(os.path.abspath(expect))
out.append("%s %-26s 期望=%s | 实得=%s" % (
"[过]" if ok else "[FAIL]", name,
os.path.relpath(expect, fix) if expect else "(空)",
os.path.relpath(got, fix) if got else "(空)"))
if not ok:
nfail += 1
finally:
shutil.rmtree(fix, ignore_errors=True)
for line in out:
print(line)
print()
print("小计:%d / %d 过" % (len(cases) - nfail, len(cases)))
print("✅ 产品根定位全过。" if not nfail else "⛔ 有用例没过 —— 先修定位再跑流水线。")
return 0 if not nfail else 1
if __name__ == "__main__":
sys.exit(main())