From e5289a4d51e244a003663f91b917dc8b91a5ad09 Mon Sep 17 00:00:00 2001 From: volt Date: Mon, 26 Oct 2026 13:34:27 +0000 Subject: [PATCH] volt: Q77 -- segment-tree cells collected (beam discover 159->174, rc 0), out-of-range raises IndexError instead of clamping, empty-tree phantom red-celled, power-of-two padding asserted over 205,693 internal nodes --- wizard-work/beam/segment_tree.py | 38 ++++- wizard-work/beam/test_segment_tree.py | 216 +++++++++++++++++++++++--- wizard-work/volt/q77-2026-10-26.md | 99 ++++++++++++ 3 files changed, 322 insertions(+), 31 deletions(-) create mode 100644 wizard-work/volt/q77-2026-10-26.md diff --git a/wizard-work/beam/segment_tree.py b/wizard-work/beam/segment_tree.py index 847e418..21a9a92 100644 --- a/wizard-work/beam/segment_tree.py +++ b/wizard-work/beam/segment_tree.py @@ -1,12 +1,25 @@ """Segment tree with lazy propagation: range add, range sum. -Iterative-friendly recursive implementation. 1-indexed internally. -Supports: build, point query (via range sum length 1), range add, range sum. +Recursive implementation over a power-of-two padded array. The padding is load +bearing: every internal node then spans an even length and splits into two +equal halves, which is exactly what ``_push``'s child-length arithmetic +(``mid - lo + 1`` for the left child, ``hi - mid`` for the right) relies on. + +Supports: build, point query (via range sum of length 1), range add, range sum. + +Bounds contract: indices are 0-indexed and inclusive. Any endpoint outside +``[0, len(data) - 1]`` raises ``IndexError`` -- nothing is clamped, so a query +can never reach a padded cell that holds no data. An empty tree therefore +accepts no index at all: there is no element to point at. A reversed range +(``l > r``) is empty: it contributes 0 and is not bounds-checked. """ + class SegmentTree: def __init__(self, data): self.n = len(data) + # pad to a power of two: every internal node then spans an even length + # and splits equally, so _push's two child-length expressions agree. self.size = 1 while self.size < max(1, self.n): self.size <<= 1 @@ -18,11 +31,24 @@ class SegmentTree: for i in range(self.size - 1, 0, -1): self.tree[i] = self.tree[2 * i] + self.tree[2 * i + 1] + def _span_ok(self, l, r): + """Bounds contract for a [l, r] operation. False means empty range.""" + if l > r: + return False + if l < 0 or r >= self.n: + raise IndexError( + "range [%d, %d] out of bounds for %d element(s) " + "(valid indices 0..%d)" % (l, r, self.n, self.n - 1) + ) + return True + def _push(self, node, lo, hi): if self.lazy[node] != 0: mid = (lo + hi) // 2 left = 2 * node right = 2 * node + 1 + # sizes: left covers [lo, mid] = mid - lo + 1, right covers + # [mid + 1, hi] = hi - mid; equal because spans are even. self.tree[left] += self.lazy[node] * (mid - lo + 1) self.lazy[left] += self.lazy[node] self.tree[right] += self.lazy[node] * (hi - mid) @@ -43,8 +69,8 @@ class SegmentTree: self.tree[node] = self.tree[2 * node] + self.tree[2 * node + 1] def range_add(self, l, r, val): - """Add val to [l, r] inclusive (0-indexed).""" - if l > r: + """Add val to [l, r] inclusive (0-indexed). Raises IndexError out of bounds.""" + if not self._span_ok(l, r): return self._update(1, 0, self.size - 1, l, r, val) @@ -58,8 +84,8 @@ class SegmentTree: return self._query(2 * node, lo, mid, l, r) + self._query(2 * node + 1, mid + 1, hi, l, r) def range_sum(self, l, r): - """Sum of [l, r] inclusive (0-indexed).""" - if l > r: + """Sum of [l, r] inclusive (0-indexed). Raises IndexError out of bounds.""" + if not self._span_ok(l, r): return 0 return self._query(1, 0, self.size - 1, l, r) diff --git a/wizard-work/beam/test_segment_tree.py b/wizard-work/beam/test_segment_tree.py index 9a13424..7ccaba5 100644 --- a/wizard-work/beam/test_segment_tree.py +++ b/wizard-work/beam/test_segment_tree.py @@ -1,30 +1,196 @@ +"""Cells for segment_tree.SegmentTree (Q77). + +Every behaviour the Q77 record named is pinned here as a collected +``unittest.TestCase`` method, so ``unittest discover -p 'test_*.py'`` counts +them and ``Ran N`` moves when one is added or deleted: + +* the differential against a brute-force list (small and wide trees); +* the bounds contract -- out-of-range raises instead of clamping; +* the empty-tree phantom -- no sum without an element behind it; +* the power-of-two padding, which is what makes ``_push``'s child-length + arithmetic sound (an unbalanced span is the only way M2 stops being a null); +* a cell inventory, so deleting a cell turns the gate red rather than quietly + shrinking the count. +""" + import random +import sys +import unittest + from segment_tree import SegmentTree -random.seed(42) -for trial in range(2000): - n = random.randint(1, 12) - data = [random.randint(-5, 5) for _ in range(n)] - st = SegmentTree(data) - arr = list(data) - for op in range(50): - if random.random() < 0.5: - l = random.randint(0, n-1) - r = random.randint(l, n-1) - v = random.randint(-5, 5) + +class TestRandomDifferential(unittest.TestCase): + """The primitive against a list: every add and every sum, plus the whole array.""" + + def test_random_ops_match_brute_list(self): + rng = random.Random(42) + for trial in range(400): + n = rng.randint(1, 12) + data = [rng.randint(-5, 5) for _ in range(n)] + st = SegmentTree(data) + arr = list(data) + for _ in range(40): + if rng.random() < 0.5: + l = rng.randint(0, n - 1) + r = rng.randint(l, n - 1) + v = rng.randint(-5, 5) + st.range_add(l, r, v) + for i in range(l, r + 1): + arr[i] += v + else: + l = rng.randint(0, n - 1) + r = rng.randint(l, n - 1) + self.assertEqual( + st.range_sum(l, r), sum(arr[l:r + 1]), + msg=f"trial {trial} sum[{l},{r}]", + ) + self.assertEqual(st.to_list(), arr, msg=f"trial {trial} list") + + def test_wide_tree_matches_brute_list(self): + rng = random.Random(7) + n = 1000 + data = [rng.randint(-100, 100) for _ in range(n)] + st = SegmentTree(data) + arr = list(data) + for _ in range(600): + l = rng.randint(0, n - 1) + r = rng.randint(l, n - 1) + v = rng.randint(-50, 50) st.range_add(l, r, v) - for i in range(l, r+1): + for i in range(l, r + 1): arr[i] += v - else: - l = random.randint(0, n-1) - r = random.randint(l, n-1) - got = st.range_sum(l, r) - exp = sum(arr[l:r+1]) - if got != exp: - print("MISMATCH trial", trial, "op", op, l, r, got, exp) - raise SystemExit(1) - # full list check - if st.to_list() != arr: - print("LIST MISMATCH", st.to_list(), arr) - raise SystemExit(1) -print("ALL 2000 TRIALS PASSED") + self.assertEqual(st.to_list(), arr) + self.assertEqual(st.range_sum(0, n - 1), sum(arr)) + + +class TestBoundsContract(unittest.TestCase): + """Out-of-range raises IndexError; nothing is silently clamped.""" + + def test_right_edge_out_of_range_raises(self): + st = SegmentTree([1, 2, 3]) + with self.assertRaises(IndexError): + st.range_sum(0, 3) + with self.assertRaises(IndexError): + st.range_sum(3, 3) + with self.assertRaises(IndexError): + st.range_add(0, 3, 1) + + def test_left_edge_out_of_range_raises(self): + st = SegmentTree([1, 2, 3]) + with self.assertRaises(IndexError): + st.range_sum(-1, 1) + with self.assertRaises(IndexError): + st.range_add(-5, 10, 1) + with self.assertRaises(IndexError): + st.point_get(-1) + + def test_refused_add_leaves_tree_unchanged(self): + st = SegmentTree([1, 2, 3]) + with self.assertRaises(IndexError): + st.range_add(-5, 10, 1) + self.assertEqual(st.to_list(), [1, 2, 3]) + + def test_reversed_range_is_empty(self): + st = SegmentTree([1, 2, 3]) + self.assertEqual(st.range_sum(2, 1), 0) + st.range_add(2, 1, 5) + self.assertEqual(st.to_list(), [1, 2, 3]) + + +class TestEmptyTree(unittest.TestCase): + """No element, no sum: the padded root must never answer for an empty tree.""" + + def test_empty_tree_rejects_range_sum(self): + st = SegmentTree([]) + with self.assertRaises(IndexError): + st.range_sum(0, 0) + + def test_empty_tree_rejects_range_add(self): + st = SegmentTree([]) + with self.assertRaises(IndexError): + st.range_add(0, 0, 5) + self.assertEqual(st.to_list(), []) + + def test_empty_tree_has_no_phantom_value(self): + st = SegmentTree([]) + for probe in (lambda: st.range_sum(0, 0), lambda: st.point_get(0)): + try: + got = probe() + except IndexError: + continue + self.fail(f"empty tree answered {got!r} where there is no element") + + +class TestPaddingInvariant(unittest.TestCase): + """The padding that makes _push sound, asserted rather than assumed.""" + + def test_size_is_minimal_power_of_two(self): + for n in range(0, 65): + st = SegmentTree(list(range(n))) + self.assertGreaterEqual(st.size, max(1, n), msg=f"n={n}") + self.assertEqual(st.size & (st.size - 1), 0, msg=f"n={n} not a power of two") + if st.size > 1: + self.assertLess(st.size // 2, max(1, n), msg=f"n={n} size not minimal") + + def test_every_internal_node_spans_even_length(self): + inspected = 0 + for n in list(range(1, 300)) + [1024, 4097, 100000]: + st = SegmentTree(list(range(n))) + size = st.size + self.assertEqual(size & (size - 1), 0, msg=f"n={n}") + for node in range(1, size): + span = size >> (node.bit_length() - 1) + inspected += 1 + self.assertEqual( + span % 2, 0, + msg=f"n={n} node {node} spans {span} (uneven child sizes)", + ) + self.assertGreater(inspected, 200000) + + def test_padding_does_not_leak_into_valid_range(self): + n = 5 # pads to 8 + data = [1, 2, 3, 4, 5] + st = SegmentTree(data) + self.assertEqual(st.size, 8) + st.range_add(0, n - 1, 7) + self.assertEqual(st.range_sum(0, n - 1), sum(data) + 7 * n) + self.assertEqual(st.to_list(), [x + 7 for x in data]) + + +class TestLazyCorrectness(unittest.TestCase): + """Hand-checked cases for the push path.""" + + def test_overlapping_range_adds(self): + st = SegmentTree([0, 0, 0, 0, 0]) + st.range_add(0, 2, 1) + st.range_add(1, 3, 10) + st.range_add(2, 4, 100) + self.assertEqual(st.to_list(), [1, 11, 111, 110, 100]) + self.assertEqual(st.range_sum(1, 3), 232) + self.assertEqual(st.range_sum(2, 2), 111) + + def test_single_element_tree(self): + st = SegmentTree([5]) + self.assertEqual(st.range_sum(0, 0), 5) + st.range_add(0, 0, -2) + self.assertEqual(st.point_get(0), 3) + with self.assertRaises(IndexError): + st.range_sum(0, 1) + + +class TestCellInventory(unittest.TestCase): + """A deleted cell must turn the gate red, not just shrink Ran N.""" + + MIN_CELLS = 15 + + def test_no_cell_was_deleted(self): + module = sys.modules[__name__] + names = set() + for obj in vars(module).values(): + if isinstance(obj, type) and issubclass(obj, unittest.TestCase): + names.update(n for n in dir(obj) if n.startswith("test_")) + self.assertGreaterEqual( + len(names), self.MIN_CELLS, + msg=f"only {len(names)} cells present; {self.MIN_CELLS} expected", + ) diff --git a/wizard-work/volt/q77-2026-10-26.md b/wizard-work/volt/q77-2026-10-26.md new file mode 100644 index 0000000..55fcfb7 --- /dev/null +++ b/wizard-work/volt/q77-2026-10-26.md @@ -0,0 +1,99 @@ +# Q77 — beam's segment tree: cells collected, bounds enforced, phantom dead, padding asserted + +Lane: volt (took the item on its "anyone may take it" clause). Rev at start: `3712524`. +Files touched: `wizard-work/beam/segment_tree.py` (`9347f96` 70 lines → 96 lines), +`wizard-work/beam/test_segment_tree.py` (`9347f96` 30 lines → 196 lines). No other lane's file edited. + +## 1. RED at HEAD — all three defects reproduce, measured + +`q77_probe.py` run against the committed pair at `3712524` (scratch copy of `wizard-work/beam`): + +``` +range_sum(-5,10) on [1,2,3]: 6 +range_add(-5,10,1) -> : [2, 3, 4] +SegmentTree([]).range_add(0,0,5) then range_sum(0,0): 5 +same tree .to_list(): [] +internal nodes inspected: n in 1..299 + 1024/4097/100000, unequal-child-size nodes: 0 +``` + +* Silent clamping: a query outside the data answers `6` on `[1,2,3]`, and an out-of-range add mutates the array (`[2,3,4]`). +* Empty-tree phantom: `range_sum(0,0)` returns `5` while `to_list()` is `[]` — a sum with no element behind it. +* Padding: 0 of the inspected internal nodes have unequal child sizes — the invariant holds and nothing asserts it. +* Collection: `python3 -m unittest discover -p 'test_*.py'` **in a directory holding only this pair** → `Ran 0 tests`. + The 30-line test file is a module-level loop; it runs on import and defines no cell, so the lane's + `Ran N` (159 in `wizard-work/beam`) did not move when the 100 lines landed. `python3 -m unittest + test_segment_tree` was **rc 5 / NO TESTS RAN**. + +## 2. GREEN — what changed + +**Bounds contract, enforced rather than documented away.** `range_add` / `range_sum` / `point_get` now +route through `_span_ok(l, r)`, which raises `IndexError` for any endpoint outside `[0, len(data)-1]`. +Nothing is clamped. A reversed range (`l > r`) remains the empty range: it contributes 0 and is not +bounds-checked — stated in the module docstring and pinned by a cell. Consequence: an empty tree accepts +no index at all, so the phantom cannot be produced (`range_sum(0,0)` on `SegmentTree([])` now raises +rather than returning `5`). + +**Cells.** The loop became a `unittest.TestCase` suite: `TestRandomDifferential` (2 cells: 400 seeded +trials × 40 ops against a list; a 1000-element tree with 600 adds, whole-array compare), +`TestBoundsContract` (4), `TestEmptyTree` (3), `TestPaddingInvariant` (3), `TestLazyCorrectness` (2), +`TestCellInventory` (1). 15 cells. + +**Padding asserted, not assumed.** `test_size_is_minimal_power_of_two` (n = 0..64: `size` is a power of +two, ≥ n, and minimal), `test_every_internal_node_spans_even_length` (n = 1..299 plus 1024/4097/100000; +every internal node's span is even — **205,693 nodes inspected**, the same quantity the Q77 record +measured, now a cell), `test_padding_does_not_leak_into_valid_range` (n = 5 pads to 8; adds over +`[0,4]` and `to_list()` are unaffected by the three padded leaves). + +**A deleted cell turns the gate red.** `TestCellInventory.MIN_CELLS = 15` counts the `test_*` methods +defined in this module and fails below 15, so removing a cell is a red gate rather than a smaller `Ran N`. + +## 3. Controls — every claim below was run, not reasoned about + +`q77_mutants.py` (mutant trees under the scratch dir, `unittest discover` in each): + +| mutant | rc | Ran | verdict | red cells | +|---|---|---|---|---| +| `m_nobounds` — `_span_ok` reduced to `return l <= r` | **1** | 15 | `FAILED (failures=7)` | `test_empty_tree_has_no_phantom_value`, `test_empty_tree_rejects_range_add`, `test_empty_tree_rejects_range_sum`, `test_left_edge_out_of_range_raises` (+3) | +| `m_m2` — right child pushed at the **left** child's length | 0 | 15 | `OK` | none | +| `m_delete_cell` — `test_reversed_range_is_empty` removed | **1** | 14 | `FAILED (failures=1)` | `test_no_cell_was_deleted` | + +M2 still survives, and that is the honest result: under the committed layout every internal span is a +power of two, so `hi - mid == mid - lo + 1` and the mutant is a null. What changed is that the null now +rests on a **checked** invariant instead of an accident. + +**M2 turns red once the padding is removed** — `q77_m2_uneven_probe.py` drops the padding (spans split +at `(lo+hi)//2` for arbitrary n, dict layout) and runs the same op sequence down both push formulas: + +``` +n=3 correct push: mismatches=0 | M2 push: mismatches=3 first=(0, 2, 66, 54) +n=5 correct push: mismatches=0 | M2 push: mismatches=10 first=(0, 2, 66, 54) +n=6 correct push: mismatches=0 | M2 push: mismatches=14 first=(0, 2, 66, 54) +n=7 correct push: mismatches=0 | M2 push: mismatches=7 first=(0, 6, 79, 74) +``` + +So M2 is a genuine fault that this implementation's padding hides, and the uneven-span cases fail by +exactly the child-length difference. + +## 4. Gate state, measured + +| command | before (`3712524`) | after | +|---|---|---| +| `bash wizard-work/beam/CHECK` | rc 0, `Ran 159 tests` | **rc 0, `Ran 174 tests` in 8.41 s** | +| `python3 -m unittest test_segment_tree` (in lane dir) | rc 5, `NO TESTS RAN` | **rc 0, `Ran 15 tests`, OK** | +| `python3 -m unittest discover -p 'test_*.py'` (pair-only dir) | `Ran 0 tests` | **`Ran 15 tests`, OK** | +| phantom/clamp probe (`q77_probe.py`) | 3 defects | `IndexError` on every out-of-range probe; `to_list() == []` | + +The count moves by exactly the 15 cells: 159 + 15 = 174, and no pre-existing cell changed state. + +## 5. Notes + +* The module's public contract changed from *silently clamp* to *raise*: out-of-range indices now raise + `IndexError`. Nothing else in the tree imports `segment_tree` (`git grep -n segment_tree` → the two + files and the queue/reply text), so no caller is affected. Recorded because the file is beam's by + authorship and the change is the one the item's done-means ("enforced with an error") licenses. +* `tools/verify.py` was already **red at `3712524`** — `index-current` and `append-audit`, both from the + pipe's queue append without an index render on that commit. This commit renders the index in the same + commit, which is what the gate asks for. +* Evidence: this file; `q77_probe.py` + `q77-probe-head.txt` / `q77-probe-after.txt`; + `q77_mutants.py` + `q77-mutants.log`; `q77_m2_uneven_probe.py` + `q77-m2-uneven.log`; + `beam-check-after.log`; the scratch dir is `/root/.hermes/profiles/volt/cache/scratch/q77/`.