Skip to content

Commit 128bda4

Browse files
committed
correct: Preserve mixed-case preflight seeds
After both mixed-case interpretations complete their required preflight, the CLI competitor scheduler must not rerun that same required work under the already-consumed shared deadline. Restrict only the executed follow-up work to optional character classes while retaining the full admitted frontier for cumulative disclosure accounting. This preserves a required-pass candidate if optional work reaches the deadline and keeps the public capture-mass calculation unchanged. The regression asserts that both full mixed-case follow-up searches enter optional-only mode. Refs #37.
1 parent 1435560 commit 128bda4

5 files changed

Lines changed: 27 additions & 19 deletions

File tree

‎src/codex32/_cli_input.py‎

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -456,6 +456,7 @@ def _correction_candidates(
456456
fingerprint_match: Callable[[CorrectionCandidate], bool | None] | None = None,
457457
seed_candidates: Sequence[CorrectionCandidate] = (),
458458
required_only: bool = False,
459+
optional_only: bool = False,
459460
) -> tuple[tuple[CorrectionCandidate, ...], bool, float]:
460461
count = len(value.replace(" ", ""))
461462
targets, primary, reduced, _timed = _correction_plan(profile, byte_length, count, target)
@@ -474,6 +475,7 @@ def _correction_candidates(
474475
capture_layers=capture_layers,
475476
seed_candidates=seed_candidates,
476477
required_only=required_only,
478+
optional_only=optional_only,
477479
)
478480
if allowed is not None:
479481
candidates = tuple(candidate for candidate in candidates if allowed(candidate))
@@ -525,13 +527,12 @@ def _scheduled_candidates(
525527
if not complete:
526528
return (), False
527529
capture_layers: list[tuple[int, int]] = []
528-
candidates, complete, deadline = search(
529-
first, deadline=deadline, capture_layers=capture_layers, seed_candidates=seeded
530-
)
530+
full_search = partial(search, capture_layers=capture_layers, optional_only=retry is not None)
531+
candidates, complete, deadline = full_search(first, deadline=deadline, seed_candidates=seeded)
531532
if retry is None:
532533
return candidates, complete
533-
retry_candidates, retry_complete, _deadline = search(
534-
retry, deadline=deadline, capture_layers=capture_layers, seed_candidates=(*seeded, *candidates)
534+
retry_candidates, retry_complete, _deadline = full_search(
535+
retry, deadline=deadline, seed_candidates=(*seeded, *candidates)
535536
)
536537
complete = complete and retry_complete
537538
annotated = []

‎src/codex32/_competitors.py‎

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -175,18 +175,23 @@ def _search_competitors(
175175
allowed: Callable[[CorrectionCandidate], bool] | None,
176176
*,
177177
seed_candidates: Sequence[CorrectionCandidate] = (),
178+
optional_only: bool = False,
178179
) -> tuple[tuple[CorrectionCandidate, ...], bool]:
179180
results = {candidate.artifact.text.lower(): candidate for candidate in seed_candidates}
180181
fixed: dict[int, CorrectionCandidate | None] = {}
181182
completed: set[_Layer] = set()
182183
try:
183184
for state in states:
184-
if _FIXED in state.counts:
185+
if not optional_only and _FIXED in state.counts:
185186
_check_deadline(deadline)
186187
fixed[state.target] = _search_fixed(state, frontier, results, allowed)
187188
targets = {state.target: state for state in states}
188189
layers = sorted(
189-
(key for key in frontier if key[1] != _FIXED),
190+
(
191+
key
192+
for key in frontier
193+
if key[1] != _FIXED and (not optional_only or key[1].unit != 4 and key[1].distance > 2)
194+
),
190195
key=lambda key: (_tier(key[1]), frontier[key]),
191196
)
192197
for key in layers:

‎src/codex32/indel.py‎

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -606,16 +606,13 @@ def finish(
606606
if competitors:
607607
from codex32._competitors import _search_competitors
608608

609-
result = (
610-
_search_competitors(
611-
states,
612-
frontier,
613-
deadline,
614-
allowed,
615-
seed_candidates=seed_candidates,
616-
)
617-
if seed_candidates
618-
else _search_competitors(states, frontier, deadline, allowed)
609+
result = _search_competitors(
610+
states,
611+
frontier,
612+
deadline,
613+
allowed,
614+
seed_candidates=seed_candidates,
615+
optional_only=optional_only,
619616
)
620617
return finish(*result)
621618
results = {candidate.artifact.text.lower(): candidate for candidate in seed_candidates}

‎tests/test_correction_disclosure.py‎

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,9 @@ def test_scheduler_annotates_from_admission_even_when_no_work_finished(monkeypat
6060
(48, indel._StructuralClass(0, 0, adjacent=2), 0, 0): rank + 1,
6161
}
6262
monkeypatch.setattr(indel, "_frontier", lambda *args: frontier)
63-
monkeypatch.setattr("codex32._competitors._search_competitors", lambda *args: ((candidate,), complete))
63+
monkeypatch.setattr(
64+
"codex32._competitors._search_competitors", lambda *args, **kwargs: ((candidate,), complete)
65+
)
6466
result, finished = indel._search_many(
6567
(CorrectionContext("ms", 48),),
6668
source,
@@ -309,7 +311,9 @@ def test_residue_exactly_five_bits_is_not_gated_even_with_zero_addends(residue):
309311
def test_previous_case_interpretation_search_is_charged_even_without_a_candidate(monkeypatch):
310312
candidate = replace(_candidate(), capture_volume=(1 << 60) + 1)
311313
monkeypatch.setattr(indel, "_frontier", lambda *args: {(48, indel._FIXED, 0, 0): 1})
312-
monkeypatch.setattr("codex32._competitors._search_competitors", lambda *args: ((candidate,), True))
314+
monkeypatch.setattr(
315+
"codex32._competitors._search_competitors", lambda *args, **kwargs: ((candidate,), True)
316+
)
313317
previous = [(1 << 60, 65)]
314318
result, _ = indel._search_many(
315319
(CorrectionContext("ms", 48),),

‎tests/test_mixed_case_deadline.py‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ def test_scheduled_truncation_survives_a_complete_later_pass(monkeypatch: pytest
6666
def truncated_first(value: str, *_args: object, **kwargs: object) -> tuple[object, bool, float]:
6767
if kwargs.get("required_only"):
6868
return (), True, 0.0
69+
assert kwargs.get("optional_only") is True
6970
full_searches.append(value)
7071
if len(full_searches) == 1:
7172
return (replace(found[0], search_complete=False),), False, 0.0

0 commit comments

Comments
 (0)