Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .github/actions/run-gates/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,14 @@ runs:
- name: Lint the tooling (check_lint.mjs)
shell: bash
run: node scripts/check_lint.mjs
# Unit tests for the site search, run by Node's own test runner: what the
# entries builder/search.mjs writes hold, which the build's check does not
# read, and guards that the online client, the offline client and
# eval/site_search.mjs's replica still agree. No browser, no built tree,
# well under a second.
- name: Unit-test the site search (test/search.test.mjs)
shell: bash
run: node --test test/search.test.mjs
# A regex that backtracks exponentially does not fail a build, it stops
# one: the corpus passes until a page happens to contain the trigger, and
# then a render worker sits inside String.replace forever. VOID_TAGS_RE
Expand Down
10 changes: 8 additions & 2 deletions WIP.Search.md
Original file line number Diff line number Diff line change
Expand Up @@ -618,8 +618,14 @@ found" (`.search-no-result`, reused rather than adding a class, since
visually it's the same single centred message) and the same text in the
`a11y-status` live region, then yields (a `requestAnimationFrame` raced by
a 100 ms timer, since frames never fire in a hidden tab, then
`setTimeout(fn, 0)`) so that message actually paints before the
synchronous, comparatively expensive index build runs on the main thread.
`setTimeout(fn, 0)`) so that message actually paints before the index
build begins on the main thread. The build runs 25 ms at a time and yields
between slices (`buildIndexInSlices()`, lunr's own work in pieces: an
entry added, a hundred fields' vectors, a term put into the token set), so
the reader can keep typing while the message shows; the longest task left
is about 50 ms. Built in one piece, the index held the main thread for
about 1.6 s, the box took no keystrokes, and the first search was for the
text typed before the build began.
A keystroke during the load leaves the message in place. Checked in a
browser: the message stays up until results replace it, with no empty
panel in between, both online and in the offline tree.
Expand Down
5 changes: 3 additions & 2 deletions WIP.md
Original file line number Diff line number Diff line change
Expand Up @@ -451,7 +451,7 @@ Why the report separates the wedged task from the merely blocked ones, and why
- `build.bat` — runs `node builder\tbdocs.mjs --src docs --check-audit-index` (which implies `--check`) and produces three trees in one pass: the online copy at `_site/`, a `file://`-browsable copy at `_site-offline/`, and the sparse pagedjs source at `_site-pdf/`. The offline pass adds ~700 ms and the PDF pass adds ~150 ms on top of the ~2 s online build. Toggle `also_build_offline` / `also_build_pdf` in `_config.yml` (or pass `--no-offline` / `--no-pdf`) to skip a sibling output. `--check` adds ~1.7 s and runs the link + integrity check over the HTML while it is still in worker memory; `build.bat --no-check` gets a plain build.
- `serve.bat` — runs `tbdocs --serve`: initial build, then a long-lived process with watcher, debounced rebuilds, and SSE-driven browser auto-reload. Writes to `docs/_serve/` (disjoint from `build.bat`'s `_site*/`) and skips the offline + PDF passes — so a one-off `build.bat` for the PDF or offline mirror doesn't disturb the live preview. Ctrl+C to stop.
- `check.bat` — the gates that read the built site: a freshness check that refuses a stale tree (`scripts/check_tree_fresh.mjs`), the DOT diagram fit check (`scripts/check_dot_fit.mjs`), the a11y sample-coverage check (`scripts/pick_a11y_sample.mjs --check`), then the accessibility check (`scripts/check_a11y.mjs`). The link + integrity check moved into `build.bat`. ~37 s.
- `test.bat` — the tests the *toolchain* has to pass: the publish-allowlist self-test (`scripts/check_publish_policy.mjs`), the gate-list check (`scripts/check_gate_lists.mjs`), the CI-workflow roster check (`scripts/check_ci_workflows.mjs`), the lint gate (`scripts/check_lint.mjs`), the regex-safety gate (`scripts/check_regex_safety.mjs`), the code-region gate (`scripts/check_code_regions.mjs`), the page-count drift-guard probes (`scripts/check_page_baseline.mjs`), the book-coverage probes (`scripts/check_book_coverage.mjs`), the symbol-index probes (`scripts/check_symbol_index.mjs`), the command-line probes and cases (`scripts/check_cli.mjs`), and the axe source-patch verification (`scripts/check_axe_patch_equiv.mjs`). ~9 s. See [What belongs in test.bat rather than check.bat](WIP.Build.md#what-belongs-in-testbat-rather-than-checkbat).
- `test.bat` — the tests the *toolchain* has to pass: the publish-allowlist self-test (`scripts/check_publish_policy.mjs`), the gate-list check (`scripts/check_gate_lists.mjs`), the CI-workflow roster check (`scripts/check_ci_workflows.mjs`), the lint gate (`scripts/check_lint.mjs`), the site-search unit tests (`node --test test/search.test.mjs`), the regex-safety gate (`scripts/check_regex_safety.mjs`), the code-region gate (`scripts/check_code_regions.mjs`), the page-count drift-guard probes (`scripts/check_page_baseline.mjs`), the book-coverage probes (`scripts/check_book_coverage.mjs`), the symbol-index probes (`scripts/check_symbol_index.mjs`), the command-line probes and cases (`scripts/check_cli.mjs`), and the axe source-patch verification (`scripts/check_axe_patch_equiv.mjs`). ~9 s. See [What belongs in test.bat rather than check.bat](WIP.Build.md#what-belongs-in-testbat-rather-than-checkbat).
- `book.bat` — renders the PDF from `docs\_site-pdf\book.html` via `node book\render-book.mjs` into `docs\_pdf\twinBASIC Book.pdf`. Run `build.bat` first to populate `_site-pdf/`; `book.bat` refuses a tree older than its sources rather than rendering the previous book (see [The book refuses a stale source tree](WIP.Build.md#the-book-refuses-a-stale-source-tree)).

- `examples.bat` — compiles the documentation's own twinBASIC code samples, every `tb` fence marked `check_build`, and reports the ones the compiler refuses against the line in the page they came from. Needs a twinBASIC install and Windows, so it is outside every gate and outside CI; ~120 s over the 1,129 samples marked as of 2026-09-25. Two modes need no compiler at all: `--census` classifies every fence and says how many classifiable ones are still unmarked, and `--report <survey.json>` groups a saved `--propose --json` survey by diagnostic, section and unresolved name. `--propose` itself does compile. See [Compiling the reference's own code samples](#compiling-the-references-own-code-samples) and [WIP.ExamplesBuild.md](WIP.ExamplesBuild.md).
Expand All @@ -469,7 +469,7 @@ build.bat && check.bat

On the dev box that is ~4 s of build against ~37 s of check, of which the axe scan is ~20 s. [builder/PLAN-checks.md](builder/PLAN-checks.md) records how the link checker got folded into the build's task graph, what it cost and what it saved; the axe follow-ons are designed there but not implemented.

**If the change touched `builder/`, `scripts/`, `lib/`, `book/`, `eval/`, `wisdom/`, `test/`, the site's scripts in `docs/assets/js/`, a wrapper or a workflow, run `test.bat` as well** --- another ~9 s. Eight of its eleven gates cannot be affected by an edit under `docs/` at all. **Three can.** `check_lint.mjs` lints the site's two scripts in `docs/assets/js/` along with the tooling. `check_gate_lists.mjs` is the easy one to predict: it reads `README.md` and every page under `docs/Documentation/`, so an edit to any developer page that states a gate count can fail it. **`check_code_regions.mjs` is the one worth understanding**, and which half of it a content edit reaches is worth keeping straight. Its corpus sweep reads `DOCS_DIR`, which is `<repo>/docs`, and tokenises all 906 markdown files, so a page that provokes a rewrite into *altering* a code region fails it --- that half is content-dependent. Its fixed probes are not: they run against their own sources whatever the tree holds, and they cover the **mirror** fault, where a rewrite silently stops firing. The sweep structurally cannot see that one, because text the rewrite skipped is stashed and restored unchanged and every region still matches. So run `test.bat` after adding an unusual code construct --- a fence whose contents include a fence marker, a 4-space indented block, an admonition wrapping a fence --- and read the built page as well, because for the mirror fault the gate is asserting that the stasher still works rather than checking your page:
**If the change touched `builder/`, `scripts/`, `lib/`, `book/`, `eval/`, `wisdom/`, `test/`, the site's scripts in `docs/assets/js/`, a wrapper or a workflow, run `test.bat` as well** --- another ~9 s. Nine of its twelve gates cannot be affected by an edit under `docs/` at all. **Three can.** `check_lint.mjs` lints the site's two scripts in `docs/assets/js/` along with the tooling. `check_gate_lists.mjs` is the easy one to predict: it reads `README.md` and every page under `docs/Documentation/`, so an edit to any developer page that states a gate count can fail it. **`check_code_regions.mjs` is the one worth understanding**, and which half of it a content edit reaches is worth keeping straight. Its corpus sweep reads `DOCS_DIR`, which is `<repo>/docs`, and tokenises all 906 markdown files, so a page that provokes a rewrite into *altering* a code region fails it --- that half is content-dependent. Its fixed probes are not: they run against their own sources whatever the tree holds, and they cover the **mirror** fault, where a rewrite silently stops firing. The sweep structurally cannot see that one, because text the rewrite skipped is stashed and restored unchanged and every region still matches. So run `test.bat` after adding an unusual code construct --- a fence whose contents include a fence marker, a 4-space indented block, an admonition wrapping a fence --- and read the built page as well, because for the mirror fault the gate is asserting that the stasher still works rather than checking your page:

```sh
build.bat && check.bat && test.bat
Expand Down Expand Up @@ -506,6 +506,7 @@ wrapper:
| `test.bat` | `check_cli` | `lib/cli.mjs` parses as a strict `parseArgs` does, and keeps a lenient tool's leniency where asked; each tool's recorded command-line errors still exit and print as recorded, run with an IDE and a browser that do not exist |
| `test.bat` | `check_ci_workflows` | both CI workflows run every wrapper gate, with the same arguments and order, and build with `build.bat`'s flags |
| `test.bat` | `check_lint` | Biome finds nothing in the tooling, warnings included, and checked at least one script |
| `test.bat` | `test/search.test.mjs` | the search entries `builder/search.mjs` writes hold what they should, and the copies of the search client still agree. Run by `node --test`; the gate roster reads such a line as a gate, named by its path |
| `test.bat` | `check_publish_policy`, `check_gate_lists`, `check_page_baseline`, `check_book_coverage`, `check_axe_patch_equiv` | the gates on the gates |

**A gate belongs in `test.bat` rather than `check.bat` if it would still mean
Expand Down
35 changes: 17 additions & 18 deletions book/render-book.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import { dirname, resolve } from 'node:path';
import { writeFileSync, existsSync } from 'node:fs';
import puppeteer from 'puppeteer';
import { PDFDocument } from 'pdf-lib';
import { parseCli, withUsageError } from '../lib/cli.mjs';
// Side-effecting imports. Mutate pdf-lib's live module exports
// before any pdf-lib operation -- order doesn't matter. See
// perf/notes/08-pdf-lib.md.
Expand Down Expand Up @@ -201,24 +202,22 @@ const __dirname = dirname(fileURLToPath(import.meta.url));

// --- arg parsing --------------------------------------------------------

const args = process.argv.slice(2);
let inputArg = null;
let outputArg = null;
let outlineTagsArg = 'h1,h2,h3,h4';
let timeoutMs = 0;
const additionalScripts = [];
for (let i = 0; i < args.length; i++) {
const a = args[i];
if (a === '-o' || a === '--output') outputArg = args[++i];
else if (a === '--outline-tags') outlineTagsArg = args[++i];
else if (a === '-t' || a === '--timeout') timeoutMs = parseInt(args[++i], 10);
else if (a === '--additional-script') additionalScripts.push(args[++i]);
else if (!inputArg && !a.startsWith('-')) inputArg = a;
else {
console.error(`unknown arg: ${a}`);
process.exit(2);
}
}
const { values, positionals } = withUsageError(() => parseCli(process.argv.slice(2), {
options: {
output: { type: 'string', short: 'o' },
'outline-tags': { type: 'string', default: 'h1,h2,h3,h4' },
timeout: { type: 'string', short: 't', default: '0' },
'additional-script': { type: 'string', multiple: true },
},
positionals: { max: 1 },
unknown: 'error',
acceptsValue: () => true,
}), { format: (err) => `unknown arg: ${err.arg}`, exitCode: 2 });
const inputArg = positionals[0];
const outputArg = values.output;
const outlineTagsArg = values.outlineTags;
const timeoutMs = parseInt(values.timeout, 10);
const additionalScripts = values.additionalScript;
if (!inputArg || !outputArg) {
console.error('usage: node render-book.mjs <input.html> -o <output.pdf> [--outline-tags ...] [-t ms] [--additional-script path]...');
process.exit(2);
Expand Down
Loading
Loading