Reference Python implementation of CND (Context Native Document) — a format for representing compiled documents as a tree of typed, cross-referenceable nodes, designed for retrieval and LLM-context pipelines.
This package is the standard itself: the CND schema, node types, and a base display/visitor layer. It does not include any indexing, chunking, or storage logic — those are consumer concerns built on top of this contract.
pip install cnd-sdk
# with the optional Rich-based display helpers:
pip install cnd-sdk[display]from cnd import Cnd, MarkdownRenderer
cnd = Cnd.model_validate_json(open("doc.cnd").read())
renderer = MarkdownRenderer()
for traverse in cnd.iter():
print(traverse.ctx.depth, traverse.node.type, renderer.render(traverse.node))Converting a whole CND into one standalone document — a renderer handles
one node, a converter handles the document (front matter, sections in
reading order, footnote and bibliography sections with their markers
resolved). Conversion is one-way and lossy by construction; each
converter's docstring says what it drops, and warnings reports what this
particular document lost:
from cnd.converters import HtmlConverter, MarkdownConverter
result = MarkdownConverter().convert(cnd)
open("doc.md", "w").write(result.text)
print(result.warnings)
open("doc.html", "w").write(HtmlConverter().convert(cnd).text)Pretty-printing a CND tree (requires cnd-sdk[display]):
from cnd.visitors.node_display_visitor import NodeDisplayVisitor
NodeDisplayVisitor().visit(cnd)src/cnd/typst/cnd.typ is the official pure-Typst authoring package. Map or
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]), 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.
A reference to a label inside it, or on it, finds nothing: decorative content
is not part of the document.
Import the whole file rather than destructuring — #import "/cnd.typ": table
would shadow Typst's own built-in table.
It works on any stock Typst toolchain — the only contract is the
cnd.metadata state key that CND emitters read. From Python, the file is
available as importlib.resources.files("cnd") / "typst" / "cnd.typ".
See ADR 0023.
The cnd command is the executable oracle of the fixture corpus — the tool
to reach for when implementing CND in another language and asking "does my
implementation agree?".
cnd validate doc.cnd # the invariants JSON Schema cannot express
cnd hash doc.cnd --nodes # the reference content hashes
cnd diff old.cnd new.cnd # what moved between two builds
cnd build doc.decl.yaml # compile a declaration into a CND (needs [yaml] for YAML)
cnd inspect doc.cnd # readable tree trace (needs [display])Exit code 0 means conformant, 1 means it is not (or the file did not
parse). --json on validate/hash gives machine-readable output, since
comparing two implementations is a diff rather than a read.
This is all five conformance verbs
docs/adr/0020 specifies.
Agreeing with these is necessary for conformance, not yet sufficient — and
the corpus matches, carrying all four vector kinds the same ADR requires
(hashes.json, traversal.json, matching.json, and the
declaration → CND pairs under fixtures/declaration/). cnd declare
(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 v2 is a versioned
reference algorithm, not a normative rule
(docs/adr/0018;
v2 itself is docs/adr/0029).
Reproducing matching.json proves an implementation runs v2 — it is not
something the format requires of anyone.
A CND is an immutable build artifact and its node ids are not durable, so
"is this the same node as last build?" is answered after the fact, by
matching two CNDs
(docs/adr/0018):
from cnd.reconcile import diff
report = diff(previous, current)
[change.new.node.text for change in report.changed]Id inheritance is the other half — the same matching, consumed instead of reported:
from cnd.reconcile import reconcile
result = reconcile(current, previous) # current, with previous' idsThe matching is a versioned reference algorithm, not part of the
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): 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.
The full CND specification lives in spec/cnd-spec.md,
with the JSON Schema at
schema/cnd.schema.json
and shared test fixtures in fixtures/.
Apache-2.0 — see LICENSE.