Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Resolution

Parsing turns each document into a list of occurrences: inline links and images, reference style links, autolinks, and any reference definition no reference in the document consumes, since an orphaned [api]: ./guide.md still maintains a destination someone will trust. In an agent instruction file, CLAUDE.md, CLAUDE.local.md or GEMINI.md, a line holding only @path to a named file is an import the agent loads, and it is read as a reference too. Each occurrence keeps two spellings of its destination. The raw one is the exact bytes from the source. The semantic one is what those bytes mean after the format’s own decoding. So [a](&b) records both &b and &b, and a change to either the spelling or the meaning is visible later.

Markdown is read the way Python-Markdown nests a block under an opener line. The admonition, details and tabbed extensions put a body under !!! note, ??? tip or === "Tab" by indenting it four columns, which CommonMark reads as indented code once a blank line comes before it. That body is parsed as Markdown instead, so a link inside a MkDocs admonition is checked like any other, and the same lines inside a fence stay the example they show. Across the corpus only MkDocs trees write an opener with a body under it, so the reading is not gated on a mkdocs.yml.

A full or collapsed reference whose label no definition in the document declares, [text][gone] or [gone][] and their image forms, is read as well, since Markdown leaves it as the bracketed text it spells: it is a link that broke, and it resolves as label-not-declared. A pair glued to what surrounds it, a[i][j] or [A-Z][a-z]*, is an index or a pattern rather than a link. A page holding a template marker, {{ or {%, proves no label absent, because a generator’s template can write the definition Markdown then reads, and under a mkdocs.yml such a label is declined rather than missing, because mkdocs-autorefs answers it from the site’s own inventory.

What the parser cannot see into is declared instead of skipped. Raw HTML blocks and MDX expressions become opaque regions, reported with their size and place as opaque-html-region and opaque-mdx-region findings, so a link hidden inside JSX is a stated blind spot rather than an invisible one. JSX splits in two there, and the split is the MDX grammar’s own. A tag standing alone on its line opens a block, so what stands between it and its closing tag is Markdown of the page, already parsed into blocks: that body is read like any other, and only the tags on either side are opaque. A tag in the run of a line is one paragraph’s phrasing instead, and it stays opaque whole, attributes and body together. Either way the attributes and any expression among them stay unread, so an identity a component computes is still a blind spot. That holds for a lowercase <a href> or <img src> too. An MDX build hands those values to the browser as written, and Docusaurus turns a path into a bundled file only when Markdown syntax writes it, so a JSX path answers to the page URL rather than the tree. The one exception is a literal id or name on a lowercase element, read as the identity it declares, since reading one too many identities can only leave a broken anchor unreported. An HTML region still yields what a renderer would follow: <a href> and <img src> values resolve like any markdown destination, character references decoded into the semantic spelling, alongside the headings and id attributes the anchor tables already harvest. A tag spelled inside a comment or a script, style, textarea, or title body is followed by no renderer and is never mined, and the rest of the region stays the declared blind spot. Raw output injection is opaque in every dialect: AsciiDoc passthrough blocks and reStructuredText raw directives inject output the parser cannot read and count as opaque regions too. Markdown and MDX draw the line wider and treat every raw HTML region as opaque, comments included, while AsciiDoc and reStructuredText code blocks, literal blocks, and comments render as visible text or not at all and are never opaque. An AsciiDoc URL written directly is a link the way Asciidoctor reads it, with an attribute list, https://host/page[text], bare, or between angle brackets, a bare one ending before the punctuation that closes its sentence, and a mailto: only with an attribute list.

Each destination then passes through the generic resolver; trusted absolute forge spellings continue through the private dialect module. A relative path resolves from the document’s own directory and must stay inside the repository; ../../../etc/passwd is an invalid-reference, not a file read, and so is !file-loader!./asset.pdf, which is a bundler’s inline request rather than a path. A path beginning with / is a site route, not a repository-root shorthand. It stays unsupported unless sealed, candidate-bound site-build evidence maps that exact route and optional decoded anchor to a published source-backed or generated page, either directly or through a proved fragment-aware terminal redirect, unless it is a Sphinx :doc: target under a conf.py the tree holds, or unless a .amiss/router.yml above the document declares the URL path its own directory is served at and the tree holds the page the rest of the route names. Forge URLs need the complete identity group, not only the repository name. When the invocation provides --repository, --ref, and --default-branch-ref and selects a dialect, a URL on the declared host that names the same repository in that dialect’s spelling is converted to a path when it names the candidate branch or one full lowercase object ID in the run’s declared SHA-1 or SHA-256 format. Exact IDs on all five dialects resolve only through that commit and its objects already present under the declared Git roots. A completely walked local tree can prove a missing path; an unavailable commit, tree, or target object instead retains unsupported-version-scope with the exact commit and contained path, plus the decoded URL for the provider-evidence layer. The engine still fetches nothing. A named branch or tag outside the candidate remains version-scoped without guessed commit identity. Forge query semantics remain unsupported except for Bitbucket Cloud’s canonical fileviewer=file-view-default presentation choice and Bitbucket Data Center’s exact revision selectors. Transclusion-dependent historical absence remains unsupported because an object walk is not a historical site build. A URL outside the declared repository is external. It records the decoded destination so the layer that does fetch can read the list without walking the tree again, and it raises no finding, because there is nothing it decided.

Five dialects exist, each pinned to the exact URL grammar its forge’s browser emits. The github dialect reads owner/name/blob-or-tree/ref/path and serves GitHub and any GitHub Enterprise host the identity declares. The raw form reads the same way, and on github.com so does the content host, raw.githubusercontent.com/owner/name/ref/path, which serves files only; any of them may spell the branch as refs/heads/main. The gitlab dialect reads the canonical separator form group[/subgroup...]/name/-/blob-or-tree/ref/path, nested groups compared whole. The gitea dialect serves Gitea, Forgejo, and Codeberg with typed selectors: src/branch/ splits like the others, src/commit/ resolves its full lowercase object ID from the local object database and retains a known immutable scope when those objects are unavailable, and src/tag/ is always out of version scope because no tag is a trusted ref. The bitbucket-cloud dialect reads owner/name/src/commitish/path; Cloud places the commitish in one segment, so another branch or tag still retains a known path, while a candidate branch containing / cannot match that form. Its canonical default-viewer query is presentation only. The bitbucket-data-center dialect accepts an optional literal installation context with no projects or users segment, followed by projects/<key>/repos/<name>/browse/<path> or the corresponding users/<slug> personal route. No query means the declared default branch; at= must carry the exact candidate ref, another full branch or tag ref, or one full object ID. The history selector Atlassian documents as until=<oid>&untilPath=<path> is accepted only when the full ID has the run’s object format and the decoded path repeats the browse path. These boundaries follow Atlassian’s repository route, commit browsing, and history URL contracts. Line anchors follow the forge: #L10-L20 is a line reference on github and gitea, #L10-20 on gitlab, #lib.rs-10 on bitbucket-cloud when lib.rs is the target’s exact basename, and #10-20 on bitbucket-data-center. Relative references use the run’s declared dialect when one is present. A recognized reference’s intent kind names the dialect that read it, not the host, so an Enterprise repository’s links carry the same kind GitHub’s do. A branch spelled exactly like a full object ID is refused as ambiguous rather than assigned whichever interpretation happens to win on a forge.

One document, every destination shape:

[guide](guide.md)                     resolves beside this document
[guide](guide)                        resolves to guide.md, the spelling a router serves
[site](/docs/guide/)                  resolves only from matching sealed site-build evidence
[escape](../../../etc/passwd)         invalid-reference: it leaves the repository
[dir](sub/)                           the author promised a directory
[gh](https://github.com/o/r/blob/main/src/lib.rs)   a path only for o/r, github, and --ref refs/heads/main
[lines](../src/lib.rs#L45-L48)         exact inclusive line selection under github or gitea
[web](https://example.com/manual)     external: recorded with its destination, never fetched
[wiki](https://ja.wikipedia.org/wiki/日本)   external too: judged as the URL a browser requests for it
[anchor](guide.md#setup)              resolves when a known renderer publishes that heading identity

The same decision, drawn:

resolvedestdestinationrelrelative pathdest->relrouteleading-slashsite routedest->routeforgeforge URL,same repositorydest->forgewith identity + dialectotherany other URLdest->othertreeresolve againstthe treerel->treeunsupunsupported-reference-semanticsroute->unsupscopecandidate refor exact commit IDforge->scopescope->treecandidate ref or local objectsversunsupported-version-scopescope->versother named ref or unavailable objectextexternal,recorded not fetchedother->exthittarget bytesand mode readtree->hitfoundmissexplicit-target-missingtree->missabsentdecltarget-declared-untrackedtree->declabsent, declared

A relative destination the tree does not hold is asked once more, under the spellings a documentation router serves: guide and guide.html for guide.md, a directory’s index for its README.md, and last the route a document publishes for itself, which is the id or slug its frontmatter declares in a tree that holds a docusaurus.config.*. The first spelling that names a file resolves the reference to that file, and the report names the file that answered while the occurrence keeps the destination the author wrote. A spelling reaches nothing that is not already in the tree, so it can widen what resolves and never invents a target; a promised directory and a same-repository forge URL are never re-spelled at all. A site generator whose configuration file is in the tree anchors a destination somewhere other than beside the document: an Antora resource ID at its family directory, a Docusaurus bare path or @site/ alias at the content root or the site directory, a Sphinx :doc: target with a leading slash at the directory holding conf.py, a raw HTML destination under a mkdocs.yml at the directory the page is published at, a Zola @/ destination at the content directory beside config.toml, a Jekyll {% link %} or {% post_url %} tag at the site’s source, a Hugo ref or relref beside the page and then under the language’s content directory, and an mdBook destination climbing past its book’s root under the src of the book that holds the page it names. Markdown never reads those templates as links, since each holds spaces, so they are read out of prose where they open, and a quoted URL through relative_url is the site route it names. Both generators fail the build on a template naming nothing, so such a target is missing rather than undecided, while one with no site of its generator above the page is unsupported-reference-semantics. The file’s presence selects the rule, and four of them are read further for what they bind: an Antora descriptor for its name, a conf.py for the suffixes it reads, a Hugo configuration for the content root and base a site route is answered under, and an Astro configuration for whether it loads Starlight, whose content root and base answer the same way. The nearest such file above the document selects it, and where none is above it and the tree holds exactly one, that one does, since a site in website/ reads pages that sit outside it. An alias in a tree holding several sites and none above the document is unsupported-reference-semantics rather than a directory of that name, since the value arrives when the site is built and this run cannot say which site would arrive.

Other generators decide a page’s URL in a configuration this engine never opens, and a relative destination is resolved against that URL rather than against the file. Under a Hugo configuration, a _config.yml, an eleventy.config.*, an .eleventy.js or an astro.config.* on the document’s ancestor chain, a destination the tree does not hold takes unsupported-reference-semantics with reason: unmodelled-route, the answer a leading-slash site route already takes, instead of being claimed missing. A destination ending in .md, .mdx or .markdown is the exception: a build reads those files and serves what it made under a name of its own, so the missing path stands. Under a book.toml the same answer covers a destination ending in .html that no book source reaches, because that names a page of the built site. Only the path side moves: a fragment on a document the tree holds is still read against the identities that document publishes, and a tree declaring none of these files reports every missing path it reported before.

A tree whose site is built somewhere else keeps no such configuration, so it names its own router in .amiss/router.yml instead. That turns on the spellings that reach files the tree already holds, and no others. The same file says where the directory is published, and a slash-rooted route opening with that base is read as a path under it. A route reaching no file keeps the boundary it had, so the base can resolve a destination and cannot claim one. The one exception runs the other way: when the base side served a route from a page the candidate no longer holds, the candidate reads that route under the base’s anchoring, so deleting the page is the missing target it is rather than a reference that seems to have left. The same holds under a generator whose build might supply a path, such as an Antora component an extension assembles: a page the base held in the tree was no build’s, so deleting it is missing rather than left to the build.

What a documentation router serves holds the spellings, the routers they were harvested from, the generator rules and what selects each, and what the union costs. What a repository declares is the section with the names you can write and four trees read with the file and without it.

A destination that opens {{ or {% never reaches the tree at all. The build fills it in, so it takes the same unsupported-reference-semantics an AsciiDoc {attribute} takes. The opening is enough, since an HTML attribute value ends at the next quote and hands over an expression with the closer cut off, and a file whose own name carries a single brace stays a path.

A destination no spelling reaches, in a tree that declares no such generator, is kind: missing with reason: path-not-found. The path that row names is the destination read from the document’s own directory wherever a rule kept that reading, so a finding never carries a directory a rule anchored at: a raw <img src="../../img/gone.png"> under a mkdocs.yml is missing at img/gone.png rather than under the directory the page is published at. The row also carries near: the one tracked path equal to the missed one apart from case, when exactly one exists and null otherwise. It answers the break a case-insensitive working copy hides, where Guide.md opens locally and resolves nowhere on the tree the forge and Linux CI read. A repository holding both spellings names a real ambiguity and stays bare. A lone reference whose written path part is the missed intent’s exact tail turns that neighbor into the finding’s fix, replacing only the bytes the author wrote while a fragment rides untouched beside them. A reference-style link’s path is written in its definition, so the fix edits the definition, and references sharing one definition share that one edit, which is the one case where a finding over several occurrences carries a fix.

When the missed path existed in the base tree and disappeared from the candidate, the same resolution also carries same_object_at if exactly one candidate-added entry has the identical Git mode and object ID and that identity belongs to exactly one removed path. Copies, duplicate content, mode changes, and edited moves leave it null. An index holds no directory entries, so under --index a removed directory pairs with the one added directory holding exactly its files, and only when no other removed directory held the same tree. When the author wrote a plain relative path that reached the missed file from beside its document, that one move also supplies the fix: the path rewritten, relative the same way, to where the change put the same bytes, with any fragment left as written. A site route, a generator’s anchoring, or a path that needs escaping keeps the fact without a fix, since the author’s own spelling cannot be carried over. The human form prints it after the reason as same bytes at and the path. The case-only near fact remains independent and answers first.

A destination no spelling reaches is asked one last question, against a declaration the repository already publishes for Git rather than for this engine. Only the tracked .gitignore files on the path’s own ancestor chain can name it, and the nearest one that answers travels with the finding. What comes back is target-declared-untracked, a record under both profiles, so the reference stays counted rather than cleared. The engine never asks whether a path is ignored; it asks whether a line’s own spelling bounds what that line can clear, since a line reaching further than it spells would answer for an unbounded number of references.

Two shapes qualify. The first is a literal that a slash anchors to the file’s own directory, at the front or in the middle, with no pattern or escape byte and no empty, ., or .. segment; it names one path, and a trailing slash makes it answer for that path’s descendants too. The second is the bare *, which says the directory holding the file keeps nothing, so it answers for every path under that directory. A negation is counter-evidence to a * standing beside it: !.gitignore keeps that one entry out of the declaration, while a negation this cannot bound, !*.md or anything carrying a slash, drops the emptied directory altogether.

Everything else is refused. A pattern with no slash matches wherever the tree happens to hold that name, at any depth, so one word in a root file could answer for a path anywhere in the repository; those references stay missing targets. Git applies no ignore rule to a file already tracked, and neither does this: a path the tree holds never reaches the question.

AsciiDoc destinations reach one rule of their own before anything else. A target still holding {name} cannot be a path, because the value arrives when the site is built and this engine reads two trees, so it is unsupported-reference-semantics rather than a guess at a directory called {name}. Across Quarkus that is roughly a quarter of every reference, so reporting them as missing would have buried the real breaks. The double-angle shorthand keeps an unambiguous document.adoc#anchor as an inter-document target rather than turning the entire value into a local ID. Inside an Antora component, a document under modules/<name>/ with antora.yml at the component root, an xref, a family-qualified include, or an image is read as the resource ID Antora reads it as and anchored at the family directory of its module, so xref:index.adoc[] in modules/api/nav.adoc names modules/api/pages/index.adoc. The component is every source root whose antora.yml spells the same name, and one reserving the ext block is assembled by an extension, so a resource it does not hold is undecided rather than absent. Outside Antora an image joins imagesdir, which is empty unless a document sets it, so a document that sets none reads its images beside itself, the way Asciidoctor and a forge’s preview both do, and one whose header sets a single literal directory reads them under it. A document another includes takes its includer’s value, and one that sets it anywhere past the header, twice, or through an attribute leaves its images undecided. A heading anchor on an AsciiDoc target resolves through the Asciidoctor rule in What thirteen renderers call a heading, the only rule whose identities all carry a prefix.

An AsciiDoc destination that needs the build’s own state is declined as attribute-dependent rather than guessed: one holding an unexpanded {attribute}, an image whose imagesdir the run cannot know, since it is an attribute too, and a cross reference to an extensionless name, which is a page identity a site catalogue answers. A link or an include is read as written, so link:LICENSE[] names that file, include::NOTICE[] is missing when the tree lacks it, and a climb out of the tree is a traversal, and an image at a URL is external.

An AsciiDoc document publishes three identities a cross reference can name. A block anchor, [#install], [[install]], [source#install] or [id=install] on a line of its own, and an inline anchor, [[remove-refs]], [[[bib]]] or anchor:remove-refs[] in the flow of a list item, a paragraph or a section title, are identities as written. A section title is the reference text a natural cross reference names, so <<API entrypoints>> reaches the section titled API entrypoints however far down the page it sits, which is the reverse lookup Asciidoctor runs when the target is no known ID. It runs that lookup only where the target carries a space or a capital, so a title carrying neither publishes nothing beyond the identity the renderer rules already give it, and an inline anchor is read only where its ID follows Asciidoctor’s own grammar, since that scan runs over prose rather than over a line that carries nothing else. A paragraph whose first line is indented is literal text, rendered as the characters it holds, so nothing inside it is a reference or an anchor at all: the s!Figure (\d+)!<<fig-$1>>!g in a shell command in Asciidoctor’s own migration page is not a cross reference to fig-$1. An indented line carrying a list marker is a list item, which Asciidoctor checks first.

A reStructuredText heading anchor resolves through the Docutils rule in What thirteen renderers call a heading, and the labels a document declares outright with .. _name: resolve as themselves. The Sphinx roles that name a document, a file or a label are modelled by name, which is why the grammar profile says docutils-rst-sphinx-refs: :doc:, :download:, :ref:, :numref: and :term:. A :download: names a file as written, beside its document, or under the directory holding conf.py when it opens with a slash, and so do the paths an image, figure, include or literalinclude names, since Sphinx reads each of them the same way. A :numref: is a label the way a :ref: is. A :term: names a term some glossary directive in the tree declares, compared without case, and a term nobody declares is missing unless conf.py mentions intersphinx anywhere, which can bring in another project’s glossary through an extension that never names it. An image a substitution definition names, .. |logo| image:: logo.png, is an image like any other, and the link an image or figure opens, its :target: option, is a destination of its own. A relative :doc: target resolves beside its document, read under each suffix its root reads and again as the author wrote it, so a docname carrying a dot of its own reaches the file that name takes the suffix of. One already spelled with a suffix keeps it, since that spelling was the adapter’s and the adapter runs before any root is known. A source-root-absolute one resolves under the directory holding conf.py when that file sits above the document in the tree, under each suffix that conf.py declares, and stays a declared site route when nothing names the Sphinx root. An entry of a toctree body is a docname the same way and resolves the same way, bare or as Title <docname>; its options, self, URLs, and glob patterns name no single document and are passed over. A :ref: resolves against the snapshot’s label table, built after discovery from every name a document whose profile reads roles declares and bounded by declared-labels-per-snapshot, and a :numref: too: a unique declaration resolves to its declaring document, a name nobody declares is a missing target, and a name declared twice is undecided rather than guessed between. Labels follow the Docutils simple-name rule, case-folded with whitespace runs collapsed, a phrase declaration may arrive backtick-quoted or sit inside a list item or grid-table cell, and an undeclared name carrying a colon is treated as another project’s inventory, declared unsupported rather than reported missing. So are genindex, modindex, py-modindex and search, which Sphinx declares itself for the index and search pages every build writes. The extensions a conf.py loads add two more: sphinx.ext.autosectionlabel declares every section title as a label, prefixed with the docname and a colon where autosectionlabel_prefix_document is true, and sphinx.ext.autodoc or sphinx.ext.autosummary pull labels out of Python docstrings no document holds, so under either one a name nothing here declares is declined as well. A prefixless name absent from the local table can resolve only through one unique label in complete, candidate-bound Intersphinx evidence supplied through the sealed trust boundary. Local declarations retain precedence; duplicate external labels stay unsupported, while absent, partial, stale, malformed, or mismatched evidence leaves the name missing. The engine never fetches an inventory. Every other role stays an open extension point, declared rather than read into.

A Sphinx project that writes its pages in Markdown spells the same two roles in MyST, and they are answered the same way. {doc}`quickstart` is the docname :doc: names, taking .md rather than .rst, and {ref}`install-step` is the label :ref: names, looked up in the same table, which a MyST document fills through (name)=, its attribute blocks, the :name: a directive carries, the terms of a {.glossary} list and the object a domain:type directive describes. {term}`environment` is answered the way :term: is. The directives that name a file, {image}, {figure}, {include} and {literalinclude}, are references the way their reStructuredText forms are, read from the opener of a backtick or colon fence, and so is {download}`data.csv`. The myst-link rule in What thirteen renderers call a heading points a plain link at that same table: [text](name) where the tree holds no such file, and [text](#name) where the document itself publishes no such identity, are looked up as labels before either is reported missing, so the tree keeps whatever answer it had and a name nobody declares stays the missing target it was. A source-root docname such as {doc}`/api` resolves under that root the same way, and a root that loads MyST reads .md beside the suffixes conf.py declares, since MyST adds it when it loads, so a :doc: and a {doc} each reach a page written in the other format. Every other role is counted and left alone: {py:class}`Widget`, {func}`echo` and {issue}`4211` name a domain inventory Sphinx builds while it runs or a link template conf.py holds, so each is recorded as unsupported semantics rather than resolved, guessed, or reported missing. A plain link can name the same object, [Explicit text](#mypackage.MyClass). Where a {py:class} directive in the tree describes that class the name is already in the table and the link reaches the page holding it, and where nothing in the tree describes it the link stays a missing target, since reading any dotted fragment as an inventory name would swallow every real anchor break whose slug carries a dot.

Nothing here is read without a Sphinx declaration over the document, and two things count as one. A conf.py above the file is the first. The second is a page under one that renders the file in place of an {include}, since Sphinx parses an included file as part of the page holding the directive. That is how a changelog at the repository root writes the labels its site resolves, while the same file in a tree nothing includes stays the prose it looks like. The second reading says which files Sphinx parses and nothing about what each publishes: the including page’s own identity set is unchanged, so a fragment into it is answered exactly as before. The count says how many references the run saw rather than how many it answered.

Heading evaluation expands the closed local include subset in source order. An AsciiDoc include::path[] or option-free, document-level reStructuredText include participates when its literal relative target was already scanned under the same adapter; each nested path is relative to the file that includes it, except under a conf.py, where Sphinx reads every nested path from the page and so does this expansion. The same holds for everything else an included file writes: a file a Sphinx page includes, directly or through other included files, reads each relative reference from that page’s directory rather than its own, so an image, a :doc:, a download or an include in it names the page’s file, and one the page lacks is missing there. A file several pages include has to reach its target from each of them. An include carrying options is left out, since MyST’s relative-docs and relative-images read paths from the included file instead. An Antora partial is read the same way for its cross references and images, from the module of each page that includes it, while its own includes stay with the partial, which is how an Antora build reads them. An include selecting a tag or lines leaves the partial read from its own module, since that include renders only part of it. An MDX partial joins the same subset: a default import of a relative Markdown document rendered as an element, which is how Docusaurus composes one page out of several files, and the identities flow to the page rather than back to the partial. So a fragment written inside a document Docusaurus publishes no page for is undecided rather than absent: the identity it names belongs to whichever page renders the file, and one partial may be rendered into several. What a documentation router serves holds the names a content path excludes and what stays a path there. So does a MkDocs snippet line, --8<-- "path", under a tree that declares MkDocs, resolved from the directory holding that declaration rather than from beside the document. Each include line is also a reference of its own, since both generators expand it before Markdown reads the page, in a fence as much as in prose: the snippet line under that declaration, and an mdBook {{#include}}, {{#rustdoc_include}} or {{#playground}} in a page of the book’s source directory, read beside the page. So a deleted or renamed listing is a missing target, and one that changes under unchanged prose is a check. A backslash before an mdBook command leaves it text.

A selector after the path is read too. A line range, listing.rs:2:10 or code.py:1:4, becomes the line fragment L2-L10, so the start must fall inside the file and those lines are what the reference depends on; the end stops at the end of the file, the way every include grammar stops it. A name is a marker the file has to carry, spelled the way its own generator reads it: an mdBook ANCHOR: name, a snippet’s --8<-- [start:name], an AsciiDoc tag::name[] for include::file[tag=name], and for literalinclude the text a :start-after:, :start-at:, :end-before: or :end-at: names, or a def or class of the last name a :pyobject: spells. A marker the file does not carry is selection-not-found, missing like a deleted file. Comprehensive Rust includes exercise.rs:magnitude and exercise.rs:normalize, and the file carries neither anchor. A generator instruction under the same declaration, ::: pydantic.config, is an edge this engine reads and cannot follow, because what it pulls in is built by a program rather than held by the tree, so the page keeps the identities it writes itself and absence in it stays undecided. Two more spellings under that declaration are read the same way: a heading whose text is an <!-- md:setting name --> comment a hook expands, and a === "Title" content tab the tabbed extension slugs under settings that live in mkdocs.yml. Neither identity is in the tree, so neither page proves absence. An option-free literalinclude contributes no parsed headings. Its selection is checked the way a line fragment is: :lines: 5-8 must fall inside the file and tracks those lines alone, a list or an open end reads as the span from its first selected line to its last, and a :pyobject: or a start or end text is a marker checked the way an include’s is. The range is spelled the way a run with no forge, or a GitHub or Gitea one, spells a line fragment, so under a GitLab or Bitbucket Data Center identity it goes unchecked. The graph is bounded by references-per-document, parser-nesting, and aggregate-heading-anchor-evaluation-bytes-per-snapshot. A cycle, an unscanned or non-local target, a build-time attribute, include options, or a nested parser context leaves the identities collected up to that edge partial: a published identity can still resolve, but absence stays undecided rather than becoming a guessed missing anchor. Expanded AsciiDoc remains partial even when every edge is available because its document-attribute and conditional state is not reproduced; reStructuredText can prove absence inside the closed option-free subset. The same holds in the other direction for a chapter: an AsciiDoc document another one includes renders inside the book that includes it, and so does one an Antora component keeps among its partials or examples, so a cross reference in it may name an identity another chapter declares. Absence there stays undecided too, while a document nothing includes still proves it.

Resolution is exact, and the small rules matter. A trailing slash means the author promised a directory, so sub/ must be a tree and guide.md/ is a type mismatch even though guide.md exists. A GitHub or GitLab URL promises no kind at all: both forges redirect a blob URL naming a directory to its tree and back, and serve either with a trailing slash. A destination that normalizes to nothing names the repository root: . in a root document, .. one directory down, or a forge URL with nothing after its ref. Every snapshot holds the root and no repository path spells it, so the link is declined as unsupported-reference-semantics with reason: repository-root rather than called invalid. Percent-encoding is decoded exactly once: %252F stays as the literal three characters %2F instead of turning into a second slash. A percent escape may decode to bytes that are not text at all, and those bytes are simply the path. bad-%FF-name.md resolves against the tree entry carrying that exact byte, because Git names files in bytes and so does the resolver.

Fragments split by kind. Query strings are recorded as digests and acquire no semantics here. One narrow divergence is deliberate: a fragment whose escapes decode outside UTF-8 is dropped rather than digested, since carrying it would change the recorded identity of every existing observation for no resolution gain. A recognized numeric line fragment selects the inclusive raw lines. A range beyond the blob is resolution kind: missing with reason: line-fragment-out-of-range, reported as an explicit missing target. A valid range replaces the whole-file projection with only the selected bytes and file mode, so a change outside the range does not claim this occurrence’s dependency changed. Git LFS pointers and trees have no line selection and stay unsupported.

Every other fragment on a document target is a heading anchor, and a heading identity belongs to the renderer rather than to Markdown. Twelve rules are pinned, one per renderer or per configuration of one, and the resolver asks whether any of them would publish the anchor, counting the headings a document writes as raw HTML and the identities it declares outright, in raw HTML or in an attribute block, as well. A definition-list term is read beside them, because one renderer publishes an identity for a term on the counter its headings occupy. An anchor no rule publishes is kind: missing with reason: heading-anchor-not-found, an ordinary missing target; the row also carries near, the one published identity the fragment names apart from typography when exactly one exists and null otherwise. The fold covers the two spellings the pinned rules disagree on, case and the separator character, in two steps: case alone first, then case and the separator together, since the mdn rule publishes with _ what most rules publish with -. A duplicate suffix written _1 still folds together with -1 in the second step, and a lone reference over a verbatim-located fragment turns that neighbor into the finding’s fix. A fragment holding :~: carries a text directive, which a browser answers by finding text on the page, so it names no identity and is declined as an unsupported fragment. The union is deliberate: adding a rule can only grow what an anchor may match, and no repository policy narrows it. A document can add to it, by declaring an identity the way it would add a heading, which is an edit to the target that a reviewer reads rather than a setting that clears a finding. A footnote joins the union too, under the spellings six renderers publish for a note and its first call, fn:1 and fnref:1 among them. What thirteen renderers call a heading holds the rules, what each was checked against, and how far apart they are.

What the check will not do is judge on a parse that did not happen. A target that is not a parsing document class, an LFS pointer, a document the parser rejects, one the anchor budget cannot afford, or one no router publishes as a page keeps unsupported-reference-semantics, which now means exactly “not evaluated”. The projection stays the whole file: an anchor says where to look, not which bytes the reference depends on.

Version scope is equally narrow. The candidate is read, and a full immutable ID is read only from objects already present under the declared Git roots; unavailable objects are delegated for provider evidence instead of fetched. --default-branch-ref supplies a second trusted spelling so the resolver can split a ref from its path without guessing, and a URL naming the default branch while the candidate ref differs is still unsupported-version-scope. Site generators and language-aware tools still own route and symbol semantics. A complete site-build producer can contribute exact positive source-backed or generated route, anchor, and fragment-aware terminal-redirect facts for the candidate; absent, ambiguous, or stale mappings remain unsupported, while conflicting ownership and broken declared redirects become build defects retaining every available source. Guessing beyond that evidence would turn honest ignorance into a false pass. The resolver tests pin these distinctions.

Each resolved target is read from the object store and hashed, so the comparison knows the exact selected bytes and file mode on both sides. Numeric positions do not prove that those bytes still mean what the prose claims; they only make movement and byte drift observable. A symlink or submodule target, or a path beneath one, is unsupported-target-kind, because following one leaves the world of exact bytes where the guarantees live. A Git LFS pointer file is recognized and its committed pointer bytes are hashed. Those bytes include the declared OID, so an OID-text change is observable; a backing-store change that leaves the committed pointer unchanged is not.

Last change: , commit: 0987dade