lacs-project / lacs-project/sysknife

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

Open
#371 3 comments 0 reactions 1 assignee Claimed by @sonalisrisivani View on GitHub
claimed documentation easy good first issue
Dominant language
Rust
Stars
12
Forks
19
Avg merge
18h 57m
Merged PRs (30d)
116

Description

`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:559` 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:492` 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:492`.
- 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:559` 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](https://github.com/lacs-project/sysknife/blob/main/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.

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.