Skip to content

TS morton core + hive arithmetic (phase 6a/6b of issue #1)#7

Merged
espg merged 4 commits into
mainfrom
claude/1-phase6a-ts-morton
Jul 21, 2026
Merged

TS morton core + hive arithmetic (phase 6a/6b of issue #1)#7
espg merged 4 commits into
mainfrom
claude/1-phase6a-ts-morton

Conversation

@espg

@espg espg commented Jul 21, 2026

Copy link
Copy Markdown
Owner

Refs #1 (phase 6a+6b)

Pure-TS morton core (src/lib/morton/, no three.js, no fetch/UI wiring) implementing the mortie v1.0 spec, per the ratified phase-6 plan and its path_grouping amendment. 6c (detector + data path) and 6d (hive virtual store) are out of scope here and untouched.

What's in

6a — word codec + decimal grammar (word.ts, decimal.ts)

  • BigInt codec for the packed 64-bit word (spec §1): 4-bit prefix (base_id + 1), 54-bit body (27 × 2-bit tuples), 6-bit suffix with the full band table (0..=27 order==suffix; 28..=47 order-28/29 area preorder r = t28*5 + (t29 present ? t29+1 : 0); 48..=63 order-29 point r2 = t28*4 + t29). Order extraction, kind predicate (suffix range only, per §4 — no metadata), base-cell decode with loud rejection of the empty sentinel / invalid prefixes.
  • Decimal grammar (§2) incl. the terminal p kind suffix: legal solely on full order-29 strings, selects the POINT word; unmarked order-29 strings always parse AREA (the §4 tie-break); paths never carry p. Round-trip is lossless for both kinds.
  • Morton→NESTED by body-bit arithmetic (wordToNested), clipToOrder (mortie clip2order semantics: clip at-or-above native order is identity, a real clip always yields an area word), and viewNestedId — the viewer-side float64 cast: POINT clips 29→24 first, AREA never clips (order > 24 throws toward the phase-6d proxy/virtual-store path), results ≤ order 24 are exact JS Numbers (12·4²⁴ < 2⁵³).

6b — manifest + store arithmetic (manifest.ts, hive.ts, coverage.ts)

  • morton_hive.json parse for /1/3, discriminated by spec only; path_grouping absent→1, loud on anything but a positive integer.
  • Leaf-path computation with path_grouping chunking as the only construction code path (leading components full-width, last carries the remainder), path_grouping: 3 fixture cases included per the amendment; /2 windowed and /3 window-only (all.zarr) basenames; windowed-name split on the first _; shardIdFromPath recovering ids from digit components (any grouping — how /3 leaves get their id back).
  • Root coverage.moc ranges parse (§7.3, box/ranges tier only — the zstd leaf bitmap stays server-side): tolerant envelope parse (unusable → null, regenerable-cache posture), loud range validation (string endpoints only, same base, same order, non-reversed), exact expansion, O(ranges) containment, and coveredLeafPaths = manifest + root MOC → every leaf path with zero requests (MOC-first; deliberately no LIST fallback).
  • D19 product-root discovery model: classifyStoreRoot(objects, prefixes) — manifest at the root ⇒ bare store; product-grammar children ([a-z0-9_-]{1,192} minus the -?[1-6] base-component exclusion) ⇒ sorted product entries.

Golden provenance (TS↔Python↔Rust pinned to one convention)

  • 433142211 ↔ word 5347180132572332040 ↔ NESTED 238416 (order 8) — moczarr tests/test_fabricate.py GOLDEN_WORD/GOLDEN_NESTED.
  • Southern order-19 word 11570383905173274643 ↔ NESTED 2483716583387 — moczarr GOLDEN_WORD_O19_SOUTH/GOLDEN_NESTED_O19_SOUTH; decimal -41132132113423444234 derived once with mortie 0.9.0.
  • The -5112333 family (words, leaf paths -5/1/1/2/3/3/3/-5112333.zarr etc., windowed split cases incl. the malformed _20_19 reject) — moczarr tests/conftest.py / tests/test_convention.py.
  • The eight southern order-8 SOUTH_WORDS literals and the cross-base shard set (3444444, 6111111, 6444444, -1111111, -4444444, northern-before-southern raw-sort order) — moczarr tests/test_ranges.py TestSouthernAndMultiBaseGoldens (cross-base words derived once with mortie 0.9.0).
  • Suffix-band family: one path encoded at orders 26/27/28/29 (suffixes 26/27/43/45, words from mortie 0.9.0) plus its POINT twin (suffix 61); clip goldens 29→28 and 29→24 cross-checked against mortie clip2order.
  • SERC manifest + root coverage.moc envelope and the six covered leaf paths — moczarr tests/data/serc_hive (zagg-written, post zagg#314).

Judgment calls

  1. Point-band goldens are arithmetic-derived: installed mortie 0.9.0 predates p parse/emission (mortie PR Feature: Hide min bound d70-t/gridlook#121) and its geo2mort(order=29) emits order-29 area words, so the point twin (48 + t28*4 + t29) is pinned from the frozen §1/§4 formulas rather than a mortie output; its clip-to-24 result is cross-checked against the mortie-derived area clip (same path ⇒ same word).
  2. /3 temporal posture: moczarr doesn't parse /3 yet, so this reader accepts a /3 manifest with an absent temporal block as schedule: none (matching §6.4's all.zarr construct); /2 still requires temporal.schedule and /1 still refuses a temporal block (moczarr parity).
  3. path_grouping: 0 is malformed (throws) rather than normalized to 1 — zagg's or 1 normalization is its resume-compare projection, not read validation; the manifest is loud-on-malformed by posture.
  4. viewNestedId throws on area words above order 24 instead of returning a BigInt — per the amendment ("genuinely-finer data takes the proxy path"); the raw BigInt remains available via wordToNested.
  5. classifyStoreRoot throws when a root has neither a manifest nor product-shaped children (not a morton-hive root), rather than guessing.

Testing

58 new vitest cases (CPU-only) in tests/unit/lib/morton/; full suite 137 passing. npm run test, npm run typecheck, npm run lint-ci, and npm run build all green locally (baseline verified green before the change).

🤖 Generated with Claude Code

https://claude.ai/code/session_01N32Mx4rWhbTK8xXm7Mt2ax

espg and others added 2 commits July 20, 2026 18:11
Comment thread src/lib/morton/hive.ts
}
const [, base, tail] = m;
const components = [base];
for (let i = 0; i < tail.length; i += pathGrouping) {

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)

Top finding — the path_grouping remainder rule is spec-silent; freeze it before it can drift. This loop implements "leading components full-width, last carries the remainder." I derived the rule from spec §6.1 and it is not stated there: §6.1 says only "declares how many digits each component chunks; readers chunk the digit string per the manifest, never by assumption" — it never says which end absorbs the remainder when order % path_grouping != 0. §8 (frozen list) doesn't pin it either.

Cross-impl status (verified, not asserted): moczarr PR-A (d70-t#17) convention.group_digits chose the same rule — [digits[i:i+pg] for i in range(0, len, pg)] — and the zagg-written serc_hive_pg3 fixture (path_grouping: 3) confirms it: 4331422414/331/422/41/…, -433412214-4/334/122/14/…. I ran gridlook coveredLeafPaths against moczarr leaf_path on both SERC fixtures (pg1 and pg3): exact MATCH. So all three implementations agree today.

The risk is that the agreement is undocumented. Because nothing normative pins the remainder end, a future writer/reader is free to pick "last full, first remainder" and silently mis-shard every non-evenly-divisible id. Recommend filing a spec-gap issue on mortie (the d70-t#62 successor) to add the remainder rule to §6.1 and citing it in this docstring. No code change — the rule here is correct and matches moczarr and the fixture.

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

The normative fix is filed as espg/mortie#124 (freeze "leading components full-width, last carries the remainder" into §6.1, plus the base-component-stands-alone note); gridlook, moczarr PR-A, and the serc_hive_pg3 fixture all already implement it, so the code stands pending the spec amendment — no code change here.

Comment thread src/lib/morton/coverage.ts Outdated
const usable =
raw["spec"] === COVERAGE_SPEC &&
raw["encoding"] === "ranges" &&
typeof order === "number" &&

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)

Coverage parse-tolerance boundary diverges from moczarr — the "mirroring moczarr" claim in the header is imprecise. moczarr's parse_root_coverage gates the tolerant (→None) bucket on spec + encoding only; everything else (the order shape, the ranges shape, endpoint validity) is loud at ranges_words. This usable check additionally folds the order-is-integer and ranges-is-array checks into the tolerant bucket, moving the split point.

Confirmed by running the same envelopes (valid spec+encoding) through both readers:

envelope gridlook parseRootCoverage moczarr
order: "6" (string) null — whole MOC dropped dict → ranges_words reads it, returns the shard
order missing null dict → loud KeyError
ranges missing / non-array null dict → loud error

Two concerns: (a) for order:"6" the two readers produce different coverage from the same recoverable envelope — moczarr reads the shard, gridlook discards the entire MOC; (b) this reader is MOC-first with no LIST fallback, so a structurally-broken root MOC (right spec/encoding, mangled ranges) reads as nullno coverage → the store renders empty, whereas moczarr surfaces a loud error. Silent-empty is the more dangerous failure mode for a corrupt cache.

Recommend either matching moczarr's boundary (gate only on spec+encoding; let a bad order/ranges throw at expansion), or — if the stricter parse is deliberate — dropping the "mirroring moczarr" wording and having espg confirm the silent-empty posture is intended given there is no walk fallback. The endpoint-validation loud bucket (base-crossing / reversed / wrong-order / non-string) is exact parity; only this parse boundary differs.

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 the boundary to moczarr's parse_root_coverage vs ranges_words split: the tolerant-null bucket now gates on spec+encoding only, a numeric-string order (e.g. "6") is read like a JSON number, and a structurally-corrupt order/ranges (missing, non-numeric, non-list, or a mangled range entry) now throws loud instead of reading silent-empty (cb4adf9).

Comment thread src/lib/morton/manifest.ts Outdated
*/
function pathGroupingOf(raw: Record<string, unknown>): number {
const value = raw["path_grouping"];
if (value === undefined || value === null) {

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)

path_grouping: null parity divergence with moczarr. Here an explicit JSON null is normalized to 1 (docstring: "Absent/null reads as 1"). moczarr's parse_manifest treats only an absent key as 1 (payload.get("path_grouping", 1)); an explicit null falls through to isinstance(None, int)ValueError. Confirmed: {"spec":"morton-hive/1","cell_order":8,"shard_order":6,"path_grouping":null} is accepted by gridlook (pg=1) and rejected by moczarr. Low severity (writers emit the integer or omit the key), but the two readers disagree on the same document — worth reconciling in one direction so a serializer-nulled or hand-edited manifest can't read differently in the browser vs Python. Flagging for espg.

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

An explicit path_grouping: null now rejects (loud, via asInt) — only an ABSENT key defaults to 1, matching moczarr's payload.get("path_grouping", 1), so the two readers agree on the same document (3c6f7b2).

@espg

espg commented Jul 21, 2026

Copy link
Copy Markdown
Owner Author

🤖 from Claude (review)

Fresh-context adversarial review of phase 6a+6b (BigInt morton codec + hive/manifest/coverage arithmetic). The catastrophic class here is cross-language drift against the three other implementations (mortie rust, moczarr python, the normative spec), so the review was weighted toward differential testing, not reading.

Verdict: the §1/§2/§4 codec is sound

I derived the 28..=47 suffix→order band table by hand from the spec formula (r = t28*5 + (t29_present ? t29+1 : 0), suffix = 28 + r; points 48 + t28*4 + t29) and it matches orderOf/isPointWord/areaSuffix/pointSuffix/decodeTail exactly:

suffix order kind
0..27 == suffix area
28, 33, 38, 43 28 area
29-32, 34-37, 39-42, 44-47 29 area
48..63 29 point

Then cross-language differential tests (moczarr uv run python + mortie 0.9.0 as reference, gridlook driven via npx tsx probes — no repo files added):

  • 3,898-word corpus (golden families: 433142211/o8, o19-south, area29/point N+S twins, SOUTH_WORDS, cross-base set, the suffix-band family at orders 26/27/28/29 + point twin; plus 4,000 random valid words spanning every order 0..29, both hemispheres, both kinds). Checked orderOf, isPointWord, wordToNested (vs mort2healpix), renderMortonDecimal (vs morton_decimal), decimal round-trip, parse-of-python-decimal, and viewNestedId (vs fabricate_cell_ids, incl. point clip 29→24 and the area-above-24 throw). 0 mismatches.
  • clipToOrder vs mortie clip2order: 15,872 (word, k) pairs, k ∈ {0,1,8,19,24,27,28,29}, area + point words. 0 mismatches. (POINT clip 29→24 == clip-then-fabricate confirmed within the corpus.)
  • Grammar negatives vs moczarr morton_word: 41p/4p/-6p/433142211p/p/40/4x all throw on both sides; 41/4/-4 and a full order-29 string ±p accepted on both. Exact parity.
  • SERC coveredLeafPaths vs moczarr leaf_path on both serc_hive (pg1) and serc_hive_pg3 (pg3): exact MATCH.

No correctness bug found in the codec — the bullseye holds.

Findings posted inline (3)

  1. path_grouping remainder rule is spec-silent (hive.ts) — top finding. §6.1 never states which component absorbs the remainder. gridlook, moczarr PR-A (Include standard_name d70-t/gridlook#17), and the zagg-written pg3 fixture all chose "leading full-width, last remainder" and agree today, but nothing normative pins it → latent drift. Recommend a mortie spec-gap issue (Irregular Grid changes d70-t/gridlook#62 successor) to freeze the rule. No code change.
  2. Coverage parse-tolerance boundary diverges from moczarr (coverage.ts) — the "mirroring moczarr" claim is imprecise. moczarr gates the tolerant→null bucket on spec+encoding only; this parse also folds order-integer / ranges-array into it. Result: order:"6" is read by moczarr but dropped by gridlook, and a mangled-ranges root MOC reads as silent-empty here (no LIST fallback) where moczarr errors loudly. Reconcile the boundary or re-word + confirm the silent-empty posture.
  3. path_grouping: null (manifest.ts) — gridlook normalizes explicit null → 1; moczarr rejects explicit null (only absent → 1). Low severity, but the two readers disagree on the same manifest.

Noted, not posted

  • /3 manifest acceptance (judgment call 2) — gridlook accepts morton-hive/3; moczarr parse_manifest still rejects it (unknown spec). Intentional and documented (gridlook ahead of moczarr); parity for /1+/2 is exact. Fine.
  • cell_order: 8.0 — gridlook accepts (JSON 8.0 → JS 8, Number.isInteger true); moczarr rejects (isinstance(8.0, int) false). Inherent JS/Python JSON-number artifact, not actionable.
  • Point-band goldens arithmetic-derived (judgment call 1) — sound: mortie 0.9.0 mort2healpix decodes a raw point word to the same order-29 NESTED as its area twin, and the point suffix 48+t28*4+t29 is the frozen §1/§4 formula; the corpus exercised real mortie point decodes with 0 mismatch.
  • Judgment calls 3/4/5 assessed: pathGrouping-0-throws (matches moczarr), viewNestedId-throws-above-24-area (matches spec §4 + fabricate warn), classifyStoreRoot-throws-on-neither — all defensible.

Gates green locally: npm run test 137 passing, typecheck, lint-ci clean.

espg and others added 2 commits July 20, 2026 18:37
@espg
espg marked this pull request as ready for review July 21, 2026 01:39
@espg
espg merged commit a553703 into main Jul 21, 2026
3 checks passed
@espg
espg deleted the claude/1-phase6a-ts-morton branch July 21, 2026 05:59
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