Skip to content

v1.0 specification & conventions page (issue #62)#118

Merged
espg merged 12 commits into
mainfrom
claude/62-spec-page
Jul 20, 2026
Merged

v1.0 specification & conventions page (issue #62)#118
espg merged 12 commits into
mainfrom
claude/62-spec-page

Conversation

@espg

@espg espg commented Jul 20, 2026

Copy link
Copy Markdown
Owner

Closes #62. Refs #48.

Adds docs/specification.md — the v1.0 specification & conventions page the 1.x stability contract points at — linked from the README, with a drift pin (mortie/tests/test_spec_page.py) that regenerates the resolution table from order2res and compares it literally against the page (the issue's acceptance criterion).

This PR consolidates content already recorded on the #62 thread and adds the pieces ratified since on the zagg side (zagg#305 carries the decision record; zagg's docs/design/sparse_coverage.md is the decisions registry and now cites this page as the normative home for grammars and constants — the acceptance criterion recorded on zagg#305 is that no normative constant lives only in the design doc).

What the page freezes

  • §1 bit layout[4-bit prefix | 54-bit body | 6-bit suffix], sourced from src_rust/src/decimal_morton.rs module docs (prefix base_id + 1, 27×2-bit body, suffix regions 0..=27 / 28..=47 area / 48..=63 point, canonical zero-fill, unsigned storage, raw-sort Z-order).
  • §2 decimal grammar — render-only repr, emit conventions (strings display / uint64 storage / capped legacy i64), the base-component grammar -?[1-6], and the order-29 point/area parse non-injectivity note.
  • §3 resolution/Δ table — orders 0–29 → nside, order2res cell scale, exact cell area; generated from order2res, pinned by test_spec_page.py.
  • §4 resolution: "exact" | "point" discriminator + 29→24 clip rule — espg-ratified field name/values/placement (zagg#305 decision 1): dggs attrs block, emission per data kind (writer always knows), clip rule applies to point only.
  • §5 zarr DGGS convention block — distinct grid name: "morton" + coordinate: "morton" (never name: healpix + indexing_scheme: morton — the scheme-blind silent-misread hazard from zagg D16), and the self-declared convention UUID 3e22156d-ea9e-4e01-95fe-e3809a4b41e7 (minted once, permanent; zarr_conventions is a list so a future upstream registry entry coexists).
  • §6 morton-hive layout/1/3 grammars consolidated from the thread (/2 windowed-leaf record, /3 token ratification): underscore split rule, window-label grammar table, the reserved all token (permanently excluded from the window grammar), /3 sidecar names {window}.stats.json / all.stats.json, node invariant, commit stamp, overview role flagging, and the product-name grammar ([a-z0-9_-]+, base-component exclusion) per zagg D19's final named-roots form — note the earlier heads-up comment described the interim hash-root design; the page records the ratified named-root revision instead.
  • §7 coverage MOC serializations — 4-slot null-padded tier-0 box, ranges|bitmap|full discriminator, the bitmap bit convention and root-MOC range ordering as contract with zstd level non-normative (zagg O8 addendum, espg-blessed 2026-07-20).
  • §8 frozen-for-1.x statement.

Phases

  • Phase 1 — spec page + README link + drift-pin test

Testing

  • mortie/tests/test_spec_page.py (2 tests) green against a local pip install -e ".[test]" build (maturin-via-pip; Rust 1.96).
  • flake8 mortie --select=E9,F63,F7,F82 clean; full pytest mortie/tests: 672 passed, 11 skipped.

Questions for review

  1. Product-name length capResolved (espg): cap at 192 chars. §6.5 now specifies [a-z0-9_-]{1,192}; §8 records the cap in the frozen list. Derivation on the page: POSIX 255-byte filename ceiling − 13 chars reserved for the immutable-provenance decoration (+ + 12-hex catalog fingerprint) = 242 hard ceiling; 192 sits 50 under it and keeps total-path budgets comfortable (S3 1,024-byte key / macOS PATH_MAX at order-24 depth). Folded in 34679c8.
  2. Area column constantsResolved (espg): unify both columns on the exact HEALPix sphere R = 6371.0088 km. §3 now derives cell area (4πR²/(12·4^order)) and cell scale (sqrt(area), RMS cell spacing) from that one model; the drift pin regenerates both columns from it. mortie.tools.order2res in code keeps the historical 111 × 58.6323 constant for behavioral compatibility (consumed by res2display and the test_coverage_boundary.py pad); §3 carries an explicit code-vs-page note and the migration is tracked in Unify order2res Earth model with the spec page sphere (R=6371.0088) #119. Folded in 541f080.
  3. Sign-off on this page is decision 3 on zagg#305; espg review requested there.

🤖 Generated with Claude Code

https://claude.ai/code/session_019zYV2pvzhVp8cv7e7H4JY3

@espg espg added the implement label Jul 20, 2026
@codecov

codecov Bot commented Jul 20, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 94.33%. Comparing base (41d557a) to head (231106b).

Additional details and impacted files

Impacted file tree graph

@@           Coverage Diff           @@
##             main     #118   +/-   ##
=======================================
  Coverage   94.33%   94.33%           
=======================================
  Files           9        9           
  Lines        1307     1307           
=======================================
  Hits         1233     1233           
  Misses         74       74           
Flag Coverage Δ
unittests 94.33% <ø> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.


Continue to review full report in Codecov by Harness.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 41d557a...231106b. Read the comment docs.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@codspeed-hq

codspeed-hq Bot commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

Merging this PR will improve performance by 20.07%

⚠️ Different runtime environments detected

Some benchmarks with significant performance changes were compared across different runtime environments,
which may affect the accuracy of the results.

Open the report in CodSpeed to investigate

⚡ 2 improved benchmarks
✅ 65 untouched benchmarks
⏩ 1 skipped benchmark1

Performance Changes

Benchmark BASE HEAD Efficiency
fast_norm2mort_orders[6] 327.8 ns 269.4 ns +21.65%
fast_norm2mort_orders[10] 373.6 ns 315.3 ns +18.5%

Tip

Curious why this is faster? Comment @codspeedbot explain why this is faster on this PR, or directly use the CodSpeed MCP with your agent.


Comparing claude/62-spec-page (231106b) with main (41d557a)

Open in CodSpeed

Footnotes

  1. 1 benchmark was skipped, so the baseline result was used instead. If it was deleted from the codebase, click here and archive it to remove it from the performance reports.

Comment thread docs/specification.md Outdated
`[start, end)`; readers never parse semantics out of a custom label. The
charset excludes `_` by construction.
- The reserved token `all` (§6.4) is **permanently excluded from the window
label grammar** under every schedule.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 from Claude (review)

F1 (major) — all exclusion extends a /3-scoped ratification onto the already-frozen /2 grammar.

This sentence lives in §6.3 (morton-hive/2) and asserts the reserved all token is excluded from the window-label grammar "under every schedule" — and §8 lists "the all reserved token" among the immutable 1.x guarantees. But the ratification tied all to /3 only: it is the structural name of the /3 schedule: none leaf (all.zarr). In a /2 store the schedule: none leaf is the bare {full_id}.zarr; all has no structural role there, and the /2 explicit-list grammar was frozen as opaque [0-9A-Za-z-]{1,32} in issuecomment-4986809092, which admits a label all.

zagg D23 says all is "excluded from the window grammar forever" in the context of the /3 revision — "the window grammar" there is the /3 grammar. Retroactively narrowing the separately-frozen /2 explicit-list grammar is a contract change to /2 that the record does not clearly ratify.

Recommend either (a) scope the exclusion to /3 (where all is a real token), or (b) if espg intends a cross-version reservation, state it as a deliberate cross-version tightening of the /2 grammar and confirm on the thread — don't present it as already-frozen. As written it's an overclaim placed in the frozen-contract list.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 from Claude

Fixed in c015d84 — scoped the exclusion instead of narrowing the frozen /2 grammar.

§6.3 now states the reservation as forward-going: no store written under any spec version may declare an explicit window labeled all, but this is a constraint on new writers, not a retroactive narrowing of the frozen /2 opaque [0-9A-Za-z-]{1,32} grammar. A pre-existing /2 store that declared an all label stays readable under its frozen grammar — /2 labels are opaque and manifest-resolved, and all has no structural role in /2 (the schedule: none leaf there is the bare {full_id}.zarr, not all.zarr). §6.4 marks the token as structural only in /3 and cross-references §6.3; §8 now reads "the all reserved token (structural in /3; a forward-going window-label exclusion across all spec versions)".

This keeps the espg-ratified "excluded from the window grammar forever" intent (D23, tied to /3) while making the cross-version scoping explicit rather than presenting a retroactive /2 change as already-frozen. The scoping is now on record and explicit — espg can adjust to the harder retroactive form (banning all even in a legacy /2 manifest) if that was the intent.

Comment thread docs/specification.md Outdated
exceed 2^53 and would be silently mangled by float-based JSON parsers.
- Carrier fields (`source`, `generated_at`, optional `time_range`) are
informative cache metadata; the ranges are a regenerable cache of the
leaf-stamp truth.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 from Claude (review)

F2 (minor) — root-MOC envelope: required order (and spec) fields not enumerated.

§7.3 names the informative carrier fields (source, generated_at, time_range) and the ranges/encoding fields, but the actual serialization (zagg build_root_coverage, COVERAGE_SPEC = "morton-moc/1") emits a required order integer (the shard order) and a spec key in every envelope:

{"spec": "morton-moc/1", "encoding": "ranges", "order": <int>,
 "source": ..., "generated_at": ..., "ranges": [...], "time_range"?: [...]}

The order is only implied here ("shard-order coverage", "same-order cells" — recoverable from the decimal endpoints). For a normative serialization a reader shouldn't have to infer the key set: enumerate the required keys (spec, encoding, order, ranges) explicitly, the way §7.2 enumerates the bitmap sidecar's fields. spec is given in the §7 header but not tied to this envelope.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 from Claude

Fixed in ff7f877 — §7.3 now enumerates the required key set explicitly, the way §7.2 does for the bitmap sidecar. Added a Required keys list: spec ("morton-moc/1"), encoding ("ranges"), order (integer shard order of the listed cells), and ranges; with source, generated_at, and optional time_range called out as informative carrier fields. This matches exactly what zagg build_root_coverage emits (src/zagg/hive.py, envelope dict at the COVERAGE_SPEC/"ranges"/int(order) construction). A reader now validates the set rather than inferring order from the decimal endpoints.

Comment thread docs/specification.md Outdated
Informative (the formulas are contract; the rendered values are derived).
`nside = 2^order`; the cell-scale column is `order2res(order)` =
`111 × 58.6323 × 0.5^order` km (`mortie.tools.order2res`); the area column is
the exact HEALPix cell area `4πR² / (12 · 4^order)` with `R = 6371.0088` km.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 from Claude (review)

F3 (minor) — the resolution table's two columns use different Earth models, and the area constant has no code source of truth.

The cell scale column is order2res = 111 × 58.6323 × 0.5^order km (an implied sphere of R ≈ 6366 km, 111 km/deg), while the cell area column uses 4πR²/(12·4^order) with R = 6371.0088 km. So the two columns describing the same cells rest on different radii — sqrt(area) and cell scale diverge by ~0.2%. This is the PR's own open Question 2; flagging it as a review item so the resolution is on record.

Related test note: EARTH_RADIUS_KM = 6371.0088 is redefined locally in test_spec_page.py (not imported from mortie), and order2res's radius is a third, different value. The drift pin therefore proves the table is internally consistent with a test-local constant, not that the area column tracks any mortie source of truth — fine as a drift pin, but the area column's authority is the test file itself. If the two-radius split stays, a one-line note in §3 ("cell scale and cell area use different Earth models — see PR discussion") would pre-empt the confusion.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 from Claude

Fixed the transparency in d09b724; left the constants as the standing question. §3 now carries a note stating both models explicitly: cell scale keeps the historical order2res constant (111 km/deg × 58.6323, implied R ≈ 6366 km), cell area uses the exact HEALPix sphere area at mean radius R = 6371.0088 km, so sqrt(area) and cell scale diverge by ~0.2%. It also records that the drift pin only proves each column is internally consistent with its own constant, not that they share a radius.

Whether to unify the two radii is espg's call — this is the PR's open Question 2, so I left the constants as documented and flagged the divergence rather than picking a radius. If you want them unified, the cleanest path is a single EARTH_RADIUS_KM sourced from mortie (not test-local) feeding both columns.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 from Claude

Resolved per espg's ruling on this thread's open question: unify both columns on one Earth model — the exact HEALPix sphere R = 6371.0088 km (541f080).

  • §3 now derives cell area = 4πR²/(12·4^order) and cell scale = sqrt(area) = sqrt(4πR²/(12·4^order)) (the RMS cell spacing) from that single radius. HEALPix is equal-area by construction, so sqrt(area) is an exact per-cell edge scale, not an approximation — and by definition it no longer diverges from the area column.
  • The drift pin (test_spec_page.py) now regenerates both columns from the sphere formula; it no longer imports order2res for the scale column, and test_table_covers_full_order_range still pins the MAX_ORDER row count.
  • The code is deliberately not changed in this PR: mortie.tools.order2res keeps its historical 111 × 58.6323 constant (R ≈ 6366 km) because it is consumed behaviorally — res2display and the coverage buffer-pad in tests/test_coverage_boundary.py:78 (pad_cells * order2res(order) / KM_PER_DEG), and test_tools.py pins its values. §3 carries an explicit code-vs-page note stating the page is normative and order2res is ~0.2% smaller at every order.
  • Unification of order2res onto this sphere (a behavioral change) is tracked as a follow-up: Unify order2res Earth model with the spec page sphere (R=6371.0088) #119, linked from both the §3 note and PR Question 2 (now marked resolved).

@espg

espg commented Jul 20, 2026

Copy link
Copy Markdown
Owner Author

🤖 from Claude

Folded two espg-directed review rulings that had no dedicated inline thread, recording them here for the record.

§4 resolution discriminator — per-id, not homogeneity (783bf83). espg asked whether a mixed-order store (zagg D24) slots under exact; settled answer: yes. §4 now states the discriminator is a per-id encoding-semantics claim, never a homogeneity claim: exact asserts only that each id's encoded order is the true resolution of the thing that id labels (grid-derived cells) — it makes no uniform-order claim. A regionally mixed-order store is exact everywhere; the per-region heterogeneity is carried by the morton words and the coverage MOC themselves, not by the discriminator.

§6.5 product-name length cap — 192 chars (34679c8). espg's final ruling is 192. §6.5 grammar is now [a-z0-9_-]{1,192} and §8's frozen list records the cap. Derivation on the page: POSIX filename-component ceiling 255 bytes (single-byte ASCII ⇒ bytes = chars) − 13 chars reserved for the immutable-provenance decoration (+ + 12-hex catalog fingerprint, shared with the product component when a D23 downloader materializes the tree as {name}+{catalog-hash}/) = 242 hard ceiling; 192 sits 50 under it and keeps total-path budgets comfortable (~400 chars of the 1,024-byte S3 key budget / macOS PATH_MAX at order-24 depth under realistic prefixes). Charset and base-component exclusion unchanged. PR Question 1 marked resolved.

@espg

espg commented Jul 20, 2026

Copy link
Copy Markdown
Owner Author

🤖 from Claude

In response to the discriminator questions on this review: §4 is reworked into one coherent treatment of a three-value resolution discriminator, exact | point | mixed (sha 4a3823a).

  • exact — each id's encoded order is the true resolution of the thing it labels (grid-derived); makes no uniform-order claim, so a regionally mixed-order store (zagg D24) is exact everywhere, with heterogeneity carried by the morton words + MOC. The earlier D24 sentence is integrated here, not duplicated.
  • point — locations cast to order 29 with no area claim, uniform across the declared array; the 29→24 clip rule applies to point only.
  • mixed (your proposal, settled 2026-07-21) — order-29 ids are points, ids at any other order are exact, per-id recovery via the reserved order 29 as an in-band discriminator. Two constraints frozen with it: (1) the clip rule is inapplicable (clipping destroys the in-band signal; mixed keeps points at 29 and uses the other Number-safety measures — hub-side fabrication, aggregation); (2) genuinely-exact order-29 cells are unrepresentable under mixed (29 is spent) — a true order-29 exact grid declares exact. Motivation: exact aggregated cells + order-29 raw locations in one coordinate (the HHDC direction).

Folded alongside: declaration scoping (point-ness carried entirely by the declaration for exact/point, never the encoding — post-clip a point is bit-for-bit an exact cell; mixed semantics at non-29 orders need separate declared fields; mixed works only because 29 is reserved); the declaration selects the order-29 decode (resolving the §2 non-injectivity — under exact an order-29 word is a genuine cell and is not clippable, taking the same fabrication/aggregation measures as mixed); and the writer-side hazard (location ids mis-written into exact masquerade undetectably as order-29 cells → the discriminator is required, not optional).

Also updated: the §4 clip-rule subsection (now naming exact-29-not-clippable and mixed-inapplicable), §8's frozen line (three values + point-only clip scope), and the §5 bullet/example (still exact, consistent). test_spec_page.py green; flake8 --select=E9,F63,F7,F82 clean.

@espg

espg commented Jul 20, 2026

Copy link
Copy Markdown
Owner Author

🤖 from Claude

§4 superseded per the final ruling (espg, 2026-07-21)462ffb1, one commit replacing the declaration design (including 4a3823a's three-value form):

  1. The resolution declaration is deleted; kind is encoding-carried. Suffix 0..=47 = area word, an exact cell at every order 0–29 (an order-29 area word is a genuine order-29 cell — nothing unrepresentable); suffix 48..=63 = order-29 point, full stop. No store/array metadata field; the §5 attrs example and §8 frozen list updated accordingly. Mixed content in one coordinate is well-formed by construction.
  2. The one remaining ambiguity is pinned precisely: packed words never collide (disjoint suffix ranges); only the order-29 decimal string denotes both kinds. Normative tie-break, now stated bit-exactly: parsing a decimal string always yields the AREA word — for final stored tuples t28,t29, parse ⇒ suffix 28 + t28·5 + t29 + 1, never the point's 48 + t28·4 + t29. Golden-pinned at the bit level in test_spec_page.py::TestDecimalParseTieBreak (verified against _decimal_to_word; sub-29 strings shown unambiguous). Fully determined by the existing packing code — no blocking question.
  3. Membership at coarser levels = the ordinary truncation (clip2order), a transient cast — one sentence, informative.
  4. The 29→24 clip left the spec: reframed as a viewer-side transient float64-safety cast (gridlook/JS display layer), an implementation note only — never encoding semantics.

zagg follow-through (removing the resolution field emission + constants that #312/#314 introduced, O10 record update) lands on PR englacial/zagg#314; noted there.

Comment thread mortie/tests/test_spec_page.py Outdated

# Points exist only at order 29: any shorter string denotes exactly
# one word (an area cell), which round-trips bit-identically.
arr = MortonIndexArray.from_latlon(np.array([45.0]), np.array([45.0]))

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 from Claude (review)

F4 (medium) — test_sub29_strings_are_unambiguous never exercises a sub-order-29 string.

from_latlon(lat, lon) defaults to order=MAX_ORDER (= 29), so this builds an order-29 area cell, not a shorter string. I confirmed the rendered id has 29 order digits and suffix 44 (in the 28..=47 area region), so the word & 0x3F <= 47 assertion passes on an order-29 word.

That means the "points exist only at order 29 / any shorter string denotes exactly one word" property the test name and docstring promise is not actually pinned — the sub-29 case is untested. The test still asserts something true (an order-29 area word round-trips), but it duplicates coverage the order-29 half of test_order29_string_parses_to_area_word already gives.

Suggest passing an explicit sub-29 order, e.g. from_latlon(..., order=10) (verified: renders 14111411111, suffix 10, round-trips bit-identically), so the "shorter string is unambiguous" claim is genuinely golden-pinned.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 from Claude

Fixed in 31fa1e0. test_sub29_strings_are_unambiguous now builds an explicit sub-29 cell — from_latlon(..., order=10) (verified from_latlon accepts order, default MAX_ORDER) — so the test genuinely exercises a shorter string instead of the order-29 area word. It renders 14111411111 (11 digits), and the assertions now pin len(dec) < 29, suffix <= 27 (the sub-29 area region, below the order-29 28..47 band), and exact bit-identical round-trip. pytest mortie/tests/test_spec_page.py green (4 passed); flake8 --select=E9,F63,F7,F82 clean.

Comment thread docs/specification.md Outdated

With the kind suffix, the decimal round-trip is **lossless for both
kinds**: an area word renders unmarked and parses back to itself; a point
word renders `p`-marked and parses back to itself. The residual ambiguity

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 from Claude (review)

F5 (low-medium) — §2.1 states p-marked point rendering as present fact, but it isn't implemented and the golden test shows points render unmarked.

Here §2.1 says, unconditionally and in the present tense: "a point word renders p-marked and parses back to itself." But §4 (lines 226–229 of the same commit) explicitly hedges the other way — "the p emission/acceptance is tracked as a mortie implementation change (issue linked from the PR)" — i.e. decimal_repr does not emit p today. I confirmed this: from_latlon(45, 45, points=True).decimal_repr() returns an unmarked 29-digit string, and test_order29_string_parses_to_area_word relies on exactly that (it derives t28,t29 from the unmarked dec and expects the area word).

So within one commit §2 asserts the p round-trip as done while §4 says it's pending, and the pinned test agrees with §4. Suggest making §2.1 conditional/normative-target ("MAY render p-marked", or "once the suffix is emitted…") to match §4's status note, so a reader/implementer doesn't conclude mortie currently emits p.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 from Claude

Reconciled in 231106b. The implementation now exists — the p emit/accept is implemented in PR #121 (issue #120) with golden vectors — so the §2.1↔§4 tension is resolved by describing the lossless round-trip as the normative contract and citing the concrete implementation, rather than hedging one section as "pending". §2.1 now reads "the normative contract … The p emit/accept is implemented in PR #121 (issue #120), with golden vectors." §4's former "tracked as a mortie implementation change (issue linked from the PR)" is likewise replaced with the same PR #121 / issue #120 citation. Note this PR's branch does not itself carry the emit change (points still render unmarked here); that lands via #121, which the text now points to directly.

@espg

espg commented Jul 20, 2026

Copy link
Copy Markdown
Owner Author

🤖 from Claude (review)

Two low-severity consistency notes from the p-suffix pass (both non-blocking; the tie-break formulas themselves verify exactly against src_rust/src/decimal_morton.rs — area suffix 28 + t28*5 + t29 + 1, point 48 + t28*4 + t29, stored 0..=3 tuples — and TestDecimalParseTieBreak passes and pins them bit-exactly):

F6 (low) — §7 root-MOC endpoints are never p-marked, but the page leaves it implicit. §7.3 says range endpoints are "decimal strings (§2)" at the shard order; since points exist only at order 29 and shards live at coarse orders, an endpoint can never be a point id, so it's always an unmarked area word. That's currently only implied by "cell"/"shard order" wording. Now that p exists as a legal decimal-string suffix, one clause stating MOC endpoints are always unmarked area words would close the loop. Harmless as-is (an unmarked string parses to the area word by the tie-break regardless).

F7 (low/nit) — "render-only" vs "render/interchange" terminology drift. §2 opening (line 82) and §8 (line 516) still call the decimal string "render-only", while the new text describes lossless round-trip interchange and labels the suffix "render/interchange-only" (lines 97, 521). No substantive conflict — "render-only" is about the string's role (display form vs uint64 storage/compute), orthogonal to round-trip reversibility — but the two phrasings now sit side by side; worth a quick harmonization.

@espg

espg commented Jul 20, 2026

Copy link
Copy Markdown
Owner Author

🤖 from Claude

Folded both notes from the p-suffix consistency pass in 231106b.

F6 (low) — §7 root-MOC endpoints are never p-marked, but the page leaves it implicit.

§7.3 now states it explicitly. The endpoints bullet reads: "They are always unmarked area words at the shard order, never p-marked: points live only at order 29 and cannot be range endpoints." Closes the loop rather than leaving it implied by "cell"/"shard order" wording.

F7 (low/nit) — "render-only" vs "render/interchange" terminology drift.

Harmonized on render/interchange. §2 opening (was "render-only external form") and §8 (was "its render-only status") now both say render/interchange, matching the suffix's existing "render/interchange form only" / "render/interchange-only" phrasing. No substantive change — just the two phrasings no longer sit side by side.

pytest mortie/tests/test_spec_page.py green; flake8 --select=E9,F63,F7,F82 clean.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

v1.0 docs: specification & conventions page (bit-layout + resolution/Δ table + frozen-convention statement)

1 participant