Skip to content

feat: layered-stack solvers, exact columnar, hill-additive, telemetry - #8

Merged
0xdiid merged 3 commits into
mainfrom
feat/layered-stack-solvers
Aug 1, 2026
Merged

feat: layered-stack solvers, exact columnar, hill-additive, telemetry#8
0xdiid merged 3 commits into
mainfrom
feat/layered-stack-solvers

Conversation

@0xdiid

@0xdiid 0xdiid commented Aug 1, 2026

Copy link
Copy Markdown
Owner

No description provided.

0xdiid and others added 3 commits July 31, 2026 22:27
Three decompositions, each turning a joint search into a cheaper staged one.

stack.py / stack_gpu.py — staged attack on stacks of transpositions under a
periodic substitution. A transposition preserves monogram frequencies, so a
periodic substitution sitting OUTSIDE any stack of transpositions peels off the
RAW ciphertext by per-coset chi-square, with no transposition search at all.
That fails when cosets are short (a period-45 key over 224 letters leaves 5
letters per coset), so the coupled search in layered.crack_layered is kept
alongside it. Batching the inner DP across candidates on GPU — one kernel launch
per mask for the whole batch — gives 124x: 99k-123k evals/s against 799 on CPU,
verified bit-equal against the scalar solver.

Measured depth curve on width-9 columnars over ~315 letters: depth 1 exact with
no search, depth 2 exact in 2.3s on GPU, depth 3 does not converge. That last one
is a SIGNAL wall, not a compute wall — 6.5M evaluations, best score flat after
generation 10 for 390 more generations. At depth 2 a correct outer order
immediately exposes English through the exact inner solve, so there is a
gradient; at depth 3 both outer layers must be near-right before anything shows,
so single-swap mutations get no partial credit. Unicity is not the obstacle
(~55 bits against ~1000 bits of English redundancy at that length). Documented in
the module so nobody spends a week throwing hardware at it.

columnar_exact.py — the innermost columnar of a stack is exactly solvable. Its
columns are PLAINTEXT columns, so the objective decomposes pairwise and Held-Karp
gives the optimum in O(2^w * w^2) instead of O(w! * n). Width 9: 41k ops against
363k full decodes; widths 12-16 become reachable. Wide widths are declared out of
reach and refused rather than hanging silently.

hill_affine.py + ciphers/hill_additive.py — CT = M*(P + K) for a Hill matrix over
a periodic additive. Applying any candidate inverse gives M^-1*CT = P + K, so the
additive never has to be searched jointly with the matrix: what is left is an
ordinary periodic Vigenere, solved by chi-square plus a quadgram polish. 26^9
collapses to a dictionary scan.

telemetry.py — live progress and rate reporting for long sweeps. A no-hit sweep
holds a steady rate for its full duration while a hit short-circuits early, so
the rate itself is early evidence, visible from the first heartbeat rather than
at the end.

Two harness lessons encoded as guards: a bare `except: pass` around sub-solvers
hid an ImportError for a whole regression run (a wrong answer won the ranking
instead), and a startswith(16 chars) regression check passed a decode whose error
started at char 17 — regression checks now compare full plaintext.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159mM39E2XA2ExfLsB1VUXW
- optional-dependency sentinels (numpy in columnar_exact/hill_affine, torch in
  stack_gpu) are annotated Any so the None fallback is not an assignment to a
  Module-typed name
- Progress.predict narrows limit before formatting it
- annotate the two inverse-index caches and the candidate list whose element
  type widens across branches
- deep-stack reconstruction raises instead of indexing a possibly-None best
  order, which is also the honest behaviour when no generation scored

No behavioural change; ruff and mypy both clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159mM39E2XA2ExfLsB1VUXW
…out the extra

The previous attempt annotated the sentinels `Any`, which type-checks only when the
module RESOLVES. mypy runs with ignore_missing_imports, so in an environment lacking
the package the import itself binds the name and the None fallback becomes a
redefinition — which is why CI failed on torch while local passed.

- numpy (columnar_exact, hill_affine): match the precedent already used by
  scoring.py and analysis.py — a type: ignore[assignment] on the sentinel. numpy is
  installed in CI, so the ignore is always used and warn_unused_ignores is satisfied.
- torch (stack_gpu): torch is an optional extra and is absent from the CI type
  environment, so neither form works in both — a bare sentinel is Module-vs-None
  where it resolves, and a type: ignore is unused where it does not. Type-check the
  import under TYPE_CHECKING and run the fallback at runtime, which is redefinition-
  free either way.

Verified against two interpreters: the dev venv (torch + numpy present) and a
torch-free venv that reproduces the CI type environment. Clean in both.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159mM39E2XA2ExfLsB1VUXW
@0xdiid
0xdiid merged commit d73b52a into main Aug 1, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant