Skip to content

The corpus job can compare again, and the engine renders composite figures - #39

Merged
dereuromark merged 3 commits into
mainfrom
feat/composite-figures-composite-sat-6436a4ec
Aug 15, 2026
Merged

The corpus job can compare again, and the engine renders composite figures#39
dereuromark merged 3 commits into
mainfrom
feat/composite-figures-composite-sat-6436a4ec

Conversation

@dereuromark

@dereuromark dereuromark commented Aug 15, 2026

Copy link
Copy Markdown
Contributor

Part of markup-carve/carve#1220 (secondary-engine tracker); spec markup-carve/carve#1215, design markup-carve/carve#1122.

What this repository can and cannot do about composite figures

carve-go has no parser. It embeds a wasm32-wasip1 build of carve-rs and renders by driving it over the WASI stdio contract, and the README states the consequence: "The Go output is therefore byte-for-byte the output of the engine it wraps." So there is no port to write here. Implementing PART 9 section 4c in this module means rebuilding against a carve-rs that implements it, and carve-rs now does: markup-carve/carve-rs#986 landed, with markup-carve/carve-rs#996 (cell attributes) on top. The tracker's checklist line for this repository ("port parser/renderers/writer per section 4c") describes a native engine; this one is a wrapper.

The three changes

The corpus population guard reads the pages where the spec keeps them. The guard counts ::: compare blocks in the spec's example pages so the corpus size is checked against something the runner does not itself write. It read them from docs/examples/; markup-carve/carve#1194 filed them under resources/examples/. The read is fatal when it misses, by design, so the job stopped at the first page:

no corpus source page at .../docs/examples/core.md: no such file or directory.
tests/corpus is generated from these pages; if the spec moved them, this
helper has to move with them

That is the guard working. It is also why the drift behind it was invisible: the job had been red since 2026-08-14 with a wiring error stacked on top of a stale artifact, and only the wiring error was legible. The helper now takes the same route to resources/ that corpus_ast_test.go already takes for ast-schema.json.

The embedded wasm is rebuilt, via this repository's own build-wasm.sh, which writes REV and the digest in the same step as the bytes. REV moves from e2b53a5b (main) through c83d6492 to 98de7874, current carve-rs main.

TestToHTML_Table asserted a bare <th>; the header cell carries scope="col", pinned by the corpus documents this rebuild brings into line.

Measured

Against carve 1436f39a, 1025 corpus pairs:

state diverging
main, with the guard fixed so the comparison runs at all 81
rebuilt from carve-rs c83d6492 13
rebuilt from carve-rs 98de7874 0

The 13 that closed in the last step were the two constructs carve-rs itself had not implemented when the earlier rebuild was taken: nine 318-composite-figures* documents and four 319-cell-attributes-* documents. All eleven 318-composite-figures documents and all six 319-cell-attributes-bind-after-the-kind-and-alignment-markers documents now match the spec.

The corpus job stays strict. Nothing here suppresses anything; this repository has no allowlist and does not gain one.

Proof, both directions

Reverted. With the artifact built from c83d6492 and everything else in this branch unchanged, exactly those 13 documents diverge, and each diverges by producing the pre-section-4c shape. 318-composite-figures-4:

::: figure
![lone](l.png)
^ The only panel
:::
^ Figure #: One panel is valid

rendered a generic container and left the caret line a paragraph:

<div class="figure">
  <figure>
    <img src="l.png" alt="lone">
    <figcaption>The only panel</figcaption>
  </figure>
</div>
<p>^ Figure #: One panel is valid</p>

Restored. With the artifact built from 98de7874, all 1025 pairs match, including that one.

The control now discriminates, and it did not before

318-composite-figures-8 is the control for the whole clause: an opener carrying a title or a [label] must stay a generic container rather than become a figure group. It passed before this rebuild too, but vacuously - the engine had no rule separating a bare opener from a decorated one, so mutating the document to make both openers bare changed nothing in the output but two metadata lines. Both spellings still rendered <div class="figure">.

Re-run against the rebuilt artifact, the same mutation changes the production. The document as written:

<div class="figure">
  <p class="admonition-title">A titled figure div</p>
  <figure>
    <img src="a.png" alt="one">
    <figcaption>(a) One</figcaption>
  </figure>
</div>
<p>^ Not a group caption</p>
<div class="figure">
  <p class="div-label">g</p>
  <p>Body.</p>
</div>

and with the title and the [label] stripped, so both openers are bare:

