feat(门禁): 新增 skill-size 钩子——SKILL.md 不得超插件写侧上限 65536
dsh-mnemon 的 MAX_SKILL_BYTES = 65536 只在写侧校验,加载侧从不检查, 所以正文越界的技能照常加载、却永远生成不出草案:同日两次卡死 (83194 字节与 72601 字节)都是这个形态,复位台账无效,只能瘦身。 故把上限机械化,越界与悬空指针在提交期就拦下。 - scripts/check_skill_size.py:纯标准库(裸 python 也能跑,故不解析 .venv);FAIL >65536、FAIL reference/ 指针悬空、FAIL UTF-8 BOM、 WARN ≥49152(75%——实测一次「采纳」把主文从 49821 顶到 62806); 总是全树扫,只拿 reference/x.md 触发的提交正是漏网场景; --root 可复指到用户级 ~/.dsh/skills;--selftest 五形态(干净 0 / 临界 0+WARN / 超限 1 / 悬空+BOM 1 / 根不存在 2)全过 - .pre-commit-config.yaml 加 skill-size,files: ^\.dsh/skills/ - 新增 docs/门禁说明/skill-size.md,含盲区:拦不住插件自身的采纳写盘、 用户级技能目录不在触发面、不解析 frontmatter、不查 U+FFFD - 计数同步 51 -> 52(实跑 48 -> 49,索引 28 -> 29 篇):AGENTS.md 与 门禁索引,另 7 处文档旧计数改写、CI 指南带日期实测行只加注 本笔先落地,技能正文的渐进展开拆分在下一笔。
This commit is contained in:
@@ -820,3 +820,29 @@ repos:
|
||||
entry: python scripts/check_nested_comment.py
|
||||
language: system
|
||||
types: [c]
|
||||
|
||||
# ─── SKILL.md 体积与 reference/ 指针门禁(第 52 条,2026-10-29 加)─────────────────────
|
||||
# 为什么要有:dsh-mnemon 插件对技能正文有 65536 字节的**写侧**硬上限(validateSkillContent),
|
||||
# 但**加载侧不查**——超限的 SKILL.md 照常可读、照常进技能目录,只有一件事会坏:自进化每次
|
||||
# 「生成草案」都在落盘那一步被拒,那一行永久卡在「生成中」、界面只剩转圈、连「忽略」都不出现
|
||||
# (Draft/Dismiss 只在 pending 渲染)。实测 2026-10-29 一天内两个技能各卡一次(第二个 72,601
|
||||
# 字节),每次白烧一个 30 分钟的草案 Agent。**在加这根之前,仓库里没有任何一处会告诉你某个
|
||||
# 技能已经废了**——文件能读、能改、能提交,全绿。
|
||||
# 判据:> 65536 FAIL(与插件同数、同口径:utf-8 字节数不是字符数);>= 49152(75%)WARN 不拦
|
||||
# (越线通常只隔一次草案采纳:实测 49821 → 62806);正文里 (reference/xxx.md) 指向的文件不在
|
||||
# → FAIL;带 UTF-8 BOM → FAIL(插件按 utf-8 解析,BOM 会让 frontmatter 读歪)。
|
||||
# 全树扫不只看变更文件:钩子拿到 filenames 只当触发用,判定始终扫整个 .dsh/skills/——因为改
|
||||
# reference/*.md 时 SKILL.md 没动,但它可能正是被那次外置撑过线/掏空的那一份。
|
||||
# 为什么 entry 敢用裸 python:AGENTS.md「钩子 entry 的裸 python 不等于 .venv」那条教训针对的是
|
||||
# 第三方依赖(fontTools);本脚本纯标准库,任意 python3 均可(实测 .venv 3.12 与 PATH 上 3.14
|
||||
# 都跑通),所以不需要脚本内再自解析解释器。
|
||||
# ⚠ 盲区:只管仓库内 .dsh/skills/;用户级 ~/.dsh/skills/ 不在仓库里,本钩子照不到(手工
|
||||
# `node "$HOME/.dsh/scripts/mnemon-skill-ledger-doctor.mjs" --root .` 或本脚本 --root 都只报,
|
||||
# 要拦得另配 shell 钩子)。只认 markdown 链接形态的指针,纯文本里写的路径不算。
|
||||
# 自证:--selftest 五形态(干净 0 / 75% 线 0 带 WARN / 超上限 1 / 悬空指针+BOM 1 / 根不存在 2)。
|
||||
# 详解与限制 → docs/门禁说明/skill-size.md。
|
||||
- id: skill-size
|
||||
name: "SKILL.md 不超插件写侧上限 65536 且 reference/ 指针不悬空"
|
||||
entry: python scripts/check_skill_size.py
|
||||
language: system
|
||||
files: ^\.dsh/skills/
|
||||
|
||||
+1
-1
@@ -1059,7 +1059,7 @@ date +%s # 对比 last_online
|
||||
| Runners 页 runner 显示 **`offline`** | daemon 进程不在:`gitea-runner` v4 的 `daemon` **首次 Declare 失败就直接退出**(Gitea 重启/机器重启后无人拉起) | §2.4 用 `ci_runner_bootstrap.ps1` 装看门狗 + 计划任务;`-Status` 复查 |
|
||||
| `static-gates` 报 `no Python with a SYSTEM-visible pre-commit` | runner 是 SYSTEM:PATH 上的 python 无 pre-commit、用户级 site 不可见、`.venv` 未入库(§4.1 三重坑) | `ci_runner_bootstrap.ps1 -SetEnv` 写机器级 `DTU_PRECOMMIT_PYTHON`,或把 pre-commit 装进机器级解释器;**改完必须重启 runner** |
|
||||
| `static-gates` 报 `runner missing cppcheck` | 机器 PATH 无 Cppcheck 且无机器级 `DTU_CPPCHECK` | `-SetEnv`(会自动发现 `C:\Program Files\Cppcheck`)或装 `Cppcheck.Cppcheck` |
|
||||
| **钩子条数对不上**(CI 的 `--all-files` 报告条数 = 定义数 − 1 − 3 manual:定义数 **51** → 报告 **47** 行,另由「手动阶段门禁」步跑 3 根;2026-10-12 **当时**实测 `master` 18 定义 / 17 报告,`v2-on-ui` 26 定义 / 25 报告——**增删钩子的分支必须重算这个差值**) | 两处都不进 `--all-files` 报告:`gitlint` 的 `stages: [commit-msg]` 写在**上游** `.pre-commit-hooks.yaml`(项目 `.pre-commit-config.yaml` 里 grep 不到,别据此断定"没限制");`cppcheck` / `test-cppcheck` / `test-coverage` 带 `stages: [manual]`,只由「手动阶段门禁」步逐根 `--hook-stage manual` 跑。都会表现为全绿——静默空转 | commit-msg 阶段已由 `scripts/ci_gitlint_range.ps1` 单独校验、manual 三根已由「手动阶段门禁」步兜住;**以后增删钩子必须核对"定义数 == 日志条数 + gitlint + manual 三根"** |
|
||||
| **钩子条数对不上**(CI 的 `--all-files` 报告条数 = 定义数 − 1 − 3 manual:定义数 **51** → 报告 **47** 行,另由「手动阶段门禁」步跑 3 根(2026-10-29 起声明数 52 → 报告 48 行,公式不变);2026-10-12 **当时**实测 `master` 18 定义 / 17 报告,`v2-on-ui` 26 定义 / 25 报告——**增删钩子的分支必须重算这个差值**) | 两处都不进 `--all-files` 报告:`gitlint` 的 `stages: [commit-msg]` 写在**上游** `.pre-commit-hooks.yaml`(项目 `.pre-commit-config.yaml` 里 grep 不到,别据此断定"没限制");`cppcheck` / `test-cppcheck` / `test-coverage` 带 `stages: [manual]`,只由「手动阶段门禁」步逐根 `--hook-stage manual` 跑。都会表现为全绿——静默空转 | commit-msg 阶段已由 `scripts/ci_gitlint_range.ps1` 单独校验、manual 三根已由「手动阶段门禁」步兜住;**以后增删钩子必须核对"定义数 == 日志条数 + gitlint + manual 三根"** |
|
||||
| gitlint 步骤 **Failed 但提交信息其实合规**(同一条命令在本地全绿),日志里是 `exit code: 254` + `Missing git configuration: please set user.name` | `actions/checkout` 把 HOME 临时指向每次任务的临时目录,而 SYSTEM 账号没有全局 git 身份 → gitlint **在读到任何规则之前**就因配置错误退出(**254 不是规则违规**)。旧脚本末尾那句 `::error::...rule: single-line Chinese title, < 100 chars` 是**写死的猜测文案**,把环境问题伪装成"提交信息不合格";且该步 `exit 1` 会让后面的 `--all-files` **整个不跑**——看着像"门禁拦住了",实际当时 26 个钩子一个都没执行 | `ci_gitlint_range.ps1` 先探测身份,缺失就用 `GIT_CONFIG_COUNT/KEY/VALUE` 注入一次性身份(**不写任何文件**,开发者机器不受影响),并把 254 与真实违规分开报(**2 = 门禁没跑起来**,1 = 信息确实违规)。实测同一命令:空 HOME 下改前 254、改后 `0 failed`(2026-10-12 task 31 首次暴露) |
|
||||
| push 后 Actions 页**空白/无任务** | ① 服务端开关未开 ② workflow 没 push 到远程 ③ 只改了 `docs/**`(被 `paths-ignore` 滤掉) ④ **新建分支的首次 push**(实测 2026-10-12:创建 `v2-on-ui-ci` 那次 push 没产生 run——runner 在线空闲、job 日志零新增;紧接着一次普通 push 立刻出 task。原因:`before` 是全零 SHA,变更文件集算不出来,`paths-ignore` 求值时被滤空) | ①②③ 逐条核对 §1 / §5.1 / §5.2;④ 用 `workflow_dispatch` 手动触发,或再推一个非文档路径的提交 |
|
||||
| 任务一直 **Waiting / 排队** | 没有 label 匹配 `windows` 的 runner | 检查 `--labels "windows:host"`;Settings→Runners 是否 Idle |
|
||||
|
||||
@@ -15,7 +15,7 @@ cppcheck 是开源 C/C++ 静态分析工具,**不运行代码**,通过语法
|
||||
|
||||
### 日常速度
|
||||
|
||||
- **提交期**:不跑(`stages: [manual]`),一次带代码的 `git commit` 只跑 48 根快钩子
|
||||
- **提交期**:不跑(`stages: [manual]`),一次带代码的 `git commit` 只跑 49 根快钩子
|
||||
- **全库扫描**(约 20 s;2026-10-28 实测 `cppcheck` 20.5 s、`test-cppcheck` 38.1 s):手工复跑 `& '.venv\Scripts\python.exe' -m pre_commit run --all-files --hook-stage manual <hook-id>`——**一次只能给一个 id**(`pre_commit run a b c` 实测报 `unrecognized arguments`),故 `cppcheck` / `test-cppcheck` 各跑一次
|
||||
|
||||
---
|
||||
|
||||
@@ -93,7 +93,7 @@ git commit
|
||||
|
||||
| 机制 | 口径 |
|
||||
|---|---|
|
||||
| **三个阶段** | pre-commit 阶段 **47** 个钩子 + commit-msg 阶段 `gitlint`(= 一次 `git commit` 实跑 **48**)+ `manual` 阶段 3 根(`cppcheck` / `test-cppcheck` / `test-coverage`:`git commit` 不跑,只在远端 CI 的「手动阶段门禁」步与 `scripts/ci_local.py` 的 manual 段跑;三根实测合计约 165 s ≈ 一次带代码提交耗时的三分之二)→ 口径 = YAML 解析后的 hook 条目数 **51**;自查命令与「声明 / 文档 / 实跑」三数同源核对 → `docs/门禁说明/README.md` §2;`default_install_hook_types: [pre-commit, commit-msg]` 是显式声明的——换机后 `pre-commit install` 才会带上 commit-msg 钩子 |
|
||||
| **三个阶段** | pre-commit 阶段 **48** 个钩子 + commit-msg 阶段 `gitlint`(= 一次 `git commit` 实跑 **49**)+ `manual` 阶段 3 根(`cppcheck` / `test-cppcheck` / `test-coverage`:`git commit` 不跑,只在远端 CI 的「手动阶段门禁」步与 `scripts/ci_local.py` 的 manual 段跑;三根实测合计约 165 s ≈ 一次带代码提交耗时的三分之二)→ 口径 = YAML 解析后的 hook 条目数 **51**;自查命令与「声明 / 文档 / 实跑」三数同源核对 → `docs/门禁说明/README.md` §2;`default_install_hook_types: [pre-commit, commit-msg]` 是显式声明的——换机后 `pre-commit install` 才会带上 commit-msg 钩子 |
|
||||
| **一根钩子在输出里出现两次** | 未声明 `stages:` 的钩子**默认属于所有阶段**,pre-commit 与 commit-msg 两遍都会列出它;第二遍的"文件"是提交信息文件,几乎必然 `(no files to check)Skipped`。**只看输出结尾会把"实跑通过"读成"没跑"**——判某钩子跑没跑要认它**第一次**出现的那行(实测 2026-10-24:只改 `.pre-commit-config.yaml` 的提交里 `test-coverage` 与 `bsp-err` 都是 Passed,尾巴那一遍是 Skipped) |
|
||||
| **stash 陷阱** | pre-commit 跑钩子前会 stash 未暂存改动,钩子看到的是"没有你中间状态"的树。**改代码 + 同时更新基线/受管区的,必须先 `git add` 再跑钩子**,否则会拿到假报警(本仓踩过,`docs/门禁说明/bsp-err.md` §4 记了现场)。**它藏不住什么,见下一行** |
|
||||
| **stash 覆盖面:只覆盖已跟踪文件的未暂存改动**(2026-10-26 读源码定案,**对所有钩子通用**) | 机制在 pre-commit 的 `staged_files_only._unstaged_changes_cleared()`:`git diff-index <tree>` 取补丁 → `git checkout -- .` → 跑完 `git apply` 还原。所以「钩子实跑 = 只看到 HEAD + 本笔 staged」这句**对已跟踪文件成立、对未跟踪文件不成立**:未跟踪文件压根不在 `diff-index` 里,**不会被藏**;`git add -N`(intent-to-add)也救不了——pre-commit 只 `git rm --cached` 把它撤下索引(`_intent_to_add_cleared()`),**文件本体仍在盘上**。后果:别人写了尚未入库的文件(新页面实例 / 新单测目录 / 新文档)躺在树里时,**任何一笔碰到该文件所属钩子 `files:` 的提交都会被这份半成品拖红**——红的内容没错,但账不该记在这笔提交上。实测:`ui_page_clock.c`、`ui_page_reset.c` 处于未跟踪且未绑 `#page` 期间,`generated-fresh` 对**所有人**的 UI 提交都会 FAIL(现场日志 `docs/门禁说明/generated-fresh.md` §6 第 8 条)。三条结论:① 提交排序——会让全树变红的半成品要么先落地(绑引用 + 重跑生成器)要么先挪出工作树;② 凡「判定基准取工作区」的钩子(重跑比对类、全库统计类)在多人同树下必须配**静默窗口**;③ 真要 `--no-verify` 抢跑,正文必须点名是哪几个**未跟踪**文件拖的红,别让它被读成「钩子不可信」 |
|
||||
@@ -332,7 +332,7 @@ $env:PATH = [Environment]::GetEnvironmentVariable("Path","Machine") + ";" + [Env
|
||||
|
||||
| 文件 | 位置 | 说明 |
|
||||
|---|---|---|
|
||||
| `.pre-commit-config.yaml` | 仓库根 | 钩子配置(**51 个钩子**声明数,一次 `git commit` 实跑 48 根;口径 = YAML 解析后的 hook 条目数,自查命令 → `docs/门禁说明/README.md` §2。声明数与一次提交的实跑数分列,勿混写;计数改动的完整台账只记在 AGENTS.md 提交门禁节与本目录 README §2,本行不重述)。逐项详解在 `docs/门禁说明/`(索引 `docs/门禁说明/README.md`;主机侧单测 **22 根**合一篇,含各套件来历与「19 套无专属钩子」台账 `docs/门禁说明/host-unit-tests.md` §6;UI 侧另有 `font-charset` 与 `ui-golden` 两篇) |
|
||||
| `.pre-commit-config.yaml` | 仓库根 | 钩子配置(**52 个钩子**声明数,一次 `git commit` 实跑 49 根;口径 = YAML 解析后的 hook 条目数,自查命令 → `docs/门禁说明/README.md` §2。声明数与一次提交的实跑数分列,勿混写;计数改动的完整台账只记在 AGENTS.md 提交门禁节与本目录 README §2,本行不重述)。逐项详解在 `docs/门禁说明/`(索引 `docs/门禁说明/README.md`;主机侧单测 **22 根**合一篇,含各套件来历与「19 套无专属钩子」台账 `docs/门禁说明/host-unit-tests.md` §6;UI 侧另有 `font-charset` 与 `ui-golden` 两篇) |
|
||||
| `.gitlint` | 仓库根 | gitlint 规则(宽松,兼容中文) |
|
||||
| `.cubemx_survival_baseline.json` | 仓库根 | CubeMX 易失定制基线(哈希;`--write` 重建) |
|
||||
| `scripts/check_mojibake.py` | scripts/ | U+FFFD 检测脚本 |
|
||||
|
||||
@@ -174,7 +174,7 @@
|
||||
**已落地**:脚本、基线台账、口径(本文);`test-coverage` 已并进 `scripts/ci_local.py` 的 manual 段(`--coverage` 旗标保留只为兼容旧命令)。
|
||||
|
||||
**接线状态:已挂**(2026-10-24,`test-coverage` 落在 `.pre-commit-config.yaml` 第 8 节,用户裁定 **FAIL** 语义)。
|
||||
它带 `stages: [manual]`——**`git commit` 阶段不跑**(一次带代码的提交只跑 48 根快钩子),由远端 CI 的
|
||||
它带 `stages: [manual]`——**`git commit` 阶段不跑**(一次带代码的提交只跑 49 根快钩子),由远端 CI 的
|
||||
「手动阶段门禁(覆盖率总闸 + 全量 cppcheck)」步与 `scripts/ci_local.py` 的 manual 段承担;手工复跑
|
||||
`& '.venv\Scripts\python.exe' -m pre_commit run --all-files --hook-stage manual test-coverage`。
|
||||
|
||||
|
||||
@@ -245,7 +245,7 @@ D:/Code/DTU Code/
|
||||
| 道 | 资源 | 并行性 | 锁 / 机制 |
|
||||
|---|---|---|---|
|
||||
| **台架道** | COM6 / ST-LINK / 设备(§7.1) | **串行**——约束资源,**永不空转**(约束优先,TOC):代码道在跑时必须为下一个台架任务备料 | `bench.lock` |
|
||||
| **代码道** | CPU / 源码 | **并行**——worktree 独立空间;前提 = 写范围互不相交 + 强门禁(`ci_local` 跑满声明的 51 根、含 manual 段,口径 → `docs/门禁说明/README.md` §2;同 Linux 内核子系统树并行而能合回的逻辑,见 `docs.kernel.org/process/2.Process.html` 的 subsystem tree 模型) | 无锁;分支 ≤2 天、绿即合(trunk-based 短分支) |
|
||||
| **代码道** | CPU / 源码 | **并行**——worktree 独立空间;前提 = 写范围互不相交 + 强门禁(`ci_local` 跑满声明的 52 根、含 manual 段,口径 → `docs/门禁说明/README.md` §2;同 Linux 内核子系统树并行而能合回的逻辑,见 `docs.kernel.org/process/2.Process.html` 的 subsystem tree 模型) | 无锁;分支 ≤2 天、绿即合(trunk-based 短分支) |
|
||||
| **外部窗口** | 标准源 / 第三方主站等外部设备 | **Fixed Date 类**(Kanban 服务分类):窗口一到全道让路;窗口前 2 日特性冻结(只修稳定,Zephyr RC 语义 → `docs.zephyrproject.org/latest/project/release_process.html`) | `bench.lock` + 外部预约 |
|
||||
|
||||
**四条规则**:
|
||||
|
||||
@@ -198,7 +198,7 @@ python tools/font_gen/extract_cn.py # 任意 python3:
|
||||
|
||||
| 项 | 要求 | 说明 |
|
||||
|---|---|---|
|
||||
| pre-commit | 已安装并 `pre-commit install` | 提交门禁(`pre-commit` 阶段 47 根 + commit-msg 阶段 `gitlint`,一次提交实跑 48 根) |
|
||||
| pre-commit | 已安装并 `pre-commit install` | 提交门禁(`pre-commit` 阶段 48 根 + commit-msg 阶段 `gitlint`,一次提交实跑 49 根) |
|
||||
| cppcheck | 在 PATH 中 | 钩子直接调 `cppcheck --enable=warning,performance,portability,style`(`language: system`)。⚠ `cppcheck` / `test-cppcheck` / `test-coverage` 三根带 `stages: [manual]`:`git commit` 不跑,由远端 CI 的「手动阶段门禁」步与 `scripts/ci_local.py` 的 manual 段跑(CI runner 缺 cppcheck 则该步失败) |
|
||||
| python | 在 PATH 中 | 本地钩子调 `scripts/check_mojibake.py`(U+FFFD 乱码)、`scripts/check_coding_standard.py`(规范 V1.0,纯标准库) |
|
||||
| **MSVC `cl.exe`** | **装了 VS 即有(§2.3)** | **`watchdog-unit-test` 钩子必需**:改 `App/util/task_watchdog.*` 或 `test/host/watchdog/**` 时触发,用 `cl.exe` 离线编译真实源码跑单测。**未装 VS 时该钩子报环境错误(退出码 2)→ 拦截提交** |
|
||||
|
||||
+11
-10
@@ -1,7 +1,7 @@
|
||||
# 门禁逐项说明(每种门禁一篇)
|
||||
|
||||
> 本目录逐项说明各门禁:**声明的 51 个钩子到底在干什么、拦谁、放谁、什么时候会骗人**——其中 3 根慢钩子带
|
||||
> `stages: [manual]`,一次 `git commit` 只实跑 48 根(口径见下条)。
|
||||
> 本目录逐项说明各门禁:**声明的 52 个钩子到底在干什么、拦谁、放谁、什么时候会骗人**——其中 3 根慢钩子带
|
||||
> `stages: [manual]`,一次 `git commit` 只实跑 49 根(口径见下条)。
|
||||
> 每个门禁一篇,八段固定结构:查什么 / 为什么要它 / 触发面 / 报错怎么读 / 好处 /
|
||||
> **限制与盲区** / 豁免与基线 / 改门禁本身要注意什么。**最后一节是这套文档存在的理由**——
|
||||
> 一份只看"拦了什么"的门禁文档,会让人以为绿就是对的。
|
||||
@@ -9,18 +9,18 @@
|
||||
- 配置事实源:`.pre-commit-config.yaml`。**钩子数的唯一口径 = YAML 解析后的 hook 条目数**,不是文件里的文本行数。
|
||||
自查(打印声明数;加/删一条钩子后,这个数要与 **AGENTS.md 的「共 N 个钩子」**、**本目录 §2 标题**同时对齐):
|
||||
`& '.venv\Scripts\python.exe' -c "import yaml; d=yaml.safe_load(open('.pre-commit-config.yaml', encoding='utf-8')); print(sum(len(r['hooks']) for r in d['repos']))"`
|
||||
→ **51**(2026-10-26:49 → 50 加 `generated-fresh`;2026-10-27:50 → 51 加 `nested-comment`;
|
||||
2026-10-28:51 → 52 加 `id-check-new`、随即 52 → 51 摘掉 `id-check`(退役全库语料审计))。
|
||||
→ **52**(2026-10-26:49 → 50 加 `generated-fresh`;2026-10-27:50 → 51 加 `nested-comment`;
|
||||
2026-10-28:51 → 52 加 `id-check-new`、随即 52 → 51 摘掉 `id-check`(退役全库语料审计);2026-10-29:51 → 52 加 `skill-size`(SKILL.md 写侧上限与 reference 指针,详见 [`skill-size.md`](skill-size.md)))。
|
||||
⚠ **别用 `grep -c` 去数配置里的声明行**:该文件里有一条**注释掉的**候选钩子(clang-format,第 65 行),
|
||||
文本 grep 会多算 1 = 52——「声明 52 / 文档 51 / 实跑 51」这种三个数不同源的争执就是这么来的。真要 grep,
|
||||
用**锚定行首缩进**的模式(它天然跳过以 # 开头的注释行)2026-10-28 实测直接得 **51**,与 YAML 口径一致;
|
||||
会多算 1 = 52 的是**不锚定行首、按子串**匹配声明行的写法(那条被注释掉的候选也被它算进去)。
|
||||
用**锚定行首缩进**的模式(它天然跳过以 # 开头的注释行)2026-10-28 实测直接得 **51**(当时口径),与 YAML 口径一致;
|
||||
会多算 1(比口径多 1)的是**不锚定行首、按子串**匹配声明行的写法(那条被注释掉的候选也被它算进去)。
|
||||
⚠ 反过来的坑也真踩过(2026-10-27):在配置**注释里复述那个 grep 字面量**,会让文本 grep 再虚高 1——
|
||||
数数只认 YAML 解析,注释与文档里都别写出那个字面量(`docs/门禁说明/nested-comment.md` §8 记了现场)。
|
||||
实跑侧口径:`scripts/ci_local.py` 的静态门禁汇总行 `[SUMMARY] …: pre-commit hooks -- passed=A failed=B skipped=C`(方括号里那一段是**层级名**,脚本输出与文档层的语义名正分头迁移 → 抓这行**按 `passed=` 字段匹配**,别把层级名前缀写死进字面量,否则会 grep 不到而误判「没有实跑数」),
|
||||
`A+B+C` = **commit 阶段**的钩子数(47 = 51 − 3 根 `stages: [manual]` − `gitlint`),再加 `gitlint` 1 条(它的
|
||||
`A+B+C` = **commit 阶段**的钩子数(48 = 52 − 3 根 `stages: [manual]` − `gitlint`),再加 `gitlint` 1 条(它的
|
||||
commit-msg 阶段声明写在**上游**仓库自带的钩子清单里,本配置文件里查不到,见 `scripts/ci_local.py` 文件头)
|
||||
→ 47 + 1 = **48 = 一次 `git commit` 实跑的钩子数**;再加 3 根 manual 阶段的慢钩子才是声明数 51,三数同源。
|
||||
→ 48 + 1 = **49 = 一次 `git commit` 实跑的钩子数**;再加 3 根 manual 阶段的慢钩子才是声明数 52,三数同源(2026-10-29 加 `skill-size` 之前这四个数是 51 / 47 / 48)。
|
||||
枚举自查(不执行任何钩子,`SKIP` 列全部 id):`SKIP=<全部 id> -m pre_commit run --all-files --hook-stage commit`
|
||||
输出 47 行、`--hook-stage manual` 输出 50 行(47 常规 + 3 慢的)。
|
||||
- **三根慢钩子在 manual 阶段**:`cppcheck`、`test-cppcheck`、`test-coverage` 带 `stages: [manual]` → `git commit`
|
||||
@@ -44,7 +44,7 @@
|
||||
⚠ 增量拦截最容易误会的一点:**绿灯不等于没问题**,只等于"没有比上次更差"。
|
||||
每个增量拦截门禁的基线数字都写进它自己的文档,读那篇时第一眼看的就是"现在在册多少条"。
|
||||
|
||||
## 2. 索引(51 个 hook → 28 篇,其中 1 篇是已退役门禁的留档 + 1 根只有本表指针(专篇待补)+ 1 根详解在 docs/测试说明/)
|
||||
## 2. 索引(52 个 hook → 29 篇,其中 1 篇是已退役门禁的留档 + 1 根只有本表指针(专篇待补)+ 1 根详解在 docs/测试说明/)
|
||||
|
||||
> 带 `stages: [manual]` 的三根(`cppcheck` / `test-cppcheck` / `test-coverage`)**提交期不跑**,只在远端 CI 的
|
||||
> 「手动阶段门禁」步与 `scripts/ci_local.py` 的 manual 段跑;下表对应两行的「文档」列已标注。
|
||||
@@ -64,6 +64,7 @@
|
||||
| [`task-table.md`](task-table.md) | `task-table` | README/AGENTS 里的任务表是否与 `Src/freertos.c` 一致 | `README.md`、`AGENTS.md`、`Src/freertos.c` | 已写 |
|
||||
| [`registration-check.md`](registration-check.md) | `registration-check` | 新 `.c` 是否双端注册(Keil 工程 + 模拟器 CMake);UI 页面描述符表是否接线 | `.c`、`.uvprojx`、`CMakeLists.txt` | 已写 |
|
||||
| [`nested-comment.md`](nested-comment.md) | `nested-comment` | 块注释体内再出现 /*(ARMCC 9-D 嵌套注释;诱因是注释里写路径通配) | 全仓 `.c/.h`(第三方与生成物除外) | 已写 |
|
||||
| [`skill-size.md`](`skill-size.md`) | `skill-size` | SKILL.md 不超插件写侧上限 65536 字节(**只在写侧校验、加载侧不查**:超限的技能照常能读、每次「生成草案」却被拒、永久卡在「生成中」)+ `reference/` 指针不悬空 + 不带 BOM | `.dsh/skills/**`(filenames 只当触发,判定始终全树扫) | 已写 |
|
||||
| [`generated-fresh.md`](generated-fresh.md) | `generated-fresh` | 生成器幂等性:把生成器重跑到**系统临时目录**,与工作区逐字节必须一致(不一致 = 生成物比生成器旧) | `tools/**`、UI 源码、`ui_menu_tree.txt`、`ui_cn_chars.txt`、`docs/images/*.svg`、两份注册表 | 已写 |
|
||||
| [`lv-conf-sync.md`](lv-conf-sync.md) | `lv-conf-sync` | 两份 `lv_conf.h`(MCU / 模拟器)的特性子集是否一致 | 两份 lv_conf 或脚本 | 已写 |
|
||||
| [`cubemx-survival.md`](cubemx-survival.md) | `cubemx-survival` | CubeMX 重生成会不会把定制静默抹掉(人工目检审计兜底) | `.ioc`、`Src/`、`Inc/` | 已写 |
|
||||
@@ -95,7 +96,7 @@
|
||||
|
||||
## 4. 现状与下一步
|
||||
|
||||
- **本目录 28 篇已全部成文**(2026-10-27 实测当时 27 篇;2026-10-28 复核:`docs/门禁说明/` 下除 README 外 28 篇,与上表「文档」列
|
||||
- **本目录 29 篇已全部成文**(2026-10-29 加 `skill-size` 一篇 → 29;2026-10-27 实测当时 27 篇;2026-10-28 当时复核:`docs/门禁说明/` 下除 README 外 28 篇,与上表「文档」列
|
||||
**一一对应**——「有页无行」与「有行无页」两个差集都是空集;一行覆盖多个 hook 的行(`cppcheck`+`test-cppcheck`、
|
||||
22 个 `*-unit-test`)按行内列举计。上一句写「23 篇」是存量误差:2026-10-25 连加 `bom-guard` /
|
||||
`setview-check` / `uinav-selftest` 三根、本轮加 `generated-fresh` 都没同步它(本轮前实有 25 篇)。
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
# SKILL.md 体积与指针(skill-size)— 别让"能读能改"骗过你
|
||||
|
||||
> 一句话:dsh-mnemon 插件对技能正文有 **65536 字节的写侧硬上限**,而它**只在写侧校验、加载侧不查**——
|
||||
> 超限的技能照常可读、照常进目录、照常提交,只有自进化会**永久卡在「生成中」**。本闸把这条隐性约束
|
||||
> 变成提交时的显式判定。脚本 \`scripts/check_skill_size.py\`(纯标准库、只读,2026-10-29 接线)。
|
||||
|
||||
## 1. 它在查什么
|
||||
|
||||
扫 \`.dsh/skills/<name>/SKILL.md\`(不是只扫变更文件,见 §3)。
|
||||
|
||||
| 判定 | 条件 | 依据 |
|
||||
|---|---|---|
|
||||
| **FAIL** | utf-8 **字节数** > 65536 | 与插件 \`MAX_SKILL_BYTES\` 同数、同口径(字节,不是字符;正文里中文多,字符数会少算近一半) |
|
||||
| **FAIL** | 正文里 \`(reference/xxx.md)\` 指向的文件不存在 | 渐进展开的前提是"指针能兑现";悬空指针等于把规程删了却还写着"见那篇",比内联更坏 |
|
||||
| **FAIL** | 文件带 UTF-8 BOM | 插件按 utf-8 解析,BOM 会让 frontmatter 首个键读歪(与 \`bom-guard\` 同源,但那根不管 \`.dsh/skills\`) |
|
||||
| **WARN**(不拦提交) | 字节数 >= 49152(上限的 75%) | 越线通常只隔一次草案采纳(实测一次采纳把正文从 49821 顶到 62806 字节) |
|
||||
| 不算 | 用户级 \`~/.dsh/skills/\` | 不在仓库里,本闸照不到(见 §6) |
|
||||
| 不算 | 正文纯文本里写的 \`reference/…\` 路径(非 markdown 链接) | 只认链接形态;写散文里的那种靠评审 |
|
||||
|
||||
## 2. 为什么要它
|
||||
|
||||
- **仓库里原本没有任何一处会说这句话**:「这个技能已经废了」。文件能读、能改、能提交,门禁全绿;
|
||||
坏的是——自进化每次点「生成草案」都在**落盘那一步**被写侧拒绝,而界面此时只剩一个转圈:
|
||||
\`drafting\` 那一行既不回到 \`pending\`(\`Draft\` / \`Dismiss\` 只在 pending 渲染),也没有超时回收
|
||||
(插件只对回测 run 有 \`RUN_STALE_MS\` = 45 分钟,对建议行没有对应机制)。
|
||||
- **代价是静默的**:每次尝试白烧一个 25–35 分钟的草案 Agent。实测 2026-10-28 至 29 两天内卡死三次,
|
||||
其中两次同因(超上限):\`dsh-run-code-tool-invocation\` 涨到 **83194 字节**(超 17658),
|
||||
\`task-worktree\` 涨到 **72601 字节**(超 7065,git HEAD 里还是 57917)——**都是手改/采纳堆上去的,
|
||||
没有一次是插件自己写的**,所以插件的写侧校验只在"下一次草案落盘"时才撞上,撞上就死循环。
|
||||
- 加这根之后:越线的文件**进不了仓库**;已经在仓库里的(本次全部 ≤42%)被 WARN 提前一格点出来。
|
||||
|
||||
## 2.1 接线当日实测(都可复现)
|
||||
|
||||
| 测项 | 结果 |
|
||||
|---|---|
|
||||
| 全树实扫 | \`.dsh/skills\` 共 **17 个 SKILL.md:0 FAIL、0 WARN**,最大 \`task-worktree\` 28139 字节(42%);退出码 0 |
|
||||
| \`--selftest\` 五形态 | **5/5**:干净树 0 / 75% 线 0(带 WARN)/ 65546 字节 1 / 悬空指针 + BOM 1 / 根不存在 2 |
|
||||
| 钩子接线 | YAML 解析条目 **51 → 52**;\`stages: [manual]\` 仍 **3** 根(一次 \`git commit\` 实跑 49);\`pre-commit run skill-size --all-files\` 与 \`--files .dsh/skills/task-worktree/SKILL.md\` 两种触发都 **Passed** |
|
||||
| 解释器面 | 钩子 entry 用裸 \`python\`:\`.venv\` 3.12 与 PATH 上的 3.14.7 都跑通——本脚本纯标准库,**不属于** AGENTS.md「裸 python 不等于 .venv」那条教训的适用面(那条针对 fontTools 这类第三方依赖) |
|
||||
| 负例落点 | 在 \`--selftest\` 的**临时树**里造,不在 \`.dsh/skills/\` 下造探针目录:那是活动技能目录,加载器与自进化都会看见它(见 §8) |
|
||||
|
||||
## 3. 触发面
|
||||
|
||||
- \`files: ^\\.dsh/skills/\` —— 覆盖 \`SKILL.md\` 与 \`reference/*.md\` 两类改动。**为什么必须覆盖到
|
||||
reference**:把某一节外置出去时,改的常常是"新增 reference 文件 + 主文变短",两根都要能触发;
|
||||
反过来只删主文、留下悬空指针,也只有 reference 目录参与触发才拦得住。
|
||||
- pre-commit 4.x **已移除 \`always: true\`**(对 local 钩子只告警并跳过),所以"想总是跑"只能靠
|
||||
\`files:\` 覆盖到触发面——这根的触发面就是整个 \`.dsh/skills/\`。
|
||||
- **判定始终全树扫**,忽略钩子传来的 filenames(只当触发用):因为 SKILL.md 越线时,变更文件可能只是
|
||||
一个 \`reference/xxx.md\`,只看 filenames 就会绿着放过去。代价是每次提交多扫 17 个小文件(<20 ms 级)。
|
||||
- 阶段:默认 commit 阶段(\`git commit\` 与 \`ci_local.py\` 都会跑)。
|
||||
|
||||
## 4. 报错怎么读
|
||||
|
||||
FAIL .dsh/skills/task-worktree/SKILL.md 72601 字节 > 写侧上限 65536(超 7065):这个技能的自进化
|
||||
每次「生成草案」都会被拒、永久卡在「生成中」。把深规程逐字搬进同目录 reference/<篇名>.md 并留
|
||||
指针(别重排既有步骤号);已卡死的台账行按 mnemon-skill-ledger-stuck-repair 技能处理。
|
||||
|
||||
三种正解,按情况挑:
|
||||
|
||||
1. **还没卡死** → 外置:把最深的整节**逐字**搬进 \`<skill>/reference/<篇名>.md\`,主文保留原小标题 +
|
||||
可执行判据的摘要 + 一行指针;**不要重排既有步骤号**(正文里有「见第 12 步」这类交叉引用)。
|
||||
2. **已经卡死** → 先外置把体积降回去,再按 \`mnemon-skill-ledger-stuck-repair\` 技能做取证与自愈;
|
||||
只复位台账行而文件仍超限 = 下一轮继续卡(插件会再烧一个草案 Agent)。
|
||||
3. **悬空指针** → 补文件,或者把指针连它承诺的内容一起搬回主文;**别只删指针行**(那是把规程删了)。
|
||||
|
||||
单独跑(两种都行):
|
||||
|
||||
& '.venv\Scripts\python.exe' scripts/check_skill_size.py --verbose
|
||||
& python scripts/check_skill_size.py --root .
|
||||
|
||||
## 5. 好处
|
||||
|
||||
- 把插件的一条**只存在于写侧**的约束,搬到了提交期:越线在写它的那一次就被点名,而不是在下一次
|
||||
有人点「生成草案」时以"界面转圈 30 分钟"的形式出现。
|
||||
- 让"渐进展开"有机器背书:\`reference/\` 指针不再只是文档约定,悬空即红。
|
||||
- 把一次静默失败(无日志、无报错、无按钮)换成一行可 grep 的提交期失败。
|
||||
|
||||
## 6. 限制与盲区
|
||||
|
||||
- **主体不同**:插件校验的是"要写进去的那份 content",本闸校验的是"仓库里的这份文件"。数值一致,
|
||||
但**草案采纳是插件写的,本闸拦不住那一次写入**——只能在下一个碰这个文件的提交里响。真正的写入侧
|
||||
修复要插件自己做(上游 issue 草稿见 \`~/.dsh/scripts/dsh-mnemon-drafting-wedge-issue.md\`)。
|
||||
- **用户级技能照不到**:\`~/.dsh/skills/\` 不在仓库里。手工兜底两条:本脚本 \`--root\`,或
|
||||
\`node "$HOME/.dsh/scripts/mnemon-skill-ledger-doctor.mjs" --root .\`(后者还会扫出全机器所有工作区的
|
||||
\`SKILL.md\` 并顺带查台账卡死行)。都**只报不改**。
|
||||
- 只判字节数与指针存在性:**不判 frontmatter 是否可解析**(\`check-yaml\` 的 \`files:\` 不含 \`.md\`,
|
||||
frontmatter 坏了目前只有加载器知道),也不判 U+FFFD——\`no-mojibake\` 的触发面同样不含 \`.dsh/skills\`。
|
||||
- 49152 是启发式,不是插件行为:内容密度决定头量消耗速度(实测一次草案采纳 +12985 字节)。
|
||||
- 名字里带 \`reference/\` 但用别的写法(尖括号、反引号裸路径、相对 \`../\`)的引用一律不算,静默放过。
|
||||
|
||||
## 7. 豁免与基线
|
||||
|
||||
- **无基线、无白名单**:新闸零语料,接线当日 17 个 SKILL.md 全绿,所以不需要"只减不增"的存量账。
|
||||
将来若真出现不得不豁免的技能,**别加白名单**——那等于把 §2 的静默失败请回来;正确做法是外置那一节。
|
||||
- WARN 不是基线,它不需要还清;但 WARN 条数只减不增是好的工程信号(说明技能还在长胖)。
|
||||
|
||||
## 8. 改这套门禁本身
|
||||
|
||||
- **65536 必须跟插件同源**:改数之前先在插件包里确认 \`MAX_SKILL_BYTES\` / \`validateSkillContent\` 的
|
||||
实际数值(\`~/.dsh/profiles/web/node_modules/dsh-mnemon-source-skills/lib/index.js\`)。两边不一致时,
|
||||
**以插件写侧为准**,并把本文件 §1 的表一起改。
|
||||
- 别把 FAIL 降成 WARN、也别把 WARN 抬到 FAIL:前者回到静默,后者会把"内容多但没坏"的技能全拦在提交外。
|
||||
- 改判据必须同步加 \`--selftest\` 形态——那根是唯一的牙;\`--selftest\` 不过就别提交。
|
||||
- **别在 \`.dsh/skills/\` 下造探针目录做负例**:那目录是活动技能面,加载器会把它当技能、自进化可能给它
|
||||
建建议行。负例一律在临时树里造(\`--selftest\` 就是这个做法)。
|
||||
- 加/删钩子要同批改三处计数:\`docs/门禁说明/README.md\` §1 与 §2 标题、\`AGENTS.md\` 的「共 N 个钩子」
|
||||
与文末文档表(口径见 \`docs/门禁说明/README.md\` §2 首条:唯一口径是 YAML 解析条目数)。
|
||||
+1
-1
@@ -57,7 +57,7 @@
|
||||
```bash
|
||||
# 全库跑门禁(用仓库 .venv 的 python,其 pre-commit 随 requirements.txt 安装;
|
||||
# 其他装有 pre-commit 的解释器可用 DTU_PRECOMMIT_PYTHON 指定)
|
||||
# 只覆盖 `pre-commit` 阶段的 47 根(`git commit` 实跑 = 47 + gitlint = 48 根)
|
||||
# 只覆盖 `pre-commit` 阶段的 48 根(`git commit` 实跑 = 48 + gitlint = 49 根)
|
||||
& '.venv\Scripts\python.exe' -m pre_commit run --all-files
|
||||
|
||||
# manual 阶段三根(带 stages: [manual],提交期不跑):一次只能给一个 id
|
||||
|
||||
@@ -0,0 +1,208 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""SKILL.md 体积与指针门禁(skill-size)。
|
||||
|
||||
为什么要有这根(实测 2026-10-29,两个技能同日各卡死一次):dsh-mnemon 插件对技能正文有
|
||||
**65536 字节的写侧硬上限**(``validateSkillContent()``),但它**只在写侧校验、加载侧不查**——
|
||||
超限的 SKILL.md 照常 ``skill()`` 可读、照常进技能目录,唯独自进化每次「生成草案」都在落盘那一步
|
||||
被拒:那一行永久卡在「生成中」(drafting)、界面只剩转圈、连「忽略」都不出现(Draft/Dismiss 只在
|
||||
pending 渲染)。换句话说:**文件能读、能改、能提交,仓库里没有任何一处会告诉你这个技能已经废了**,
|
||||
直到下一次卡死、白烧一个 30 分钟的草案 Agent。本门禁把这条隐性约束变成提交时的显式判定。
|
||||
|
||||
判据:
|
||||
1. `.dsh/skills/<name>/SKILL.md` 的 utf-8 字节数 > 65536 → FAIL(与插件写侧上限同数、同口径:
|
||||
字节数不是字符数)。
|
||||
2. 字节数 >= 49152(上限的 75%)→ WARN,不拦提交。草案是在这个区间里被"下一次采纳"顶过线的
|
||||
(实测 49821 → 62806 只隔一次采纳),提前一格留头量比卡死后再拆便宜。
|
||||
3. 正文里 `(reference/xxx.md)` 指向的文件不存在 → FAIL。渐进展开的前提是"指针能兑现";
|
||||
悬空指针等于把规程删了却还写着"见那篇",比内联更坏。
|
||||
|
||||
退出码:0 = 通过(可有 WARN);1 = 有 FAIL;2 = 用法/环境错误(根目录不存在、不在本仓库内)。
|
||||
纯标准库,任意 python3 均可(不吃 .venv 的第三方包,所以钩子 entry 用裸 python 也安全)。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
import tempfile
|
||||
|
||||
CAP_BYTES = 65536 # 与 dsh-mnemon MAX_SKILL_BYTES 同数
|
||||
WARN_BYTES = 49152 # CAP 的 75%
|
||||
SKILLS_SUBDIR = os.path.join(".dsh", "skills")
|
||||
LINK_RE = re.compile(r"\(reference/([A-Za-z0-9._\-]+\.md)\)")
|
||||
|
||||
|
||||
def _utf8_stdout() -> None:
|
||||
for stream in (sys.stdout, sys.stderr):
|
||||
try:
|
||||
stream.reconfigure(encoding="utf-8", errors="replace")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def find_skills_dir(start: str):
|
||||
"""从 start 往上找含 .dsh/skills 的目录(钩子从仓库根跑,手工跑时可能在子目录)。"""
|
||||
cur = os.path.abspath(start)
|
||||
for _ in range(12):
|
||||
cand = os.path.join(cur, SKILLS_SUBDIR)
|
||||
if os.path.isdir(cand):
|
||||
return cand
|
||||
parent = os.path.dirname(cur)
|
||||
if parent == cur:
|
||||
break
|
||||
cur = parent
|
||||
return None
|
||||
|
||||
|
||||
def rel(p: str) -> str:
|
||||
try:
|
||||
return os.path.relpath(p, os.getcwd()).replace(os.sep, "/")
|
||||
except Exception:
|
||||
return p.replace(os.sep, "/")
|
||||
|
||||
|
||||
def check_tree(skills_dir: str, verbose: bool = False):
|
||||
"""返回 (fails, warns, checked)。fails/warns 是可直接打印的行。"""
|
||||
fails = []
|
||||
warns = []
|
||||
checked = 0
|
||||
for name in sorted(os.listdir(skills_dir)):
|
||||
d = os.path.join(skills_dir, name)
|
||||
skill_md = os.path.join(d, "SKILL.md")
|
||||
if not os.path.isfile(skill_md):
|
||||
continue
|
||||
checked += 1
|
||||
with open(skill_md, "rb") as fh:
|
||||
raw = fh.read()
|
||||
size = len(raw)
|
||||
if size > CAP_BYTES:
|
||||
fails.append(
|
||||
"FAIL " + rel(skill_md) + " " + str(size) + " 字节 > 写侧上限 " + str(CAP_BYTES) +
|
||||
"(超 " + str(size - CAP_BYTES) + "):这个技能的自进化每次「生成草案」都会被拒、"
|
||||
"永久卡在「生成中」。把深规程逐字搬进同目录 reference/<篇名>.md 并留指针"
|
||||
"(别重排既有步骤号);已卡死的台账行按 mnemon-skill-ledger-stuck-repair 技能处理。"
|
||||
)
|
||||
elif size >= WARN_BYTES:
|
||||
warns.append(
|
||||
"WARN " + rel(skill_md) + " " + str(size) + " 字节 = 上限的 " +
|
||||
str(size * 100 // CAP_BYTES) + "%(阈值 " + str(WARN_BYTES) +
|
||||
"):再吃一次草案采纳就可能越线,建议先把某一节外置到 reference/。"
|
||||
)
|
||||
if raw.startswith(b"\xef\xbb\xbf"):
|
||||
fails.append("FAIL " + name + ": SKILL.md 带 UTF-8 BOM(插件按 utf-8 解析,frontmatter 会读歪)")
|
||||
text = raw.decode("utf-8", errors="replace")
|
||||
for target in sorted(set(LINK_RE.findall(text))):
|
||||
if not os.path.isfile(os.path.join(d, "reference", target)):
|
||||
fails.append("FAIL " + name + ": 指针 reference/" + target + " 悬空——正文引用了它,文件不在。"
|
||||
"要么补文件,要么把指针连同它承诺的内容一起搬回正文。")
|
||||
if verbose:
|
||||
print(" ok " + name + " " + str(size) + " 字节 (" + str(size * 100 // CAP_BYTES) + "%)")
|
||||
return fails, warns, checked
|
||||
|
||||
|
||||
def run(paths, roots_extra, verbose: bool = False) -> int:
|
||||
"""paths:钩子传进来的文件(只用于触发;判定始终全树扫,这样只改 reference/*.md 也照样量体积)。"""
|
||||
dirs = []
|
||||
for r in roots_extra:
|
||||
if not os.path.isdir(r):
|
||||
print("ERROR --root 不存在: " + r, file=sys.stderr)
|
||||
return 2
|
||||
skills = r if os.path.basename(r) == "skills" else os.path.join(r, SKILLS_SUBDIR)
|
||||
if not os.path.isdir(skills):
|
||||
print("WARN 跳过 " + r + ":没有 " + SKILLS_SUBDIR.replace(os.sep, "/"))
|
||||
continue
|
||||
dirs.append(skills)
|
||||
if not dirs:
|
||||
base = find_skills_dir(os.getcwd())
|
||||
if base is None:
|
||||
print("ERROR 找不到 .dsh/skills(不在本仓库内?用 --root 指过去)", file=sys.stderr)
|
||||
return 2
|
||||
dirs.append(base)
|
||||
all_fail = []
|
||||
all_warn = []
|
||||
total = 0
|
||||
for d in dirs:
|
||||
fails, warns, checked = check_tree(d, verbose=verbose)
|
||||
all_fail += fails
|
||||
all_warn += warns
|
||||
total += checked
|
||||
if verbose or fails or warns:
|
||||
print("== " + d.replace(os.sep, "/") + ":SKILL.md " + str(checked) + " 个")
|
||||
for line in all_warn:
|
||||
print(line)
|
||||
for line in all_fail:
|
||||
print(line)
|
||||
if all_fail:
|
||||
print("")
|
||||
print("skill-size:" + str(len(all_fail)) + " 项不合格 / 共扫 " + str(total) + " 个 SKILL.md(上限 " + str(CAP_BYTES) + " 字节)")
|
||||
return 1
|
||||
tail = "(" + str(len(all_warn)) + " 条 WARN)" if all_warn else ",无悬空指针"
|
||||
print("skill-size:" + str(total) + " 个 SKILL.md 全在写侧上限内" + tail)
|
||||
return 0
|
||||
|
||||
|
||||
def _make(tmp: str, skill: str, size: int, link=None, bom: bool = False) -> None:
|
||||
d = os.path.join(tmp, SKILLS_SUBDIR, skill)
|
||||
os.makedirs(os.path.join(d, "reference"), exist_ok=True)
|
||||
body = "---\nname: t\ndescription: d\n---\n\n# t\n\n"
|
||||
# 先把正文垫到恰好 size 字节(链接行加在 padding 之后,所以带链接的用例只多不少)
|
||||
text = body + ("x" * max(0, size - len(body.encode("utf-8"))))
|
||||
if link:
|
||||
text += "\nsee [x](reference/" + link + ").\n"
|
||||
data = text.encode("utf-8")
|
||||
if bom:
|
||||
data = b"\xef\xbb\xbf" + data
|
||||
with open(os.path.join(d, "SKILL.md"), "wb") as fh:
|
||||
fh.write(data)
|
||||
|
||||
|
||||
def selftest() -> int:
|
||||
"""在临时目录造四种形态,验判定真有牙(不是"永远绿")。"""
|
||||
ok = True
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
_make(tmp, "clean", 2000, link="present.md")
|
||||
with open(os.path.join(tmp, SKILLS_SUBDIR, "clean", "reference", "present.md"), "w") as fh:
|
||||
fh.write("# p\n")
|
||||
rc = run([], [tmp])
|
||||
ok = ok and rc == 0
|
||||
print("selftest 干净树 -> " + str(rc) + "(期望 0)")
|
||||
_make(tmp, "warnish", WARN_BYTES + 10)
|
||||
rc = run([], [tmp])
|
||||
ok = ok and rc == 0
|
||||
print("selftest 75% 线 -> " + str(rc) + "(期望 0 带 WARN)")
|
||||
_make(tmp, "fat", CAP_BYTES + 10)
|
||||
rc = run([], [tmp])
|
||||
ok = ok and rc == 1
|
||||
print("selftest 超上限 -> " + str(rc) + "(期望 1)")
|
||||
os.remove(os.path.join(tmp, SKILLS_SUBDIR, "fat", "SKILL.md"))
|
||||
os.remove(os.path.join(tmp, SKILLS_SUBDIR, "warnish", "SKILL.md"))
|
||||
_make(tmp, "dangling", 3000, link="gone.md")
|
||||
_make(tmp, "bommed", 3000, bom=True)
|
||||
rc = run([], [tmp])
|
||||
ok = ok and rc == 1
|
||||
print("selftest 悬空指针/BOM -> " + str(rc) + "(期望 1)")
|
||||
rc = run([], [os.path.join(tmp, "nope")])
|
||||
ok = ok and rc == 2
|
||||
print("selftest 根不存在 -> " + str(rc) + "(期望 2)")
|
||||
print("SELFTEST " + ("PASS" if ok else "FAIL"))
|
||||
return 0 if ok else 1
|
||||
|
||||
|
||||
def main(argv=None) -> int:
|
||||
_utf8_stdout()
|
||||
ap = argparse.ArgumentParser(description="SKILL.md 体积与 reference/ 指针门禁")
|
||||
ap.add_argument("paths", nargs="*", help="钩子传入的变更文件(仅用于触发;判定始终全树扫)")
|
||||
ap.add_argument("--root", action="append", default=[], help="额外要扫的根(可重复),其下需有 .dsh/skills")
|
||||
ap.add_argument("--selftest", action="store_true", help="在临时目录造形态自证判定有牙")
|
||||
ap.add_argument("--verbose", action="store_true", help="逐个打印体积")
|
||||
args = ap.parse_args(argv)
|
||||
if args.selftest:
|
||||
return selftest()
|
||||
return run(args.paths, args.root, verbose=args.verbose)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Reference in New Issue
Block a user