mortie specification & conventions (v1.0)
This page is the normative record of the mortie 1.x stability contract: the packed-word encoding, the decimal string grammar, the morton-hive store layout, the coverage-MOC serializations, and the zarr DGGS convention block for morton-declared stores. Everything marked contract here is frozen for the 1.x major-version series; anything not marked contract is informative.
Design rationale — why each decision was made, with trade studies and
ratification records — lives in zagg's
docs/design/sparse_coverage.md
(the decisions registry, D-numbered entries). That document cites this page;
this page is the spec. Grammars and constants are normative here only —
duplicated normative text drifts.
Contents:
- The packed 64-bit morton word
- Decimal string representation
- Resolution table
- Order-29 points: encoding-carried kind and the decimal-parse tie-break
- Zarr DGGS convention block
- Morton-hive store layout
- Coverage MOC serializations
- Frozen for 1.x
1. The packed 64-bit morton word
Contract. A morton index is one unsigned 64-bit word encoding a HEALPix
NESTED cell (or an order-29 point) with its order carried intrinsically.
Source of truth in code: src_rust/src/decimal_morton.rs (MAX_ORDER = 29,
BODY_TUPLES = 27).
Bit layout (MSB → LSB)
[ 4-bit prefix ][ 54-bit body (27 x 2-bit) ][ 6-bit suffix ]
63 .. 60 59 .. 6 5 .. 0
- prefix (bits 63–60) — the HEALPix base cell stored as
base_id + 1, so the 12 base cells occupy1..=12.0is the empty/null sentinel;13..=15are invalid. The+1shift is monotonic, so a raw unsigned sort is preserved as a Z-order curve. - body (bits 59–6) — 27 two-bit tuples, one per order
1..=27. Order 1 occupies the highest tuple (bits 59–58), order 27 the lowest (bits 7–6). The stored value is0..=3, interpreted as1..=4(a decode-time+1, matching the decimal grammar in §2). - suffix (bits 5–0) — one plain unsigned integer
0..=63, a preorder numbering of the path tail past tuple 27:
| suffix | meaning |
|---|---|
0..=27 |
variable-length area element; the order is the suffix value (0 = base-cell-only). 27 = order-27 with empty tail. |
28..=47 |
order-28/29 area cells in parent-first preorder: r = t28*5 + (t29_present ? t29 + 1 : 0), suffix = 28 + r, with t28/t29 the stored 0..=3 tuple values. Each t28 owns a 5-block: [t28] then its four [t28,t29] children. |
48..=63 |
order-29 point, max-encoded (no area claim): r2 = t28*4 + t29, suffix = 48 + r2. A point sorts after every area cell sharing its body (highest suffix range) — the finest, last thing there. |
- Canonical zero-fill: every bit below an element's order is zero-filled, so two encodings of the same cell are bit-equal — integer equality, hashing, dedup, and the raw sort all work on the word directly.
- Unsigned storage: the word is stored and exchanged as
uint64. The signed "negative = southern" form is a presentation detail of the decimal string (§2), never a storage form. Reinterpreting the word asint64is an error: base cells 7–11 set bit 63 and would read back negative. - Z-order: a raw unsigned sort of packed words is a Z-order (Morton) curve traversal, parent-before-children across the whole order range 0–29.
Kind: area vs point
A decoded word is either an area element (a real cell with spatial extent, orders 0–29) or an order-29 point (a location cast to maximum resolution with no area claim — e.g. a raw lat/lon conversion). The kind is carried by the suffix range, not by any external flag — the load-bearing convention §4 states normatively — and the only place the two kinds can collide is the order-29 decimal string, resolved by the §4 parse tie-break.
2. Decimal string representation
Contract. The decimal string is the render/interchange external form of a
morton index (paths, logs, inventories, display). Packed uint64 words are
the storage and compute form; output types are never data-dependent:
- strings for display/interchange at every order;
- packed
uint64for storage and compute; - legacy signed
i64only via the explicit, cappedto_legacy_i64()escape hatch.
Grammar:
morton-decimal = ["-"] base-digit *order-digit [kind-suffix]
base-digit = "1" / "2" / "3" / "4" / "5" / "6"
order-digit = "1" / "2" / "3" / "4"
kind-suffix = "p" ; POINT ids only; render/interchange form only
- Sign + base digit form a constant-width component with 12 values
(
1..6/-1..-6; the sign renders the southern base cells). One order-digit follows per order; digits are1-4, never0. The string length minus the sign/base component is the order. - String prefix = spatial ancestor at every level:
-31123is a descendant of-311, and lexicographic grouping under a fixed-width prefix is spatial grouping. - The base component grammar is
-?[1-6]— reserved wherever names must be distinguishable from morton components (see §6.5). - Kind suffix
p(espg-ruled in review, 2026-07-21): the render form MAY carry a terminalpon point ids (e.g.-62…21p), and only there — apis legal solely on a full order-29 string, because points exist only at order 29. Paths never carry it: points don't live in paths, and the §6 path grammar is unchanged. Rationale: no letter occurs anywhere in the base grammar (sign, base digit, digits1-4), so a letter suffix is maximally distinguishable, greppable, and inert in shells, markup, and URLs;*and a terminal.were considered and rejected (glob/markdown hazards; trailing-dot stripping).
Order-29 kind marking and the unmarked tie-break
With the kind suffix, the decimal round-trip is lossless for both
kinds — the normative contract: an area word renders unmarked and parses
back to itself; a point word renders p-marked and parses back to itself.
The p emit/accept is implemented in
PR #121 (issue #120), with
golden vectors. The residual ambiguity
is only the unmarked order-29 string — path components and legacy
renders — which denotes both kinds and parses as the area word (the
normative §4 tie-break; every pre-suffix string is an area context, so the
rule is fully backward compatible).
3. Resolution table
Informative (the formulas are contract; the rendered values are derived).
nside = 2^order; both columns derive from one Earth model — the exact
HEALPix sphere at mean radius R = 6371.0088 km. Every order-k HEALPix
cell has identical area 4πR² / (12 · 4^order) (HEALPix is equal-area by
construction); the cell scale is the square root of that area — the RMS
cell spacing sqrt(4πR² / (12 · 4^order)). The table below is regenerated
from these formulas and pinned by mortie/tests/test_spec_page.py so it
cannot drift.
Note — code and page unified. These are the normative, sphere-derived
values, and mortie.tools.order2res now derives from the same sphere:
order2res(order) = sqrt(4πR² / (12 · 4^order)) with the single
mortie.tools.EARTH_RADIUS_KM = 6371.0088 constant. Its consumers
(res2display and the buffer-pad computation in
tests/test_coverage_boundary.py) therefore read the cell-scale column
below directly. This replaced the historical flat constant
111 km/deg × 58.6323 × 0.5^order (an implied sphere R ≈ 6366 km), a
behavioral change of ~0.2% at every order, per
mortie #119.
| order | nside | cell scale | cell area |
|---|---|---|---|
| 0 | 1 | 6,519.623 km | 4.25055e+07 km2 |
| 1 | 2 | 3,259.812 km | 1.06264e+07 km2 |
| 2 | 4 | 1,629.906 km | 2.65659e+06 km2 |
| 3 | 8 | 814.953 km | 664,148 km2 |
| 4 | 16 | 407.476 km | 166,037 km2 |
| 5 | 32 | 203.738 km | 41,509.3 km2 |
| 6 | 64 | 101.869 km | 10,377.3 km2 |
| 7 | 128 | 50.935 km | 2,594.33 km2 |
| 8 | 256 | 25.467 km | 648.582 km2 |
| 9 | 512 | 12.734 km | 162.146 km2 |
| 10 | 1024 | 6.367 km | 40.5364 km2 |
| 11 | 2048 | 3.183 km | 10.1341 km2 |
| 12 | 4096 | 1.592 km | 2.53352 km2 |
| 13 | 8192 | 795.852 m | 633,381 m2 |
| 14 | 16384 | 397.926 m | 158,345 m2 |
| 15 | 32768 | 198.963 m | 39,586.3 m2 |
| 16 | 65536 | 99.482 m | 9,896.58 m2 |
| 17 | 131072 | 49.741 m | 2,474.15 m2 |
| 18 | 262144 | 24.870 m | 618.536 m2 |
| 19 | 524288 | 12.435 m | 154.634 m2 |
| 20 | 1048576 | 6.218 m | 38.6585 m2 |
| 21 | 2097152 | 3.109 m | 9.66463 m2 |
| 22 | 4194304 | 1.554 m | 2.41616 m2 |
| 23 | 8388608 | 77.720 cm | 0.604039 m2 |
| 24 | 16777216 | 38.860 cm | 0.15101 m2 |
| 25 | 33554432 | 19.430 cm | 0.0377525 m2 |
| 26 | 67108864 | 9.715 cm | 0.00943811 m2 |
| 27 | 134217728 | 4.857 cm | 0.00235953 m2 |
| 28 | 268435456 | 2.429 cm | 0.000589882 m2 |
| 29 | 536870912 | 1.214 cm | 0.000147471 m2 |
4. Order-29 points
Contract. Kind is carried by the encoding itself — never by store or array metadata. The packed word's suffix region (§1) is the kind:
- suffix
0..=47decodes as an area word — an exact cell at its encoded order, at every order 0–29. An order-29 area word is a genuine order-29 cell; nothing is unrepresentable. - suffix
48..=63decodes as an order-29 point — a location with no area claim, full stop.
There is no resolution/kind field in the §5 attrs block and readers never
consult a declaration: two encodings, two meanings, one word. Mixed content
— exact cells at any orders alongside order-29 points — is well-formed in a
single coordinate by construction, because each word carries its own kind.
(Provenance: espg-ratified 2026-07-21 on the PR #118 review, superseding
the drafted declaration-based designs.)
The decimal-parse tie-break (contract)
Packed words never collide across kinds — the suffix ranges are disjoint. The one ambiguity in the convention is the decimal repr at order 29: a full order-29 string (base component + 29 digits) denotes both the order-29 area cell and the max-encoded point on the same path, because the string carries path only, never kind (§2). Strings at orders 0–28 denote area cells alone (points exist only at order 29). The normative tie-break:
A
p-marked string yields the POINT word; an unmarked string always yields the AREA word. For an order-29 path with final two (stored) tuple valuest28,t29: unmarked ⇒ the area word (suffix28 + t28·5 + t29 + 1);p-marked ⇒ the point word (suffix48 + t28·4 + t29). Round-trip identity holds for both kinds; the unmarked-string rule is the tie-break for the one truly ambiguous form (§2), and since every pre-suffix string is unmarked it is fully backward compatible.
In channels that strip or cannot carry the suffix (paths above all),
point-ness does not survive the string: those channels carry the packed
word when kind matters. The unmarked tie-break is what mortie's parser has
always implemented, golden-pinned by mortie/tests/test_spec_page.py; the
p emission/acceptance is implemented in
PR #121 (issue #120), with
golden vectors.
Parse-side API (normative surface)
The tie-break above is a parse-side contract, so it is worth stating in
the terms a parse caller sees. The public entry points, all implementing the
same kernel (decimal_morton::from_decimal_repr):
| Entry point | Shape |
|---|---|
mortie.decimal_to_word(s, dtype=np.uint64) |
scalar; numpy-only (no pandas import), dtype selects np.uint64 / int / MortonIndexScalar |
mortie.decimals_to_words(arr) |
vectorized; the inverse of MortonIndexArray.to_decimal(), shape-preserving, uint64 out |
MortonIndexArray.from_decimal(arr) |
the same parse as an ExtensionArray constructor, the inverse of .to_decimal() |
What a parse-side caller must know: an unmarked order-29 id parses to the
area word, so a point word does not round-trip through an unmarked string.
Emit renders point words p-marked, so word → to_decimal → from_decimal
is the identity end to end — but any channel that strips the marker (a
path component, a legacy render, a hand-typed id) returns the area word for
what may have been a point. At orders 0–28 there is no ambiguity to resolve.
Pinned by mortie/tests/test_decimal_parse.py.
Points at coarser levels (informative)
Membership of a point in a coarser cell is the ordinary truncation
(coarsen / clip2order: drop the path tail below the target order) — a
transient cast computed where needed, never a stored re-encoding.
Viewer-side float64 casts (informative)
IEEE-754 float64 (JavaScript Number, plain JSON parsers) is
integer-exact only to 2^53, which covers NESTED ids only through order 24.
A display layer in such a runtime (e.g. the gridlook viewer) may
transiently cast point ids to order ≤ 24 for Number safety — an
implementation detail of that display layer, not encoding semantics; other
viewers and future runtimes need no such cast. Area cells are never
coarsened this way: coarsening an area cell changes the labelled thing
(that is aggregation, cf. zagg D24) — servers fabricate or aggregate
instead.
5. Zarr DGGS convention block
Contract. A zarr store whose cell coordinate is packed morton words declares, on the group holding the cell-indexed arrays:
{
"zarr_conventions": [
{
"schema_url": "https://github.com/espg/mortie/blob/main/docs/specification.md#dggs-attrs",
"spec_url": "https://github.com/espg/mortie/blob/main/docs/specification.md",
"uuid": "3e22156d-ea9e-4e01-95fe-e3809a4b41e7",
"name": "morton-dggs",
"description": "Packed-u64 morton (HEALPix) DGGS convention"
}
],
"dggs": {
"name": "morton",
"coordinate": "morton",
"...": "grid parameters (refinement level, ellipsoid, ...)"
}
}
name: "morton"andcoordinate: "morton"— the grid name is distinct, nevername: "healpix"+indexing_scheme: "morton". A scheme-blind reader that recognizeshealpixbut ignores the indexing scheme would silently decode morton words as NESTED ids and mis-place every cell; an unknown grid name makes it hard-reject with a diagnostic instead. This matches moczarr's xdggs registration (grid_name: "morton").- There is no kind/
resolutionfield: point-vs-area kind is carried by the word encoding itself (§4), never by attrs. - Convention identity — the
zarr_conventionsentry above is the self-declared convention record (the zarr-conventions mechanism supports self-declared entries). The UUID3e22156d-ea9e-4e01-95fe-e3809a4b41e7is minted once and permanent; readers may key on it.zarr_conventionsis a list: a future upstream dggs-registry entry coexists alongside this one rather than replacing it.
6. Morton-hive store layout
Contract. The morton-hive layout stores one self-describing zarr per
spatial shard under a morton digit tree. The convention is versioned by the
manifest's spec string (morton-hive/1, /2, /3); every frozen
grammar remains valid forever for stores declaring its version — readers
discriminate by the spec string, never by sniffing names.
6.1 Common structure (all versions)
{store_root}/
morton_hive.json <- static manifest, versioned `spec` string
coverage.moc <- optional root coverage MOC (§7.3)
{sign+base}/{d1}/{d2}/.../ <- one decimal digit per path level
{leaf}.zarr/ <- vanilla zarr v3 leaf (naming per version)
<declared sidecars> <- stats record, sub-shardmap (per version)
- Digit tree: path components are the decimal-string components of §2 —
the constant-width
{sign+base}first, then one digit ([1-4]) per order. Shards live at mixed orders, so every order is a legal node. A manifestpath_groupingparameter (default1) declares how many digits each component chunks; readers chunk the digit string per the manifest, never by assumption. - Grouping remainder (contract): the
{sign+base}component stands alone and never participates in grouping —path_groupingchunks only the order-digit string that follows it. When the order is not a multiple ofpath_grouping, the leading digit components are full-width and the final component carries the remainder (never remainder-first). This is the only split that keeps a coarser shard's grouped prefix a directory-prefix of its finer descendants, so shared ancestor directories survive across mixed shard orders. Worked example — order 8 atpath_grouping: 3chunks the eight order-digits33142241as3+3+2(331/422/41), leaving{sign+base}4as its own leading component:
4/331/422/41/433142241.zarr <- id 433142241, order 8, path_grouping: 3
- Node invariant: below a product root, a node contains only digit
children,
*.zarrobjects, and the declared leaf-adjacent sidecar names — nothing else, ever. The walker's child classification depends on the name set being closed. - Termination: object stores have no empty prefixes and S3 LIST is strongly consistent, so a delimiter-LIST returning no digit children is a definitive "nothing finer exists". Absence is trustworthy.
- Commit stamp: presence is not trustworthy without one. A leaf's root
metadata is finalized last (a root-group attrs update carrying the
stamp); a
.zarr/prefix whose root metadata lacks the stamp is debris — incomplete, ignorable, safe to overwrite on retry. - Overview flagging: ancestor-node zarrs must carry
role: overviewattrs; the role is never inferred from tree position (a shallow zarr may be coarse source in a sparse region). - Manifest:
morton_hive.jsonat the (product) root is the reader's bootstrap; with it every shard path is computable arithmetically with zero requests. Itsspecstring versions the convention.
6.2 morton-hive/1 — bare leaves
Leaf basename is the full morton id: {full_id}.zarr (e.g.
.../1/2/3/-31123.zarr/). Self-describing without parsing its path.
6.3 morton-hive/2 — time-windowed leaves
A /2 store's manifest carries a temporal block declaring time
encoding/units/epoch/calendar, the membership timestamp field, the window
schedule, and the append policy. A /1 store is a /2 store with
schedule: none.
Leaf naming:
{full_id}_{window}.zarr <- windowed leaf (schedule != none)
{full_id}.zarr <- bare leaf (schedule == none only)
- Separator is
_; it never appears in morton ids nor in window labels, so the split is unambiguous. Parse rule: split on the first_. - One schedule per store; at most one leaf per (id, window); bare and windowed source leaves never mix in one store.
Window schedules and label grammar (lexicographic order = chronological order within a store):
| schedule | label grammar | example label | window |
|---|---|---|---|
none (default) |
(no label) | — | unbounded; re-run replaces the leaf |
yearly |
YYYY |
2025 |
[2025-01-01T00:00Z, 2026-01-01T00:00Z) |
monthly |
YYYYMM |
202511 |
calendar month |
daily |
YYYYMMDD |
20251103 |
calendar day |
quarterly (grammar-reserved, not implemented) |
YYYYQ[1-4] |
2025Q3 |
calendar quarter |
| explicit list | opaque, [0-9A-Za-z-]{1,32} |
melt-2019 |
declared per label in the manifest |
- Boundaries are UTC calendar terms, half-open
[start, end), regardless of the store's native time encoding (the temporal block declares the conversion). An observation stream straddling a boundary contributes to both windows. - Explicit labels are opaque: the manifest maps each label to its
[start, end); readers never parse semantics out of a custom label. The charset excludes_by construction. - The reserved token
all(§6.4) is excluded from the window-label grammar going forward: no store written under any spec version may declare an explicit window labeledall. In/2the token has no structural role — theschedule: noneleaf is the bare{full_id}.zarr, notall.zarr(that is a/3construct) — so the reservation is a forward-going constraint on new writers, not a retroactive narrowing of the frozen/2opaque grammar. A pre-existing/2store whose manifest happened to declare analllabel stays readable under its frozen grammar:/2labels are opaque and manifest-resolved, soallthere is just another custom label. - Per-shard stats sidecars (when written):
stats_{window}.json, orstats.jsonunderschedule: none.
6.4 morton-hive/3 — window-only leaf naming
Completes the axis separation: product = root prefix (§6.5), space = digit path, time = basename — each identity axis in exactly one place.
{window}.zarr <- leaf, basename = time window alone
all.zarr <- schedule: none (reserved token)
- The morton id no longer appears in the basename; it is recoverable
arithmetically from the digit path and recorded in the leaf's stamp
attrs / stats sidecar (
shard_key). allis a reserved token: it names the/3schedule: noneleaf (reads as "all time"), cannot collide with the digit-shaped generative labels, and is excluded from the window-label grammar going forward (no store, any spec version, may declare an explicit window labeledall). The token is structural only in/3; §6.3 records how the same forward-going reservation applies to/2, which has noallleaf.- Sidecar naming aligns to the leaf:
{window}.stats.json/all.stats.json. /1and/2stores keep their frozen grammars (§6.2, §6.3) forever; readers discriminate by the manifestspecstring.
6.5 Product roots and the product-name grammar
A multi-product store is a directory of stores: each product lives under
its own human-readable root prefix {name}/, and a product subtree is a
complete, unmodified morton-hive store (bare-named manifest, MOC, digit
tree). A bare single-product store (manifest at the store root) remains
fully valid; readers distinguish the two root forms by content — a
manifest at the root ⇒ bare store; name-shaped prefixes ⇒ product
directory.
Product-name grammar (contract):
product-name = 1*192( lowercase-alphanum / "-" / "_" ) ; [a-z0-9_-]{1,192}
with the base-component exclusion: a product name must not match the
morton base-component grammar -?[1-6] (§2), so the walker's child
classification stays unambiguous. Names are URL-safe by construction (no
percent-encoding, no case-folding hazards).
- Length 1–192 characters. The charset is single-byte ASCII, so 192
chars = 192 bytes. Derivation: the POSIX filename-component ceiling is 255
bytes; a D23 downloader that materializes the tree locally shares the
product's path component with a 13-character immutable-provenance
decoration (
{name}+{catalog-hash}/— a+plus a 12-hex catalog fingerprint), leaving a hard ceiling of255 − 13 = 242. The 192 cap sits 50 characters under that ceiling and keeps total-path budgets comfortable (~400 chars of the 1,024-byte S3 total-key budget, and macOSPATH_MAX, at order-24 digit depth under realistic prefixes).
6.6 Repr reminder for paths
Hive paths embed decimal components (§2). The §2 non-injectivity note applies: leaf ids at order 29 could not distinguish point from area — hive stores avoid the ambiguity structurally (shards live at coarse orders, and in-store kind rides the packed words themselves, §4) — and any parsed path string resolves by the §4 tie-break; no implementation may round-trip point-ness through a path string.
7. Coverage MOC serializations
Contract (spec: "morton-moc/1"). Coverage is declared in three tiers;
all three share the envelope's encoding discriminator:
"ranges" | "bitmap" | "full".
7.1 Stamp envelope: the tier-0 morton box
The leaf commit stamp carries the shard's morton box — the canonical
≤ 4-member MOC covering its occupied cells — padded to exactly four
decimal-string slots; the pad sentinel is JSON null. encoding:
"full" declares a fully occupied subtree (the shard id itself is the exact
MOC; no bitmap sidecar is written).
7.2 Leaf bitmap sidecar (coverage.moc inside the leaf)
Exact cell-order occupancy for one shard subtree:
- The raw bitmap has
4^depthbits (depth = cell_order - shard_order); raw size is exactlyceil(4^depth / 8)bytes. - Bit convention (contract, golden-vector-pinned): bit
iis the i-th shard-subtree cell in ascending packed-word order — equivalently, the base-4 rank of the cell's digit tail with digits1..4mapped to0..3— packed MSB-first within each byte (np.packbitsorder). - The payload is one zstd stream over the raw bitmap. The zstd stream format is contract; the compression level is non-normative (any level decodes identically).
- A decoder must reject a payload whose decompressed size is not exactly the raw bitmap size — never zero-pad or truncate (a partial cell set is a false negative).
7.3 Root coverage MOC (coverage.moc at the store/product root)
A JSON envelope with encoding: "ranges" listing shard-order coverage.
Required keys (a reader validates the set, never infers it):
spec—"morton-moc/1"(§7 header), tying the envelope to this serialization;encoding—"ranges"for this tier;order— integer, the shard order of every cell inranges(the common order the decimal endpoints share);ranges— the list of[first, last]runs below.
source, generated_at, and optional time_range are informative carrier
fields. Field semantics:
- A range is an inclusive
[first, last]run of same-order cells within one base cell, consecutive in base-4 digit-tail rank (the same ascending packed-word order as §7.2) — range ordering is contract (golden-vector-pinned). - Endpoints are decimal strings (§2), never JSON numbers: packed words
exceed 2^53 and would be silently mangled by float-based JSON parsers.
They are always unmarked area words at the shard order, never
p-marked: points live only at order 29 and cannot be range endpoints. - The carrier fields above are informative cache metadata; the ranges are a regenerable cache of the leaf-stamp truth.
8. Frozen for 1.x
The 1.x contract guarantees, immutable within the major version:
- the §1 bit layout, order range 0–29, canonical zero-fill, unsigned storage, and the raw-sort Z-order property;
- the §2 decimal grammar, its render/interchange status, and the emit conventions
(strings display /
uint64storage / capped legacyi64escape hatch); - the §4 encoding-carried kind convention (suffix
0..=47= area, exact at every order;48..=63= order-29 point) and the decimal parse rules (p-marked string ⇒ point word; unmarked string ⇒ area word — the tie-break; thepkind suffix is render/interchange-only and never appears in paths); - the §5 convention identity (UUID) and the
name: "morton"/coordinate: "morton"declaration; - the §6 hive grammars —
/1,/2,/3each frozen for stores declaring itsspecstring — including the_split rule, the window-label grammars, theallreserved token (structural in/3; a forward-going window-label exclusion across all spec versions, §6.3/§6.4), the product-name grammar (charset, base-component exclusion, and the 1–192 character length cap), and the node invariant; - the §7 coverage contracts: the 4-slot null-padded box, the
encodingdiscriminator values, the bitmap bit convention, and the root-MOC range ordering (zstd level and other codec parameters stay non-normative).
Extensions (new schedules, new spec versions, new encodings) are additive
under new discriminator values; existing stores never reparse under new
rules.