@tanlinfeng/project-docs-workflow 2.2.0 → 2.3.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.
Files changed (27) hide show
  1. package/README.md +73 -152
  2. package/bin/install.js +9 -3
  3. package/package.json +1 -1
  4. package/skills/docs-html-view/SKILL.md +119 -0
  5. package/skills/docs-html-view/agents/openai.yaml +5 -0
  6. package/skills/docs-html-view/assets/conf.py.template +44 -0
  7. package/skills/docs-html-view/assets/guard.py +128 -0
  8. package/skills/docs-html-view/references/technical.md +151 -0
  9. package/skills/docs-html-view/requirements.txt +5 -0
  10. package/skills/docs-html-view/scripts/gen_nav.py +306 -0
  11. package/skills/docs-html-view/scripts/prepare_env.py +154 -0
  12. package/skills/project-docs-workflow/SKILL.md +15 -6
  13. package/skills/project-docs-workflow/assets/templates/entries/current-module.md +19 -0
  14. package/skills/project-docs-workflow/assets/templates/entries/pdr-spec.md +1 -1
  15. package/skills/project-docs-workflow/assets/templates/project/adr-readme.md +2 -1
  16. package/skills/project-docs-workflow/assets/templates/project/agents-block.md +1 -1
  17. package/skills/project-docs-workflow/assets/templates/project/agents-issues.md +6 -5
  18. package/skills/project-docs-workflow/assets/templates/project/agents-workflow.md +9 -6
  19. package/skills/project-docs-workflow/assets/templates/project/current-00-readme.md +42 -0
  20. package/skills/project-docs-workflow/assets/templates/project/pdr-00-readme.md +6 -7
  21. package/skills/project-docs-workflow/references/adr.md +5 -1
  22. package/skills/project-docs-workflow/references/current.md +39 -0
  23. package/skills/project-docs-workflow/references/issues.md +3 -2
  24. package/skills/project-docs-workflow/references/pdr.md +15 -10
  25. package/skills/project-docs-workflow/references/rendering.md +29 -0
  26. package/skills/project-docs-workflow/references/workflow.md +6 -5
  27. package/skills/project-docs-workflow/assets/templates/entries/pdr-current.md +0 -19
package/README.md CHANGED
@@ -1,198 +1,119 @@
1
1
  # @tanlinfeng/project-docs-workflow
2
2
 
3
- > 项目文档工作流 skill 包:提供 issue tracker、ADR、PDR、术语表与协作工作流约定,自带轻量依赖 skill 与 ref 兜底,可独立分发。
3
+ 面向 AI agent 的项目文档工作流 skill:帮助项目建立并维护 issue tracker、ADR、PDR、领域术语和开发工作流。主 skill 自带依赖兜底材料,可独立使用。
4
4
 
5
- ## 这是什么
5
+ ## 快速开始
6
6
 
7
- `project-docs-workflow` 是一套面向 AI agent 的「项目文档工作流」skill。它把项目中的几类关键文档沉淀为可复用的模板与约定:
8
-
9
- - **issue tracker**:任务 / 缺陷的登记与追踪约定
10
- - **ADR**(Architecture Decision Record):架构决策记录
11
- - **PDR**(Product/Project Design Record):设计与需求记录
12
- - **terminology**:领域术语表,统一团队与 agent 的词汇
13
- - **workflow**:上述文档之间的协作与更新流程
14
-
15
- skill 本身是自包含的:主 skill 内部带 `references/deps/*.md` 兜底材料,因此即使没有安装任何依赖 skill,也能独立工作。包内同时附带了若干**轻量依赖 skill**(来自 `mattpocock/skills` 的精简版本)以及一个**附赠的 `doc-opt` skill**。
16
-
17
- ## 安装
18
-
19
- 通过 npx 直接安装到默认的 skills 根目录(`~/.agents/skills`):
7
+ **前置条件:** Node.js >= 18,并可使用 `npx`。安装器默认将 skill 安装到 `~/.agents/skills`。
20
8
 
21
9
  ```bash
22
10
  npx @tanlinfeng/project-docs-workflow
23
11
  ```
24
12
 
25
- 常用选项:
26
-
27
- | 命令 | 说明 |
28
- | --- | --- |
29
- | `npx @tanlinfeng/project-docs-workflow` | 默认动作 `install`:安装主 skill 及所有依赖 skill |
30
- | `npx @tanlinfeng/project-docs-workflow --dir <path>` | 指定 skills 根目录(默认 `~/.agents/skills`) |
31
- | `npx @tanlinfeng/project-docs-workflow --force` | 覆盖已存在的 `project-docs-workflow`(**仅主 skill**;依赖 skill 仍不覆盖) |
32
- | `npx @tanlinfeng/project-docs-workflow --dry-run` | 只打印将执行的操作,不写入任何文件 |
33
- | `npx @tanlinfeng/project-docs-workflow --with-matt-skills` | 可选:依赖 skill 改从上游 `mattpocock/skills` 安装**完整版**(需 Node >= 22.20) |
34
- | `npx @tanlinfeng/project-docs-workflow --matt-agent <name>` | 上游安装使用的 agent(默认 `codex`,落盘到其全局目录 `~/.agents/skills`) |
35
- | `npx @tanlinfeng/project-docs-workflow check` | 检查依赖 skill 的解析状态,全部可解析时退出码为 0,否则为 1 |
36
- | `npx @tanlinfeng/project-docs-workflow uninstall` | 依据**安装清单**卸载本包安装的 skill(只删本包装的,见下文「卸载」) |
37
- | `npx @tanlinfeng/project-docs-workflow uninstall --dry-run` | 只打印卸载计划(将删除 / 保留哪些),不写入 |
38
- | `npx @tanlinfeng/project-docs-workflow uninstall --force` | 跳过交互确认;非 TTY 场景下**必须**加此参数 |
39
-
40
- ### 覆盖策略(安全优先)
13
+ 这一步只安装 skill,**不会初始化任何项目文档**。安装完成后,进入要设置文档的**目标项目根目录**,使用当前 agent 自己的 skill 调用方式手动运行 `project-docs-workflow`。例如可以这样请求:
41
14
 
42
- 安装器**永不覆盖用户已存在的同名 skill**。若目标目录中已存在同名目录,会打印 `同名 skill 已存在,保留用户版本:<name>` 并跳过。只有对主 skill `project-docs-workflow` 使用 `--force` 时才会覆盖它;依赖 skill 即使加了 `--force` 也仍会跳过。
43
-
44
- ### 可选:从上游安装「完整版」依赖 skill
45
-
46
- 包内自带的依赖 skill 是来自 `mattpocock/skills` 的**轻量版**。如果你更想要**完整上游版本**,可以启用 `--with-matt-skills`:安装器会调用官方 `skills` CLI(npm 包 `skills`)执行
47
-
48
- ```bash
49
- npx --yes skills@latest add mattpocock/skills -g -y -a <agent> -s <name1> <name2> ...
50
- ```
15
+ > 请使用 project-docs-workflow skill,为当前项目初始化项目文档工作流。
51
16
 
