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

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.
package/README.en.md CHANGED
@@ -4,7 +4,11 @@
4
4
 
5
5
  **English** | [简体中文](https://github.com/chengzhao-dev/dsh-quarto-book/blob/main/README.md)
6
6
 
7
- Two skills and one tool for [DeepSeek Harness](https://github.com/deepseek-ai) (DSH): authoring Quarto Book chapters, maintaining a reusable book theme, and checking rendered HTML before publishing. The skills are read-only — they never execute book code and never edit `.qmd` files. `quarto_render_check` runs `quarto render` once when called, so it executes the book's configured engine and writes `_book/`.
7
+ Author and check Quarto Book chapters and maintain a reusable theme; no business code, only inside `type: book` repositories.
8
+
9
+ - Write `.qmd`: `writing-quarto` covers the chapter skeleton, structure and prose.
10
+ - Change the theme: `designing-theme` covers CSS, fonts and palettes.
11
+ - Check rendered HTML: `quarto_render_check` judges publishability once, and `quarto_pass_report` prints the closing receipt.
8
12
 
9
13
  ## What it provides
10
14
 
@@ -13,6 +17,7 @@ Two skills and one tool for [DeepSeek Harness](https://github.com/deepseek-ai) (
13
17
  | `writing-quarto` | Creating or editing `content/**/*.qmd`, `index.qmd`, `_quarto.yml`; chapter skeletons, Chinese prose, code and figures, content checks | CSS, fonts and colours |
14
18
  | `designing-theme` | Theme CSS, SCSS, fonts, palettes, navigation and callout styling | Chapter prose |
15
19
  | `quarto_render_check` | Check that `output-dir` is safe, render once when asked, and inspect HTML in `_book/` for publishing | Editing `.qmd`, fixing config automatically, or re-running a failed render |
20
+ | `quarto_pass_report` | Compress the run into a fixed-line receipt card: skill, mode, files and steps changed, items held, check verdicts | Saving to disk, quoting chapter prose, or changing the text based on the receipt |
16
21
 
17
22
  For when the skills appear and which rule each step maps to, see [`docs/plugin-flow.md`](docs/plugin-flow.md). It is a human-facing walkthrough, not the rule source used while a skill runs.
18
23
 
@@ -21,10 +26,10 @@ Both skills are discovered from the package's `skills/` directory and loaded on
21
26
  ## Install
22
27
 
23
28
  ```bash
24
- dsh plugin --profile web add npm:@chengzhao-dev/dsh-quarto-book@0.3.5
29
+ dsh plugin --profile web add npm:@chengzhao-dev/dsh-quarto-book@0.4.0
25
30
  ```
26
31
 
27
- Replace `--profile` with the profile you actually run. The desktop app always runs the `desktop` profile, and that profile is owned by the app, so `dsh --profile desktop` is rejected; install or upgrade a plugin with `dsh plugin --profile desktop add <package>`, and use the app's Settings → Plugins → Add plugin with `npm:@chengzhao-dev/dsh-quarto-book@0.3.5` only if the app refuses that write. To follow a GitHub tag instead, use `github:chengzhao-dev/dsh-quarto-book#v0.3.5`.
32
+ Replace `--profile` with the profile you actually run. The desktop app always runs the `desktop` profile, and that profile is owned by the app, so `dsh --profile desktop` is rejected; install or upgrade a plugin with `dsh plugin --profile desktop add <package>`, and use the app's Settings → Plugins → Add plugin with `npm:@chengzhao-dev/dsh-quarto-book@0.4.0` only if the app refuses that write. To follow a GitHub tag instead, use `github:chengzhao-dev/dsh-quarto-book#v0.4.0`.
28
33
 
29
34
  Upgrading from the old unscoped package `dsh-quarto-book` (removed from npm with 0.3.4, so this only affects profiles that installed it earlier): remove it from the same profile first with `dsh plugin --profile <profile> remove dsh-quarto-book`, then add the scoped package. Keeping both installed registers `writing-quarto` and `designing-theme` twice at the same rank 550.
30
35
 
@@ -119,6 +124,32 @@ Call `quarto_render_check(qmd_path: "content/chapter.qmd", dry_run: true)`. It r
119
124
 
120
125
  The raw render output never becomes a separate terminal card. The render timeout is 120 seconds; timeouts, a missing `quarto` executable and non-zero exits all go into `errors`.
121
126
 
127
+ ## Run receipt
128
+
129
+ At the end of a run the skill calls `quarto_pass_report` once, compressing the run into a few fixed lines. The card is the receipt; the reply does not repeat it, and the tool saves nothing to disk, quotes no chapter prose, and changes no text based on the receipt.
130
+
131
+ Every parameter is a closed set:
132
+
133
+ | Field | Values |
134
+ | --- | --- |
135
+ | `skill` | `writing-quarto` or `designing-theme` |
136
+ | `executed` | Boolean; when `false`, `mode` must be `skipped` and `skip_reason` (at most 40 characters) is required |
137
+ | `mode` | `polish`, `rewrite` (writing), `theme` (theme), `skipped` (not executed) |
138
+ | `files` | Up to 8 `{ path, edits }` entries |
139
+ | `steps` | Only steps with edits: `1`–`9` for writing, or `定表面`, `定字体`, `定尺度`, `定颜色`, `定组件`, `通过检查` for the theme; their `edits` must sum to the `files` total |
140
+ | `held` | `fences`, `facts`, `css`, `prose` |
141
+ | `checks` | `content`, `html`, `layout`, with status `pass`, `fail` or `skip` |
142
+
143
+ ```text
144
+ 已执行 writing-quarto · 增量润色
145
+ content/ch.qmd
146
+ 改了 5 处:定句子 3,定用词 1,定版式 1
147
+ 未改:围栏、事实
148
+ 检查:内容通过;HTML 可用
149
+ ```
150
+
151
+ A zero-change run still prints a card (`改了 0 处`), so "the plugin never ran" stays distinguishable from "it ran and changed nothing"; when the current file is not a chapter the output is one line: `未执行 writing-quarto:…`.
152
+
122
153
  ## Theme
123
154
 
124
155
  Theme assets live in `skills/designing-theme/assets/theme/`:
package/README.md CHANGED
@@ -4,7 +4,11 @@
4
4
 
5
5
  [English](https://github.com/chengzhao-dev/dsh-quarto-book/blob/main/README.en.md) | **简体中文**
6
6
 
7
- 为 [DeepSeek Harness](https://github.com/deepseek-ai)(DSH)提供两个技能和一个工具:写 Quarto Book 章节、维护可复用的书主题、在渲染后检查 HTML 是否可发布。技能只读,不执行书里的代码,也不改写 `.qmd`;`quarto_render_check` 只在被调用时运行一次 `quarto render`,会执行书配置的引擎并写入 `_book/`。
7
+ 为 Quarto Book 写章节、校验结构并维护可复用主题;不改业务代码,只在 `type: book` 的仓库里出现。
8
+
9
+ - 写 `.qmd`:`writing-quarto` 管章节骨架、结构与行文。
10
+ - 改主题:`designing-theme` 管 CSS、字体与 palette。
11
+ - 渲染后检查 HTML:`quarto_render_check` 判一次可发布性,`quarto_pass_report` 出收尾回执。
8
12
 
9
13
  ## 提供什么
10
14
 
@@ -13,6 +17,7 @@
13
17
  | `writing-quarto` | 新建或修改 `content/**/*.qmd`、`index.qmd`、`_quarto.yml`;章节骨架、中文行文、代码与图表、内容检查 | 不改 CSS、字体和颜色 |
14
18
  | `designing-theme` | 调整主题的 CSS、SCSS、字体、palette、导航与 Callout 样式 | 不改章节正文 |
15
19
  | `quarto_render_check` | 先判断 `output-dir` 是否安全,再按需渲染一次,检查 `_book/` 里的 HTML 是否可发布 | 不改 `.qmd`,不自动修配置,不重复渲染 |
20
+ | `quarto_pass_report` | 收尾时把执行结论压成一张固定行数的回执卡片:技能、模式、改过的文件与步骤、未改项、检查结论 | 不保存到磁盘,不贴章节原文,不据回执改文章 |
16
21
 
17
22
  装上之后什么时候出现、每一步对应哪条规则,见 [`docs/plugin-flow.md`](docs/plugin-flow.md)。那是给人回看的流程说明,不是技能执行时的规则来源。
18
23
 
@@ -21,10 +26,10 @@
21
26
  ## 安装
22
27
 
23
28
  ```bash
24
- dsh plugin --profile web add npm:@chengzhao-dev/dsh-quarto-book@0.3.5
29
+ dsh plugin --profile web add npm:@chengzhao-dev/dsh-quarto-book@0.4.0
25
30
  ```
26
31
 
27
- `--profile` 换成你实际运行的那个 profile。桌面应用固定跑 `desktop` profile,且该 profile 由应用独占管理,`dsh --profile desktop` 直接启动会被拒绝;安装或升级插件可用 `dsh plugin --profile desktop add <包>`,如果应用拒绝写入,再走「设置 → 插件 → 添加插件」,地址填 `npm:@chengzhao-dev/dsh-quarto-book@0.3.5`。想跟 GitHub 标签,也可以写 `github:chengzhao-dev/dsh-quarto-book#v0.3.5`。
32
+ `--profile` 换成你实际运行的那个 profile。桌面应用固定跑 `desktop` profile,且该 profile 由应用独占管理,`dsh --profile desktop` 直接启动会被拒绝;安装或升级插件可用 `dsh plugin --profile desktop add <包>`,如果应用拒绝写入,再走「设置 → 插件 → 添加插件」,地址填 `npm:@chengzhao-dev/dsh-quarto-book@0.4.0`。想跟 GitHub 标签,也可以写 `github:chengzhao-dev/dsh-quarto-book#v0.4.0`。
28
33
 
29
34
  从旧的无作用域包 `dsh-quarto-book` 升级时(该包已随 0.3.4 从 npm 删除,只影响之前装过它的 profile),先在同一个 profile 里 `dsh plugin --profile <profile> remove dsh-quarto-book` 再装新包。两个包同时留着会以相同的 rank 550 各注册一遍 `writing-quarto` 和 `designing-theme`。
30
35
 
@@ -119,6 +124,32 @@ project:
119
124
 
120
125
  渲染进程的原始输出不单独占一张终端卡片。命令超时是 120 秒;超时、找不到 `quarto` 或非零退出都写进 `errors`。
121
126
 
127
+ ## 执行回执
128
+
129
+ 技能收尾时调用一次 `quarto_pass_report`,把这次执行的结论压成固定几行。卡片就是回执,回复里不再复述;工具不保存到磁盘,不贴章节原文,也不据回执改文章。
130
+
131
+ 入参是闭集:
132
+
133
+ | 字段 | 取值 |
134
+ | --- | --- |
135
+ | `skill` | `writing-quarto` 或 `designing-theme` |
136
+ | `executed` | 布尔;为 `false` 时 `mode` 必须是 `skipped`,并给 `skip_reason`(最多 40 字) |
137
+ | `mode` | `polish`、`rewrite`(写作),`theme`(主题),`skipped`(未执行) |
138
+ | `files` | 最多 8 个 `{ path, edits }` |
139
+ | `steps` | 只列有改动的步骤:写作填 `1`–`9`,主题填 `定表面`、`定字体`、`定尺度`、`定颜色`、`定组件`、`通过检查`;`edits` 合计必须等于 `files` 的合计 |
140
+ | `held` | `fences`、`facts`、`css`、`prose` |
141
+ | `checks` | `content`、`html`、`layout`,状态只取 `pass`、`fail`、`skip` |
142
+
143
+ ```text
144
+ 已执行 writing-quarto · 增量润色
145
+ content/ch.qmd
146
+ 改了 5 处:定句子 3,定用词 1,定版式 1
147
+ 未改:围栏、事实
148
+ 检查:内容通过;HTML 可用
149
+ ```
150
+
151
+ 没改也会出卡(`改了 0 处`),这样「插件没跑」和「跑了但没改」分得开;当前文件不是章节时输出一行 `未执行 writing-quarto:…`。
152
+
122
153
  ## 主题
123
154
 
124
155
  主题资产在 `skills/designing-theme/assets/theme/`:
package/cordis.patch.yml CHANGED
@@ -1,8 +1,8 @@
1
1
  # dsh-quarto-book 的 bundle 补丁。
2
2
  #
3
3
  # 只做加法:插入一行宿主插件,不修改、禁用或替换官方已有的行。这一行加载本包
4
- # 的 `main` 入口,由它在 `ctx.skills` 上注册包内的 writing-quarto 与
5
- # designing-theme 两个技能。
4
+ # 的 `main` 入口,由它注册包内的 writing-quarto 与 designing-theme 两个技能,
5
+ # 以及 quarto_render_check 与 quarto_pass_report 两个工具。
6
6
  #
7
7
  # 从 Git 或 npm 安装,例如:
8
8
  #
@@ -2,14 +2,15 @@
2
2
 
3
3
  本文写给排障的人和回看的维护者,回答「装好之后,哪一层在什么条件下生效」。它是人读的流程说明,不是技能执行时的规则来源:技能正文不读本文,作用于写作的约束都写在 `skills/writing-quarto/SKILL.md` 与 `skills/designing-theme/SKILL.md` 里。
4
4
 
5
- ## 四层
5
+ ## 五层
6
6
 
7
7
  | 层 | 读者看到什么 | 生效条件 | 排障入口 |
8
8
  | --- | --- | --- | --- |
9
- | 1 安装 | 插件出现在 profile 的依赖里 | `dsh plugin --profile <profile> add npm:@chengzhao-dev/dsh-quarto-book@0.3.5` 成功,profile 的 `package.json` 有这条依赖 | `docs/versioning.md` |
9
+ | 1 安装 | 插件出现在 profile 的依赖里 | `dsh plugin --profile <profile> add npm:@chengzhao-dev/dsh-quarto-book@0.4.0` 成功,profile 的 `package.json` 有这条依赖 | `docs/versioning.md` |
10
10
  | 2 技能名出现 | 选题里能看到 `writing-quarto`、`designing-theme` | 会话 `cwd` 向上找到 git 根,沿途 `_quarto.yml` 有一行独立的 `type: book`,且这本书至少有一个章节源文件 | `docs/plugin-boundaries.md` |
11
11
  | 3 开始写章节 | 九个写作步骤和三套案例文件 | 第 2 层通过;章节文件是 `.qmd` 或 `.md` | `skills/writing-quarto/SKILL.md` |
12
12
  | 4 渲染检查 | `quarto_render_check` 返回 `safe_to_render` 与 `html_report` | 与第 2 层同一道门禁 | `skills/writing-quarto/steps/09-check.md` |
13
+ | 5 执行回执 | 一轮结束时出现 `quarto_pass_report` 卡片 | 技能完成判据要求收尾调用一次;写作与主题共用 | `skills/writing-quarto/steps/09-check.md` |
13
14
 
14
15
  ## 1 安装
15
16
 
@@ -31,7 +32,7 @@ profile 里还留着旧的无作用域包 `dsh-quarto-book` 时(该包已随 0
31
32
 
32
33
  ## 2 技能名出现
33
34
 
34
- `apply()` 注册技能 provider 和 `quarto_render_check` 工具,不写文件。provider 的 `list({ cwd })` 只在发现候选时做一次有界只读检查:
35
+ `apply()` 注册技能 provider 与两个工具(`quarto_render_check`、`quarto_pass_report`),不写文件。provider 的 `list({ cwd })` 只在发现候选时做一次有界只读检查:
35
36
 
36
37
  - 从会话 `cwd` 向上找到 git 根,沿途的 `_quarto.yml` 里有一行独立的 `type: book`,并且这本书至少有一个 `.qmd` 或 `.md` 章节源文件,就返回两个技能;否则返回空列表。书根下的 `README.md`、`CHANGELOG.md`、`LICENSE.md` 等仓库说明文件不算章节源文件。
37
38
  - 没有 `cwd`、不在 git checkout 里、`_quarto.yml` 缺失、只在注释里写了 `# type: book`、或只有 book 配置没有任何章节源文件,都按「不是书」处理。
@@ -58,3 +59,13 @@ profile 里还留着旧的无作用域包 `dsh-quarto-book` 时(该包已随 0
58
59
  - `dry_run: false` 才运行一次 `quarto render`,最多 1 次;成功时同一次返回 `html_report`,不再为同一份文件重跑。`html_report.usable` 是最终结论。
59
60
  - 工具与技能共用同一道门禁:不在书里、`_quarto.yml` 不是 `type: book`、或书里没有章节源文件时直接拒绝,不启动渲染。渲染根取 `cwd` 向上最近的合法书根,`qmd_path` 只用来定位卡片。
60
61
  - 不手跑 `quarto render`,也不截图 PNG;命令超时是 120 秒。实现与返回字段见 `src/tools/render-check.ts`,判定流程见 `steps/09-check.md`。
62
+
63
+ ## 5 执行回执
64
+
65
+ 第 9 步的「完成判据」要求收尾调用一次 `quarto_pass_report`,写作与主题共用:
66
+
67
+ - 入参是闭集:`skill`、`executed`、`mode`、`files`、`steps`、`held`、`checks`。`executed: false` 时 `mode` 必须是 `skipped` 并给 `skip_reason`,否则卡片会指出前后矛盾。
68
+ - 卡片就是回执:技能与模式、改过的文件、改了哪些步骤、未改项、检查结论。回复里不再复述;工具不保存到磁盘,不贴章节原文。
69
+ - `steps` 的 `edits` 合计必须等于 `files` 的合计;主题只认六个步骤名,不在名单里的名字(例如 `01-task`)直接拒绝。
70
+ - 当前文件不是章节时输出一行 `未执行 writing-quarto:…`;没改也会出卡(`改了 0 处`),把「插件没跑」和「跑了但没改」分开。
71
+ - 实现与参数见 `src/tools/pass-report.ts`,完成判据见 `steps/09-check.md`。
@@ -10,16 +10,16 @@
10
10
  - 磁盘:profile 的 `package.json` 里 `dependencies`,以及 `node_modules/@chengzhao-dev/dsh-quarto-book/package.json` 的 `version`。
11
11
  - 运行时加载清单:`dsh --profile <profile> --dump-config`,确认 `# == @chengzhao-dev/dsh-quarto-book` 这一层在。官方「设置 → 插件」只显示模块名和启用状态,不显示版本。
12
12
 
13
- README 里的安装命令钉住一次发布:npm 写 `@0.3.5`,git 写 `#v0.3.5`。不带版本号的 `github:` 地址跟踪默认分支的某次提交,`package.json` 的版本号不会自动变成发布号。
13
+ README 里的安装命令钉住一次发布:npm 写 `@0.4.0`,git 写 `#v0.4.0`。不带版本号的 `github:` 地址跟踪默认分支的某次提交,`package.json` 的版本号不会自动变成发布号。
14
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`。
15
+ 包名与模块名不是一回事:npm 与 profile 依赖里是 `@chengzhao-dev/dsh-quarto-book`,`cordis.patch.yml` 的 `name` 必须与它逐字相同;插件模块导出的 `name` 与技能 provider 的常量仍是短名 `dsh-quarto-book`,技能名与工具名不变。旧的无作用域包 `dsh-quarto-book` 是另一个 npm 坐标,已在迁到作用域包时从 npm 删除;之前装过它的 profile 仍要先 `remove` 再装新包,两份同时留着会以相同的 rank 550 各注册一遍 `writing-quarto` 和 `designing-theme`。
16
16
 
17
17
  ## 三档
18
18
 
19
19
  到达 `1.0.0` 之前停在 `0.x`。抬哪一档只看对外契约,不看改了多少字。
20
20
 
21
21
  - **修订号(`0.1.1`、`0.2.1`…)**:约定措辞、正反例、参考文、README、注释、不改变既有书通过与否的检查词表或提示。以后绝大多数提交走这一档。
22
- - **次版本 `0.2.0`、`0.3.0`**:新增技能名或工具名、新增会让旧书失败的检查、脚本命令行不兼容、主题路径或 `_quarto.yml` 接线契约变化、完成判据变成强制步骤。
22
+ - **次版本 `0.2.0`、`0.3.0`、`0.4.0`**:新增技能名或工具名、新增会让旧书失败的检查、脚本命令行不兼容、主题路径或 `_quarto.yml` 接线契约变化、完成判据变成强制步骤(`0.4.0` 新增 `quarto_pass_report`,并把收尾回执写进完成判据)。
23
23
  - **`1.0.0`**:技能名、工具名、cwd 门禁、脚本入口和「项目只放 `quarto-book-project`」这些对外契约冻结,并承诺以后的修订号不再让旧书突然失败。
24
24
  - **`engines.dsh`**:只在要支持的 DSH 版本窗口变化时改,与上面三档无关。DSH 进入 `0.3` 时再把上界打开。
25
25
 
@@ -36,7 +36,7 @@ flowchart TD
36
36
 
37
37
  ## 发布
38
38
 
39
- 发布时三处一起动:`package.json` 的 `version`、`package-lock.json` 顶部的版本、同号的 annotated git tag(`0.3.5` 对应 `v0.3.5`)。不在 `SKILL.md` 里再维护一套包版本。
39
+ 发布时三处一起动:`package.json` 的 `version`、`package-lock.json` 顶部的版本、同号的 annotated git tag(`0.4.0` 对应 `v0.4.0`)。不在 `SKILL.md` 里再维护一套包版本。
40
40
 
41
41
  发布走 npm trusted publisher。`0.3.4` 是这个作用域包的首个版本,由本机手工 `npm publish` 引导,同时登记了关联(仓库 `chengzhao-dev/dsh-quarto-book`、文件 `publish.yml`、动作 `npm publish`);旧包 `dsh-quarto-book` 也在这次发布后从 npm 删除。之后的版本不再本机发布,顺序固定:
42
42
 
@@ -44,7 +44,7 @@ flowchart TD
44
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
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
46
  4. 关联匹配后打 annotated `v<版本>` 并推送;`publish.yml` 在 `v*` 标签上运行。
47
- 5. 以 Actions 全绿且 `npm view @chengzhao-dev/dsh-quarto-book@<版本> version` 返回该版本为发布完成。`0.3.4` 是首发引导版本,由本机手工发布,当次 Actions 因关联尚未登记而失败,不作完成判据。npm 上的版本不可撤回,失败就停在那里,不改回版本号。
47
+ 5. 以 Actions 全绿且 `npm view @chengzhao-dev/dsh-quarto-book@<版本> version` 返回该版本为发布完成。`0.3.4` 是首发引导版本,由本机手工发布,当次 Actions 因关联尚未登记而失败,不作完成判据;`0.3.5` 是关联登记后第一个由 Actions 全绿发布的版本。npm 上的版本不可撤回,失败就停在那里,不改回版本号。
48
48
 
49
49
  关联登记后两天内必须推标签;过期就重新登记,不沿用旧凭据。
50
50
 
package/lib/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  // dsh-quarto-book 的 bundle 入口。
2
2
  //
3
3
  // 把包内两个技能注册成一个 bundle 作用域的技能 provider,并注册
4
- // `quarto_render_check` 工具。provider 只读,只扫描 `skills/<name>/SKILL.md`;
4
+ // `quarto_render_check` 与 `quarto_pass_report` 两个工具。provider 只读,只扫描 `skills/<name>/SKILL.md`;
5
5
  // 每个技能目录作为 resourceBase,使相对引用、脚本和主题资源都从安装后的
6
6
  // 包内解析。
7
7
  //
@@ -16,6 +16,7 @@ import path from 'node:path';
16
16
  import { fileURLToPath } from 'node:url';
17
17
  import { findBookRoot } from './workspace.js';
18
18
  import { registerRenderCheckTool } from './tools/render-check.js';
19
+ import { registerPassReportTool } from './tools/pass-report.js';
19
20
  export const name = 'dsh-quarto-book';
20
21
  export const inject = ['skills', 'tools'];
21
22
  const PROVIDER = 'dsh-quarto-book';
@@ -185,5 +186,6 @@ export function apply(ctx) {
185
186
  }));
186
187
  if (ctx.tools && typeof ctx.tools.register === 'function') {
187
188
  registerRenderCheckTool(ctx.tools);
189
+ registerPassReportTool(ctx.tools);
188
190
  }
189
191
  }
@@ -0,0 +1,332 @@
1
+ // `quarto_pass_report`:把一次写作或主题润色的结果压成固定行数的卡片。
2
+ //
3
+ // 入参是闭集:模式、文件、步骤、未改项、检查状态都只能取登记过的值。
4
+ // 卡片由工具自己拼接,返回的文本就是卡片本身,不附章节原文,也不贴改写后的句子。
5
+ // 没改也要出卡,这样「插件没跑」和「跑了但没改」在会话里分得开。
6
+ export const PASS_SKILLS = ['writing-quarto', 'designing-theme'];
7
+ export const PASS_MODES = ['polish', 'rewrite', 'theme', 'skipped'];
8
+ export const HELD_ITEMS = ['fences', 'facts', 'css', 'prose'];
9
+ export const CHECK_NAMES = ['content', 'html', 'layout'];
10
+ export const CHECK_STATUSES = ['pass', 'fail', 'skip'];
11
+ export const MAX_FILES = 8;
12
+ export const MAX_SKIP_REASON = 40;
13
+ /** 写作九步的短名,与 `skills/writing-quarto/SKILL.md` 的「九步」逐字对应。 */
14
+ export const WRITING_STEPS = {
15
+ '1': '定任务',
16
+ '2': '定结构',
17
+ '3': '排列内容块',
18
+ '4': '定段落',
19
+ '5': '定句子',
20
+ '6': '定用词',
21
+ '7': '定版式',
22
+ '8': '回读并退回',
23
+ '9': '检查与渲染',
24
+ };
25
+ /** 主题步骤表的名字,与 `skills/designing-theme/references/theme-spine.md` 逐字对应。 */
26
+ export const THEME_STEPS = ['定表面', '定字体', '定尺度', '定颜色', '定组件', '通过检查'];
27
+ const MODE_LABELS = {
28
+ polish: '增量润色',
29
+ rewrite: '全章重构',
30
+ theme: '主题',
31
+ skipped: '未执行',
32
+ };
33
+ const HELD_LABELS = {
34
+ fences: '围栏',
35
+ facts: '事实',
36
+ css: '样式',
37
+ prose: '正文',
38
+ };
39
+ const CHECK_LABELS = {
40
+ content: '内容',
41
+ html: 'HTML',
42
+ layout: '版式',
43
+ };
44
+ const PASS_VERDICTS = {
45
+ content: '通过',
46
+ html: '可用',
47
+ layout: '通过',
48
+ };
49
+ const FAIL_VERDICTS = {
50
+ content: '未通过',
51
+ html: '不可用',
52
+ layout: '未通过',
53
+ };
54
+ const TOP_LEVEL_KEYS = ['skill', 'executed', 'mode', 'files', 'steps', 'held', 'checks', 'skip_reason'];
55
+ function fail(message) {
56
+ throw new Error(message);
57
+ }
58
+ function isRecord(value) {
59
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
60
+ }
61
+ function isPlainCount(value) {
62
+ return typeof value === 'number' && Number.isInteger(value) && value >= 0;
63
+ }
64
+ function allowedSteps(skill) {
65
+ return skill === 'writing-quarto' ? Object.keys(WRITING_STEPS) : [...THEME_STEPS];
66
+ }
67
+ function stepLabel(skill, name) {
68
+ return skill === 'writing-quarto' ? (WRITING_STEPS[name] ?? name) : name;
69
+ }
70
+ function normalizeFiles(value) {
71
+ if (value === undefined)
72
+ return [];
73
+ if (!Array.isArray(value))
74
+ fail('files 必须是数组');
75
+ if (value.length > MAX_FILES)
76
+ fail(`files 最多 ${MAX_FILES} 个路径`);
77
+ return value.map((item) => {
78
+ if (!isRecord(item))
79
+ fail('files 的每一项必须是 { path, edits }');
80
+ const keys = Object.keys(item);
81
+ if (keys.some((key) => key !== 'path' && key !== 'edits'))
82
+ fail('files 的每一项只允许 path 和 edits');
83
+ const filePath = item.path;
84
+ if (typeof filePath !== 'string' || filePath.trim() === '')
85
+ fail('files 的 path 必须是非空字符串');
86
+ if (!isPlainCount(item.edits))
87
+ fail('files 的 edits 必须是非负整数');
88
+ return { path: filePath.trim(), edits: item.edits };
89
+ });
90
+ }
91
+ function normalizeSteps(value, skill) {
92
+ if (value === undefined)
93
+ return [];
94
+ if (!Array.isArray(value))
95
+ fail('steps 必须是数组');
96
+ const allowed = allowedSteps(skill);
97
+ const seen = new Set();
98
+ return value.map((item) => {
99
+ if (!isRecord(item))
100
+ fail('steps 的每一项必须是 { name, edits }');
101
+ const keys = Object.keys(item);
102
+ if (keys.some((key) => key !== 'name' && key !== 'edits'))
103
+ fail('steps 的每一项只允许 name 和 edits');
104
+ const name = item.name;
105
+ if (typeof name !== 'string' || !allowed.includes(name)) {
106
+ fail(`steps 的 name 只能是:${allowed.join('、')}`);
107
+ }
108
+ if (seen.has(name))
109
+ fail(`steps 的 name 重复:${name}`);
110
+ seen.add(name);
111
+ if (typeof item.edits !== 'number' || !Number.isInteger(item.edits) || item.edits < 1) {
112
+ fail('steps 的 edits 必须是正整数');
113
+ }
114
+ return { name, edits: item.edits };
115
+ });
116
+ }
117
+ function normalizeHeld(value) {
118
+ if (value === undefined)
119
+ return [];
120
+ if (!Array.isArray(value))
121
+ fail('held 必须是数组');
122
+ const seen = new Set();
123
+ return value.map((item) => {
124
+ if (typeof item !== 'string' || !HELD_ITEMS.includes(item)) {
125
+ fail(`held 只能是:${HELD_ITEMS.join('、')}`);
126
+ }
127
+ if (seen.has(item))
128
+ fail(`held 重复:${item}`);
129
+ seen.add(item);
130
+ return item;
131
+ });
132
+ }
133
+ function normalizeChecks(value) {
134
+ if (value === undefined)
135
+ return [];
136
+ if (!Array.isArray(value))
137
+ fail('checks 必须是数组');
138
+ const seen = new Set();
139
+ return value.map((item) => {
140
+ if (!isRecord(item))
141
+ fail('checks 的每一项必须是 { name, status }');
142
+ const keys = Object.keys(item);
143
+ if (keys.some((key) => key !== 'name' && key !== 'status'))
144
+ fail('checks 的每一项只允许 name 和 status');
145
+ const name = item.name;
146
+ if (typeof name !== 'string' || !CHECK_NAMES.includes(name)) {
147
+ fail(`checks 的 name 只能是:${CHECK_NAMES.join('、')}`);
148
+ }
149
+ if (seen.has(name))
150
+ fail(`checks 的 name 重复:${name}`);
151
+ seen.add(name);
152
+ const status = item.status;
153
+ if (typeof status !== 'string' || !CHECK_STATUSES.includes(status)) {
154
+ fail(`checks 的 status 只能是:${CHECK_STATUSES.join('、')}`);
155
+ }
156
+ return { name: name, status: status };
157
+ });
158
+ }
159
+ function checkLine(checks) {
160
+ const ordered = CHECK_NAMES.map((name) => checks.find((check) => check.name === name)).filter((check) => check !== undefined && check.status !== 'skip');
161
+ if (ordered.length === 0)
162
+ return undefined;
163
+ return '检查:' + ordered.map((check) => {
164
+ const label = CHECK_LABELS[check.name];
165
+ const verdict = check.status === 'pass' ? PASS_VERDICTS[check.name] : FAIL_VERDICTS[check.name];
166
+ // 中文标签直接接结论,英文标签(HTML)前面留一个空格。
167
+ return `${label}${/[A-Za-z]$/.test(label) ? ' ' : ''}${verdict}`;
168
+ }).join(';');
169
+ }
170
+ /** 校验入参并生成卡片;入参不合格时抛错,不猜测默认值。 */
171
+ export function buildPassReport(input) {
172
+ if (!isRecord(input))
173
+ fail('回执入参必须是对象');
174
+ const unknownKey = Object.keys(input).find((key) => !TOP_LEVEL_KEYS.includes(key));
175
+ if (unknownKey)
176
+ fail(`未知字段:${unknownKey}`);
177
+ const skill = input.skill;
178
+ if (typeof skill !== 'string' || !PASS_SKILLS.includes(skill)) {
179
+ fail(`skill 只能是:${PASS_SKILLS.join('、')}`);
180
+ }
181
+ const typedSkill = skill;
182
+ if (typeof input.executed !== 'boolean')
183
+ fail('executed 必须是布尔值');
184
+ const mode = input.mode;
185
+ if (typeof mode !== 'string' || !PASS_MODES.includes(mode)) {
186
+ fail(`mode 只能是:${PASS_MODES.join('、')}`);
187
+ }
188
+ const typedMode = mode;
189
+ if (!input.executed) {
190
+ if (typedMode !== 'skipped')
191
+ fail('executed 为 false 时 mode 必须是 skipped');
192
+ const reason = input.skip_reason;
193
+ if (typeof reason !== 'string' || reason.trim() === '')
194
+ fail('未执行时必须给出 skip_reason');
195
+ if ([...reason.trim()].length > MAX_SKIP_REASON)
196
+ fail(`skip_reason 最多 ${MAX_SKIP_REASON} 个字`);
197
+ if (input.files !== undefined && Array.isArray(input.files) && input.files.length > 0) {
198
+ fail('未执行时 files 必须为空');
199
+ }
200
+ if (input.steps !== undefined && Array.isArray(input.steps) && input.steps.length > 0) {
201
+ fail('未执行时 steps 必须为空');
202
+ }
203
+ if (input.held !== undefined && Array.isArray(input.held) && input.held.length > 0) {
204
+ fail('未执行时 held 必须为空');
205
+ }
206
+ if (input.checks !== undefined && Array.isArray(input.checks) && input.checks.length > 0) {
207
+ fail('未执行时 checks 必须为空');
208
+ }
209
+ return { receipt: `未执行 ${typedSkill}:${reason.trim()}` };
210
+ }
211
+ if (typedMode === 'skipped')
212
+ fail('executed 为 true 时 mode 不能是 skipped');
213
+ if (typedSkill === 'designing-theme' && typedMode !== 'theme')
214
+ fail('designing-theme 只接受 mode: theme');
215
+ if (typedSkill === 'writing-quarto' && typedMode === 'theme')
216
+ fail('writing-quarto 不接受 mode: theme');
217
+ const files = normalizeFiles(input.files);
218
+ if (files.length === 0)
219
+ fail('已执行时 files 至少给 1 个路径');
220
+ const steps = normalizeSteps(input.steps, typedSkill);
221
+ const held = normalizeHeld(input.held);
222
+ const checks = normalizeChecks(input.checks);
223
+ const fileEdits = files.reduce((sum, file) => sum + file.edits, 0);
224
+ const stepEdits = steps.reduce((sum, step) => sum + step.edits, 0);
225
+ if (fileEdits !== stepEdits)
226
+ fail(`files 的改动合计 ${fileEdits} 与 steps 的合计 ${stepEdits} 不一致`);
227
+ const lines = [`已执行 ${typedSkill} · ${MODE_LABELS[typedMode]}`, files.map((file) => file.path).join('、')];
228
+ if (fileEdits === 0) {
229
+ lines.push('改了 0 处');
230
+ }
231
+ else {
232
+ lines.push(`改了 ${fileEdits} 处:` + steps.map((step) => `${stepLabel(typedSkill, step.name)} ${step.edits}`).join(','));
233
+ }
234
+ if (held.length > 0)
235
+ lines.push('未改:' + held.map((item) => HELD_LABELS[item]).join('、'));
236
+ const checksText = checkLine(checks);
237
+ if (checksText)
238
+ lines.push(checksText);
239
+ return { receipt: lines.join('\n') };
240
+ }
241
+ const RESULT_SCHEMA = {
242
+ type: 'object',
243
+ additionalProperties: false,
244
+ properties: { receipt: { type: 'string' } },
245
+ required: ['receipt'],
246
+ };
247
+ export function registerPassReportTool(tools) {
248
+ tools.register({
249
+ name: 'quarto_pass_report',
250
+ description: '一次写作或主题润色收尾时调用一次,出一张短回执卡片:执行了哪个技能、什么模式、改了哪些文件与步骤、未改项、检查结论。入参是闭集:skill、mode、files(最多 8 个,每条带 path 与 edits)、steps(写作 1-9,主题用定表面/定字体/定尺度/定颜色/定组件/通过检查,各带 edits,合计必须等于 files 的 edits 合计)、held、checks、skip_reason。没改也传 0;不适用时 executed:false、mode:skipped、给 skip_reason。卡片就是回执,不要在回复里再复述。',
251
+ parameters: {
252
+ type: 'object',
253
+ additionalProperties: false,
254
+ properties: {
255
+ skill: { type: 'string', enum: [...PASS_SKILLS] },
256
+ executed: { type: 'boolean' },
257
+ mode: { type: 'string', enum: [...PASS_MODES] },
258
+ files: {
259
+ type: 'array',
260
+ maxItems: MAX_FILES,
261
+ items: {
262
+ type: 'object',
263
+ additionalProperties: false,
264
+ properties: {
265
+ path: { type: 'string' },
266
+ edits: { type: 'integer', minimum: 0 },
267
+ },
268
+ required: ['path', 'edits'],
269
+ },
270
+ },
271
+ steps: {
272
+ type: 'array',
273
+ items: {
274
+ type: 'object',
275
+ additionalProperties: false,
276
+ properties: {
277
+ name: { type: 'string' },
278
+ edits: { type: 'integer', minimum: 1 },
279
+ },
280
+ required: ['name', 'edits'],
281
+ },
282
+ },
283
+ held: { type: 'array', items: { type: 'string', enum: [...HELD_ITEMS] } },
284
+ checks: {
285
+ type: 'array',
286
+ items: {
287
+ type: 'object',
288
+ additionalProperties: false,
289
+ properties: {
290
+ name: { type: 'string', enum: [...CHECK_NAMES] },
291
+ status: { type: 'string', enum: [...CHECK_STATUSES] },
292
+ },
293
+ required: ['name', 'status'],
294
+ },
295
+ },
296
+ skip_reason: { type: 'string', maxLength: MAX_SKIP_REASON },
297
+ },
298
+ required: ['skill', 'executed', 'mode'],
299
+ },
300
+ output: {
301
+ schema: RESULT_SCHEMA,
302
+ render: (_args, value) => [{ type: 'text', text: value.receipt }],
303
+ presentationMeta: (_args, value) => ({ summary: value.receipt.split('\n')[0], lines: value.receipt.split('\n') }),
304
+ },
305
+ async execute(args) {
306
+ return buildPassReport(args);
307
+ },
308
+ presentCall(args) {
309
+ const files = Array.isArray(args.files) ? args.files : [];
310
+ return {
311
+ card: 'generic',
312
+ title: '写作回执',
313
+ kind: 'execute',
314
+ locations: files.map((file) => ({ path: String(file.path) })),
315
+ rawInput: `${args.skill} · ${args.mode}`,
316
+ content: [
317
+ {
318
+ type: 'text',
319
+ text: args.executed === false ? `${args.skill}:未执行` : `${args.skill} · ${args.mode}(${files.length} 个文件)`,
320
+ },
321
+ ],
322
+ };
323
+ },
324
+ presentResult(_args, result) {
325
+ return {
326
+ card: 'generic',
327
+ title: '写作回执',
328
+ content: result.content,
329
+ };
330
+ },
331
+ });
332
+ }
package/locale/en.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "meta": {
3
3
  "title": "Quarto Book Writing",
4
- "description": "Author Chinese Quarto Book chapters and maintain a reusable book theme: structure checks, prose conventions, Mermaid and typography assets."
4
+ "description": "Author and check Quarto Book chapters and maintain a reusable theme; no business code, only inside type: book repositories."
5
5
  }
6
6
  }
package/locale/zh.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "meta": {
3
3
  "title": "Quarto 图书写作",
4
- "description": "编写中文 Quarto Book 章节并维护可复用主题:结构校验、行文约定、Mermaid 与版式资产。"
4
+ "description": "为 Quarto Book 写章节、校验结构并维护可复用主题;不改业务代码,只在 type: book 的仓库里出现。"
5
5
  }
6
6
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@chengzhao-dev/dsh-quarto-book",
3
- "version": "0.3.5",
4
- "description": "Quarto Book 写作与主题技能包:章节写作、结构校验与可复用主题 · Quarto Book authoring and theme skills for DeepSeek Harness",
3
+ "version": "0.4.0",
4
+ "description": "为 Quarto Book 写章节、校验结构并维护可复用主题;不改业务代码,只在 type: book 的仓库里出现。 · Author and check Quarto Book chapters and maintain a reusable theme; no business code, only inside `type: book` repositories.",
5
5
  "type": "module",
6
6
  "main": "./lib/index.js",
7
7
  "exports": {
@@ -30,6 +30,14 @@
30
30
  "dsh": {
31
31
  "bundle": {
32
32
  "patch": "./cordis.patch.yml"
33
+ },
34
+ "catalog": {
35
+ "category": "workflow",
36
+ "summary": {
37
+ "zh": "为 Quarto Book 写章节、校验结构并维护可复用主题;不改业务代码,只在 type: book 的仓库里出现。",
38
+ "en": "Author and check Quarto Book chapters and maintain a reusable theme; no business code, only inside `type: book` repositories."
39
+ },
40
+ "capabilities": ["skills", "tools"]
33
41
  }
34
42
  },
35
43
  "icon": "./icon.svg",
@@ -40,3 +40,4 @@ metadata:
40
40
  - [ ] `quarto render` 通过,明暗主题下页面无错位或对比度问题;`scripts/checking-layout.py` 通过。
41
41
  - [ ] 代码块 filename 标题在长路径输入下不撑破容器,窄屏仍可读;超长代码在块内横向滚动,不出整页横向滚动条。
42
42
  - [ ] 若项目内有主题副本,已在交付说明中写清改动是否同步,以及以哪一份为准。
43
+ - [ ] 收尾调用一次 `quarto_pass_report`:`skill` 填 `designing-theme`,`mode` 填 `theme`,`steps` 用主题步骤表的名字(`定表面`、`定字体`、`定尺度`、`定颜色`、`定组件`、`通过检查`),检查结论照实填。卡片就是回执,回复里不再复述。
@@ -41,3 +41,4 @@ metadata:
41
41
  - [ ] 九步走完,每一步的产出都落在正文里。
42
42
  - [ ] 内容与链接核对通过:项目技能存在时跑它列出的 `run.ps1 verify --changed`,否则跑本技能 `scripts/verifying-content.py` 与 `checking-links.py`。
43
43
  - [ ] 调 `quarto_render_check`:`dry_run: true` 最多 2 次,确认 `safe_to_render`;`dry_run: false` 最多 1 次,确认 `html_report.usable` 为 true。不手跑 `quarto render`,也不截图 PNG。
44
+ - [ ] 收尾调用一次 `quarto_pass_report`:`skill` 填 `writing-quarto`,`mode` 填 `polish` 或 `rewrite`,文件与步骤的改动次数、未改项、检查结论照实填。卡片就是回执,回复里不再复述;没改也传 0,当前文件不是章节时传 `executed: false`。
@@ -43,6 +43,18 @@
43
43
 
44
44
  不手跑 `quarto render`,也不手跑 `checking-output.py`、`checking-callouts.py`、`checking-dom.py`。工具已经按当前书配置调用它们。
45
45
 
46
+ ### 执行回执
47
+
48
+ 检查结论确定后,调用一次 `quarto_pass_report`,把这次执行的结论压成一张卡片:
49
+
50
+ - `skill` 填 `writing-quarto`,`mode` 填 `polish` 或 `rewrite`。
51
+ - `files` 写这次动过的章节路径和改动次数(最多 8 个);`steps` 只列有改动的步骤编号(1–9)与次数,两者合计必须一致。
52
+ - `held` 写围栏、事实这类明确没有改动的地方;没改就传 0。
53
+ - `checks` 抄内容与渲染的结论,名字只取 `content`、`html`、`layout`,状态只取 `pass`、`fail`、`skip`。
54
+ - 当前文件不是章节时传 `executed: false`、`mode: skipped`,并给一句 `skip_reason`。
55
+
56
+ 卡片就是回执,回复里不再复述;回执不保存到磁盘,也不贴改写后的句子,不根据回执改文章。
57
+
46
58
  ## 情况
47
59
 
48
60
  ### 情况:全仓重构
@@ -55,7 +67,7 @@
55
67
 
56
68
  ## 完成判据
57
69
 
58
- 内容命令通过,`dry_run: false` 返回 `render_result.ok: true`,且 `html_report.usable: true`,才算完成。任一条件不满足,回到报告指向的步骤。
70
+ 内容命令通过,`dry_run: false` 返回 `render_result.ok: true`,且 `html_report.usable: true`,并调用一次 `quarto_pass_report` 出回执,才算完成。任一条件不满足,回到报告指向的步骤。
59
71
 
60
72
  ## 正反例
61
73