Skip to content

Compiler Validation ​

How twee-ts is validated. This page is for maintainers and reviewers; nothing here changes how the package behaves.

Three layers of evidence back a release:

  1. The test suite (pnpm test): unit, property-based and differential tests of the sources, the specification conformance tests, and the documentation's own examples.
  2. The compiler contract matrix (pnpm run test:contracts): fixed cases of public behaviour, each with a stable ID, run against the build in dist/ the way a user's project imports it.
  3. Continuous integration: the tests on Linux, macOS and Windows with every supported Node.js, the packed tarball checked as consumers install it, the plugins against every supported Vite and Rollup, coverage floors and the duplication budget. Mutation testing of the core modules (pnpm run mutation, weekly in CI) is informational: its scores are compared with mutation-baseline.json, but never block.

A release is published when CI on main is green and the merge commit carries the release annotation (see CLAUDE.md, "How to release").

The compiler contract matrix ​

The matrix lives in validation/contracts/. cases.ts declares every case: a group, the invariant the group checks, the issue that owns it, and the input variants. A case ID is the group and the variant's position (RESOLVE-07), so an ID keeps its meaning for good: groups and variants are only ever appended, never reordered, renamed or removed (matrix.contract.ts freezes revision 1).

sh
pnpm run test:contracts                       # build, then run every case
pnpm exec vitest run --config vitest.contracts.config.ts -t RESOLVE   # one group, against the current build

Each case gets an empty folder of its own, with the story format cache and the home folder inside it, so no installed story format and no earlier download answers. The cases never reach the network: they serve story formats and indices from loopback servers, and any other request fails. They import @rohal12/twee-ts and @rohal12/twee-ts/vite and @rohal12/twee-ts/rollup, which the contracts' Vitest config points at the build; their types are checked against the sources by pnpm run typecheck.

GroupInvariant and variantsOwner
WRITE-01–WRITE-08Atomic output keeps the destination and the intent of a link: new and existing files, a valid link, absolute and relative dangling links, a dangling chain, a cycle, a target with an intermediate link followed by ..#219
TYPE-01–TYPE-05The published declarations reject writes to a compiled passage: tags, tag elements, metadata, source location, name#220
FORMAT-01–FORMAT-07Equivalent storyFormat() wrappers decode alike through the parser, a local format, and a download used offline: strict and relaxed objects, comments with braces, braces in values#221
RESOLVE-01–RESOLVE-09Local formats, format URLs and indices (online, then from the cache) select the same version of the major asked for; only an older one warns#224
HEAD-01–HEAD-10Modules and the Vite client become real elements of the template's head, past comment, script and attribute look-alikes of a head tag#223
VITE-01–VITE-05Production and development entry builds keep the user's configuration: inline config, define, aliases, a virtual-module plugin, a config file overridden inline#222
INPUT-01–INPUT-06Files and inline sources normalize alike; source order decides; cold and warm caches agree; a forced change at the same mtime and a changed parse option are seen; name clashes—
OUTPUT-01–OUTPUT-08HTML metadata and text round-trip; JSON start and debug overrides; private passages are left out and cannot start; Twee keeps the effective metadata; a missing IFID is reported in Twine 2 output; public copies reached through directory links stay out of overlapping Vite output (#184); Rollup writes of markless stories stay rebuildable (#530)—
CLI-01–CLI-02A failed build keeps the previous output and exits 1; output inside a source folder is never read back—
ABORT-01–ABORT-02A cancelled compile rejects with the caller's reason and keeps the previous output—
INTERACT-01–INTERACT-06One case per output mode: generated stories (fast-check, fixed seed) from files and inline sources, three line endings, trimmed or not, with tag aliases and a replaced StorySettings, build alike cold, incremental and warm—
DEPS-01–DEPS-27The Vite entry's bundle follows the files an import.meta.glob() selects, as a fresh build does: in dev (request catch-up) a file added to an eager glob, deleted from a lazy one, renamed under a keys-only one, the first file of a folder created; in dev with the watcher; under vite build --watch; a file added to a folder a recursive glob reaches through a link, with and without the watcher; a bare package whose node_modules folder is a link pointed at another version, in dev with and without the watcher and under vite build --watch (#320); a file added to the folder of a worker's glob, likewise, and for a worker started with new Worker(new URL(…)) (#424); both under vite build --watch for an entry bundled in a build of its own too; static glob base options spelled as templates, computed keys, repeated members or quoted template values; aliased glob patterns containing ?, a character class with ?, or a literal #, followed through additions, removals and renames in dev catch-up, with the watcher and under build watch with both entry build modes#341

Revision 1 (WRITE-01 to ABORT-02, 59 cases) was declared against v1.18.1, where 16 cases failed; all 59 passed on v1.18.2 and pass on main. Revision 2 added the INTERACT group, revision 3 the DEPS group, revision 4 its cases DEPS-09 to DEPS-17, revision 5 its static-option cases DEPS-18 to DEPS-21, revision 6 its aliased-pattern cases DEPS-22 to DEPS-27. Revision 7 adds WRITE-08, preserving filesystem order for an output target containing symlink/... Revision 8 adds OUTPUT-07, excluding public directory-link copies from overlapping Vite output. The output case also retargets the public link between builds of one plugin instance, keeping the earlier copies excluded, and uses junctions on Windows. Revision 9 adds OUTPUT-08, repeating Rollup writes of Twee 1/3 and Twine 1 archives inside source folders (#530). The atomic-write symbolic-link cases are skipped on Windows, as the unit tests of atomic writes are.

A failing case is a product defect. Never change an expectation to match the code. Fix the defect class (see the hardening rules in CLAUDE.md), add a regression test to test/ that failed first, and keep the case. A new variant of an existing invariant is appended to its group and reported on the owning issue; a new invariant is a new group.

Reviewing the compiler ​

A review sweep is bounded and recorded, so that successive sweeps cover a known scope and fix defect classes rather than single examples.

  1. Freeze the target. Record the commit, the Node.js, pnpm and Vite versions, and the scope. Results from different revisions are different reviews.
  2. Cover every area. A full sweep covers the whole supported surface (API, syntax, story, inputs, files, formats, output, CLI, plugins, distribution, documentation), and looks beyond the registered cases: the matrix and the test suite are a floor, not the scope.
  3. Run the evidence. Build, type-check, run the tests and the contract matrix. Probe with temporary fixtures, isolated caches, loopback servers and the public API. A probe that cannot run is blocked evidence, neither a pass nor a defect.
  4. Investigate classes. Follow a failure to its cause, and check the sibling code paths and the controls of the same domain. Classify it: a missed defect, a regression, an uncovered supported combination, or a contract that is unsupported or unclear.
  5. Keep the issue ledger. Add a new variant to the issue that owns its invariant; open an issue only for an independently fixable cause. Each issue states the invariant and its boundary, the failing and passing cases, the code paths inspected, the root cause and the closure checks.
  6. Report. Passing, failing, blocked and unreviewed work separately, with the commands, seeds and environment.

Each sweep uses one of three methods:

MethodLooks at
implementationThe code against its invariants: each module's contract, its edge cases, the sibling paths of every defect class
adversarial-integrationThe public API, the CLI and the plugins with hostile and unusual input: odd file systems, encodings, markup, formats
compatibility-distributionThe package as users get it: platforms, Node.js and peer versions, real story formats and browsers, Tweego parity, what the documentation promises

Severities: P1 loses or corrupts data, is a security hole, or breaks common use; P2 gives wrong output or behaviour for supported input, or breaks a documented contract; P3 affects an edge case, a diagnostic or performance; P4 is cosmetic.

Released under the Unlicense.