← Back to SKILL.md1# Canonical Sphinx/MyST doc-build warning -> fix rules table.2#3# This is the single source of truth for the `sphinx-fix` skill's classify/fix4# engine (sphinx_fix.py) and is referenced by the `rst-to-myst` skill as the5# canonical category->fix catalog. Keep one table; do not duplicate it.6#7# GRAMMAR CONSTRAINT (important): sphinx_fix.py prefers PyYAML but falls back to8# a small stdlib-only parser, so this file must stay within a restricted YAML9# subset: 2-space indentation, block maps and block sequences only (no flow10# `{...}`/`[...]` collections), no anchors/aliases, no `>`/`|` block scalars.11# Write every value that contains special characters as a DOUBLE-QUOTED string,12# and write every regex backslash as `\\` (e.g. `\\.` for a literal dot, `\\s`13# for whitespace) so it is a valid YAML escape. The --selftest cross-checks the14# fallback against PyYAML, so a grammar slip is caught.15#16# MATCH SEMANTICS: a rule "fires" when, per `match`, either any (default) or all17# of its `categories`/`signatures` hit. `categories` test the trailing18# `[category]` tag; `signatures` are regexes searched against the message text.19# When several rules fire for one warning, the engine prefers a rule with a20# signature hit over a category-only hit, then earliest in this file. Author21# most-specific rules first.22#23# TIERS: 1 = fatal/abort (NOT represented as a rule -- the engine detects a24# hard-broken build structurally from the log shape); 2 = structural/parse25# errors that corrupt the toctree/label graph or fail a document's parse and so26# MASK warnings beneath them (fix these first, then rebuild); 3 = plain warnings27# that mask nothing.28#29# SAFETY: `mechanical` = a deterministic rewrite; `judgment` = needs a human30# decision. v0 is human-in-the-loop and proposes both; nothing is auto-applied.31#32# VERSION RANGES: `version_sphinx`/`version_myst` record the versions a rule was33# validated against. A warning from outside the range still matches but is34# flagged `(unvalidated)`; dependency bumps are the trigger to re-confirm the35# category strings and signatures.36#37# NOTE: the MyST categories below (`myst.xref_missing`, `myst.xref_ambiguous`)38# are confirmed against real Ray RtD builds. The core-Sphinx classes39# (toctree/orphan/duplicate-label/literalinclude) are matched by signature only40# pending confirmation of their exact `[category]` tag on Sphinx 8.2.3; add the41# category when confirmed. Unmatched warnings drive the skill-improvement loop.42 43version: 144baseline_sphinx: "8.2.3"45baseline_myst: "5.1.0"46 47rules:48 - id: myst-xref-missing-cross-extension49 title: "Bare cross-extension link to an .rst/.md source"50 tier: 351 safety: mechanical52 match: all53 categories:54 - myst.xref_missing55 signatures:56 - "cross-reference target not found: '[^']+\\.(?:rst|md)'"57 cause: "A bare [text](sibling.rst) or [text](sibling.md) link pointing at an .rst source. MyST flags it as a missing xref and fail_on_warning turns it into a build failure."58 fix: "Rewrite as a {doc} role so it resolves to the document and is never ambiguous; drop the extension on the target."59 fix_template: "{doc}`text <{target_stem}>`"60 target_extract: "cross-reference target not found: '(?P<target>[^']+)'"61 version_sphinx: ">=8.0,<9"62 version_myst: ">=5.0,<6"63 notes: "Shared with rst-to-myst Hard rule 2. target_stem = target minus its extension. On a category rename, re-validate (this rule needs the myst.xref_missing tag)."64 65 - id: myst-xref-missing-html-fragment66 title: "Relative .html# self-link read as a cross-reference"67 tier: 368 safety: mechanical69 match: all70 categories:71 - myst.xref_missing72 signatures:73 - "cross-reference target not found: '[^']*\\.html#"74 cause: "A relative page.html#section URL. RST renders it, but MyST treats it as an xref target and cannot find it."75 fix: "Rewrite as a same-page fragment link [text](#section), resolved via myst_heading_anchors (h1-h3)."76 fix_template: "[text](#{sec})"77 target_extract: "cross-reference target not found: '[^']*\\.html#(?P<sec>[^']+)'"78 version_sphinx: ">=8.0,<9"79 version_myst: ">=5.0,<6"80 notes: "Shared with rst-to-myst Hard rule 2."81 82 - id: myst-xref-missing-renamed-doc83 title: "Missing MyST xref (residual: stale/renamed doc target)"84 tier: 385 safety: mechanical86 match: any87 categories:88 - myst.xref_missing89 cause: "A myst.xref_missing whose target is not a cross-extension or .html# link -- usually a stale relative link to a doc that was renamed or moved (often from a sibling page)."90 fix: "Repoint the link to the current document, preferring the {doc} role (or an extensionless target). Grep the bare stem across doc/ to find every caller."91 fix_template: "{doc}`text <DOC>`"92 target_extract: "cross-reference target not found: '(?P<target>[^']+)'"93 version_sphinx: ">=8.0,<9"94 version_myst: ">=5.0,<6"95 notes: "Residual within the myst.xref_missing family: category-only, so it loses to the cross-extension and html-fragment rules when their signature hits."96 97 - id: myst-xref-ambiguous98 title: "Ambiguous extensionless link (doc and same-named label)"99 tier: 3100 safety: mechanical101 match: any102 categories:103 - myst.xref_ambiguous104 signatures:105 - "(?:not unique|matches more than one)"106 cause: "An extensionless [text](name) link where `name` is both a document and a (name)= label, so MyST cannot tell which you meant."107 fix: "Use the {doc} role for the whole-doc link so it always resolves to the document."108 fix_template: "{doc}`text <{target_stem}>`"109 target_extract: "target (?:is not unique|not unique): '?(?P<target>[^'\\s]+)"110 version_sphinx: ">=8.0,<9"111 version_myst: ">=5.0,<6"112 notes: "Shared with rst-to-myst Hard rule 2."113 114 - id: unknown-document115 title: "{doc}/toctree reference to an unknown document"116 tier: 3117 safety: judgment118 match: any119 categories:120 - ref.doc121 signatures:122 - "unknown document:"123 cause: "A {doc} role, a :doc: reference, or a toctree entry points at a document path that does not resolve -- a renamed or moved page, a typo, or a relative path that is wrong from the referencing file. Distinct from toctree-nonexisting, which is the toctree-directive-specific wording."124 fix: "Repoint to the correct document: a path relative to the referencing page, or an absolute path from doc/source with a leading slash. Remove the reference if the target is gone. Grep the bare stem across doc/ to find every caller."125 target_extract: "unknown document: '?(?P<target>[^'\\s]+)"126 version_sphinx: ">=8.0,<9"127 notes: "Category ref.doc confirmed on real Ray RtD builds; the tag is sometimes absent on the first emission, so the signature carries the match."128 129 - id: toctree-nonexisting130 title: "toctree references a nonexisting document"131 tier: 2132 safety: mechanical133 match: any134 signatures:135 - "toctree contains reference to nonexisting document"136 cause: "A toctree entry points at a document that does not exist -- a typo or a renamed/removed file. A broken toctree masks the warnings beneath it."137 fix: "Fix the toctree entry (entries are extensionless) to the current document name, or remove it."138 target_extract: "nonexisting document '?(?P<target>[^'\\s]+)"139 version_sphinx: ">=8.0,<9"140 notes: "Signature-only pending confirmation of the [category] tag on Sphinx 8.2.3."141 142 - id: orphan-not-in-toctree143 title: "Document not included in any toctree (orphan)"144 tier: 3145 safety: judgment146 match: any147 signatures:148 - "document isn't included in any toctree"149 cause: "A page that no toctree references. Sphinx warns so pages are not silently unreachable."150 fix: "Add the page to the appropriate toctree, OR add `:orphan:` (RST) / an `orphan: true` frontmatter field (MyST) if it is intentionally standalone. Human decides which."151 version_sphinx: ">=8.0,<9"152 notes: "Signature-only pending [category] confirmation."153 154 - id: duplicate-label155 title: "Duplicate label or object description"156 tier: 2157 safety: judgment158 match: any159 signatures:160 - "duplicate (?:label|object description)"161 cause: "The same label (or autodoc object) is defined in two places, so references to it are ambiguous and the label graph is corrupted."162 fix: "Dedupe -- remove or rename one definition. Which one to keep is a judgment call; preserve the name external {ref}/:ref: callers use."163 target_extract: "duplicate (?:label|object description) (?:of )?'?(?P<target>[^'\\s]+)"164 version_sphinx: ">=8.0,<9"165 notes: "Signature-only pending [category] confirmation. The intentional ray.actor.ActorMethod.bind duplicate is pre-filtered by a suppression below."166 167 - id: undefined-ref-label168 title: "Undefined label in a {ref}"169 tier: 3170 safety: mechanical171 match: any172 categories:173 - ref.ref174 signatures:175 - "undefined label"176 cause: "A {ref}/:ref: points at a label that does not exist -- usually a renamed or dropped label. Label names are load-bearing for every caller."177 fix: "Restore the exact label name (do not invent a new one), or repoint the {ref} at the correct existing label."178 target_extract: "undefined label:?\\s+'?(?P<target>[^'\\s]+)"179 version_sphinx: ">=8.0,<9"180 notes: "Category ref.ref is a best-effort guess; signature carries the match."181 182 - id: py-xref-target-not-found183 title: "Python-domain reference target not found (meth/func/class/obj/exc)"184 tier: 3185 safety: judgment186 match: any187 categories:188 - ref.meth189 - ref.obj190 - ref.func191 - ref.class192 - ref.exc193 - ref.attr194 - ref.mod195 - ref.python196 signatures:197 - "py:\\w+ reference target not found:"198 cause: "A Python-domain role ({meth}/{class}/{func}/{obj}/{exc}/:py:*:) points at an object not in the resolved API inventory. Two very different roots: (1) the object is documented elsewhere but its autosummary stub was not generated, or its module aborted import, so the inventory entry is missing -- these arrive as a large flood sharing an object prefix and are DOWNSTREAM symptoms, not separate defects; (2) the reference is genuinely wrong -- a renamed, moved, or mistyped target, or a stdlib/third-party name that needs intersphinx."199 fix: "Check for a co-occurring tier-2 autosummary-stub-not-found or tier-1 import abort for the same module first. If present, fix that root (up to and including reverting an in-progress API-ref restructuring) and rebuild -- the flood clears at once. Only when the build is otherwise healthy is the reference itself wrong: repoint to the correct dotted path, or fix the intersphinx target for a stdlib/third-party name. For a third-party target that DOES exist upstream, suspect a stale committed inventory snapshot: upstream added the symbol after doc/source/_intersphinx/<project>.inv was last refreshed. Fix by running `python doc/source/_intersphinx/refresh.py <project>` and committing the updated .inv, not by editing the reference."200 target_extract: "py:\\w+ reference target not found: (?P<target>\\S+)"201 version_sphinx: ">=8.0,<9"202 notes: "Categories confirmed on real Ray RtD builds (ref.meth, ref.obj, ref.func, ref.python); the signature carries the rest of the py domain (ref.exc, ref.attr). safety=judgment: both the correct target and the correct root (edit one reference vs revert a restructuring) need a human decision -- never auto-apply. A correlated flood sharing an object prefix with autosummary-stub-not-found warnings is that rule's masked downstream; triage the tier-2 root, not each reference."203 204 - id: image-not-readable205 title: "Image file not readable"206 tier: 3207 safety: mechanical208 match: any209 categories:210 - image.not_readable211 signatures:212 - "image file (?:not readable|.* not readable)"213 cause: "An image path does not resolve from the referencing document."214 fix: "Fix the image path (relative to the referencing page, or an absolute /_static-style path)."215 target_extract: "image file (?:not readable|.* not readable):?\\s+(?P<target>\\S+)"216 version_sphinx: ">=8.0,<9"217 notes: "Category image.not_readable is a best-effort guess; signature carries the match."218 219 - id: docutils-inline-markup220 title: "Unbalanced inline emphasis/strong markup"221 tier: 3222 safety: judgment223 match: any224 categories:225 - docutils226 signatures:227 - "Inline .+ start-string without end-string"228 cause: "A docstring or RST body has an unbalanced inline marker -- a lone * (emphasis) or ** (strong) with no closing marker. Common in API docstrings that write *args or **kwargs, or a bare asterisk, outside a code span. It surfaces from an <autosummary> generated stub at the docstring's location."229 fix: "Balance or escape the marker in the source docstring, not in the generated stub: wrap literals in double backticks (``*args``, ``**kwargs``), or escape a standalone asterisk (\\*)."230 version_sphinx: ">=8.0,<9"231 notes: "Category docutils confirmed on real Ray RtD builds. No target extraction: the message does not carry the offending text, so locate the unbalanced marker at the cited docstring."232 233 - id: intersphinx-inventory-unreachable234 title: "intersphinx inventory not fetchable (missing snapshot + network)"235 tier: 3236 safety: judgment237 match: any238 signatures:239 - "failed to reach any of the inventories"240 cause: "Sphinx exhausted every inventory location for a project. Each entry in intersphinx_mapping lists the committed snapshot at doc/source/_intersphinx/<project>.inv FIRST and the upstream URL as fallback, so this warning means BOTH failed: the snapshot is missing or unreadable AND the network fetch failed. A present, readable snapshot cannot produce this warning -- it needs no network. So this is no longer merely transient: it points at a missing snapshot with a flaky network hiding behind it."241 fix: "Check that doc/source/_intersphinx/<project>.inv exists and is committed (it should start with the bytes '# Sphinx inventory version'). If it is missing, that is the defect -- run `python doc/source/_intersphinx/refresh.py <project>` and commit it. If the snapshot is present and intact, the local read itself failed (permissions, a bad exclude_patterns change, or the file was truncated); re-check the snapshot before blaming the network. Only if the project is absent from _intersphinx_targets is the upstream URL in conf.py the thing to confirm. This is not a per-page content fix."242 version_sphinx: ">=8.0,<9"243 notes: "Untagged (no [category]); signature-only. The URL and error detail sit on an unindented continuation line the parser does not capture as a warning record, so match on the WARNING line. safety=judgment: missing-snapshot vs local-read-failure vs genuinely-wrong-URL needs a human call. Pre-dates DOC-1050 as a pure network/transient rule; since snapshots are committed, 're-run the build' is NO LONGER the first move -- a green re-run just means the network came back and the missing snapshot is still missing."244 245 - id: literalinclude-out-of-range246 title: "literalinclude line/marker out of range"247 tier: 2248 safety: mechanical249 match: any250 signatures:251 - "Line spec '[^']*' out of range"252 - "Object named '[^']*' not found in include file"253 cause: "A {literalinclude} :lines:/:start-after:/:end-before: no longer matches the source -- the included code moved or changed. The directive fails the document's parse."254 fix: "Update the :lines: range or the :start-after:/:end-before: markers to match the current source file."255 version_sphinx: ">=8.0,<9"256 notes: "Signature-only pending [category] confirmation."257 258 - id: autosummary-stub-not-found259 title: "autosummary stub file not generated for a listed member"260 tier: 2261 safety: judgment262 match: any263 signatures:264 - "autosummary: stub file not found '[^']+'\\. Check your autosummary_generate"265 cause: "An autosummary directive lists a member whose stub .rst was never generated. A lone entry usually means the member was renamed or removed. A bulk failure across a whole module or API-ref page means autosummary could not introspect the module -- commonly the module failed to import under the build's autodoc_mock_imports set, or an in-progress API-ref restructuring moved or renamed the source. It is a tier-2 structural condition: the ungenerated stubs mask the py:* reference-target-not-found warnings for the same objects (see py-xref-target-not-found)."266 fix: "Do not fix per entry when the failure is in bulk. Determine scope first: many missing stubs sharing a module prefix point at one root -- confirm the module imports under the build's mock set (autodoc_mock_imports) and that autosummary_generate is on; when an API-ref restructuring is in flight, the correct move may be to revert or repair the file moves rather than edit each entry. A lone missing stub means the member no longer exists -- update or drop the autosummary entry."267 target_extract: "stub file not found '(?P<target>[^']+)'"268 version_sphinx: ">=8.0,<9"269 notes: "Untagged core-Sphinx warning (no [category]); signature-only. Seen at scale in a Ray API-ref rework where dozens of missing stubs masked hundreds of downstream py:ref failures. Fix this tier-2 root first, rebuild, and most of the py-xref-target-not-found flood clears."270 271# Known-benign warning classes the build already suppresses or filters in272# doc/source/conf.py. The engine segregates these into a not-actionable bucket273# and never proposes a fix. Suppressions are checked BEFORE rules, so an274# intentional duplicate (e.g. ActorMethod.bind) never reaches duplicate-label.275# When a suppression is lifted (its tracking ticket), convert its row to a rule.276suppressions:277 - id: misc-copy-overwrite278 categories:279 - misc.copy_overwrite280 reason: "Sphinx 8 copy-overwrite check fires on a redundant sphinxcontrib-redoc asset. Benign."281 tracked_by: "DOC-1257"282 283 - id: config-cache284 categories:285 - config.cache286 reason: "sphinx_design stores an unpicklable value in config; benign cache warning. Pre-existing."287 288 - id: actor-bind-duplicate289 signatures:290 - "duplicate object description of ray\\.actor\\.ActorMethod\\.bind"291 reason: "Intentional duplicate autodoc object; not a real defect."292 293 - id: collections-fetched-footnotes294 signatures:295 - "autonumbered footnote references"296 - "Unknown target name"297 location_contains: "_collections"298 reason: "Docutils footnote errors in fetched example content under _collections; benign noise filtered in conf.py."299 tracked_by: "DOC-1258"300