Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
83 commits
Select commit Hold shift + click to select a range
9849a98
test: park the modules later 0.5 plans port
Maxanox Oct 13, 2026
03b231a
feat(core): RichText, Mark and Numbering value objects (proposal 0013)
Maxanox Oct 13, 2026
93e0aad
feat(core): 0.5 node model — RichText prose, children containers, adm…
Maxanox Oct 13, 2026
90212cf
test(fixtures): rewrite fixtures to 0.5, add elements.cnd, regenerate…
Maxanox Oct 13, 2026
99e6a68
feat(core): traversal enters items, cells and footnotes; slot and pat…
Maxanox Oct 13, 2026
cdec183
feat(core): validate marks, text spans, doc.extra and footnote conten…
Maxanox Oct 13, 2026
d5374ad
feat(core): 0.5 hashing — defaults dropped, nested nodes excluded, pa…
Maxanox Oct 13, 2026
5f19cfa
docs: spec for format 0.5.0, ADRs 0025-0028 (proposal 0013)
Maxanox Oct 13, 2026
f6cfcb8
fix(core): state every default in the schema, add the explicit-defaul…
Maxanox Oct 13, 2026
189b4ea
fix(validate): no crash on empty-label edges, distinct message for em…
Maxanox Oct 13, 2026
4d3752e
feat(core): export child_slots, ChildSlot and the known-kind vocabula…
Maxanox Oct 13, 2026
43f9643
Merge pull request #8 from cosmyc-lab/cnd-17-implement-the-cnd-0-5-0-…
Maxanox Oct 13, 2026
54271dc
feat(core): one inline layer renders rich text to Markdown, HTML and …
Maxanox Oct 13, 2026
f2b1776
feat(core): placeholders and table helpers read 0.5 cells, carry desc…
Maxanox Oct 13, 2026
39a249e
feat(render): NodeRenderer gains render_text, admonition and layout; …
Maxanox Oct 13, 2026
e462052
feat(render): MarkdownRenderer renders marks, admonitions, checkboxes…
Maxanox Oct 13, 2026
e59d370
test: port test_render and test_full_coverage to 0.5 and unpark them
Maxanox Oct 13, 2026
9a87a95
feat(converters): Markdown converter on 0.5 — containers render once,…
Maxanox Oct 13, 2026
11bd9ee
fix(converters): a blank line between a container block and its marke…
Maxanox Oct 13, 2026
0ed6b16
feat(converters): HTML converter on 0.5 — footnote blocks, rich refer…
Maxanox Oct 13, 2026
341ade3
test(golden): regenerate converter goldens for 0.5, add the elements …
Maxanox Oct 13, 2026
f4fa4cc
fix(render): a code text ending in a newline adds no blank line befor…
Maxanox Oct 13, 2026
ae99499
fix(render): a heading nested in a container or a footnote keeps its …
Maxanox Oct 13, 2026
f981c52
fix(render): a prefix before a block that is not a paragraph stands o…
Maxanox Oct 13, 2026
be37672
fix(inline): judge flanking on the rendered edge of a mark's content
Maxanox Oct 13, 2026
23c91d9
test(converters): the HTML converter carries a nested paragraph's mar…
Maxanox Oct 13, 2026
f28609a
test(render): name the table kind tests after the table kind
Maxanox Oct 13, 2026
642fe78
fix(inline): a mark dropped to plain text still blocks a star merge
Maxanox Oct 13, 2026
6fde1fc
fix(render): a definition or a row cell holding a block is followed b…
Maxanox Oct 13, 2026
2f5e4c7
feat(converters): the HTML converter renders hyperlinks
Maxanox Oct 13, 2026
d3b7310
fix(render): a Markdown math block is a math fence
Maxanox Oct 13, 2026
c566e23
fix(core): a cell spans at least one row and one column
Maxanox Oct 13, 2026
fd7b2de
fix(converters): front matter escapes every control character
Maxanox Oct 13, 2026
3cc90fd
fix(converters): an HTML image emits only a safe source
Maxanox Oct 13, 2026
497726f
fix(converters): an HTML extra key cannot shadow a meta the converter…
Maxanox Oct 13, 2026
6379cf0
fix(inline): text-only superscripts and subscripts keep their meaning
Maxanox Oct 13, 2026
e5f46c0
fix(render): a block after a nested list makes the list loose
Maxanox Oct 13, 2026
1ced675
feat(core): export expand_headings
Maxanox Oct 13, 2026
ffa7754
style(render): rewrap long docstring lines, no empty admonition kind …
Maxanox Oct 13, 2026
12634bf
fix(render): a math fence holds raw source only when it is LaTeX
Maxanox Oct 13, 2026
e957505
fix(inline): wrapped edges never nest
Maxanox Oct 13, 2026
2360da5
fix(converters): an HTML link that does not render inline goes to the…
Maxanox Oct 13, 2026
271f166
docs(spec): section 7 states how links, URLs and math fences render
Maxanox Oct 13, 2026
fe38b08
fix(converters): a line ending in an href cannot leave the Markdown l…
Maxanox Oct 13, 2026
6c3a0eb
fix(core): leading Unicode whitespace no longer hides an unsafe scheme
Maxanox Oct 13, 2026
7ee136a
test(converters): write the separator characters as escapes
Maxanox Oct 13, 2026
21ae25d
fix(converters): front matter also escapes C1 controls and noncharacters
Maxanox Oct 13, 2026
cc47aee
fix(converters): judge an HTML link inline per text, not per object
Maxanox Oct 13, 2026
60005fa
fix(render): a Markdown image emits only a safe, escaped destination
Maxanox Oct 13, 2026
0bb5a13
fix(inline): a strike delimiter never touches another tilde
Maxanox Oct 13, 2026
3e27272
fix(render): text a Markdown placeholder or image alt carries stays i…
Maxanox Oct 13, 2026
8b2e18b
fix(converters): Markdown labels and cite supplements stay inert
Maxanox Oct 13, 2026
dc980e0
Merge pull request #9 from cosmyc-lab/cnd-27-render-cnd-0-5-0
Maxanox Oct 13, 2026
82ba11f
Merge pull request #10 from cosmyc-lab/cnd-36-render-links-and-harden…
Maxanox Oct 13, 2026
6d5a3a4
fix(converters): Markdown labels stay apart under case folding
Maxanox Oct 14, 2026
ecb14f1
fix(converters): a kept label character is stable under every reader'…
Maxanox Oct 14, 2026
3211d2f
Merge pull request #11 from cosmyc-lab/cnd-38-case-safe-markdown-labels
Maxanox Oct 14, 2026
57941b3
feat(cnd): Cnd.to_json writes a CND with default-valued fields omitted
Maxanox Oct 14, 2026
b6644b9
feat(declaration): declaration 0.2.0 on the 0.5 models; unpark test_d…
Maxanox Oct 14, 2026
9fa5981
feat(builder): build CND 0.5.0 from declaration 0.2.0; unpark test_bu…
Maxanox Oct 14, 2026
382eb28
feat(reconcile): match over the full walk and the spec path; unpark t…
Maxanox Oct 14, 2026
e95abb4
feat(reconcile): matching v2; a container never takes a deleted twin'…
Maxanox Oct 14, 2026
2793004
fix(reconcile): headings and figures keep their v1 hash keys
Maxanox Oct 14, 2026
59cac41
feat(reconcile): last pass pairs containers on their node hash alone
Maxanox Oct 14, 2026
692441b
test(reconcile): vector 11 exercises the container last pass
Maxanox Oct 14, 2026
6641bc8
feat(visitors): BaseVisitor dispatches admonition and layout; unpark …
Maxanox Oct 14, 2026
b6fd971
feat(display): show every 0.5 node type; unpark test_node_display_vis…
Maxanox Oct 14, 2026
eaca0a2
fix(display): never read CND text as Rich markup
Maxanox Oct 14, 2026
333af28
feat(cli): every command on CND 0.5.0; unpark test_cli
Maxanox Oct 14, 2026
781238f
feat(typst): the table hint is kind, the CND's table.kind (proposal 0…
Maxanox Oct 14, 2026
5ef4be3
docs: proposal 0013 implemented; spec 12 on rich text, numbering and …
Maxanox Oct 14, 2026
5b58961
fix(cli): read and write CND and declaration files as UTF-8
Maxanox Oct 14, 2026
da4f146
fix: refuse another format or declaration version before checking the…
Maxanox Oct 14, 2026
06f2f65
fix(builder): never share a mutable object with the declaration
Maxanox Oct 14, 2026
6e08449
fix(typst): the table hint restores the kind it replaced
Maxanox Oct 14, 2026
f0791d1
feat(visitors): visit(cnd, full=True) walks the footnotes with spec p…
Maxanox Oct 14, 2026
a99d2e9
docs(spec): section 12 lists bibliography entries as strict, accepts …
Maxanox Oct 14, 2026
69a1fdd
docs(adr): 0029 matching v2; the last pass reports container-hash
Maxanox Oct 14, 2026
9cc3299
docs(changelog): unreleased entry for CND format 0.5.0
Maxanox Oct 14, 2026
2eb3c2d
Revert "fix(typst): the table hint restores the kind it replaced"
Maxanox Oct 14, 2026
41a95d7
fix(typst): the table hint lives under its own state key, table-kind
Maxanox Oct 14, 2026
7678aeb
docs(changelog): Cnd.to_json, declaration hyperlinks, the table-kind …
Maxanox Oct 14, 2026
508e516
Merge pull request #12 from cosmyc-lab/cnd-40-produce-cnd-0-5-0
Maxanox Oct 14, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 46 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,52 @@ the minor version, fixes bump the patch version).

## [Unreleased]

The SDK implements CND **format 0.5.0** (docs/proposals/0013). This is a
**breaking** format change: a `"0.4.0"` CND does not parse as `"0.5.0"`, and
**every content hash changes** (spec §10, docs/adr/0028). No 0.4 → 0.5
upgrader is shipped (spec §11): producers move by emitting 0.5.

### Changed

- **Rich text with marks**: prose is a `RichText` with standoff marks
(emphasis, strong, code, math, …), and the four link families moved
from the node into its texts (docs/adr/0025).
- **Block containers**: every body lives in `children`, so list items,
term items, table cells and footnotes hold nodes. `numbering` replaces
`number` and `counter_label`.
- **Declaration 0.2.0**: texts accept a string shorthand or a full
`RichText`, and an authored word is `numbering.name`. A 0.1.0 declaration,
like a CND stamped with another `cnd_version`, is refused with an error
that names both versions (`declaration-version-unsupported`,
`cnd-version-unsupported`).
- **Matching v2** (docs/adr/0029, extending docs/adr/0018): runs over the full
walk with spec §8 paths, keys footnote content by its note's label and
containers by their nested content too. A last pass reports
`matched_by: "container-hash"`. `fixtures/matching.json` is now v2.
- **Typst package**: the `cnd.table` parameter is renamed from
`content_kind:` to `kind:`, after the CND's `table.kind`. The
`cnd.metadata` state carries the hint under the dedicated key
`table-kind` (it was `content_kind`), so it never collides with a
document-level `kind` an author sets; an emitter reads `table-kind`
and writes `table.kind`.

### Added

- **New nodes and fields**: `admonition` and `layout` nodes, list-item
checkboxes (`checked`), figure `description`, code `title`, and
`doc.status`, `doc.version` and `doc.extra`.
- `MarkdownConverter(text_only=True)` and `MarkdownRenderer(text_only=…)`:
Markdown with no HTML at all.
- HTML output renders `links` edges as `<a class="cnd-link">` around the
text they span, for safe URLs only.
- `Cnd.to_json()`: the public writer, which omits every default-valued
field (the result parses back to the same model and the same hash).
- **Hyperlinks in declarations**: a declaration's full text form is the
CND's `RichText`, so it carries `links` like `refs`, `cites` and
`footnotes` (spec §12).
- `BaseVisitor.visit(cnd, full=True)` walks the footnotes' content too,
with spec §8 paths.

## [0.8.0] - 2026-10-11

SDK release 0.8.0 still implements CND **format 0.4.0**: no schema change.
Expand Down
25 changes: 21 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,10 +62,16 @@ copy it into a project (e.g. as a root file `cnd.typ`), then:
#import "/cnd.typ"

#cnd.metadata.update(it => it + (revision: "4.2"))
#cnd.table(table(columns: 2, [A], [B]), content_kind: "data")
#cnd.table(table(columns: 2, [A], [B]), kind: "data")
#cnd.decorative(image("logo.png", width: 2cm))
```

`cnd.table(body, kind: ...)` records the table's kind for the emitter:
`"data"` (each row a record) or `"content"` (cells to read). The state
carries it under the dedicated key `table-kind`, which an emitter reads
and the CND writes as `table.kind` (spec §6.3); your own metadata, a
document-level `kind` included, is never touched.

`cnd.decorative(body)` marks content as purely decorative — a logo, an
ornament, an icon — so no producer emits it (spec §2, "Decorative
content"). It is Typst's `pdf.artifact`, so the rendered page is unchanged.
Expand Down Expand Up @@ -107,10 +113,11 @@ the corpus matches, carrying all four vector kinds the same ADR requires
(foreign format → declaration) is not here and never will be: it belongs
with the producers, not the hub (same ADR, §2).

`diff` sits apart from the other four: matching v1 is a **versioned
`diff` sits apart from the other four: matching v2 is a **versioned
reference algorithm, not a normative rule**
([`docs/adr/0018`](docs/adr/0018-reconciliation-reference-algorithm.md)).
Reproducing `matching.json` proves an implementation runs v1 — it is not
([`docs/adr/0018`](docs/adr/0018-reconciliation-reference-algorithm.md);
v2 itself is [`docs/adr/0029`](docs/adr/0029-matching-v2.md)).
Reproducing `matching.json` proves an implementation runs v2 — it is not
something the format requires of anyone.

## Reconciliation
Expand Down Expand Up @@ -141,6 +148,16 @@ format** — exact for labelled nodes, best-effort for the rest, and every
pairing reports which pass made it so a caller can tell the two apart.
Pass `only_exact=True` to inherit ids on labels alone.

The version implemented is **matching v2**
([`docs/adr/0029`](docs/adr/0029-matching-v2.md)): it runs over the full
walk (the body, then every footnote's content) with spec §8 paths, keys a
footnote's content by its note's label, and keys a container (list,
terms, table, layout, quote, admonition) on its own hash plus a digest of
its nested content. A last `container-hash` pass pairs a container whose
nested content was edited and that also moved. Twin headings or figures
with identical titles, and twin containers both edited and shifted, can
still swap.

Turning a foreign format into a CND (`cnd declare`) is a different kind of
absence: it belongs with the producers permanently, since bundling it would
make the hub depend on a satellite.
Expand Down
11 changes: 8 additions & 3 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,15 +23,20 @@ conventions.
| [0013](adr/0013-rename-link-span-to-text-span.md) | Rename the link-family `span` field to `text_span` | accepted |
| [0014](adr/0014-relation-to-doclang.md) | CND's relation to DocLang — coexistence and interoperability, not convergence | accepted |
| [0015](adr/0015-cnds-immutable-ids-not-durable.md) | CNDs are immutable build artifacts; node ids are not durable | accepted |
| [0016](adr/0016-content-hashing.md) | Derived content hashing — canonical serialisation and excluded presentation state | accepted |
| [0016](adr/0016-content-hashing.md) | Derived content hashing — canonical serialisation and excluded presentation state | superseded by 0028 |
| [0017](adr/0017-cross-references-resolve-by-label.md) | Cross-references resolve by target label; drop the id from link families | accepted |
| [0018](adr/0018-reconciliation-reference-algorithm.md) | Reconciliation (diff, id inheritance) is a versioned reference algorithm, not a format guarantee | accepted |
| [0018](adr/0018-reconciliation-reference-algorithm.md) | Reconciliation (diff, id inheritance) is a versioned reference algorithm, not a format guarantee | accepted, extended by 0029 |
| [0019](adr/0019-production-topology-two-doors.md) | Production topology — a declarative door and a direct door over one shared contract | accepted |
| [0020](adr/0020-repository-as-conformance-hub.md) | The repository is a conformance hub; the hub ships a conformance CLI | accepted |
| [0021](adr/0021-artifact-naming-and-file-identity.md) | Artifact naming and file identity — the CND and the declaration | accepted |
| [0022](adr/0022-rust-succession-plan.md) | Rust succession plan — Python is the reference until the format freezes | proposed |
| [0023](adr/0023-pure-typst-authoring-package.md) | Pure-Typst authoring package for CND metadata | accepted |
| [0024](adr/0024-hyperlinks-as-a-node-family.md) | Hyperlinks are a fourth forward family, href-keyed and outside label resolution | accepted |
| [0025](adr/0025-inline-edges-belong-to-rich-text.md) | Inline edges belong to rich text, not to the node | accepted |
| [0026](adr/0026-open-vocabularies.md) | Open vocabularies — known values, free values, a fallback per field | accepted |
| [0027](adr/0027-no-compatibility-guarantee-before-1-0.md) | No compatibility guarantee before format 1.0.0 | accepted |
| [0028](adr/0028-content-hashing-0-5.md) | Content hashing, 0.5 amendments — defaults dropped, nested nodes excluded, paths in the document hash | accepted |
| [0029](adr/0029-matching-v2.md) | Matching v2 — the full walk, spec §8 paths and container keys | accepted, extends 0018 |

## Proposals (docs/proposals/)

Expand All @@ -49,4 +54,4 @@ conventions.
| [0010](proposals/0010-counter-label.md) | Capture the counter label as its own field, beside the number | implemented |
| [0011](proposals/0011-decorative-content.md) | Content marked decorative is never emitted | implemented |
| [0012](proposals/0012-placeholder-context.md) | Figure placeholders carry every piece of context the node holds | implemented |
| [0013](proposals/0013-cnd-0.5.0-rich-text-and-block-containers.md) | CND 0.5.0 — rich text, block containers, and the elements a document actually uses | approved |
| [0013](proposals/0013-cnd-0.5.0-rich-text-and-block-containers.md) | CND 0.5.0 — rich text, block containers, and the elements a document actually uses | implemented |
4 changes: 2 additions & 2 deletions docs/adr/0016-content-hashing.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
---
title: Derived content hashing — canonical serialisation and excluded presentation state
status: accepted
status: superseded
date: 2026-07-22
tags: [hashing, identity, derived, change-detection]
related: [0008, 0012, 0015]
superseded-by: null
superseded-by: 0028
---

# ADR 0016 — Derived content hashing
Expand Down
72 changes: 72 additions & 0 deletions docs/adr/0025-inline-edges-belong-to-rich-text.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
---
title: Inline edges belong to rich text, not to the node
status: accepted
date: 2026-10-13
tags: [schema, breaking-change, refs, rich-text, 0.5.0]
related: [0008, 0009, 0013, 0024]
superseded-by: null
---

# ADR 0025 — Inline edges belong to rich text

## Status
Accepted. Updates where ADRs 0009 and 0024 place the link families; their
other decisions stand.

## Context
Up to format 0.4.0, the four link families (`refs`, `cites`,
`footnotes`, `links`) live on the node base (ADR 0009, ADR 0024), and an
edge's `text_span` locates its marker in "the node's rendered text"
(ADR 0013). That phrase is well defined for a paragraph, which has one
text. It is ambiguous for every node that has several: a table (which
cell?), a figure (the caption, or something else?), a term list (which
term?). Two implementations that render the same node differently place
the same span on different characters, and nothing in the file says
which one was meant.

Format 0.5.0 makes the ambiguity worse and the fix possible at once
(docs/proposals/0013). Prose becomes a `RichText` object — a `value`
string with standoff marks — in every slot that holds prose, and nodes
gain more texts: a figure has a caption and a description, an
admonition a title, a quote an attribution. Marks need to know which
text they index; so do edges. Keeping edges on the node would need a
locator for every multi-text node (`"in": "cells[3]"`), a path string
every consumer must parse and that breaks when the structure moves.
Defining one canonical rendered text per node would tie every span to a
rendering rule, so that changing the rule shifts every span.

## Decision
The four families live on `RichText`, beside `marks`. Every span — a
mark or an edge's `text_span` — indexes the `value` of the `RichText`
that holds it, in Unicode code points (ADR 0013).

1. **An edge belongs to the text where its marker opens.** Its owning
node is the nearest node that holds that text; an edge in a
bibliography entry's `formatted` text is owned by the entry.
`text_span` stays nullable, for a marker that renders no text (a
`form: "none"` citation) or one that does not close inside the text.
2. **The families keep their contracts.** They stay forward-only
(ADR 0008): a CND serializes no incoming edge. `refs`, `cites` and
`footnotes` stay label-keyed and resolve in their family's domain
(ADR 0017). `links` stays href-keyed and outside label resolution
(ADR 0024).
3. **No node carries a link family.** The node base keeps `id`, `label`,
`state_metadata` and `location`.

## Consequences
- `NodeBase` loses `refs`, `cites`, `footnotes` and `links`. A 0.4.0 CND
does not parse as 0.5.0; under the pre-1.0 policy (ADR 0027) that is
allowed, and no upgrader is shipped.
- The SDK adds `texts_of(node)` — the node's own texts in document order,
never a nested node's — and `edges_of(node)`, the edges of those texts
flattened. Code that used to read one list per node reads one list per
node again.
- The derived incoming index (`Cnd.incoming`) and validation read texts.
Validation also checks the edges and marks of a bibliography entry's
`formatted`, and every `text_span` against the length of its own
`value`.
- There is no "rendered text of a node" left to define. Renderers may
render a node however they like without moving any span.
- A node with several texts reports the edges of each text separately in
the file; a consumer that wants every outbound edge of a node flattens
them, as `edges_of` does.
67 changes: 67 additions & 0 deletions docs/adr/0026-open-vocabularies.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
---
title: Open vocabularies — known values, free values, a fallback per field
status: accepted
date: 2026-10-13
tags: [schema, extensibility, 0.5.0]
related: [0010]
superseded-by: null
---

# ADR 0026 — Open vocabularies

## Status
Accepted.

## Context
Several CND fields take a value from a list the specification defines,
but must also accept values an author or producer invents. `figure.kind`
already worked this way (ADR 0010): `"image"` and `"table"` sit beside an
author's `"atom"` or `"theorem"`. Format 0.5.0 adds more fields of the
same nature — the kind of an admonition (`"warning"`, but also an
author's `"example"`), the kind of an inline mark, the kind of a table —
and existing ones already behave this way without saying so: `code.lang`,
`raw.format`, `source.type`.

Each field had its own unwritten rules: whether a producer may invent a
value, what a consumer does with one it does not know, how a common
invented value becomes standard. A closed enum would force a
specification revision for every new producer or author convention. A
plain string with no contract leaves producers minting near-duplicates
(`"warn"` beside `"warning"`) and consumers failing in different ways.

## Decision
These fields are **open vocabularies**, and they share one contract
(docs/proposals/0013, principle 3):

1. **Known values** have a definition in the specification. A producer
uses a known value whenever one fits; it does not mint `"warn"` when
`"warning"` exists.
2. **Free values** are any other string, written as is.
3. **Consumers never fail on an unknown value.** Each open field
documents its fallback — ignore the value, treat it as unstated, or
render a generic form.
4. **Promotion.** A free value in wide use becomes a known value in a
minor version.

The open vocabularies of format 0.5.0 are `figure.kind`,
`admonition.kind`, `Mark.kind`, `table.kind`, `code.lang`, `raw.format`,
`source.type`, and the keys of `doc.extra`.

Closed enumerations stay closed where a consumer must understand the
value exactly to process the data. The node `type` discriminator is one:
an unknown node type cannot be read.

## Consequences
- **No `x-` prefix.** A prefixed free value has to be renamed everywhere
on the day it becomes standard; that is the lesson of RFC 6648, which
deprecated the practice for protocol parameters. A free value is
written as is, and promotion changes nothing in existing files.
- **Every open field documents its fallback** in the specification
(spec §6.0). A field added later as an open vocabulary must come with
one, or it is not open.
- **Promotion happens in a minor version**: a new known value adds a
definition and never changes the meaning of an existing file.
- The schema types these fields as plain strings. The known values live
in the specification and, in the reference SDK, in constants such as
`KNOWN_MARK_KINDS` and `KNOWN_ADMONITION_KINDS`; validation never
rejects a free value.
58 changes: 58 additions & 0 deletions docs/adr/0027-no-compatibility-guarantee-before-1-0.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
---
title: No compatibility guarantee before format 1.0.0
status: accepted
date: 2026-10-13
tags: [versioning, compatibility, policy]
related: [0015, 0022]
superseded-by: null
---

# ADR 0027 — No compatibility guarantee before 1.0.0

## Status
Accepted.

## Context
The format has moved in minor versions so far: 0.3.0 reworked fields and
made the link families label-keyed, 0.4.0 added a family. Each change
was weighed partly on what it would break, and 0.4.0 was kept additive.
That habit was never written down as a policy, so it read as an implied
promise.

Format 0.5.0 (docs/proposals/0013) redesigns the node model: prose
becomes rich text, the link families move into texts, and containers
hold block nodes. An additive version — keep the plain `text` strings,
add marks beside them — was considered and is cheaper to migrate, but
it leaves the underlying problems standing and needs a locator for
every multi-text node. Choosing it to preserve compatibility would trade
the format's design for continuity with a model that is not yet right.

The format is young. ADR 0022 already treats it as not frozen: Python
stays the reference until the format freezes. Consumers already pin to
a release tag (spec §11), and a CND is an immutable build artifact
(ADR 0015) that a producer rebuilds rather than edits.

## Decision
**Until `cnd_version` 1.0.0, a minor version may break anything** — field
names, node shapes, validation rules, content hashes. Design choices are
made on merit, not on how much they preserve. Consumers pin release
tags, and move to a new version by pinning its tag once their producer
emits it.

The policy is stated in the specification (spec §11) so that it is
explicit, not inferred.

## Consequences
- **No upgrader is promised** between pre-1.0 versions. One may be
written when it would carry information across; format 0.5.0 ships
none, because a 0.4 CND holds none of the new information and an
upgrade would only reshape it.
- Every breaking minor version states what it breaks in spec §11, as
0.5.0 does.
- Content hashes are not comparable across pre-1.0 versions. In-process
reconciliation (ADR 0018) is unaffected: it hashes both sides with the
same library.
- The guarantee a consumer can rely on before 1.0.0 is the pinned tag:
a given release always reads and writes the same format.
- Reaching 1.0.0 is a separate decision, with its own ADR, and is when a
compatibility policy for minor versions is written.
Loading
Loading