Browse Docs pages
This page is the authoritative reference for how Rotkeeper drives the native Oliver renderer โ a small, freestanding CommonMark/Textile/Cooklang parsing and rendering library in Zig, and the successor to the Apex renderer. It records the stable contract between the Oliver binary and rc-oliver-adapter.sh.
Executable discovery
rc-render.sh locates Oliver in this order:
RK_OLIVER_BINenvironment variable (explicit override).oliverresolved viaPATH.
If neither yields an executable file, render fails with exit 1 and prints a setup message covering installation and the environment variable.
The binary contract
Oliver is invoked once per source file, reading the file on stdin. The input language comes from input_format in bones/config/rotkeeper.yaml (markdown default, textile and cooklang alternatives), overridden to textile for any source whose extension is .textile and to cooklang for any source whose extension is .cook; the adapter and preflight pass the resulting format through on every invocation. The output profile comes from render_profile in bones/config/rotkeeper.yaml (html default, xhtml opt-in), overridden per page by a render_profile frontmatter key; only an xhtml profile appends --to xhtml, so the default invocation is byte-identical to the html-only contract:
oliver render --from markdown < file.md > body.html 2> warnings.log
oliver render --from textile < file.md > body.html 2> warnings.log
oliver render --from textile < file.textile > body.html 2> warnings.log
oliver render --from cooklang < file.md > body.html 2> warnings.log
oliver render --from cooklang < file.cook > body.html 2> warnings.log
oliver render --from markdown --to xhtml < file.md > body.xhtml 2> warnings.log
| Aspect | Contract |
|---|---|
| Invocation | oliver render --from <markdown|textile|cooklang> [--to <html|xhtml>] โ one file per invocation, stdin โ stdout; format comes from input_format in bones/config/rotkeeper.yaml (default markdown, validated to markdown/textile/cooklang at environment load; anything else warns and falls back to markdown). A source with a .textile extension always invokes --from textile and one with a .cook extension always invokes --from cooklang, overriding the config default for that file. The output profile comes from render_profile in bones/config/rotkeeper.yaml (default html, validated to html/xhtml at environment load; anything else warns and falls back to html), overridden per page by a render_profile frontmatter key; --to xhtml is appended only when the effective profile is xhtml, so the default invocation carries no --to flag and is byte-identical to the pre-XHTML contract. --to is rejected by Oliver on serialize/scale/menu |
| Input | Markdown, Textile, or Cooklang on stdin โ a leading YAML frontmatter block is stripped by Oliver (oliver meta --from <fmt> --format json extracts it; oliver render auto-strips) |
| stdout | Rendered body HTML fragment (no full page wrapping) โ an XHTML fragment under --to xhtml (no DOCTYPE, no document wrappers; see XHTML output profile) |
| stderr | Non-fatal renderer warnings; forwarded through the adapter as warnings, never into the page body. Under --to xhtml, raw HTML fails closed on stderr with error.RawHtmlNotXmlWellFormed and an actionable hint, and the page aborts with exit 1 |
| Exit 0 | Success |
| Exit 1 | Render failure (e.g. missing input, or raw HTML under --to xhtml); page is aborted |
| Version | Oliver's CLI is provisional and has no stable release yet, so Rotkeeper pins an exact source revision: CI and scripts/setup.sh install the binary published by the upstream rolling builds release and verify it reports exactly commit <OLIVER_PIN>; the Zig source build remains the fallback (the OLIVER_PIN variable in setup.sh). Moving the pin is a deliberate act โ upgrade it, re-run the harness, and update this table. The pin moved 2026-08-21 from 9ad86a3 (9ad86a3763b8bd2f227fd5da94be9fc8ea5fa5fc) via 06dd640 (06dd6403c505b4863a54c548c978e494b55eb759) to land wrap fix #115 (PR #116, wrap parseArgs); the same-day move from 6edb520c to 9ad86a3 landed Phase 6 S1+S2+S3+S4+S5 (oliver meta #107 9ad86a3, wrap #108, render links #109, plan+manifest #110) โ frontmatter, template, link rewriting, output planning, manifest now Oliver-owned with Bash dispatch/env/packaging retained. The prior move (2026-08-15) from c8a8e06 to 6edb520c adopted the upstream builds release (prebuilt binaries + published sha256sums.txt, download-first with checksum and --version commit verification), plus the --version stdout fix, GFM footnote reference/backref fixes, and the XHTML footnote-attribute serialization fix โ Markdown/Textile/Cooklang parsing untouched, footnote/task-list literals remain out of scope. The move before that (2026-08-14) from e314dbbe to c8a8e06 had picked up the XHTML output profile (--to html|xhtml, oliver PR #54, docs/XHTML.md) โ same semantics, different serialization bytes; Markdown/Textile/Cooklang parsing untouched, CommonMark 652/652 and Cooklang 60/60 gates unchanged โ plus the audit fixes #55โ#58 (NUL โ U+FFFD under the XHTML profile, CLI subcommand grammar with --to render-only). The move before that (2026-08-13) from 22b3c779 to e314dbbe had added the Cooklang frontend (CK1) plus CK2โCK5. The 2026-08-27 bump from 06dd640 to 8460f28 landed the shared template contract v2 (rotkeeper #244): wrap interpolates the extended metadata tokens (version, subtitle, tags, asset_meta, navigation, warnings) from --meta-json (html-escaped, $if$-gated), with the adapter feeding version from bones/config/version and subtitle/tags/asset_meta from the source frontmatter. 8460f28 is the merge commit of oliver PR #126 (feature commit 6db830e) โ the builds release embeds the merge SHA. The same-day bump from 8460f28 to 3f05bac landed the v3 generic hook (rotkeeper #269, oliver #127, feature commit 84d0d6d): wrap interpolates any key present in --meta-json (strings html-escaped, null โ empty, other scalars stringified, objects/arrays compact-JSON), so adding a frontmatter field needs no upstream change โ the adapter merges it into wrap_meta with typed keys winning. preflight's live smoke render (in the configured format and profile) remains the behavioral safety net on top of the pin |
The binary is deliberately narrow: it converts Markdown, Textile, or Cooklang to a body fragment. Phase 6 complete (S1 meta, S2 wrap, S3 links, S4 plan, S5 manifest): frontmatter, template, link rewriting, output planning, manifest are Oliver-owned; Bash retains dispatch, environment, filesystem boundaries, orchestration, packaging.
Rendered Markdown surface
Oliver implements the CommonMark 0.31.2 specification; its own conformance harness scores 652/652 on the normative corpus (see the Oliver docs). That is the contract for what render produces:
- Supported: ATX and Setext headings, thematic breaks, fenced and indented code blocks (info strings become
language-*classes), block quotes, tight and loose lists (ordered/unordered, nesting), code spans, emphasis and strong emphasis, inline links and autolinks (URI and emailmailto:), images, raw HTML (block and inline, passed through verbatim), entity and numeric character references, reference-style links, and GFM pipe tables (header row with required delimiter row, alignment colons:---:---:---:, escaped\|pipes, and inline-parsed cells producing<table><thead>โฆ<tbody>โฆ). - Not supported (not part of CommonMark): task lists and footnotes render as literal text. Content that needs them should stay CommonMark-safe; raw HTML is passed through verbatim as an escape hatch. The test harness asserts this boundary stays literal in
contract-table.html. - Fidelity verification: the hermetic golden (
smoke-fixture-expected.html) is produced by the fixture (fake) binary and verifies the adapter pipeline โ frontmatter stripping, link rewriting, escaping โ not CommonMark fidelity. Renderer fidelity is asserted by the real-Oliver contract-corpus pass in the test harness (bones/scripts/tests/fixtures/oliver-contract/), which runs whenever anoliverbinary is present.
Rendered Cooklang surface
Oliver implements Cooklang per the official spec and canonical corpus (60/60 on its conformance wall). That is the contract for what render produces for .cook sources and input_format: cooklang:
- Supported:
@ingredient(with{braced multiword names}, quantities/units preserved as source text),#cookware,~timers(single-word and braced; named timers render the name, unnamed the quantity/units),(preparations)shorthand,--and[- -]comments (removed from the tree),> notes,=sections, and@./pathrecipe references (parsed, never resolved). Output follows Oliver's own deterministic HTML policy:<article class="recipe">, sections with<h2>,<ol class="steps">with<li>and<br>breaks,<aside class="note">,<span class="ingredient" data-quantity data-units>,<span class="cookware">,<span class="timer">,<span class="preparation">,<span class="recipe-ref" data-ref="...">. Frontmatter is data, not content: it is never rendered. - Not supported (degrades to literal text, per the corpus): invalid tokens; unclosed
{,(,[-, or fenced blocks additionally emit a structured warning diagnostic. Recipe-reference resolution, metadata authority, and scaling are consumer territory (the.menuview andscaleRecipelive in the Oliver library, not Rotkeeper). - Recipe metadata (title, author, servings, etc.) belongs in the leading YAML frontmatter, which Rotkeeper's adapter strips before Oliver sees it โ exactly as for Markdown and Textile sources.
XHTML output profile (opt-in)
Oliver ships an explicit, deterministic, XML-compatible XHTML serialization of the same rendered document โ same normalized IR, same semantics, different bytes (oliver PR #54, docs/XHTML.md upstream). Rotkeeper exposes it as an opt-in page render mode; the default (html) path is byte-identical to the pre-XHTML contract.
- Selection:
render_profileinbones/config/rotkeeper.yaml(htmldefault,xhtmlopt-in; validated at environment load, anything else warns and falls back tohtml) arrives at the adapter asRENDER_PROFILE. A per-pagerender_profilekey in the source frontmatter overrides the config value for that file โ the per-page knob exists because raw-HTML content makes XHTML a per-page decision, not a global one. - Flag: the adapter appends
--to xhtmlonly when the effective profile isxhtml; anhtmlprofile appends nothing, keeping the default invocation byte-identical.preflight's smoke render passes--to xhtmltoo when the config selects it, so the compatibility gate covers the configured profile. - Fragments only: the XHTML profile serializes a body fragment โ no DOCTYPE, no
<html>/<head>/<body>wrappers. Rotkeeper's themes own the document wrapper, so an XHTML document is a theme variant (XML declaration +<html xmlns="http://www.w3.org/1999/xhtml">), not an adapter concern.bones/templates/theme-spooky-dark-xhtml.htmlis the reference variant; a page opts in withtemplate: theme-spooky-dark-xhtml.htmlplusrender_profile: xhtml(see XHTML Output Profile, itself an XHTML page). Void elements always serialize XML-form under--to xhtml(<hr />,<br />,<img ... />); attributes stay double-quoted in the existing fixed order; escaping is the existing policy (XML predefined escapes, NUL โ U+FFFD, raw Unicode preserved). - Fail-closed on raw HTML: Markdown raw HTML (
.raw_htmland.html_blockleaves, and Textilepre.) passes through verbatim underhtmlbut fails underxhtmlwith Oliver's typederror.RawHtmlNotXmlWellFormedand an actionable hint on stderr. Oliver never repairs, rewrites, or escapes raw HTML into fake XHTML, and the adapter surfaces the failure as ERROR + exit 1 for that page. A site flipping pages to XHTML must sweep its raw HTML first (the site's own docs historically contain raw HTML). The harness asserts both the fail-closed error path and XHTML well-formedness through an independent XML parser (xmllint) when a real Oliver binary is present. - Cooklang: the forced line break is the one byte delta (
<br>โ<br />); recipes render through the same profile mechanism.
Frontmatter โ Phase 6 S1 (Oliver-owned)
Frontmatter is parsed by Oliver via oliver meta --from <markdown|textile|cooklang> --format json < file.md (stdin โ stdout JSON). Example:
oliver meta --from markdown --format json < file.md > meta.json # {"title":"โฆ","description":"โฆ",โฆ}
oliver meta --from textile --format json < file.textile > meta.json
oliver meta --from cooklang --format json < file.cook > meta.json
# Render auto-strips the same block:
oliver render --from markdown < file.md > body.html
The fields Rotkeeper reads are:
titledescriptionauthordatetemplatepaletterender_profile(per-page XHTML opt-in;html/xhtml, overridesrender_profileinrotkeeper.yamlfor that page only)
All other frontmatter keys pass through untouched. Values are HTML-escaped on template insertion. Rules retained: block must start on line 1 --- (no BOM, no leading blank line), close at next ---, ... not honored, scalar strings only (lists/maps ignored), null/empty treated as "". For the v2 template tokens (subtitle/tags/asset_meta) the adapter reads the raw source frontmatter directly with yq --front-matter extract โ see Extended metadata provenance.
Template and input contract (stable)
This section is the stable contract that Phase 6 ("Rationalize the Oliver boundary") uses as its definition of truth before any renderer-adjacent responsibility moves out of Bash. It is derived from rc-oliver-adapter.sh; if the two ever disagree, the script is authoritative and this document must be updated.
Input side
- Sources are UTF-8 files with an optional leading YAML frontmatter block. The body format is Markdown by default (
input_format: markdown), Textile wheninput_format: textileis set, Cooklang wheninput_format: cooklangis set, and any source whose extension is.textileor.cookrenders in that format regardless of the config value. Source-file discovery covers*.md,*.textile, and*.cook; soul sidecar naming and output naming are extension-agnostic (a.textileor.cooksource gets the samefoo.htmloutput andfoo.soul.mdsidecar asfoo.md). Afoo.md/foo.textile/foo.cookpair in the same directory is a source basename collision and aborts the render โ only one source file may exist per page basename. - The frontmatter block must start on the very first line (
---on line 1 โ no BOM, no leading blank line) and close at the next---line. A YAML...document-end marker is not honored; anything after the closing---is body content in the configured format. - Only the seven fields above are consumed by
oliver meta, as scalar strings. Lists, maps, and other keys are ignored bymetaโ but the v2 template tokenssubtitle/tags/asset_metaare read from the raw source by the adapter's yq extraction (provenance below), andversionis injected frombones/config/version. - A
.soul.mdsidecar underbones/metamay override any of the six metadata fields per field (render_profileis frontmatter-only); the sidecar value wins only when it is non-empty and notnull. Without a sidecar, source frontmatter applies. templateresolution:$template$selects${TEMPLATE_DIR}/${template}. If the named template does not exist or escapesTEMPLATE_DIR, the batch manifest's default template (fromdefault_templateinbones/config/rotkeeper.yaml) is used. The page fails if no valid template resolves.
Template dialect
Templates are HTML files containing the following tokens:
| Token | Source | Insertion |
|---|---|---|
$title$ |
frontmatter title (sidecar wins) |
HTML-escaped |
$description$ |
frontmatter description (sidecar wins) |
HTML-escaped |
$author$ |
frontmatter author (sidecar wins) |
HTML-escaped |
$date$ |
frontmatter date (sidecar wins) |
HTML-escaped |
$palette$ |
frontmatter palette (sidecar wins) |
HTML-escaped |
$version$ |
bones/config/version via the adapter ($VERSION, rk_load_version) |
HTML-escaped |
$subtitle$ |
frontmatter subtitle (sidecar wins) |
HTML-escaped |
$tags$ |
frontmatter tags list, adapter-joined with , |
HTML-escaped |
$asset_meta$ |
frontmatter asset_meta map, adapter-serialized (below) |
HTML-escaped |
$navigation$ |
site nav from bones/config/rotkeeper.yaml |
raw HTML via <site-nav> placeholder (post-wrap) |
$warnings$ |
per-page renderer warnings | HTML-escaped |
$assets_root$ |
render batch (path prefix to the layout's assets dir) | literal, not escaped |
$body$ |
Oliver-rendered body fragment | literal, not escaped (it is trusted rendered HTML) |
Escaping is & < > " ' โ & < > " '. $assets_root$ and $body$ are the only tokens exempt from escaping โ templates must never place untrusted values there, and $body$ must never be escaped.
Conditionals: $if(name)$ โฆ $endif$ for every metadata token (title, description, author, date, palette, version, subtitle, tags, asset_meta, navigation, warnings). When the value is empty (or null, or the key is absent) the whole block โ including its interior newlines โ is removed; otherwise the interior text is kept.
- Evaluation is one variable pass at a time in the order listed above, and the first
$endif$in the document closes the opener. Do not nest the same variable twice; keep conditionals single-level in practice. - Order of operations: all
$if$blocks are resolved first, then the literal token substitution runs. - A known token whose value is empty/absent substitutes an empty string (it is not an error); any unrecognized
$word$token passes through to the output verbatim. - v3 generic hook (#269): a token is known if it is one of the typed metadata tokens or any key present in the
--meta-jsonobject. Present keys interpolate (strings html-escaped, null โ empty, other scalars stringified, objects/arrays as compact JSON) and$if$-gate on non-empty; keys absent from meta-json stay verbatim. Injecting a new frontmatter field therefore needs no upstream change โ the adapter merges it intowrap_metaand the template just references$field$.
This dialect โ the typed metadata tokens (v1 five + v2 six) plus $assets_root$/$body$, escape/raw split, and the v3 generic hook for any other meta-json key โ is Phase 6 S2: Oliver wrap --template <file> --meta-json <json> --assets-root <prefix> --body <file> is authoritative (direct on pin 3f05bac, wrap dialect v3, oliver #127). Example:
oliver wrap --template bones/templates/theme-spooky-dark.html \
--meta-json meta.json --assets-root ./assets/ --body body.html > page.html
# or: oliver render --template <file> --meta-json <json> (alternative, feature-detected)
The extended tokens joined the dialect with the shared template contract v2 (rotkeeper #244, pin move 06dd640 โ 8460f28, oliver #126). $version$ is not frontmatter โ the adapter injects it from the canonical version source, the same single source --version and @HELP {VERSION} use. $subtitle$/$tags$/$asset_meta$ are read from the source frontmatter by the adapter with yq --front-matter extract (see provenance below); $warnings$ is reserved โ no adapter feed exists yet, so it substitutes empty. Site navigation is delivered as raw HTML through a literal <site-nav></site-nav> placeholder (below), not an escaped token, because every non-literal wrap token html-escapes and a real nav is markup. The v3 generic hook (rotkeeper #269, pin move 8460f28 โ 3f05bac, oliver #127): the adapter merges every other scalar frontmatter key into wrap_meta (typed keys win the merge), so templates can reference any frontmatter field as $field$ โ $page_type$ on the necropolis 404 theme is the first consumer. Bash keeps TEMPLATE_DIR boundary checks; Oliver handles interpolation when wrap is present.
Extended metadata provenance (v2 + v3, implemented)
The extended tokens are live since the shared template contract v2 landed (rotkeeper #244): oliver wrap interpolates them on pins 8460f28/3f05bac (oliver #126/#127, the latter adding the v3 generic hook), and the adapter feeds them into wrap_meta:
-
v3 generic merge (#269): after building the typed object, the adapter merges every other scalar frontmatter key (
yq --front-matter extract -o=json '.') intowrap_metawith the typed keys winning (jq -s '.[0] * .[1]'). Any key therefore becomes an interpolatable$field$token; values keep their JSON type andwraprenders scalars/strings/compact-JSON per the dialect.page_typeon the necropolis 404 theme is the first consumer. -
oliver metakeeps emitting exactly the seven S1 fields โ no map/list support was added tometa.$version$is not frontmatter: the adapter injects it from the canonical version source (rk_load_versionโbones/config/version), the same single source--versionand@HELP{VERSION}use. -
The adapter (
rc-oliver-adapter.sh) enricheswrap_metawith the extended scalars before invokingwrap:versionโ$VERSION.subtitleโ frontmattersubtitle, sidecar wins (the sidecar merge is adapter-owned; the soul file is read directly becauseoliver metadrops it).tagsโ frontmattertagslist joined with,(deterministic order as authored).asset_metaโ frontmatter map serialized deterministically:name,version,author,project,licensejoined withโ, empty fields omitted,trackednever rendered (it is retrieval metadata, not display content). Example forhome/content/index.md:index.md โ 0.3.0.4 โ Filed Systems โ Rotkeeper โ All Rights Reserved.warningsis a reserved token with no adapter feed yet; it substitutes empty until an in-page warnings surface exists.
-
Frontmatter authority splits by design: Oliver owns the seven S1 fields; yq (
--front-matter extract, the same technique the harness uses) reads the extended v2 fields from the raw source becauseoliver metadrops lists/maps. A futureoliver metathat emits every scalar key (and serializesasset_meta) would collapse the split back into Oliver.
Site navigation (<site-nav> raw-HTML slot)
$navigation$ is the config-driven site nav (topic #244). Because the recalled dialect html-escapes every non-literal token โ and a nav is markup, not text โ the adapter does not feed it through wrap. Instead a template declares a literal <site-nav></site-nav> placeholder and the adapter replaces it after wrap with nav rendered from bones/config/rotkeeper.yaml:
# bones/config/rotkeeper.yaml
navigation:
label: "Primary Navigation"
items:
- label: "Home"
target: "index.html" # site-root-relative; the adapter prefixes the
class: "txp-nav-link" # current page's depth (./ or ../) + optional class
- label: "Docs"
target: "docs/index.html"
class: "txp-nav-link"
rc-utils.sh's rk_render_navigation "$assets_root" builds <nav aria-label="โฆ"><ul><li><a href="โฆ">โฆ</a></li></ul></nav> with hrefs resolved to the current page's depth (./assets/ โ ./; ../assets/ โ ../), then the adapter swaps <site-nav></site-nav> for it in the wrapped page (multi-line-safe via an ENVIRON AWK variable โ awk -v errors on newlines). Omit the config block and the placeholder is left untouched, so unaffected themes never see it. theme-textpattern is the first consumer, replacing its hardcoded tabs. ($navigation$ is not this slot; because it is a reserved token it currently substitutes empty.)
Documentation navigation and headings
For output pages under docs/ or help/, the adapter adds a breadcrumb, a
collapsible section-page list, and previous/next navigation to the literal
$body$ fragment before wrap. Each navigation has a distinct <nav>
landmark label. This works with every shipped theme, including the XHTML
wrapper, without adding an Oliver token or changing the renderer pin.
- The current render plan is the inventory. Stale output files are never navigation targets. Breadcrumb ancestors link only when their index page exists in the plan; otherwise they appear as text.
- Docs and Help are separate sections. Each section puts its
index.htmlfirst, then sorts pages by output-relative path. Previous/next links wrap around the section, so the first and last pages remain connected. A section with only one page has disabled previous/next controls instead of self-links. This is independent of the task-guide organization of the Help index. - Labels use source titles, with non-empty soul-sidecar titles taking precedence. A page without a title uses its filename stem. Labels are HTML-escaped, path segments are URL-encoded, and targets are relative to the current page. No root-absolute URLs, CDN, or JavaScript are introduced.
- The wrapper owns the single page H1. Body H1s matching the effective title are removed, retaining their attributes on an empty span so existing fragment links survive. Other body H1s become H2s. Lower-level headings and escaped code examples remain untouched. Body-only custom templates receive a page H1 in the body instead.
- Non-documentation pages keep the normal renderer body unchanged. The Textpattern masthead uses a paragraph for site branding, leaving its article title as the only wrapper H1.
The template golden fixtures render under docs/, so they cover the shared
navigation and title normalization through spooky-dark, brutal, and XHTML.
The isolated documentation harness also checks every shipped template and all
three layouts, escaped labels, nested paths, sidecar precedence, untitled
pages, preserved anchors, and singleton navigation.
Sidecar precedence
A .soul.md sidecar next to a source file (under bones/meta) may override frontmatter fields. Sidecar wins over source frontmatter for the fields listed above. Without a sidecar, source frontmatter applies.
The adapter boundary (temporary, recorded)
rc-oliver-adapter.sh is a pure Bash batch adapter (zero Python). It currently owns:
- Batch manifest (
TSV) iteration with boundary assertions (source under content, destination under output). - Frontmatter extraction and sidecar merge โ S1: Oliver
meta --from <fmt> --format jsonextracts 7 scalar fields;oliver renderauto-strips. - Template resolution โ Bash retains
TEMPLATE_DIRboundary; Oliverwrap --template <file> --meta-json <json> --assets-root <prefix> --body <file>interpolates. - Oliver invocation and stderr forwarding (including the
--to xhtmlflag when the effectiverender_profileisxhtml). - Internal
.md/.textile/.cookโ.htmllink rewriting โ S3: OliverrenderAST rewrites*.md|*.textile|*.cookโ*.html. - Template interpolation โ S2: Oliver
wraphandles the v2 dialect (11 metadata tokens +html_escape+$if$/$endif$+$assets_root$/$body$literals).
rc-render.sh owns output planning. S4: Oliver plan --content-dir <dir> --output-dir <dir> --template-dir <dir> --meta-dir <dir> --default-template <file> --oliver-bin <bin> --root-dir <dir> --dry-run <bool> --verbose <bool> maps content โ output.
rc-render.sh log_manifest owns manifest. S5: Oliver manifest --manifest <file> --add <rel> (dedup) + --verify for bones/manifest.txt.
Per the stabilization roadmap, these responsibilities are candidates for incremental movement into Oliver only after the contract above is stable. Bash keeps dispatch, environment setup, filesystem boundaries (rk_canonical_path, is_within_boundary, validate_layout_alignment, output_is_generated), orchestration, and packaging. S1+S2+S3+S4+S5 complete Phase 6; Bash retains dispatch/env/packaging.
Install paths
Oliver has no stable release yet, but upstream publishes a rolling builds release with prebuilt binaries (oliver-<os>-<arch> for linux/macos ร x86_64/aarch64) plus a published sha256sums.txt. scripts/setup.sh is download-first: it fetches the platform binary, verifies it against the published checksum, and asserts oliver --version reports exactly commit <OLIVER_PIN> before installing to /usr/local/bin/oliver. Any failure falls back to building the pinned commit from source with Zig 0.16.0:
git clone https://github.com/drawmeanelephant/oliver.git /tmp/oliver
cd /tmp/oliver
git checkout --quiet <OLIVER_PIN> # exact commit, never unpinned main
zig build # builds the library and CLI into zig-out/
install -m 0755 zig-out/bin/oliver /usr/local/bin/oliver
Then either put oliver on PATH or set RK_OLIVER_BIN=/path/to/oliver. CI environments (see .github/workflows/ci.yml) run scripts/setup.sh, which prefers the builds release (checksum + commit-version verified) and falls back to the Zig 0.16.0 source build when the download path is unavailable.
Smoke paths
- Availability check:
bash rotkeeper.sh preflightreports whether Oliver is found, executable, and actually runnable (a live smoke render through the real CLI); it fails with one setup message otherwise.renderruns the same check before rendering. - Hermetic (always): the test harness (
bash rotkeeper.sh test) builds fixture binaries and exercises frontmatter, sidecars, escaping, links, stderr separation, and manifest consistency without any Oliver dependency. The checked-in fixture atbones/scripts/tests/fixtures/oliver-smoke/renders through a fixture binary and its body is compared againstsmoke-fixture-expected.htmlon every layout pass. - Real binary (when present): the same harness renders
real-oliver-fixture.mdthrough the discovered executable and asserts exit 0, a well-formed HTML page, and the fixture title in the output. It runs on every layout pass. With a real binary present, the harness also renders an XHTML-profile page through--to xhtmland asserts the wrapped document is well-formed viaxmllint, and asserts that a raw-HTML page selected for XHTML fails witherror.RawHtmlNotXmlWellFormed. SettingRK_STRICT=1turns the real-binary skip paths (missingoliver, missing contract corpus) into hard failures so a green run always proves the real binary was exercised โ CI runs the harness withRK_STRICT=1. - Quick local check:
bash rotkeeper.sh renderon a checkout with content renders everything through Oliver; failures list the page and Oliver's stderr.