Skip to content

Second batch of accessibility, content and tooling improvements. - #192

Merged
KubaO merged 177 commits into
twinbasic:mainfrom
KubaO:staging
Sep 21, 2026
Merged

KubaO merged 177 commits into
twinbasic:mainfrom
KubaO:staging

Conversation

@KubaO

@KubaO KubaO commented Sep 21, 2026

Copy link
Copy Markdown
Collaborator
  • Self-hosted typography: Inter, Cascadia Mono and Source Serif 4 subsets.
  • Every diagram is Graphviz DOT.
  • A use-case evaluation harness (eval/) that asks whether the documentation works
    rather than whether it is accurate.
  • Six new gates: publish allowlist, code-region integrity, exponential-regex safety,
    gate-list drift, page-count baseline and diagram fit.
  • Build-time rewrites no longer corrupt code samples.
  • Silent build failures now report themselves.
  • {{tbdocs:<name>}} build-time counts** to keep build-related figures current.
  • Headless twinBASIC tooling: tbbuild.mjs, tbrun.mjs
  • Documented more attributes, the Debug object, and WithEvents.

The site rode just-the-docs' `system-ui, -apple-system, "Segoe UI", ...`
default, so every platform rendered a different typeface at different
metrics. Inter is the brand face -- twinbasic.com has always named it
first and never shipped a @font-face to deliver it.

Three OFL faces, committed as subset .woff2 with their licences and
regenerable by scripts/build_fonts.py from SHA-256-pinned upstream
releases: Inter for all web text, PDF headings and diagram labels;
Cascadia Mono for all code; Source Serif 4 for PDF body only. `opsz` is
pinned out (~70 KB/face, more than trimming the charset would save);
`wght` stays variable, which is what makes the two `font-weight: 350`
rules resolve to a real 350. 219 KB on a cold visit, cached across 869
pages.

Unified across web light and dark, the offline mirror, the PDF book, the
DOT/Gantt/Mermaid diagrams and the diagram export buttons.

Five things this surfaced that were already wrong:

  * <kbd> on eleven pages, and <pre>, were never on the mono stack.
    normalize puts pre/code/kbd/samp on generic `monospace` and JTD
    overrides only `code`, so those rendered in Courier New / Consolas.
  * The footnote back-link has never met WCAG 2.5.8 in any font --
    12.09x19 on Segoe UI, 12.09x15 on Liberation Sans, 15.97x17 on
    Inter. It passed on axe's spacing exception, and Inter's wider U+21A9
    moved the centre enough on one footnote of one page to fail. Sized
    explicitly at 24x24 instead of restoring the clearance.
  * PNG export has never worked on the two Mermaid diagrams: Chromium
    taints a canvas that has had <foreignObject> drawn into it, and the
    throw happened inside an onload handler where nothing surfaced it.
    Not fixable by config -- `htmlLabels: false` keeps node labels in a
    foreignObject on Mermaid 11. It now fails loudly and names Download
    SVG as the alternative.
  * The PDF was embedding a Times New Roman subset for seven footnote
    arrows Source Serif lacks, and a Consolas subset for the <kbd> text.
    The book's chains are self-hosted all the way down now.
  * The forked paged.js rejected any @font-face not in state `loaded`,
    which a declared-but-unused face never reaches. It now rejects
    `loading` and `error` only.

Two traps the wiring has to respect, documented at both sites: the dark
compilation re-emits every JTD base rule at raised specificity, so the
stacks must be passed to modules-dark.scss as well or dark mode silently
keeps system fonts; and @font-face must be emitted exactly once, from
the light compilation, hence the mixin.