<figure class="carve-figure-group">
  <div class="carve-figure-panels">
    <figure class="carve-figure-panel">
      <img src="a.png" alt="one">
      <figcaption>(a) One</figcaption>
    </figure>
  </div>
  <figcaption>Not a group caption</figcaption>
</figure>
<figure class="carve-figure-group">
  <div class="carve-figure-panels">
    <p>Body.</p>
  </div>
</figure>

Different element, different class, and the caret line is consumed as the group caption instead of surviving as a paragraph. The control asserts something now.

Also in the diff

nativeCarveBin now requires CARVE_BIN instead of picking a binary off the filesystem. That check asserts "the wasm renders what the native engine renders", a statement about one revision - the native side has to be the carve-rs internal/wasm/REV names. It fell back to two hardcoded target/ paths and then carve on PATH, none of which is that revision by construction, and it failed that way here: a stale local build predating scope="col" reported a byte mismatch on tables against a wasm the corpus proves correct. A check that chooses its own reference cannot tell "the wasm is wrong" from "the thing I found is old", and it reported the first. It now skips until a caller vouches for a binary, which is what CI already did on every run, having neither checkout.

No CHANGELOG entry: this repository keeps no CHANGELOG (the last rebuild, #38, touched only the artifact files).

… them

The guard that decides whether a corpus run compared a whole corpus reads the
spec's example pages and counts their `::: compare` blocks, because a count
derived from the directory under test would move with the directory and could
not detect truncation. It read them from docs/examples/.

markup-carve/carve#1194 made those pages generated sources and filed them under
resources/examples/, beside the other generator inputs. The read is fatal when
it misses, by design, so the corpus job stopped at the first page instead of
comparing anything:

    no corpus source page at .../docs/examples/core.md: no such file or
    directory. tests/corpus is generated from these pages; if the spec moved
    them, this helper has to move with them

That is the failure the guard is supposed to produce, and it is also why every
real divergence behind it went unreported: the job has been red since
2026-08-14 with a wiring error on top of a stale artifact, and only the wiring
error was visible. The helper now takes the route corpus_ast_test.go already
takes to resources/ast-schema.json.

Measured against carve main: the job goes from fatal-before-first-comparison to
1023 pairs compared, 81 of them diverging.
…ot render

This module has no parser. It embeds a wasm32-wasip1 build of carve-rs and
renders by driving it over WASI stdio, so its conformance is the embedded
artifact's conformance and "implementing" a construct here means rebuilding
against a carve-rs that implements it.

Rebuilt from carve-rs c83d64922bcb080d5a85aecd2f53adf506fbe8c6 (main). Measured
against carve main, with the population guard fixed so the comparison runs at
all: 81 of 1023 corpus documents diverged before, 13 after. The 68 that this
closes are the ordinary staleness the corpus job exists to catch - tables,
captions, attribute parsing, footnote and reference resolution.

The 13 that remain are not staleness. They are two constructs carve-rs itself
does not implement, so no rebuild reaches them:

  - 318-composite-figures{,-2,-3,-4,-5,-6,-9,-10,-11}: PART 9 §4c, composite
    figures. carve-rs#986 was the port and was closed unmerged.
  - 319-cell-attributes-bind-after-the-kind-and-alignment-markers{,-2,-3,-6}:
    an attribute block after a cell's `=` marker is still rendered as cell text.

Two of the eleven composite-figure documents pass, and they are worth naming
because of WHY they pass: -7 (a `^ ` line after a `::: note` closer is an
ordinary paragraph) and -8 (an opener carrying a title or a label stays a
generic container) both assert the PRE-§4c behavior, which this engine has
always had. -8 in particular is the control for the whole clause, and today it
passes vacuously: making its openers bare changes nothing in the output but the
title and label lines, because the engine has no rule that tells a bare opener
from a decorated one. It will only start meaning something once §4c lands
upstream.

The corpus job stays strict. Nothing here suppresses those 13; the job is red
for them, which is the accurate report.

TestToHTML_Table asserted a bare <th>. The header cell carries scope="col" now,
pinned by the corpus documents this rebuild brings into line, so the hand-written
expectation moves with it - the failure mode corpus_test.go's comment predicts
for the hand-written half of this suite.

THE NATIVE COMPARISON NOW REQUIRES CARVE_BIN. TestToHTML_ByteIdenticalToNative
and the static byte check assert "the wasm renders what the native engine
renders", which is a statement about ONE revision - the native side has to be
the carve-rs internal/wasm/REV names. The helper picked its own reference off
the filesystem instead: two hardcoded target/ paths, then `carve` on PATH, none
of which is that revision by construction. It failed exactly that way here, with
a stale local build predating scope="col" reporting a byte mismatch on tables
against a wasm the corpus proves correct. A check that chooses its own reference
cannot tell "the wasm is wrong" from "the thing I found is old", and it reported
the first. It now skips until a caller vouches for a binary, which is what CI
already did on every run.
@dereuromark

Copy link
Copy Markdown
Contributor Author

CI verdict on this head: 3 of 4 checks green, corpus failing on exactly the 13 documents named in the description - nine 318-composite-figures* and four 319-cell-attributes-*. The run reproduces the local measurement document for document, with no other divergence.

That failure is the intended report, not a defect in the branch: those constructs are unimplemented in carve-rs, and this module renders what carve-rs renders. Making the job green would mean either an allowlist this repository does not have and should not gain, or a claim that is not true.

So this needs a maintainer call rather than an automated merge, and the choice is between two shapes of red on main:

  • as it stands today - the corpus job dies at the population guard before comparing a single document, so nothing downstream of it is reported at all;
  • with this branch - 1023 pairs compared, 13 named failures, each traceable to a specific unimplemented clause.

The branch does not fix the red. It makes it say something.

…ibutes

The previous rebuild in this branch left thirteen corpus documents diverging,
all of them naming two constructs the engine did not yet have. Both have since
landed in carve-rs: markup-carve/carve-rs#986 (PART 9 section 4c, composite
figures) and markup-carve/carve-rs#996 (a cell's attributes bind after its kind
and alignment markers). Rebuilt via this repository's own build-wasm.sh, which
writes REV and the digest in the same step as the bytes.

REV moves from c83d64922bcb080d5a85aecd2f53adf506fbe8c6 to
98de7874ad2e81f69e57764562c83f4918522ac2 (carve-rs main).

Measured against carve 1436f39a, 1025 corpus pairs:

  before this commit  13 diverging
  after               0 diverging

The thirteen that closed are the nine `318-composite-figures*` documents that
were failing and the four `319-cell-attributes-*` documents that were failing.
All eleven `318-composite-figures` documents and all six `319-cell-attributes`
documents now match the spec.

The control is no longer vacuous. `318-composite-figures-8` asserts that an
opener carrying a title or a `[label]` stays a generic container. Before this
rebuild it passed for the wrong reason: the engine had no rule separating a
bare opener from a decorated one, so stripping the title and the label changed
nothing but two metadata lines. It now discriminates structurally. The document
as written renders:

    <div class="figure">
      <p class="admonition-title">A titled figure div</p>
      <figure>
        <img src="a.png" alt="one">
        <figcaption>(a) One</figcaption>
      </figure>
    </div>
    <p>^ Not a group caption</p>

and the same document with the title and the label removed, so both openers are
bare, renders a different production entirely:

    <figure class="carve-figure-group">
      <div class="carve-figure-panels">
        <figure class="carve-figure-panel">
          <img src="a.png" alt="one">
          <figcaption>(a) One</figcaption>
        </figure>
      </div>
      <figcaption>Not a group caption</figcaption>
    </figure>

No allowlist and no drift file exists in this repository, and none is added.
@dereuromark dereuromark changed the title The corpus job can compare again, and composite figures are upstream-blocked The corpus job can compare again, and the engine renders composite figures Aug 15, 2026
@dereuromark
dereuromark merged commit cfbc854 into main Aug 15, 2026
4 checks passed
@dereuromark
dereuromark deleted the feat/composite-figures-composite-sat-6436a4ec branch August 15, 2026 18:53
dereuromark added a commit that referenced this pull request Aug 15, 2026
expectedNodeTypes is this package's own record of what the language emits, and
the assertion over it is one-directional by design: a recorded type going
missing fails, an unrecorded new type is invisible. So the engine rebuild that
brought PART 9 section 4c in (#39) added a node type nothing here names.

Recording it is a tightening, not a widening. `figure_group` appears in no
carve-rs source file at the revision this package embedded before that rebuild
(c83d6492, zero hits under src) and in eleven at the one it embeds now
(98de7874), so this line would have failed the suite before the rebuild and
passes after it. From here on, a rebuild that drops the group node fails with
the type named rather than leaving nine corpus documents to explain it.

The second assertion in this file gains the same reach for free: it requires the
spec's ast-schema.json to define a shape for every type recorded here, and the
schema does define figure_group.

Measured with CARVE_SPEC_CORPUS against carve 1436f39a: 1025 pairs, whole
package green.
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.

1 participant