Skip to content

Fix: make --strict catch broken anchors, and fix the six it was hiding - #1581

Merged
ChaoWao merged 1 commit into
hw-native-sys:mainfrom
ChaoWao:docs/enforce-anchor-validation
Jul 30, 2026
Merged

Fix: make --strict catch broken anchors, and fix the six it was hiding#1581
ChaoWao merged 1 commit into
hw-native-sys:mainfrom
ChaoWao:docs/enforce-anchor-validation

Conversation

@ChaoWao

@ChaoWao ChaoWao commented Jul 29, 2026

Copy link
Copy Markdown
Collaborator

Summary

validation.links.anchors defaults to info in MkDocs 1.6, and --strict only fails on warn or above. A link to a heading that was renamed or never existed therefore built clean and 404'd in the reader's browser. Six were live on main:

Link Problem
ci.md#sanitizer-sim (3 call sites) never existed — the heading is ### Nightly sanitizer sweep
orchestrator.md#5-ring-slot--per-scope-heap-allocator doubled hyphen
l0-swimlane-profiling.md#34-viewing--insight-vs-perfetto doubled hyphen
l0-swimlane-profiling.md#72---set-arg-floor-... (4 call sites) tripled hyphen
args-dump.md#8-faq--debug-guide doubled hyphen

The last four were not typos

GitHub and MkDocs slugify differently. A heading like ## 8. FAQ / Debug Guide drops the / and leaves two spaces; GitHub renders that as 8-faq--debug-guide, MkDocs collapses it to 8-faq-debug-guide. So those links were correct in the GitHub view and broken on the published site — which is why nobody noticed, and why simply repointing them would have moved the breakage rather than removed it.

The fix is to take the punctuation out of the four headings so both dialects agree:

Was Now
## 5. Ring (slot + per-scope heap allocator) ## 5. Ring (slot and per-scope heap allocator)
### 3.4 Viewing — Insight vs Perfetto ### 3.4 Viewing: Insight vs Perfetto
### 7.2 `--set-arg` floor for a loop count … ### 7.2 Arg floor for a loop count …
## 8. FAQ / Debug Guide ## 8. FAQ and Debug Guide

Every referring link is updated with them.

Then the guard goes on

validation:
  links:
    anchors: warn

Verified it bites — pointing sanitizers.md at ci.md#does-not-exist gives:

WARNING - Doc file 'sanitizers.md' contains a link 'ci.md#does-not-exist',
          but the doc 'ci.md' does not contain an anchor '#does-not-exist'.
Aborted with 1 warnings in strict mode!
exit=1

Correcting my own comment

.github/workflows/docs.yml has claimed since #1569 that --strict "fails on a dead intra-docs link, a page missing from nav, or a bad anchor". The first two were true; the third was not. This PR makes the sentence accurate rather than editing it down.

Not fixed here

Five links point at directories with no index page — hardware/, troubleshooting/, remote-l3-worker-design/, troubleshooting/device-error-codes/. They resolve in the GitHub view and 404 on the site, and MkDocs reports them at INFO under unrecognized_links. Fixing them means adding index pages, not editing links, so it is a separate change; unrecognized_links is deliberately left at its default until then.

Testing

  • mkdocs build --strict — exit 0, zero warnings
  • Negative test: deliberately broken anchor → exit 1 (output above), then reverted
  • Generated ids read out of the built HTML rather than guessed, e.g. id="8-faq-and-debug-guide"
  • All four reworded headings verified to produce the same slug under both dialects
  • markdownlint-cli2 --config tests/lint/.markdownlint.yaml — 0 errors on all six changed docs

Docs and docs-tooling only.

`validation.links.anchors` defaults to `info`, which `--strict` ignores. A
link to a heading that was renamed or never existed therefore built clean and
404'd in the reader's browser. Six were live:

| Link | Target |
| ---- | ------ |
| `ci.md#sanitizer-sim` (3 sites) | never existed; the heading is `Nightly sanitizer sweep` |
| `orchestrator.md#5-ring-slot--per-scope-heap-allocator` | doubled hyphen |
| `l0-swimlane-profiling.md#34-viewing--insight-vs-perfetto` | doubled hyphen |
| `l0-swimlane-profiling.md#72---set-arg-floor-...` | tripled hyphen |
| `args-dump.md#8-faq--debug-guide` | doubled hyphen |

The last four were not typos. GitHub and MkDocs slugify differently: a heading
like `## 8. FAQ / Debug Guide` drops the `/` and leaves two spaces, which
GitHub renders as `8-faq--debug-guide` and MkDocs collapses to
`8-faq-debug-guide`. Those links were correct in the GitHub view and broken on
the published site.

Repointing them would only have moved the breakage, so the four headings lose
the punctuation that splits them instead — `+` to `and`, an em dash to a
colon, `/` to `and`, and a leading `--set-arg` out of the heading text. Both
slug dialects now agree, and every referring link is updated.

With the six fixed, `anchors: warn` is enabled. Verified it bites: pointing
`sanitizers.md` at `ci.md#does-not-exist` fails the build with exit 1.

This also makes the comment on the build step in `.github/workflows/docs.yml`
true. It has claimed since hw-native-sys#1569 that `--strict` fails on a bad anchor; it did
not.

Still reported at INFO and not addressed here: five links to directories with
no index page (`hardware/`, `troubleshooting/`, `remote-l3-worker-design/`,
`troubleshooting/device-error-codes/`). They resolve in the GitHub view and
404 on the site; fixing them means adding index pages, not editing links.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 29, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@ChaoWao, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 56 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 70bc5cdf-fa3f-4e94-acd2-b3e1110c1a49

📥 Commits

Reviewing files that changed from the base of the PR and between 83b75f0 and 3b30237.

