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
0.1
Initial released version. Establishes the grammar, the PART 9 semantic constraints, the conformance corpus, and the tooling conventions (carve fmt, the provenance marker, profiles, the extension contract). Everything prior to 0.1 was draft; there is no earlier released version to migrate from.