diff --git a/docs/specification.md b/docs/specification.md index 95951ae..bfa30e5 100644 --- a/docs/specification.md +++ b/docs/specification.md @@ -317,6 +317,22 @@ discriminate by the `spec` string, never by sniffing names. manifest `path_grouping` parameter (default `1`) 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_grouping` chunks only + the order-digit string that follows it. When the order is not a multiple of + `path_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 at `path_grouping: 3` chunks the eight + order-digits `33142241` as `3+3+2` (`331`/`422`/`41`), leaving `{sign+base}` + `4` as its own leading component: + + +```text +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, `*.zarr` objects, and the declared leaf-adjacent sidecar names โ€” nothing else, ever. The walker's child classification depends on the name diff --git a/mortie/tests/test_spec_page.py b/mortie/tests/test_spec_page.py index f6bad51..a9bff2c 100644 --- a/mortie/tests/test_spec_page.py +++ b/mortie/tests/test_spec_page.py @@ -120,3 +120,57 @@ def test_sub29_strings_are_unambiguous(self): assert len(dec) < 29 # a genuinely shorter string assert word & 0x3F <= 27 # sub-29 area region (below the order-29 28..47 band) assert int(_decimal_to_word(dec)) == word + + +class TestPathGroupingRemainder: + """Drift pin for the ยง6.1 path_grouping remainder rule (issue #124). + + mortie ships only ``path_grouping: 1`` (``hive_path`` emits one digit per + level), so the grouped split itself has no code surface -- the remainder + placement is prose-normative. What this test *does* pin against real code + is the worked example's skeleton: parsed out of the doc, its ``{sign+base}`` + component, per-order digit sequence, and full-id leaf must match exactly + what ``hive_path`` produces for that id, and the documented grouped + components must be a faithful leading-full-width / remainder-last partition + of that digit string. Editing the example into something incoherent, or + flipping it remainder-first, fails here. + """ + + BEGIN = "" + END = "" + + def _doc_example(self): + text = SPEC_PAGE.read_text() + assert self.BEGIN in text and self.END in text, "example markers missing" + block = text.split(self.BEGIN, 1)[1].split(self.END, 1)[0] + line = next(ln for ln in block.splitlines() if ".zarr" in ln) + path, annotation = line.split("<-", 1) + grouping = int(annotation.split("path_grouping:", 1)[1].strip()) + return path.strip(), grouping + + def test_example_skeleton_matches_hive_path(self): + from mortie import MortonIndexArray + + path, grouping = self._doc_example() + comps = path.split("/") + doc_sign_base, doc_groups, doc_leaf = comps[0], comps[1:-1], comps[-1] + + # Canonical (grouping=1) decomposition straight from the real code. + arr = MortonIndexArray.from_hive_path([doc_leaf]) + canon = arr.hive_path()[0].split("/") + sign_base, order_digits, leaf = canon[0], canon[1:-1], canon[-1] + + # {sign+base} stands alone and the full-id leaf both match real code. + assert doc_sign_base == sign_base + assert doc_leaf == leaf + # The grouped components partition exactly the order-digit string. + assert "".join(doc_groups) == "".join(order_digits) + + def test_example_split_is_remainder_last(self): + path, grouping = self._doc_example() + groups = path.split("/")[1:-1] # drop {sign+base} and leaf + order = sum(len(g) for g in groups) + expected = [grouping] * (order // grouping) + if order % grouping: + expected.append(order % grouping) # remainder rides the last chunk + assert [len(g) for g in groups] == expected