Documentation site: Starlight at /docs, themed to the project site - #4
Open
eins78 wants to merge 22 commits into
Open
Documentation site: Starlight at /docs, themed to the project site#4eins78 wants to merge 22 commits into
eins78 wants to merge 22 commits into
Conversation
packages/dexd was a gitlink to github.com/eins78/dexd, a repository that no longer exists. The player crate is placed directly in the monorepo instead of as a submodule (story decision 2026-08-17): submodule friction — the crate's text, packaging, CI and the OS image change in lockstep, and a submodule boundary got in the way of that.
Source: home-workspace experiments/2026-08-12-4k-hevc-perfect-loop/dex-loop at commit 71711ec1 (dex-loop's own history, log -1 on that path). Copied with deploy/, renamed dex-loop -> dexd throughout: crate name, binary, Debian package, systemd unit, man pages, lintian overrides, maintainer scripts, changelog head, test binary paths, and every in-source reference (usage text, log/error prefixes, doc comments). The ingest sidecar checker sidecar-check.rs -> dex-sidecar.rs, per the story's naming decision (helpers keep the dex- prefix; the main binary/package/unit becomes dexd). build.rs's git-HEAD paths adjusted for the crate now sitting two levels below the repo root (packages/dexd) instead of one. Not copied: IMPLEMENTATION-PLAN.md, PLAN.md, soak-24h/ -- private experiment records that stay in home-workspace. README.md travels for now (it still says dex-loop throughout) and is replaced with rewritten, outsider-readable docs in Phase 2 of the extraction plan; same for the historically-accurate old-name mentions in deploy/changelog's older entry text and a doc comment in dex-sidecar.rs describing the tool's pre-cargo-workspace history. No prose was rewritten and no behaviour changed: cargo check/clippy/ test --lib, cargo deny check, reuse lint and shellcheck all pass unmodified from source, same as before the move.
Copied from home-workspace's ci/dex-loop-deb.yml (same source commit as the crate, 71711ec1). Renamed dex-loop -> dexd throughout: workflow name, job/artifact names, concurrency group, cache key, systemd unit, apt package name, binary paths, tag pattern (dex-loop-v* -> dexd-v*). CRATE_DIR points at packages/dexd instead of the experiment's nested path; the changes-detection path list matches packages/dexd/** and this workflow file. Added "feature/**" to the branch push triggers so this branch's own pushes run it. Dropped the "Shellcheck the bench harness" step: it shellchecked scripts/*.sh under the experiment directory, which is harness code that intentionally stays in home-workspace and has no equivalent path in this repo. No prose was rewritten beyond substituting the renamed identifiers and paths; the design-rationale comments are unchanged.
…aid dex-loop The 0.1.0-2 entry body named dex-loop.service; the changelog ships publicly as /usr/share/doc/dexd/changelog.gz and the dexd package never had a unit by that name. Name substitution only, no wording change.
Anyone with a raw HEVC stream needs a sidecar before dexd will play it, and
until now the only thing that could write one lived in a private repository
(scripts/make-sidecar.sh). The crate shipped a checker but no writer, so the
README's answer was a hand-rolled printf with shasum in it. The writer now
lives next to the parser it has to satisfy.
The CLI has two subcommands:
dex-sidecar check <sidecar.json> <stream.265>
dex-sidecar write <stream.265> [--fps F] [--out FILE] [--force]
check is the old behaviour and the old exit codes, unchanged. The bare
two-argument form is gone; it prints usage instead. Nothing had released it,
and nothing in the tree called it.
Ported from the shell script, rule for rule:
- sha256 over the exact bytes on disk, the same read the player does.
- The frame rate comes from ffprobe's r_frame_rate, and is believed only
between 1 and 1000 fps. Outside that, ffprobe is reporting its internal
timebase (1200000/1) rather than a rate, which is what a raw stream with
no timing in its headers produces. Then, and when ffprobe is absent,
--fps is required rather than guessed.
- An explicit --fps that sits more than 0.02 fps from a detected rate is
refused unless --force is given. A typo there binds a wrong rate into a
sidecar that verifies perfectly forever.
- An existing sidecar is not replaced without --force.
- Width and height come from the same ffprobe call, and are left out when
it cannot report them.
- Before anything is written, the sidecar is parsed back with the crate's
own Sidecar::from_json and verified against the stream bytes. --fps is
validated the same way, by building a sidecar around it, so there is no
second copy of the rate format anywhere in this file.
Tests: tests/sidecar_write.rs ports the shell suite's cases against real
streams made with ffmpeg — write then check, a flipped byte failing check,
overwrite refused then forced, a typo'd --fps refused then forced, --fps
required on a stream whose encoder wrote no timing, and the malformed
command lines. They skip loudly when ffmpeg is missing; CI now installs it
and fails the step if ffprobe is absent, so they cannot skip their way to
green. Rate parsing, the tolerance, the plausible-range bound, and argument
parsing have unit tests in the bin.
… the tests Follow-ups from the port's verification: - write_atomically removes its temp file when the write itself fails, not only when the rename fails; the shell script's exit trap covered both. - The test suite no longer skips silently without ffmpeg: the test runner hides stderr from passing tests, so the old "loud skip" was invisible under a plain `cargo test`. Missing tools now fail with the install command; DEXD_ALLOW_MEDIA_SKIP=1 opts a machine into skipping. - New test for the ffprobe-absent path: --fps is required, and the written sidecar carries no width/height it could not learn. Needs no ffmpeg. - rustfmt on both files. Also recorded here, since the port's report implied stdout parity with make-sidecar.sh: `write` prints only the sidecar JSON on stdout (the "OK …" line of the round-trip check goes to stderr), so stdout is parseable.
…t owner docs/glossary.md holds the 147 terms the documentation may use without explanation (user tier and developer tier), each reviewed and approved one by one on 2026-08-18. AGENTS.md carries the writing rules for everything an outsider can read, the names that were decided, the table of retired words with what to write instead, and the glossary governance: agents propose, the owner approves; CODEOWNERS routes every change to the glossary, the rules and the lint tables to him. CLAUDE.md includes it.
…S.md scripts/docs-lint.mjs (Node 20, no dependencies) checks the rules a machine can check: plan codes, references to private documents, dates as structure, caps emphasis against the glossary's acronyms, intensifiers, retired coinages (docs/lint-coinages.tsv), codenames, first person, process talk, over-long comment blocks, headings. 82 tests. It reads docs/lint-allow.txt, where every exception must carry a reason. CI: the changes job now also reports a `docs` flag; a docs-lint job runs when docs/, AGENTS.md or the tool changed, and the required gate treats its skip as legitimate only when nothing docs-related changed. Scope is docs/ and AGENTS.md for now; the crate's comments and README join when their rewrite lands.
…upervisor thread, plain messages
The vocabulary review settled the project's names; the table in AGENTS.md
("Names that were decided") is the specification. This applies the four that
reach into code, with the tests that assert them. Renames only: no behaviour
change, no dependency change, no prose rewrite.
- The heartbeat field is `loops=`, not `wraps=`. The identifiers built on the
old coinage follow: WRAP_COUNT -> LOOP_COUNT, the Heartbeat field
`wraps` -> `loops`. Byte-position arithmetic in chunk.rs still wraps back to
byte 0 — that is the mechanism, not the coinage, and is untouched.
- The test-rig flags say what they are and are grouped under one prefix:
--bench-no-sidecar -> --test-rig-no-sidecar,
--bench-wedge-after-secs -> --test-rig-hang-after-secs,
--force-recovery-after-secs -> --test-rig-force-recovery-after-secs.
Their identifiers follow, the marker "BENCH ONLY" becomes "(test rig only)",
and "wedge/wedged" in identifiers and shipped lines becomes hangs/unresponsive
— never "hung".
- The thread that supervises playback is the supervisor thread everywhere,
which is what health.rs already called it.
- The message that told an operator to "re-ingest the asset" now names the
command that does it: "prepare the video again with dex-sidecar write". The
three nal.rs messages whose remedy is a re-encode say "prepare the video
again with a closed-GOP encode" instead, since dex-sidecar write does not
encode. Exit codes and the rest of every message are unchanged.
deploy/dexd.service passes none of these flags and needs no edit. README.md and
deploy/man/dexd.1 got the mechanical substitution only — both are rewritten
later.
Written fresh from the content inventory and the approved glossary under the rules in AGENTS.md; each went through two review rounds (outsider readability, vocabulary and lint, faithfulness to inventory rows), a copy-edit pass and a final gate. Both lint clean. Calibration pair for the register: one user guide, one design document.
…ea per paragraph, no history, short From the calibration review of the first two documents: content right, tone still the model's own. Reference points are the owner's manual and a component reference he rates well; the section states the rules and shows five draft sentences rewritten.
dexd reads /opt/dex/exhibit.yaml, then /opt/dex/exhibit.json. Nothing reads /etc/dex any more, and the .deb ships no exhibit config: no conffile, no default written by postinst, no /etc/dex directory. A missing config makes the player refuse to start and name the file to create, with the two lines that file needs. /opt/dex is the mount point of the dex card's data partition, so the card in a computer shows the video, its sidecar and exhibit.yaml together and a technician edits the config there. The unit waits for that mount. A relative `asset` resolves against the directory the config file is in, so `asset: artwork.265` beside /opt/dex/exhibit.yaml names /opt/dex/artwork.265. Absolute paths are unchanged, and the cross-check against a command-line asset compares the resolved path.
Follow-ups from the verification of the config move: the postinst notice now says what a player needs — the video, its sidecar and exhibit.yaml, all in /opt/dex — instead of nagging about one file name; a config left under /etc/dex, which dexd no longer reads, is called out once at install time with the command to move it; and the no-asset refusal shows the same bare-filename example the no-config refusal shows.
…rocess references
The owner's read of the branch: the CI workflow and Cargo.toml comments
narrate history and pass verdicts on their own design ("the cost is real
and deliberate", "what it buys", "DECLARED, not avoided"). A new subsection
under Tone says what a comment is for and shows five of his rewrites. The
lint allowlist and coinage table carried review-process references in
their reasons; those are plain now.
packages/docs renders the markdown in docs/ as a Starlight site under dex.ars.is/docs. Starlight is an Astro integration, so it shares a toolchain and an origin with packages/website. The markdown stays at the repository root. scripts/sync-docs.mjs copies it into the content collection and lifts each page's level-one heading into the title frontmatter Starlight requires; the copy is generated and git-ignored. Starlight validates that title when the collection loads, before remark plugins run, so a remark plugin cannot supply it. The site's own pages, currently the landing page, live in src/site/. src/styles/dex.css carries the brand. A palette sets seven custom properties and one mapping block translates those into the --sl-* properties Starlight reads, written once for both colour schemes. The recorded pair, the monospace body and the grotesque h1 and h2 follow packages/website; the flat chrome, the hairline separation and the absent drop shadows are what a sidebar and a header need that a one-page site does not. Five palettes preview with ?palette=, four of them the ones the project site already offers. The parameter is palette because Starlight uses data-theme for its light and dark toggle. Contrast is measured and recorded per palette: the dimmed colour in the default has to sit between 4.5:1 and the body text's 5.71:1, and #8bc0d1 measures 4.89:1.
CSS, DNS, WCAG, AAA, CODEOWNERS and KTE, each with its reason. The contrast figures recorded per palette need WCAG and AAA; the rest appear in packages/docs/README.md.
The header button replaces Starlight's light and dark select and steps through bold, quiet, quiet-flipped. Bold is the recorded pair on its full-saturation ground and is always dark; both quiet steps are brand-deep, the first in the colour scheme the reader's system asks for and the second in the other one. The palettes differ in more than lightness, so a light/dark pair does not describe them. The order puts the project site's colours first and an AAA-contrast alternative one click away, with a second click for a reader whose system setting does not suit the room. ThemeProvider.astro holds the state and applies it, inlined in the head so no step flashes before the stored one; ThemeSelect.astro is the button, wired in the header and the mobile menu alike. The state lives in localStorage under dex-view. The button's swatch is drawn from the live custom properties, so it follows a palette change without being told. ?palette= still previews any of the five, colours only; the step decides the colour scheme.
The control shows one character and nothing else: cloud for Blue, moon for Dark, sun for Light. The names moved to the button's accessible name and its tooltip, so a screen reader and a pointer both get "Colour scheme: Blue. Change it." while the header stays quiet. The glyphs are text presentations, so each takes the button's colour instead of drawing itself in an emoji font's own palette. U+263E has no emoji presentation to suppress; U+2601 and U+2600 carry U+FE0E, the text variation selector. U+2601 draws in the lower half of its em box while the other two fill theirs, so on a shared centre it reads as sitting low. The script writes the step onto data-scheme and the stylesheet lifts that one glyph.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Renders the markdown in
docs/as a Starlight site underdex.ars.is/docs, themed topackages/website. Starlight is an Astro integration, so the two sites share a toolchain and anorigin: one deploy, one DNS record, and
/aptleft free for a future package repository.The site had not been scaffolded, so this branch adds
packages/docsand the theme together.Where the text lives
The markdown stays at the repository root.
scripts/sync-docs.mjscopies it into the contentcollection and lifts each page's level-one heading into the title frontmatter Starlight requires;
the copy is generated and git-ignored. Starlight validates that title when the collection loads,
before remark plugins run, so a remark plugin cannot supply one.
Keeping
docs/as the source means CODEOWNERS still routes it,scripts/docs-lint.mjsstillchecks it, and every page reads correctly on GitHub. The site's own pages, currently the landing
page, live in
packages/docs/src/site/.Theme
packages/docs/src/styles/dex.css. A palette sets seven custom properties, and one mapping blocktranslates those into the
--sl-*properties Starlight reads, written once for both colourschemes. Adding a palette is copying a block.
Carried over from the project site: the pair recorded in
packages/branding,#8ed1deand#2922c8, monospace body copy, andsystem-uiat weight 800 onh1andh2only. Because thetreatment stops at
h2, the glossary's term headings stay monospace, which suits terms a readermeets in a file or a command.
Added for a documentation site, which has more surface than a one-page site: flat sidebar and
header on the same ground as the text column, separation by hairline, no drop shadows, the current
sidebar item marked by a rule, and rules on both axes in tables.
The colour-scheme control
One button replaces Starlight's light and dark select and steps through three schemes, because the
palettes differ in more than lightness.
brandbrand-deepbrand-deepBlue is the project site's colours and is what a reader sees first. From there the cycle goes to
the scheme the reader's system asks for, then to the other one, then back to Blue. An
AAA-contrast alternative is one click away, and the second click covers a reader whose system
setting does not suit the room.
The button shows the glyph alone; each step's name reaches a screen reader through the accessible
name and a pointer through the tooltip. State is in
localStorageunderdex-viewand survives areload.
ThemeProvider.astroholds it and applies it, inlined in the head so no step flashesbefore the stored one.
Verified in a browser under both system settings:
Contrast
Five palettes are defined; the cycle uses two and
?palette=previews any of them, colours only.Body text against background, WCAG 2.1:
?palette=brand(default)brand-deepinkamberbrand-dayThe dimmed colour in
brandfirst measured 4.24:1, under AA for normal text, and it carries thetable of contents. It has to stay under the body text's 5.71:1 to read as secondary and over
4.5:1 to pass;
#8bc0d1measures 4.89:1.Community themes
Of the published Starlight themes, most are colour schemes — Catppuccin, Nord, Gruvbox, Rosé Pine,
Flexoki, Rapide — and this project supplies its own. The layout themes bring colour schemes and a
scheme picker to remove again.
@shuttering/starlightmaps a token contract onto the same--sl-*properties
dex.cssdoes, and carries an organisation's design system this project is not part of.The arrangement here is one stylesheet with no dependency, matching how the project site overrides
its own base stylesheet.
Checks
pnpm buildclean,pnpm check0 errors, 0 warnings, 0 hintsnode scripts/docs-lint.mjs packages/docs/0 errors, 0 warningsdocs/lint-allow.txtgainsCSS,DNS,WCAG,AAA,CODEOWNERSandKTE, each with areason.
node scripts/docs-lint.mjs docs/ AGENTS.mdis unchanged by that additionseamin the landing-page copy, a coinage the glossary retires in favour ofgapless
Not in this branch
No deployment workflow.
.github/workflows/pages.ymlpublishespackages/websitealone, andserving both at one origin is a separate change.