Skip to content

docs-s2: ship the manual with the app, and bridge the reference pages into it (#346) - #560

Merged
JArmandoAnaya merged 1 commit into
mainfrom
docs/346-local-reader-packaging
Aug 2, 2026
Merged

docs-s2: ship the manual with the app, and bridge the reference pages into it (#346)#560
JArmandoAnaya merged 1 commit into
mainfrom
docs/346-local-reader-packaging

Conversation

@JArmandoAnaya

Copy link
Copy Markdown
Contributor

Closes #346 (docs-s2) — ADR-0009.

A release now carries a browser-openable copy of the full illustrated manual, reachable from Help ▸ Open Manual in Browser, alongside the in-app F1 book it already had. F1 and the .qch pipeline are behaviourally unchanged.

1. build:local — the file:// reader

Opening from file:// forces three things, and each is enforced rather than assumed:

  • format: 'file' — a browser will not serve index.html for a bare directory over file://, so pages are <slug>.html.
  • Fully relative references — a root-absolute /… resolves against the filesystem root and 404s.
  • Search off — Pagefind fetches its index over XHR, which file:// blocks. Switching it off removes the UI with it, so no search box ships that cannot answer; the landing page says where search lives instead.

Integration vs. post-processor (the spec asks for a justification). A maintained relative-links integration would be one more npm dependency inside a licence gate this project treats as law, for a transform that is ~60 lines of string work. scripts/relativize.mjs is dependency-free and idempotent by construction — it only rewrites values beginning with / and produces none — and the test proves that by running it twice and comparing bytes rather than asserting it in a comment.

It also treats a reference matching no file in the build as an error rather than rewriting it quietly. That is what catches a link to a page that was renamed, and it is how the dangling favicon below was found.

The gate checks the output, not the transform. check-local-build.mjs scans the built HTML, so it still fails if relativize.mjs were removed, reordered out of the build, or silently skipped a page.

2. Release packaging

ROADMAKER_BUNDLE_MANUAL (default OFF) with ROADMAKER_MANUAL_DIR. CMake never invokes npm: the release job builds the manual with Node and hands CMake a finished directory, so a developer build still needs no Node. Configuring with the option ON and no directory is a hard FATAL_ERROR, not a silently empty install.

Layout per ADR-0009 — Contents/Resources/manual/ (macOS), share/roadmaker/manual/ (Linux), manual/ beside the exe (Windows).

3. Help-menu action

Registered through shortcut_registry as Id::OpenManualunbound, so it renders nothing on the shortcuts page and F1 keeps belonging to the in-app viewer.

The layout question is a pure function of (executable dir, platform), so all three platforms' answers are checked by one headless test rather than only by the release job on a machine nobody is watching. The dev-build fallback is the same function returning nullopt: it shows a pointer at the online docs and says F1 still works — never a raw error, never silence.

4. The reference → guide bridge

A reference page may end with a ## Full guide section whose first link is its tutorial. The heading is the marker, so the authored link stays an ordinary relative Markdown link that renders correctly on GitHub; each generator retargets it for its own output:

Output The bridge becomes
the site an ordinary site link
the .qch rmmanual:<slug>, resolved at runtime against the packaged manual and opened in the system browser

The manual's path is only knowable at runtime, which is why the compiler emits a scheme rather than a URL. Applied to the 13 reference pages that have a matching tutorial; none were invented.

The slug arrives from a generated document, so manual_page_for refuses one that climbs out of the manual, and refuses a backslash rather than normalising it (it would mean a separator on exactly one platform).

Two defects fixed on the way

  • The adapter sent in-guide ../ links to GitHub. ../ means "up one directory", not "out of the guide" — and since the tier split the guide has subdirectories. So tutorials/*../reference/* left the site for GitHub on 15 existing links, and the bridge could never have been "an ordinary link" without fixing it. It now resolves the target first and only falls back to the repo blob when it genuinely lands outside.
  • The help build watched the wrong directory. CONFIGURE_DEPENDS still globbed tutorials/*.md (which the pipeline stopped reading in docs-s1) and missed reference/*.md (which it does read), so editing a reference page left the shipped collection stale — and the new bridge gate would have passed or failed depending on whether someone happened to reconfigure.
  • A favicon that was linked but never shipped. Starlight links one unconditionally and the project has no public/, so all 40 pages carried a dangling reference — a console 404 on a server, a genuinely broken reference under file://. It now points at the app's own icon, copied from editor/resources/branding/.

#297 is untouched and stays open. It is the help compiler's ../ rewriting (helpc/render.cpp); the adapter is a different pipeline in a different language, and the bridge path never reaches the generic rewriter.

Verification

  • 3313/3313 ctest green (core + editor), ci-macos, -Werror.
  • 15 new gtests — install layout on all three platforms, slug resolution and traversal refusal, bridge parsing, render retargeting, and the two committed-content gates.
  • 4/4 script tests; build:local and the web build both green; licence gate clean (374 packages, all permissive).
  • git ls-files '*.cpp' '*.hpp' | xargs clang-format --dry-run --Werror clean (CI's exact command).
  • Link check: differential against an extracted main — 13 findings before, the same 13 after, so these changes add none.

Sabotage results — six, each with a checked build exit code

Sabotage Result
build without relativize.mjs check-local-build fails (21 problems)
rename tutorials/shaping-lanes.md C++ bridge gate fails, naming 5 pages
…the same rename the site adapter fails independently
pagefind: true in the local build check-local-build fails
change the emitted scheme render test fails
wrong Linux layout / drop the .. refusal the matching locator test fails

One sabotage (stubbing the bridge rewrite to if (false)) did not compile, so the test ran a stale binary and reported green. It was replaced with one that compiles. Recording it because the misleading result, not the fixed one, is the reusable lesson.

Out of scope, respected

The sharp/LGPL posture is untouched — the overrides stub and passthroughImageService() are exactly as they were, and no dependency was added at all. No AWS or hosting resources; CI builds dist as an artifact and stops. No tags, no releases. No labels, milestones, or release-gate text. Versioned publishing is #347; the authoring guide and dependency cadence are #348.

Outstanding — requires a human

Two acceptance items need a display and a packaged build, so they are not attemptable in CI and are not claimed:

  • Opening the local build from file:// and navigating between pages and images by hand, on macOS/Linux/Windows.
  • Triggering Help ▸ Open Manual in Browser in a packaged build and confirming the browser opens, with F1 still opening the in-app guide in the same build.

Everything mechanically checkable about both is gated: the release smoke test asserts the manual exists at the resolved layout on all three platforms plus a page beneath it, and check-local-build.mjs asserts no root-absolute reference survives. What remains is the human confirmation the release gate asks for.

npm audit reports 1 high on the pinned astro@5.14.1; every advisory concerns the dev server, SSR/middleware, server islands, adapters or the image endpoint — none of which exist in a statically prerendered, adapter-less build. Left for #348, whose deliverable is dependency hygiene and the update cadence.

…ence pages into it (p-docs-s2)

A release now carries a browser-openable copy of the full illustrated manual,
reachable from Help ▸ Open Manual in Browser, alongside the in-app F1 book it
already had. F1 and the .qch pipeline are behaviourally unchanged.

`npm run build:local` produces the offline reader. Opening from file:// forces
three things, each enforced rather than assumed: pages are emitted as
<slug>.html, because a browser will not serve index.html for a bare directory
over file://; every reference is rewritten relative, because a root-absolute
/... resolves against the filesystem root; and search is off, because Pagefind
fetches its index over XHR. Switching search off removes the UI with it, and the
landing page says where search lives — never a search box that does nothing.

check-local-build.mjs verifies the built OUTPUT rather than the transform, so
the gate still fails if the rewriting step were dropped from the build. The
relativizer treats a reference matching no file as an error rather than
rewriting it quietly, which is what catches a link to a renamed page; it is
idempotent, and the test proves that by running it twice and comparing bytes.
A maintained relative-links integration was rejected in favour of ~60 lines of
string work: every npm package here is a permanent obligation under the licence
gate.

Packaging is opt-in (ROADMAKER_BUNDLE_MANUAL, default OFF) and CMake never
invokes npm — the release job builds the manual with Node and passes the
finished directory in as ROADMAKER_MANUAL_DIR, so a developer build still needs
no Node. The install layout is resolved by a pure function of (executable
directory, platform), so all three platforms' answers are checked by one
headless test instead of only by the release job; the release smoke test then
asserts the manual at that same layout on each platform, plus a page beneath it,
since an empty manual/ would pass an index-only check.

A reference page may end with a `## Full guide` section linking its tutorial.
The heading is the marker, so the authored link stays ordinary Markdown that
renders on GitHub; the site emits a normal link and the help compiler emits
rmmanual:<slug>, which the viewer resolves against the packaged manual at
runtime and opens externally — the path is only knowable at runtime, which is
why the compiler emits a scheme rather than a URL. Applied to the 13 reference
pages with a matching tutorial. The slug arrives from a generated document, so
manual_page_for refuses one that climbs out of the manual.

Two defects surfaced on the way and are fixed here. The adapter treated any
../-prefixed link as leaving the guide, so since the tier split every tutorial's
link to a reference page left the site for GitHub; it now resolves the target
first and only falls back to the repo blob when it lands outside. And the help
build's dependency glob still watched tutorials/ while missing reference/, so
editing a reference page left the shipped collection stale — which would have
made the new bridge gate depend on whether someone had reconfigured. #297 is the
help compiler's own ../ rewriting and is untouched.

Starlight links a favicon unconditionally and the project shipped none, so all
40 pages carried a dangling reference — a console 404 on a server, a real broken
reference under file://. It now points at the app's own icon.

Closes #346
@JArmandoAnaya
JArmandoAnaya merged commit 09593e8 into main Aug 2, 2026
18 checks passed
@JArmandoAnaya
JArmandoAnaya deleted the docs/346-local-reader-packaging branch August 2, 2026 14:01
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.

docs-s2: local reader build + release packaging + Help-menu action + bridge links

1 participant