Skip to content

chore: bump the engine pin, and give the drift job something that can fail - #20

Merged
dereuromark merged 2 commits into
mainfrom
chore/engine-pin-and-a-drift-gate-that-can-fail
Aug 17, 2026
Merged

chore: bump the engine pin, and give the drift job something that can fail#20
dereuromark merged 2 commits into
mainfrom
chore/engine-pin-and-a-drift-gate-that-can-fail

Conversation

@dereuromark

Copy link
Copy Markdown
Contributor

The pin was a carve-js revision from 2026-08-05, 333 commits behind main. Measured through the artifact rather than through the revision distance:

before: 256 of 1131 corpus documents differ, carve-js main differs on 0 -> 256 attributable to the pin
after:    0 of 1131 corpus documents differ

The bump is not mechanical - two upstream removals land with it. A block quote no longer carries an attribution field, since a caption on a quote makes a figure (markup-carve/carve#1213) and the figure branch of the substitution walk already covers that target. And beforeRender now takes the read-only context from the extension contract; page.ts unrolls carveToHtml, so it builds that context itself, with the mode fields constant because HTML is the only target here and there is no static mode.

The half that let it drift

engine-drift.yml compared revisions and reported the lag as a warning annotation. A warning cannot fail a job, so every scheduled run was green for the whole 333 commits.

Warning-only is defensible where the remedy is upstream - that is what the carve-php satellites do, because only a release can close their gap. Here the remedy is one line of package.json, so the job now renders carve main's corpus through the engine as installed and fails over a tolerance. It counts only what a bump here would fix, by taking the difference against the same corpus through carve-js main: a gate that fails for upstream debt gets its tolerance raised until it stops meaning anything.

The population guard is the other half. An absent or truncated corpus renders zero documents, finds zero divergences and reads as a clean run, so the count is compared for equality against the number of ::: compare blocks in the spec's example pages, which is what the corpus is generated from.

Mutation proof

Against the previous pin, with everything else unchanged:

documents=1131 wrong=256 reference_wrong=0 attributable=256
::error::256 of 1131 corpus documents render differently through the pinned engine but correctly through carve-js main, over the tolerance of 10.
exit=1

Against the new pin: attributable=0, gate passes. And against a corpus cut to 378 documents the population guard reports the shortfall rather than a clean sweep:

::error::378 corpus pairs found ..., but the spec's example pages declare 1131.
exit=1

Closes #19

… fail

The pin sat at a carve-js revision from 2026-08-05, 333 commits behind main.
Measured through the artifact rather than the revision distance, that engine
rendered 256 of 1131 spec-corpus documents differently; carve-js main renders
0 of them differently, so every one of the 256 was attributable to the pin.
Largest single family was table header cells losing their scope attribute, but
it also covered footnote body columns, tab handling, raw blocks, reference
definitions and continuation markers.

Bumping it needs two source changes, both of them upstream removals rather than
anything this repo did:

- a block quote no longer carries an attribution field. A caption on a quote
  makes a FIGURE (carve#1213), and the figure branch of the substitution walk
  already handles that target, so the quote branch simply loses the arm.
- beforeRender now takes the read-only context the extension contract
  describes (spec section 2.2). page.ts unrolls carveToHtml, so it has to build
  that context itself: a hook runs before any render starts, and without it a
  hook emitting output of its own renders with defaults rather than with the
  options the page is rendered under. Only HTML is ever the target here and
  there is no static mode, so the mode fields are constant.

The second half is why nobody noticed. engine-drift.yml compared revisions and
reported the lag as a warning, which cannot fail a job, so every scheduled run
was green throughout. A warning is the right instrument where the remedy is
upstream and unavailable locally, which is what the carve-php satellites are
looking at. It is the wrong one here: the remedy is one line of package.json.

So the job now renders carve main's corpus through the engine AS INSTALLED and
fails over a tolerance. It counts only what a bump here would fix, taking the
difference against the same corpus rendered through carve-js main - a gate that
fails for upstream debt gets its tolerance raised until it means nothing.

The population guard is the other half of a check that can fail. An absent or
truncated corpus renders zero documents, finds zero divergences and reads as a
clean run, so the count is compared for EQUALITY against the number of
::: compare blocks in the spec's own example pages, which is where the corpus is
generated from. Counting the corpus directory to decide how big the corpus
should be would move both sides together and guard nothing, and a hardcoded
1131 goes stale the day an example lands upstream.

Both halves were mutation-proved: the gate run against the previous pin fails
with 256 of 1131 attributable, and against a corpus cut to 378 documents it
reports the shortfall instead of a clean sweep.
…eader

Three things the pin bump moves, all of them the engine catching up with the
spec rather than anything this repo chose:

- twenty-one new engine exports, none of them extension factories: the AST-JSON
  and AST-patch error classes, the BBCode and HTML import limits and their
  errors, the smart-quote locale table, and the AST-patch, HTML-import,
  source-layout and migration utilities. Each gets a reason in the playground's
  exclusion map, which is what the drift guard reads.
- smartQuotes IS a real extension, and it is excluded anyway. The playground
  exists to show what this site renders, and the site's stack does not enable
  smart quotes, so enabling it only in the playground would make the two
  disagree on the same source - the reason defaultAttributes is excluded.
- semantic-span is a real extension the engine now ships, and it belongs in the
  playground stack, so it joins the expected list rather than the exclusions.

The table-scroll expectation loses nothing: a header cell in a body row is a
ROW header and the engine now says so, which is one of the 256 documents the
old pin got wrong.
@dereuromark
dereuromark merged commit 1bb1326 into main Aug 17, 2026
10 checks passed
@dereuromark
dereuromark deleted the chore/engine-pin-and-a-drift-gate-that-can-fail branch August 17, 2026 09:40
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.

The pinned carve-js renders 256 of 1124 corpus documents wrongly, and the drift job can only warn about it

1 participant