1a0f04856e
Review of PR #62 found four live defects and four smaller ones. The most valuable is the first, because of what it says about the rest. A TOC entry wrapping a heading whose rendered text already contained a link emitted <a href="#anchor"><a href="github...">...</a></a>. The parser closes the outer anchor early, so the entry became an empty unclickable stub and clicking "src/draw.rs" in the TOC navigated to GitHub. 19 occurrences over 10 pages. Both hrefs resolved perfectly, so the link check, the whole-site link check and a screenshot pass all missed it -- it answers "does every href resolve" and this is not that question. So the fix is two things: the TOC keeps a tag-free variant of each heading, AND a new check asserts no <a> appears inside another <a>. Proved by reverting the first half: 66 nested anchors reported, assembly exits 1. Clicking that TOC entry now lands on #src-draw-rs. esc() escaped & < > but not the quote it was interpolated into title="..." with. Five shipped bead titles contain one, and Chromium parsed the resulting tag as having three junk attributes. Now escapes " and ' as well. Today's corpus being friendly is not a reason to interpolate tracker text unescaped. Seven pages overflowed at 390px by up to 92%. The reviewer attributed it to the TOC; the TOC was real but not sufficient, and the first fix measured clean by getBoundingClientRect while the page still overflowed, because the heading text overflows its box without widening it. Only per-element scrollWidth names the culprit: hundreds of snake_case test names as headings. overflow-wrap on the headings and on .toc a, plus dropping display:table from .tickets, which had been cancelling the mobile overflow-x guard. 0 of 23 pages overflow at 390 and 1280 now, measured. table() split rows on every pipe before inline parsing, so `|Time Period|` -- this repo's own quoting syntax, in a corpus whose subject is that syntax -- was shredded into extra columns. A row that still parses, so no column count could see it. Now walked, respecting code spans and \|. Proved with a fixture: 9 tds with the fix, 12 with the old splitter reinstated. The four smaller ones: the link check counted 48 links it never verified behind a comment promising they were "checked below" when there was no below -- those are the links leaving the docs tree, and they are now resolved against the filesystem, which is why assemble.sh runs the generator LAST, once its siblings exist. The gotcha jump bar's hardcoded 48 is derived from the emitted anchors, because it failed closed on deletion and silently on addition, and the list only grows. context/code/doc-site.md cited improvise-xxxx and so broke the audit the surrounding paragraph describes; the repo-wide audit is now 189 ids, all resolving. And the Conventions paragraph's own example path now goes through inline(), so the sentence demonstrates the linking instead of asserting it. The ticket index keeps all 180 rows, open bugs and titles included, by the user's ruling: the data is already public in .beads/issues.jsonl, the page only makes it legible, and filtering it while the lede links the raw JSONL would be theatre. Gates: 1378 rust tests, 26 node --test, 16 playwright (3 skipped, webkit OPFS), all three check-comment-runs modes, 3460 site links / 0 broken, 0 nested anchors, 189 bead ids all resolving. improvise-djoo Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
108 lines
5.1 KiB
Bash
Executable File
108 lines
5.1 KiB
Bash
Executable File
#!/bin/sh
|
|
# Assemble the static site into `site/`: the landing page from `docs/` at the
|
|
# root, and the browser app from `web/` under `app/`.
|
|
#
|
|
# `web/` is the *source* directory: it also holds build.sh, assemble.sh and
|
|
# .gitignore, which have no business being served. This copies what ships.
|
|
#
|
|
# Extracted from the CI workflow so that the Pages job and the browser harness
|
|
# assemble the same tree — a harness that runs against a different arrangement
|
|
# of files is testing something nobody deploys (improvise-vvl3). Same reason
|
|
# `web/build.sh` exists rather than two copies of the wasm-pack invocation.
|
|
#
|
|
# Usage: web/assemble.sh [OUT_DIR] [CACHE_BUST_TOKEN]
|
|
#
|
|
# The token cache-busts the two generated bundles. Pages serves everything
|
|
# `max-age=600` and the wasm-pack filenames carry no content hash, so for ten
|
|
# minutes after a deploy a returning visitor runs the PREVIOUS build — which
|
|
# during improvise-9x6 debugging made a shipped fix look like it had not
|
|
# shipped, twice. CI passes the commit SHA; a local run defaults to `dev`.
|
|
#
|
|
# Both halves are stamped, because a fresh glue file does NOT pull a fresh
|
|
# .wasm: the glue locates it with `new URL('..._bg.wasm', import.meta.url)`,
|
|
# and a URL constructed against a base does not inherit the base's query
|
|
# string. Stamping only the import specifier would refresh the wrapper and
|
|
# keep the stale binary — the failure mode this is here to end.
|
|
set -eu
|
|
|
|
root=$(cd "$(dirname "$0")/.." && pwd)
|
|
out=${1:-"$root/site"}
|
|
token=${2:-dev}
|
|
|
|
test -d "$root/web/pkg" || { echo "web/pkg missing — run web/build.sh first" >&2; exit 1; }
|
|
test -d "$root/web/worker-pkg" || { echo "web/worker-pkg missing — run web/build.sh first" >&2; exit 1; }
|
|
|
|
rm -rf "$out"
|
|
mkdir -p "$out"
|
|
|
|
# The landing page is the front door and the app is what it sends you to
|
|
# (user ruling, improvise-kh8). So `docs/` lands at the ROOT and the app moves
|
|
# under `app/` whole — every path in app.js and worker.js is relative, so the
|
|
# bundles travel with it and the generated glue needs no editing.
|
|
cp -f "$root/docs/index.html" "$out/index.html"
|
|
cp -rf "$root/docs/casts" "$out/casts"
|
|
|
|
app="$out/app"
|
|
mkdir -p "$app"
|
|
# shell.js and serial.js are imported by app.js and worker.js: leaving either
|
|
# out is a page that fails to load its own module graph, not a degraded one.
|
|
cp -f "$root/web/index.html" "$root/web/app.js" "$root/web/shell.js" \
|
|
"$root/web/serial.js" "$root/web/style.css" "$root/web/worker.js" "$app/"
|
|
# The document the page opens on a browser that has never seen one.
|
|
# web/worker.js fetches it by the ?seed= URL and hands the bytes to Rust,
|
|
# which writes them through the VFS — so leaving it out is an empty grid, not
|
|
# a 404 the page could report.
|
|
cp -f "$root/web/demo.improv" "$app/"
|
|
|
|
cp -rf "$root/web/pkg" "$app/pkg"
|
|
cp -rf "$root/web/worker-pkg" "$app/worker-pkg"
|
|
rm -f "$app/pkg/.gitignore" "$app/pkg/package.json"
|
|
rm -f "$app/worker-pkg/.gitignore" "$app/worker-pkg/package.json"
|
|
|
|
# BSD sed (macOS) needs an argument to -i; GNU sed must not be given one.
|
|
sedi() { sed "$1" "$2" > "$2.tmp" && mv -f "$2.tmp" "$2"; }
|
|
|
|
sedi "s|\./pkg/improvise_wasm_client\.js|&?v=${token}|" "$app/app.js"
|
|
sedi "s|\./worker-pkg/improvise_worker_server\.js|&?v=${token}|" "$app/worker.js"
|
|
sedi "s|'improvise_wasm_client_bg\.wasm'|'improvise_wasm_client_bg.wasm?v=${token}'|" \
|
|
"$app/pkg/improvise_wasm_client.js"
|
|
sedi "s|'improvise_worker_server_bg\.wasm'|'improvise_worker_server_bg.wasm?v=${token}'|" \
|
|
"$app/worker-pkg/improvise_worker_server.js"
|
|
|
|
# One `grep` over all four files succeeds when ANY one of them matches, so
|
|
# three failed substitutions could still produce a "successful" assembly under
|
|
# `set -e` (found by Copilot on PR #24). These checks are the only guard
|
|
# against the generated glue changing shape, so each stamped reference is
|
|
# verified on its own and names itself when it is the one missing.
|
|
stamped() {
|
|
grep -q "$2" "$1" || {
|
|
echo "assemble: $1 is missing the cache-bust stamp: $2" >&2
|
|
exit 1
|
|
}
|
|
grep -n "$2" "$1"
|
|
}
|
|
stamped "$app/app.js" "pkg/improvise_wasm_client.js?v=${token}"
|
|
stamped "$app/worker.js" "worker-pkg/improvise_worker_server.js?v=${token}"
|
|
stamped "$app/pkg/improvise_wasm_client.js" "improvise_wasm_client_bg.wasm?v=${token}"
|
|
stamped "$app/worker-pkg/improvise_worker_server.js" "improvise_worker_server_bg.wasm?v=${token}"
|
|
|
|
# Every relative import the two entry points make must have been copied. The
|
|
# cp list above is a second answer to "what ships", and a missing sibling is a
|
|
# 404 at module-load time, which the page cannot report.
|
|
for entry in app.js worker.js; do
|
|
deps=$(sed -n 's|.*from "\./\([A-Za-z0-9_.-]*\.js\)".*|\1|p' "$app/$entry")
|
|
for dep in $deps; do
|
|
test -f "$app/$dep" || {
|
|
echo "assemble: $entry imports ./$dep, which was not copied" >&2
|
|
exit 1
|
|
}
|
|
done
|
|
done
|
|
|
|
# The prose site at `docs/`, generated from context/ by the same script the
|
|
# harness serves. It runs LAST because its link check verifies the links that
|
|
# leave its own tree (`../index.html`, `../app/`) against the filesystem, which
|
|
# means their targets must already be here. It exits non-zero on a break, so a
|
|
# dead cross-reference fails the assembly rather than shipping.
|
|
node "$root/scripts/build-doc-site.mjs" "$out/docs" "$token"
|