@chengzhao-dev/dsh-quarto-book 0.3.5 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,94 +0,0 @@
1
- # 两个插件的使用边界
2
-
3
- 本文回答三个问题:什么时候 `dsh-quarto-book` 和 `dsh-cpp-workflow` 应该出现,两个插件同时出现时会不会冲突,以及怎么避免它们在无关项目里参与任务。文中 `dsh-quarto-book` 指插件模块的短名,npm 包名是 `@chengzhao-dev/dsh-quarto-book`。
4
-
5
- ## 一分钟判断
6
-
7
- 1. 从会话目录向上到 git 根能找到书根,书根里有 `.qmd` 或 `.md` 章节,且 `_quarto.yml` 有 `type: book`:`dsh-quarto-book` 出现。
8
- 2. 从会话目录向上到 git 根有 `cpp-workflow-project` 项目技能,或 git 根内(跳过生成、依赖与文档示例目录)有 `CMakeLists.txt` 与 C++ 源文件:`dsh-cpp-workflow` 出现。
9
- 3. 两条都成立就两组都出现;按当前文件类型选技能,不按仓库整体选。
10
- 4. 两条都不成立就都不出现;安装插件不等于启用技能。
11
-
12
- ## 判定矩阵
13
-
14
- | 会话场景 | `dsh-quarto-book` | `dsh-cpp-workflow` | 说明 |
15
- | --- | --- | --- | --- |
16
- | 纯 Markdown / 文档仓库,没有 `_quarto.yml` | 不出现 | 不出现 | 没有书配置,也没有 CMake 与 C++ 源文件 |
17
- | 只有 `_quarto.yml` 和 README 等仓库说明文件 | 不出现 | 不出现 | 说明文件不是章节源文件 |
18
- | Quarto Book(含 `type: book` 与章节源文件,没有 C++ 工程) | 出现两个技能和渲染工具 | 不出现 | 没有 CMake + C++ 源文件 |
19
- | CMake + C++ 源文件的工程 | 不出现 | 出现两个技能 | 不要求项目技能标记 |
20
- | 显式放了 `cpp-workflow-project` 的项目 | 不出现 | 出现两个技能 | 项目技能是显式选用,允许在 CMake 尚未就绪时先接入 |
21
- | 书仓库里同时有 C++ 子工程 | 出现 | 出现 | 多语言仓库,按当前文件类型分流 |
22
- | 仓库根不是书,只在子目录里有一本书 | 根目录不出现,进入子目录后出现 | 按 C++ 证据独立判断 | Quarto 以会话 cwd 的最近书根为准 |
23
- | 只有 CMake,没有 C++ 源文件 | 不出现 | 不出现 | 可能是 C 工程或空壳配置 |
24
- | 只有零散 `.cpp` / `.hpp`,没有 CMake | 不出现 | 不出现 | 不把单个示例文件当成工程 |
25
-
26
- ## `dsh-quarto-book` 的门禁
27
-
28
- 发现技能时,从会话 `cwd` 向上、最多到最近的 git 根,沿途查找 `_quarto.yml`:
29
-
30
- 1. `_quarto.yml` 必须有独立的一行 `type: book`,注释里的 `# type: book` 不算。
31
- 2. 书根内至少有一个 `.qmd` 或 `.md` 章节源文件;`_book`、`node_modules`、`vendor`、`third_party` 等目录不参与判断,根目录的 README、CHANGELOG、LICENSE 等仓库说明文件也不算章节。
32
- 3. 没有 `cwd`、不在 git checkout、配置缺失或只有配置没有章节,都返回空列表。
33
-
34
- `quarto_render_check` 与技能共用同一道门禁:非书会话直接返回 `safe_to_render: false`,不启动 `quarto render`。工具在 `apply()` 时注册,因此会出现在工具目录里,但它在执行前拒绝非书会话;这是 DSH 工具注册没有 cwd 参数时的最小防护。
35
-
36
- `get()` 带 `cwd` 时重新过门禁,避免同一会话从书目录切到普通目录后还能展开正文;不带 `cwd` 时只服务已经加载的 candidate。
37
-
38
- ## `dsh-cpp-workflow` 的门禁
39
-
40
- 从会话 `cwd` 向上找到最近的 git 根,满足下面任一条才返回两个技能:
41
-
42
- - `cwd` 到 git 根沿途的某一层存在 `.agents/skills/cpp-workflow-project/SKILL.md`,这是项目显式选用;
43
- - git 根内同时存在 `CMakeLists.txt` 与 C++ 源文件或头文件(`.cpp`、`.cc`、`.cxx`、`.hpp`、`.hh`、`.hxx`、`.ixx`、`.cppm` 等)。
44
-
45
- 扫描限制为 6 层、1500 个目录项,跳过 `.git`、`build`、`node_modules`、`vendor`、`third_party`、`external`、`_deps`、`_book`、`docs`、`examples`、`samples` 等生成、依赖或示例目录,命中两半后立即停止。只有 CMake、只有 C++ 示例、或证据在 git 根之上,都不算工作区。
46
-
47
- C++ 检测是工程级启发式,不解析 CMake 语法。使用仅含 `.h` 的头文件库、或使用非 CMake 构建系统的工程时,放一个 `cpp-workflow-project` 项目技能即可显式选用;不要为了触发插件去伪造 `CMakeLists.txt`。
48
-
49
- ## 两个插件同时出现时
50
-
51
- 两个 provider 的 rank 都是 550,但技能名完全不重叠:
52
-
53
- - `dsh-quarto-book`:`writing-quarto`、`designing-theme`
54
- - `dsh-cpp-workflow`:`developing-cpp`、`building-cpp`
55
-
56
- rank 只在同名时决定覆盖顺序,名字不同就不会互相遮蔽。书仓库里的 C++ 子工程会让两组技能同时出现,这是多语言仓库的正常状态:写 `.qmd` 时用 Quarto 技能,改 `.h` / `.cpp` / `CMakeLists.txt` 时用 C++ 技能。两个技能包都在 `SKILL.md` 里写明「不用于只改文档 / 不用于与 C++ 无关的文件」,路由按文件类型而不是按仓库整体判断。
57
-
58
- 真正需要避免的是把通用技能复制进项目:
59
-
60
- - 项目 `.agents/skills` 的 rank 是 200,会覆盖插件的同名技能。不要把 `writing-quarto`、`designing-theme`、`developing-cpp`、`building-cpp` 复制到项目里;项目只放 `quarto-book-project` 和 `cpp-workflow-project` 这类项目事实技能。
61
- - 两个插件都只注册技能 provider;`dsh-cpp-workflow` 没有工具,`dsh-quarto-book` 的渲染工具在执行前复核书门禁。它们不会在后台扫描、构建或改写工程。
62
-
63
- ## 默认启动的含义
64
-
65
- 安装插件不等于启用技能:
66
-
67
- - 安装只把包放进 profile;每次会话的 `list({ cwd })` 重新判定,不命中就返回空数组。
68
- - 技能被列出后仍需模型或用户显式调用;列出不是自动执行。
69
- - 门禁不依赖全局配置、环境变量或项目名,只依赖会话 cwd、git 根和项目内的文件事实。
70
-
71
- ## 实际校验
72
-
73
- 在 2026-10-20 的本机仓库上直接调用编译后的 provider:
74
-
75
- | cwd | `dsh-quarto-book` | `dsh-cpp-workflow` |
76
- | --- | --- | --- |
77
- | `D:\Github\cpp-board-games` | `designing-theme`、`writing-quarto` | `building-cpp`、`developing-cpp` |
78
- | `D:\Github\cpp-board-games\content\tictactoe` | `designing-theme`、`writing-quarto` | `building-cpp`、`developing-cpp` |
79
- | `D:\Github\dsh-quarto-book` | 空 | 空 |
80
- | `D:\Github\dsh-cpp-workflow` | 空 | 空 |
81
-
82
- `quarto_render_check(qmd_path: "index.qmd", dry_run: true)` 在 `cpp-board-games` 返回 `safe_to_render: true`;在插件仓库返回拒绝并说明缺 `type: book` 或章节源文件;没有 cwd 时同样拒绝。
83
-
84
- 自动测试覆盖:
85
-
86
- - `dsh-quarto-book`:26 项,覆盖书门禁、嵌套 cwd、生成目录、`get()` 复核、工具拒绝、卡片状态与渲染后 HTML 检查。
87
- - `dsh-cpp-workflow`:31 项,覆盖项目技能标记、CMake + C++ 证据、只有一半证据、嵌套子工程、生成目录、git 根之上、`get()` 复核与 checker 分支。
88
-
89
- ## 已知边界
90
-
91
- - 两个门禁都是文件事实启发式,不是构建系统解析器。工程布局越非常规,越应该用项目技能显式选用。
92
- - `dsh-quarto-book` 的渲染工具在工具目录里全局可见,只在执行时拒绝非书会话;DSH 的 `apply()` 没有 cwd,无法在注册阶段按目录隐藏工具。
93
- - 仓库根不是书、书在子目录时,只有进入书目录的会话才会看到 Quarto 技能;这是为了避免在仓库根误用书内相对路径。
94
- - 同一 profile 安装多个技能包时,其他插件若注册同名技能,仍由 rank 决定;本仓库的两个包不与对方重名,也不占用项目技能名。
@@ -1,60 +0,0 @@
1
- # 插件工作流:从安装到渲染检查
2
-
3
- 本文写给排障的人和回看的维护者,回答「装好之后,哪一层在什么条件下生效」。它是人读的流程说明,不是技能执行时的规则来源:技能正文不读本文,作用于写作的约束都写在 `skills/writing-quarto/SKILL.md` 与 `skills/designing-theme/SKILL.md` 里。
4
-
5
- ## 四层
6
-
7
- | 层 | 读者看到什么 | 生效条件 | 排障入口 |
8
- | --- | --- | --- | --- |
9
- | 1 安装 | 插件出现在 profile 的依赖里 | `dsh plugin --profile <profile> add npm:@chengzhao-dev/dsh-quarto-book@0.3.5` 成功,profile 的 `package.json` 有这条依赖 | `docs/versioning.md` |
10
- | 2 技能名出现 | 选题里能看到 `writing-quarto`、`designing-theme` | 会话 `cwd` 向上找到 git 根,沿途 `_quarto.yml` 有一行独立的 `type: book`,且这本书至少有一个章节源文件 | `docs/plugin-boundaries.md` |
11
- | 3 开始写章节 | 九个写作步骤和三套案例文件 | 第 2 层通过;章节文件是 `.qmd` 或 `.md` | `skills/writing-quarto/SKILL.md` |
12
- | 4 渲染检查 | `quarto_render_check` 返回 `safe_to_render` 与 `html_report` | 与第 2 层同一道门禁 | `skills/writing-quarto/steps/09-check.md` |
13
-
14
- ## 1 安装
15
-
16
- 安装单位是 profile,不是仓库。命令把包装进 `<DSH_HOME>/profiles/<profile>/`:
17
-
18
- ```text
19
- <DSH_HOME>/profiles/<profile>/
20
- ├── package.json # 依赖清单
21
- └── node_modules/ #/ 依赖目录
22
- └── @chengzhao-dev/dsh-quarto-book/ #/ 安装目录
23
- ├── package.json # 包名与版本
24
- ├── cordis.patch.yml # 宿主补丁
25
- └── skills/ #/ 两个技能
26
- ```
27
-
28
- 装好后 profile 的 `package.json` 里出现 `@chengzhao-dev/dsh-quarto-book`。两处名字必须逐字一致:`package.json` 的 `name` 与 `cordis.patch.yml` 里 `- name:` 指向同一个包名;不一致时补丁插不进去,工具和技能都不会注册。
29
-
30
- profile 里还留着旧的无作用域包 `dsh-quarto-book` 时(该包已随 0.3.4 从 npm 删除),先在同一个 profile 里删旧包再装新包。两个包同时留着时,会以相同的 rank 550 各注册一遍 `writing-quarto` 和 `designing-theme`。版本号与发布步骤见 `docs/versioning.md`。
31
-
32
- ## 2 技能名出现
33
-
34
- `apply()` 注册技能 provider 和 `quarto_render_check` 工具,不写文件。provider 的 `list({ cwd })` 只在发现候选时做一次有界只读检查:
35
-
36
- - 从会话 `cwd` 向上找到 git 根,沿途的 `_quarto.yml` 里有一行独立的 `type: book`,并且这本书至少有一个 `.qmd` 或 `.md` 章节源文件,就返回两个技能;否则返回空列表。书根下的 `README.md`、`CHANGELOG.md`、`LICENSE.md` 等仓库说明文件不算章节源文件。
37
- - 没有 `cwd`、不在 git checkout 里、`_quarto.yml` 缺失、只在注释里写了 `# type: book`、或只有 book 配置没有任何章节源文件,都按「不是书」处理。
38
- - 判定实现是 `src/workspace.ts` 的 `findBookRoot` 与 `isQuartoBookWorkspace`;扫描跳过 `_book`、`node_modules`、`vendor`、`third_party` 等目录,最多 4000 个目录项。
39
- - 技能名出现只说明当前会话过了门禁,不代表已经加载;只有调用 `skill` 才读写技能正文。
40
-
41
- 完整判定矩阵、与 `dsh-cpp-workflow` 的共存规则和实际校验记录见 `docs/plugin-boundaries.md`。
42
-
43
- ## 3 开始写章节
44
-
45
- 入口是 `writing-quarto` 的 `SKILL.md`:`适用场景` 说明哪些文件归它管,`两种走法` 写增量润色与全章重构的分岔和不变量,`九步` 列出每一步对应的 `steps/` 文件,`完成判据` 说明什么时候可以停。每一步只打开同号的一个文件;判不准才打开 `examples/` 里同号的案例;要排图、查渲染或全仓重构才打开 `quarto/`。
46
-
47
- - 章节(`content/**/*.qmd`、`index.qmd`):走完整九步。每一步的「情况:」标题下是只在特定场景才生效的追加判定,正文里的反引号引用要能在目标文件里找到同名标题。
48
- - 书仓库根目录的 `README.md`:仓库说明页,不是章节。只走第 5–8 步,不套第 1–4 步的任务与结构骨架,不写「常见错误」「自测问题」,不调 `quarto_render_check`;第 8 步发现问题只退回第 5、6、7 步。`README.en.md` 同样处理;`CHANGELOG.md`、`LICENSE`、`AGENTS.md` 不在写作范围。
49
- - 事实与确定性的不变量写在 `SKILL.md`「两种走法」,不复制进各步:不新增事实、数字、命令、性能结论、引文和出处;不把留有余地的说法改成确定说法;围栏里的代码、命令、路径、URL 与 YAML 不改。
50
-
51
- 改主题文件(CSS、SCSS、字体、palette、导航与 Callout 样式)转 `designing-theme`;它的启用条件写在自己的 `SKILL.md` 里,不由本文决定。
52
-
53
- ## 4 渲染检查
54
-
55
- 第 9 步先跑内容与链接脚本,再调 `quarto_render_check`:
56
-
57
- - `dry_run: true` 只判断输出目录,最多 2 次:第 1 次返回 `safe_to_render: false` 时只改一次 `output-dir`,第 2 次仍不安全就停住,把 `warnings` 交给用户。
58
- - `dry_run: false` 才运行一次 `quarto render`,最多 1 次;成功时同一次返回 `html_report`,不再为同一份文件重跑。`html_report.usable` 是最终结论。
59
- - 工具与技能共用同一道门禁:不在书里、`_quarto.yml` 不是 `type: book`、或书里没有章节源文件时直接拒绝,不启动渲染。渲染根取 `cwd` 向上最近的合法书根,`qmd_path` 只用来定位卡片。
60
- - 不手跑 `quarto render`,也不截图 PNG;命令超时是 120 秒。实现与返回字段见 `src/tools/render-check.ts`,判定流程见 `steps/09-check.md`。
@@ -1,55 +0,0 @@
1
- # 版本号与发布顺序
2
-
3
- 插件自己的版本只有一处:`package.json` 的 `"version"`。`locale/`、两份 `SKILL.md`、`cordis.patch.yml` 都不带版本。
4
-
5
- 旁边的 `"engines": { "dsh": ">=0.2.0-rc.1 <0.3.0-0" }` 与 peer `@deepseek-ai/dsh-skill` 是宿主兼容范围,表示能装在哪一档 DSH 上。改章节约定、案例或主题措辞时不动它。
6
-
7
- ## 装上之后怎么看版本
8
-
9
- - 命令行:`dsh plugin --profile <profile> list`(等价于在该 profile 里跑 `pnpm list`),读出已解析的包版本。桌面应用跑的是 `desktop` profile,它由应用独占管理,命令行只读不写。
10
- - 磁盘:profile 的 `package.json` 里 `dependencies`,以及 `node_modules/@chengzhao-dev/dsh-quarto-book/package.json` 的 `version`。
11
- - 运行时加载清单:`dsh --profile <profile> --dump-config`,确认 `# == @chengzhao-dev/dsh-quarto-book` 这一层在。官方「设置 → 插件」只显示模块名和启用状态,不显示版本。
12
-
13
- README 里的安装命令钉住一次发布:npm 写 `@0.3.5`,git 写 `#v0.3.5`。不带版本号的 `github:` 地址跟踪默认分支的某次提交,`package.json` 的版本号不会自动变成发布号。
14
-
15
- 包名与模块名不是一回事:npm 与 profile 依赖里是 `@chengzhao-dev/dsh-quarto-book`,`cordis.patch.yml` 的 `name` 必须与它逐字相同;插件模块导出的 `name` 与技能 provider 的常量仍是短名 `dsh-quarto-book`,技能名与工具名不变。旧的无作用域包 `dsh-quarto-book` 是另一个 npm 坐标,已随 0.3.4 从 npm 删除;之前装过它的 profile 仍要先 `remove` 再装新包,两份同时留着会以相同的 rank 550 各注册一遍 `writing-quarto` 和 `designing-theme`。
16
-
17
- ## 三档
18
-
19
- 到达 `1.0.0` 之前停在 `0.x`。抬哪一档只看对外契约,不看改了多少字。
20
-
21
- - **修订号(`0.1.1`、`0.2.1`…)**:约定措辞、正反例、参考文、README、注释、不改变既有书通过与否的检查词表或提示。以后绝大多数提交走这一档。
22
- - **次版本 `0.2.0`、`0.3.0`**:新增技能名或工具名、新增会让旧书失败的检查、脚本命令行不兼容、主题路径或 `_quarto.yml` 接线契约变化、完成判据变成强制步骤。
23
- - **`1.0.0`**:技能名、工具名、cwd 门禁、脚本入口和「项目只放 `quarto-book-project`」这些对外契约冻结,并承诺以后的修订号不再让旧书突然失败。
24
- - **`engines.dsh`**:只在要支持的 DSH 版本窗口变化时改,与上面三档无关。DSH 进入 `0.3` 时再把上界打开。
25
-
26
- 同一次改动里若又有措辞又有破坏性检查,取较高的那一档。
27
-
28
- ```mermaid
29
- flowchart TD
30
- change[一次改动] --> host{要改能安装的 DSH 版本窗口}
31
- host -->|是| engines[只改 engines.dsh]
32
- host -->|否| contract{技能名 工具名 脚本入口 或会让旧书失败的检查}
33
- contract -->|是| minor[次版本 0.3.0]
34
- contract -->|否| patch[0.x 修订号]
35
- ```
36
-
37
- ## 发布
38
-
39
- 发布时三处一起动:`package.json` 的 `version`、`package-lock.json` 顶部的版本、同号的 annotated git tag(`0.3.5` 对应 `v0.3.5`)。不在 `SKILL.md` 里再维护一套包版本。
40
-
41
- 发布走 npm trusted publisher。`0.3.4` 是这个作用域包的首个版本,由本机手工 `npm publish` 引导,同时登记了关联(仓库 `chengzhao-dev/dsh-quarto-book`、文件 `publish.yml`、动作 `npm publish`);旧包 `dsh-quarto-book` 也在这次发布后从 npm 删除。之后的版本不再本机发布,顺序固定:
42
-
43
- 1. 提交并推送默认分支,先不打标签。
44
- 2. 只读核对:`npm view @chengzhao-dev/dsh-quarto-book version` 应仍是上一个发布号;`npm whoami` 未登录就停;`npm trust list @chengzhao-dev/dsh-quarto-book --json` 要能匹配 GitHub Actions、仓库 `chengzhao-dev/dsh-quarto-book`、文件 `publish.yml`、动作 `npm publish`。
45
- 3. 作用域包的关联已在 `0.3.4` 首发时登记,`npm trust list` 查不到或对不上就停住,把 `npm trust github @chengzhao-dev/dsh-quarto-book --repository chengzhao-dev/dsh-quarto-book --file publish.yml --allow-publish` 交给用户执行。用户回复「publisher 已配置」后重查,不跳过。
46
- 4. 关联匹配后打 annotated `v<版本>` 并推送;`publish.yml` 在 `v*` 标签上运行。
47
- 5. 以 Actions 全绿且 `npm view @chengzhao-dev/dsh-quarto-book@<版本> version` 返回该版本为发布完成。`0.3.4` 是首发引导版本,由本机手工发布,当次 Actions 因关联尚未登记而失败,不作完成判据。npm 上的版本不可撤回,失败就停在那里,不改回版本号。
48
-
49
- 关联登记后两天内必须推标签;过期就重新登记,不沿用旧凭据。
50
-
51
- `.github/workflows/publish.yml` 只在 `v*` 标签上运行,权限是 `contents: read` 与 `id-token: write`,不设 GitHub Environment。步骤是 checkout、`actions/setup-node@v6`(Node 24、`registry-url: https://registry.npmjs.org`)、`npm ci`、`npm test`、`npm run build`、`npm publish --provenance`。`package.json` 的 `repository.url` 保持 `git+https://github.com/chengzhao-dev/dsh-quarto-book.git`,`publishConfig` 为 `access: public` 与 `provenance: true`。
52
-
53
- 发布后两种安装各钉各的:git 用 `#v<版本>`,npm 用 `@<版本>`。
54
-
55
- 写书时不读这份文档:它是本仓库的维护规则,不进任何一步。
@@ -1,36 +0,0 @@
1
- # writing-quarto 的目录分层
2
-
3
- 技能正文只有九步。每一步一个文件,案例和渲染约定各有自己的目录;本文件只记这套分层与改法,写作规则本身不在这里。
4
-
5
- ## 目录
6
-
7
- ```text
8
- skills/writing-quarto/
9
- ├── SKILL.md # 入口:九步与完成判据
10
- ├── examples/ #/ 案例:每步的正反例
11
- ├── quarto/ #/ 渲染:Book 结构、元素与排错
12
- ├── scripts/ #/ 检查:内容核对脚本
13
- └── steps/ #/ 步骤:九步的规则正文
14
- ```
15
-
16
- ## 四层各放什么
17
-
18
- **steps/** 放规则正文,一个文件一步,文件名按 `NN-名字.md` 编号。读者沿 `SKILL.md` 的九步走进来,读当前这一步的一个文件就够。每个 step 内部固定四段:产出、通用情况、情况、正反例。写法是判断标准和判定信号;正反例句不抄进来,只写指向 `examples/` 的引用。
19
-
20
- **examples/** 放正反例,文件名与 step 同号,一个主题一个文件。案例只在判不准时打开:每步的「正反例」节写清该开哪个文件、看哪一节。案例可以贴不佳原文和改写,规则条文不在这里复制。
21
-
22
- **quarto/** 放 Book 结构与渲染层约定:`basics.md` 管项目结构与 front matter,`authoring.md` 管元素写法,`mermaid.md` 与 `coords.md` 管图,`terminal.md` 管命令与输出,`render.md` 管 HTML 输出、链接与排错,`refactor.md` 管全仓重构。排图、查渲染错误或做全仓重构才打开。
23
-
24
- **scripts/** 放可机器判定的检查,正文只写调用方式与何时运行。规则能用脚本判定的不再抄进 step。
25
-
26
- `SKILL.md` 只留入口:适用场景、两种走法、九步各一句、完成判据。不贴目录树,不复制任何一步的条文。
27
-
28
- ## 新增或移动一条规则
29
-
30
- 先判断它属于哪一步,再落到那个 step 的「通用情况」;只有一种情形才成立时落到「情况」。同一个判定在别的 step 里已经有条文,就引用它,不复制。规则要例子才讲得清时,在 step 的「正反例」节加一行引用,例子写进 `examples/` 同号文件。
31
-
32
- 只在渲染层成立的内容放 `quarto/`,写作层不回引。
33
-
34
- ## 自检
35
-
36
- `npm run check` 跑 `scripts/checking-skill-links.mjs`,核对:九步、案例、渲染文件、两份技能的脚本都在;每一步的 H1 编号与 SKILL.md 的九步清单对齐;旧目录 `references/`、`conventions/`、`knowledge/` 不再出现,装机脚本也不引用它们;`kb_id` 不再出现;反引号里的路径能解析;`` `steps/…md`「节」 `` 这种引用里的节名真在目标文件的标题里;已收口的冲突句不再出现;目录树的标记规则只在 `steps/03-blocks.md`,列表项格式只在 `steps/07-layout.md`;README、docs 与案例「改写」围栏里的树,目录行带 `#/`、文件行 `#` 比它右移 4 列、同一棵树的两档注释不混用。目录树的写法只在 `steps/03-blocks.md`「情况:目录树」一处成文,别处提到时指过去。
@@ -1,249 +0,0 @@
1
- // 技能文件的自检:路径、旧布局残留、已收口的冲突句、标点与树注释的落点。
2
- // 用法:node scripts/checking-skill-links.mjs
3
- import { readFileSync, readdirSync, existsSync, statSync } from 'node:fs';
4
- import { join, dirname, resolve, relative, sep } from 'node:path';
5
- import { fileURLToPath } from 'node:url';
6
-
7
- const repo = resolve(dirname(fileURLToPath(import.meta.url)), '..');
8
- const skill = join(repo, 'skills', 'writing-quarto');
9
- const problems = [];
10
- const fail = (file, message) => problems.push(`${relative(repo, file)}: ${message}`);
11
-
12
- const expected = [
13
- 'SKILL.md',
14
- ...['01-task', '02-structure', '03-blocks', '04-paragraphs', '05-sentences', '06-wording', '07-layout', '08-reread', '09-check'].map((n) => `steps/${n}.md`),
15
- ...['01-language-scope', '02-structure-shape', '03-block-after', '03-excerpt-lead', '03-layout-list', '03-qmd-elements', '04-paragraph-cases', '04-paragraph-opening', '05-sentence-brevity', '05-sentence-frame', '05-sentence-reference', '06-wording-cases', '08-appendix'].map((n) => `examples/${n}.md`),
16
- ...['authoring', 'basics', 'coords', 'mermaid', 'refactor', 'render', 'terminal'].map((n) => `quarto/${n}.md`),
17
- ...['answering-disclosure.lua', 'checking-callouts.py', 'checking-dom.py', 'checking-links.py', 'checking-names.py', 'checking-output.py', 'testing-check-ascii-names.py', 'testing-content.py', 'verifying-content.py'].map((n) => `scripts/${n}`),
18
- ];
19
-
20
- const theme = join(repo, 'skills', 'designing-theme');
21
- const themeExpected = ['SKILL.md', 'scripts/checking-layout.py', 'scripts/measuring-pages.mjs', 'references/theme-system.md', 'knowledge/reading-surface.md'];
22
-
23
- const gone = ['references', 'conventions', 'knowledge', 'writer-spine.md', 'writing-spine.md', 'task-routes.md', 'INDEX.md'];
24
-
25
- for (const file of expected) {
26
- if (!existsSync(join(skill, file))) problems.push(`缺少 skills/writing-quarto/${file}`);
27
- }
28
- for (const file of themeExpected) {
29
- if (!existsSync(join(theme, file))) problems.push(`缺少 skills/designing-theme/${file}`);
30
- }
31
- for (const name of gone) {
32
- if (existsSync(join(skill, name))) problems.push(`旧路径仍在 skills/writing-quarto/${name}`);
33
- }
34
-
35
- function walk(dir, out = []) {
36
- for (const entry of readdirSync(dir)) {
37
- const path = join(dir, entry);
38
- if (statSync(path).isDirectory()) walk(path, out);
39
- else if (/\.(md|py|lua|ps1)$/.test(path)) out.push(path);
40
- }
41
- return out;
42
- }
43
-
44
- // 装机脚本也读技能目录:它引用旧布局时同样会断,必须一起扫。
45
- const watched = [...walk(skill), ...walk(join(repo, 'templates')), ...walk(join(repo, 'scripts')).filter((p) => p.endsWith('.ps1'))].filter((p) => !p.includes(`${join('scripts', 'diagrams')}`) || p.endsWith('.md'));
46
-
47
- const conflicts = ['不另立第二套', '标记方式统一见', '全角冒号判失败', '注释是名词', '不用全角冒号挂解释'];
48
- const oldTrees = [/\bconventions\//, /\breferences\/zh\//, /\bknowledge\/cases\//, /\bkb_id\b/, /\bwriting-quarto[/\\](?:conventions|knowledge|references)\b/];
49
- // 检出的工作树可能是 CRLF(Windows + autocrlf),先归一化再解析,
50
- // 否则行尾的 \r 会让注释匹配和标题匹配静默失配。
51
- const read = (file) => readFileSync(file, 'utf8').split('\r\n').join('\n');
52
-
53
-
54
- for (const file of watched) {
55
- const text = read(file);
56
- const rel = relative(skill, file).split(sep).join('/');
57
-
58
- for (const phrase of conflicts) {
59
- if (text.includes(phrase)) fail(file, `已收口的冲突句仍在:${phrase}`);
60
- }
61
- for (const pattern of oldTrees) {
62
- const hit = pattern.exec(text);
63
- if (!hit) continue;
64
- const line = text.slice(0, hit.index).split('\n').length;
65
- const lineText = text.split('\n')[line - 1] ?? '';
66
- if (lineText.includes('designing-theme/')) continue;
67
- fail(file, `旧布局路径仍在第 ${line} 行:${hit[0]}`);
68
- }
69
- // 条文只留一处:其他地方可以画树,但不许再写一套标记规则。
70
- if (rel !== 'steps/03-blocks.md' && /tree -a|目录注释以|文件注释以|标记方式统一见|目录用 `#\/`/.test(text)) {
71
- fail(file, '目录树的标记规则只允许写在 steps/03-blocks.md');
72
- }
73
- if (text.includes('引导词**:完整句') && rel !== 'steps/07-layout.md') {
74
- fail(file, '列表项格式只允许写在 steps/07-layout.md');
75
- }
76
- }
77
-
78
- // 反引号里指向技能或仓库的路径必须存在;书仓库自己的路径不在这里核对。
79
- const own = /^(steps|examples|quarto|scripts|templates|docs|lib|locale)\//;
80
- for (const file of watched) {
81
- if (!file.endsWith('.md')) continue;
82
- const text = read(file);
83
- for (const match of text.matchAll(/`([^`\n]+)`/g)) {
84
- const token = match[1].trim();
85
- if (!own.test(token) || /[\s*{}<>]/.test(token)) continue;
86
- if (!existsSync(resolve(skill, token)) && !existsSync(resolve(repo, token))) {
87
- fail(file, `路径不存在:${token}`);
88
- }
89
- }
90
- }
91
-
92
- // 「`steps/03-blocks.md`「情况:目录树」」这类引用,节名必须真在目标文件里。
93
- // 路径能解析而节名不存在,读者打开文件找不到那句话,等于断链。
94
- function headings(file) {
95
- const titles = new Set();
96
- let fence = false;
97
- for (const line of read(file).split('\n')) {
98
- if (/^\s*(```|~~~)/.test(line)) { fence = !fence; continue; }
99
- if (fence) continue;
100
- const match = /^#{1,4}\s+(.+?)\s*#*\s*$/.exec(line);
101
- if (match) titles.add(match[1].trim());
102
- }
103
- return titles;
104
- }
105
-
106
- const headingCache = new Map();
107
- for (const file of watched) {
108
- if (!file.endsWith('.md')) continue;
109
- const text = read(file);
110
- // 节名本身可能带引号(如「结构助词「的」」),所以内层要允许一对嵌套引号。
111
- for (const match of text.matchAll(/`((?:steps|examples|quarto)\/[^`\s]+\.md|SKILL\.md)`「([^「」]*(?:「[^「」]*」[^「」]*)*)」/g)) {
112
- const [, target, section] = match;
113
- const targetFile = resolve(skill, target);
114
- if (!existsSync(targetFile)) continue;
115
- if (!headingCache.has(targetFile)) headingCache.set(targetFile, headings(targetFile));
116
- if (headingCache.get(targetFile).has(section)) continue;
117
- const line = text.slice(0, match.index).split('\n').length;
118
- fail(file, `第 ${line} 行引用的节名不存在:${target}「${section}」`);
119
- }
120
- }
121
-
122
- // 每一步的文件名编号、H1 编号与 SKILL.md 的九步清单必须对齐:重排或复制步骤时最容易漏掉这里。
123
- const stepSlugs = ['01-task', '02-structure', '03-blocks', '04-paragraphs', '05-sentences', '06-wording', '07-layout', '08-reread', '09-check'];
124
- for (const [index, slug] of stepSlugs.entries()) {
125
- const file = join(skill, 'steps', `${slug}.md`);
126
- const heading = read(file).split(/\r?\n/).find((line) => line.startsWith('# ')) ?? '';
127
- const number = index + 1;
128
- if (!heading.startsWith(`# 第 ${number} 步`)) fail(file, `H1 应是「# 第 ${number} 步…」,实际是「${heading}」`);
129
- }
130
- const skillText = read(join(skill, 'SKILL.md'));
131
- for (const slug of stepSlugs) {
132
- if (!skillText.includes(`steps/${slug}.md`)) fail(join(skill, 'SKILL.md'), `九步清单没点到 steps/${slug}.md`);
133
- }
134
-
135
- // 目录树注释只有「名词」与「名词:一句」两档,同一棵树不混用。
136
- function treeBlocks(text) {
137
- const blocks = [];
138
- let current = null;
139
- let fence = false;
140
- for (const line of text.split('\n')) {
141
- if (/^\s*(```|~~~)/.test(line)) {
142
- if (current) { blocks.push(current); current = null; }
143
- fence = !fence;
144
- continue;
145
- }
146
- if (!fence) continue;
147
- if (/[├└]──/.test(line)) {
148
- current ??= [];
149
- const comment = /(?:#\/|\s#)\s?(.*)$/.exec(line);
150
- if (comment) current.push(comment[1].trim());
151
- } else if (current) {
152
- blocks.push(current);
153
- current = null;
154
- }
155
- }
156
- if (current) blocks.push(current);
157
- return blocks;
158
- }
159
-
160
- for (const file of [join(repo, 'README.md'), join(repo, 'README.en.md'), ...walk(join(repo, 'docs')).filter((p) => p.endsWith('.md'))]) {
161
- const text = read(file);
162
- for (const block of treeBlocks(text)) {
163
- const styles = new Set();
164
- for (const comment of block) {
165
- const colons = comment.split(':').length - 1;
166
- if (colons > 1) fail(file, `树注释有第二个冒号:${comment}`);
167
- if (/[。;]/.test(comment)) fail(file, `树注释不止一句:${comment}`);
168
- if (colons === 1 && comment.split(':')[1].trim() === '') fail(file, `树注释冒号后为空:${comment}`);
169
- styles.add(colons === 0 ? 'noun' : 'clause');
170
- }
171
- if (styles.size > 1) fail(file, `同一棵树混用了两档注释:${block.join(' / ')}`);
172
- }
173
- }
174
-
175
- // 目录 `#/`、文件 `#` 且右移 4 列:列错一位读者就看不出哪个是目录。
176
- // 只查整棵树(README、docs)与案例里标成「改写」的围栏;「不佳」保留同一作对照。
177
- function columnFences(file, text) {
178
- const fences = [];
179
- let current = null;
180
- let fence = false;
181
- let first = '';
182
- for (const line of text.split('\n')) {
183
- if (/^\s*(```|~~~)/.test(line)) {
184
- if (fence) { fences.push({ first, lines: current ?? [] }); current = null; }
185
- fence = !fence;
186
- first = '';
187
- continue;
188
- }
189
- if (!fence) continue;
190
- if (!first && line.trim()) first = line.trim();
191
- current ??= [];
192
- current.push(line);
193
- }
194
- return fences
195
- .filter(({ lines }) => lines.some((line) => /[├└]──/.test(line)))
196
- .filter(({ first }) => {
197
- if (file.startsWith(join(repo, 'skills'))) return first.startsWith('改写(');
198
- return true;
199
- });
200
- }
201
-
202
- for (const file of [
203
- join(repo, 'README.md'),
204
- join(repo, 'README.en.md'),
205
- ...walk(join(repo, 'docs')).filter((p) => p.endsWith('.md')),
206
- ...walk(join(skill, 'examples')),
207
- ]) {
208
- for (const { lines } of columnFences(file, read(file))) {
209
- const tree = [];
210
- for (const [index, line] of lines.entries()) {
211
- if (!/[├└]──/.test(line)) continue;
212
- const depth = (/^(?:│ | )*/.exec(line)?.[0].length ?? 0) / 4;
213
- const marker = /( +)(#\/?) /.exec(line);
214
- if (!marker) { fail(file, `树行没有行尾注释:${line.trim()}`); continue; }
215
- const column = marker.index + marker[1].length;
216
- // 目录不带结尾斜杠,只能看后面还有没有更深的节点。
217
- let hasChild = false;
218
- for (const next of lines.slice(index + 1)) {
219
- if (!/[├└]──/.test(next)) continue;
220
- const nextDepth = (/^(?:│ | )*/.exec(next)?.[0].length ?? 0) / 4;
221
- if (nextDepth <= depth) break;
222
- hasChild = true;
223
- break;
224
- }
225
- tree.push({ column, line, marker: marker[2], isDir: hasChild || marker[2] === '#/' });
226
- }
227
- const dirs = tree.filter((entry) => entry.isDir);
228
- const files = tree.filter((entry) => !entry.isDir);
229
- for (const entry of tree) {
230
- if (entry.isDir && entry.marker !== '#/') fail(file, `目录行注释缺 #/:${entry.line.trim()}`);
231
- if (!entry.isDir && entry.marker === '#/') fail(file, `文件行不应写 #/:${entry.line.trim()}`);
232
- }
233
- if (!dirs.length || !files.length) continue;
234
- const dirColumn = dirs[0].column;
235
- for (const entry of dirs.slice(1)) {
236
- if (entry.column !== dirColumn) fail(file, `目录行 #/ 没对齐:${entry.line.trim()}`);
237
- }
238
- for (const entry of files) {
239
- if (entry.column !== dirColumn + 4) fail(file, `文件行 # 未右移 4 列(目录在 ${dirColumn},文件在 ${entry.column}):${entry.line.trim()}`);
240
- }
241
- }
242
- }
243
-
244
- if (problems.length) {
245
- console.error(problems.join('\n'));
246
- console.error(`\n${problems.length} 处待修`);
247
- process.exit(1);
248
- }
249
- console.log(`checked ${watched.length} files; layout, paths and wording are clean`);
@@ -1,113 +0,0 @@
1
- #Requires -Version 7.0
2
- <#
3
- .SYNOPSIS
4
- Copy this bundle's skill trees into another repository's .agents layout.
5
-
6
- .DESCRIPTION
7
- The plugin package and an embedded .agents copy have different jobs. This
8
- script copies only the skill payload, never the bundle manifests or runtime
9
- directories, so the target repository stays a plain Quarto book.
10
-
11
- Layout written:
12
- <target>/.agents/skills/writing-quarto/
13
- <target>/.agents/skills/designing-theme/
14
-
15
- .PARAMETER Target
16
- Repository root that receives the .agents tree.
17
-
18
- .PARAMETER Source
19
- Bundle root. Defaults to the parent of this script.
20
-
21
- .PARAMETER Force
22
- Replace existing destination directories.
23
-
24
- .EXAMPLE
25
- pwsh -File scripts/embedding-agents.ps1 -Target D:\Github\my-book
26
- #>
27
- [CmdletBinding()]
28
- param(
29
- [Parameter(Mandatory = $true)]
30
- [string]$Target,
31
-
32
- [string]$Source = (Split-Path -Parent $PSScriptRoot),
33
-
34
- [switch]$Force
35
- )
36
-
37
- Set-StrictMode -Version Latest
38
- $ErrorActionPreference = 'Stop'
39
-
40
- $forbiddenNames = @('package.json', 'cordis.patch.yml', 'icon.svg')
41
- $forbiddenDirs = @('lib', 'locale')
42
- $excludedDirs = @('__pycache__', '.pytest_cache')
43
-
44
- function Resolve-FullPath {
45
- param([string]$Path, [string]$Label)
46
- if (-not (Test-Path -LiteralPath $Path -PathType Container)) {
47
- throw "$Label does not exist or is not a directory: $Path"
48
- }
49
- return (Resolve-Path -LiteralPath $Path).Path
50
- }
51
-
52
- function Assert-NoBundleFiles {
53
- param([string]$Path, [string]$Label)
54
- foreach ($name in $forbiddenNames) {
55
- if (Test-Path -LiteralPath (Join-Path $Path $name)) {
56
- throw "$Label contains bundle file '$name'; refusing to embed: $Path"
57
- }
58
- }
59
- foreach ($name in $forbiddenDirs) {
60
- if (Test-Path -LiteralPath (Join-Path $Path $name) -PathType Container) {
61
- throw "$Label contains bundle directory '$name/'; refusing to embed: $Path"
62
- }
63
- }
64
- }
65
-
66
- function Assert-SkillSource {
67
- param([string]$Path, [string]$Label)
68
- $skillFile = Join-Path $Path 'SKILL.md'
69
- if (-not (Test-Path -LiteralPath $skillFile -PathType Leaf)) {
70
- throw "$Label has no SKILL.md: $Path"
71
- }
72
- Assert-NoBundleFiles -Path $Path -Label $Label
73
- }
74
-
75
- function Copy-SkillTree {
76
- param([string]$From, [string]$To)
77
-
78
- Assert-SkillSource -Path $From -Label 'source skill'
79
- if (Test-Path -LiteralPath $To) {
80
- if (-not $Force) {
81
- throw "destination exists; pass -Force to replace: $To"
82
- }
83
- Remove-Item -LiteralPath $To -Recurse -Force
84
- }
85
- New-Item -ItemType Directory -Path $To -Force | Out-Null
86
- Copy-Item -Path (Join-Path $From '*') -Destination $To -Recurse -Force
87
-
88
- foreach ($name in $excludedDirs) {
89
- Get-ChildItem -LiteralPath $To -Directory -Recurse -Filter $name -ErrorAction SilentlyContinue |
90
- Remove-Item -Recurse -Force
91
- }
92
- Get-ChildItem -LiteralPath $To -File -Recurse -Include '*.pyc', '*.pyo' -ErrorAction SilentlyContinue |
93
- Remove-Item -Force
94
-
95
- Assert-SkillSource -Path $To -Label 'copied skill'
96
- }
97
-
98
- $sourceRoot = Resolve-FullPath -Path $Source -Label 'source'
99
- $targetRoot = Resolve-FullPath -Path $Target -Label 'target'
100
-
101
- $writing = Join-Path $sourceRoot 'skills/writing-quarto'
102
- $theme = Join-Path $sourceRoot 'skills/designing-theme'
103
-
104
- $writingDestination = Join-Path $targetRoot '.agents/skills/writing-quarto'
105
- $themeDestination = Join-Path $targetRoot '.agents/skills/designing-theme'
106
-
107
- Copy-SkillTree -From $writing -To $writingDestination
108
-
109
- Copy-SkillTree -From $theme -To $themeDestination
110
-
111
- Write-Host "Embedded skills into $targetRoot"
112
- Write-Host " writing-quarto -> $writingDestination"
113
- Write-Host " designing-theme -> $themeDestination"