Mermaid sizes each node box to the text it measured, so re-pointing
font-family in a committed export clips every label. scripts/
render_mermaid.mjs re-exports from the _Images/*.md sources with Inter
loaded and asserts no label overflows.

Diagram exports embed the faces they use as data: URIs -- an SVG in
<img> renders in secure static mode and fetches nothing, so an exported
PNG asking for Inter rasterised pixel-identical to one asking for a font
that does not exist. The bytes come from cache; ~25 ms per click.

Verified: build.bat and check.bat clean; full axe sweep over all 869
pages x 2 themes x 2 viewports, 3476 audits, 0 violations; PDF renders
1992 pages embedding all three faces.
Both files joined composite cache keys with a literal U+0000 in a template
literal. The separator choice is right -- a NUL cannot occur in a path, URL
or fragment, so keys cannot collide -- but written as a raw byte it makes the
source not-plain-text: grep reports "Binary file matches" and hides the line,
and file(1) reports "data". git was unaffected, since both NULs sit past its
8000-byte sniff window, which is why it went unnoticed through review.

The escape produces the identical string, so this is a source-encoding change
only. Both files are now JavaScript source, Unicode text, UTF-8 text.
checks.yml's fixture-built gate has been red since 58f9c6e. The font work
put two <link rel="preload" href="/assets/fonts/..."> tags on every page;
test/fixtures/check-src/ has no assets/fonts/, so each of its three pages
gained two broken links and the broken count went 3 -> 9 against a
hard-coded FIXTURE_BUILT_ONLINE.broken = 3.

The two checker implementations agreed throughout -- what failed was the
fixture's own expectation, which is the assertion working as designed.

Stubs rather than a raised expectation: the checker only tests that a target
exists, and six accidental broken links would dilute a category the fixture
exists to hold at an exact, meaningful number. Note the fixture can break
without being touched, since it is built by the real tbdocs and inherits
whatever the page template emits unconditionally.
The page is advertised as the complete interface reference and received
+3/-2 lines across a range that added four tasks and five modules.

Corrects two statements that would misdirect: task priority claimed
lower-number-wins (sab-scheduler keeps the largest pri it finds, and
flush:i relies on it), and renderJoin was shown with expected = [], the
pre-bug shape f177c28 fixed.

Adds Section 5 for linkJoin / checkBook / checkReport, the vendorAssets
seed, and export tables for check.mjs, check-tree.mjs, link-check.mjs,
vendor-assets.mjs and dot-metrics.mjs. Documents the check that now runs
inside flush:i, the four check* fields on SharedState, the four check
BuildOpts, sitemapIncludes / searchIncludes / stripFontPreloads, and the
stale expected lists on markdownInit, writeAssets and writePdf.
writeOffline and writePdf are no longer terminal.
Worked Example B showed `if (!p) continue;` in a render submit callback.
tbdocs.mjs throws there now, and WIP.md names it as a rule new fan-out
code must not regress -- on the merge path, a missing piece is a bug, not
a case to handle. Replaced, with a callout giving the reason.

Also fixes the priority ordering (higher claims first) and the in-tree
plugin count, which is fourteen since headingLevelNormalizePlugin,
videoLinkPlugin and remoteImagePlugin landed.
…ists

check.bat runs five steps; the page said three cheaper gates precede the
axe scan and never named check_dot_fit.mjs at all, which 6170e07 added.

Drops the claim that the Graphviz path carries "no in-tree patches" --
false since dot-metrics.mjs began writing Inter's advance widths into the
WASM module's memory, and contradicted by Builder.md two pages over. Adds
a short section on why that patch exists and what the fit gate proves.
The page promises "every executable in the documentation repository".
Adds entries for check_tree_fresh, check_dot_fit, check_axe_patch_equiv,
check_a11y_fingerprint, sweep_a11y, build_fonts, build_dot_metrics and
lib/axe-scan -- two of them gates check.bat runs, two more gates CI runs.

Fixes three errors: check.bat has five steps, not four; the PDF rename
instruction pointed at .github/workflows/jekyll-gh-pages.yml, which does
not exist; and "CI invokes it the same way" as build.bat was false for
both workflows and contradicted by the next paragraph. Replaced with a
table of what each caller actually passes.

Also splits the a11y scan's description: lib/axe-scan.mjs defines the
scan, check_a11y.mjs only reports it, and someone told to change the scan
was being pointed at the wrong file.
Builder.md's module map, task lists and "what runs where" never mentioned
check.mjs, link-check.mjs, check-tree.mjs or the three check tasks, though
build.bat passes --check-audit-index on every run. Adds a Verification
group, the Check section, dot-metrics.mjs, and the check that rides along
inside flush:i.

Figures: 31 static tasks not 28; five Gantt sections not four (index.md
repeated the four-section list); ~34 modules; the scheduler SAB is 170 KB,
measured, not 140 KB. PDF-Generation.md showed a book.bat command with
docs/-relative paths and ^ continuations, stale since the batch files
moved to the repository root in 8ac701b.

Authoring.md gains the three opt-out frontmatter keys -- search_exclude in
particular is load-bearing now that the build's link check honours it.
Every non-Core file path in "Where things live", the page-template
examples and the per-symbol workflow predated 58a5e1c, which split the
packages into Reference/Default/ and Reference/Built-In/. The file had no
mention of either directory. Prefixed, with a banner noting the reorg
moved files and not URLs -- the cross-section linking tables resolve
against permalinks and were never affected -- and that Assert's folder is
now TwinBasicAssertions.

Contradictions, each against the tree: build.bat passes
--check-audit-index, not --check; book.bat writes "twinBASIC Book.pdf";
STATE_AUDITS has two entries and so adds eight audits, which is the only
figure consistent with the 60 the same section quotes; tbIDE has 23
CoClasses, as the file itself says seventeen lines later.

Misattributions: font-weight 350 is asked for by the vendored layout.scss
and navigation.scss, not custom.scss, which declares none; the two idiom
anchors live in the published package pages, not in the WIP notes, and
redirect_from cannot rescue a renamed fragment because redirects.mjs emits
whole-page stubs; check_dot_fit runs in checks.yml but not the deploy
workflow; check_links_diff calls the script side in-process; there are
nine finding categories, not ten.

Decimal is a full data type in twinBASIC -- the bullet said the reverse,
contradicting the very page it tells contributors to consult.

Adds AppGlobalClassObject, a thirteenth package that publishes nothing,
with both causes recorded.
PLAN.md called Phase 10 both shipped and planned, described verification
by diffing against Jekyll through verify-phase1..8.mjs / _triage.mjs /
_diff.mjs -- none of which exist, and there is no Jekyll build left to diff
-- listed a "dependencies" key package.json does not have, and never
mentioned PLAN-sab-pull-scheduler.md, which is the scheduler the build
actually runs. Banners rather than rewrites: the narrative is history, the
forward pointers are what was missing.

PLAN-13 sized svg-inline.js at ~80 lines; it is 338, having gained 177 in
this range. PLAN-a11y's top summary still said six sample pages, with its
own superseded note 271 lines below. PLAN-checks said both workflows run
the same four gates; checks.yml runs six and the deploy workflow four,
deliberately.

builder/README.md told you to npm install in builder/, which has no
package.json, pointed at docs/build.bat and friends a move ago, linked a
deleted assets/README.md, omitted every check and fetch flag, stopped its
plan list at PLAN-12, and claimed both CI workflows invoke check_links.mjs
directly, which neither has since b97c75f.
Ten parsed flags appeared in no header block: --unminified, sweep_a11y's
--root-dir/--out/--stock-axe/--recycle-every, pick_a11y_sample's
--root-dir/--budget, and check_links_diff's --max-lines/--base-path-tree/
--build-base-path. --recycle-every occurred exactly once in the whole
repository, on its own argv line.

check_a11y.mjs said the sample is eleven pages; it is thirteen -- the
figure was corrected six -> eleven in three files while the sample was
being widened to thirteen in the same batch. check_links.mjs documented
only exit code 2 of four. tbdocs.mjs listed 9 of 17 flags. tbdocs.mjs and
check.mjs both still said 230 MB, the figure 9f6c38f corrected to ~270 MB
everywhere else. probe-tabs-vs-procs printed a usage line naming a file
that does not exist.
test/ had no README. test/fixtures/check-src/ is consumed only by
check_links_diff.mjs, its per-category expected counts are hard-coded in
that script, and nothing in the tree said so -- editing the fixture without
updating them fails the gate by design.

The sharper point, learned the hard way: the fixture is built by the real
tbdocs, so it inherits whatever the page template emits unconditionally.
Adding font preloads broke it in a commit that touched neither the fixture
nor the checker. When that happens the fix is to add the stub the template
now expects, not to raise the expected count.
33 lines that invited pull requests and never mentioned Node, npm ci, any
of the four batch files, or where the developer documentation lives.
Companion to REVIEW-c9f2dfe0-1b6922b.md, covering the prose rather than
the code. 36 findings, each verified against source before inclusion, plus
the two live defects the amendment pass turned up: the fixture-built CI
gate red since the font work, and AppGlobalClassObject publishing nothing
while serving its raw markdown on docs.twinbasic.com.
A UTF-8 BOM decodes to U+FEFF, which node's utf8 reader hands back as the
first character. gray-matter's test then sees --- instead of ---,
reports no frontmatter, and the page is filed as a static asset: it
vanishes from the nav, the sitemap and the search index, and its raw
markdown is copied into the output tree and served verbatim, frontmatter
keys and all.

That happened to Reference/Built-In/AppGlobalClassObject/index.md and went
unnoticed for months -- an excluded or misfiled page is not a missing one,
so nothing reported it. Windows editors add a BOM without being asked, so
this is fixed at the one place every .md and .html passes through rather
than by policing the files.
"**/_*/**" existed to drop the Affinity sources in _Images, and took the
37 pages under Reference/Built-In/AppGlobalClassObject/_App/ with them.
The twinBASIC interface really is named _App, after the COM
hidden-interface convention, so the folder name and the build convention
collided head-on.

Measured before and after: pages 871 -> 908, nothing lost, static files
unchanged, and every gained page under AppGlobalClassObject/_App/. The
pattern matches a directory, so the five published underscore-prefixed
pages under CustomControls/Framework/ were never affected. Adds **/*.af
so a stray Affinity source filed outside _Images cannot ship either.
index.md carried the BOM and no permalink, so it derived
/Reference/Built-In/AppGlobalClassObject/index.html -- a URL nobody chose,
against which its own relative links did not resolve. Declares
/tB/Packages/AppGlobalClassObject/ like every sibling package, and
corrects the interface link from _App/_App to _App/, which is where that
page's permalink puts it.

