Versioning & Changelog
This page defines how the Carve specification is versioned and records every normative change. It is the source of truth for "did the language change in a way that affects my documents?"
Versions
The spec carries a version (the Version: field in grammar.ebnf), currently 0.1. It is pre-1.0: the language is still settling, so a minor bump may include behavior changes. From 1.0 onward, behavior changes reserve a major bump.
Implementations declare which spec version they conform to. The carve fmt --stamp tool records it inside a document as a trailing provenance marker:
%% carve-version: 0.1; generated-by: carve-js 0.1.0So a document carries the spec version it was last processed under, and this page tells you what changed since; together they answer whether a document needs attention after a spec upgrade.
Change categories
Every changelog entry is tagged with its migration impact:
[behavior]— changes the rendered output of some existing input. These are the entries that can require document migration: a.crvthat rendered one way before may render differently. Review these when upgrading.[clarification]— pins or documents existing behavior without changing output. No migration needed; impls may have converged to match.[addition]— new syntax or capability. Backward-compatible: existing documents are unaffected; only documents that opt into the new construct change.
When upgrading a document across spec versions, you only need to act on [behavior] entries between the document's stamped carve-version and the target version.
Declaring a target version
The marker above is written by tooling. An author who wants to state which version a document targets does it in frontmatter:
---
carve-version: 0.1
---The key is optional. carve lint reads it and reports carve-version-unsupported when a document targets a version the processor does not implement, which is otherwise a silent degradation: the constructs the author relied on parse as something else and nothing says so. The two fields do not compete - the frontmatter key is the author's intent, the trailing marker is what last processed the file - and a document carrying only the marker is still checked.
Behavior changes inside 0.1.x
A 0.1.x release may still change what an existing document renders to, and the semantic-span work did it more than once. The end state is three reserved span attributes in core - abbr, time, kbd - and here is what moved.
1. Three names gained a meaning as span attributes. On an ordinary [content]{attrs} span each is now consumed into its HTML element instead of reaching the output as an attribute (PART 9 §9):
[x]{time="2026-01-01"}<span time="2026-01-01">x</span> <!-- before -->
<time datetime="2026-01-01">x</time> <!-- after -->A document that used abbr, time or kbd as a plain marker attribute on a span renders differently after upgrading.
2. Leftover attributes moved onto the semantic element. A consumed name renames the span rather than wrapping it, so an id or class lands on the element:
[Tab]{#k .key kbd}<span id="k" class="key"><kbd>Tab</kbd></span> <!-- before -->
<kbd id="k" class="key">Tab</kbd> <!-- after -->A stylesheet or script written against the wrapper needs a look.
3. Four names left core for an extension. samp, var, cite and dfn briefly selected elements too; they are now the opt-in SemanticSpan extension's, so a core processor leaves them as ordinary attributes. cite is the one to check - it is a real HTML attribute on blockquote and q, so {cite="…"} on a span was a reasonable thing to write, and while the extension is enabled its value reaches no output at all.
4. The :name[…] spelling lost its handlers. :kbd[Tab] renders <span class="ext-kbd">Tab</span> in a core processor; the SemanticSpan extension accepts it as a soft-deprecated form, scheduled for removal in 0.2. This is the one break in RELEASED behavior rather than in a development window: :kbd[x] has rendered <kbd> in carve-js since its first release. The rewrite is mechanical - :kbd[Tab] becomes [Tab]{kbd}.
5. The language attribute claimed the {:…} slot, which was unassigned and therefore literal:
[Le Bon Usage]{:fr}<p>[Le Bon Usage]{:fr}</p> <!-- before -->
<p><span lang="fr">Le Bon Usage</span></p> <!-- after -->The exception was taken deliberately and on evidence: attribute names cannot start with :, so nothing could collide, and an audit of the organization plus a public .crv code search found no literal use to break. A malformed tag ({:en_US}, {:français}) still leaves the block literal, so a typo looks like a typo rather than half-parsing.
6. The plain-text target stopped flattening nested lists. Before, depth was erased and every item came out a sibling; now each list ancestor indents its item by two spaces (PART 11 §10h). A pipeline that parsed the plain output by column will see different columns.
To find affected documents, search for the seven names used as attributes on a span, and for :name[…] with any of them. Where a value mattered, move it to an attribute that survives - a title, or a link if it was a URL.
A bare ::: figure container is now a composite figure. The kind word figure is reserved (PART 9 section 4c, carve#1122): a bare opener produces a figure_group - one numbered figure whose captioned children are panels - where it used to produce a generic <div class="figure">, and the ^ line after its closing fence attaches as the group caption where it used to stay a literal paragraph:
{#ep}
::: figure
> To be
:::
^ Figure #: A pull quote<div class="figure" id="ep"> <!-- before -->
<blockquote><p>To be</p></blockquote>
</div>
<p>^ Figure #: A pull quote</p>
<figure class="carve-figure-group" id="ep"> <!-- after -->
<blockquote><p>To be</p></blockquote>
<figcaption>Figure 1: A pull quote</figcaption>
</figure>An opener carrying a quoted title or a [label] keeps the old generic-container shape. Documents that already hold a bare ::: figure fence reclassify, and a previously dangling caption starts consuming a figure number, which can renumber later figures in the same document - carve lint reports the affected shapes.
Checking documents mechanically
The marker is machine-readable, so this does not have to be done by eye. In carve-php:
use MarkupCarve\Carve\Stamp;
Stamp::read($source); // ['version' => '0.1', 'generatedBy' => 'carve-php 0.1.0'] or null
Stamp::needsReview($source); // true when the document predates the engine's spec versionand from the CLI, for a repository of stored documents:
carve --stamp-info doc.crv # report the version and the writer
carve --stamp-check doc.crv # exit 1 when the document predates this spec versionAn unstamped document counts as needing review: its provenance is unknown, and assuming it is current is the unsafe direction. Hand-written documents are unstamped until carve fmt --stamp touches them.
Which implementations can read it
Every implementation both writes and reads the marker, behind the same two CLI flags with the same output where there is a CLI, so any of them can check a document another wrote.
| implementation | reads it with |
|---|---|
| carve-php | Stamp::read / Stamp::needsReview, --stamp-info / --stamp-check |
| carve-js | readStamp / needsReview, same two flags |
| carve-rs | read_stamp / needs_review, same two flags |
| carve-go | ReadStamp / NeedsReview |
| carve-rb | Carve.read_stamp / Carve.needs_review? |
| carve-py | carve.read_stamp / carve.needs_review |
The three bindings drive carve-rs, so each answers exactly what that engine answers: carve-go reads --stamp-check's exit status across the wasm boundary, while carve-rb and carve-py call the crate directly.
The marker format is the contract, not any one API, so a document stamped by any engine is readable by any other. That was verified rather than assumed - each engine reads the markers the others write, in both the line and block forms, and carve-js pins carve-php's exact bytes as test fixtures.
Changelog
Released changes live in CHANGELOG.md, which is written per merge and cut into sections at each tag.
This page carried its own copy, and the copy was wrong: both entries it filed under a future 0.2 had already shipped in released 0.1.4 - the alignment-run terminator and the two-axis table model alike. An ungated second changelog drifts into contradiction, so there is one file and this is a pointer to it.