Development
August 19, 2026 ยท View on GitHub
Setup
git submodule update --init # check out the shared Carve corpus (spec/)
npm install
npm run build
npm test
npm run package
Open the repository in VS Code and press F5 to launch an Extension
Development Host.
Grammar token-snapshot tests
The TextMate grammar (syntaxes/carve.tmLanguage.json) is verified against the
shared Carve corpus,
vendored through the spec/ submodule. Each category classified as covered has
a representative .crv document and a committed golden scope snapshot under
tests/snapshots/.
npm run test:grammar # verify committed scopes
npm run test:grammar:update # regenerate snapshots after a deliberate change
tests/categories.json is the coverage matrix. Every corpus category must be
either covered by a snapshot or skipped with a reason why it has no distinct
TextMate scope. The coverage test rejects new unclassified categories.
Corpus through the extension
The grammar snapshots above measure TextMate scopes and cannot see engine output
at all, so they stay green whether the bundled engine is correct or months
stale. npm run test:corpus drives every corpus document through the two
surfaces the extension actually ships and is part of npm test:
npm run build # it measures dist/, not the sources
npm run test:corpus
npm run test:corpus -- --manifest /tmp/before.tsv # one row per document
- The preview and export path (
renderPreviewBody) is compared byte-for-byte against the corpus.html. - Every document is opened in the real language server process, spawned from the
path
serverModulePath()hands the client, and its diagnostics, outline and folding ranges are collected. - The engine is resolved from BOTH module graphs - the extension's own and carve-lsp's - and the run fails if they land on different copies, or if the two packages pin different engine revisions. That is the state the extension shipped in before #133: the language server ran a parser the preview was not using.
The run refuses to report anything over a population it did not check the size
of. The number of documents must equal the number of ::: compare blocks the
spec's resources/examples/ pages declare, so an empty or truncated corpus is a
failure rather than a fast green run.
Pass --manifest on both sides of an engine bump and diff the two files: totals
alone cannot tell a document that lost a diagnostic from another that gained
one.
Updating the corpus
Update the submodule and regenerate the snapshots:
git -C spec fetch origin main
git -C spec checkout origin/main
npm run test:grammar:update
Review both the submodule change and generated snapshot diff. New categories
must be deliberately added to covered or skip in tests/categories.json.
When the engine is behind the spec
The spec moves ahead of the published engine routinely: a ruling lands, the
corpus gains a document for it, and @markup-carve/carve only carries it after
its next release. npm run test:corpus then reports renders differently for
documents nothing in this repository can fix.
ENGINE_LAG in tools/corpus-through-extension.mjs waives exactly those, and
it is keyed by ENGINE_PIN. Add a document only when the engine provably
predates the rule it pins, and name the ruling in the value.
Empty it at the next engine bump. This is part of releasing, not a cleanup task for later:
- Raise the engine dependency.
- Set
ENGINE_LAG = {}andENGINE_PINto the new revision. - Run
npm run test:corpus. Whatever still mismatches goes back in the list, with its ruling named; everything else is fixed and stays out.
Two gates make the list expire loudly rather than quietly becoming permanent, and both fail the run rather than only printing:
- a waived document that renders correctly again fails with
an engine-lag waiver is no longer needed, because a stale waiver hides the next regression on that same document; - moving the engine pin without emptying the list fails with
the engine pin moved and ENGINE_LAG was not emptied, because every waiver in it was written against the old revision and says nothing about the new one.
A non-empty ENGINE_LAG at release time means the shipped extension renders
those documents differently from the spec. That is acceptable while it is
recorded and expiring; it is not acceptable as a permanent state.
Packaging and local installation
npm run package
code --install-extension vscode-carve-0.1.0.vsix
Use the filename emitted by npm run package if its version differs from the
example above.