0088 — TypeScript, never JavaScript: the only authored form is typed, and the JavaScript that must exist is generated¶
Status: proposed · Date: 2026-10-08 · Extends: ADR-0066 (one design system; it copied channel.js to every target), ADR-0062 and ADR-0067 (the TypeScript workspace and core) · Cites: sub-doctrines 9.b, 10.e, 12.b and 12.e · Related: ADR-0016, ADR-0051
Owes: the advertised ADR count in CLAUDE.md, AGENTS.md, GEMINI.md, README.md
and docs/index.md; a properdocs.yml nav entry; docs/llms.txt regenerated from that
nav; the canon's entry in src/vibey_tools/gh/docs/doctrines.md and its regenerated
corpus-index.json. All are in the change that carries this record.
Context¶
The project's application code is TypeScript and Python, and both are typed. JavaScript
was the gap. Measured on develop at ae15cc72b, before this change, git ls-files listed
20 JavaScript files:
design/web/channel.js, hand-written, copied by the design generator to eight places;math.js, hand-written in one place and committed in three;- the VS Code task panel's webview page,
clients/vscode/media/panel.js, held to a type checker only by// @ts-checkcomments; - two build scripts and two smoke-test files in
clients/vscode/, plain Node; babel.config.jsandmetro.config.jsfor the krypton app;- and JavaScript no file listing could see: the cookie-consent banner and the analytics start-up, written as Python string literals inside the release-surfaces workflow, in three managed copies (the repository's, the vibey-gh tenant's, and the template both render from).
None of it was checked by the compiler that checks everything beside it. The panel's own comments say as much: a JSDoc type is a request to be checked, and a build that does not run the check does not grant it.
Decision¶
- TypeScript is the only authored form (sub-doctrine 9.f). No exemption for scripts, configuration, tests or small helpers.
- A
.jsthat must exist is an artifact. A browser cannot execute a.tsfile, and Expo's Metro and Babelrequiretheir configuration as.js. Each such file is compiled by the repository's owntsc(the onenpm ciinstalls; ADR-0062) from a.tssource understrict,noUncheckedIndexedAccess,exactOptionalPropertyTypesand the rest of the flags@vibey/corealready uses. It is committed at the path the consumer expects, behind a banner that names its source. - The list is declared, and the rule is a test.
scripts/typescript_artifacts.tomllists every artifact and every place it is committed (ADR-0051).tests/meta/test_typescript_artifacts.pyfails when a tracked.jsis not on that list, when a listed file does not name its source, and when a committed file differs from what its source compiles to. CI installs Node and runs the check as Gate 0a; with no compiler it fails rather than skips, because a gate that quietly does not run reports a success it did not observe. - Tools that run from a developer's machine are compiled, not committed. The VS Code
build scripts and smoke tests compile with
tsc -p tsconfig.tools.jsoninto untracked, ignored.jsbeside their sources. - The design generator no longer owns scripts. It keeps tokens, stylesheets and icons; the compiled scripts are this mechanism's, so no file has two writers.
- A workflow carries configuration, never JavaScript. The release-surfaces workflow reads
the managed assets
analytics.jsandconsent.js(compiled fromanalytics.tsandconsent.ts) and inlines them as it always inlined its snippets, so the published pages are unchanged in shape. The two settings travel as attributes on the script tag:<script data-ga-id="G-…" data-consent="true">. A meta test fails on a workflow that writes an inline<script>body any other way.
Consequences¶
- Twenty hand-written JavaScript files, and the JavaScript in the workflow's strings, became
nine TypeScript sources, one declared list and twenty-one generated artifacts. The compiled
channel.js,math.js,panel.jsand the two app configs are the same programs; the types are new.explorer.ts(the ledger explorer) is born typed. - The panel's message protocol, the ledger site format and the release
surfacesobject are now interfaces, so a field renamed on one side is a compile error on the other. - Editing a source and forgetting to regenerate fails the build and names the command.
- Contributors need Node to change a script, as they already do to change a client.
- The two inlined snippets were checked against their old behaviour in Node with a stubbed browser:
the same
gtagcalls in the same order,Argumentsobjects as gtag.js reads, the consent default only when consent is on, and the same banner flow. They have not run on a live Pages deploy; the first release after this change is where that is seen.
Alternatives rejected¶
- Keep JavaScript for configuration, typed by
// @ts-check. The status quo for the panel. It checks only where an editor or a build happens to run the checker, and the exemption is exactly the shape a rule erodes through. - Run
.tsdirectly (Node's type stripping). Node 24 can, but the VS Code extension declaresnode >=20, Metro and Babel load.jsregardless, and a browser cannot. - Do not commit the compiled files; build them at deploy. Every tenant builds its
documentation from a clean checkout with
properdocs, a Python wheel and a Docker image force-includesdocs/javascripts/*.js; none of them has Node. A committed artifact with a drift test is the pattern the design tokens already use. - Add a bundler. Nothing here imports anything at run time;
tscalone is enough, and a second toolchain would be a second thing to keep (10.e).
Rule status¶
Conduct that binds future decisions, survives a rewrite and is not mechanism, so under 12.b
it is a sub-doctrine and then ratified: filed as 9.f under doctrine 9, beside the
declared seam (9.b). The record argues it; only
src/vibey_tools/gh/docs/doctrines.md states it. Until the operator's merge ratifies it, it
is a proposal.