cc113c732f
- repo-stats reads the gotcha list from context/gotchas.md (the digest in repo-map.md no longer measures the doc-growth signal) and reports it as `docs.gotchas`; the test fixture and its derived expectations follow. - AGENTS.md and the wave agent-rules protect gotchas.md and file-inventory.md like the repo map they were split from. - doc-site.md's publication table lists the two new pages. - file-inventory.md's Context docs rows describe the three files correctly. - the code-review skill points at gotchas.md for the long form. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
343 lines
21 KiB
Markdown
343 lines
21 KiB
Markdown
# The documentation site — the arguments
|
|
|
|
`scripts/build-doc-site.mjs` turns the prose in `context/` into the static site
|
|
served at `/docs/` on GitHub Pages. This is the reasoning behind what it
|
|
publishes, what it deliberately does not, and why it is a hundred-odd lines of
|
|
node rather than a static site generator (improvise-djoo).
|
|
|
|
---
|
|
|
|
## 1. What is published, and what is not
|
|
|
|
The manifest is the `SECTIONS` array at the top of the script, and **a document
|
|
is published only by being named there**. That is the point: a glob over
|
|
`context/**` would publish the next working file somebody drops in, and the
|
|
decision about what a stranger should read is not one to make by accident.
|
|
|
|
Published, in four sections:
|
|
|
|
| Section | Contents | Why |
|
|
|---|---|---|
|
|
| Start here | `docs/design-notes.md`, `context/design-principles.md`, `context/repo-map.md`, `context/gotchas.md`, `context/file-inventory.md` | What the project is for, the doctrine, and the single best explanation of how it is built. The map is a digest; the last two are its long forms, published so a `gotcha N` citation and the per-file listing have somewhere to land. |
|
|
| Decision records | `context/adr/*.md` | An ADR is written for an outside reader by construction — it states the options that were rejected, which is the part that is worth reading and the part that is nowhere else. |
|
|
| The arguments, by area | `context/code/*.md` | This is the reasoning that used to live in code comments and was moved out under the two-line cap (design-principles §6). It is the most valuable prose in the repository and it is addressed to exactly the reader a documentation site has. |
|
|
| Test evidence | `context/test-evidence/*.md` | One entry per test whose narrative is longer than its code. |
|
|
|
|
Not published:
|
|
|
|
- **`context/plan.md`** is a *prompt*: it opens "You are working in the
|
|
`improvise` repository… Your job is to prepare this repository for a public
|
|
Show HN launch", and it instructs a reader to audit a `context/SPEC.md` that
|
|
no longer exists. It is an instruction to an agent about a launch, not a
|
|
description of the program, and publishing a launch plan on the site the
|
|
launch points at inverts the two.
|
|
- **`context/cloud-agents.md`** is operational notes for agents running in a
|
|
cloud session — tracker hydration, `bd` pitfalls, what the sandbox blocks.
|
|
It is true and it is useful and it is addressed to nobody who visits a
|
|
website.
|
|
- **`context/code/pr-review.md`** is how an agent reads a PR's reviews — where
|
|
a ChatGPT Codex review appears, how to tell it from this repo's own agents'
|
|
comments, and why absence is not approval. It is about *this repository's
|
|
process*, not about the program, which is the `cloud-agents.md` line and not
|
|
the `tooling.md` one: `tooling.md` explains checkers a reader of the code
|
|
will meet, and nobody visiting a website is about to read a PR thread.
|
|
|
|
None of the three is referenced by any published document except as a row in
|
|
the file inventory (`file-inventory.md`), which is inside a code fence, so leaving them
|
|
out creates no dangling link. That was checked rather than assumed.
|
|
|
|
**And "not published" is now derived rather than assumed** (improvise-res9).
|
|
The generator walks `context/**/*.md` plus `docs/design-notes.md`, subtracts
|
|
the manifest, and fails unless the remainder is exactly the `OMITTED` map in
|
|
the script — which carries the reason beside the name, so this section and the
|
|
code cannot drift into disagreeing. An allowlist and not a count, because an
|
|
omission is a decision that should have to be argued in code; and the map is
|
|
checked in *both* directions, because an entry naming a file nobody can find
|
|
asserts a reason for nothing, which is the failure a dead ticket id is.
|
|
|
|
The check was latent when it was filed and **live by the time it was written**:
|
|
`context/code/kb-tool.md` had been added after this table was drafted and was
|
|
publishing nothing, with no argument for its absence anywhere. The table above
|
|
says `context/code/*.md` is published; the manifest disagreed and nothing could
|
|
see it. That is the whole failure mode in one file.
|
|
|
|
**Test evidence is the one judgement call worth stating.** The argument for
|
|
leaving it out is that hundreds of entries about individual tests are noise
|
|
to a reader who wanted to know what a pivot table is. The argument for keeping it is
|
|
stronger: every one of the `context/code/*.md` documents points at its
|
|
evidence file by name, so omitting it would break the published prose in
|
|
exactly the way this site is otherwise careful not to; and the evidence file is
|
|
the most distinctive artefact the repository has — a written record of the
|
|
injected fault behind each load-bearing test is not a thing most projects can
|
|
show. It is therefore published as its **own section**, last, described as a
|
|
contributor reference rather than a tour, so nobody wanders into it expecting
|
|
a narrative.
|
|
|
|
## 2. Ticket ids resolve to a page, never to a URL that does not exist
|
|
|
|
The prose cites bead ids roughly 190 times — this paragraph deliberately spells
|
|
none, because a document that gives one as an example makes itself a source of
|
|
audit noise, and a placeholder is worse than a real id: it is ID-shaped and
|
|
resolves to nothing. On a public site those citations are
|
|
meaningless strings, and the obvious repair — link them to an issue tracker —
|
|
is unavailable: beads keeps its issues in a Dolt database on a git ref, and
|
|
there is no public issue URL to point at. Inventing one would produce 190
|
|
links that 404, which is worse than plain text because a link is a promise.
|
|
|
|
So the ids resolve **inward**. `tickets.html` is generated from
|
|
`.beads/issues.jsonl` — already committed, already public — and carries id,
|
|
status and title, nothing else; descriptions are internal working notes and are
|
|
not the site's business. Each citation in the prose becomes a link to that
|
|
page's anchor, with the title in a `title=` attribute so a hover answers the
|
|
question without a navigation.
|
|
|
|
**`issues.jsonl` is a passive export and lags the database**, which is not a
|
|
defect to work around but a fact to render honestly: an id the export does not
|
|
carry is emitted as plain text with a dotted underline and no link, and the
|
|
ticket page lists it as "not exported". The site therefore never links to an
|
|
anchor it did not generate, whatever state the export is in. A generator that
|
|
instead linked every id it saw would produce a page that is right today and
|
|
broken the first time somebody cites a fresh ticket.
|
|
|
|
**The page publishes every cited id, open bugs and their titles included, and
|
|
that is a decision rather than a default** (user ruling). The data is already
|
|
public in `.beads/issues.jsonl` in a repository anyone can clone; the page only
|
|
makes it legible. An honest open-bug list is a normal thing for an open-source
|
|
project to have, and filtering one while the page's own lede links the raw
|
|
JSONL would be theatre. Note the second-order reason to keep it unfiltered:
|
|
a citation whose row was suppressed would be a link into a page that does not
|
|
answer it, which is the failure this whole section exists to avoid.
|
|
|
|
**Titles are tracker data and are escaped as attribute values.** Five shipped
|
|
bead titles contain a double quote; interpolated into `title="…"` by an `esc`
|
|
that handled only `&`, `<` and `>`, they broke out of the attribute and the
|
|
browser parsed the remains as three junk attributes. `esc` now escapes `"` and
|
|
`'` as well. Today's corpus being friendly is not a reason to interpolate
|
|
untrusted-shaped text unescaped — the tracker is written by whoever files a
|
|
bug.
|
|
|
|
## 3. Source paths link only where the file exists
|
|
|
|
A code span whose entire content is a repo path becomes a link — to the
|
|
generated page when the path is one of the published documents, to GitHub
|
|
otherwise. Two constraints make that safe. The whole span must be the path, so
|
|
a sentence that merely mentions a directory is untouched; and **the path must
|
|
exist on disk at generation time**, so a citation that has gone stale degrades
|
|
to plain text rather than to a 404. The GitHub ref is the cache-bust token
|
|
`web/assemble.sh` already receives, used only when it is a 40-hex sha and
|
|
falling back to `main` otherwise — a local `dev` build must not emit
|
|
`blob/dev/…`.
|
|
|
|
## 4. The link check is the test, and it is part of the build
|
|
|
|
The generator collects every anchor it emits and every internal href it writes,
|
|
resolves them against each other, and **exits non-zero** if any href names a
|
|
page it did not generate or a fragment that page does not carry.
|
|
`web/assemble.sh` runs under `set -eu`, so a broken cross-reference fails the
|
|
assembly rather than shipping.
|
|
|
|
This is not ceremony. It caught two real defects on its first two runs: a
|
|
fence-close regex that compared a three-character marker against a
|
|
one-character string, which swallowed every document from its first code block
|
|
onward; and a list parser that ended the gotcha list at the first blank line,
|
|
so 47 of the 48 gotcha anchors were never emitted while the "jump to gotcha N"
|
|
strip linked to all of them. Both were invisible in a rendered page that
|
|
otherwise looked fine.
|
|
|
|
It is proved by injection rather than asserted: renaming the emitted anchor ids
|
|
makes it report 48 missing anchors, and pointing the ticket links at a page
|
|
name that is never generated makes it report 763 missing pages — two different
|
|
diagnoses, because "the page is absent" and "the page is there but the anchor
|
|
is not" call for different repairs.
|
|
|
|
Links that **leave** the `docs/` subtree — `../index.html`, `../app/` — name
|
|
files this script does not generate, so they are resolved against the
|
|
filesystem instead. That is the whole reason `assemble.sh` runs the generator
|
|
**last**: its siblings have to exist before they can be checked. An earlier
|
|
version skipped those 48 links while still counting them as checked, behind a
|
|
comment promising they were "checked below" when there was no below.
|
|
|
|
That ordering is itself checked now (improvise-res9). The generator reads
|
|
`web/assemble.sh` and refuses to run unless its own invocation is that script's
|
|
last command. PR #63 appended a loop and it happened to land above the
|
|
generator; one landing below would have made the outward-link check pass
|
|
against a half-built tree, silently and in the one direction this section says
|
|
must not go unnoticed.
|
|
|
|
**And the link check is not sufficient, which is the more important lesson.**
|
|
It answers one question — does every href resolve — and a defect outside that
|
|
question is invisible to it however carefully it is run. The worked example is
|
|
a nested `<a>`: a TOC entry wrapping a heading whose rendered text already
|
|
contained a link produced `<a href="#anchor"><a href="github…">…</a></a>`, and
|
|
the HTML parser closes the outer anchor early, so the entry became an empty
|
|
unclickable stub and clicking it navigated to GitHub. Both hrefs resolved
|
|
perfectly. It shipped past the link check, past the whole-site link check, and
|
|
past a screenshot pass, and was caught by a reviewer *clicking a link*. The
|
|
repair is therefore two things, not one: the TOC keeps a tag-free variant of
|
|
each heading, **and** a separate check asserts no `<a>` appears inside another
|
|
`<a>` — proved by reverting the first half and watching the second report 66.
|
|
|
|
The general form: when a check misses a defect, add the check that could have
|
|
seen it rather than only fixing the defect, and ask what *question* the
|
|
existing check was answering.
|
|
|
|
**`site/` is build output and is not tracked** (improvise-1xfb.4). It is
|
|
`web/assemble.sh`'s default `OUT_DIR`, and `site/docs` is this script's.
|
|
Every reader builds its own copy from source: CI's `build` job runs
|
|
`nix build .#site`, which assembles into the store; the harness assembles
|
|
into `harness/serve/`; `scripts/build-web-release.sh` assembles into a
|
|
temporary stage. For a while 27 generated pages under `site/docs` were
|
|
committed by accident. They went stale, still saying 12 categories after the
|
|
cap was raised, and nothing read them.
|
|
|
|
## 5. Why a node script and not a generator
|
|
|
|
The landing page is hand-written HTML with no build step and one pinned CDN
|
|
script; the same posture applies here. A static site generator would add a
|
|
dependency tree, a lockfile and a toolchain to a repository whose site is
|
|
currently four files, in exchange for features this corpus does not use — the
|
|
docs contain no front matter, no shortcodes and, as it happens, **not a single
|
|
markdown link**. Every cross-reference in them is prose naming a path or an
|
|
id, which is precisely why §2 and §3 are the interesting parts and Markdown
|
|
rendering is the boring part.
|
|
|
|
So the renderer is a deliberately small subset covering exactly what the corpus
|
|
contains: ATX headings, fenced code, pipe tables, blockquotes, ordered and
|
|
unordered lists with indented continuations, thematic breaks, and inline code,
|
|
strong, emphasis and links. Anything it does not recognise becomes paragraph
|
|
text, so **no input line is ever dropped** — the failure mode is an ugly
|
|
paragraph, never a missing one.
|
|
|
|
Two details in it are load-bearing and easy to undo. **Code spans are replaced
|
|
by placeholders before emphasis is matched**, because this corpus routinely
|
|
opens `**` on one side of a code span and closes it on the other; matching
|
|
emphasis per segment silently drops that bold, which is a fidelity loss with
|
|
no visible symptom. And **strong is matched before emphasis, with a body that
|
|
may contain a single `*` pair**, because `[^*]+` cannot match
|
|
`**a *b* c**` and the leftover `*` then pairs across the bold — which is how a
|
|
single sentence in gotcha 33 came out as three italic paragraphs.
|
|
|
|
A third, added later and for the same reason: **a table cell boundary is an
|
|
unescaped pipe outside a code span**, so the row is walked rather than
|
|
`split("|")`. This corpus documents a file format whose own quoting syntax is
|
|
`|Time Period|`, and a sentence containing one inside a table cell would have
|
|
been shredded into extra columns — a row that still parses, so no column count
|
|
could detect it. It was latent when fixed, which is the point: the trigger is
|
|
one ordinary sentence away in a document whose subject is pipe quoting.
|
|
|
|
One deliberate divergence from GFM inside that rule: a `\|` **inside a code
|
|
span** in a table cell keeps its backslash, where GFM would unescape it to a
|
|
bare pipe. Splitting is suppressed inside code spans, so the escape is never
|
|
needed there and never consumed. The corpus contains no instance; it is
|
|
recorded as a decision rather than left to be rediscovered as a bug.
|
|
|
|
## 6. Navigation
|
|
|
|
`repo-map.md` carries a digest of the numbered gotchas and `gotchas.md` the long
|
|
form; the map was condensed because it was too long to put in every new agent's
|
|
context. Splitting per gotcha was rejected: the gotchas cross-reference each
|
|
other constantly ("see gotcha 35", "for the reason gotcha 39 watches…"), and a
|
|
reader following those wants a scroll, not one page load per number. Each of the
|
|
two stays one page, with an id on every list item and a compact strip of
|
|
numbered links under the heading.
|
|
The ids come from the list item's **own number**, not from a counter, so an
|
|
inserted gotcha renumbers the anchors exactly as it renumbers the prose that
|
|
cites them.
|
|
|
|
The strip's contents are **derived from the anchors actually emitted**, never
|
|
from a count. A hardcoded 48 failed in only one direction: deleting a gotcha
|
|
left a link to a missing anchor, which the link check catches, while *adding*
|
|
one produced an unlinked gotcha and a clean build. The list only ever grows, so
|
|
the hardcoded form was silent in precisely the direction it would be used —
|
|
the same shape as an exclusion list that over-matches, failing in the direction
|
|
that looks like success.
|
|
|
|
**Long unbreakable headings need `overflow-wrap`.** The test-evidence pages are
|
|
hundreds of `snake_case` test names as headings; at phone width seven of them
|
|
overflowed the viewport by up to 92%. The diagnosis is worth keeping because
|
|
the first guess was wrong: `getBoundingClientRect` reported every element
|
|
inside the viewport, because the text overflows its box without widening it.
|
|
Only `scrollWidth` per element names the culprit. Measure the property that
|
|
actually overflowed rather than the one that usually does.
|
|
|
|
## 7. The phone nav is a checkbox, and that is not arbitrary
|
|
|
|
At phone width the sidebar's 25 links stacked above the content, so every page
|
|
opened on roughly 1500px of nav. The wall was not inherited: **all of this CSS
|
|
is new**, so it shipped for the first time with the site, which is the test for
|
|
whether a defect is pre-existing and the one most easily got wrong in the
|
|
direction of declining work you introduced.
|
|
|
|
`<details>` is the obvious control and it is **wrong here**, for a reason no
|
|
spec-level reading predicts. The sidebar layout has to force the disclosure
|
|
*open* at wide widths, and a closed `<details>` hides its content through
|
|
`::details-content` — which `display` cannot override. Measured in Chromium,
|
|
the desktop sidebar came out collapsed to a single link with its toggle also
|
|
hidden: unusable, and in the half nobody would think to re-check after a
|
|
phone-only change. A hidden checkbox plus a `<label>` puts the hiding in *our*
|
|
media query, so forcing it open is ordinary cascade. Anyone reading the markup
|
|
will see a checkbox where `<details>` obviously belongs; this paragraph is why
|
|
it must not be tidied into one.
|
|
|
|
**And the first probe of the fix could not see the difference.** Counting nav
|
|
links whose `getBoundingClientRect().height > 0` reported **26 visible in both
|
|
the collapsed and the expanded state**, because Chrome reports a height for
|
|
`content-visibility`-hidden children — so it would have passed the broken
|
|
`<details>` build. `checkVisibility({contentVisibilityAuto: true,
|
|
visibilityProperty: true})` discriminates.
|
|
|
|
That is the same instrument failing twice: §6's overflow fix also measured
|
|
clean by `getBoundingClientRect` while the page still scrolled. **A measurement
|
|
that cannot distinguish the two states is not a measurement** — the repo's
|
|
signature defect, a test that passes while the thing it names is broken,
|
|
arriving through the instrument rather than through the assertion. Before
|
|
trusting a browser measurement, injure the thing being measured and check the
|
|
number moves.
|
|
|
|
**And the fix's own probe had the same shape one level up.** Counting *visible*
|
|
nav links answers "does operating the toggle reveal the links" — correct and
|
|
complete for a mouse — and is silent on whether any non-mouse route exists. The
|
|
first version of the checkbox carried `hidden`, which is `display:none`, which
|
|
removes it from the tab order *and* from the accessibility tree: the label is
|
|
not focusable on its own, so at ≤900px the navigation could be clicked and
|
|
reached no other way, and the accessibility snapshot contained neither the
|
|
checkbox nor any of the 25 links. A visibility count cannot see that, however
|
|
sharp the visibility instrument. The toggle is therefore visually hidden but
|
|
focusable at narrow widths and `display:none` at wide ones, and the check is
|
|
tab order, `.focus()` and the accessibility snapshot — measured at 390, 900,
|
|
901 and 1280, because the regression lives on one side of a media query.
|
|
|
|
So the rule from §6 has a second half: **a measurement of the wrong property is
|
|
not a measurement either, however precise it is.** `checkVisibility` fixed the
|
|
visibility instrument; it does not turn a visibility count into an operability
|
|
check. Ask what the defect would *do* to a user, then pick the instrument that
|
|
observes that.
|
|
|
|
**A correction, and the risk the original argument missed.** The commit that
|
|
added `content: "\2630 " / ""` said Firefox ignores the alt-text form and falls
|
|
back to reading the content, so the change was "an improvement in two engines
|
|
and a no-op in the third". **That mechanism is wrong** — Firefox shipped the
|
|
syntax in 133, and all three engines parse it (measured: Chrome 141, Firefox
|
|
142, WebKit 26 each compute the declaration intact). The conclusion held; the
|
|
reason given for it did not, and the reason was the load-bearing part, because
|
|
it was what made the change look like a one-way door. It is one because every
|
|
engine shipped to supports the syntax — not because the unsupported path is
|
|
harmless. That commit message is superseded by this paragraph (design-principles
|
|
§6: an argument must not live only in a commit message).
|
|
|
|
**The unsupported path is not harmless, and that is the risk neither the change
|
|
nor its first review named.** `/ alt` in an engine that does not know it is a
|
|
*parse error for the whole `content` declaration*, so the glyph would not lose
|
|
its alt text — it would **vanish**. A silent visual regression, the opposite of
|
|
the assumed failure mode. The glyph is therefore verified painted in all three
|
|
engines by pixel diff against `::before { content: none }`, with the text
|
|
starting 20px further right in each.
|
|
|
|
That check also produced this file's fourth instance of the wrong-property
|
|
trap, and the sharpest, because it produced a confident **false positive**: the
|
|
first probe measured the label's *width* with and without `::before` and
|
|
reported the glyph missing in all three engines. The label is `display: block`,
|
|
so its width comes from the container and cannot see inline content at all.
|
|
Right question, wrong property, and this time the wrong instrument accused
|
|
working code rather than clearing broken code.
|