@tanlinfeng/project-docs-workflow 2.2.0 → 3.0.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/NOTICE +8 -10
- package/README.md +97 -143
- package/bin/install.js +731 -139
- package/package.json +2 -2
- package/skills/docs-html-view/SKILL.md +119 -0
- package/skills/docs-html-view/agents/openai.yaml +5 -0
- package/skills/docs-html-view/assets/conf.py.template +44 -0
- package/skills/docs-html-view/assets/guard.py +128 -0
- package/skills/docs-html-view/references/technical.md +151 -0
- package/skills/docs-html-view/requirements.txt +5 -0
- package/skills/docs-html-view/scripts/gen_nav.py +306 -0
- package/skills/docs-html-view/scripts/prepare_env.py +154 -0
- package/skills/project-docs-workflow/SKILL.md +27 -12
- package/skills/project-docs-workflow/assets/templates/entries/current-module.md +19 -0
- package/skills/project-docs-workflow/assets/templates/entries/pdr-spec.md +1 -1
- package/skills/project-docs-workflow/assets/templates/project/adr-readme.md +2 -1
- package/skills/project-docs-workflow/assets/templates/project/agents-block.md +1 -1
- package/skills/project-docs-workflow/assets/templates/project/agents-domain.md +3 -3
- package/skills/project-docs-workflow/assets/templates/project/agents-issues.md +6 -5
- package/skills/project-docs-workflow/assets/templates/project/agents-workflow.md +9 -6
- package/skills/project-docs-workflow/assets/templates/project/current-00-readme.md +42 -0
- package/skills/project-docs-workflow/assets/templates/project/pdr-00-readme.md +6 -7
- package/skills/project-docs-workflow/references/adr.md +5 -1
- package/skills/project-docs-workflow/references/coexistence.md +8 -6
- package/skills/project-docs-workflow/references/current.md +39 -0
- package/skills/project-docs-workflow/references/deps/boundaries.md +4 -2
- package/skills/project-docs-workflow/references/deps/domain-modeling.md +1 -1
- package/skills/project-docs-workflow/references/deps/grilling.md +3 -1
- package/skills/project-docs-workflow/references/deps/tdd.md +2 -2
- package/skills/project-docs-workflow/references/issues.md +3 -2
- package/skills/project-docs-workflow/references/pdr.md +15 -10
- package/skills/project-docs-workflow/references/rendering.md +29 -0
- package/skills/project-docs-workflow/references/workflow.md +6 -5
- package/skills/deepwork/SKILL.md +0 -15
- package/skills/domain-modeling/SKILL.md +0 -83
- package/skills/domain-modeling/agents/openai.yaml +0 -3
- package/skills/grill-me/SKILL.md +0 -13
- package/skills/grill-with-docs/SKILL.md +0 -18
- package/skills/grilling/SKILL.md +0 -46
- package/skills/grilling/agents/openai.yaml +0 -3
- package/skills/improve-codebase-architecture/SKILL.md +0 -15
- package/skills/project-docs-workflow/assets/templates/entries/pdr-current.md +0 -19
- package/skills/retro/SKILL.md +0 -19
- package/skills/tdd/SKILL.md +0 -50
- package/skills/tdd/agents/openai.yaml +0 -3
package/package.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tanlinfeng/project-docs-workflow",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "3.0.0",
|
|
4
4
|
"publishConfig": {
|
|
5
5
|
"access": "public"
|
|
6
6
|
},
|
|
7
|
-
"description": "项目文档工作流 skill 包:提供 issue tracker / ADR / PDR /
|
|
7
|
+
"description": "项目文档工作流 skill 包:提供 issue tracker / ADR / PDR / 术语表与工作流约定;随包分发主 skill 与 doc-opt / docs-html-view,内置依赖回退 ref(不随包分发独立依赖 skill),可独立使用。",
|
|
8
8
|
"license": "MIT",
|
|
9
9
|
"bin": {
|
|
10
10
|
"project-docs-workflow": "bin/install.js"
|
|
@@ -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,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}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
# docs-html-view technical reference
|
|
2
|
+
|
|
3
|
+
## Pinned dependencies
|
|
4
|
+
|
|
5
|
+
Verified on PyPI 2026-10-14; the set is mutually compatible. Sphinx 9.1.0 requires Python >= 3.12.
|
|
6
|
+
|
|
7
|
+
| Package | Pin | Note |
|
|
8
|
+
| :--- | :--- | :--- |
|
|
9
|
+
| Sphinx | `Sphinx==9.1.0` | needs Python >= 3.12 |
|
|
10
|
+
| MyST parser | `myst-parser==5.1.0` | Markdown support |
|
|
11
|
+
| RTD theme | `sphinx-rtd-theme==3.1.0` | stock theme via `html_theme` |
|
|
12
|
+
|
|
13
|
+
`requirements.txt` ships this declaration. `prepare_env.py` installs nothing unless `--install` is passed; the caller runs it only after approval.
|
|
14
|
+
|
|
15
|
+
## Layout
|
|
16
|
+
|
|
17
|
+
```
|
|
18
|
+
<project root>/
|
|
19
|
+
.venv-docs/ # default render venv (or the project's docs env)
|
|
20
|
+
docs/ # Sphinx source root
|
|
21
|
+
pdr|current|adr/ # approved trees, never modified
|
|
22
|
+
issues/ agents/ ... # excluded
|
|
23
|
+
docsHtmlView/ # workspace
|
|
24
|
+
conf.py # from assets/conf.py.template
|
|
25
|
+
guard.py # copied from assets/guard.py
|
|
26
|
+
index.md # generated landing page (marked)
|
|
27
|
+
nav/<tree>/**/index.md # generated toctrees (marked)
|
|
28
|
+
docs-html-view.config.json # optional, user-authored
|
|
29
|
+
build/html/docsHtmlView/index.html # landing page (root_doc = docsHtmlView/index)
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
`docsHtmlView` is the workspace directory name, not the site name.
|
|
33
|
+
|
|
34
|
+
## Files shipped by this skill
|
|
35
|
+
|
|
36
|
+
- `scripts/prepare_env.py` — create/reuse a recognized venv; optional pinned install / freeze. Stdlib only.
|
|
37
|
+
- `scripts/gen_nav.py` — generate `index.md` + `nav/**` toctrees (marked files only).
|
|
38
|
+
- `assets/conf.py.template` — the managed `conf.py` for a new site.
|
|
39
|
+
- `assets/guard.py` — the parse/render guard extension.
|
|
40
|
+
- `requirements.txt` — the pins.
|
|
41
|
+
|
|
42
|
+
## Commands
|
|
43
|
+
|
|
44
|
+
Inspect is done with normal read-only tools; there is no inspect subcommand and nothing runs the project interpreter before approval. After approval, define the prepared environment's absolute paths from the project root (override `--venv` to reuse a project docs env):
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
SKILL="${SKILL:-$HOME/.agents/skills/docs-html-view}"
|
|
48
|
+
SITE_TITLE="APPROVED TITLE" # the confirmed project/site title
|
|
49
|
+
DOCS_PY="$PWD/.venv-docs/bin/python" # venv python
|
|
50
|
+
DOCS_SPHINXBUILD="$PWD/.venv-docs/bin/sphinx-build" # venv sphinx-build
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
First-time setup (new site only; skip if `docs/docsHtmlView/` already exists and instead show a manual merge plan):
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
python3 "$SKILL"/scripts/prepare_env.py --project-root . --venv .venv-docs
|
|
57
|
+
python3 "$SKILL"/scripts/prepare_env.py --project-root . --venv .venv-docs --install
|
|
58
|
+
mkdir -p docs/docsHtmlView
|
|
59
|
+
( cd docs/docsHtmlView && "$DOCS_PY" -m sphinx.cmd.quickstart -q -p "$SITE_TITLE" -a "AUTHOR" -v "1.0" --no-sep )
|
|
60
|
+
# apply assets/conf.py.template -> docs/docsHtmlView/conf.py
|
|
61
|
+
# tokens are UNQUOTED; replace __SITE_TITLE__/__AUTHOR__/__RELEASE__ with repr(value)
|
|
62
|
+
# copy assets/guard.py -> docs/docsHtmlView/guard.py
|
|
63
|
+
# remove the generated docs/docsHtmlView/index.rst
|
|
64
|
+
# Makefile: SOURCEDIR = .. BUILDDIR = build SPHINXOPTS = -c . -w build/warnings.log
|
|
65
|
+
"$DOCS_PY" "$SKILL"/scripts/gen_nav.py --project-root . --title "$SITE_TITLE"
|
|
66
|
+
mkdir -p docs/docsHtmlView/build
|
|
67
|
+
( cd docs/docsHtmlView && make html SPHINXBUILD="$DOCS_SPHINXBUILD" SPHINXOPTS='-c . -w build/warnings.log' )
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Repeat render (same approved scope and title, no re-ask):
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
"$DOCS_PY" "$SKILL"/scripts/gen_nav.py --project-root . --title "$SITE_TITLE"
|
|
74
|
+
mkdir -p docs/docsHtmlView/build
|
|
75
|
+
( cd docs/docsHtmlView && make html SPHINXBUILD="$DOCS_SPHINXBUILD" SPHINXOPTS='-c . -w build/warnings.log' )
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
No `make`:
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
mkdir -p docs/docsHtmlView/build
|
|
82
|
+
( cd docs/docsHtmlView && "$DOCS_SPHINXBUILD" -b html -c . -d build/doctrees -w build/warnings.log .. build/html )
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
`gen_nav.py` exits `1` when no approved tree contains documents, so a later `make` cannot silently build a stale site. Warnings (missing/empty trees) go to stderr; `--json` prints the summary on stdout. Without `--title` the site title defaults to `Documentation`; pass the approved `--title` (or set `site_title`) so repeat renders keep the agreed title.
|
|
86
|
+
|
|
87
|
+
Optional resolved-version snapshot (approved output):
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
python3 "$SKILL"/scripts/prepare_env.py --project-root . --venv .venv-docs \
|
|
91
|
+
--freeze docs/docsHtmlView/requirements.lock.txt
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
`prepare_env.py` flags: `--project-root` (default cwd), `--venv` (default `.venv-docs`, relative to project root unless absolute), `--base-python` (interpreter used to create the venv), `--requirements`, `--install`, `--freeze PATH`. It refuses to reuse a path that exists without `pyvenv.cfg`, refuses Python < 3.12, and refuses `--install`/`--freeze` unless the interpreter actually belongs to the target venv. It never installs Python/uv and never uses global/system pip.
|
|
95
|
+
|
|
96
|
+
`gen_nav.py` flags: `--project-root` (default cwd), `--title` (site title override), `--json`.
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
## Sphinx configuration
|
|
100
|
+
|
|
101
|
+
`SOURCEDIR=..`, config dir `docs/docsHtmlView` (`-c .`), `BUILDDIR=build` → output `build/html`; `SPHINXOPTS = -c . -w build/warnings.log` (create `build/` first) so warnings are captured for reporting. `root_doc = docsHtmlView/index`, so the landing page is `build/html/docsHtmlView/index.html` (the `build/html/` layout is otherwise unchanged; no redirect or extra page).
|
|
102
|
+
|
|
103
|
+
`conf.py` uses:
|
|
104
|
+
|
|
105
|
+
- `include_patterns` — **enumerated**: `docsHtmlView/index.md`, `docsHtmlView/nav/**`, and one `<root>/**` per approved tree. Not a `docsHtmlView/**` wildcard.
|
|
106
|
+
- `exclude_patterns` — build, `.venv-docs`, workspace venvs, VCS internals.
|
|
107
|
+
- `myst_enable_extensions = ["colon_fence", "deflist", "fieldlist"]`, `myst_heading_anchors = 3`.
|
|
108
|
+
- `docs_html_view_allowed_roots` — the same roots, read by `guard.py`.
|
|
109
|
+
|
|
110
|
+
**Changing `allowed_dirs` is a manual, approved edit with no automatic sync.** Update all three by hand — `include_patterns` and `docs_html_view_allowed_roots` in `conf.py`, and `allowed_dirs` in `docs-html-view.config.json` — then re-run `gen_nav.py`; a mismatch between them must be resolved before `make`. Missing/empty trees are fine (reported and skipped).
|
|
111
|
+
|
|
112
|
+
### Guard (`assets/guard.py`)
|
|
113
|
+
|
|
114
|
+
- `builder-inited`: registers a refusing directive for `include`, `literalinclude`, `raw` (case-insensitive; also hit via `eval-rst` and colon fences), and validates each allowed root is a simple relative name resolving inside `docs`.
|
|
115
|
+
- `doctree-read` (priority 100, before Sphinx's asset collectors at 500): rejects any `raw` node that is not purely HTML comment(s)+whitespace (so PDR-template comments pass, active markup fails); for images and `download_reference` nodes, resolves the original reference via `app.env.relfn2path`, requires it inside an approved root using `realpath`/`commonpath` (symlinks rejected), or raises `SphinxError` before any copy; remote targets are replaced with an inline placeholder and a Sphinx warning, never fetched.
|
|
116
|
+
- This is the authoritative boundary and the reason no regex-only scan is used. It guards trusted docs; `conf.py` is trusted executable code and is not sandboxed.
|
|
117
|
+
|
|
118
|
+
## Optional project config
|
|
119
|
+
|
|
120
|
+
`docs/docsHtmlView/docs-html-view.config.json` is user-authored and never overwritten. It must be a JSON object; a parse error, a non-object, a non-string `site_title`, a non-string `navigation_order`, or an unsafe `allowed_dirs` entry is refused (not silently dropped).
|
|
121
|
+
|
|
122
|
+
| Key | Read by | Effect |
|
|
123
|
+
| :--- | :--- | :--- |
|
|
124
|
+
| `site_title` | `gen_nav.py` | site title (also settable via `gen_nav.py --title`) |
|
|
125
|
+
| `allowed_dirs` | `gen_nav.py` | explicit trees; one directory name each (no separators, not hidden, not `build`/`docsHtmlView`); defaults to `pdr`, `current`, `adr` |
|
|
126
|
+
| `navigation_order` | `gen_nav.py` | list of path strings relative to `docs/` placed first, in order; then numeric prefix, then case-insensitive name |
|
|
127
|
+
| `author`, `release` | human / agent only | template token values when applying `conf.py.template`; not read by the scripts |
|
|
128
|
+
| `python` | human / agent only | preferred interpreter when choosing the venv path; not read by the scripts |
|
|
129
|
+
|
|
130
|
+
## Navigation rules
|
|
131
|
+
|
|
132
|
+
- Marked generated files only (`% docs-html-view: generated navigation; do not edit`, a MyST comment); unmarked collisions and symlinks are refused.
|
|
133
|
+
- **Preflight before any write:** approved dirs, source trees, and any existing `nav/` (including ancestors) are checked for symlinks; every output path and existing collision is validated before the first write or prune, so a refusal leaves the tree unchanged.
|
|
134
|
+
- `gen_nav.py` exits `1` when no approved tree has documents, and prints warnings (missing/empty trees) to stderr.
|
|
135
|
+
- Hierarchy mirrors the tree; each directory gets a `{toctree}` of its documents and child-index links; entry titles come from the source H1 (path-only toctree entries), so no source text can inject MyST.
|
|
136
|
+
- Directory/site headings are sanitized (no `<`, `>`, backticks, braces, brackets, pipes, newlines).
|
|
137
|
+
- The full ADR tree renders, including retired/deleted tombstones.
|
|
138
|
+
- Re-running prunes only marked stale nav files; user files are never touched.
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
## Approval and consent
|
|
142
|
+
|
|
143
|
+
The Skill asks once, before any write/install/scaffold, covering packages, environment, files, `.gitignore` additions, output path, and title. There is no stored consent state or fingerprint. Repeated renders inside the approved scope do not re-ask; dependency upgrades, overwriting existing files, or changing the approved roots/range require a fresh ask. Missing trees or malformed required config/venv → report and stop, never guess. The template tokens are unquoted, so substitute each with `repr(value)` (valid, escaped Python literal), never raw interpolation into executable config.
|
|
144
|
+
|
|
145
|
+
## Limitations
|
|
146
|
+
|
|
147
|
+
- `.md` / `.rst` only; no `.mdx`.
|
|
148
|
+
- `include` / `literalinclude` / `raw` refused.
|
|
149
|
+
- Remote assets become inline placeholders plus a warning; active HTML is refused (only HTML comments are allowed), and `conf.py` is trusted code.
|
|
150
|
+
- No custom theme, browser, server, or deployment.
|
|
151
|
+
- Failed `make` may leave stale/partial `build/` output; no publisher normalises it.
|