Static site (just-the-docs theme look-and-feel) deploying to docs.twinbasic.com. Source under docs/, build pipeline (tbdocs) under builder/.
This file is the contract: what a session needs before touching anything. The engineering casebook --- why a tool is built the way it is, what shipped broken, and the measurements behind each decision --- lives in siblings, and none of them is loaded automatically. Open the one covering what you are about to change.
| About to | Read first |
|---|---|
write or edit any page under docs/ |
WIP.Authoring.md --- page template, frontmatter, cross-section linking tables, per-symbol workflow |
| document a specific package | that package's own file, listed under Package API notes |
| run or change the twinBASIC compiler harness | WIP.Harness.md --- export, the attribute census, tbbuild, tbrun, the add-in test runner |
change builder/, scripts/, or any gate |
WIP.Build.md --- the pipeline and every gate's failure history |
| touch fonts, diagrams, or the PDF's type | WIP.Typography.md, then WIP.Fonts.md for the generator |
| change the accessibility scan | WIP.A11y.md --- the axe scan, the sample, the fingerprint gate |
| work on the sample-compiling harness | WIP.ExamplesBuild.md |
| run or change Wisdom, the Discord harvester | WIP.Wisdom.md --- the three-phase pipeline and the Phase 3 extract flow |
| work on the IDE help add-in, or on testing IDE add-ins by machine | WIP.HelpAddin.md --- the plan, the IDE facts it rests on, and the probes still open |
| run a use-case evaluation round | eval/README.md --- start every evaluator with eval/run_case.mjs, never as a subagent: a subagent inherits this session's CLAUDE.md, and with it this file, which is the answer key the corpus withholds |
The rule that decides where a new note belongs: this file says what to do, a sibling says why it is done that way. A measurement, a war story, or a "this shipped broken and nobody noticed" goes in a sibling. A rule you must follow goes here.
Reference documentation is complete for all thirteen packages, adapted from primary sources (Microsoft VBA-Docs CC-BY-4.0 for the runtime library, .twin source for the twinBASIC-specific packages). The CEF and WebView2 packages also carry a tutorial set.
| Package | Reference | Tutorials |
|---|---|---|
| VBA package | done | — |
| VBRUN package | done | — |
| VB package | done | — |
| WebView2Package | done | done |
| Assert package | done | — |
| CustomControls / CustomControlsPackage | done | — |
| cefPackage (CEF) | done | done |
| WinEventLogLib | done | — |
| WinNamedPipesLib | done | — |
| WinServicesLib | done | — |
| tbIDE | done | — |
| WinNativeCommonCtls | done | — |
| AppGlobalClassObject | done | — |
The rest of this file is the maintenance guide for updating existing pages or adding new ones — high-level package surface notes, page templates, cross-section linking conventions, and the integrity check.
Packages are nested one level deeper than a bare
docs/Reference/<Package>/.58a5e1csplit them intodocs/Reference/Default/--- the three packages every project references (VB,VBA,VBRUN) --- anddocs/Reference/Built-In/--- the ten that ship with the IDE but are referenced on demand (AppGlobalClassObject,CEF,CustomControls,TwinBasicAssertions,WebView2,WinEventLogLib,WinNamedPipesLib,WinNativeCommonCtls,WinServicesLib,tbIDE).Core/did not move.The reorg moved files, not URLs. Every
permalink:is unchanged, so the Cross-section linking tables below are unaffected --- they resolve against the rendered URL, never the file path. Only the on-disk paths in this section and in Per-symbol workflow carry the prefix.Note also that the
Assertpackage's folder isBuilt-In/TwinBasicAssertions/, though its title, nav parent and permalinks all still sayAssert.
-
docs/Reference/Core/— language statements/keywords (Dim,For-Next,Sub, ...). -
docs/Reference/Default/<Package>/<Mod>/— runtime library (VBA, VBRUN), grouped by modules. -
docs/Reference/Default/<Package>/<Mod>/index.md— module landing page listing its members. -
docs/Reference/Default/VB/<Class>.md— single-file class page. No current VB class uses this shape; all VB classes are folder-style. -
docs/Reference/Default/VB/<Class>/index.md— folder-style class page (e.g.CheckBox/index.md,CheckMark/index.md). -
docs/Reference/Built-In/WebView2/— WebView2 package: the WebView2 control class plus its small wrapper classes (request / response / headers / environment options) and thewv2…enumerations. -
docs/Reference/Built-In/CustomControls/— CustomControls package: the eight Waynes… custom controls, their sharedStyles/helper classes (Fill,Borders,Corners,TextRendering, …), theFramework/DESIGNER surface (interfaces, CoClasses, theCanvas/SerializeInfoUDTs), and theEnumerations/(CornerShape,FillPattern,DockMode, …). -
docs/Reference/Built-In/CEF/— CEF (Chromium Embedded Framework) package: the CefBrowser control, itsEnvironmentOptionssub-page, and the two user-facing enumerations (CefLogSeverity,cefPrintOrientation). This is a much smaller surface than WebView2 — the package is currently BETA and many WebView2-equivalent features are not yet exposed. -
docs/Reference/Built-In/WinEventLogLib/— Windows Event Log package: the genericEventLog(Of T1, T2)class and theEventLogHelperPublicmodule with its singleRegisterEventLogInternalhelper. Three pages total —index.md,EventLog.md,EventLogHelperPublic.md. -
docs/Reference/Built-In/WinNamedPipesLib/— Windows Named Pipes package: the IOCP-based async pipe framework —NamedPipeServer+NamedPipeServerConnectionon the server side,NamedPipeClientManager+NamedPipeClientConnectionon the client side. Five pages total (index.md+ one per class). -
docs/Reference/Built-In/WinServicesLib/— Windows Services package: a thin OS-services wrapper.Services(predeclared singleton) coordinates one or moreServiceManagerconfigurations;ServiceCreator(Of T)is the generic factory the dispatcher uses to instantiate each user-definedITbServiceclass;ServiceStateis a read-only state snapshot for an installed service. Four public enums (ServiceTypeConstants,ServiceStartConstants,ServiceControlCodeConstants,ServiceStatusConstants) live underEnumerations/. -
docs/Reference/Built-In/WinNativeCommonCtls/— Windows Native Common Controls compatibility package: a VB6-compatible Microsoft Common Controls 6.0 (MSCOMCTL.OCX) replacement, written on top of the Win32 ComCtl32 controls. Eight controls (DTPicker, ImageList, ListView, MonthView, ProgressBar, Slider, TreeView, UpDown), plus eight sub-object classes (ListImages / ListImage, ListItems / ListItem, ColumnHeaders / ColumnHeader, Nodes / Node) reached through container properties on the three collection-bearing controls, plus ~16 user-facing enumerations. Each control is a<Name>BaseCtl([COMCreatable(False)]) plus a thin<Name>leaf tagged[WindowsControl(...)]— the same split VB-package and CEF use. -
docs/Reference/Built-In/AppGlobalClassObject/— theAppglobal object available in every twinBASIC project: the_Appinterface plus its property pages under_App/(Build,Comments,CompanyName,EXEName, …). 38 files.This package published nothing at all until two causes were fixed, and both leave a live rule. A blanket
**/_*/**rule in_config.yml'sexclude:, there to drop_Images, swallowed all 37 pages under_App/--- the twinBASIC interface really is named_App, after the COM hidden-interface convention. It is scoped to**/_Images/**(plus**/*.af) now, so never widen an exclude to a bare underscore prefix. Separatelyindex.mdcarried a UTF-8 BOM, which sits in front of the---and stopsgray-matterrecognising any frontmatter at all, sodiscoverfiled it as a static file and served the raw markdown verbatim;discover.mjsstrips a leading BOM before parsing now, and editors on Windows add one without being asked. -
docs/Reference/Built-In/tbIDE/— IDE Extensibility package (this is the addin SDK). The package is type-only — it ships public interfaces + CoClasses that an addin DLL binds to; every implementation behind them lives in the twinBASIC IDE itself. The user-facing surface is one entry-point factory (tbCreateCompilerAddin) plus 23 CoClasses grouped by role: the addin contract (AddIn), the root API (Host), the loadedProject, the editors collection (Editor/CodeEditor/Editors), the virtual file system (FileSystem/FileSystemItem/Folder/File), the in-IDE UI surface (Toolbar/Toolbars/Button/ToolWindow/ToolWindows), the HTML DOM inside a tool window (HtmlElement/HtmlElements/HtmlElementProperty/HtmlElementProperties/HtmlEventProperty/HtmlEventProperties), theDebugConsole,KeyboardShortcuts,Themes, and the single concrete user-instantiable helper classAddinTimer. Flat layout — one page per CoClass / Class plus the index landing. -
docs/Reference/Statements.md— alphabetical index of language statements. -
docs/Reference/Procedures and Functions.md— alphabetical index of procedures/functions. -
docs/LLVM/— the LLVM section: compiling with the LLVM back end. A top-level section between Features and Reference Section in the nav, atnav_order: 6(Features moved to 5 to make room).index.mdis the landing page andGetting-Started.mdthe only page so far, with its screenshots underImages/. Everything it describes arrived in BETA 984; the local BETA 983 compiler restricts+llvmto standard-module procedures and to Professional/Ultimate, and has no LLVM project settings at all. Both of the page's samples are markedcheck_buildand compile clean on 983 --- but the harness asks only the front end, which accepts[CompilerOptions]with every CPU flag in it; nothing runs LLVM code generation, so a clean run says nothing about the 984 behaviour the page describes.A new top-level section needs four things besides its folder, and nothing checks the first and the third: a
nav_orderbetween its neighbours; an entry indocs/_book.ymlfor every page, in a part or inleft_out:with a reason, which the build warns about under itspdf:summary when one is missing; a line in Where content lives indocs/Documentation/Authoring.md; and the page-count rise thatbuild.batwrites tobuilder/page-baseline.json, committed with the pages. The Challenges and Videos sections are inleft_out:, and so are the IDE pages that are still screenshots and labels; the IDE part names its pages one by one, so a new IDE page warns until it goes into the part or intoleft_out:. A link from the book to a left-out page opens the website, and the pass overbook.htmllists it asOUT OF BOOK. A section index lists its topics by hand and setshas_toc: false, or the template appends a second, automatic list of its children. -
Footer rendering — builder/template.mjs's
renderFooterCustom()renders the copyright line and, whenvba_attribution: trueis set in a page's frontmatter, an additional CC-BY-4.0 attribution line beneath it. -
Contributor authoring guide — docs/Documentation/Authoring.md is the public "start here" page that distils this file's authoring conventions (page template, heading levels, formatting, plain-English prose, attribution policy, cross-section linking) for a new contributor. This file remains the exhaustive maintainer source of truth; keep the two in sync when a convention changes.
Per-package content-shape references live in sibling files. Open the relevant one when updating an existing page or adding a new one; the actual rendered docs under docs/Reference/<Package>/ remain the source of truth.
- WebView2 Package — the
WebView2control + wrapper classes +wv2…enums. - Assert Package — three sibling modules (
Exact/Strict/Permissive) with identical 15-member APIs but different comparison semantics. - CustomControls Package — eight
Waynes…custom controls + sharedStyles/helpers +Framework/DESIGNER surface +Enumerations/. - CEF Package — the
CefBrowsercontrol +EnvironmentOptionssub-page + two enums; smaller surface than WebView2 (currently BETA). - WinEventLogLib Package — the generic
EventLog(Of T1, T2)class +EventLogHelperPublicmodule + the message-table backing pattern. - WinNamedPipesLib Package — IOCP-based async pipe framework: server + client manager + per-side connection classes + the
Cookie/ transient-Data()/ManualMessageLoopidioms. - WinServicesLib Package — thin OS-services wrapper:
Servicessingleton +ServiceManager+ServiceCreator(Of T)+ServiceState+ITbService+ four enums. - tbIDE Package — the addin SDK (type-only compiler package): 23 CoClasses +
AddinTimer+ the HTML/DOM[COMExtensible]surface + samples 10–15 idiom map. - WinNativeCommonCtls Package — VB6-compatible
MSCOMCTL.OCXreplacement: 8 controls + 8 sub-objects + per-control nested enums + 10 module-level enums.
The three "winlibs" packages — WinServicesLib, WinEventLogLib, and WinNamedPipesLib — share an essential set of integration idioms: composition-delegation on EventLog(Of …), the ManualMessageLoopEnter / Leave pattern coupling NamedPipeServer to a service's ChangeState handler, and PropertyBag as the canonical pipe payload. When working on any of the three, check the other two for cross-references.
The package sources are not in this repository --- they are inside the
.twinproj files an IDE install ships. Exported sources are the strongest
available evidence for anything the documentation asserts about legal syntax,
and for a question no shipped source demonstrates, something has to put the
construct in front of the compiler. Four tools do that, and each finds the IDE
itself: the newest twinBASIC_IDE_BETA_<n> on the Desktop, with --ide or
TB_IDE overriding. No install path is hardcoded anywhere in the tooling,
because an install path contains a username.
"$TB/bin/twinBASIC_win32.exe" export "<some>.twinproj" "C:\out\dir\" --overwrite
node builder/census_attributes.mjs --out census.md # every attribute, by enclosing construct
node scripts/tbbuild.mjs C:/probe/Thing.twinproj # does it compile
node scripts/tbrun.mjs <exported-source-dir> # what does it print- Give the executable backslashed paths, and
exporta full path to the project.exportprefixes\\?\to its project path, so a relative one, or one with forward slashes, reportsinput twinproj file does not exist; and a folder named with forward slashes cannot be created or even found, even when it exists. With backslashesexportcreates every missing level of its output folder. Redirect stdin (</dev/null) when looping, or the executable consumes the loop's input and the second iteration never runs. tbbuildexit codes: 0 clean, 1 the project has errors, 2 the harness failed, 3 the compile never settled, 4 the project crashes the compiler.--jsonreturns one object,--keepleaves the IDE running.- It runs the IDE on a private Windows desktop, so it cannot seize focus mid-sentence. Set
TBBUILD_SHOW=1while working interactively and leave it unset for unattended runs --- a wedged IDE nobody can see is the failure that costs an afternoon. - One project per IDE, 8--11 seconds each and flat in project size. Reusing a live IDE for a second project wedges it, so the cold start is the unit of work, not overhead to optimise away. Concurrency is how to go faster: distinct
--portvalues give distinct DevTools ports, user-data folders and desktops. - Keep a probe that might crash the compiler in a project of its own. twinBASIC runs the compiler in the same process as user code, so one bad probe can take the run down and cost the other thirty their answer.
tbruntakes an exported tree, not a.twinproj, because it has to pinproject.buildPathin its own staged copy --- a project still on the default template opens a native Save dialog that is invisible on the private desktop, and the build simply never happens while every health check says the IDE is fine. The probe is a module with a[RunAfterBuild]Sub, and must start withDebug.Cls.- A census is evidence, not applicability. The corpus not using an attribute somewhere does not mean the compiler refuses it there, and the reverse also holds. Only a probe settles that.
- End an IDE by its pid, never by image name.
taskkill /IM twinBASIC.exeends every other run's IDE, another session's included, and the user's own.tbbuild --keepprints the pid for this reason.
Why each of those is true, what the WebView/CDP route costs, why the compiler's own websockets cannot be driven instead, and the seven ways a sweep of this corpus returns a wrong answer: WIP.Harness.md.
Testing an IDE add-in is addin-test.bat, run by a person as examples.bat is. Each
lane in test/addin/lanes.mjs builds the add-ins it tests into a private copy of the
install and operates an IDE; the plan it serves is WIP.HelpAddin.md, and
how it works is WIP.Harness.md, The add-in test
runner.
addin-test.bat # every lane
addin-test.bat --only sample15 # one lane; --port N moves the lanes' ports- Never build or copy a test add-in into the real install's
addins\, or into%APPDATA%\twinBASIC\addins\. Either way it loads into the user's own IDE (P6 measured the second). A test add-in goes only into a lane's own folders: its copy of the install, whereaddAddinrefuses anywhere else, or theAPPDATAthe lane gives every IDE it starts. That privateAPPDATAis also what keeps the user's own add-ins out of the test IDEs; never start a lane IDE without it. - A test never opens a real browser. Every IDE the harness starts has
TB_ADDIN_TEST=1, and an add-in under test printsopen <url>to the DEBUG CONSOLE instead. Never start a test IDE with the variable removed unless its add-in opens nothing either way. - Name in
lanes.mjsevery application an add-in under test passes toSaveSetting, or its settings stay changed after the run:SaveSettingwrites the key the user's own copy of the add-in reads.
WIP.Authoring.md is required reading before writing or
editing any page under docs/. It carries the page template and its
frontmatter keys, the attribution policy, the per-symbol workflow, the
twinBASIC-vs-VBA deviations to flag, and the cross-section linking tables.
Those tables are not optional guidance. Relative links resolve against the
rendered URL --- the page's permalink: --- rather than the file path, and
the URL prefixes are not uniform across packages: VBA pages sit one segment
shallower than VBRUN pages, and folder-style classes one deeper than
single-file ones. A cross-section link written by analogy with a neighbouring
page is usually wrong, and the build's link check is what will tell you.
Three things are short enough to state here, because they decide whether a page is in the right place at all:
- Placement. A pure language keyword, parsed by the compiler with no runtime call, goes in
docs/Reference/Core/. A runtime function or property goes under its package and module, withredirect_from: /tB/Core/<name>so legacy links still work. Packages are nested one level deeper than a baredocs/Reference/<Package>/--- see Where things live. - Attribution is per page, decided by content provenance, not by package membership. Set
vba_attribution: trueonly on a page actually derived from a specific VBA-Docs source page. A symbol merely existing in VBA is not sufficient. - Link to the canonical location, meaning the page's own
permalink:, never to one of itsredirect_fromaliases.
The public, contributor-facing distillation of the same conventions is docs/Documentation/Authoring.md. Keep the two in step when a convention changes; this file and its authoring sibling remain the exhaustive maintainer source of truth.
The audience is international: standard-English readers worldwide, often non-native, who may not parse idiomatic software-developer jargon. Use plain English in reference and tutorial prose.
The guiding principle: replace metaphors imported from outside programming; keep vocabulary with a specific technical meaning inside Win32 / COM / event-driven programming. If a phrase is the kind of thing a reader would have to look up in a tech blog, it doesn't belong in reference prose.
The vocabulary tables further down cover word choice. The rules in this subsection cover sentence shape and voice — the structural side of writing for an international audience.
-
Page opening. One-sentence verb-phrase summary directly under the H1, in present tense, no preamble. Good: "Activates an application window." / "Writes an Error-type entry to the log." Avoid: "The Const statement is used to declare constants in place of literal values." For class pages, a noun-phrase descriptor is acceptable — "A CheckBox is a Win32 native control that displays..."
-
Voice and tense. Active voice by default. Passive only for subjectless operations where there is no obvious agent — "the entry is written", "the constant is private by default". Present tense for behavior —
returns, notwill return. Don't give the class human traits: it doesn't "decide", "want", or "know" — it returns, raises, contains. -
Sentence shape. One idea per sentence. Prefer two short sentences over one compound sentence with nested clauses. Em-dash (
—) for parenthetical asides; reserve parentheses for code-ish notation like(default). -
Person and pronouns. Reference body prose uses third-person impersonal — "the constant", "the source", "the entry". Rewrite "you" to the impersonal form even in VBA-derived pages. "You" is acceptable inside
Examplelead-ins and in tutorial prose. Avoid first-person ("we", "I"). -
Parameter descriptions. Italic
*required*/*optional*flag, then a short prose description. Lead with the type when it matters — "A String naming the source...", "A T1 value naming the event...". Don't restate the parameter's name inside its own definition. Property setters omit the flag — the[ = *value* ]brackets on the syntax line carry that information. -
Callout severity. Three severity levels, used distinctly:
> [!NOTE]— twinBASIC-vs-VBA deviations, behavior clarifications, useful caveats. Not for marketing/why-bother prose — that should be a plain paragraph.> [!IMPORTANT]— requirements that affect correctness: admin rights, threading constraints, ordering.> [!WARNING]— operations that can corrupt state or lose data.
One callout per concern; don't stack a NOTE and an IMPORTANT for the same point.
-
See Also. Last section on the page, after
Example. Format:- [Symbol](Symbol) <noun>where<noun>is the kind: statement, function, property, method, class, module, package. Pages with annotations use- [Symbol](Symbol) -- short description— the--source renders as a typographic dash via markdown-it's typographer (en-dash for--, em-dash for---). Don't write literal—in source; keep--for consistency across the docs. Order by conceptual proximity, not strict alphabetical. -
Name the fault directly; never build up to it. Setting up a contrast and then withholding the point is coy, and it makes the reader parse the sentence twice to extract one fact. Avoid: "double-clicking it is the obvious shortcut, and it is the one that misleads"; "the specificity trap --- which is the one thing that reliably catches people out". Use: "double-clicking it is obvious, and wrong"; "the specificity trap: a rule that loses it applies in light mode and silently does not in dark". Say what the thing is and what it does, in that order, in one clause. The same applies to "and that is the one that…", "which is precisely the…" and "therein lies the…".
| Term | Use instead |
|---|---|
at rest (idle state) |
idle, in its default state |
bake in / baked into |
embedded, stored, included |
bite / bites (figurative) |
affects, matters, goes wrong |
broker (as verb) |
manages, handles |
carry / carries (figurative) |
has, contains, includes |
catches up |
resumes, processes the queue |
comes up (a connection) |
is established, becomes ready |
drive / driven (figurative) |
controlled by, determined by, powered by |
footgun / footguns |
easy mistake to make, hazard, pitfall |
for free (figurative) |
as a side effect, without extra effort |
hand off / hand over / hand back |
returns, passes, delivers |
hand-rolled |
manually constructed, custom-built |
handful / handy |
a few; useful |
heavy hitter / heavy-hitter (figurative) |
main items, biggest items, most significant |
in flight / in-flight |
pending, in progress |
in one shot |
in a single call |
in order to |
to |
keep honest / keeps it honest |
verifies, guards against silent failure |
kick off / kicks off |
start, begin |
land (figurative — "where the call lands") |
appears, arrives, ends up at |
leverage / leveraging |
use, take advantage of |
load-bearing (figurative) |
essential, critical, central |
mid-call |
during the call |
on the wire |
transmitted, over the network |
orchestration / orchestrate |
coordination, manual handling |
picks up (figurative) |
receives, reads, captures, inherits |
pinned to (UI layout) |
attached to, fixed to |
reach for / reaching into |
use, access |
sensible defaults |
reasonable defaults, or list them inline |
spin up / spins up |
start, create |
stash (as verb) |
store, save |
sticks (figurative — "the zoom sticks") |
is preserved, is retained |
surface (as verb) |
expose, appear as, make available, raise |
surface area (figurative API surface) |
set of members, interface |
swallow (a keystroke) |
consume, discard |
taps into |
hooks into, intercepts |
tear down (figurative) |
destroy, unload |
twirling (UI animation) |
spinning, or describe concretely |
under the hood |
internally |
utilize |
use |
walk (as verb — "walk the chain", "walks the children") |
traverse, go through, iterate over |
walks through (tutorials) |
demonstrates, explains, describes step by step |
wire up / wired up / wired in |
connect, attach, link |
Vague compliments that add no information. Be concrete instead — if a feature is fast, say what it is faster than; if an API is small, say how many members it has.
powerfulrobustclean(as a vague compliment — "clean architecture"; literal uses like "clean shutdown" stay)rich(vague — "rich information"; literal "Rich Text Format" stays)easily(filler — "easily share" → "share")
Don't over-correct these — they are precise technical vocabulary or otherwise pull their weight:
- Programming / Win32 / COM:
no-op,round-trip/round-tripping,fire-and-forget,marshal/marshalled(between threads),pump(messages) /message pump,spawn(a thread or process),mixin,first-class(type),boilerplate,falls through,short-circuit,idiom/idiomatic,canonical. - Standard prose:
ends up,modern,lightweight,talk to(interop),out of the box,on the fly,work around/workaround. - Vivid but tolerable:
ship/ships with,bridge(figurative),cascade(figurative),fold in(compile-time emit). - Audience-appropriate VB6 vocabulary:
drop it onto a form. - Marketing register (Videos section only — promotional copy):
sneak peek,game-changing,drop-in,seamless.
Some kept terms are referenced by in-doc anchors. The most prominent is idiom / idiomatic — the published package pages reference anchors like #service-host-idiom (defined in docs/Reference/Built-In/WinNamedPipesLib/index.md, linked from NamedPipeServer.md and WinServicesLib/index.md) and #composition-delegation-idiom (defined in docs/Reference/Built-In/WinEventLogLib/, linked from WinServicesLib/index.md). Don't rename these casually; and note that redirect_from cannot rescue one --- builder/redirects.mjs emits whole-page stubs and has no fragment remapping, so a renamed anchor simply breaks every link into it.
markdown-it's typographer (enabled in builder/render.mjs) converts the ASCII source forms to typographic characters at build time:
| Source | Rendered | Use for |
|---|---|---|
-- |
en-dash – |
bullet-list separator (rule 7), ranges |
--- |
em-dash — |
parenthetical asides (rule 3), breaks in thought |
Alt text converts too. markdown-it's own replacements rule does not descend into an
image token's children, but kramdownDashesPlugin (builder/render.mjs,
registered after it) walks with walkTokens, which recurses, so it reaches image alt and
converts the dash like any other text. Measured: zero literal -- survives in any alt=
anywhere in the built site, in all three trees.
Verify through
builder/, never a bare markdown-it. A plainmarkdown-itwithtypographer: trueleaves--in alt text untouched, so testing against the npm dependency reproduces a wrong claim perfectly and tells you nothing about this site. Render through the pipeline, or read the built HTML.
The source uses the ASCII forms; the rendered HTML uses the typographic characters. Literal – or — in docs/ markdown source is forbidden — see the Don'ts at the end of this file. scripts/convert_em_dash_separators.mjs is the canonical normaliser if any literals slip back in.
WIP.md itself (and other files outside docs/) is not rendered through tbdocs and is exempt — literal em-dashes here render directly in the GitHub viewer, which is fine.
Three self-hosted faces, one system, everywhere the docs render. All SIL OFL 1.1,
all committed as subset .woff2 under docs/assets/fonts/
alongside their licence files.
| Face | Where | Variable axes | Subset size |
|---|---|---|---|
| Inter | all web text; PDF headings, running heads and captions; DOT diagram labels | wght 100-900 |
152 KB + 166 KB italic |
| Cascadia Mono | all code, inline and block, web and PDF | wght 200-700 |
67 KB + 77 KB italic |
| Source Serif 4 | PDF body text only | wght 200-900 |
138 KB + 109 KB italic |
Inter is the brand face: twinbasic.com has always named it first in its own
stack, it just never shipped a @font-face to deliver it. Cascadia Mono is the
ligature-free cut of Microsoft's terminal font --- in a language reference
the literal characters are the subject matter, so a face that draws -> as one
mark is working against the text, and any replacement must be ligature-free too.
Source Serif is the book's body face and nothing else's; no web stylesheet
references it, so no reader ever downloads it.
Reader cost is 219 KB cold (both roman faces, preloaded), 166 KB more the first time a page sets italic text and 77 KB beyond that only if the italic is code --- against the 3.4 MB search index every page already pulls.
How the six .woff2 files are produced --- the generator, the Unicode coverage, why
opsz is pinned and wght is not, the two content changes that fell out of the coverage
audit, and the state of the JavaScript port --- lives in WIP.Fonts.md.
Two things from it that touch the rest of this file: regenerating Inter means
regenerating builder/inter-metrics.json too (see Teaching Graphviz what Inter
measures), and the generator is Python for a
reason that is documented there and should be read before anyone tries to port it.
Every diagram is Graphviz DOT --- sources under docs/assets/images/dot/ for
shared diagrams, Images/ beside a page for one that belongs to it. The .svg
is a build artifact.
- Edit the
.dot, never the.svg, and never set a diagram'sfont-familyanywhere else. Graphviz sizes every box to the text it measured, and its WASM build has no font machinery at all --- it falls back to Times for any face it does not know. A face the layout never saw leaves labels painted outside their boxes, which is how 27 labels across three diagrams shipped that way, unreported, for months. node scripts/check_dot_fit.mjsis the gate on exactly that, and runs insidecheck.bat. Run it after touching any diagram.- Scale a diagram whole, or not at all. Stretching an SVG up inflates its labels; shrinking the text alone unmoors the labels from their boxes. Both stylesheets learned this, from opposite directions.
- A
@font-faceadded to docs/_sass/custom/_fonts.scss needs its stack added to docs/_sass/modules-dark.scss as well, and must stay inside theemit-font-facesmixin. The dark compilation re-emits its whole payload under two selectors at raised specificity: a face declared there would be invalid, and a stack left out there applies in light mode and silently does not in dark. - Run the full a11y sweep after any change that moves type metrics. An inline element's measured height is its font's content area, so
target-sizeresults move with the face. The thirteen-page sample stayed clean through the entire font migration and would have shipped a regression.
Why Graphviz needs Inter's widths patched into its Times table and what breaks
that patch, why the web rule is width: auto and the PDF's is zoom: 0.875,
how an exported diagram carries its own font, why cluster and edge labels take
currentColor while node labels must not, and what the book's fallback chains
must never do: WIP.Typography.md.
Anything that participates in rendering the online site, the offline site, or the PDF book is handled by tbdocs, the in-tree Node.js static site generator. Module-level documentation lives next to the code under builder/; the user-facing summary is on the tbdocs Internals page.
Everything under scripts/, builder/, book/, eval/ and wisdom/ is
Node.js, and a new tool joins them there. Three files are not, each for a stated
reason rather than by oversight: scripts/impexp.py is a published download
offered to readers rather than tooling, scripts/build_fonts.py stays Python
because the JavaScript HarfBuzz build produces wrong CFF2 metrics
(WIP.Fonts.md), and scripts/lib/tb-launch.ps1 is Win32 calls
Node cannot make without a native FFI addon --- a private desktop, and the job
object the IDE runs in --- and is never run as a file, so the execution policy
never comes into it. The full accounting, and what the two ports gained, is in
WIP.Build.md.
Nothing under docs/Documentation/ should require Claude, an agent or a skill to follow.
The audience is a contributor with an editor and a terminal; agent-assisted authoring is a
local convenience, not part of the contract the published documentation makes. That is why
the document-symbol skill lives under the gitignored .claude/ and is described only here.
Wisdom is the one deliberate exception, and its public page stays public --- Phase 3 is a set of Claude agents, so a page describing the tool without them would describe nothing. That has been settled once; do not re-propose relocating it. The reasoning, and everything about running the tool, is in WIP.Wisdom.md.
The site builds via builder/, a custom Node.js static site generator (tbdocs). See builder/PLAN.md for the architecture overview, builder/README.md for the quickstart, and the tbdocs Internals site page for the high-level tour.
A task-graph scheduler / parallelisation pass is designed in builder/PLAN-scheduler.md and has been implemented (Phases 0--4).
Two reference samples had shipped that do not compile --- one passing an icon key in a slot
the same package's prose says is validated, one a Sub with no name. Every gate was green
over them, because a tb fence is something check_code_regions.mjs protects the
contents of and never evaluates.
examples.bat over scripts/check_examples.mjs is what asks
the compiler now. A sample opts in by carrying check_build in its fence info string; the
tool works out what to generate around it, packs many samples into one project, builds them
through tbbuild on concurrent lanes, and reports each diagnostic against the line in the
page it came from. 1,119 samples are marked and the run takes about 110 seconds.
It is never wired into build.bat, check.bat, test.bat or CI: it needs a twinBASIC
install, which npm install is not, and Windows with a private desktop and a
CDP-reachable WebView2, which CI has not. sweep_a11y.mjs has the same arrangement.
So a pull request that adds or changes a sample pastes the run's command and summary
line into its description --- the contributor-facing statement is
Checking that a sample compiles.
WIP.ExamplesBuild.md is the file for this --- the markup, the
slots, the template projects and their stage sets, the batching and bisect-on-crash rules,
what actually collides inside one project, and what the first full run found. Two results
from it belong here because they are about the harness rather than about the samples:
[RunAfterBuild] is one per project (TB5114), which is what check_run has to be
designed around; and tbbuild used to report a clean build on a project that crashed the
compiler, which is fixed and is the reason to distrust any "it stopped changing, so it
must be done" heuristic against this compiler.
A task a worker claims and never finishes wedges the whole graph in silence: its
successors' dependency counts never drop, the scheduler's promise never settles,
and the process sits there with its last log line on screen. Scheduler watches
for it --- no task completing for --stall-timeout seconds (default 120,
0 disables) prints what was outstanding, including the source pages behind a
wedged render:i chunk, and fails the build. Readers get this at When a build
stops instead of failing.
Why the report separates the wedged task from the merely blocked ones, and why
--serve replaces its whole worker pool rather than the one bad lane:
WIP.Build.md.
-
build.bat— runsnode builder\tbdocs.mjs --src docs --check-audit-index(which implies--check) and produces three trees in one pass: the online copy at_site/, afile://-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. Togglealso_build_offline/also_build_pdfin_config.yml(or pass--no-offline/--no-pdf) to skip a sibling output.--checkadds ~1.7 s and runs the link + integrity check over the HTML while it is still in worker memory;build.bat --no-checkgets a plain build. -
serve.bat— runstbdocs --serve: initial build, then a long-lived process with watcher, debounced rebuilds, and SSE-driven browser auto-reload. Writes todocs/_serve/(disjoint frombuild.bat's_site*/) and skips the offline + PDF passes — so a one-offbuild.batfor 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 intobuild.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 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), and the axe source-patch verification (scripts/check_axe_patch_equiv.mjs). ~8 s. See What belongs in test.bat rather than check.bat. -
book.bat— renders the PDF fromdocs\_site-pdf\book.htmlvianode book\render-book.mjsintodocs\_pdf\twinBASIC Book.pdf. Runbuild.batfirst to populate_site-pdf/;book.batrefuses a tree older than its sources rather than rendering the previous book (see The book refuses a stale source tree). -
examples.bat— compiles the documentation's own twinBASIC code samples, everytbfence markedcheck_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; ~110 s over the 1,119 samples marked today. Two modes need no compiler at all:--censusclassifies every fence and says how many classifiable ones are still unmarked, and--report <survey.json>groups a saved--propose --jsonsurvey by diagnostic, section and unresolved name.--proposeitself does compile. See Compiling the reference's own code samples and WIP.ExamplesBuild.md. -
addin-test.bat— tests IDE add-ins by operating an IDE: every lane intest/addin/lanes.mjsbuilds the add-ins it tests into a private copy of the install, opens a project and checks what the add-in does. Outside every gate and outside CI for the same reasons asexamples.bat; ~140 s for the ten lanes today: Samples 10 and 15, and the eight probe lanes behind Stage 2's answers in WIP.HelpAddin.md. Exit 0 every lane passed and the registry is as it was found, 1 a lane failed, 2 the harness failed or could not put the registry back. See Driving the twinBASIC compiler for its rules.
Three generators sit outside that loop and produce committed artifacts rather than build output — none runs during a build, and none is needed for one. python scripts/build_fonts.py rebuilds the subset webfaces under docs/assets/fonts/ and needs a network connection; node scripts/build_dot_metrics.mjs regenerates builder/inter-metrics.json from those webfaces and needs only a browser. See Typography. node scripts/build_package_api.mjs regenerates builder/package-api.json, the packages' declared API that the build's symbol index (tB/symbols.json, for the IDE help add-in) is annotated from; it needs a twinBASIC install, so run it when the reference is re-indexed against a newer build and commit it with the pages. See WIP.HelpAddin.md, Stage 3.
After a batch of changes, verify the site builds clean and all links resolve:
build.bat && check.batOn 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 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/, book/, eval/ or wisdom/, run test.bat as well --- another ~8 s. Six of its eight gates cannot be affected by a content edit at all. Two can. 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 has ROOT = <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:
build.bat && check.bat && test.batTools and Scripts owns the authoritative lists,
and scripts/check_gate_lists.mjs fails the run if README.md or any page
under docs/Documentation/ disagrees with them --- so do not state a gate
count in prose on those pages unless you mean to maintain it. The roster, by
wrapper:
| wrapper | gate | asks |
|---|---|---|
build.bat |
link + integrity check | broken intra-site links, missing pages, malformed redirect_from, duplicate ids, remote <img src>, sitemap / search-index gaps, canonical mismatches. Runs on the worker lanes that produced the HTML, so neither tree is written out only to be read back |
build.bat |
publish allowlist | refuses any file that may not ship. It aborts the build rather than flipping an exit code, because a tree with a private key in it is one upload-pages-artifact from being published |
build.bat |
page-count baseline | a rise rewrites builder/page-baseline.json and says so; a fall fails the build |
build.bat |
symbol-index URLs | every URL tB/symbols.json has published is still in it: a new one rewrites builder/symbol-baseline.json, a lost one --- most often a reworded member heading --- fails the build |
build.bat |
nav integrity | every nav-visible parent: resolves to exactly one page |
check.bat |
check_tree_fresh |
the tree is not older than the sources that produced it |
check.bat |
check_dot_fit |
every diagram label sits inside the box Graphviz drew for it |
check.bat |
pick_a11y_sample --check, check_a11y |
see WIP.A11y.md |
test.bat |
check_code_regions |
no source or HTML rewrite altered a code region |
test.bat |
check_regex_safety |
no regex in the tree can backtrack exponentially |
test.bat |
check_symbol_index |
the symbol index still places each kind of symbol, from fixtures |
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
something with no documentation in the tree. That is the whole rule; it is
about what a gate interrogates, not about what it happens to open.
Both CI workflows run every one of these as its own step, unconditionally and
without invoking the .bat files --- so skipping test.bat locally changes
what a content edit costs you, never what reaches staging.
The nav integrity check (builder/nav.mjs) runs during COMPUTE and aborts the build on two failure modes, both otherwise silent:
- Ambiguity — multiple pages share the title declared in
parent:andgrand_parent:is either absent or insufficient to disambiguate. The page would silently appear under every matching parent. - Orphan — no page has the title declared in
parent:. The page would silently disappear from the navigation sidebar.
Each gate's failure history --- what it caught, what shipped green past it, and
the rule that came out of it --- is WIP.Build.md. Two of those
rules bind every session and are repeated under Don'ts: never rewrite
markdown source or rendered HTML without a code guard, and whitespace inside
inline <code> is content.
Favor concise one-line git commit messages.
A bug in twinBASIC itself goes in BUGS-TO-REPORT.md, which is a
queue rather than a record: an entry is deleted once it has been filed upstream. Each one
carries the build it was seen on and a narrowed reproduction --- the compiler crash
recorded there is two lines, and neither line reproduces it alone. Documentation defects
do not go there; they are fixed in docs/, or recorded in the relevant WIP.*.md until
they are.
-
Don't commit
.claude/orCLAUDE.md— both gitignored. (WIP.mdis committed;CLAUDE.mdis just a local@WIP.mdimport shim.) -
Don't touch
_site/or_site-offline/(build outputs, gitignored). -
Don't walk
docs/for its markdown with a privatereaddir. CallmarkdownFilesfrom scripts/lib/markdown-files.mjs, which never enters the build's output trees. A walk that does enter them crashes whenever a runningserve.batrewrites_serve; see The code-region gate. Any other walk ofdocs/decides what is an output tree with the same module'sisOutputTree, ascheck_tree_fresh.mjsdoes, rather than a list of its own. -
Don't judge rendered styling by opening a built page as a
file://URL in the in-app browser pane. It does not apply the page's stylesheets, so everything renders unstyled and any conclusion about colour, spacing, layout or contrast drawn from it is worthless. Useserve.bat, which serves over HTTP at localhost and renders for real. The confusing part is thatfile://is fine through puppeteer --scripts/check_a11y.mjs,scripts/sweep_a11y.mjsand theperf/rigs all load_site-offline/overfile://and get correct computed styles, which is the entire reason the offline tree exists (see Site integrity check). So: puppeteer for measuring,serve.batfor looking. Never the preview pane on afile://path. -
Don't write literal en-dash
–or em-dash—indocs/markdown source. Use--(renders as en-dash) or---(renders as em-dash) — markdown-it's typographer does the conversion at build time.scripts/convert_em_dash_separators.mjsnormalises any strays. -
Never write or edit a file with a shell heredoc. No
cat > file <<'EOF', noprintfinto a file, nosed -ifor a content edit. Use the file-writing and file-editing tools. A heredoc mangles exactly the characters this repository is made of ---—,–,§,→,×in the prose, and every backslash in a regex --- breaks on the shell's own metacharacters, and fails late and partially, which is worse than not writing the file at all. The shell is for running things, not for authoring them.It fails silently, which is the part worth fearing. A scratch classifier written through
<<'EOF'had every"\\s+"delivered as"\s+", matched nothing, and reported 444 unclassifiable fences against a true 32 --- a number that reads as a finding about the corpus and was a finding about the quoting. -
Don't push or force-push without explicit user request.
-
Don't leave a remote image URL in a finished page. A pasted
https://github.com/user-attachments/assets/...link is fine to write --- builder/vendor-assets.mjs downloads it todocs/assets/attachments/gh-<uuid>.<ext>on the next local build and rewrites the render to point there; commit the downloaded file with the edit. Any other remote host has no such handling: download it yourself and commit it under the section'sImages/folder. Remote images cost a network round trip per page view, break thefile://offline mirror, and abort the PDF book render -- the forked paged.js inbook/lib/dropped async image loading, so an image still in flight when the page-breaking pass runs raises instead of degrading. The build enforces this unconditionally (see Site integrity check);--check-remote-assetsis the standalone checker's flag, not atbdocsone. The check is scoped to<img>;<iframe>is untouched, but the site no longer has any embeds. A video is authored as a marked link --[Title](https://www.youtube.com/watch?v=<id>){: .video }-- whichvideoLinkPlugin(builder/render.mjs) renders as a locally vendored poster frame linking out to the video page, styled by.video-linkindocs/_sass/custom/custom.scss. That makes the site free of third-party requests entirely; don't reintroduce an embed or a hotlinkedimg.youtube.comthumbnail. -
Don't hand-edit a diagram's
.svg, and don't change itsfont-familyanywhere but the.dot. The.svgis a build artifact; the next build overwrites it. More to the point, Graphviz sizes every box to the text it measured, so a face the layout never saw leaves labels hanging outside their boxes --- which is exactly how 27 labels shipped that way across three diagrams. Edit the.dot, rebuild, and letnode scripts/check_dot_fit.mjsconfirm it; see Diagrams. -
Don't add a
@font-facetodocs/_sass/custom/_fonts.scsswithout also adding the stack tomodules-dark.scss, and don't move the@font-faceblock out of theemit-font-facesmixin. The dark compilation re-emits its whole payload under two selectors at raised specificity: a face declared there would be invalid, and a stack left out there applies in light mode and silently does not in dark. -
Don't widen
SOURCE_EXTENSIONSin builder/publish-policy.mjs to make a build pass. The build refusing a file is the gate working. Remove the file fromdocs/, or add a pattern toexclude:in_config.yml; widen the allowlist only when the type genuinely belongs on the published site, and never by foldingBUILD_EXTENSIONSinto it. See The publish allowlist. -
Don't add a rewrite over markdown source or rendered HTML without a code guard. A pre-render source rewrite goes inside
applyPreRenderRewrites, betweenmaskCodeRegionsand itsrestore; a rendered-HTML rewrite usesreplaceOutsideCodeor the<code>/<pre>leading-alternation shape. Four rewrites shipped without one and corrupted real code samples, including control-flow indentation in a language reference and six code spans in the published PDF.node scripts/check_code_regions.mjsis the gate. See Never rewrite markdown source without knowing what is code. -
Don't invent semantics — read the relevant primary source before paraphrasing (VBA-Docs for VBA-derived pages; the package's
.twinsources for twinBASIC-specific ones). -
Don't add boilerplate sections (Remarks, See Also) if the source has nothing meaningful for them.
-
Never add
Co-Authored-By:(or any "Co-authored by" / "Generated with Claude" / similar) trailers to commit messages. Repository policy. Plain commit messages only.