52
- - **交互式询问**:不带 `--with-matt-skills` 时,仅当 stdout 与 stdin **都是 TTY**、动作为 `install`(非 `check`)、且非 `--dry-run` 时,才提示一次 `是否从上游安装依赖 skill 的完整版(mattpocock/skills)?[y/N] `,**默认否**(空输入或其它输入均视为否)。非 TTY / 管道 / CI 场景**完全不询问**,不会挂起。
53
- - **受管集合**:仅这 8 个名字参与上游安装:`grilling`、`grill-me`、`grill-with-docs`、`domain-modeling`、`tdd`、`deepwork`、`retro`、`improve-codebase-architecture`;`doc-opt` 与主 skill 不属于上游集合,维持原有行为。
54
- - **永不覆盖**:调用上游前会先检测这 8 个名字里哪些在目标目录中**已存在**,已存在者**原样保留、绝不提交给上游**;只有缺失的名字才会通过 `-s` 传给上游。
55
- - **顺序与去重**:先跑上游;随后复核上游目标目录,仍然缺失的名字才回退复制本包的轻量版。启用上游时,由上游负责的名字不会再复制轻量版;未启用时(默认)行为与之前完全一致。
56
- - **降级、绝不失败**:Node < 22.20、找不到 `npx`、上游非 0 退出或网络错误时,只打印中文告警并回退到缺失项的轻量版安装,整体仍以退出码 0 结束。
57
- - **agent 与目录注意**:上游固定以 `-a <agent> -g` 安装到该 agent 的**全局目录**(默认 `codex` → `~/.agents/skills`,正好与默认目标一致)。可用 `--matt-agent <name>` 覆盖 agent。若你传入了非默认的 `--dir`,上游部分**无法跟随 `--dir`**(仍落到 agent 全局目录),安装器会明确告警,同时 `--dir` 下的轻量版仍按原逻辑处理。
58
- - **dry-run**:`--dry-run --with-matt-skills` 只打印将执行的上游命令、以及哪些名字因已存在而跳过,不实际执行。
17
+ skill 会检查并创建缺少的内容,包括:
59
18
 
60
- ## 卸载
61
-
62
- 安装时,本包会在 `<target>/.project-docs-workflow.install.json`(`<target>` 为 `--dir` 或默认 `~/.agents/skills`)写入一份**安装清单**,记录每个被处理过的 skill 及其**归属**。卸载完全依据这份清单,而不是靠猜测。
63
-
64
- 清单形如:
65
-
66
- ```json
67
- {
68
- "version": "2.2.0",
69
- "target": "/home/me/.agents/skills",
70
- "updatedAt": "2026-10-12T05:24:36.386Z",
71
- "entries": {
72
- "project-docs-workflow": { "kind": "main", "action": "installed", "at": "…" },
73
- "tdd": { "kind": "lite", "action": "installed", "at": "…" },
74
- "doc-opt": { "kind": "doc-opt", "action": "installed", "at": "…" },
75
- "deepwork": { "kind": "lite", "action": "skipped", "at": "…" },
76
- "retro": { "kind": "upstream", "action": "installed", "agent": "codex", "at": "…" }
77
- }
78
- }
79
- ```
19
+ - `AGENTS.md` 中的 `## Agent skills` 区块
20
+ - `docs/agents/issues.md`、`docs/agents/domain.md`、`docs/agents/workflow.md`
21
+ - `docs/agents/deps/` 依赖兜底文件
22
+ - `docs/issues/` issue 目录树
23
+ - `docs/adr/README.md`、`docs/pdr/00-README.md`、`docs/current/00-README.md` 和 `CONTEXT.md`
80
24
 
81
- - `kind`:`main` / `lite` / `doc-opt`(本包复制的轻量版)/ `upstream`(委托上游 `skills` CLI 安装的完整版)。
82
- - `action`:`installed`(本次由本包创建)、`forced`(被 `--force` 覆盖,仍属本包)、`skipped`(安装时已存在,本包原样保留,**永不删除**)。
83
- - 每次成功 `install` 都会**合并更新**清单(不会丢失旧条目);`--dry-run` 不写清单。
25
+ 每个项目手动运行一次即可。以后模板更新时可以再次运行,以检查对齐情况并报告差异;运行是幂等的,不会改写已有内容。例外是:若 `AGENTS.md` 已存在但没有 `## Agent skills`,skill 会在文件末尾追加该区块;若区块已存在,则只比较、不重写。
84
26
 
85
- **归属规则(安全优先)**:`uninstall` 只删除清单中 `action` 为 `installed` / `forced` 的名字——无论是本包复制的轻量版,还是本包委托上游安装的完整版。凡是安装时**已存在**(记为 `skipped`)的名字,一律不碰;首次记为 `skipped` 的名字即使后续再次安装也始终保持 `skipped`。
27
+ ## 命令与选项
86
28
 
87
- 用法:
29
+ | 场景 | 命令 / 选项 | 说明 |
30
+ | --- | --- | --- |
31
+ | 安装到默认目录 | `npx @tanlinfeng/project-docs-workflow` | 默认动作是安装,目标为 `~/.agents/skills`。 |
32
+ | 预览安装 | `npx @tanlinfeng/project-docs-workflow --dry-run` | 仅显示计划,不写入文件。 |
33
+ | 指定 skills 根目录 | `npx @tanlinfeng/project-docs-workflow --dir <path>` | 覆盖默认目录。 |
34
+ | 覆盖主 skill | `npx @tanlinfeng/project-docs-workflow --force` | 安装时只覆盖 `project-docs-workflow`;依赖 skill 不覆盖。卸载时则表示跳过交互确认。 |
35
+ | 安装上游完整版 | `npx @tanlinfeng/project-docs-workflow --with-matt-skills` | 可选;需 Node.js >= 22.20。自定义 agent 可加 `--matt-agent <name>`。 |
36
+ | 检查依赖 | `npx @tanlinfeng/project-docs-workflow check` | 只检查依赖 skill 的解析状态;不检查主 skill 是否安装,也不检查项目文档是否初始化。 |
37
+ | 预览卸载 | `npx @tanlinfeng/project-docs-workflow uninstall --dry-run` | 只显示删除计划,不询问、不删除。 |
38
+ | 交互卸载 | `npx @tanlinfeng/project-docs-workflow uninstall` | 根据安装清单卸载本包记录的内容;TTY 下询问确认(默认否)。 |
39
+ | 非交互卸载 | `npx @tanlinfeng/project-docs-workflow uninstall --force` | 跳过确认;非 TTY 下必须显式传入 `--force`。 |
88
40
 
89
- ```bash
90
- npx @tanlinfeng/project-docs-workflow uninstall [--dir <path>] [--dry-run] [--force]
91
- ```
41
+ 更多安装依赖的选项及行为见下方「高级细节」。
92
42
 
93
- - **确认**:在交互式终端会询问一次 `确认卸载本包安装的 skill?[y/N] `,**默认否**。非交互式(管道 / CI / 非 TTY)**拒绝执行**,除非显式传入 `--force`。`--dry-run` 从不询问、从不写入。
94
- - **删除方式**:`main` / `lite` / `doc-opt` 直接删除 `<target>/<name>` 目录。`upstream` 的完整版**优先委托上游** `skills` CLI 卸载(按 agent 批量):`npx --yes skills@latest remove <names> -g -y -a <agent>`;当无法委托时(Node < 22.20、找不到 `npx`、上游非 0 退出)会打印告警并**回退直接删除**目录,并提示上游锁文件 `~/.agents/.skill-lock.json` 可能残留过期条目,可用 `npx skills@latest list` 核对。
95
- - **收尾**:处理完成后删除清单文件本身;**不会**删除目标目录本身,其它文件与空目录保持不动。
96
- - **幂等**:没有可删项时打印友好提示并以退出码 0 结束。若**清单缺失**,会告警「无清单,尽力而为」,此时**仅**在 `project-docs-workflow` 存在时询问是否删除它,其余 skill 一律不动。
43
+ ## 卸载安全须知
97
44
 
98
- ## 用法示例
45
+ 安装器会在目标 skills 根目录写入 `<target>/.project-docs-workflow.install.json`,卸载时依据清单记录的 skill **名称和操作类型**决定删除哪些路径:`installed` / `forced` 项会删除,`skipped` 项保留。清单记录的是名称和操作,**不记录内容哈希**:如果你之后把自己或其他工具的内容放到一个记录为本包安装的同名路径下,卸载仍会删除该路径。运行卸载前请先检查清单和目标目录;不确定时先使用 `--dry-run`。
99
46
 
100
- ```bash
101
- # —— 安装 ——
102
- npx @tanlinfeng/project-docs-workflow # 装到默认 ~/.agents/skills(轻量依赖)
103
- npx @tanlinfeng/project-docs-workflow --dir ~/.config/opencode/skills
104
- npx @tanlinfeng/project-docs-workflow --dry-run # 预演,不写入
105
- npx @tanlinfeng/project-docs-workflow --force # 覆盖主 skill(依赖仍不覆盖)
106
- npx @tanlinfeng/project-docs-workflow --with-matt-skills # 依赖改从上游装完整版(需 Node >= 22.20)
107
- npx @tanlinfeng/project-docs-workflow --with-matt-skills --matt-agent opencode
108
-
109
- # —— 检查依赖解析 ——
110
- npx @tanlinfeng/project-docs-workflow check # 全部可解析退出 0,否则 1
111
-
112
- # —— 卸载 ——
113
- npx @tanlinfeng/project-docs-workflow uninstall --dry-run # 只出计划
114
- npx @tanlinfeng/project-docs-workflow uninstall # 交互确认(默认否)
115
- npx @tanlinfeng/project-docs-workflow uninstall --force # 非交互必传;只删本包安装的
116
- npx @tanlinfeng/project-docs-workflow uninstall --dir ~/.config/opencode/skills --force
117
- ```
47
+ 如果清单不存在,卸载器只会处理主 skill `project-docs-workflow`,不会推测或删除其他依赖。实际卸载中,只有存在可移除条目时才会在完成后删除清单;dry-run、取消操作和没有可移除条目的运行都会保留清单。清单里的上游完整版会尽量通过 `skills` CLI 卸载;无法调用 CLI 时会回退为直接删除,可能留下上游锁文件中的过期条目。
118
48
 
119
- - 交互式终端下、不带 `--with-matt-skills` 安装时,会**询问一次**是否装上游完整版(默认否);管道 / CI 不提示、不挂起。
120
- - 卸载在非 TTY 下没有 `--force` 会**拒绝执行**(退出码 1);`--dry-run` 从不询问、从不写入。
49
+ ## 手动复制
121
50
 
122
- ## 直接复制(不使用 npm 安装器)
51
+ 也可以不使用 npm 安装器,直接将 `skills/` 中需要的目录复制到 skills 根目录;只复制 `skills/project-docs-workflow` 也可以独立工作,因为主 skill 内含依赖 ref 兜底。手动复制不创建安装清单,也**不由安装器强制执行不覆盖规则**。之后运行 `uninstall` 时无法识别手动复制的依赖;若没有安装清单,它只会处理主 skill。
123
52
 
124
- 也可以完全跳过安装器,手动把 `skills/` 下需要的内容复制进你的 skills 根目录:
53
+ ## docs-html-view(可选:手动渲染文档)
125
54
 
126
- - 逐个复制 `skills/*` 中的目录;**遇到同名目录就跳过,绝不覆盖用户版本**。
127
- - 也可以**只复制 `skills/project-docs-workflow` 这一个目录**:它会通过内部的 ref 兜底(`references/deps/*.md`)独立工作,无需其他依赖 skill。
55
+ 本包附带一个第一方 skill `docs-html-view`(与主 skill、`doc-opt` 一样,不属于上游集合)。安装器会随包一起分发它,但**安装时不会运行它**,项目文档初始化时也不会启用它;它只在用户手动调用时工作。
128
56
 
129
- ## 两级解析协议
57
+ 用途:把项目文档(PDR / Current / ADR)用标准的 Read the Docs(RTD)主题与 MyST Markdown 解析渲染成静态 HTML,输出到 `docs/docsHtmlView/build/html`。仅供本地浏览与分享,不替代源文档。
130
58
 
131
- skill 在引用依赖能力时,遵循两级解析:
59
+ **边界(重要):**
132
60
 
133
- 1. **按名解析**:如果 skills 根目录下存在同名 skill(`<target>/<name>/SKILL.md`),直接使用该已安装的 skill。
134
- 2. **ref 兜底**:否则,回退到主 skill 内部自带的 `references/deps/<fallback>.md`。
61
+ - **先确认再动手**:初始化与构建前,agent 会先列出所需的 Python 包、环境以及将要创建 / 修改的文件,取得你的明确同意后才执行;不会静默装包或改环境。
62
+ - **环境**:优先复用项目明确约定的文档环境;没有合适环境时才创建独立的 `.venv-docs`,不修改系统或应用环境。
63
+ - **标准构建**:首次使用 `sphinx-quickstart` 初始化,之后复用配置,通过 `make html`(或 `sphinx-build`)按需增量构建,不自建渲染封装器。
64
+ - **全手动**:不注册自动钩子,不自动打开浏览器,不启动常驻服务,不部署。
65
+ - **源文档只读**:渲染只读取源文档,不修改 `docs/` 下的 PDR / Current / ADR 源文件。
66
+ - **不参与 `check`**:`project-docs-workflow check` 只检查主 skill 的依赖解析状态,**不检查** `docs-html-view`,也不校验其渲染结果。
135
67
 
136
- | 引用名 | 兜底文件 |
137
- | --- | --- |
138
- | `grilling` | `grilling.md` |
139
- | `grill-me` | `grilling.md` |
140
- | `grill-with-docs` | `grilling.md`、`domain-modeling.md` |
141
- | `domain-modeling` | `domain-modeling.md` |
142
- | `tdd` | `tdd.md` |
143
- | `deepwork` | `boundaries.md` |
144
- | `retro` | `boundaries.md` |
145
- | `doc-opt` | `doc-opt.md` |
146
- | `improve-codebase-architecture` | `boundaries.md` |
68
+ 具体的脚本、命令与参数以该 skill 的 `SKILL.md` 为准(安装后位于 `<skills 根目录>/docs-html-view/SKILL.md`),README 不再重复其接口。
147
69
 
148
- `check` 子命令会对照上表逐个报告「按名可解析(skill 已安装)」或「ref 兜底可用」,只要每个名字能通过任一级解析即视为通过。
70
+ ## English summary
149
71
 
150
- ## 内置 skill 与来源
72
+ `@tanlinfeng/project-docs-workflow` is an agent skill for setting up and maintaining project documentation workflows: issues, ADRs, PDRs, domain terminology, and development practices. Install it with `npx @tanlinfeng/project-docs-workflow` (Node.js >= 18; default destination `~/.agents/skills`). Installation does not initialize a project: from the target project root, manually invoke the skill using your agent's own skill mechanism. It creates missing documentation, reports drift on reruns, and does not overwrite existing content, except that it appends a missing `## Agent skills` block to an existing `AGENTS.md`.
151
73
 
152
- | skill | 说明 | 来源 |
153
- | --- | --- | --- |
154
- | `project-docs-workflow` | 主 skill:issue / ADR / PDR / 术语 / 工作流 | 本包 |
155
- | `grilling`、`grill-me`、`grill-with-docs` | 需求拷问相关 | `mattpocock/skills` 精简版(MIT,Matt Pocock) |
156
- | `domain-modeling` | 领域建模 | 同上 |
157
- | `tdd` | 测试驱动开发 | 同上 |
158
- | `deepwork`、`retro`、`improve-codebase-architecture` | 精简/占位版本 | 同上 |
159
- | `doc-opt` | 文档优化(附赠完整版) | 本包附赠 |
74
+ The installer supports custom directories, dry runs, dependency checks, and manifest-based uninstall. Uninstall removes recorded package-owned paths by name, not by content hash; review the manifest or use a dry run before deleting anything. See the advanced details below for optional upstream dependencies and manifest behavior.
160
75
 
161
- 部分内置 skill 内容摘自 <https://github.com/mattpocock/skills>(MIT License,Copyright (c) 2026 Matt Pocock),详见 [`NOTICE`](./NOTICE)。
76
+ The package also ships a first-party `docs-html-view` skill (manual invocation only; distributed but not run at install). It renders PDR/Current/ADR docs to `docs/docsHtmlView/build/html` with Sphinx, MyST, and the stock RTD theme. After approval of packages, environment, and files, it uses `sphinx-quickstart` once and standard `make html` or `sphinx-build` thereafter. It never hooks, opens a browser, serves, deploys, or mutates source docs. `check` does not cover it.
162
77
 
163
- 上表中的 8 个上游依赖 skill 都可以改用完整版:安装时加 `--with-matt-skills` 即会从上游安装(见上文「可选:从上游安装『完整版』依赖 skill」)。
78
+ <details>
79
+ <summary>高级细节</summary>
164
80
 
165
- ## 版本说明
81
+ ### 可选上游依赖
166
82
 
167
- 本包版本 `2.x` 与「模板生成 v2」对齐:模板结构与生成约定以 v2 为准。
83
+ 本包默认安装自带的轻量依赖 skill。可以通过 `--with-matt-skills` 改为尝试从 `mattpocock/skills` 安装完整版;上游 `skills` CLI 需要 Node.js >= 22.20。没有传此选项时,只有在 stdin 和 stdout 都是 TTY、执行安装且不是 dry-run 时,才会询问一次是否安装上游版本,默认否;非 TTY 场景不询问。可以用 `--matt-agent <name>` 指定上游 CLI 的 agent,默认是 `codex`。
168
84
 
169
- ## License
85
+ 仅以下 8 个依赖名参与上游安装:`grilling`、`grill-me`、`grill-with-docs`、`domain-modeling`、`tdd`、`deepwork`、`retro`、`improve-codebase-architecture`。主 skill、`doc-opt` 和 `docs-html-view` 不属于上游集合。目标目录中已存在的同名 skill 路径会保留,不会提交给上游。安装器通过 `npx --yes skills@latest` 调用上游;如果 Node 版本不足、找不到 `npx`、上游命令失败或网络错误,会回退复制缺失项的轻量版本,包安装仍以退出码 0 结束。
170
86
 
171
- MIT,见 [`LICENSE`](./LICENSE)。
87
+ **上游安装目录注意:** 上游 CLI 使用 `-g -a <agent>`,安装到所选 agent 的全局 skills 目录,而不保证与 `--dir` 相同。安装器的安装后复核针对 `--dir`:两处目录不一致时,可能在 agent 全局目录安装完整版,同时在 `--dir` 下复制轻量版;`--dir` 中的清单只记录该目录里的项目,不记录独立的全局安装。这种错配既可能由自定义 `--dir` 引起,也可能由将 `--matt-agent` 改为其他 agent 引起。启用 `--with-matt-skills` 前,请确认 `--dir` 与所选 agent 的全局 skills 目录一致;否则建议使用默认轻量版。
172
88
 
173
- ---
89
+ ### 安装清单格式
174
90
 
175
- ## English summary
91
+ 清单合并记录每个 skill 名称的处理结果,不含内容哈希。字段包括包版本、目标目录、更新时间和按名称索引的条目;条目记录类型、操作以及上游 agent(适用时)。示例:
176
92
 
177
- `project-docs-workflow` is an npm-packaged agent **skill bundle** for project documentation workflows: issue tracking, ADRs, PDRs, a terminology glossary, and the workflow that ties them together. The main skill is self-contained via internal `references/deps/*.md` fallbacks, and ships with lightweight dependency skills (trimmed from [mattpocock/skills](https://github.com/mattpocock/skills), MIT, Matt Pocock) plus a bundled `doc-opt` bonus skill.
93
+ ```json
94
+ {
95
+ "version": "<package-version>",
96
+ "target": "/home/me/.agents/skills",
97
+ "updatedAt": "<iso-timestamp>",
98
+ "entries": {
99
+ "project-docs-workflow": { "kind": "main", "action": "installed", "at": "…" },
100
+ "tdd": { "kind": "lite", "action": "installed", "at": "…" },
101
+ "deepwork": { "kind": "lite", "action": "skipped", "at": "…" },
102
+ "retro": { "kind": "upstream", "action": "installed", "agent": "codex", "at": "…" }
103
+ }
104
+ }
105
+ ```
178
106
 
179
- Install with `npx @tanlinfeng/project-docs-workflow` (defaults to `~/.agents/skills`; options: `--dir <path>`, `--force`, `--dry-run`, `--with-matt-skills`, `--matt-agent <name>`, and the `check` / `uninstall` subcommands). The installer **never overwrites** an existing skill of the same name — it skips it, preserving your version. `--force` overwrites only the main `project-docs-workflow` skill.
107
+ `kind` 可为 `main`、`lite`、`doc-opt`、`docs-html-view` 或 `upstream`;`action` 可为 `installed`、`forced` 或 `skipped`。卸载按记录的路径/名称操作:`skipped` 以及未知状态的条目保留。上游项会尽量通过 `skills` CLI 批量移除;若 CLI 无法运行或返回失败,安装器会直接删除对应目录作为回退,这可能使 `~/.agents/.skill-lock.json` 留有过期记录。
180
108
 
181
- Every successful install writes an **install manifest** at `<target>/.project-docs-workflow.install.json` recording, per skill name, the `kind` (`main`/`lite`/`doc-opt`/`upstream`) and `action` (`installed`/`forced`/`skipped`), merged across re-installs. `uninstall [--dir <path>] [--dry-run] [--force]` removes **only** the names this package owns (`installed`/`forced`) and never touches names that pre-existed (`skipped`, decided by the record, not by guessing). On a TTY it asks once (`[y/N]`, default No); on a non-TTY it refuses unless `--force` is given. Lite/main/doc-opt copies are deleted directly; upstream full versions are removed via the `skills` CLI (`npx --yes skills@latest remove <names> -g -y -a <agent>`), falling back to direct deletion with a warning about a possibly-stale `~/.agents/.skill-lock.json` if delegation is impossible. `--dry-run` prints the plan and never prompts or writes; the manifest is deleted afterwards, the target directory itself is left in place.
109
+ ### 依赖解析与来源
182
110
 
183
- Optionally, `--with-matt-skills` installs the **full upstream** versions of the dependency skills from `mattpocock/skills` via the official `skills` CLI (`npx --yes skills@latest add mattpocock/skills -g -y -a <agent> -s ...`, requires Node >= 22.20). Without the flag, an interactive TTY gets a single prompt (default **No**); non-TTY/piped/CI runs never prompt. The 8 upstream-managed names are `grilling`, `grill-me`, `grill-with-docs`, `domain-modeling`, `tdd`, `deepwork`, `retro`, `improve-codebase-architecture`; `doc-opt` and the main skill are unaffected. Any name already present is left untouched and never sent upstream, and the installer re-checks afterwards, falling back to the bundled lite copy for anything still missing. If upstream can't run (Node too old, no `npx`, non-zero exit, network error), it warns and falls back — the install still exits 0. Upstream installs into the agent's global dir (`codex` → `~/.agents/skills`), so it cannot follow a custom `--dir` (a warning is printed).
111
+ `check` 逐项检查依赖是否能通过目标目录中同名 skill 的 `SKILL.md` 解析,或由主 skill 内置的 `references/deps/*.md` 兜底。每项至少有一种解析方式即通过;这不是主 skill 安装检查,也不是项目初始化检查。
184
112
 
185
- You can also copy `skills/*` directly into your skills root (skip any existing directory name, never overwrite); copying just `skills/project-docs-workflow` works standalone thanks to the internal ref fallbacks.
113
+ 内置轻量依赖来自 [mattpocock/skills](https://github.com/mattpocock/skills)(MIT,Matt Pocock);`doc-opt` 与 `docs-html-view` 是本包附带的第一方 skill。主 skill 对引用能力先按名解析已安装 skill,不存在时使用内部 ref 兜底。详见 [`NOTICE`](./NOTICE)。
186
114
 
187
- Resolution is two-level: use an installed skill by name if present, otherwise follow `references/deps/*.md` inside the skill. Version `2.x` aligns with template generation v2. Licensed MIT (see `LICENSE` and `NOTICE`).
115
+ </details>
188
116
 
189
- **Examples**
117
+ ## License
190
118
 
191
- ```bash
192
- npx @tanlinfeng/project-docs-workflow # install (default ~/.agents/skills)
193
- npx @tanlinfeng/project-docs-workflow --dir <path> --dry-run
194
- npx @tanlinfeng/project-docs-workflow --with-matt-skills # full upstream deps (Node >= 22.20)
195
- npx @tanlinfeng/project-docs-workflow check
196
- npx @tanlinfeng/project-docs-workflow uninstall --dry-run
197
- npx @tanlinfeng/project-docs-workflow uninstall --force # only removes what this package installed
198
- ```
119
+ MIT,见 [`LICENSE`](./LICENSE)。
package/bin/install.js CHANGED
@@ -21,7 +21,7 @@ const UPSTREAM_REPO = "mattpocock/skills";
21
21
  const UPSTREAM_DEFAULT_AGENT = "codex";
22
22
  // 上游安装器要求 Node >= 22.20(本项目不引入 semver 依赖,手工比较 major/minor)。
23
23
  const UPSTREAM_MIN_NODE = { major: 22, minor: 20 };
24
- // 仅这 8 个依赖 skill 属于上游集合;doc-opt 与主 skill 不属于。
24
+ // 仅这 8 个依赖 skill 属于上游集合;doc-opt、docs-html-view 与主 skill 不属于。
25
25
  const MATT_DEP_NAMES = [
26
26
  "grilling",
27
27
  "grill-me",
@@ -343,9 +343,15 @@ function removeManifest(target) {
343
343
  }
344
344
  }
345
345
 
346
- // 依赖 skill 的 kind:doc-opt 单列,其余依赖(含其它 skills/* 目录)视为 lite。
346
+ // 依赖 skill 的 kind:本包附带的第一方 skill 各用独立 kind(doc-opt、docs-html-view),
347
+ // 其余非上游目录视为轻量依赖 lite;这样不会把第一方 skill 误记为 lite(上游轻量)归属。
348
+ const FIRST_PARTY_DEP_KINDS = {
349
+ "doc-opt": "doc-opt",
350
+ "docs-html-view": "docs-html-view",
351
+ };
352
+
347
353
  function depKind(name) {
348
- return name === "doc-opt" ? "doc-opt" : "lite";
354
+ return FIRST_PARTY_DEP_KINDS[name] || "lite";
349
355
  }
350
356
 
351
357
  // —— 上游卸载命令组装 ——
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tanlinfeng/project-docs-workflow",
3
- "version": "2.2.0",
3
+ "version": "2.3.0",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -0,0 +1,119 @@
1
+ ---
2
+ name: docs-html-view
3
+ description: Render a project's PDR / Current / ADR docs to local HTML with Sphinx + MyST (stock RTD theme), after a read-only inspection and explicit approval.
4
+ disable-model-invocation: true
5
+ argument-hint: "[project root]"
6
+ ---
7
+
8
+ # Docs HTML View
9
+
10
+ Render the human-readable documentation trees of a project (`docs/pdr`, `docs/current`, `docs/adr`) into a local HTML site using **standard Sphinx** with MyST (`myst_parser`) and the **stock** `sphinx_rtd_theme`. There is no custom renderer: scaffolding uses `sphinx-quickstart`, navigation is a small generated-toctree helper, and the build is `make html` (or a plain `sphinx-build`). The output is a local folder you open yourself; nothing is served or deployed.
11
+
12
+ This skill is manually triggered and never automatic, and it never fetches anything at build time. It may install the pinned render dependencies, but only after your explicit approval. Inspect read-only, ask for approval **once**, then first-time setup and repeat manual renders run without re-asking — until a setting changes.
13
+
14
+ ## Boundaries
15
+
16
+ - **User-triggered only.** Never install, scaffold, or build while editing docs.
17
+ - **Bounded writes.** The approved set is: `docs/docsHtmlView/` (config, generated navigation, build output), the approved render environment directory (e.g. `.venv-docs/`), and any `.gitignore` additions you approved. Rendering reads `docs/pdr`, `docs/current`, `docs/adr` and never rewrites them, and it never writes into source trees.
18
+ - **Approved trees only.** Default trees are `pdr`, `current`, `adr`; a tree may be extended explicitly in the config file, never auto-expanded. `docs/issues`, `docs/agents`, tooling, and build output are excluded. A missing or empty tree is reported, not created. No documents in the approved trees → **stop: do not create `docs/docsHtmlView/`, do not scaffold, do not build.**
19
+ - **Stock theme.** No custom UI/design work.
20
+ - **No invented links.** Navigation is generated from the real tree; no cross-tree associations are added and no document bodies are rewritten.
21
+ - **No browser/server/deploy.** Producing HTML is the end. README/local output only.
22
+
23
+ ## Flow
24
+
25
+ Run from the project root (`cwd`). Define the skill directory and, after approval, the prepared environment's absolute paths once — `DOCS_PY` is the venv **python**, `DOCS_SPHINXBUILD` the venv **sphinx-build**:
26
+
27
+ ```bash
28
+ SKILL="${SKILL:-$HOME/.agents/skills/docs-html-view}"
29
+ SITE_TITLE="APPROVED TITLE" # the confirmed project/site title
30
+ DOCS_PY="$PWD/.venv-docs/bin/python" # or the project's existing docs env
31
+ DOCS_SPHINXBUILD="$PWD/.venv-docs/bin/sphinx-build" # same env
32
+ ```
33
+
34
+ 1. **Inspect (read-only).** Use normal read-only tools (`ls`, `read`, `grep`) to learn the project: which of `docs/pdr`, `docs/current`, `docs/adr` exist and contain documents, whether `docs/docsHtmlView/` already exists, which Python/venv exists, and the real project/site title. **Never execute the project's interpreter or `conf.py` before approval.** If no approved tree contains documents, stop here — there is nothing to render. Report the plan: pins, environment path, files created, `.gitignore` additions, output path, and title.
35
+
36
+ 2. **Ask once.** Get explicit approval **before any write, install, or scaffold**. Cover concretely: packages/pins (`requirements.txt`), where the environment lives, the pip command, the files created, the `.gitignore` additions you propose (e.g. `.venv-docs/`, `docs/docsHtmlView/build/`), the build/output paths (`docs/docsHtmlView/build/html`), and the confirmed site title — `docsHtmlView` is a directory name, not the site name.
37
+ - Within the approved scope, repeated manual rendering does **not** re-ask.
38
+ - Dependency upgrades, overwriting existing files, or changing the approved root/range **do** require a fresh ask.
39
+
40
+ 3. **Prepare the environment (approved only).** Prefer an existing project docs environment; otherwise a project-root `.venv-docs`. No global/system installs, no automatic `uv`/Python installation. If no suitable Python exists, stop and tell the user. The helper is stdlib-only, so run it with any working Python; then point `DOCS_PY`/`DOCS_SPHINXBUILD` at the environment it prepared:
41
+
42
+ ```bash
43
+ python3 "$SKILL"/scripts/prepare_env.py --project-root . --venv .venv-docs
44
+ python3 "$SKILL"/scripts/prepare_env.py --project-root . --venv .venv-docs --install
45
+ ```
46
+
47
+ The helper only creates/reuses a recognized venv (a directory with `pyvenv.cfg`; it refuses to clobber a non-venv) and installs the pinned requirements only when `--install` is passed (after your approval). It never runs on ordinary doc edits, and distributing the skill package installs nothing.
48
+
49
+ 4. **First-time setup (approved, only when `docs/docsHtmlView/` does not exist).** The approved trees already exist, so only the workspace dir is created — **after** the inspection confirmed documents exist and you approved:
50
+
51
+ ```bash
52
+ mkdir -p docs/docsHtmlView
53
+ ( cd docs/docsHtmlView && "$DOCS_PY" -m sphinx.cmd.quickstart -q -p "$SITE_TITLE" -a "AUTHOR" -v "1.0" --no-sep )
54
+ ```
55
+
56
+ Then, **only for the newly generated config**:
57
+ - replace the generated `conf.py` using `assets/conf.py.template`; the template leaves `__SITE_TITLE__` / `__AUTHOR__` / `__RELEASE__` **unquoted**, so use `template_text.replace(token, repr(value))` for each token. Never interpolate raw user text unescaped into executable config source;
58
+ - copy `assets/guard.py` to `docs/docsHtmlView/guard.py`;
59
+ - remove the quickstart's generated `index.rst` (the landing page is generated `index.md`);
60
+ - edit the generated `Makefile`: set `SOURCEDIR = ..` and `BUILDDIR = build` (replace the generated `SOURCEDIR = .` / `BUILDDIR = _build`), and either change the generated `SPHINXOPTS ?=` line to `SPHINXOPTS = -c . -w build/warnings.log` or pass it on the `make` command line (below). Optionally remove the now-unused generated `_build/`. (`make.bat` may be edited the same way; not required on Linux/macOS.)
61
+
62
+ If `docs/docsHtmlView/` already exists, **do not overwrite**: present an explicit manual merge plan and let the user apply it. Re-running setup never re-scaffolds.
63
+
64
+ 5. **Generate navigation (before every build).** The helper writes only marked files under the workspace, exits non-zero if no approved tree has documents, and never runs Sphinx:
65
+
66
+ ```bash
67
+ "$DOCS_PY" "$SKILL"/scripts/gen_nav.py --project-root . --title "$SITE_TITLE"
68
+ ```
69
+
70
+ Always pass the approved title (or set `site_title` in the config); without it, the site title silently defaults to `Documentation`. Repeat renders reuse the same agreed `SITE_TITLE`, without re-asking or switching titles.
71
+
72
+ 6. **Build (repeatable without re-approval).**
73
+
74
+ ```bash
75
+ mkdir -p docs/docsHtmlView/build
76
+ ( cd docs/docsHtmlView && make html SPHINXBUILD="$DOCS_SPHINXBUILD" SPHINXOPTS='-c . -w build/warnings.log' )
77
+ ```
78
+
79
+ If `make` is unavailable, use the same paths explicitly:
80
+
81
+ ```bash
82
+ mkdir -p docs/docsHtmlView/build
83
+ ( cd docs/docsHtmlView && "$DOCS_SPHINXBUILD" -b html -c . -d build/doctrees -w build/warnings.log .. build/html )
84
+ ```
85
+
86
+ Output lands under `docs/docsHtmlView/build/html`; because `root_doc = docsHtmlView/index`, the **landing page is `docs/docsHtmlView/build/html/docsHtmlView/index.html`** (open that file). The `build/html/` directory layout itself is unchanged — no redirect or extra index page is generated. Warnings go to `docs/docsHtmlView/build/warnings.log`. The `build/` dir is created only after approval. Source-doc edits are legitimate rebuilds. A change to an approved setting (roots, title, environment) requires a fresh ask. Do not wrap `make`/`sphinx-build` in another script.
87
+
88
+ **Changing the approved trees is a manual, approved edit.** If `allowed_dirs` changes, update all three together by hand: `include_patterns` and `docs_html_view_allowed_roots` in `docs/docsHtmlView/conf.py`, and `allowed_dirs` in `docs/docsHtmlView/docs-html-view.config.json`; then re-run `gen_nav`. There is no automatic sync, and a mismatch must be resolved before `make`. A missing/empty tree is fine (reported, skipped).
89
+
90
+ 7. **Report.** Summarise the normal Sphinx result — see Statuses below.
91
+
92
+ ## Safety model
93
+
94
+ The boundary is a small template Sphinx extension (`assets/guard.py`), not a build orchestrator. It is a best-effort guard for **trusted** project docs, not a universal sandbox; the project's own `conf.py` remains executable Python.
95
+
96
+ - **File insertion blocked at parse time:** `include`, `literalinclude`, and `raw` directives are replaced by a refusing directive (case-insensitive, so `INCLUDE` too; also reached through `eval-rst` and MyST colon fences). Active/raw HTML is refused; only pure HTML-comment (plus whitespace) raw nodes are allowed, so the harmless `<!-- ... -->` comments in the PDR templates render — but no comment+markup smuggling. No regex-only scan is relied on.
97
+ - **Local assets checked before copy:** at `doctree-read` (priority 100, before Sphinx's asset collectors copy anything) every parsed image and download target is resolved with the environment's reference resolver and must resolve (symlinks included) inside an approved root, or the build fails. Reference-style images, `:download:`, and uppercase directives are covered.
98
+ - **Remote assets are never fetched:** remote images/downloads are replaced with an inline placeholder and a Sphinx warning is emitted, so a clean build is not reported as full fidelity.
99
+ - **Navigation is generated, not authored:** `gen_nav.py` writes only marked files (`% docs-html-view: generated navigation; do not edit`, a MyST comment that renders as no content), sanitizes generated titles (no `<`, `>`, backticks, braces, brackets, pipes, or newlines), and runs a full preflight — approved dirs, source trees, and any existing `nav/` (including ancestors) are checked for symlinks, and every output/collision is validated before the first write, so a refusal leaves the tree unchanged. It removes only its own marked stale nav files and never writes into source trees.
100
+
101
+ Approved scope that links out to excluded docs produces ordinary Sphinx "missing reference" warnings; those are reported, never auto-added.
102
+
103
+ ## Statuses
104
+
105
+ Read the normal Sphinx exit code and the warning log (`build/warnings.log`):
106
+
107
+ - **clean success** — build exited 0 and the warning log is empty.
108
+ - **preview with warnings** — build exited 0 but the warning log has entries; report the first warnings and the log path.
109
+ - **failure** — `gen_nav.py` refused (or exited 1 with no documents), `guard.py` rejected the build, or Sphinx exited non-zero. A failed `make` may leave stale or partial output under `build/`; state that clearly and do not present it as a fresh build. There is no publisher step: whatever `make` left is what exists.
110
+
111
+ ## Limitations
112
+
113
+ - Supported sources are `.md` and `.rst` under the approved trees; `.mdx` and other extensions are not rendered.
114
+ - `include` / `literalinclude` / `raw` are refused; this is not an arbitrary-Markdown renderer.
115
+ - Remote images/downloads become inline placeholders plus a Sphinx warning; active HTML is refused, so "no browser network" is not claimed for arbitrary markup.
116
+ - The guard is a trusted-doc guardrail, not a sandbox for untrusted input.
117
+ - The dependency helper installs only the pinned requirements, only with `--install`, and only after consent; it never installs Python or uv.
118
+
119
+ See `references/technical.md` for the exact commands, template apply steps, config keys, and navigation rules.
@@ -0,0 +1,5 @@
1
+ interface:
2
+ display_name: "Docs HTML View"
3
+ short_description: "Render PDR/Current/ADR docs to local HTML after approval"
4
+ policy:
5
+ allow_implicit_invocation: false
@@ -0,0 +1,44 @@
1
+ # docs-html-view managed Sphinx config (template).
2
+ #
3
+ # Apply only to the conf.py that sphinx-quickstart just generated for a NEW site:
4
+ # copy this file over docs/docsHtmlView/conf.py and replace the __TOKENS__ below,
5
+ # then copy assets/guard.py to docs/docsHtmlView/guard.py. For an existing site,
6
+ # do not overwrite: show the user a manual merge plan first.
7
+ import os
8
+ import sys
9
+
10
+ sys.path.insert(0, os.path.abspath(os.path.dirname(__file__)))
11
+
12
+ project = __SITE_TITLE__
13
+ author = __AUTHOR__
14
+ release = __RELEASE__
15
+
16
+ extensions = ["myst_parser", "guard"]
17
+ html_theme = "sphinx_rtd_theme"
18
+ html_theme_options = {}
19
+
20
+ source_suffix = {".md": "markdown", ".rst": "restructuredtext"}
21
+ root_doc = "docsHtmlView/index"
22
+
23
+ # Enumerate exactly: landing page, generated navigation, and the approved trees.
24
+ # Deliberately NOT a docsHtmlView/** wildcard, so foreign workspace files are ignored.
25
+ include_patterns = [
26
+ "docsHtmlView/index.md",
27
+ "docsHtmlView/nav/**",
28
+ "pdr/**",
29
+ "current/**",
30
+ "adr/**",
31
+ ]
32
+
33
+ exclude_patterns = [
34
+ "docsHtmlView/build/**",
35
+ "docsHtmlView/.venv*/**",
36
+ ".venv-docs/**",
37
+ "**/.git/**",
38
+ ]
39
+
40
+ myst_enable_extensions = ["colon_fence", "deflist", "fieldlist"]
41
+ myst_heading_anchors = 3
42
+
43
+ # Read by guard.py; must list the same roots as include_patterns above.
44
+ docs_html_view_allowed_roots = ["pdr", "current", "adr"]
@@ -0,0 +1,128 @@
1
+ """docs-html-view guard: a best-effort parse/render guard for trusted project docs.
2
+
3
+ This is a trusted-document renderer, not a universal sandbox. It blocks
4
+ file-insertion directives, refuses active/raw HTML (allowing only HTML comments),
5
+ and checks local image/download references against the approved roots at
6
+ ``doctree-read`` (before Sphinx's asset collectors copy anything). Remote
7
+ images/downloads are never fetched. The project's own ``conf.py`` remains
8
+ executable Python.
9
+ """
10
+ from __future__ import annotations
11
+
12
+ import os
13
+ import re
14
+ from urllib.parse import urlparse
15
+
16
+ from docutils import nodes
17
+ from docutils.parsers.rst import Directive, directives
18
+ from sphinx import addnodes
19
+ from sphinx.errors import SphinxError
20
+ from sphinx.util import logging
21
+
22
+ logger = logging.getLogger(__name__)
23
+
24
+ BLOCKED = ("include", "literalinclude", "raw")
25
+ # A raw node is allowed only when it is entirely HTML comment(s) and whitespace.
26
+ # This permits the harmless `<!-- ... -->` comments used in the PDR templates while
27
+ # still refusing any active markup (no comment+script smuggling).
28
+ _COMMENT_ONLY = re.compile(r"\s*(?:<!--.*?-->\s*)*", re.DOTALL)
29
+
30
+
31
+ class _Blocked(Directive):
32
+ has_content = True
33
+ optional_arguments = 1
34
+ final_argument_whitespace = True
35
+
36
+ def run(self):
37
+ raise SphinxError("docs-html-view: '%s' directive is not allowed" % self.name)
38
+
39
+
40
+ def _block_directives(app):
41
+ # Registered after extensions load; docutils lowercases names, so this also
42
+ # catches INCLUDE and directives reached through eval-rst or colon fences.
43
+ for name in BLOCKED:
44
+ directives.register_directive(name, _Blocked)
45
+
46
+
47
+ def _validate_roots(app):
48
+ """Every allowed root must be a simple relative name that resolves inside docs."""
49
+ srcdir_real = os.path.realpath(app.srcdir)
50
+ for name in app.config.docs_html_view_allowed_roots:
51
+ if not isinstance(name, str) or not name or os.path.isabs(name) or "\\" in name:
52
+ raise SphinxError("docs-html-view: invalid allowed root %r" % (name,))
53
+ parts = name.split("/")
54
+ if any(part in ("", ".", "..") for part in parts):
55
+ raise SphinxError("docs-html-view: allowed root must be a simple relative name: %r" % (name,))
56
+ root_real = os.path.realpath(os.path.join(app.srcdir, name))
57
+ try:
58
+ inside = os.path.commonpath([root_real, srcdir_real]) == srcdir_real
59
+ except ValueError:
60
+ inside = False
61
+ if not inside:
62
+ raise SphinxError("docs-html-view: allowed root escapes docs: %r" % (name,))
63
+
64
+
65
+ def _remote(uri: str) -> bool:
66
+ return bool(urlparse(uri).scheme) or uri.startswith("//")
67
+
68
+
69
+ def _within(path: str, roots) -> bool:
70
+ real = os.path.realpath(path)
71
+ for root in roots:
72
+ root_real = os.path.realpath(root)
73
+ try:
74
+ if os.path.commonpath([real, root_real]) == root_real:
75
+ return True
76
+ except ValueError:
77
+ continue
78
+ return False
79
+
80
+
81
+ def _guard_assets(app, doctree):
82
+ docname = app.env.docname
83
+ roots = [os.path.join(app.srcdir, r) for r in app.config.docs_html_view_allowed_roots]
84
+
85
+ for node in doctree.findall(nodes.raw):
86
+ if not _COMMENT_ONLY.fullmatch(node.astext() or ""):
87
+ raise SphinxError("docs-html-view: active/raw HTML is not allowed in %s" % docname)
88
+
89
+ def check(uri, kind):
90
+ """True if usable locally, False if remote; raises when local-but-outside."""
91
+ if not uri:
92
+ return True
93
+ if _remote(uri):
94
+ return False
95
+ clean = uri.split("#", 1)[0]
96
+ try:
97
+ _rel, abspath = app.env.relfn2path(clean, docname)
98
+ except Exception:
99
+ abspath = None
100
+ if abspath is None or not _within(abspath, roots):
101
+ raise SphinxError(
102
+ "docs-html-view: %s outside approved roots in %s: %s" % (kind, docname, uri)
103
+ )
104
+ return True
105
+
106
+ for node in doctree.findall(nodes.image):
107
+ uri = node.get("uri", "")
108
+ if check(uri, "image"):
109
+ continue
110
+ logger.warning("docs-html-view: remote image not fetched: %s", uri, location=docname)
111
+ node.replace_self(nodes.emphasis("", "[remote image not fetched] " + uri))
112
+
113
+ for node in doctree.findall(addnodes.download_reference):
114
+ uri = node.get("reftarget") or node.get("refuri") or ""
115
+ if check(uri, "download"):
116
+ continue
117
+ logger.warning("docs-html-view: remote download not fetched: %s", uri, location=docname)
118
+ node.replace_self(nodes.emphasis("", "[remote download not fetched] " + uri))
119
+
120
+
121
+ def setup(app):
122
+ app.add_config_value("docs_html_view_allowed_roots", [], "env")
123
+ app.connect("builder-inited", _block_directives)
124
+ app.connect("builder-inited", _validate_roots)
125
+ # doctree-read (priority 100) so we run before Sphinx's asset collectors (500),
126
+ # which copy downloads during read and register images for copy.
127
+ app.connect("doctree-read", _guard_assets, priority=100)
128
+ return {"parallel_read_safe": True, "parallel_write_safe": True}