📒 Files selected for processing (7)
  • docs/dfx/args-dump.md
  • docs/dfx/l0-swimlane-profiling.md
  • docs/orchestrator.md
  • docs/sanitizers.md
  • docs/testing.md
  • docs/troubleshooting/local-timeout-defaults.md
  • mkdocs.yml

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@ChaoWao
ChaoWao merged commit 8bcb0f6 into hw-native-sys:main Jul 30, 2026
32 of 33 checks passed
@ChaoWao
ChaoWao deleted the docs/enforce-anchor-validation branch July 30, 2026 01:26
ChaoWao added a commit to ChaoWao/simpler-fork that referenced this pull request Jul 30, 2026
`detect-changes` carried two definitions of "changing this cannot change what
the code does". `NON_CODE` covered `docs/ .docs/ .claude/ .gitignore
.pre-commit-config.yaml *.md`; a separate `docs_only` meant only "every file
ends in .md". The arch-specific jobs gated on the wide one and the
**self-hosted hardware unit tests gated on the narrow one**, so a PR touching
just `.gitignore` skipped every scene test and still booked `ut-a2a3` and
`ut-a5` on real silicon.

`NON_CODE` also did not know about docs tooling living outside `docs/`.
`mkdocs.yml` is pure docs config at the repo root, and it was the one file in
PR hw-native-sys#1581 (six `docs/*.md` plus `mkdocs.yml`) that survived every filter — that
PR ran the entire matrix, including `st-onboard-a2a3`, `st-onboard-a5`,
`ut-a2a3` and `ut-a5`, and a pre-existing flake in the a5 L3 close path then
reddened it.

`docs/ci.md` documented the narrow `docs_only` as deliberate, on the grounds
that "a change to `.pre-commit-config.yaml` or a `.claude/` script can still
change what CI does". Neither holds. `pre-commit` is ungated and every gated
job declares `needs: [detect-changes, pre-commit]`, so that config's only
consumer always runs — `ut` and friends do not read it. And no workflow invokes
anything under `.claude/`: `grep -rn '\.claude' .github/workflows/` finds only
comments. The three scripts there are local/agent tooling.

So there is now one set. `NON_CODE` gains `mkdocs.yml` and
`.github/workflows/docs.yml` — the latter because `docs.yml` runs
unconditionally on every PR and is therefore its own gate. `ci.yml` stays out
deliberately: a change to the gates must run everything, including whatever it
just switched off. `docs_only` becomes `non_code_only`, defined as "no changed
file falls outside NON_CODE", and all four consumers move with it; the old name
would have been a lie once the set widened past markdown.

The empty-diff case is also unified. The emptiness test used to live inside the
`non_code_only` branch alone, which left that flag false — UT and packaging ran
— while both arch flags independently came out false too, skipping every scene
test. Two axes failing in opposite directions on the same input, under a doc
claiming it "runs the full matrix". A single guard now short-circuits before
any pattern runs: attribution is impossible, so `non_code_only=false`, both
arch flags `true`, return.

The arch outputs otherwise need no change — they already subtract `NON_CODE`
before deciding, so they inherit the widening.

Replayed the pattern against twelve file lists. Newly skipping: docs-only,
`.gitignore`-only, `.claude/`-only, `docs/_hooks/*.py`,
`workflows/docs.yml`-only. Still running everything: `ci.yml`, `src/common/`,
`python/simpler/`, and an empty diff. Still single-sided: `src/a5/` only,
`examples/a2a3/` only, and `docs/` mixed with `src/a5/`.

`docs/ci.md` moves with the code: principle 6, the job matrix, the flag list,
and the two paragraphs whose rationale no longer describes the design.

`.claude/rules/ci-change-detection.md` records the invariants rather than the
patterns: one vocabulary, membership by effect rather than path, never gate an
expensive job more loosely than a cheap one, why the two axes are layered, fail
open once at the top, why a schedule trigger needs its own workflow, and that a
gating bug surfaces as a green check — so it has to be verified by looking at
what ran, not by reading the regex. It also names the two jobs that are
deliberately ungated, `detect-changes` and `pre-commit`, rather than claiming
every job is gated.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
ChaoWao added a commit that referenced this pull request Jul 30, 2026
…1589)

`detect-changes` carried two definitions of "changing this cannot change what
the code does". `NON_CODE` covered `docs/ .docs/ .claude/ .gitignore
.pre-commit-config.yaml *.md`; a separate `docs_only` meant only "every file
ends in .md". The arch-specific jobs gated on the wide one and the
**self-hosted hardware unit tests gated on the narrow one**, so a PR touching
just `.gitignore` skipped every scene test and still booked `ut-a2a3` and
`ut-a5` on real silicon.

`NON_CODE` also did not know about docs tooling living outside `docs/`.
`mkdocs.yml` is pure docs config at the repo root, and it was the one file in
PR #1581 (six `docs/*.md` plus `mkdocs.yml`) that survived every filter — that
PR ran the entire matrix, including `st-onboard-a2a3`, `st-onboard-a5`,
`ut-a2a3` and `ut-a5`, and a pre-existing flake in the a5 L3 close path then
reddened it.

`docs/ci.md` documented the narrow `docs_only` as deliberate, on the grounds
that "a change to `.pre-commit-config.yaml` or a `.claude/` script can still
change what CI does". Neither holds. `pre-commit` is ungated and every gated
job declares `needs: [detect-changes, pre-commit]`, so that config's only
consumer always runs — `ut` and friends do not read it. And no workflow invokes
anything under `.claude/`: `grep -rn '\.claude' .github/workflows/` finds only
comments. The three scripts there are local/agent tooling.

So there is now one set. `NON_CODE` gains `mkdocs.yml` and
`.github/workflows/docs.yml` — the latter because `docs.yml` runs
unconditionally on every PR and is therefore its own gate. `ci.yml` stays out
deliberately: a change to the gates must run everything, including whatever it
just switched off. `docs_only` becomes `non_code_only`, defined as "no changed
file falls outside NON_CODE", and all four consumers move with it; the old name
would have been a lie once the set widened past markdown.

The empty-diff case is also unified. The emptiness test used to live inside the
`non_code_only` branch alone, which left that flag false — UT and packaging ran
— while both arch flags independently came out false too, skipping every scene
test. Two axes failing in opposite directions on the same input, under a doc
claiming it "runs the full matrix". A single guard now short-circuits before
any pattern runs: attribution is impossible, so `non_code_only=false`, both
arch flags `true`, return.

The arch outputs otherwise need no change — they already subtract `NON_CODE`
before deciding, so they inherit the widening.

Replayed the pattern against twelve file lists. Newly skipping: docs-only,
`.gitignore`-only, `.claude/`-only, `docs/_hooks/*.py`,
`workflows/docs.yml`-only. Still running everything: `ci.yml`, `src/common/`,
`python/simpler/`, and an empty diff. Still single-sided: `src/a5/` only,
`examples/a2a3/` only, and `docs/` mixed with `src/a5/`.

`docs/ci.md` moves with the code: principle 6, the job matrix, the flag list,
and the two paragraphs whose rationale no longer describes the design.

`.claude/rules/ci-change-detection.md` records the invariants rather than the
patterns: one vocabulary, membership by effect rather than path, never gate an
expensive job more loosely than a cheap one, why the two axes are layered, fail
open once at the top, why a schedule trigger needs its own workflow, and that a
gating bug surfaces as a green check — so it has to be verified by looking at
what ran, not by reading the regex. It also names the two jobs that are
deliberately ungated, `detect-changes` and `pre-commit`, rather than claiming
every job is gated.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant