Skip to content

CND 0.5.0 production: declaration 0.2.0, builder, matching v2, CLI, visitors - #12

Merged
Maxanox merged 25 commits into
format-0-5-0from
cnd-40-produce-cnd-0-5-0
Oct 14, 2026
Merged

Maxanox merged 25 commits into
format-0-5-0from
cnd-40-produce-cnd-0-5-0

Conversation

@Maxanox

@Maxanox Maxanox commented Oct 14, 2026

Copy link
Copy Markdown
Contributor

This is the last plan of the CND 0.5.0 series. It ports everything that was still on the old format, and leaves no test module parked. With it merged, format-0-5-0 is ready for main; only the release steps remain.

What changes

  • Writer. Cnd.to_json() writes a CND without its default-valued fields. Reading the output back gives the same CND and the same content hash. Every CND the SDK writes goes through it.
  • Declaration 0.2.0.
    • A text can be written as a plain string or as the full rich-text object, which can carry hyperlinks.
    • The numbering name is authored, and a nested numbering.value is refused.
    • Every value object is strict.
    • A 0.1 declaration is refused with an error that names its version.
  • Builder.
    • It mints ids for every node, including content nested in containers and footnotes.
    • It numbers the full walk.
    • It threads heading_path through containers.
    • It reports numbering-without-engine when an authored numbering cannot be resolved.
    • It never shares a mutable object with the declaration.
  • Version checks. A CND or declaration of another version is refused before its shape is checked, by the CLI loaders and by validate() (rule cnd-version-unsupported).
  • Reconciliation, matching v2 (ADR 0029, extends 0018).
    • It walks the full tree, uses the spec §8 paths, and keys footnote content by its note's label.
    • Containers are keyed on their own hash plus a digest of their contents, so twin containers keep their own ids.
    • A last container-hash pass pairs a container whose content was edited and which also moved.
    • The vectors gain cases 08–11.
  • Visitors and display.
    • BaseVisitor dispatches every 0.5 node type. visit(cnd, full=True) also walks footnotes, with spec paths.
    • cnd inspect shows the new nodes, and never reads CND text as Rich markup.
  • CLI. Every command works on 0.5 documents. hash --nodes lists the full walk, as hashes.json does.
  • Typst package. cnd.table(kind: …) replaces content_kind:. The hint is stored under its own state key, table-kind, so author metadata is never touched.
  • Closing the series.
    • Every vector is regenerated and reproducible.
    • The parking list is gone.
    • Spec §12 is rewritten and proposal 0013 is marked implemented.
    • The CHANGELOG has an [Unreleased] entry.

Review

  • Every task had its own review. Reconciliation needed four fix rounds and the display one.
  • The final whole-branch review ran the full production path on a declaration using every 0.5 element: declaration, build, write, validate, hash, Markdown, HTML, rebuild, reconcile. Nothing was lost and every id was kept. Its findings were fixed in one wave and re-reviewed.
  • 1155 tests pass. The Typst compile tests run when typst is installed.

Before main

Two release steps remain:

  • bump pyproject to 0.9.0;
  • tag the release.

Follow-ups: CND-26, CND-50, CND-51.

Part of CND-40

🤖 Generated with Claude Code

https://claude.ai/code/session_01XxXUiDn2QyzNTYnZpeVFVS

Maxanox and others added 25 commits October 14, 2026 11:26
Proposal 0013 lets a CND omit every field that holds its default. The
writer keeps id, required fields and free-form objects; the round trip
and the content hash are pinned over the whole fixture corpus.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XxXUiDn2QyzNTYnZpeVFVS
…eclaration

Texts take the CND's RichText or a plain string; the link families live
in texts, so hyperlinks are declarable; numbering keeps only the
authored name; list items, term items, cells and footnotes hold
declared nodes and are strict. Version 0.2.0: a 0.1.0 declaration is
refused rather than misread.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XxXUiDn2QyzNTYnZpeVFVS
…ilder

Every nested node is minted an id; heading_path is threaded through
every container; the counter engine numbers headings, figures and
equations in full-walk order and keeps only authored words. Authored
numbering with the engine off is a build error
(numbering-without-engine), never a silent drop. Goldens are written
through Cnd.to_json.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XxXUiDn2QyzNTYnZpeVFVS
…est_reconcile

Matching v1 keeps its passes and keys; the node set is the full walk and
the structural path is the spec section 8 path, with footnote content
keyed by the note's label so a pool reorder moves nothing. The remap and
its guards reach every slot and the footnotes. New vector cases: an
edited list item, two cells as two slots, footnote content.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XxXUiDn2QyzNTYnZpeVFVS
…s id

MATCHER_VERSION is now v2: the same 0.5 inputs pair differently from the
previous matcher (spec section 8 path, full walk, footnote content keyed
by its note's label). A node hash excludes nested nodes, so two
containers of one shape hash alike; in the two hash passes a node with
child slots is keyed on its node hash plus a digest of its descendants'
hashes. The spec node hash is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XxXUiDn2QyzNTYnZpeVFVS
A heading whose section changed has not itself changed (spec section 10),
so a moved heading must stay recognisable. The nested-content digest
applies only to list, terms, table, layout, quote and admonition, whose
own hash carries little content; headings and figures already hash their
text or caption. Restores the original moved-into-a-section expectation.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XxXUiDn2QyzNTYnZpeVFVS
A container whose nested content was edited and which also shifted
position missed every earlier pass and lost its id. After path+type, the
still-unmatched list, terms, table, layout, quote and admonition pair on
their own node hash without the digest; twins have already paired through
the digest. Also fixes a stale "v1" in a test docstring.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XxXUiDn2QyzNTYnZpeVFVS
matching.json is the cross-implementation oracle, so every pass needs a
vector. Case 11: a table with one cell edited and a paragraph inserted
above it pairs on its node hash alone.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XxXUiDn2QyzNTYnZpeVFVS
…test_visitor

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XxXUiDn2QyzNTYnZpeVFVS
…itor

Admonition and layout panels, rich-text details (marks, every link
family read from the node's texts, hyperlinks by href), numbering as
number and counter, task-list counts, code titles, figure descriptions,
document status, version and extra, and footnote blocks in the pool
panel.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XxXUiDn2QyzNTYnZpeVFVS
The document panel and the pool panels passed CND values (title, status,
version, extra, footnote and bibliography text) to Rich as plain strings,
which Rich parses as markup: a stray closing tag crashed the render. They
are wrapped in Text now. The document grid also gets a column gap.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XxXUiDn2QyzNTYnZpeVFVS
hash --nodes lists the full walk, as fixtures/hashes.json does; build
writes through Cnd.to_json; inspect displays every 0.5 node type.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XxXUiDn2QyzNTYnZpeVFVS
…hyperlinks; empty parking list

Every test module runs again. Spec section 12 states the shorthand, the
authored numbering name and the build error without the engine, the
moved strictness boundary, and that hyperlinks are declarable through
rich text. The CND-26 wording items are fixed (paragraph example id,
"rendered text"). Proposal 0013 notes that the authoring hint rename
supersedes the content_kind part of ADR 0023.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XxXUiDn2QyzNTYnZpeVFVS
Every read_text and write_text of CND or declaration content in the CLI
and the fixture scripts names its encoding, so a non-UTF-8 locale can
neither misread a file nor write one another implementation cannot read.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XxXUiDn2QyzNTYnZpeVFVS
… shape

A 0.1 declaration failed on "Extra inputs" errors that never named its
version, and `cnd validate` printed ok for a CND stamped 0.4.0 or 9.9.9.

- The CLI loaders read cnd_version and declaration_version from the raw
  data first and refuse another version by name, with the supported one.
- Declaration refuses another declaration_version in a before-validator,
  so a caller of model_validate gets the same single error.
- validate() gains cnd-version-unsupported for a CND built in code with
  another version; spec section 9 lists it.

Tests use main's own minimal.cnd (stamped 0.4.0, a shape 0.4 parsed
unchanged) and article.decl.yaml (0.1.0) as real old documents.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XxXUiDn2QyzNTYnZpeVFVS
build() handed source, doc, raw and nested state_metadata values to the
CND as the declaration's own objects. Mutating the declaration after a
build silently edited the earlier CND, and diff(c1, c2) then reported
nothing changed. build() now works on a deep copy of the declaration.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XxXUiDn2QyzNTYnZpeVFVS
After `cnd.metadata.update(it => it + (kind: "report", revision: "1"))`
and one `cnd.table(..., kind: "data")`, the state was `(revision: "1")`:
the hint's cleanup removed the author's document-level kind, and an
inner hint removed an outer one. The hint now saves the previous value
in a context before its update and restores it after the table, or
removes the key only when it was absent. The body stays outside the
context, at the call site.

The new tests compile a document with `typst query` and skip when the
typst binary is missing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XxXUiDn2QyzNTYnZpeVFVS
…aths

The BaseVisitor docstring told users to walk cnd.footnotes[i].children,
which yields ("nodes", 0) paths that name a body node. visit() gains
full=True, which runs Cnd.iter_all(): the body, then each footnote's
content, with spec section 8 paths and slot "footnotes". The default
stays the body walk. iter_all() now takes max_depth and stop_predicate,
so should_stop_descent prunes inside footnotes too.

Spec section 8 no longer claims `cnd inspect` shows the order a CND
walks in: it shows the body walk only.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XxXUiDn2QyzNTYnZpeVFVS
…declaration 0.2.0 only

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XxXUiDn2QyzNTYnZpeVFVS
ADR 0029 extends ADR 0018 and records what matching v2 computes. It
covers the full walk, spec section 8 paths, footnote content keyed by
its note's label, and the container key: node hash plus a descendants
digest, with headings and figures left out. It also covers the last
pass, empty containers and the known limits. The docs index marks 0018
"extended by 0029".

The last pass, which pairs leftover containers on their node hash alone,
now reports matched_by "container-hash" (PASS_CONTAINER_HASH) instead of
"hash", so a caller can tell it from an unchanged move. In
matching.json, the one changed hunk is case 11's table. The match.py
docstring's numbered passes now match its prose. The README, the CLI and
the diff docstring point at ADR 0029.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XxXUiDn2QyzNTYnZpeVFVS
The author still writes `cnd.table(..., kind: "data")`. The state now
carries the hint under the dedicated key `table-kind`, which an emitter
reads and the CND writes as `table.kind`. A document-level `kind` the
author sets therefore never collides with the hint. The hint keeps the
plain set-then-unset pattern: nothing is read back, so no context
update has to settle.

Compile tests run with the typst binary, and skip without it:
- five sequential hinted tables, failing on any convergence warning;
- a document-level kind that survives two hinted tables, while each
  table sees its own table-kind.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XxXUiDn2QyzNTYnZpeVFVS
…state key

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XxXUiDn2QyzNTYnZpeVFVS
@Maxanox
Maxanox merged commit 508e516 into format-0-5-0 Oct 14, 2026
3 checks passed
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