feat(skills): 两包资源索引机制与双向对账脚本
- product-planning:新建 references/manifest.md 作资源唯一登记处(资源/落点/引用点/口径); 顶层 SKILL 加两条硬规矩,design-capture、stage-delivery、execution-runbook、stage-requirements 四处把散落路径改为「落点见 references/manifest.md」 - session-mechanism:01-文档索引 新增 §四「包内资源索引」,并说明不放 manifest.md 的理由 (那份由 install.py 本机生成、被 .gitignore 挡着) - 新增两包共用的 scripts/check_resource_index.py(双向对账)与 product-planning/scripts/check_figures.py(图片机检), selftest 补对应用例,06-技能优化规程 写入写法规矩 - .gitignore:新增 _留痕/ 不入库(改造前整包快照,180K/688K,库内从无先例)
This commit is contained in:
1 parent
a9d2adaf72
commit
acfb252bee
14 files changed
+626
-39
No files matched your search
@@ -476,12 +476,12 @@ python .workbuddy/collab/collabd.py --set-life 进行中 --by "主会话"
|
||||
**⑨ 我要优化技能本身**(用户说「**优化技能**」「**技能怎么改更好**」「**梳理技能**」「**梳理一下技能的要求和描述**」「**技能太长**」「**这条规则写在哪**」「**这个技能能不能拷到别的机器用**」——任一句即触发)⇒ 读 **`references/06-技能优化规程.md`**(2026-10-15 立)。
|
||||
**一句话判据**:**写技能时,假设读者是"另一台机器上的另一个会话"** —— 拿它回放一遍,毛病自己浮出来。
|
||||
**三层,按 B → C → D 这个顺序做**(🔴 本规程**只收通用要求**;`env/` 怎么放、路径怎么写、什么不入仓这类**某个技能包专有的配置规则不在其中**):
|
||||
· **B 结构层**(结构清晰·重点突出·无冲突·边界清晰)—— #1 单一真源 #2 注入面与真源同源(🔴 每轮注入的面**改文字必须量长度**)#3 命名一致 #4 正文不自相矛盾。
|
||||
· **B 结构层**(结构清晰·重点突出·无冲突·边界清晰)—— #1 单一真源 #2 注入面与真源同源(🔴 每轮注入的面**改文字必须量长度**)#3 命名一致 #4 正文不自相矛盾 #5 要求落在生成时看得见的面上 #6 资源路径只在资源清单写一次。
|
||||
· **C 流程层**(流程顺畅·功能正常)—— #5 步骤顺序正确 #6 编号连续 #7 无跳步 #8 步骤间职责不越界 #9 字段归属正确 #10 上下游不断裂 #11 引用路径正确 #12 字段不缺失 #13 产物链完整(**P0**)。
|
||||
· **D 表述层**(简洁·高效·实用·无歧义)—— #14 肯定表述(正本 ⇒ `references/rules.md §9`)#15 说人话 #16 一页纸。
|
||||
🔴 **优先级**:B 结构 > C 流程 > D 表述 —— 前两层出毛病**机制失效或断链**,表述只是**效果差**。
|
||||
🔴 **目标(用户 2026-10-15 原话)**:让技能描述**结构清晰·重点突出·简洁·高效·实用·无歧义·流程顺畅·功能正常·规则边界清晰·无冲突**。
|
||||
🔴 **文末附「技能优化描述 · 总表」17 条三层**;**验法按层不同**:B ⇒ 同源比对 md5/C ⇒ 画数据流看断点/D ⇒ 人读(⛔ 别假装它有自动结论)。
|
||||
🔴 **文末附「技能优化描述 · 总表」18 条三层**;**验法按层不同**:B ⇒ 同源比对 md5/C ⇒ 画数据流看断点/D ⇒ 人读(⛔ 别假装它有自动结论)。
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -39,6 +39,7 @@
|
||||
| **判断「要不要做/怎么做/能不能先凑合」** | `04-决策方法论.md`(判据正文)+ `dsh-decision-method/`(U/A/X 三份素材库) | — |
|
||||
| **写语气/去 AI 味** | `作业规矩/04-去AI味与说话方式.md`(**会话场景加固版**,含交付前清单与质量评分)+ `humanizer-en/SKILL.md`(**英文硬核版**:55 模式/5 语气档/0–100 打分) | — |
|
||||
| **作业总规矩/工作区纪律/多棒接力** | `作业规矩/00-作业总规矩(原 agent-operating-rules).md` + `作业规矩/02-工作区纪律.md` + `作业规矩/03-多棒接力编排.md` | — |
|
||||
| **包内有哪些资源、谁在用、在哪**(脚本 / 看板 / 资产 / 子规程 · 要改路径前先看) | 本文件 §四「包内资源索引」 | — |
|
||||
|
||||
> 📌 **上表三行 2026-10-10 补齐**:`04-决策方法论.md`、`dsh-decision-method/`、`作业规矩/`、`humanizer-en/` 原先只被正文引用、没进本表
|
||||
> ⇒ 换会话时会被当成孤儿(本包 §二 规则 1 要求「新增文档必须在这张表里登记」)。
|
||||
@@ -70,3 +71,46 @@
|
||||
⚠️ **10-04 的教训**:一条红线立在对的地方(工作区日志 197 KB),仍然等于没立
|
||||
⇒ **凡是「下次必须做到」的事,必须落在 `references/` 或 `SKILL.md`,⛔ 不能只写日志。**
|
||||
判据:`selftest.py::t_no_invented_consequence` 量的就是这个(红线在动手层**第一段**)。
|
||||
|
||||
## 四、🔴 包内资源索引(有哪些资源 · 谁在用 · 在哪)
|
||||
|
||||
> **这一节管什么**(2026-10-16 新增,用户原话:「产品规划 和 会话技能 都检查如何解决 资源索引的问题,避免资源到处引用」):
|
||||
> 回答「**某个资源被谁引用、引用点在哪**」,⛔ 不必靠 `grep` 全包找。
|
||||
> **盘面分工(三层,⛔ 别混)**:`05-技能库盘点规程.md` 管**库层面**(技能根下有哪些技能、该不该入仓)|
|
||||
> `06-技能优化规程.md` 管**写法**(描述 / 结构 / 流程怎么写,其 §B #6 就是本节的规矩)|**本节管包内资源索引**。
|
||||
>
|
||||
> 🔴 **本节为什么在这里、不在 `manifest.md`**:`references/manifest.md` 是 `install.py --manifest` 在本机生成的
|
||||
> 逐文件 md5 表,**被仓根 `.gitignore` 挡着(不入库)** ⇒ 索引写那里=别台机器取仓后**看不到**,机制静默失效。
|
||||
> 本节放在**入库**的这份索引里;`product-planning` 没有同类入库载体,它的索引新建成 `references/manifest.md`。
|
||||
>
|
||||
> **机检**:`python scripts/check_resource_index.py --pkg "<本包根>" --index references/01-文档索引.md`
|
||||
> (同一份实现也给 `product-planning` 用:`--pkg "<技能根>/product-planning"`,那边用默认 `--index`)。
|
||||
> 四个闸门:落点是否存在 / 登记的引用点是否为真 / `口径=只写一次` 的资源有没有人绕开清单在别处抄路径 /
|
||||
> 有没有登记了却没人用的资源。
|
||||
>
|
||||
> **`口径` 列**:`只写一次` = 该资源路径字面量只许出现在本清单与 `许可字面量` 列登记的文件里;
|
||||
> `可多次` = 只登记「谁在用、在哪」,不限字面量。🔴 **本包资源一律 `可多次`** —— 这里的资源是**代码与规程**,
|
||||
> 引用必须精确到文件(写个模糊名字反而没法用);「只写一次」那道闸门是给**资产 / 样式套**这类资源用的,
|
||||
> 目前 `product-planning` 的「样式套目录」在用。
|
||||
>
|
||||
> 🔴 `被谁引用` 与 `许可字面量` 是两件事:前者「谁在用」(可由 `--sync-refs` 自动回写,超过 12 个只记个数);
|
||||
> 后者「谁被允许写路径字面量」(人工开的口子 —— ⛔ 自动回写会把它冲成「谁都许可」,闸门就空了)。
|
||||
|
||||
| 资源 | 落点 | 干什么 | 被谁引用 | 许可字面量 | 口径 | 入仓 |
|
||||
|---|---|---|---|---|---|---|
|
||||
| 机制脚本 | `scripts/` | 常驻与机制脚本(`collabd.py` / `collabctl.py` / `goalctl.py` / `supervise-launch.py` / `guard.py` / `deploy_code.py` 等) | SKILL.md、boards/会话看板/board-launch.py、boards/会话看板/board-launch.py.tpl、boards/会话看板/board.py、boards/项目看板/project-board.py、install.py、references/00-动手前必过.md、references/02-功能优先协作协议.md、references/03-回复排版-核心块.md、references/04-决策方法论.md、references/06-技能优化规程.md、references/architecture.md(共 44 个) | — | 可多次 | 是 |
|
||||
| 宿主钩子 | `scripts/hooks/` | 挂在宿主事件上的钩子脚本 | SKILL.md、boards/会话看板/board.html、boards/会话看板/board.py、boards/会话看板/board_ext.py、install.py、references/03-回复排版-核心块.md、references/99-速查清单.md、references/architecture.md、references/collab-detail.md、references/collab.md、references/deploy.md、references/humanizer-en/SKILL.md(共 32 个) | — | 可多次 | 是 |
|
||||
| 锁机制 | `scripts/lock/` | 锁原语与释放工具 | SKILL.md、boards/会话看板/board-render-probe.js、boards/会话看板/board.html、boards/会话看板/board.py、boards/会话看板/tmp/supervise-inbox/board.json、boards/项目看板/project-board.html、boards/项目看板/project-board.py、install.py、references/architecture.md、references/collab-detail.md、references/collab.md、references/deploy.md(共 38 个) | — | 可多次 | 是 |
|
||||
| 取证工具 | `scripts/forensics/` | 取证 / 诊断工具 | SKILL.md、references/architecture.md、references/collab.md、references/forensics.md、references/manifest.md、references/pitfalls.md、references/作业规矩/00-作业总规矩(原 agent-operating-rules).md、scripts/hooks/wb-result-hook.py | — | 可多次 | 是 |
|
||||
| 资源索引机检 | `scripts/check_resource_index.py` | 本节与 `product-planning` 资源表的双向对账(两份技能共用这一份实现) | references/06-技能优化规程.md、references/manifest.md、scripts/check_resource_index.py | — | 可多次 | 是 |
|
||||
| 会话看板 | `boards/会话看板/` | 会话架构看板(端口 20099);页面与脚本同处一包 | SKILL.md、boards/会话看板/board-launch.py、boards/会话看板/board.py、boards/会话看板/board_ext.py、boards/会话看板/tmp/supervise-inbox/board.json、boards/项目看板/project-board.py、references/architecture.md、references/collab-detail.md、references/deploy.md、references/manifest.md、references/pitfalls.md、scripts/collabctl.py(共 14 个) | — | 可多次 | 是 |
|
||||
| 项目看板 | `boards/项目看板/` | 项目+任务看板(端口 20100);页面与脚本同处一包 | SKILL.md、boards/项目看板/project-board-launch.py、boards/项目看板/project-board.html、boards/项目看板/project-board.py、references/manifest.md | — | 可多次 | 是 |
|
||||
| 作业规矩 | `references/作业规矩/` | 作业总规矩等写法类规程 | SKILL.md、references/02-功能优先协作协议.md、references/03-回复排版-核心块.md、references/architecture.md、references/collab-detail.md、references/manifest.md、references/rules.md、scripts/check_resource_index.py、scripts/hooks/reply-style-guard.py、scripts/hooks/skill-load-guard.py、scripts/selftest.py | — | 可多次 | 是 |
|
||||
| 决策方法论 | `references/dsh-decision-method/` | `04-决策方法论.md` 背后的素材库(U / A / X 条目) | references/02-功能优先协作协议.md、references/04-决策方法论.md、references/dsh-decision-method/素材库-A-AI推理.md、references/dsh-decision-method/素材库-U-用户决策.md、references/dsh-decision-method/素材库-反例-X.md、references/manifest.md、references/pitfalls.md、scripts/hooks/decision-rules-hook.py、scripts/hooks/skill-load-guard.py、scripts/session-rules-check.py | — | 可多次 | 是 |
|
||||
| 英文文案判据 | `references/humanizer-en/` | 英文版文案判据(中文版在 `humanizer` 技能) | SKILL.md、references/collab-detail.md、references/manifest.md、references/作业规矩/04-去AI味与说话方式.md、scripts/selftest.py | — | 可多次 | 是 |
|
||||
| 输出阶梯 | `references/karpathy-output-ladder/` | 输出阶梯判据 | references/karpathy-output-ladder/SKILL.md、references/manifest.md | — | 可多次 | 是 |
|
||||
| 规程与背景文档 | `references/` | 顶层 `*.md`:`00`–`06` 规程 + `99-速查清单` + 本文件 + `architecture` / `rules` / `pitfalls` / `taskgraph` / `collab` / `collab-detail` / `deploy` / `forensics` / `supervise-persistence` | SKILL.md、boards/会话看板/board-launch.py.tpl、boards/会话看板/board_ext.py、install.py、references/02-功能优先协作协议.md、references/03-回复排版-核心块.md、references/04-决策方法论.md、references/05-技能库盘点规程.md、references/06-技能优化规程.md、references/99-速查清单.md、references/architecture.md、references/collab-detail.md(共 40 个) | — | 可多次 | 是 |
|
||||
| 留痕 | `references/_留痕/` | 改动前快照(append-only,⛔ 不进机检扫描范围) | SKILL.md、boards/会话看板/board-launch.py、boards/会话看板/board.html、boards/会话看板/board.py、boards/会话看板/board_ext.py、boards/项目看板/project-board.py、install.py、references/02-功能优先协作协议.md、references/architecture.md、references/collab.md、references/dsh-decision-method/素材库-A-AI推理.md、references/manifest.md(共 31 个) | — | 可多次 | 是 |
|
||||
| 设计令牌 | `assets/design-tokens.css` | 包内静态样式令牌(看板页面用) | SKILL.md、references/manifest.md | — | 可多次 | 是 |
|
||||
| 装机脚本 | `install.py` | 一键配置;`--manifest` 重算 `references/manifest.md` 那张逐文件表 | SKILL.md、boards/会话看板/board.py、boards/会话看板/board_ext.py、boards/项目看板/project-board.py、install.py、references/06-技能优化规程.md、references/deploy.md、references/manifest.md、references/pitfalls.md、references/rules.md、scripts/check_resource_index.py、scripts/collabctl.py(共 29 个) | — | 可多次 | 是 |
|
||||
| 本机配置 | `env/roots.env` | 每台机器一份的工作区 roots(`*.example` 入仓,实体不入仓) | SKILL.md、boards/会话看板/board-launch.py、boards/会话看板/board-launch.py.tpl、boards/会话看板/board.py、boards/会话看板/board_ext.py、boards/项目看板/project-board-launch.py、boards/项目看板/project-board.py、install.py、references/deploy.md、references/manifest.md、references/pitfalls.md、references/rules.md(共 34 个) | — | 可多次 | 否 |
|
||||
@@ -47,6 +47,21 @@
|
||||
⚠️ 只写进 `references/` 等着回头查 ⇒ 等于没立;机核脚本只作**兜底**,⛔ 不当主手段。
|
||||
(用户 2026-10-16 令:「要生成的时候 就按照要求,不是事后再修复」。)
|
||||
|
||||
**6 资源路径只在资源清单写一次**
|
||||
单包资源索引。本包有什么资源(脚本 / 看板 / 资产 / 子规程 / 同根技能依赖)、落在哪、被谁引用,**唯一登记处**是该包里
|
||||
**入库的那份索引文件**(本包=`references/01-文档索引.md §四`;`product-planning`=`references/manifest.md`);
|
||||
别的文字里**只写资源名**,⛔ 不把路径抄进散文、段文档、脚本注释与模板。
|
||||
🔴 **索引文件必须放入库的那份** —— 实测踩过:本包 `references/manifest.md` 被仓根 `.gitignore` 挡着
|
||||
(`install.py --manifest` 在本机生成、随机器变)⇒ 索引写那里=别台机器取仓后看不到,机制静默失效。
|
||||
判据:**改一次目录,要改几个文件** —— 一处以上 ⇒ 那是照抄,不是引用。
|
||||
机检:`python scripts/check_resource_index.py --pkg "<本包根>" --index references/01-文档索引.md`
|
||||
(`product-planning` 用默认 `--index`)—— 四项(落点是否存在 / 登记的引用点是否为真 /
|
||||
有没有人绕开清单在别处抄路径 / 有没有登记了却没人用的资源);⛔ 有 🔴 不算改完,不静默放过。
|
||||
⚠️ 何时把资源设成 `只写一次`:**资产 / 样式套**这类「换个落点也不影响怎么用」的(现例:`product-planning` 的样式套目录);
|
||||
**代码与规程**设成 `可多次` —— 引用必须精确到文件,写模糊名字反而没法用。
|
||||
(用户 2026-10-16 原话:「为什么这么多地方引用 一个样式库,是不是技能中没有统一索引机制」+
|
||||
「产品规划 和 会话技能 都检查如何解决 资源索引的问题,避免资源到处引用」。)
|
||||
|
||||
### C 流程层(→ 流程顺畅 · 功能正常)
|
||||
|
||||
> 来路:2026-08-10 另一工作区对创作流程做全量审计归纳的 13 项里,9 项属这一层;
|
||||
@@ -86,10 +101,10 @@
|
||||
|
||||
---
|
||||
|
||||
## 技能优化描述 · 总表(17 条,三层)
|
||||
## 技能优化描述 · 总表(18 条,三层)
|
||||
|
||||
> **怎么来的**:① B/C/D 三层判据(=本规程正文);② 另一工作区的 `技能审计检查清单.md`(`E:/ProgramData/AIProject/mcn-short-video/技能审计检查清单.md`,2026-08-10 创作流程全量审计的 13 项;跨根引用,仅作来路说明)。
|
||||
> **用法**:按 **1 → 17** 逐条过。**分级**:**P0 = 会让人出事或断链**(#13 产物链)/**P1 = 不一致或冗余**(B 层 + C 层其余)/**P2 = 效果差**(D 层)。
|
||||
> **用法**:按 **1 → 18** 逐条过。**分级**:**P0 = 会让人出事或断链**(#13 产物链)/**P1 = 不一致或冗余**(B 层 + C 层其余)/**P2 = 效果差**(D 层)。
|
||||
> 🔴 **判据正本 = 本规程正文的对应项**,表里只给锚点与反例;⛔ **本表只收通用要求** —— `env/` 怎么放、
|
||||
> 路径怎么写、什么不入仓这类**专有配置规则不在本表**,见 `01-文档索引.md` 的「技能环境配置规范」。
|
||||
|
||||
@@ -112,3 +127,4 @@
|
||||
| 15 | D 表述 | 说人话 | 见 §D #15 | 四种 AI 味:三段式排比连用/"…着"结尾的肤浅分析/模糊归因/拆成加粗小标题+竖排碎句 |
|
||||
| 16 | D 表述 | 一页纸 | 见 §D #16 | 一份 300 行的"速查" |
|
||||
| 17 | B 结构 | 要求落在生成时看得见的面上 | 见 §B #5 | 只写进 `references/` 等着回头查 ⇒ 等于没立 |
|
||||
| 18 | B 结构 | 资源路径只在资源清单写一次 | 见 §B #6 | 同一资源路径抄在 6 个文件 20 处,改目录漏改不报错 |
|
||||
@@ -0,0 +1,320 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""资源索引 · 双向对账机检(技能包内通用)
|
||||
|
||||
用途
|
||||
----
|
||||
每个技能包在 `references/manifest.md` 里维护一张「资源索引」表:
|
||||
|
||||
| 资源 | 落点 | 干什么 | 被谁引用 | 许可字面量 | 口径 | 入仓 |
|
||||
|
||||
本脚本对这张表做**双向对账**,四个闸门:
|
||||
|
||||
G1 落点存在 —— 每行 `落点` 必须在磁盘上(`入仓=否` 的行本机缺只给 ⚠,属正常)
|
||||
G2 引用为真 —— `被谁引用` 里登记的每个文件必须存在、且真的提到该资源(⛔ 不许登记假引用点;
|
||||
`许可字面量` 指向的文件也必须存在,⛔ 不许开空头许可)
|
||||
G3 字面量收敛 —— `口径=只写一次` 的行:该资源的路径字面量只许出现在清单本身与 `许可字面量` 登记的文件里,
|
||||
出现在别处 ⇒ 🔴。这条就是「避免资源到处引用」的机检(⛔ 漏一处必报,不静默放行)
|
||||
G4 孤儿 / 未登记 —— `被谁引用` 为空的行 ⇒ 🔴;`assets/` 与 `references/stage-*/assets/` 下
|
||||
磁盘上有一级子项而表里没有 ⇒ 🔴
|
||||
|
||||
🔴 `被谁引用` 与 `许可字面量` 是**两件事,别合并**:前者是「谁在用」(可由 `--sync-refs` 自动回写),
|
||||
后者是「谁被允许写路径字面量」(人工开的口子,⛔ 自动回写会把它冲成"谁都许可"⇒ G3 变空闸门)。
|
||||
|
||||
「被谁引用」怎么认:既认**路径形态**(含该资源路径尾两段),也认**名字形态**(含资源名或路径末段)
|
||||
—— 因为收敛之后散文里就只剩名字了,只认路径会把收敛好的资源误判成孤儿。
|
||||
|
||||
用法
|
||||
----
|
||||
python check_resource_index.py --pkg <技能包根>
|
||||
python check_resource_index.py --pkg <技能包根> --index references/01-文档索引.md
|
||||
python check_resource_index.py --pkg <技能包根> --sync-refs # 用扫描结果回写「被谁引用」列
|
||||
|
||||
🔴 索引文件必须放在该技能包**入库(git 跟踪)**的那份文件里 —— 实测踩过:
|
||||
`session-mechanism/references/manifest.md` 被 `.gitignore` 挡着(它是 `install.py --manifest`
|
||||
在本机生成、随机器变)⇒ 索引写那里=别台机器取仓后**看不到**,机制静默失效。
|
||||
该包的索引改放 `references/01-文档索引.md`(入库),用 `--index` 指过去。
|
||||
|
||||
退出码:0 全过 | 1 有 🔴 | 2 用法或清单结构错(找不到表头 ⇒ 停手,⛔ 不猜)
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
try: # 出口一律声明编码(本机铁律)
|
||||
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
|
||||
sys.stderr.reconfigure(encoding="utf-8", errors="replace")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
HEADER = "| 资源 |"
|
||||
COLS = ["资源", "落点", "干什么", "被谁引用", "许可字面量", "口径", "入仓"]
|
||||
COLLAPSE = "只写一次" # 口径取值:只写一次 / 可多次
|
||||
SYNC_COL = 3 # 「被谁引用」在第几列(0 起)
|
||||
SYNC_CAP = 12 # 回写「被谁引用」时最多列几个文件,其余只记个数
|
||||
|
||||
TEXT_EXT = {".md", ".py", ".js", ".mjs", ".cjs", ".json", ".html", ".htm",
|
||||
".css", ".sh", ".bash", ".tpl", ".txt", ".yaml", ".yml"}
|
||||
SKIP_DIR = {"_留痕", "__pycache__", "logs", "node_modules", ".git", "env",
|
||||
".obsidian", "marks"}
|
||||
SKIP_FILE = {"install.log"}
|
||||
|
||||
|
||||
def is_scannable(p: Path, pkg: Path) -> bool:
|
||||
rel = p.relative_to(pkg)
|
||||
if any(part in SKIP_DIR for part in rel.parts):
|
||||
return False
|
||||
if p.name in SKIP_FILE or p.suffix not in TEXT_EXT:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def scan_files(pkg: Path) -> list[Path]:
|
||||
out = [p for p in pkg.rglob("*") if p.is_file() and is_scannable(p, pkg)]
|
||||
return sorted(out, key=lambda q: q.relative_to(pkg).as_posix())
|
||||
|
||||
|
||||
def read_text(p: Path) -> str:
|
||||
try:
|
||||
return p.read_text(encoding="utf-8", errors="replace")
|
||||
except Exception:
|
||||
return ""
|
||||
|
||||
|
||||
def cell(s: str) -> str:
|
||||
return s.strip().strip("`").strip()
|
||||
|
||||
|
||||
def split_refs(raw: str) -> list[str]:
|
||||
"""`被谁引用` / `许可字面量` 列 → 条目清单(按 `、;;,` 分隔)。
|
||||
|
||||
🔴 原样保留条目文本,⛔ 不在这一步剥括号 —— 真实文件名里可能就带全角括号
|
||||
(实测踩过:`references/作业规矩/00-作业总规矩(原 agent-operating-rules).md` 被剥成
|
||||
`…/00-作业总规矩.md` ⇒ 不存在 ⇒ 报假 🔴)。括号交给 `resolve()` 按「原样 → 去括号」两候选试。
|
||||
"""
|
||||
if not raw or raw.strip() in ("—", "-", "(无)", "(无)"):
|
||||
return []
|
||||
out: list[str] = []
|
||||
for it in re.split(r"[;;、,,]", raw):
|
||||
s = cell(it)
|
||||
if s:
|
||||
out.append(s)
|
||||
return out
|
||||
|
||||
|
||||
def resolve(pkg: Path, item: str) -> Path | None:
|
||||
"""把一条登记项解析成真实路径:先按原样,再按「剥掉括号说明」的样子各试一次。"""
|
||||
if item.startswith("<技能根>/"):
|
||||
cands = [same_root_dir(pkg, item)]
|
||||
else:
|
||||
cands = [pkg / item, pkg / re.sub(r"([^)]*)", "", item).strip(),
|
||||
pkg / re.sub(r"\([^)]*\)", "", item).strip()]
|
||||
for c in cands:
|
||||
if c.exists():
|
||||
return c
|
||||
return None
|
||||
|
||||
|
||||
class Row:
|
||||
def __init__(self, raw: str):
|
||||
c = raw.strip().strip("|").split("|")
|
||||
c += [""] * (len(COLS) - len(c))
|
||||
self.name = cell(c[0])
|
||||
self.loc = cell(c[1])
|
||||
self.what = cell(c[2])
|
||||
self.refs_raw = c[3].strip()
|
||||
self.allow_raw = c[4].strip() # 许可字面量:允许写该资源路径字面量的文件(G3 用)
|
||||
self.mode = cell(c[5])
|
||||
self.inrepo = cell(c[6])
|
||||
segs = [s for s in self.loc.replace("<技能根>/", "").split("/") if s]
|
||||
self.leaf = segs[-1] if segs else self.loc # 路径末段
|
||||
self.key = "/".join(segs[-2:]) if len(segs) >= 2 else self.leaf # 路径尾两段
|
||||
if self.loc.startswith("<技能根>/") and len(segs) == 1:
|
||||
self.key = segs[0]
|
||||
|
||||
|
||||
def load_table(man: Path) -> tuple[list[Row], int]:
|
||||
lines = man.read_text(encoding="utf-8").replace("\r\n", "\n").split("\n")
|
||||
head = next((i for i, ln in enumerate(lines) if ln.startswith(HEADER)), None)
|
||||
if head is None:
|
||||
raise SystemExit(f"🔴 {man} 里找不到 `{HEADER}` 表头 ⇒ 停手(⛔ 不猜、不追加)")
|
||||
rows: list[Row] = []
|
||||
i = head + 2
|
||||
while i < len(lines) and lines[i].startswith("|"):
|
||||
if cell(lines[i].strip().strip("|").split("|")[0]):
|
||||
rows.append(Row(lines[i]))
|
||||
i += 1
|
||||
if not rows:
|
||||
raise SystemExit(f"🔴 {man} 的表里没有任何数据行 ⇒ 停手")
|
||||
return rows, head
|
||||
|
||||
|
||||
def path_hits(r: Row, pkg: Path, files: list[Path], man: Path) -> list[tuple[Path, int, str]]:
|
||||
"""G3 用:找**路径形态**(含资源路径尾两段字面量)的出现。"""
|
||||
pat = re.compile(re.escape(r.key))
|
||||
out: list[tuple[Path, int, str]] = []
|
||||
for f in files:
|
||||
if f == man:
|
||||
continue
|
||||
for n, ln in enumerate(read_text(f).split("\n"), 1):
|
||||
if r.key in ln and pat.search(ln):
|
||||
out.append((f, n, ln.strip()[:120]))
|
||||
return out
|
||||
|
||||
|
||||
def ref_hits(r: Row, pkg: Path, files: list[Path], man: Path) -> list[str]:
|
||||
"""「被谁引用」用:路径形态 或 名字形态(资源名 / 路径末段)都算。"""
|
||||
toks = {t for t in (r.key, r.leaf, r.name) if t}
|
||||
hits: list[str] = []
|
||||
for f in files:
|
||||
if f == man:
|
||||
continue
|
||||
txt = read_text(f)
|
||||
if any(t in txt for t in toks):
|
||||
hits.append(f.relative_to(pkg).as_posix())
|
||||
return hits
|
||||
|
||||
|
||||
def same_root_dir(pkg: Path, loc: str) -> Path:
|
||||
name = loc.replace("<技能根>/", "").strip("/").split("/")[0]
|
||||
return pkg.parent / name
|
||||
|
||||
|
||||
def main() -> int:
|
||||
ap = argparse.ArgumentParser(description="资源索引 · 双向对账机检")
|
||||
ap.add_argument("--pkg", required=True, help="技能包根(如 E:/.../skills/product-planning)")
|
||||
ap.add_argument("--index", default="references/manifest.md",
|
||||
help="资源索引表所在文件(默认 references/manifest.md;"
|
||||
"某包的清单若不入库,就把索引放进该包**入库**的那份索引文件,再指过来)")
|
||||
ap.add_argument("--sync-refs", action="store_true",
|
||||
help="用扫描结果回写「被谁引用」列(逗号分隔的文件清单)")
|
||||
ap.add_argument("--max-list", type=int, default=10, help="每个闸门最多列几条明细")
|
||||
args = ap.parse_args()
|
||||
|
||||
pkg = Path(args.pkg).expanduser().resolve()
|
||||
if not pkg.is_dir():
|
||||
print(f"🔴 技能包根不存在:{pkg}")
|
||||
return 2
|
||||
man = pkg / args.index
|
||||
if not man.is_file():
|
||||
print(f"🔴 找不到资源索引文件 {man}(用 --index 指定;默认 references/manifest.md)")
|
||||
return 2
|
||||
|
||||
rows, head = load_table(man)
|
||||
files = scan_files(pkg)
|
||||
print(f"═══ 资源索引对账 · {pkg.name} ═══")
|
||||
print(f"清单:{man.relative_to(pkg).as_posix()} | 表行:{len(rows)} | 扫描文件:{len(files)}\n")
|
||||
|
||||
g1: list[str] = []
|
||||
g2: list[str] = []
|
||||
g3: list[str] = []
|
||||
g4: list[str] = []
|
||||
computed: dict[str, list[str]] = {}
|
||||
|
||||
for r in rows:
|
||||
# ── G1 落点存在 ────────────────────────────────────────────────
|
||||
if r.loc.startswith("<技能根>/"):
|
||||
cand = same_root_dir(pkg, r.loc)
|
||||
if not cand.is_dir() and r.inrepo != "否":
|
||||
g1.append(f"「{r.name}」落点(同根技能)不存在:{cand}")
|
||||
else:
|
||||
cand = pkg / r.loc.rstrip("/")
|
||||
if not cand.exists():
|
||||
if r.inrepo == "否":
|
||||
print(f" ⚠ 「{r.name}」落点本机缺(入仓=否 ⇒ 正常,新机器取仓后这一层为空):{r.loc}")
|
||||
else:
|
||||
g1.append(f"「{r.name}」落点不存在:{r.loc}")
|
||||
|
||||
# ── 引用扫描(G2 / G3 / --sync-refs 共用)──────────────────────
|
||||
computed[r.name] = ref_hits(r, pkg, files, man)
|
||||
declared = split_refs(r.refs_raw)
|
||||
|
||||
# ── G2 引用为真 ────────────────────────────────────────────────
|
||||
for d in declared:
|
||||
dp = resolve(pkg, d)
|
||||
if dp is None:
|
||||
g2.append(f"「{r.name}」登记的引用点不存在:{d}")
|
||||
elif not dp.is_file():
|
||||
pass # 同根技能目录:只判存在
|
||||
elif r.name not in read_text(dp) and r.leaf not in read_text(dp) \
|
||||
and r.key not in read_text(dp):
|
||||
g2.append(f"「{r.name}」登记的引用点其实没提到它:{d}")
|
||||
|
||||
# ── G3 字面量收敛(仅 `只写一次`)──────────────────────────────
|
||||
if r.mode == COLLAPSE:
|
||||
allow: set[str] = set()
|
||||
for d in split_refs(r.allow_raw):
|
||||
if resolve(pkg, d) is None:
|
||||
g2.append(f"「{r.name}」`许可字面量` 指向的文件不存在:{d}") # ⛔ 不许开空头许可
|
||||
allow.add(d.replace("\\", "/"))
|
||||
for f, n, t in path_hits(r, pkg, files, man):
|
||||
rel = f.relative_to(pkg).as_posix()
|
||||
if rel in allow:
|
||||
continue
|
||||
g3.append(f"「{r.name}」字面量出现在未登记处:{rel}:{n} ⇒ {t}")
|
||||
|
||||
# ── G4 另一半:孤儿登记 + 磁盘上有而表里没有 ────────────────────
|
||||
for r in rows:
|
||||
if not split_refs(r.refs_raw):
|
||||
g4.append(f"「{r.name}」`被谁引用` 为空 ⇒ 没人引用的登记项")
|
||||
registered = {r.loc.rstrip("/") for r in rows}
|
||||
for d in [pkg / "assets"] + sorted(pkg.glob("references/stage-*/assets")):
|
||||
if not d.is_dir():
|
||||
continue
|
||||
for sub in sorted(d.iterdir()):
|
||||
if sub.name.startswith(".") or sub.name.startswith("_"):
|
||||
continue
|
||||
rel = sub.relative_to(pkg).as_posix().rstrip("/")
|
||||
if not any(rl.endswith(rel) or rel.endswith(rl) for rl in registered):
|
||||
g4.append(f"磁盘上有、清单里没有:{rel}")
|
||||
|
||||
# ── 汇总 ────────────────────────────────────────────────────────────
|
||||
gates = [("G1 落点存在", g1), ("G2 引用为真", g2),
|
||||
("G3 字面量收敛", g3), ("G4 孤儿 / 未登记", g4)]
|
||||
bad = 0
|
||||
for title, items in gates:
|
||||
if items:
|
||||
bad += 1
|
||||
print(f"🔴 {title}:{len(items)} 处")
|
||||
for x in items[: args.max_list]:
|
||||
print(f" · {x}")
|
||||
if len(items) > args.max_list:
|
||||
print(f" … 余 {len(items) - args.max_list} 处")
|
||||
else:
|
||||
print(f"✅ {title}:0")
|
||||
|
||||
if args.sync_refs:
|
||||
raw = man.read_bytes()
|
||||
crlf = b"\r\n" in raw
|
||||
lines = raw.decode("utf-8").replace("\r\n", "\n").split("\n")
|
||||
for i in range(head + 2, len(lines)):
|
||||
if not lines[i].startswith("|"):
|
||||
break
|
||||
c = lines[i].strip().strip("|").split("|")
|
||||
if len(c) < len(COLS) or not cell(c[0]):
|
||||
continue
|
||||
lst = computed.get(cell(c[0]), [])
|
||||
shown = lst[:SYNC_CAP]
|
||||
txt = "、".join(shown)
|
||||
if len(lst) > SYNC_CAP:
|
||||
txt += f"(共 {len(lst)} 个)" # 括号里的计数会被 split_refs 剥掉,不影响 G2 校验
|
||||
c[3] = " " + (txt if txt else "—") + " "
|
||||
lines[i] = "|" + "|".join(c) + "|"
|
||||
out = "\n".join(lines)
|
||||
man.write_bytes((out.replace("\n", "\r\n") if crlf else out).encode("utf-8"))
|
||||
print(f"\n✍️ 已用扫描结果回写「被谁引用」列:{man.name}")
|
||||
|
||||
print()
|
||||
if bad:
|
||||
print(f"结论:🔴 不通过 —— {bad} 个闸门有命中(⛔ 漏一处必报,不静默放行)")
|
||||
return 1
|
||||
print("结论:✅ 通过 —— 四个闸门全 0")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -7920,14 +7920,16 @@ def t_term_table_single_source():
|
||||
return rows
|
||||
|
||||
|
||||
@case("🔴 06 的 16 条条目名:SKILL.md §0 ⑨ 与 06 正文/总表三方逐条同名(⛔ 写成简写=对不上)")
|
||||
@case("🔴 06 条目名:SKILL.md §0 ⑨ 与 06 正文/总表三方逐条同名(⛔ 写成简写=对不上)")
|
||||
def t_skill06_entries_same_source():
|
||||
"""🔴 2026-10-15 立(当日实测踩到)。
|
||||
"""🔴 2026-10-15 立(当日实测踩到);2026-10-16 改比对键。
|
||||
|
||||
`SKILL.md §0 ⑨` 是**匹配面**(点开技能就读到),`06` 正文与总表是**真源**。
|
||||
实测:§0 把 C 层写成简写「职责不越界」,而正文是「步骤间职责不越界」——
|
||||
两边都不报错,读者按简写去正文里搜就搜不到。
|
||||
⇒ 判据落在**16 条逐条同名**上(数字 + 名字都要对上)。
|
||||
⇒ 判据落在**条目名逐条同名**上。
|
||||
🔴 条数**以 06 总表为准**(⛔ 不再硬编码),所以 06 增删条目时只改 06 与 §0 ⑨,
|
||||
⛔ 不必回头改闸门 —— 硬编码过「应 16」,2026-10-16 实测因此漏过一整条(B#5)。
|
||||
"""
|
||||
import re as _re
|
||||
SK = (HERE.parent / "SKILL.md").read_text(encoding="utf-8", errors="replace")
|
||||
@@ -7937,28 +7939,41 @@ def t_skill06_entries_same_source():
|
||||
i = SK.find("**⑨ 我要优化技能本身**")
|
||||
j = SK.find("\n\n", i) if i >= 0 else -1
|
||||
blk = SK[i:j] if i >= 0 and j > i else ""
|
||||
a = {int(n): nm for n, nm in _re.findall(r"#(\d{1,2}) ([^(\s]+)", blk)}
|
||||
b = {int(n): nm for n, nm in _re.findall(r"^(?:- )?\*\*(\d{1,2}) ([^(\s]+)\*\*", P6, _re.M)}
|
||||
c = {int(n): nm for n, nm in _re.findall(
|
||||
r"^\| (\d{1,2}) \| [A-D] [^|]*\| ([^|\s]+) \|", P6, _re.M)}
|
||||
# 🔴 2026-10-16 改:**按条目名比,不按编号比**。
|
||||
# 原写法把三处读成 `{编号: 名字}` 再逐号比对 ⇒ 编号一撞就静默丢条目:
|
||||
# B 层与 C 层都用 `#5`(B#5 要求落在生成时看得见的面上/C#5 步骤顺序正确),
|
||||
# 字典后写覆盖先写 ⇒ §0 ⑨ 与 06 正文都**看不到 B#5**,而 06 总表把它编成 17
|
||||
# ⇒ 报成「首处不一致 #17」,真正缺的那一条反而不点名(2026-10-16 实测)。
|
||||
# 名字才是判据的本意(原注释:读者按简写去正文里搜就搜不到),编号只是导航。
|
||||
# 顺带把「应 16」这个硬编码去掉 ⇒ 以 **06 总表为准**,条数增减不必再改闸门。
|
||||
a = [nm for _n, nm in _re.findall(r"#(\d{1,2}) ([^(\s]+)", blk)]
|
||||
b = [nm for _n, nm in _re.findall(r"^(?:- )?\*\*(\d{1,2}) ([^(\s]+)\*\*", P6, _re.M)]
|
||||
c = [nm for _n, nm in _re.findall(
|
||||
r"^\| (\d{1,2}) \| [A-D] [^|]*\| ([^|\s]+) \|", P6, _re.M)]
|
||||
|
||||
# 🔴 句末标点不算差异(§0 是句子、正文是标题、「配置外置 /」后面跟分隔符)——
|
||||
# 2026-10-15 实测:不清洗时 #5 会因「入仓按白名单。」多一个句号而报红,那是**假红**。
|
||||
def _clean(d):
|
||||
return {k: _re.sub(r"[。.,,、;;//]+$", "", v) for k, v in d.items()}
|
||||
def _names(lst):
|
||||
out = []
|
||||
for v in lst:
|
||||
v = _re.sub(r"[。.,,、;;//]+$", "", v)
|
||||
if v and v not in out:
|
||||
out.append(v)
|
||||
return out
|
||||
|
||||
a, b, c = _clean(a), _clean(b), _clean(c)
|
||||
A, B, C = _names(a), _names(b), _names(c)
|
||||
|
||||
rows = [("§0 ⑨ 读到条目名 %d 条(应 16)" % len(a), len(a) == 16),
|
||||
("06 正文读到条目名 %d 条(应 16)" % len(b), len(b) == 16),
|
||||
("06 总表读到条目名 %d 条(应 16)" % len(c), len(c) == 16)]
|
||||
for tag, other in (("06 正文", b), ("06 总表", c)):
|
||||
bad = sorted(k for k in set(a) | set(other) if a.get(k) != other.get(k))
|
||||
rows = [("§0 ⑨ 读到条目名 %d 条(应 %d = 06 总表)" % (len(A), len(C)), len(A) == len(C)),
|
||||
("06 正文读到条目名 %d 条(应 %d = 06 总表)" % (len(B), len(C)), len(B) == len(C))]
|
||||
for tag, other in (("06 正文", B), ("06 总表", C)):
|
||||
missing = [x for x in other if x not in A]
|
||||
extra = [x for x in A if x not in other]
|
||||
detail = ""
|
||||
if bad:
|
||||
k0 = bad[0]
|
||||
detail = "(⛔ 首处不一致 #%d:§0=%r/%s=%r)" % (k0, a.get(k0), tag, other.get(k0))
|
||||
rows.append(("§0 ⑨ 与 %s **16 条逐条同名**%s" % (tag, detail), not bad))
|
||||
if missing:
|
||||
detail = "(⛔ §0 ⑨ 缺:%s)" % "、".join(missing[:3])
|
||||
elif extra:
|
||||
detail = "(⛔ §0 ⑨ 多出:%s)" % "、".join(extra[:3])
|
||||
rows.append(("§0 ⑨ 与 %s **逐条同名**%s" % (tag, detail), not missing and not extra))
|
||||
return rows
|
||||
|
||||
|
||||
|
||||
Reference in new issue
Block a user