From 0867a1b11a04dbbac67499f26d6edfe39a8a69a0 Mon Sep 17 00:00:00 2001 From: Ben Westgate Date: Sun, 27 Sep 2026 11:36:01 -0500 Subject: [PATCH] cli: Remove pre-review dead code Remove unreachable creation guards and the permanently false correction ambiguity field, align the CLI test Core stub with production, and move reference-only correction helpers out of the installed package.\n\nSecurity: fail-closed correction and wallet behavior are unchanged.\n\nRefs #38. --- pyproject.toml | 1 - src/codex32/_cli_input.py | 18 +++++----- src/codex32/cli.py | 10 ++---- src/codex32/indel.py | 40 ---------------------- tests/test_cli.py | 16 ++++----- tests/test_correction_disclosure.py | 10 +++--- tests/test_correction_indel.py | 4 +-- tools/correction_benchmark.py | 3 +- tools/correction_reference.py | 53 +++++++++++++++++++++++++++++ 9 files changed, 78 insertions(+), 77 deletions(-) create mode 100644 tools/correction_reference.py diff --git a/pyproject.toml b/pyproject.toml index ce4a9fc..369196a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -48,7 +48,6 @@ dev = [ "twine>=5,<7", ] - [tool.setuptools.packages.find] where = ["src"] diff --git a/src/codex32/_cli_input.py b/src/codex32/_cli_input.py index 6693af6..99630d7 100644 --- a/src/codex32/_cli_input.py +++ b/src/codex32/_cli_input.py @@ -456,7 +456,7 @@ def _correction_candidates( fingerprint_match: Callable[[CorrectionCandidate], bool | None] | None = None, seed_candidates: Sequence[CorrectionCandidate] = (), required_only: bool = False, -) -> tuple[tuple[CorrectionCandidate, ...], bool, float, bool]: +) -> tuple[tuple[CorrectionCandidate, ...], bool, float]: count = len(value.replace(" ", "")) targets, primary, reduced, _timed = _correction_plan(profile, byte_length, count, target) deadline = monotonic() + 10 if deadline is None else deadline @@ -484,7 +484,7 @@ def _correction_candidates( if len(candidates) == 1 and not candidates[0].search_complete else () ) - return results, complete, deadline, False + return results, complete, deadline def _scheduled_candidates( @@ -499,7 +499,7 @@ def _scheduled_candidates( allowed: Callable[[CorrectionCandidate], bool] | None = None, deadline: float | None = None, fingerprint_match: Callable[[CorrectionCandidate], bool | None] | None = None, -) -> tuple[tuple[CorrectionCandidate, ...], bool, bool]: +) -> tuple[tuple[CorrectionCandidate, ...], bool]: """Search both case interpretations under one deadline and capture ledger.""" search = partial( _correction_candidates, @@ -519,18 +519,18 @@ def _scheduled_candidates( # These discovery passes deliberately do not charge capture_layers; # the full searches below account each admitted frontier once. for required_value in (first, retry): - seeded, complete, deadline, _ = search( + seeded, complete, deadline = search( required_value, deadline=deadline, seed_candidates=seeded, required_only=True ) if not complete: - return (), False, False + return (), False capture_layers: list[tuple[int, int]] = [] - candidates, complete, deadline, ambiguous = search( + candidates, complete, deadline = search( first, deadline=deadline, capture_layers=capture_layers, seed_candidates=seeded ) if retry is None: - return candidates, complete, ambiguous - retry_candidates, complete, _deadline, retry_ambiguous = search( + return candidates, complete + retry_candidates, complete, _deadline = search( retry, deadline=deadline, capture_layers=capture_layers, seed_candidates=(*seeded, *candidates) ) annotated = [] @@ -540,7 +540,7 @@ def _scheduled_candidates( unique: dict[str, CorrectionCandidate] = {} for item in _best(annotated, prefer_common=byte_length == "?", fingerprint_match=fingerprint_match): unique.setdefault(item.artifact.text.lower(), item) - return tuple(unique.values()), complete, ambiguous or retry_ambiguous + return tuple(unique.values()), complete def _fingerprint_matcher( diff --git a/src/codex32/cli.py b/src/codex32/cli.py index fe72bce..b20bddd 100644 --- a/src/codex32/cli.py +++ b/src/codex32/cli.py @@ -426,8 +426,6 @@ def _create( raise _UsageError("--bytes applies only to a new random seed.") if not (sys.stdin.isatty() and sys.stdout.isatty()): raise _UsageError("Bitcoin backup creation requires an interactive terminal.") - if threshold and not sys.stdin.isatty(): - raise _UsageError("Shared creation requires an interactive terminal.") if threshold and shares is None and indices is None: if threshold in (2, 3): shares = {2: 3, 3: 5}[threshold] @@ -572,15 +570,11 @@ def _correct( candidate = None if candidate is not None: candidates: tuple[CorrectionCandidate, ...] = (candidate,) - complete, ambiguous = True, False + complete = True else: - candidates, complete, ambiguous = _scheduled_candidates( - search_value, erased, hrp, byte_length, immutable - ) + candidates, complete = _scheduled_candidates(search_value, erased, hrp, byte_length, immutable) if not complete and not candidates: raise _CommandError("The correction search did not complete within ten seconds.") - if ambiguous: - raise _CommandError("More than one correction is possible; none was selected.") if not candidates: raise _CommandError("No valid correction found. Check the original backup.") if context.master_seed and len(candidates) > 1: diff --git a/src/codex32/indel.py b/src/codex32/indel.py index ca55bea..c71f319 100644 --- a/src/codex32/indel.py +++ b/src/codex32/indel.py @@ -88,8 +88,6 @@ def _classes(unit: int, depth: int) -> tuple[_StructuralClass, ...]: _CHARACTER_CLASSES = _classes(1, 4) _GROUP_CLASSES = _classes(4, 2) _CLASSES = (_FIXED, *_CHARACTER_CLASSES, *_GROUP_CLASSES) -# Compatibility for older offline tools; public search uses the full class set. -_REDUCED_CLASSES = tuple(shape for shape in _CLASSES if shape.unit == 4 or shape.distance <= 3) def _group_boundary(immutable_length: int) -> int: @@ -161,44 +159,6 @@ def _alignment_counts( return counts -def _alignment_count( - shape: _StructuralClass, - observed_length: int, - target_length: int, - immutable_length: int, -) -> int: - return sum(_alignment_counts(shape, "q" * observed_length, target_length, immutable_length).values()) - - -def _reductions( - values: tuple[int, ...], - characters: str, - count: int, - offset: int, -) -> Iterator[tuple[tuple[int, ...], tuple[tuple[int, str], ...]]]: - for deleted in combinations(range(len(values)), count): - removed = frozenset(deleted) - kept = tuple(index for index in range(len(values)) if index not in removed) - retained = tuple(values[index] for index in kept) - position = 0 - for kept_index in kept: - while values[position] != values[kept_index]: - position += 1 - if position != kept_index: - break - position += 1 - else: - edits = tuple((offset + index, characters[index]) for index in deleted) - yield retained, edits - - -def _variants( - text: str, target: int, shape: _StructuralClass, immutable: int, prefix_length: int = 3 -) -> Iterator[_Variant]: - for view in _views(text, target, shape, immutable, prefix_length): - yield _view_variant(view, text, prefix_length) - - def _views(text: str, target: int, shape: _StructuralClass, immutable: int, base: int) -> Iterator[_View]: source = tuple(CHARSET.find(char.lower()) for char in text[base:]) initial = _View(source, ((0, len(source)),), len(source)) diff --git a/tests/test_cli.py b/tests/test_cli.py index d89e317..02e74c3 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -91,7 +91,6 @@ class _FakeBitcoinCore: chain: str = "main" version: int = 320000 imported: MasterSeed | None = None - private: bool | None = None account: int | None = None timestamp: int | str | None = None @@ -112,7 +111,7 @@ def initialize( timestamp: int | str = "now", ) -> str: self.imported = secret - self.private, self.account, self.timestamp = private, account, timestamp + self.account, self.timestamp = account, timestamp return "test-wallet" @@ -1785,7 +1784,7 @@ def test_correction_bytes_rejects_an_unsupported_ms_size() -> None: def test_cli_never_accepts_an_incomplete_structural_search() -> None: original = VECTOR_1["secret_s"] damaged = original[:19] + original[20:] - with patch("codex32._cli_input._correction_candidates", return_value=((), False, 0.0, False)): + with patch("codex32._cli_input._correction_candidates", return_value=((), False, 0.0)): result = _invoke(["correct"], damaged) assert result.exit_code == 3 @@ -1924,7 +1923,7 @@ def test_correct_searches_mixed_case_erasures_before_normalized_alignment(monkey def stop_after_first(value, *_args, **_kwargs): searched.append(value) - return (), False, None, False + return (), False, None monkeypatch.setattr("codex32._cli_input._correction_candidates", stop_after_first) @@ -1983,12 +1982,12 @@ def test_correct_accounts_retry_frontier_after_incomplete_first_search(monkeypat def incomplete_first(value, *_args, **kwargs): if kwargs.get("required_only"): - return (), True, 0.0, False + return (), True, 0.0 full_searches.append(value) kwargs["capture_layers"].append((1, 5)) if len(full_searches) == 1: - return (candidate,), False, 0.0, False - return (), False, 0.0, False + return (candidate,), False, 0.0 + return (), False, 0.0 monkeypatch.setattr("codex32._cli_input._correction_candidates", incomplete_first) @@ -2022,7 +2021,6 @@ def test_wallet_commands_initialize_selected_master_seed_destinations() -> None: assert xprv.stderr.endswith("Keep it secret.\n\n") assert private.stdout == "" assert private_core.imported == parse_codex32(VECTOR_1["secret_s"]) - assert private_core.private is True assert "Warning: This gives Bitcoin Core the master private key, which can spend funds." in private.stderr assert "Use only the intended encrypted wallet" not in private.stderr assert "\x1b[" not in private.stderr + private.stdout @@ -2900,7 +2898,7 @@ def test_incomplete_candidate_has_no_search_warning_and_is_never_accepted_automa candidate = _correct_fixed(source, suspected_profile=Profile.MS) assert candidate is not None candidate = replace(candidate, search_complete=False) - with patch("codex32._cli_input._correction_candidates", return_value=((candidate,), False, 0.0, False)): + with patch("codex32._cli_input._correction_candidates", return_value=((candidate,), False, 0.0)): result = _invoke(["correct"], source[:-1] + "?") assert result.exit_code == 1 and result.stdout == "" assert "Search incomplete" not in result.stderr diff --git a/tests/test_correction_disclosure.py b/tests/test_correction_disclosure.py index 6c9b262..abcac50 100644 --- a/tests/test_correction_disclosure.py +++ b/tests/test_correction_disclosure.py @@ -100,7 +100,7 @@ def test_noninteractive_gate_emits_only_operational_error(entrypoint, plain): patch.object(sys, "stdin", io.StringIO(VECTOR_1["secret_s"][:-1] + "?")), contextlib.redirect_stdout(stdout), contextlib.redirect_stderr(stderr), - patch.object(_cli_input, "_correction_candidates", return_value=((_candidate(),), True, None, False)), + patch.object(_cli_input, "_correction_candidates", return_value=((_candidate(),), True, None)), ): status = entrypoint(["correct", *(["--plain"] if plain else [])]) prog = "codex32" if entrypoint is cli.main else "ms32" @@ -128,7 +128,7 @@ def respond(prompt, prefill=""): monkeypatch.setattr(_cli_input, "_suggestions", lambda *args, **kwargs: (candidate,)) monkeypatch.setattr(cli, "_suggestions", lambda *args, **kwargs: (candidate,)) monkeypatch.setattr( - _cli_input, "_correction_candidates", lambda *args, **kwargs: ((candidate,), True, None, False) + _cli_input, "_correction_candidates", lambda *args, **kwargs: ((candidate,), True, None) ) core = _FakeBitcoinCore() monkeypatch.setattr(cli.BitcoinCore, "connect", lambda *args: core) @@ -164,7 +164,7 @@ def respond(prompt, prefill=""): monkeypatch.setattr(_cli_input, "_editable_input", respond) monkeypatch.setattr( - _cli_input, "_correction_candidates", lambda *args, **kwargs: ((candidate,), True, None, False) + _cli_input, "_correction_candidates", lambda *args, **kwargs: ((candidate,), True, None) ) with ( patch.object(sys, "stdin", _TTYInput()), @@ -182,7 +182,7 @@ def test_redirected_stderr_blocks_low_discrimination_disclosure(monkeypatch): responses = iter((source[:-1] + "?",)) monkeypatch.setattr(_cli_input, "_editable_input", lambda *args, **kwargs: next(responses)) monkeypatch.setattr( - _cli_input, "_correction_candidates", lambda *args, **kwargs: ((candidate,), True, None, False) + _cli_input, "_correction_candidates", lambda *args, **kwargs: ((candidate,), True, None) ) stdout, stderr = io.StringIO(), io.StringIO() with ( @@ -219,7 +219,7 @@ def confirm(prompt): monkeypatch.setattr(_cli_input, "_confirmation_input", confirm) monkeypatch.setattr( - _cli_input, "_correction_candidates", lambda *args, **kwargs: ((candidate,), True, None, False) + _cli_input, "_correction_candidates", lambda *args, **kwargs: ((candidate,), True, None) ) stdout, stderr = io.StringIO(), _TTYOutput() with ( diff --git a/tests/test_correction_indel.py b/tests/test_correction_indel.py index 05d6d97..646e8b5 100644 --- a/tests/test_correction_indel.py +++ b/tests/test_correction_indel.py @@ -19,14 +19,11 @@ _CLASSES, _FIXED, _GROUP_CLASSES, - _REDUCED_CLASSES, - _alignment_count, _alignment_counts, _capacities, _frontier, _keep, _prepare, - _reductions, _required_header_substitutions, _search_many, _search_target, @@ -34,6 +31,7 @@ from codex32.profiles.ms32 import TEXT_LENGTHS from tools._wallet_test_vectors import FIXTURE_SEED, core_fingerprint from tools.correction_capture import cross_length_classes +from tools.correction_reference import _REDUCED_CLASSES, _alignment_count, _reductions SOURCE = VECTOR_1["secret_s"] CONTEXT = CorrectionContext(Profile.MS, expected_length=len(SOURCE)) diff --git a/tools/correction_benchmark.py b/tools/correction_benchmark.py index f0e74eb..922632d 100644 --- a/tools/correction_benchmark.py +++ b/tools/correction_benchmark.py @@ -15,13 +15,12 @@ from codex32.correction import CorrectionContext from codex32.indel import ( _CHARACTER_CLASSES, - _alignment_count, _required_header_substitutions, _search_many, - _variants, ) from codex32.profiles import Profile from codex32.profiles.ms32 import TEXT_LENGTHS +from tools.correction_reference import _alignment_count, _variants DELTAS = (-8, -4, -3, -2, -1, 0, 1, 2, 3, 4, 8) AUTOMATIC_48_COUNTS = (40, *range(44, 53), 56) diff --git a/tools/correction_reference.py b/tools/correction_reference.py new file mode 100644 index 0000000..b55bd40 --- /dev/null +++ b/tools/correction_reference.py @@ -0,0 +1,53 @@ +"""Reference-only structural helpers used by tests and correction benchmarks.""" + +from collections.abc import Iterator +from itertools import combinations + +from codex32.indel import ( + _CLASSES, + _alignment_counts, + _StructuralClass, + _Variant, + _view_variant, + _views, +) + +_REDUCED_CLASSES = tuple(shape for shape in _CLASSES if shape.unit == 4 or shape.distance <= 3) + + +def _alignment_count( + shape: _StructuralClass, + observed_length: int, + target_length: int, + immutable_length: int, +) -> int: + return sum(_alignment_counts(shape, "q" * observed_length, target_length, immutable_length).values()) + + +def _reductions( + values: tuple[int, ...], + characters: str, + count: int, + offset: int, +) -> Iterator[tuple[tuple[int, ...], tuple[tuple[int, str], ...]]]: + for deleted in combinations(range(len(values)), count): + removed = frozenset(deleted) + kept = tuple(index for index in range(len(values)) if index not in removed) + retained = tuple(values[index] for index in kept) + position = 0 + for kept_index in kept: + while values[position] != values[kept_index]: + position += 1 + if position != kept_index: + break + position += 1 + else: + edits = tuple((offset + index, characters[index]) for index in deleted) + yield retained, edits + + +def _variants( + text: str, target: int, shape: _StructuralClass, immutable: int, prefix_length: int = 3 +) -> Iterator[_Variant]: + for view in _views(text, target, shape, immutable, prefix_length): + yield _view_variant(view, text, prefix_length)