Lists the package on Built-In.md, Packages.md and Reference/index.md,
which have undercounted since it was written: ten built-in packages,
thirteen in total.
discover.mjs falls back to "/" + srcRel + ".html" when permalink is
absent, which is only right on a site whose file tree mirrors its URL
tree. This one deliberately does not -- Reference/Built-In/CEF/ publishes
at /tB/Packages/CEF/ -- so outside Features/ a derived URL is structurally
wrong, and wrong silently: the page builds, links into it 404, and nothing
reports either. It is also the URL the IDE help system and in-source
[Documentation("...")] attributes resolve against.

Aborts in the same pass as the nav-parent check, which has the same
failure character, and covers every page rather than only nav-visible ones
-- a nav_exclude page is still reachable by URL.

906 of 908 pages already complied. The two that did not were publishing at
a .html URL none of their own siblings use; the check then found a third,
the test fixture's Kitchen.md. All three now declare one, and the output
path is unchanged in every case.
Authoring.md states the requirement where a contributor meets it, with
the reason: the file tree does not mirror the URL tree, so a derived
permalink would be wrong and wrong silently. Pipeline-Stages documents
validatePermalinks beside the nav-parent check and corrects the Page
table, which described the fallback as if it were reachable.

WIP.md's status table gains the thirteenth package, and its note now
records what was actually wrong -- the _* glob and the BOM -- rather than
saying the package cannot publish.
indexed_from, exclude_from_docs and exclude_kinds are provenance for the
package authoring pass, not build input. Together they say what a
completeness check should expect: re-index a package from a newer build,
and anything new not named in the exclude lists is a documentation gap.
Without them a re-index cannot tell "not yet written" from "deliberately
omitted".

