Skip to content

Seven links in the published book point at pages mdBook never wrote #371

Description

@vladimirrott

Rechecked 2026-09-23 at 0ad26871. docs/cli.md:568 is now :572 and
docs/the-audit-chain.md:492 is now :501, corrected below everywhere they appear. All seven links still resolve at source and
still point at pages mdBook never writes.

mdbook build rewrites every relative .md link to .html. A link that points
out of docs/, and a link to a page missing from docs/SUMMARY.md, both come
out pointing at a file the build never wrote. Seven links across four pages land
on nothing. The repo runs markdown-link-check in CI, and it cannot see any of
this: it resolves the Markdown source, where all seven targets exist on disk. It
would not read these four pages either, which is #372.

Reproduced at 7d19864 with mdBook 0.4.40, the version .github/workflows/docs.yml
pins and installs:

$ mdbook --version
mdbook v0.4.40
$ mdbook build -d /tmp/sk-book
2026-09-05 10:49:20 [INFO] (mdbook::book): Book building has started
2026-09-05 10:49:20 [INFO] (mdbook::book): Running the html backend
$ grep -o 'href="[^"]*user-stories[^"]*"' /tmp/sk-book/cli.html | sort -u
href="user-stories.html"
$ ls /tmp/sk-book/user-stories.*
ls: cannot access '/tmp/sk-book/user-stories.*': No such file or directory

Walking every href in the built tree, with print.html skipped because it
duplicates the rest:

$ cd /tmp/sk-book
$ python3 - <<'PY'
import re, pathlib, urllib.parse
root = pathlib.Path('.')
for html in sorted(root.rglob('*.html')):
    if html.name == 'print.html':
        continue
    for href in re.findall(r'href="([^"]+)"', html.read_text(errors='replace')):
        if href.startswith(('http://', 'https://', '#', 'mailto:', '//')):
            continue
        target = href.split('#')[0].split('?')[0]
        if not target.endswith('.html'):
            continue
        if not (html.parent / urllib.parse.unquote(target)).resolve().exists():
            print(f"{html}\t{href}")
PY
cli.html	../README.html
cli.html	user-stories.html
contributing/CONTRIBUTING.html	../../SECURITY.html
contributing/testing.html	ubuntu-vm-testing.html
contributing/testing.html	../testing/user-stories.html
contributing/testing.html	../../HACKING.html
the-audit-chain.html	../SECURITY.html#audit-anchoring-in-the-default-deployment

I did not check these against the published site. Verifying them there needs an
outbound request this account is not allowed to make, so the evidence above is a
local build of the same tree with the same mdBook version the workflow installs.

Why it matters

docs/cli.md:572 offers the reader the user stories, which is the document that
explains what SysKnife does end to end. docs/contributing/testing.md:333 sends
a contributor to the VM recording procedure. Both are the moment somebody
decided to go deeper, and both give them a 404 on a site whose whole job is to
answer that.

All seven targets are present in the tree, which is why a source-level link
checker passes over them:

$ ls -l README.md SECURITY.md HACKING.md docs/user-stories.md docs/testing/user-stories.md docs/contributing/ubuntu-vm-testing.md 2>&1
-rw-rw-r-- 1 entropia entropia 18618 sep  4 07:01 docs/contributing/ubuntu-vm-testing.md
-rw-rw-r-- 1 entropia entropia 19617 sep  4 07:01 docs/testing/user-stories.md
-rw-rw-r-- 1 entropia entropia 15508 sep  4 07:01 docs/user-stories.md
-rw-rw-r-- 1 entropia entropia 34220 sep  4 07:01 HACKING.md
-rw-rw-r-- 1 entropia entropia 22548 sep  4 07:01 README.md
-rw-rw-r-- 1 entropia entropia 20463 sep  4 07:01 SECURITY.md

Fix the two classes differently.

Four links point outside docs/. README.md, SECURITY.md and HACKING.md
live at the repo root, so mdBook cannot include them at any depth of ../, and
the .md to .html rewrite happens anyway. One file already
does it both ways: docs/the-audit-chain.md:267 links SECURITY.md with a
relative path and breaks, and docs/the-audit-chain.md:501 links the same file
as https://github.com/lacs-project/sysknife/blob/main/SECURITY.md and works.

Three links point at documents that exist and are unlisted.
docs/user-stories.md, docs/testing/user-stories.md and
docs/contributing/ubuntu-vm-testing.md are all in the tree. mdBook renders only
what SUMMARY.md names, so it copied them across as raw Markdown and wrote no
HTML for any of them.

Scope

  • Repoint the four outward links at https://github.com/lacs-project/sysknife/blob/main/...,
    copying the form already used at docs/the-audit-chain.md:501.
  • Decide, per document, whether the three unlisted files belong in the book. Two
    of them are named by CLAIM_FILES in scripts/check_evidence_claims.py, so the
    project already treats them as load-bearing. Adding them to SUMMARY.md is the
    smaller change; linking them by URL is defensible for anything meant to stay
    out of the published book.
  • The trap: docs/user-stories.md and docs/testing/user-stories.md are two
    different documents that share a basename.
$ md5sum docs/user-stories.md docs/testing/user-stories.md
17cb3f780131c1c62baa75bc0b62ca30  docs/user-stories.md
7ff722164b0ec573ab89e09dc9a92609  docs/testing/user-stories.md
$ wc -l docs/user-stories.md docs/testing/user-stories.md
  438 docs/user-stories.md
  593 docs/testing/user-stories.md

docs/cli.md:572 wants one and docs/contributing/testing.md:388 wants the
other. Read both before you pick a target for either link.

Tests first

A guard that builds the book and fails on any internal .html link with no file
behind it. The walk above is the whole implementation. Two things decide whether
it is worth having:

Assert the walk found links at all. A selector that matches nothing reports
zero broken links and passes, which is the failure this tracker keeps meeting;
#362 is the current example, and it cites #315 as the same defect one job over.
Count the hrefs examined and fail when that count is zero.

Prove it bites. Delete one line from docs/SUMMARY.md, rebuild, and watch the
guard name the page that lost its target. Then put it back and watch it go green.

Where it runs is a real choice. .github/workflows/docs.yml already installs
mdBook and builds the book, so a step there costs nothing and catches the break
on the commit that ships it. A tests/release/*.test.sh reaches ci-local.sh
and every contributor, and has to handle a machine with no mdBook: skip loudly,
naming the check that did not run, rather than passing quietly. #346 covers why
a silent local skip is its own defect.

Difficulty

easy. The seven fixes are one line each. The guard is a twenty-line walk over
files the docs job already produces, and deciding which of the three documents
join SUMMARY.md is the only judgement in it.

Getting started

CONTRIBUTING.md
has the build and test commands. This one needs no VM, no daemon and no LLM key:
cargo install mdbook --version 0.4.40 (or the release tarball the workflow
downloads) and mdbook build is the whole environment. No CLA and no copyright
waiver. The project is MIT.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

claimedSomeone has said in the thread that they are working on thisdocumentationImprovements or additions to documentationeasyDifficulty: self-contained, no deep context neededgood first issueGood for newcomers

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions