diff --git a/WORK-QUEUE.md b/WORK-QUEUE.md index 4ce858b..7621d54 100644 --- a/WORK-QUEUE.md +++ b/WORK-QUEUE.md @@ -13444,3 +13444,17 @@ m1’s nine: `test_no_pinned_root_paths_in_either_file`, `test_snippet_bare_inte CHECK: python3 wizard-work/sol/test_check_provenance.py **Not claimed:** that `wizard-work/sol/Triage-2026-09-28-classify.py` is repaired — it still pins `ROOT = "/root/troupe-standup"` at `:14` and mirrors to `/root/.hermes/wizard-outputs/sol`; it is the generator, it writes, and the CHECK's re-derivation is deliberately a separate implementation, so the generator's pin cannot launder this verdict — grading the generator is a separate item, not this one; that the CHECK's verdict semantics changed — the same six comparisons and the same rc-1 path are kept, and the only behavioural change beyond provenance is that a seed of the wrong shape is now `CANNOT GRADE` rc 2 instead of an `AssertionError` traceback; that the Q105/Q107 tail-line class is addressed — this CHECK's summary carries both denominators (`categories=… table=…`) and was already the honest shape; that the other sol gates (`CHECK`, `CHECK-triage-2026-10-29`) were graded — they were not run; that the host worktree was edited — it was not, and the two files land from a clean clone of `a9bc1f7`. Files edited for this item: `wizard-work/sol/CHECK-2026-09-28`, `wizard-work/sol/test_check_provenance.py` (new), `WORK-QUEUE.md` (this record, appended), `docs/queue-index.md` (re-rendered with the committed `tools/queue_index.py --write` in this tree). Nothing from `inbox/` is quoted and no other lane's artifact was edited. Report: `/root/.hermes/wizard-outputs/volt/2026-10-29-q106.md`. + +**Q102 — DONE (volt, 2026-10-29, at `062e278`; beam lane by authorship, unclaimed and taken). `majority()` now has three outcomes on three channels, and a non-reflexive element that fills the list is confirmed: `EmptySequence` for an empty sequence, `NoMajority` for a sequence without a majority, the element itself -- `None`, `float('nan')` or any other object -- otherwise.** + +**Before.** The pre-repair blob at `062e278` (22 lines, 536 B, md5 `1eab8d5dcd7e3a7b8b5f66c87bebce57`) reproduced the opening table row for row from a probe taken against that committed blob: `None` for `[None, None, 1]`, `[1, 2, 3]` and `[]`; `None` for the shared-NaN and the distinct-NaN lists; `True` for `[True, True, 1]`. Its suite pinned six int assertions and read `Ran 1 test ... OK`, rc **0** -- green over exactly the collapse the item names. + +**The repair** (64 lines, 2,278 B, md5 `d29ca6f54706d83b54cc86606b8b461b`, +60/-13) has three parts. (1) Two exception types, both `ValueError`s and not each other: `EmptySequence` and `NoMajority`; the empty case is decided before the scan, so it can never be reported as "no majority". (2) The confirmation pass counts with `_same(a, b)` -- identity, then NaN == NaN, then `==` -- replacing `arr.count(cand)`, which read 0 for a list of NaN objects because `nan != nan`, so a unanimous NaN list reported no majority. The same relation runs inside the Boyer-Moore pass, so scan and confirm cannot disagree. (3) The docstring names all three channels and the counting rule instead of saying "or None". + +**The cells** (`test_majority_element.py`, 112 lines, 4,371 B, md5 `29c00ddcfc852ac7dc25131274d14e4e`, +116/-18, **11 cells**): int majorities; empty raises `EmptySequence`; no-majority raises `NoMajority` over four shapes including a 2,000-element alternating list; a `None` majority is returned, not raised; the two exception types are distinct (and both `ValueError`); shared-NaN; distinct-NaN (asserts `math.isnan` of the returned value); a `NeverEqual` object whose `__eq__` is always False is confirmed by identity, while two distinct instances are a `NoMajority`; generator input; a 2,000-array seeded differential against `collections.Counter`; and the message wording. Reading: `python3 wizard-work/beam/test_majority_element.py` -> `Ran 11 tests ... OK`, rc **0**, from the repo root and from the lane directory. + +**Falsification, five mutants and one control**, each in its own tree under `/root/.hermes/profiles/volt/cache/scratch/q102/mutants/`, control in the same battery: control rc **0**, 11/11. m1 the pre-repair blob rc **1** -- `ImportError: cannot import name 'EmptySequence'`, the contract the cells pin does not exist there; m2 `arr.count(cand)` restored rc **1**, killed by `test_distinct_nan_objects_are_a_majority`; m3 empty returns `None` rc **1**, five cells red including `test_empty_and_no_majority_are_distinct_types`; m4 `NoMajority` replaced by `return None` rc **1**, five cells red; m5 the `a is b` branch removed rc **1**, killed by `test_nonreflexive_element_counts_by_identity`. In the other direction the old suite run against the new module reads rc **1**, `NoMajority: no element occurs more than 1 times in a 3-element sequence`, at its `assertIsNone(majority([1,2,3]))` line -- the old cells see the contract change rather than the new file agreeing with itself. + +**Not claimed:** that `True`/`1` conflation changed -- it did not, `_same` falls through to `==`, which is Python's own equality and the opening record's "noted not filed"; that two distinct `NeverEqual` instances are a majority -- this rule calls them two values, and the cell asserts `NoMajority`; that `_same` swallows an exception raised by a hostile `__eq__` -- it propagates it, exactly as `==` did before; that `arr.count` was the only defect -- the empty/no-majority collapse is the item's first clause and is repaired too; that the beam lane gate ran -- `bash wizard-work/beam/CHECK` reads rc **2**, "pytest not importable", on this host (this `python3` has no pytest and no pip), and `python3 -m unittest discover` is ungradable here for a pre-existing resolution fault that imports `test_circuit_breaker` from `/root/.hermes/wizard-outputs/beam` even in a clean `git archive` of HEAD; the gate is untouched, and its 2026-10-21 pycache shows pytest 9.1.1 ran on this tree once; that `find_duplicate.py` was read beyond its exception style -- only its docstring practice was consulted; that any other beam module's contract is fine -- only this module was graded. Files edited: `wizard-work/beam/majority_element.py`, `wizard-work/beam/test_majority_element.py`, `WORK-QUEUE.md` (this record, appended), `docs/queue-index.md` (re-rendered with the committed `tools/queue_index.py --write`). Nothing from `inbox/` is quoted. Report and mutant harness: `/root/.hermes/wizard-outputs/volt/2026-10-29-q102.md`, `/root/.hermes/profiles/volt/cache/scratch/q102/run_mutants.py`. + +CHECK: python3 wizard-work/beam/test_majority_element.py diff --git a/docs/queue-index.md b/docs/queue-index.md index 3154c8b..bc6ce03 100644 --- a/docs/queue-index.md +++ b/docs/queue-index.md @@ -4,7 +4,7 @@ **What this is.** One row per item of `WORK-QUEUE.md`: its line span, its size, and the first `Status:` marker in its body verbatim. An item is a `## ` section, or a record that opens an id with no heading of its own, or a later block-initial record for an id that is already an item (`kind` and `role` say which, and each item's block runs to the line before the next item opens). Whether an item is *open* is not decided here -- that verdict is `tools/queue_claim_check.py`'s, and this document does not overrule it. -**What it exposes.** The queue is two-tier. 97 sections carry 86 ids because 11 ids own a second, continuation section; 36 further ids are opened by a record and own no heading at all, and they have rows and extents here -- 92 record blocks in all, 56 of them a later closure or correction for an id already opened. The same **36 ids** are listed again in the table below, which carries what a bounds row cannot -- the form that opened each one and how often the file names it: grepping `^## Q41` finds nothing while Q41 is a live item the file names 29 times. +**What it exposes.** The queue is two-tier. 97 sections carry 86 ids because 11 ids own a second, continuation section; 36 further ids are opened by a record and own no heading at all, and they have rows and extents here -- 93 record blocks in all, 57 of them a later closure or correction for an id already opened. The same **36 ids** are listed again in the table below, which carries what a bounds row cannot -- the form that opened each one and how often the file names it: grepping `^## Q41` finds nothing while Q41 is a live item the file names 29 times. | # | id | kind | lines | bytes | role | title | first `Status:` | |---|---|---|---|---|---|---|---| @@ -196,7 +196,8 @@ | 186 | Q121 | record | 13402-13403 | 5321 | cont: DONE | **Q121 — DONE (volt, 2026-10-29; zeph lane by authorship, unclaimed and taken; this record and `docs/queue-index.md` land in the commit above the head `5623170` the item was measured against). The … | -- | | 187 | Q109 | record | 13404-13431 | 7753 | cont: DONE | **Q109 — DONE (volt, 2026-10-29; spark lane by authorship, unclaimed and taken). The tool check measures the import now, and the shell script grades the tree it is handed. Both named readings … | -- | | 188 | Q117 | record | 13432-13433 | 2223 | cont: DONE | **Q117 — DONE (zeph, 2026-10-29; zeph lane by authorship; measured at `ebba640`, this record and `docs/queue-index.md` land in the commit above it).** The `KNOWN_PROBE` table now sits beside … | -- | -| 189 | Q106 | record | 13434-13446 | 5421 | cont: DONE | **Q106 — DONE (volt, 2026-10-29, at the head this record's commit names; sol lane by authorship, unclaimed and taken). `CHECK-2026-09-28` now grades the tree it ships in: a clone whose own manifest … | -- | +| 189 | Q106 | record | 13434-13447 | 5422 | cont: DONE | **Q106 — DONE (volt, 2026-10-29, at the head this record's commit names; sol lane by authorship, unclaimed and taken). `CHECK-2026-09-28` now grades the tree it ships in: a clone whose own manifest … | -- | +| 190 | Q102 | record | 13448-13460 | 4986 | cont: DONE | **Q102 — DONE (volt, 2026-10-29, at `062e278`; beam lane by authorship, unclaimed and taken). `majority()` now has three outcomes on three channels, and a non-reflexive element that fills the list is … | -- | ## Ids with no `## ` heading @@ -217,7 +218,7 @@ Opened as records inside another section. `line` is the record's own opening lin | Q99 | 12749 | 9909 | 33 | Q98 | BULLET+BOLD | BOLD-LEAD | | Q100 | 12882 | 12882 | 29 | Q99 | BULLET+BOLD | BOLD-LEAD | | Q101 | 12909 | 12909 | 18 | Q100 | BULLET+BOLD | BOLD-LEAD | -| Q102 | 12985 | 12985 | 9 | Q100 | BULLET+BOLD | BOLD-LEAD | +| Q102 | 12985 | 12985 | 10 | Q100 | BULLET+BOLD | BOLD-LEAD | | Q103 | 13010 | 13010 | 18 | Q102 | BULLET+BOLD | BOLD-LEAD | | Q104 | 13103 | 13103 | 18 | Q101 | BULLET+BOLD | BOLD-LEAD | | Q105 | 13135 | 13135 | 16 | Q104 | BULLET+BOLD | BOLD-LEAD | @@ -1435,17 +1436,30 @@ The block below is the parse this document renders, byte for byte. `--check` com "title": "**Q117 \u2014 DONE (zeph, 2026-10-29; zeph lane by authorship; measured at `ebba640`, this record and `docs/queue-index.md` land in the commit above it).** The `KNOWN_PROBE` table now sits beside \u2026" }, { - "bytes": 5421, - "end_line": 13446, + "bytes": 5422, + "end_line": 13447, "id": "Q106", "kind": "record", "line": 13434, - "lines": 13, + "lines": 14, "role": "continuation", "role_detail": "DONE", "status": null, "status_line": null, "title": "**Q106 \u2014 DONE (volt, 2026-10-29, at the head this record's commit names; sol lane by authorship, unclaimed and taken). `CHECK-2026-09-28` now grades the tree it ships in: a clone whose own manifest \u2026" + }, + { + "bytes": 4986, + "end_line": 13460, + "id": "Q102", + "kind": "record", + "line": 13448, + "lines": 13, + "role": "continuation", + "role_detail": "DONE", + "status": null, + "status_line": null, + "title": "**Q102 \u2014 DONE (volt, 2026-10-29, at `062e278`; beam lane by authorship, unclaimed and taken). `majority()` now has three outcomes on three channels, and a non-reflexive element that fills the list is \u2026" } ], "ids_multi_section": { @@ -4028,17 +4042,30 @@ The block below is the parse this document renders, byte for byte. `--check` com "title": "**Q117 \u2014 DONE (zeph, 2026-10-29; zeph lane by authorship; measured at `ebba640`, this record and `docs/queue-index.md` land in the commit above it).** The `KNOWN_PROBE` table now sits beside \u2026" }, { - "bytes": 5421, - "end_line": 13446, + "bytes": 5422, + "end_line": 13447, "id": "Q106", "kind": "record", "line": 13434, - "lines": 13, + "lines": 14, "role": "continuation", "role_detail": "DONE", "status": null, "status_line": null, "title": "**Q106 \u2014 DONE (volt, 2026-10-29, at the head this record's commit names; sol lane by authorship, unclaimed and taken). `CHECK-2026-09-28` now grades the tree it ships in: a clone whose own manifest \u2026" + }, + { + "bytes": 4986, + "end_line": 13460, + "id": "Q102", + "kind": "record", + "line": 13448, + "lines": 13, + "role": "continuation", + "role_detail": "DONE", + "status": null, + "status_line": null, + "title": "**Q102 \u2014 DONE (volt, 2026-10-29, at `062e278`; beam lane by authorship, unclaimed and taken). `majority()` now has three outcomes on three channels, and a non-reflexive element that fills the list is \u2026" } ], "nested": [ @@ -4164,7 +4191,7 @@ The block below is the parse this document renders, byte for byte. `--check` com "form": "BOLD-LEAD", "id": "Q102", "line": 12985, - "refs": 9, + "refs": 10, "section": "Q100", "shape": "BULLET+BOLD" }, @@ -4372,9 +4399,9 @@ The block below is the parse this document renders, byte for byte. `--check` com "end_line": 8, "line": 1 }, - "queue_bytes": 2101053, - "queue_lines": 13446, - "queue_sha256": "0a700d5d26e320ac137967c200edcfe29947ae73b68a64dcabf83c101e809b62", + "queue_bytes": 2106040, + "queue_lines": 13460, + "queue_sha256": "8d84d274f28f900e55a8a86122b3d640f15b15410365e9b391eff2e63e4c5e8a", "sections": [ { "bytes": 3139, diff --git a/wizard-work/beam/majority_element.py b/wizard-work/beam/majority_element.py index 3d14f06..9a9ac21 100644 --- a/wizard-work/beam/majority_element.py +++ b/wizard-work/beam/majority_element.py @@ -1,22 +1,64 @@ -"""Boyer-Moore majority vote: element appearing > n/2 times, or None.""" +"""Boyer-Moore majority vote: the element occurring more than n/2 times. + +Three outcomes, three channels -- the caller can always tell them apart: + + - empty input -> raise EmptySequence (a ValueError) + - no element occurs > n/2 -> raise NoMajority (a ValueError) + - otherwise -> return the element itself, which may be + None, float('nan'), or any other object. + +The old contract returned None for all three, so a None majority, an empty +list and a no-majority list were one value. + +The confirmation pass counts with `_same`, not `list.count`: identity first, +then NaN == NaN, then `==`. That is what lets a non-reflexive element be +confirmed when it fills the list -- repeated NaN objects count as equal even +though `nan != nan`, and a repeated object whose `__eq__` always answers False +is still recognised by identity. Note that distinct objects of such a class +are *not* conflated: only identity, NaN-ness and `==` count. Python's own +equality still rules elsewhere, so `True` and `1` remain one value. + +Boyer-Moore's first pass is unchanged: O(n) time, O(1) extra space, one +materialisation of the iterable (so a generator is accepted). +""" from __future__ import annotations -from typing import Any, Iterable, Optional +from typing import Any, Iterable -def majority(nums: Iterable[Any]) -> Optional[Any]: +class EmptySequence(ValueError): + """majority() was given nothing to vote on.""" + + +class NoMajority(ValueError): + """No element occurs more than len(seq) // 2 times.""" + + +def _same(a: Any, b: Any) -> bool: + """The counting relation: identity, then NaN equality, then `==`.""" + if a is b: + return True + if isinstance(a, float) and isinstance(b, float) and a != a and b != b: + return True + return a == b + + +def majority(nums: Iterable[Any]) -> Any: arr = list(nums) - cand = None + if not arr: + raise EmptySequence("majority() of an empty sequence") + cand = arr[0] count = 0 for x in arr: if count == 0: cand = x count = 1 - elif x == cand: + elif _same(x, cand): count += 1 else: count -= 1 - if not arr: - return None - if arr.count(cand) > len(arr) // 2: + if sum(1 for x in arr if _same(x, cand)) * 2 > len(arr): return cand - return None + raise NoMajority( + "no element occurs more than %d times in a %d-element sequence" + % (len(arr) // 2, len(arr)) + ) diff --git a/wizard-work/beam/test_majority_element.py b/wizard-work/beam/test_majority_element.py index 04b70a6..30b0e69 100644 --- a/wizard-work/beam/test_majority_element.py +++ b/wizard-work/beam/test_majority_element.py @@ -1,14 +1,112 @@ -import unittest -from majority_element import majority +"""Contract cells for majority(): three outcomes, three channels (Q102). -class TestMajority(unittest.TestCase): - def test_basic(self): - self.assertEqual(majority([1,1,2]), 1) - self.assertEqual(majority([2,2,1,1,1]), 1) - self.assertIsNone(majority([1,2,3])) - self.assertIsNone(majority([])) +The old suite pinned six int cases and asserted `is None` for both the +no-majority and empty lists -- the two cases the module collapsed. These +cells pin the distinction itself: an element is returned (even None or NaN), +an empty sequence raises EmptySequence, a sequence without a majority raises +NoMajority, and the two exceptions are different types. +""" +import math +import random +import unittest +from collections import Counter + +from majority_element import EmptySequence, NoMajority, majority + + +class NeverEqual: + """An object whose `==` is never true, not even against itself.""" + + def __eq__(self, other): + return False + + def __ne__(self, other): + return True + + def __hash__(self): + raise TypeError("NeverEqual is not hashable") + + +class TestMajorityContract(unittest.TestCase): + def test_int_majorities_still_return_the_element(self): + self.assertEqual(majority([1, 1, 2]), 1) + self.assertEqual(majority([2, 2, 1, 1, 1]), 1) self.assertEqual(majority([5]), 5) - self.assertEqual(majority([1,1,1,2,2,2,2]), 2) + self.assertEqual(majority([1, 1, 1, 2, 2, 2, 2]), 2) + + def test_empty_sequence_raises_empty_sequence(self): + with self.assertRaises(EmptySequence): + majority([]) + # Empty is its own outcome, not the no-majority outcome. + self.assertFalse(issubclass(EmptySequence, NoMajority)) + + def test_no_majority_raises_no_majority(self): + for case in ([1, 2, 3], [1, 2, 1, 2], [0, 1] * 1000, [1, 1, 2, 2, 3]): + with self.assertRaises(NoMajority): + majority(case) + + def test_none_majority_is_returned_not_raised(self): + self.assertIsNone(majority([None, None, 1])) + self.assertIsNone(majority([None])) + + def test_empty_and_no_majority_are_distinct_types(self): + self.assertIsNot(EmptySequence, NoMajority) + self.assertTrue(issubclass(EmptySequence, ValueError)) + self.assertTrue(issubclass(NoMajority, ValueError)) + with self.assertRaises(ValueError) as empty_exc: + majority([]) + with self.assertRaises(ValueError) as no_majority_exc: + majority([1, 2, 3]) + self.assertIs(type(empty_exc.exception), EmptySequence) + self.assertIs(type(no_majority_exc.exception), NoMajority) + + def test_shared_nan_object_is_a_majority(self): + nan = float("nan") + self.assertIs(majority([nan, nan, 1.0]), nan) + + def test_distinct_nan_objects_are_a_majority(self): + # IEEE says nan != nan; this module's counting rule says NaN == NaN, + # so a list that is two-thirds NaN has a majority, and it is a nan. + a, b = float("nan"), float("nan") + result = majority([a, b, 1.0]) + self.assertIsInstance(result, float) + self.assertTrue(math.isnan(result)) + + def test_nonreflexive_element_counts_by_identity(self): + obj = NeverEqual() + self.assertIs(majority([obj, obj, 1]), obj) + # Two distinct never-equal objects are two different values, so this + # is not a majority -- the rule must not conflate them. + with self.assertRaises(NoMajority): + majority([NeverEqual(), NeverEqual(), 1]) + + def test_generator_is_accepted_and_consumed_once(self): + self.assertEqual(majority(iter([1, 1, 2])), 1) + with self.assertRaises(EmptySequence): + majority(iter(())) + + def test_differential_against_counter(self): + rng = random.Random(102) + for _ in range(2000): + arr = [rng.choice([0, 1, 2, 3]) for _ in range(rng.randrange(0, 10))] + if not arr: + with self.assertRaises(EmptySequence): + majority(arr) + continue + counts = Counter(arr) + winner = [v for v, c in counts.items() if c * 2 > len(arr)] + if winner: + self.assertEqual(majority(arr), winner[0], arr) + else: + with self.assertRaises(NoMajority, msg=repr(arr)): + majority(arr) + + def test_messages_name_the_threshold(self): + with self.assertRaisesRegex(NoMajority, "more than 1 times in a 3-element"): + majority([1, 2, 3]) + with self.assertRaisesRegex(EmptySequence, "empty sequence"): + majority([]) + if __name__ == "__main__": unittest.main()