| name | sphinx-fix |
| description | Diagnose a failing Ray Sphinx / Read the Docs documentation build. Parses the Sphinx warning stream (an RtD build log, a local build, or pasted text), classifies each warning against a rules table, and proposes the canonical fix in severity-tier order. Detects a hard-broken build, segregates known-benign suppressed classes, and lists every unclassified warning. Use when a `docs/readthedocs.com:anyscale-ray` check fails, when asked "why is the docs build failing?" or "what warning is breaking this PR?", or to turn a Sphinx warning dump into an ordered fix list. |
| user-invocable | true |
| argument-hint | <warnings file | pasted Sphinx log | '-' for stdin> |
Diagnose and fix Ray Sphinx doc-build warnings
The Ray docs build with fail_on_warning: true (.readthedocs.yaml), so any single Sphinx warning fails the build. The expensive part of a docs change is usually not the edit — it's finding the warning. This skill turns a warning stream into a compact, severity-ordered list of findings, each with its canonical fix.
It is deterministic and read-only: a stdlib-only script parses and classifies; it never edits files. v0 is human-in-the-loop — you (or the agent) apply each fix and rebuild. Nothing is auto-applied.
Where it sits among the doc-build skills:
sphinx-fix (this skill) — diagnose build output: classify warnings, propose fixes.
rst-to-myst — convert source: migrate .rst pages to MyST .md. It shares one rules source with this skill (rules.yaml below).
rtd_doctor.py (doc/rtd_doctor.py) — preflight the environment: assert the local toolchain matches Read the Docs before you build.
When to use
- A
docs/readthedocs.com:anyscale-ray PR check failed and you need to know why and how to fix it.
- You have a Sphinx warning dump (from RtD, a local build, or a paste) and want it classified.
- You're iterating on a docs PR and want to confirm the build is clean before pushing.
Not for: authoring or converting pages (use rst-to-myst), setting up the build environment (use rtd_doctor.py + make rtd), or fetching the rendered HTML (open the preview URL).
Input modes
Both modes feed the same parser and rules table; only the log source differs. The parser tolerates surrounding log noise, so you can pipe a raw build log in without pre-filtering.
-
A warning stream you already have — paste it to a file, or pipe it. This includes the output of the team's Read the Docs log tooling (the private /rtd-build-logs skill's warnings subcommand), the RtD build page's raw log, or any captured Sphinx output. This is the cheapest mode and needs no local build.
-
A local build — capture stderr (and, with -W -w warnings.txt, a warnings file) from a full make rtd / sphinx-build run and feed it in. Use this for a tight local loop. A local build must be a full build for cross-reference checks to be faithful — an incremental make local pulls a cache and misses cross-file breakage (a rename that breaks a link in an unchanged file).
Running the engine
python doc/.claude/skills/sphinx-fix/sphinx_fix.py warnings.txt
python -m sphinx -T -W --keep-going -b html . _build/html 2>&1 \
| python doc/.claude/skills/sphinx-fix/sphinx_fix.py
python doc/.claude/skills/sphinx-fix/sphinx_fix.py warnings.txt --json
python doc/.claude/skills/sphinx-fix/sphinx_fix.py --explain myst-xref-missing-cross-extension
Stdlib-only — no install. It prefers PyYAML to read rules.yaml but falls back to a bundled parser, so it runs in a bare environment. Optionally pass --sphinx-version/--myst-version to gate findings against each rule's validated range.
Exit codes: 0 clean · 1 classified findings to fix · 2 hard-broken build (abort) · 3 only unclassified warnings (no rule matched). Precedence 2 > 1 > 3 > 0.
Reading the output
The report has five parts, in priority order:
- Abort banner (only if present) — a hard-broken build (a
conf.py/extension error, a traceback, or SEVERE: with no completion summary) aborted before the warning pass. Fix this first; the rest of the log is unreliable. For an autosummary import abort, the banner adds a hint: — the named module is usually a decoy for an unrelated module whose import chain broke (often a dep mocked by autodoc_mock_imports but used at import time); trace it and make the eager import lazy.
- Root-cause groups (only if present) — a single structural root (an ungenerated
autosummary stub for a module or class) masks a flood of downstream py:* reference target not found warnings for the same objects. Rather than list the flood as N equal-looking rows, the report collapses it: ROOT [T2] ... — <prefix>.* (N stubs not generated, masking M downstream references), the root's fix once, then the masked references collapsed under it (capped in the rendered view, with the dropped count shown). Fix the root, rebuild, and the masked references clear together — don't chase the individual references. The full flat list is always in --json.
- Findings, grouped by tier (1 → 2 → 3), for everything not absorbed into a root-cause group. Each row is
[T2] <rule-id> <path:line> plus the warning message, the canonical fix: (with a Suggested: rewrite for mechanical rules), and a safety: flag. judgment findings need your decision.
- Unclassified — warnings no rule matched. Never silently dropped. Resolve each with the user, then file a skill-improvement ticket so
rules.yaml gains a rule (see below).
- Suppressed — known-benign classes the build already filters; not actionable.
Severity tiers and the iterate loop
-W --keep-going lists every warning of a build that completes. Two things break the "one pass finds everything" assumption, so the skill triages by tier and you must iterate:
- Tier 1 — fatal/abort. The build died before the warning pass.
--keep-going does not save you. Fix it, rebuild, re-run.
- Tier 2 — structural/parse. A broken
toctree, a duplicate label, or a failed literalinclude corrupts the doc graph and masks the warnings beneath it. Fix all tier-2 findings, then rebuild — a fresh batch of tier-3 warnings often appears.
- Tier 3 — plain warnings. These mask nothing; fix them in any order.
Always fix the highest tier first, then re-acquire and re-parse. Never report "all clear" after a single pass when a tier-1 or tier-2 condition was present — the report's verdict line says so explicitly.
Applying fixes (human-in-the-loop)
The engine proposes; you apply. For each finding:
- Mechanical fixes (link rewrites, fragment links, toctree entries,
literalinclude ranges, image paths) — apply the Suggested: rewrite, confirming the target. For the MyST link rewrites, cross-check rst-to-myst Hard rule 2 (the {doc}-role and #fragment rules are shared).
- Judgment fixes — you decide: which of two duplicate labels to keep (preserve the name external
{ref}/:ref: callers use), or whether an orphan page should join a toctree or be marked :orphan:.
A rule graduates to auto-apply only once there is evidence it is always safe — not in v0.
Unclassified warnings → grow the rules table
The seed table is incomplete by construction (the Sphinx 8→9 march and contributors new to MyST will surface classes no historical log shows). When the report lists an unclassified warning:
- Resolve it with the user — determine the cause and the canonical fix.
- Prompt the user to file a skill-improvement ticket capturing the new category/signature and its fix.
- Add a rule to
rules.yaml (and, if it's a link class, keep it consistent with rst-to-myst).
This feedback loop is the intended way the table grows — from real misses, not a one-time corpus.
conf.py suppression awareness
The build already suppresses or filters known-benign classes in doc/source/conf.py (e.g. misc.copy_overwrite, and docutils footnote noise from fetched _collections content). The engine segregates these into the Suppressed bucket and never proposes a fix — do not "fix" them. They are tracked for removal under their own tickets (DOC-1257, DOC-1258); when a suppression is lifted and its warning resurfaces, convert its suppressions: entry in rules.yaml into a rule.
Version-bump re-validation
rules.yaml records a baseline (Sphinx/myst versions) and a per-rule validated range. MyST and Sphinx bumps rename or retire [category] tags (the tag default itself flipped at Sphinx 8.0), so matching also keys on message signatures as forward-compatibility insurance. After a dependency bump, re-confirm the category strings and signatures against a real build; any finding the engine flags (unvalidated for this version) is the re-validation worklist. Some core-Sphinx rules are signature-only today pending confirmation of their exact [category] on Sphinx 8.2.3 — confirm and add the category as you encounter them.
Done
A change is verified when a full make rtd (or the green RtD PR preview) shows no warnings, and a regression check against /en/master shows the rendered content unchanged except where you intended (per doc/.claude/CLAUDE.md).
Maintaining this skill
rules.yaml is the single source of truth and stays within a restricted YAML subset so the stdlib fallback can parse it (see the header comment in the file). Don't introduce flow collections or block scalars.
- After editing
rules.yaml or sphinx_fix.py, run the selftest: python doc/.claude/skills/sphinx-fix/sphinx_fix.py --selftest. It checks schema invariants, the fixture goldens, and that the fallback parser agrees with PyYAML. Regenerate goldens after an intentional output change with --update-golden (and eyeball the diff).