Nothing recorded this, and the keys read as stray metadata someone would
eventually strip. WIP.md now carries the meaning, the six packages that
use them, the 2026-06-04 commits that set them, and the instruction to
bump indexed_from on re-index -- a stale value asserts a check was run
against a build that no longer matches.

Also notes that further package documentation work using these keys is in
progress and not yet merged, so a package may already be written before
anyone starts it again.
…l path

An install path contains a username. It now takes the newest
twinBASIC_IDE_BETA_<n> on %USERPROFILE%/Desktop, with --ide and TB_IDE as the
overrides for an install kept elsewhere.
… for it

parseCommandLine is in ide/main2.js, not main.js, and the trailing space was
recorded as measured through GetCmdLine when it had not been. Both now come
from an A/B on twinBASIC.exe's own Win32_Process.CommandLine: two cmd batch
files differing by one byte, one opening the project and one drawing the
dialog.
… file

The parse runs before root.loadProject, so the path need not exist. A
nonexistent path with a trailing space gives "Bad command line syntax" and
without it gives ERROR_FILE_NOT_FOUND -- two different dialogs from one byte,
which says the space changes the parse rather than the file lookup.
…ike found

Bypassing the WebView is not possible: the compiler's port and pass key are
minted by host objects inside the page, and --compiler= is an opaque handle
rather than a port. Reusing a live IDE for a second project wedges its
renderer, so a fresh IDE per project is the design and concurrency is the way
to scale -- three projects take 26s sequentially and 10s in parallel.

Also corrects a figure this session invented: a run is 8-11s and flat in
project size, not the "roughly 40 seconds" previously committed unmeasured.
…k holding a lone ``` swallowed every admonition after it
…e one page it guarded, and publish the stall watchdog
….) arguments and report the rest with a reason
…nto, and discoverability flat across three re-runs
…give serve-mode and the offline copy a door, derive the enumeration total
…vents a page, reconcile the event mechanism across five pages
…s a Class, confirmed against exported sample sources
…c authors, and the parameter description already covers the behaviour
… output; document the measured 14-column print zone
…tate the error-9 hazard on LBound/UBound; fix a stray control char in tbrun's Debug.Cls check
@KubaO
KubaO merged commit 7b74688 into twinbasic:main Sep 21, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant