Skip to content

Tile classification and codebase cleanup#48

Merged
apirogov merged 92 commits into
mainfrom
codebase_cleanup
Jul 16, 2026
Merged

Tile classification and codebase cleanup#48
apirogov merged 92 commits into
mainfrom
codebase_cleanup

Conversation

@apirogov

Copy link
Copy Markdown
Owner

No description provided.

apirogov and others added 30 commits July 13, 2026 16:15
point_mod_rect now returns the representative in the half-open box
*centered* on the anchor -- a true fundamental domain of the shift
lattice width*Z + (height*i)*Z, so every lattice class has exactly one
representative and there is no edge/corner aliasing (a fully-closed box
keeps distinct re=lo and re=hi copies of the same class). Odd dimensions
put the boundaries on half-integers (the origin unit cell is
[-1/2, 1/2)^2), which are not ring elements; each boundary test doubles
into an exact ring sign test (q.re >= c.re + w/2 becomes
(2*(q-anchor) - w_step).re_sign() >= 0), so no non-ring value is ever
built. Half-open on the low side: a point on the upper boundary folds
down onto the lower one.

cyc_explore drops its four-corner seed (all the same torus point) for a
single origin seed, and in unit-square mode draws the fixed centered
frame instead of the point cloud's own extent.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…); track settings.local.json

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Inverse of int_coeffs_slice -- reconstruct a ring element from its integer-basis
coefficients, so generic code can round-trip ring elements through [i64].

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ypes, windowed enumeration

The exact glue/collision machinery in geom/patch.rs grows to support the
classification pipeline: windowed cold-cache candidate enumeration with a
boundary accessor, glue-geometry extraction, junction vertex-type queries,
and TileSet::single / tiles::dodecagon fixtures used repo-wide.
boundary_vertices (the seed-frame vertex list) is public: it is the
reference frame for every replayed placement, so renderers need it.

analysis/vertextypes: inner chains are now FAITHFUL -- every junction's
recorded inner reproduces its actual boundary angle (asserted for ALL
catalog entries). Root cause of the n=12 tiler 700511 false-reject under
the cursed-vertex prune: the lossy inner aliased a closable corona-2
junction onto a dead notch type. The faithfulness property is what makes
the cursed-vertex forward-check prune in the Heesch search SOUND.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Iso<T> (rotation index + ring shift): composition, inverse, tile/point
application, centroid; dir_of_unit / gluing_iso (read the isometry that
glues one directed unit edge onto another -- a translation when
anti-parallel, a half-turn when parallel), cryst_order, and build_orbit
(breadth-first group orbit with radius + cap). The shared algebra under
the periodic detectors and the certificate verifier.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The candidate-filter pipeline's search layer: sound one-sided tests that
bucket enumerated tiles into cannot-tile / periodic / undecided (the
chiral aperiodic-monotile hunt). Scope: single-chirality, edge-to-edge
-- and edge-to-edge is NO LOSS for the cannot-tile claims by the
edge-to-edge reduction theorem (docs/edge_to_edge_reduction.md: slid
fault lines are full, parallel, discrete; strip realignment yields an
e2e tiling), so finite Heesch proves no single-chirality tiling AT ALL.

Accept side (each a partial, verify-gated detector of 'has a k-tile
translation fundamental cell'): the Conway p1/p2 criterion; the
Beauquier-Nivat translation factorization (bn_criterion -- COMPLETE for
pure-translation tilings, the brick-wall class whose bumpy
non-palindromic arcs Conway structurally misses and whose rosette-grain
patches blind the torus grower; learned on the n=14 residue, where 27
of 31 fast-pass survivors were BN tiles); the edge-gluing isohedral
search (p3/p4/p6); the seeded-restart anisohedral cluster search; and
the torus cover (fold a grown patch onto R^2/L). Soundness is 'float
proposes, exact check disposes' end to end: the gold check (per-class
exact surroundedness over a PROVEN-coverage Gauss-reduced block) plus
the exact integer multiplicity gate (cross_2i / signed_area_4i ring
identities). The float-lattice toolbox (lattice.rs) only proposes and
sizes; a wrong float is a miss, never an accept.

Reject side (heesch.rs): TRUE Heesch number by exhaustive corona burial
-- coordinate-keyed frozen-vertex sealing (edge burial alone is only an
upper bound), one branch loop for both phases, concave-first fail-first
ordering, budget-limited with sound Finite-only rejects, deepest-corona
witness banked in the same search, and the lazily-built cursed-vertex
prune (sound by vertex-type faithfulness; measured on the hard tail).

patch_graph.rs carves connected fundamental domains out of grown
patches; grow.rs builds the lattice-exposing corona patches AND replays
stored recipes back into per-tile placements (replay_placements -- the
witness-reuse and rendering primitive).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
cert.rs: a verdict is only useful if someone else can re-check it
without redoing the search. PeriodicCert is the NP-witness of periodic
tiling -- 'build' assembles one fundamental domain from pure edge-index
glue instructions, 'glue' is the self-gluing rule that grows it
unboundedly; verify() replays it through the exact gold check + integer
multiplicity gate. HeeschCert is the cannot-tile half: 'build' is the
sample gap-free k-corona; the co-NP upper half is recorded as the
search outcome (Finite = exhausted = sound reject). Certs carry no
coordinates and reject oversized hostile recipes (REPLAY_CAP).

mint.rs: every detector positive becomes a cert through a verify()
gate -- a mis-carve yields None, never a wrong cert. Includes the
translation minter (BN lattices -> k=1 self-glue, via Translation) and
WITNESS-FIRST detection (witness_torus_cert: a stored corona witness is
routinely deeper than any funnel grow -- the two k=18-domain n=14 tiles
certify from their witnesses in 0.05s where the coronas-4 grower's
57-tile patch was statistically too small; measured, regression-pinned).

cascade.rs: ONE funnel executor (run_stages: walk a Vec<Stage> until a
stage settles the tile, banking Heesch witnesses so the terminal
Undecided verdict reuses the deepest one) and TWO policies as data:
fast_stages and deep_stages, orderings intentionally divergent and
measured -- see the POLICY NOTE.

EVIDENCE: the full n<=14 store re-verifies under these checks --
33,279,563 verdicts (480,286 periodic / 32,799,276 cannot-tile / 1
undecided: THE SPECTRE), reverify_fail 0.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Two drivers over the shared funnel executor plus the store machinery a
200M-tile campaign needs:

- run_fast / run_fast_range (--perim or --from/--to): windowed,
  tile-major, resumable TWO ways -- store lines (fine-grained) and the
  <store>.ranges COVERAGE SIDECAR (whole windows recorded with their
  at-emission P/N/U tallies). The coverage invariant: every tile is
  accounted for by a kept line or a covered range's tally, so boring
  certs can be filtered away without losing the scanned-range record;
  fully-covered relaunches cost microseconds.
- run_deep: re-classifies the Undecided residue, appending each result
  to the <store>.deep.jsonl overlay as it lands (kill-safe, resumable);
  cheap accepts + witness replay sweep the whole residue FIRST so
  instant certifications are never head-of-line blocked behind
  hours-long grinders. run_merge applies the overlay onto the store --
  a cert overrides Undecided, never the reverse.
- run_pack + store_lookup / --pack + --lookup: index-sorted store,
  O(log n) byte-bisection point queries; --lookup accepts a dafsa index
  or a turn word in ANY presentation (rotation/chirality/orientation),
  free-canonicalized via rat_enum::canonical::ccw_free_canonical
  (spectre-pinned: the stored canonical is the lex-min of the MIRROR's
  rotations -- a naive one-chirality lookup misses half of all chiral
  tiles).
- run_verify: replay every kept cert + completeness per perimeter --
  strict all-lines for full stores, coverage-based (ranges + kept lines,
  each tile counted exactly once) for filtered ones.

Validated end to end: n=14 fully classified and verified through these
drivers (28,499,301 tiles; the fast pass in 11.9h, the residue via
overlay+merge; final census P=393,973 / N=28,105,327 / U=1 = the
spectre); coverage roundtrip test (two half-range launches -> filter ->
zero-work relaunch in 0.6ms -> verify COMPLETE via coverage).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The n=15+ workflow: scan a perimeter window-by-window, keep only the
interesting tiles, never lose the scanned-range record, survive any
interruption.

- filter_store.py: drop boring verdict lines under the coverage
  invariant (only lines INSIDE covered ranges may go; uncovered lines
  are their own scan record and are always kept). Keep predicate:
  Undecided / CannotTile with heesch >= K or budget-Unknown / Periodic
  with meta-tile >= K (or none).
- screen.py: the orchestrator loop -- classify one window, filter,
  update <store>.status.json (windows done, cumulative tallies, kept
  lines, rolling rate, ETA), continue; final coverage-verify.
  Deliberately near-stateless: the sidecar is the ground truth, so a
  restart skips covered windows without launching the binary (measured
  57ms for a full-perimeter restart sweep).
- screen_launcher.sh start/stop/status: campaign constants in one
  place; stop kills orchestrator + classifier AND strips the in-flight
  window's partial lines so the harvest stays pure (bounded redo on the
  next start). Campaign data lives on the persistent mount
  (workspace/data/, gitignored) -- /home/ubuntu is container-ephemeral.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Unify the three near-duplicate DFS loops (rat_enum_step, collect_seeds,
and a standalone automaton walk) into one generic
rat_enum_step<ZZ, B: Boundary>. The geometry -- extend/backtrack the
boundary and read the head + turning sequence -- is now the Boundary
trait; the walk (direction order, canonical + reachability prunes,
closure recording, and the seed-split policy that subsumes collect_seeds)
is written once.

Two backends:
- Snake: exact-arithmetic self-avoidance (intersect), the existing path,
  bit-identical after the refactor (the OEIS A316192 pin still passes).
- DominoBoundary: the transfer-matrix automaton in geom::celltable -- the
  head is a (cell, state) advanced by a precomputed transition table (no
  cell_floor), and self-avoidance is a per-cell fragment-conflict bit
  lookup (no multiplication). Same tree, same free-rat set and branch
  stats as Snake; ~1.8-1.9x faster single-threaded on ZZ12.

Works for any ring, not just HasZZ4: the fundamental cell is a
parallelogram of two ring elements {1, unit closest to a quarter turn},
folded by exact wedge-sign tests (on HasZZ4 rings this is the axial unit
square {1, i}). A unit edge provably spans <=1 cell per family, so the
edge-to-cell decomposition is an exact <=3-cell split for every ring
(no superset fallback; a build-time assert pins the invariant, and a
sampling test confirms coverage). Validated automaton == Snake on ZZ6
(hexagonal), ZZ10, and ZZ12; ZZ10 ~1.2-1.6x.

geom::celltable (StateAlphabet + FragmentAlphabet) is plain geometry: it
depends only on cyclotomic, no solver or Z3 involvement, so it lives
under geom, not smt.

Exposed as `rat_enum --mode auto-bench` (free, single-threaded, any ring
via dispatch_ring!). One shared Bench arm selects the backend; no
hot-loop branch.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…umeration

Durable witness renderer (render.rs: corona-ring / rotation colorings) plus its
pipeline hooks: classify_tiles --render <IDX|WORD> draws a tile's maximal corona
patch, and --coronas enumerates all distinct k-coronas of a tile
(heesch::enumerate_coronas), rendering each. Store-less; makes the durable
presentation methods pipeline-reachable.
Generalize run_fast/run_deep/run_verify over <T: IsRing> and dispatch the
concrete ring at runtime via dispatch_ring! (as the rest of the codebase
does), reading the ring and free-per-perimeter counts from the asset
metadata (asset_ring / asset_free_counts). This lets the screen target any
ratdb ring -- e.g. ZZ10 -- not just ZZ12.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Decide whether a tile tiles a scaled-up copy of itself: k^2 rotated/
translated copies (single chirality, no reflections) exactly tiling the
k-scaled tile. Backtracking edge-gluing fill of the fixed region, always
covering the canonical lowest-leftmost uncovered edge; exact ring
arithmetic throughout (point_in_polygon + intersect_unit_segments + an
edge-cancellation frontier). Fixed region + fixed count make it sound AND
complete -- a "no" is a proof, not merely "not found".

RepTileCert { k, build } is a standalone, verify-gated witness: a replayable
PatchMatch recipe (replays via replay_placements, renders via witness_svg)
in its own struct and store, decoupled from the periodic/Heesch verdict
types (cert.rs untouched). verify is frame-independent -- the copies' union
boundary must itself be a k-scaled copy of the tile.

reptile_screen_one adds a per-scale node budget: finding a rep-tiling is
fast, but proving "no" at large k can blow up, so the budget caps that into
an Inconclusive instead of a hang. 11 tests: triangle/square (rep-4/9),
L-tromino/L-tetromino (rep-4, concave), T-tetromino (rep-16), ZZ10 Penrose
rhombi (ring-generic), build round-trip, verify rejection.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
--reptile <IDX|WORD>: rep-tile check of one tile, printing the verdict plus
a serialized (verify-gated) RepTileCert, and optionally rendering the
witness patch to SVG.

--reptile-screen: screen a cert store's PERIODIC tiles for rep-tiles (a
rep-tile tiles the plane, so it is necessarily periodic). Streams the store
(it can be gigabytes), picks periodic candidates with a cheap substring
filter, runs the budgeted fill in parallel (thread::scope), and writes one
<index>\t<RepTileCert> line per hit plus a .inconclusive sidecar for the
budget-capped ones. A fill that finds a tiling but cannot mint a verified
cert is surfaced as an anomaly, never silently dropped.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…match cycle)

The bit-parallel boundary matcher is tiling-specific: it is built from
tiles and emits tile-indexed TileMatch values. It lived in stringmatch
and reached up into geom::matches for its output types, while geom
depended on stringmatch::{match_length, forward_match_length} -- a
production dependency cycle between the two lowest layers.

Move BitParallelMatcher (and its private CyclicBitset helper) into
geom::bitparallel, next to the match vocabulary it produces and its sole
consumer analysis::matchfinder. stringmatch now keeps only the generic,
geometry-agnostic sequence primitives (extend/period/cyclic) plus the
DAFSA store, and no longer imports geom. The dead stringmatch::TileMatch
re-export is dropped.

Pure relocation, no behavior change. Phase 1.1 of the architecture
untangle plan (.claude/superpowers/specs/architecture_untangle_plan.md).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…alysis inversion)

MatchFinder (legal-glue enumeration between two tile boundaries) and
MatchTypeIndex (the catalog of distinct match types over a tileset) are
geometry-over-a-tileset, not "analysis": they depend only on cyclotomic,
geom, and stringmatch -- nothing else in analysis. Yet geom::patch
imported analysis::matchtypes::MatchTypeIndex, a layering inversion
(foundational geometry depending on the high-level analysis layer).

Move both modules into geom (geom::matchfinder, geom::matchtypes), next
to the glue mechanics and match vocabulary they build on. Update the
five consumers' paths (analysis::{matchfinder,matchtypes} -> geom::{..})
and both module docs. Also fix a latent-broken doc link in patch.rs
(TileMatch lives in geom::matches, not matchtypes).

After this, geom depends only on {cyclotomic, stringmatch}; combined
with Phase 1.1, the two lowest layers form a clean acyclic base.

Pure relocation, no behavior change. Phase 1.2 of the architecture
untangle plan (.claude/superpowers/specs/architecture_untangle_plan.md).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The reachable-point BFS needs only unit steps (Units), so free
exploration is ring-generic. Only the unit-square fold needs the
imaginary unit i (the vertical shift is i*height), which exists solely
in HasZZ4 rings -- ZZ6/ZZ10/ZZ14 have no i and thus no unit square to
fold into. (The centered-cell trick already avoids non-ring half-integer
boundary values via doubling + sign tests; that is orthogonal to needing
i for the vertical shift.)

Factor the BFS into one generic explore_with(fold) parametrized by the
per-step representative map; explore_free (identity, all rings) and
explore_folded (point_mod_rect, HasZZ4) are thin wrappers. Free-mode
rendering dispatches over every ring via dispatch_ring!; fold-mode keeps
the HasZZ4-only match (now also covering ZZ32). The CLI rejects a
non-divisible-by-4 ring only when --unit-square is requested.

Verified: ZZ10/ZZ6 free exploration now runs (previously panicked at the
ring%4 guard); ZZ10 --unit-square rejects with a clear message; ZZ12
--unit-square unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The polyomino enumerator's entire engine (Patch, glue-site enumeration,
recursive growth, grid tracing) plus its ~280 lines of tests lived
inline in src/bin/polyomino.rs with no library module -- one of the
inconsistent "logic trapped in the binary" cases. Move the engine and
tests into analysis::polyomino behind one entry point,
enumerate_free_polyominoes(max_size) -> (counts, ProfileStats), and
reduce the bin to arg parsing + printing (709 -> 55 lines).

Delete the dead code the bin carried (make_free stub, enumerate_onesided,
both #[allow(dead_code)]); the OEIS A000105 test now drives the public
entry point. Replace the hand-rolled #[cfg(feature="debug")] pprof
ladder with the shared util::profile::ProfileGuard (--pprof PATH),
matching the other binaries.

Verified: 15 moved tests pass (incl. free-polyomino counts vs OEIS
A000105 through size 12); bin output unchanged.

Phase 2.3 of the architecture untangle plan.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
patch_enum and tileset_collect each defined their own TileSetKind enum
(clap ValueEnum) mapping the same names to the same tileset::
constructors. Hoist one shared TileSetKind into geom::tileset as pure
metadata -- the variant set, a lowercase label(), FromStr, and an ALL
list -- with NO clap derive, so the library stays clap-free (clap parses
it via FromStr; the only cost is a slightly plainer --help value list,
covered by help text).

Each binary keeps its own small ts_zzN builder (they cover different
rings: patch_enum ZZ4/ZZ12, tileset_collect ZZ12/ZZ10), now matching on
the shared enum. Phase 2.1 of the architecture untangle plan.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The tileset_collect binary carried the whole collect/validate driver
inline -- the per-kind collectors and validators, tileset (re)builders,
envelope, and ring/kind dispatch -- over lib primitives that already
existed (NeighborhoodIndex / OpenVertexTypeIndex / SeqExplorer and their
Collection round-trips). Move that driver into analysis::collect behind
two entry points: run_collect(tileset, kind) -> payload and
run_validate(envelope) -> Result. CollectKind + Envelope are pub serde
types; CollectKind parses from the CLI via FromStr (no clap in the lib).
The binary is now clap + IO only (577 -> 98 lines).

Verified: collect square/vtype -> validate round-trips clean (80 types,
384 transitions, "OK").

Phase 2.4 of the architecture untangle plan.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The bin's parallel brute_force_grow (the --validate runtime path) and the
single-threaded one in analysis::patch_enum's tests look like
duplication but are deliberately independent: the test one is a minimal,
obviously-correct oracle that must not share code with what it checks
(enum_patches and the parallel version), or a shared bug could hide in
both. Add a cross-referencing comment on each. No code change.

Phase 2.7 of the architecture untangle plan (resolution: keep separate).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
bin/rat_enum.rs carried ~790 lines of tests (free_tests +
opt_correctness_tests) plus the #[cfg(test)] helper wrappers rat_enum /
rat_enum_free -- all exercising the library through tilezz::rat_enum::*.
Tests belong next to the code they test, so hoist them into
src/rat_enum/tests.rs (behind `#[cfg(test)] mod tests;`), rewriting the
external `tilezz::` paths to intra-crate `crate::`. The binary drops from
1801 to 939 lines and no longer forces test-only imports into its top.

Verified: the 15 relocated tests pass in their new location (incl. the
OEIS A284869/A316198/A316200 pins and the ring-step cross-validations);
`rat_enum --ring 12 -n 6 --mode bench` still runs.

Phase 2.6 (test hoist) of the architecture untangle plan.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Formatting-only normalization of files this branch added or restructured
(polyomino / tileset_collect binaries, and the modules whose import
blocks reordered when matchfinder/matchtypes moved into geom). main's own
pre-existing fmt drift is fixed on main (folded into its owning commits),
not here.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The reachable-point BFS lived inline in the cyc_explore binary and used
crossbeam::scope -- but crossbeam is a cli-only dependency, so the walk
could not move into the library as-is. Port it to std::thread::scope
(already used elsewhere in the crate) and lift it into
cyclotomic::explore::reachable_points: a ring-generic, print-free BFS
taking the per-step fold and an on_round progress callback. The binary
keeps only the thin free/folded fold-choice wrappers, progress printing,
and rendering.

Since cyc_explore was crossbeam's only user, drop the crossbeam
dependency entirely (Cargo.toml + Cargo.lock).

Verified: 3 new module tests pass (ZZ4 L1-ball counts, unit-cell fold
collapses cardinal steps, on_round reporting); bin output unchanged
(ZZ10 free "10 + 50 + 150 = 211", ZZ12 --unit-square "4 + 8 = 13"). Also
dropped the now-dead --verbose flag (it only gated a thread-count
diagnostic).

Phase 2.5 of the architecture untangle plan.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
analysis/ bundled two near-independent domains coupled by a single leaf
edge: the combinatorial catalogs (vertextypes / neighborhood /
seq_explorer / patch_enum / collect) and the tiling-classification
subsystem (Heesch / periodicity / reptile / certificates / the two-mode
cascade). Promote the latter out of analysis into a top-level `classify`
module and name it honestly (it decides tiling; it is not a "filter").

Pure move: git mv src/analysis/filter -> src/classify and rewrite
::analysis::filter -> ::classify across the tree (including the lone
rustdoc link in geom::patch). classify's only remaining edge into
analysis is heesch -> analysis::vertextypes (a catalog lookup), now a
normal cross-module use. Verified: builds with AND without the cli
feature (classify is not cli-gated); clippy + fmt clean.

Phase 3.1 (rename) of the architecture untangle plan.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Rename the rat-enumeration engine's module rat_enum -> enumerate for a
clearer name. The BINARY stays `rat_enum` (its Cargo.toml entry and CLI
name are unchanged, so the RO-Crate reproduce scripts that invoke
`rat_enum` stay valid). git mv src/rat_enum -> src/enumerate and rewrite
::rat_enum:: -> ::enumerate:: across the tree; the #[cfg(feature="cli")]
gate on the module is preserved, as is the #[cfg(test)] helper fn also
named rat_enum (a thin enumeration wrapper the tests call).

Changes the published lib API path (tilezz::rat_enum -> tilezz::enumerate);
acceptable pre-1.0.

Phase 3.4 of the architecture untangle plan.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
stringmatch was ~83% DAFSA storage + RO-Crate publishing and ~17% actual
string matching -- the name fit only the minority. Move the DAFSA stack
(core/lazy/rat) and the RO-Crate dataset publishing (rocrate) out to a
top-level `dataset` module. stringmatch keeps only the generic cyclic
matching primitives (extend/period/cyclic) it is named for.

git mv src/stringmatch/dafsa -> src/dataset; rewrite stringmatch::{dafsa,
Dafsa, RatDafsa, LazyRatDafsa, LazyRatDafsaAsync, DEFAULT_TARGET_BLOCK_BYTES}
-> dataset::... across the tree (splitting one grouped import that mixed
LazyRatDafsaAsync with the still-in-stringmatch repetition_factor).
dataset re-exports the store types at its root, so consumers spell them
dataset::RatDafsa etc. Builds with AND without cli; DAFSA is
self-contained (no refs back to stringmatch's primitives). 52 dataset
tests pass.

Phase 3.3 of the architecture untangle plan (rocrate -> dataset::rocrate).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…iles

classify_tiles.rs was a 2046-line god-file mixing three concerns: asset
metadata access, the on-disk verdict-store persistence, and the drivers
(run_fast / run_deep / run_verify). Extract the store persistence into a
classify::store module: the coverage sidecar (CoveredRange / read_ranges
/ append_range / covered_union / read_resume), packing (run_pack),
random-access lookup (store_lookup + line_at / line_index), and the deep
overlay path + merge (deep_overlay_path / run_merge / MergeStats). The
drivers stay in classify_tiles and import what they need from store; the
binary sources run_merge / run_pack / store_lookup from classify::store.

classify_tiles.rs 2046 -> 1647; store.rs 417. Pure move, no logic change;
store tests (coverage sidecar, resume, pack + lookup, merge) pass.

Phase 3.1b of the architecture untangle plan.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Two binaries carried presentation logic inline. Move each to the right
library home, per the "generalizable -> vis, domain-specific -> colocated"
split, thinning both bins:

- cyc_explore's point-cloud rendering (render_vis / add_cell) is generic
  (Vec<Vec<P64>> layers + a bounds box -> PNG grid or animated GIF), so it
  becomes vis::pointcloud (grid_png / animation_gif + a MarkerScale sizing
  policy), reusable by any caller with layered point sets. Bin 355 -> 209.
- rat_enum's Render-mode GIF assembly is rat-specific (tile styling,
  index-labelled vertices), so it moves to
  enumerate::output::render::rats_gif, next to the rat renderer, over the
  generic vis primitives. Bin 939 -> 895.

The domain adapters classify/render + enumerate/output/render correctly
stay ABOVE vis (moving them into vis would invert the layering). Also
fixes a stale stringmatch -> dataset doc reference (3.3 fallout). Verified
PNG + GIF output unchanged.

Phase 4 of the architecture untangle plan.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
apirogov and others added 28 commits July 15, 2026 19:13
…real gate

Reviewing the accept-side detectors, the soundness structure checks out: every
periodic ACCEPT is gated on a minted PeriodicCert, and mint returns a cert only
if cert.verify() passes -- which runs the full-domain gold_check
(all_exactly_surrounded over one tile PER TRANSLATION CLASS) plus two exact
multiplicity gates. That is k>1-sound.

But the isohedral/aniso module docs mis-located the guarantee, crediting the
detection-time `verify_tiling` pre-filter -- which is CENTER-ONLY and, as
tiling.rs's own all_exactly_surrounded doc states, k=1-sound only (a defect
strictly between tiles of other translation classes goes unseen). isohedral's
p3/p4/p6 cells and aniso's k-clusters are k>1, so verify_tiling alone is NOT
what makes a wrong gluing unacceptable -- the cert gate is. A maintainer
trusting the old wording could drop the cert check and introduce a real k>1
soundness bug. Repoint both docs at the verify-gated cert; keep verify_tiling
described as the fast pre-filter. Doc only; accept path unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… cert)

"orbit's pure-translation (rot==0) placements -> shortest independent basis
pair" was implemented twice: isohedral::lattice_from_orbit (sort shortest-first,
PARALLEL_EPS filter) and cert::verify inline (no sort, xy()!=0 filter, then
independent_pair). Same idea, different clothes. Lift to one pub(crate)
lattice_from_orbit in lattice.rs (the home of independent_pair/basis_inverse),
Option-returning; both callers now share it.

This gives cert::verify the shortest-first sort it lacked. Behavior-preserving
where it matters: covolume is invariant across bases of one lattice, and
shortest-first makes independent_pair MORE likely to return a true basis (not a
sublattice pair), so the exact multiplicity gates (covol_eq_m_areas /
area_eq_k_area) and coset_reps are unaffected. Gated on the classify suite:
64 pass, including the mint "minted cert must self-verify" tests.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The db_lookup_recovers_every_walk_form test called
enumerate::dfs::rat_enum_with with the OLD signature -- before the DFS was made
generic over the Boundary backend, which prepended an `mk: Fn(&[i8]) -> B`
maker argument and the B/Mk type params. The test still passed max_steps as the
first arg, so it failed to compile (E0107/E0061/E0277). It is gated behind the
rat_explorer feature, so `cargo test --features cli` never built it -- but
`cargo clippy --all-targets --all-features` (the CI checks command) did, and was
failing on it. Update the call to match: pass Snake::from_slice_trusted as the
maker and the ::<ZZ12, Snake<ZZ12>, _> turbofish (mirrors enumerate/tests.rs).

Found during the classify review. (Remaining --all-features clippy failures are
a separate pre-existing clippy-version-drift issue, ~12 lints crate-wide.)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
`cargo clippy --all-targets --all-features -- -D warnings` (the CI checks
command) had been failing on ~12 clippy-version-drift lints across 8 files --
newer stable clippy flagging code an older one passed. All trivial, all
behavior-preserving; mostly `cargo clippy --fix`:

- geom/matchtypes: 4x unnecessary `usize as usize` casts dropped.
- classify/{aniso,patch_graph,heesch}, geom/patch/tests: redundant `&pm` ->
  `pm` (ref immediately dereferenced by the auto-deref).
- classify/classify_tiles: `c % n == 0` -> `c.is_multiple_of(n)`.
- util/parallel: `.reduce(|a, b| reduce(a, b))` -> `.reduce(reduce)`.
- combinatorics/neighborhood: a `let Some(x) = ... else { return None }` (an
  earlier grown() conversion of mine) -> `let x = ...?` (fn returns Option).

Now green: clippy --all-targets --all-features -- -D warnings exits 0; full
suite 943 pass. Combined with the web-test fix (cb991ec) and the cargo doc
gate (a1d5460), the checks job is now clean end to end.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ssage

Pass over the CLI bins + the rat_explorer WASM module:

- Added the missing //! module docs to bin/cyc_explore and bin/patch_enum
  (the only two bins without one).
- web/rat_explorer hand-rolled the 12-arm ring match TWICE (dispatch_ring for
  analyze_for_ring, and closing_free_canonical_for_ring) -- the exact match the
  `dispatch_ring!` macro (used by cyc_explore + classify_tiles) provides, same
  ring set. Replaced both with the macro; narrowed the now-unused
  `cyclotomic::*` glob to `IsRing` (module) + `ZZ12` (tests). cyc_explore's
  unit-square-fold match is left hand-rolled ON PURPOSE -- it is a restricted
  subset (rings with a ZZ4 subring: 4,8,12,16,20,24,32,60), not the full set.
- Fixed a stale message: dispatch_ring!'s default-fallback panic listed
  4,6,8,10,12,16,20,24,32,60 but the macro has always handled 14 and 18 too.

Doc + dedup only, behavior-preserving. clippy --all-targets --all-features
-D warnings and cargo doc -D warnings both clean; web db_lookup test passes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Worker-count resolution was duplicated across two backends: num_cpus::get() in
three bins (patch_enum, cyc_explore, rat_enum) and std available_parallelism in
classify (workers_or_all, heesch, classify_tiles). num_cpus -- an optional dep
pulled by the `cli` feature -- was used for nothing else.

Add util::available_workers() (std available_parallelism, fallback 4, matching
classify's existing map_or(4, ...) exactly) as the one "count the cores"
helper, and repoint all six sites. Each keeps its own request/cap policy:
rat_enum still caps the request at the core count, cyc_explore still treats
--threads None as "all", classify's sweeps are byte-identical. The shipped
standalone src/dataset/extras/verify.rs is deliberately NOT touched (it is
include_str!'d, not compiled -- no crate:: refs allowed).

Then drop num_cpus: removed from [dependencies] and the `cli` feature (+
Cargo.lock). One generic impl, one fewer dependency in the published crate.
Behavior-preserving: full suite 943 pass; clippy/doc -D warnings green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
GrowingPatch::num_tiles() returned self.match_index.tileset().num_tiles() --
the number of tile SHAPES in the tileset, NOT tiles in the patch. Its only
patch-instance callers were two lines in a cert test:
    let n = gp.num_tiles();  ...  assert_eq!(recon.num_tiles(), n);
Both patches share one tileset, so that assertion was `k == k` -- always true,
silently checking nothing (it was meant to verify reconstruction preserves the
tile count). The adjacent `recon.to_rat() == direct_rat` is the real
reconstruction check; drop the vacuous line.

Remove the method (YAGNI): tileset shape count is tileset().num_tiles() (the
patch shouldn't re-expose it under a name that reads as "tiles in the patch").
If a real per-patch tile count is ever needed it belongs on the future Graphed
layer as graph.node_count() -- an honest structural count incl. interior tiles
-- not a tileset passthrough. See specs/patch-layering.md.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
coarse_junction_at (GrowingPatch) has zero callers, and CoarseJunction was
referenced only by that unused producer. Its own doc says it is "the right
equivalence for seg enumeration" -- i.e. it was built for the segment-type
enumeration deleted earlier this session (same orphan class as segment_kind and
num_tiles). Remove both.

If coarse junctions are ever needed again they belong as an
OpenVertexType::to_coarse() adapter (the coarse angle is derivable:
full_turn - sum of the interior petal angles), not a parallel patch method.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…lta/Relabel)

First stage of the GrowingPatch layering plan (.claude/superpowers/specs/
patch-layering.md). Pure plumbing -- no behavior change, no type split yet.

- New pub types in geom::patch: GlueDelta { removed, new_edges, new_id } (the
  structural "what one glue changed" delta the future metadata layers will
  project from -- allocation-free ranges+scalars, forward-only/not-invertible)
  and Relabel { rot } (what normalize did).
- add_tile / add_tile_growing: -> bool  becomes  -> Option<GlueDelta> (Some on
  success, None on the same failure paths). grown unchanged externally.
- normalize: -> usize (rot)  becomes  -> Relabel { rot }.
- Propagated to all ~40 callers, behavior-identical (bool -> .is_some()/.is_none()/
  ?; usize -> .rot). PatchGraph::add_tile (its own method) untouched except its
  inner gp.add_tile call. Nothing consumes the delta yet -- it is the hook.

Verified: cargo check/clippy --all-targets --all-features -D warnings clean;
full release suite 943 pass / 0 fail (unchanged).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ns<BasicPatch>

Second stage of the layering plan (.claude/superpowers/specs/patch-layering.md).
Behavior-identical structural split, no call-site changes outside geom/patch.

- BasicPatch<T>: the core -- match_index, boundary_seq, grid, patch_tile_ids,
  next_tile_id (everything EXCEPT inner_chains). Owns the glue (add_tile ->
  GlueDelta), match-finding, normalize (-> Relabel), and all boundary reads.
- WithJunctions<P> { inner: P, inner_petals }: the junction layer. inner_petals
  is the old inner_chains, moved up. Owns junction_vertex_type_at / segments /
  witness constructors, and OVERRIDES add_tile + normalize to project the petals
  on top of the core op.
- pub type GrowingPatch<T> = WithJunctions<BasicPatch<T>> -- the whole existing
  public surface is preserved (WithJunctions re-exposes core methods via explicit
  one-line delegators; no Deref), so every consumer is untouched.

The delicate bit: add_tile's inner_petals maintenance is now a projection.
WithJunctions::add_tile snapshots the (cheap, flat) pre-glue edges+ptids, delegates
to inner.add_tile (collision check + GlueDelta), then rebuilds inner_petals from
that snapshot ONLY on success (new_n = delta.new_edges.end == old new_len).
Behavior-identical to the old fused path.

No renames yet (EdgeInfo/inner_chains/junction naming = a separate later pass).
neighbor_junction_offsets + tile_segments are placed on WithJunctions per the
goal-state layout even though they read only core data (revisit in a later stage
if a core-only consumer needs them).

Verified: cargo check/clippy --all-targets --all-features -D warnings clean;
943 pass / 0 fail (behavior oracle, unchanged); heesch bound-2 bench 1.77-1.79s/rep
vs 1.78 baseline (no regression -- the per-glue snapshot clone is not measurable).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…e raw path

Third stage of the layering plan (.claude/superpowers/specs/patch-layering.md).
Witness construction now builds a real WithJunctions<BasicPatch> incrementally
via a trusted grid glue, instead of a gridless RawBoundary + one-shot from_parts.

- BoundaryGrid: unify the splice behind one splice_run(.., check: bool) -- the
  commit logic lives once. try_splice_run = check:true; splice_run_trusted =
  check:false (skips the polyline_all_clear collision scan, always commits, grid
  still maintained). Trusted path debug_asserts the check would have passed.
- BasicPatch/WithJunctions::add_tile_trusted: add_tile's twin routed through the
  trusted splice. Caller guarantees a real non-colliding match (witness rebuild);
  skips the expensive check but keeps the grid so the patch can be checked-grown
  afterward. Debug-asserts pm is a real match.
- construct_witness_from_vt_sequence(_inner) + flower_petal_glue_trusted rebuilt
  on add_tile_trusted (seed = single_tile; no final from_parts). Junction-position
  tracking preserved exactly via GlueDelta (new_junc_pos = delta.new_edges.start;
  prior_juncs remap via delta.removed). from_parts kept as the snapshot-deserialize
  path. RawBoundary/RawGlueResult/glue_*_to_raw_boundary/flower_petal_glue DELETED
  (git grep RawBoundary == 0).

Behavior verification (the raw path and glue_geometry have slightly different glue
rejection profiles -- raw rejected seg_len_new==0 keystones + checked only the CCW
junction hturn; glue_geometry permits keystones + rejects EITHER junction hturn):
- The full NT enumeration output (all entry/boundary/class counts for square, hex,
  spectre) is BYTE-IDENTICAL before/after -- the divergence is unexercised on the
  real research tilesets.
- NT enumeration + witness construction is combinatorics-research-only; it appears
  in NO classify/bin path, so it cannot affect the certified classification pipeline.
- 943 pass / 0 fail (incl. witness round-trips); clippy --all-targets
  --all-features -D warnings clean. NT build bench 14.3-14.5s vs 14.8-15.3s
  baseline (no regression -- incremental grid offset by dropping from_parts rebuild).

Residual: a FUTURE research run on some other tileset could hit the differing glue
cases and get a different witness. Documented; not eliminated (the stricter hturn
rejection is arguably more correct, and keystones look structurally unreachable in
open-chain witness construction).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…r-identical)

Prep for the WithAdjacency layer: the shared corona search no longer hard-codes
GrowingPatch, so a graph-carrying stack can be buried in Stage 3b with zero replay.

- New pub trait CoronaPatch<T>: Clone in geom::patch -- the exact patch surface bury
  consumes (core boundary reads + get_matches_* + add_tile -> GlueDelta + the junction
  query junction_vertex_type_at that has_cursed_frozen needs). Impl'd for
  WithJunctions<BasicPatch<T>> by forwarding to the inherent methods.
- bury / branch / exposed / frozen_open / frozen_coords / has_cursed_frozen /
  capture_placement and the TileSink protocol (+ PathSink / BestPathSink / CollectSink /
  PlacementSink) are now generic over P: CoronaPatch<T>. All call sites still pass the
  concrete GrowingPatch, so P infers to WithJunctions<BasicPatch> everywhere -- nothing
  observable changes.

Verified: cargo check/clippy --all-targets --all-features -D warnings clean; 943 pass /
0 fail (heesch/cert/torus unchanged); heesch bound-2 bench 1.80s/rep vs 1.78 baseline
(within noise -- monomorphized, single instantiation).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… the recipe

Completes Stage 3: the tile-adjacency graph is now built DURING the corona search
(zero replay), and mint's carve reads it directly.

- WithAdjacency<P> (classify::patch_graph): { inner: P, graph: PatchGraph }, a
  composable layer parallel to WithJunctions. Its CoronaPatch<T> impl delegates every
  method to inner EXCEPT add_tile, which projects the graph from the GlueDelta
  (snapshot pre-glue edges+ptids -> tile_match_runs -> push_node + push_edges), exactly
  as WithJunctions projects petals. single_tile pre-seeds node 0, so the first glue is
  handled uniformly by add_tile -- PatchGraph::grow_seed + the old gp-mutating
  PatchGraph::add_tile are DELETED.
- grow_coronas_build now runs bury on WithAdjacency<WithJunctions<BasicPatch>> (the
  stack carries junctions for bury's cursed prune AND the graph) and returns
  (placements, PatchGraph) -- NO recipe. TileSink::mark gains a &patch param so grow's
  sink captures the finished graph at the winning corona closure (one clone, not
  per-branch).
- mint torus_cert/mint_cover/carve_domain take &PatchGraph instead of a recipe;
  carve_domain's Stage-1 recipe replay is deleted -- connected_transversal + assemble
  run on the natively-grown graph.
- Inlined the trivial CoronaPatch forwarders (correct default).

Verified independently:
- SOUNDNESS ORACLE: torus certs (build+glue+via) BYTE-IDENTICAL before(2377a0b)/after
  across k=1 (square/hex/triangle) AND the graph-dependent carve k=8/3/4/14 -- the
  native graph reproduces the recipe-replay graph exactly (cert types self-verify, so
  this independent diff is the real check).
- 943 pass / 0 fail; clippy --all-targets --all-features -D warnings clean.
- Torus path 1.26s vs 1.25s (native-grow graph-clone-during-search is negligible).
- Heesch bound-2 bench ~1.81s/rep (was ~1.78 pre-3a); #[inline] on the forwarders
  changed it 0, so the delta is session thermal drift, not genericization cost --
  the generic bury is zero-cost as monomorphization predicts.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ingPatch alias

Enforces the "pay no more than needed" contract: each consumer now names the minimal
patch layer it uses, and the transitional alias is gone.

- New canonical typedef `pub type EPatch<T> = WithJunctions<BasicPatch<T>>` (the
  extensional patch: core + junction typing). DELETED `pub type GrowingPatch`; all 129
  reference sites retyped (git grep GrowingPatch == 0).
- -> BasicPatch<T> (core; add_tile skips inner_petals -- the probe-clone win):
  classify/aniso (cluster growth), classify/cert (replay_build/reconstruct),
  PatchGraph::assemble, and the core-level doc-links across geom/{patch,vertices,glue,
  grid,matches,matchtypes}.
- -> EPatch<T> (junctions): classify/heesch, combinatorics/{neighborhood,vertextypes,
  mod,seq_explorer}, the WithAdjacency<EPatch> stack, and the junction/witness docs.
- Fixed a pre-existing broken doc-link (construct_minimal_witness is #[cfg(test)], so
  its intra-doc link was unresolvable under cargo doc) surfaced by the new doc gate.

CAVEAT (honest): reptile + grow's replay_recipe/replay_placements stayed EPatch, NOT
BasicPatch as the inventory listed -- they feed capture_placement, which is bounded
<P: CoronaPatch> (needs junctions), and BasicPatch cannot implement CoronaPatch. So
reptile does NOT yet get the petal-skip win. Narrowing it needs a core `Patch<T>` trait
(core reads, no junctions) to weaken capture_placement's bound -- a follow-up, tracked.

Verified: cargo check/clippy/doc --all-targets --all-features -D warnings clean;
943 pass / 0 fail; aniso heavy test 122.7s vs 125.0s (~1.8% faster -- petal-skip win,
modest since petals are a small fraction of aniso's total).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…nsional graph

The PatchGraph type was redundant with WithAdjacency (a wrapper holding a graph).
WithAdjacency now carries the adjacency inline and is the graph itself.

- WithAdjacency<P> { inner: P, shape: Vec<usize>, adj: Vec<Vec<TileMatch>> } -- adjacency
  inlined. neighbors/degree/len + adj()/shape() accessors on it; push_node/push_edge are
  private add_tile helpers. add_tile projects into self.shape/self.adj directly.
  PatchGraph struct DELETED (git grep PatchGraph == 0).
- connected_transversal + assemble are now free pub(crate) fns in classify::mint,
  colocated with carve_domain, taking the adjacency as slices (graph-pure, no geometry) --
  which also lands the long-deferred "connected_transversal is domain-specific, move it
  out of the graph API" decision.
- pub type IGraph<T> = WithAdjacency<EPatch<T>> -- the grower/carve stack alias.
- grow_coronas_build / replay_placements_graph return (.., IGraph<T>); the grow sink
  clones the IGraph patch once at the winning corona. mint's carve reads ig.adj()/
  ig.shape()/ig.len() + the free carve fns. bfs_from/components are private test helpers
  over adjacency slices.

Verified independently:
- SOUNDNESS: torus certs BYTE-IDENTICAL to the pre-4b baseline (sha256 ed6a9f85..,
  23137 bytes; identical to the Stage-3b dump) across k=1 (square/hex/triangle) AND the
  graph-dependent carve k=8/3/4/14. The inlined adjacency reproduces PatchGraph exactly.
- 943 pass / 0 fail; clippy + doc --all-targets --all-features -D warnings clean;
  git grep PatchGraph == 0. Torus path 1.25-1.26s vs 1.25s (whole-patch clone fires once
  at the winning mark -- no measurable slowdown).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… BasicPatch

Gives the probe-clone consumers the petal-skip win they missed in Stage 4.

- Split the bury-surface trait: pub trait Patch<T>: Clone (the 9 core methods --
  boundary reads, the two local match enumerators, add_tile) + pub trait
  CoronaPatch<T>: Patch<T> (adds ONLY junction_vertex_type_at). New impl Patch for
  BasicPatch (inline forwarders); the old monolithic CoronaPatch impls for
  WithJunctions/WithAdjacency split into Patch + CoronaPatch halves.
- capture_placement weakened CoronaPatch -> Patch (it reads only patch_tile_ids/edges/
  boundary_positions). replay_recipe now builds BasicPatch (was EPatch). reptile's
  build_from_placements grows BasicPatch -> its per-candidate clone().add_tile probes
  skip the inner_petals projection.
- Cascade: count_coronas (a replay_recipe caller) now feeds a &BasicPatch to
  corona_closed/advance_closed_coronas, so those + frozen_open/frozen_coords/exposed
  relaxed CoronaPatch -> Patch (they read only core data). bury/branch/has_cursed_frozen/
  sinks stay CoronaPatch (need junctions). The graph replay path (IGraph) is untouched.

Verified: cargo check/clippy/doc --all-targets --all-features -D warnings clean;
943 pass / 0 fail (behavior-identical -- guards the heesch count_coronas relaxation);
reptile build_from_placements ~1.79 ms/iter vs ~1.90-1.99 (BasicPatch, ~6-10% faster,
identical output acc=19600).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Behavior-identical rename pass (the deferred half of the layering work). "vertex" was
overloaded -- a plain per-edge boundary VERTEX vs an interesting JUNCTION where tiles
meet; the classification machinery is all about junctions. Standardized accordingly;
plain "vertex" now survives only for per-step boundary vertices.

- OpenVertexType -> OpenJunctionType, ClosedVertexType -> ClosedJunctionType,
  VTypeKind -> JunctionKind (type only; enum variants + serde wire untouched),
  OpenVertexTypeIndex -> OpenJunctionTypeIndex, + the *Info companions and the
  vertex_type_raw_from / cursed_vertex_types / test-fn derivations.
- junction_vertex_type_at -> junction_type_at (CoronaPatch trait + impls + inherent).
- inner_chains -> inner_petals (accessor) / update_inner_chains -> update_inner_petals.
- EdgeInfo.tile_id -> tile_type_id (names the tileset SHAPE, not an instance),
  .tile_offset -> canon_offset. Compiler-guided (tile_id/tile_offset are shared with
  Segment -- only EdgeInfo's were renamed). #[serde(rename)] keeps the JSON wire keys
  ("tile_id"/"tile_offset") byte-stable, so no persisted data breaks; same for the
  inner_chains wire keys on SurroundedTile/WitnessSnapshot.

Verified: cargo check/clippy/doc --all-targets --all-features -D warnings clean;
943 pass / 0 fail (UNCHANGED -- renames add no tests; git diff has 0 net #[test]/
#[ignore] changes); git grep of the old identifiers == 0 (only the intended
serde-rename wire literals remain).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…Jtype)

The three renames that needed a decision, now resolved.

- WithAdjacency::len -> num_tiles (it counts tile INSTANCES = graph nodes; distinct
  from the old misnamed GrowingPatch::num_tiles that returned a shape count). That
  frees the name, so boundary_len -> len (the patch's boundary edge count), with
  #[allow(clippy::len_without_is_empty)] (a live patch always has >= 3 boundary edges).
- grown -> with_tile(&self) -> Option<Self>: the consuming `grown` is replaced by a
  borrow-and-clone probe primitive. All ~55 sites converted (`x.clone().grown(pm)` ->
  `x.with_tile(pm)`; the single-tile bootstrap clones are trivial). No `fn grown` left.
- module combinatorics/vertextypes -> junctiontypes; the Vtype/vtype family -> Jtype/
  jtype (VtypeRecord -> JtypeRecord, collect_vtype -> collect_jtype, CollectKind::Vtype
  -> Jtype, .vtypes -> .jtypes, CLI keyword "vtype" -> "jtype"). Added a doc explaining
  a JUNCTION is the geometric point where boundary segments from multiple tiles meet at
  one vertex (vs a plain per-step boundary vertex).

WIRE: the VT/NT serde artifacts are renamed CLEAN (no back-compat #[serde(rename)]) --
EdgeInfo now serializes tile_type_id/canon_offset, inner_petals, jtypes, CollectKind
"jtype". Per the design call: VT/NT collections/envelopes are NOT published RatDB data,
so their wire may change (regenerate any local dumps). Only the RatDB/dataset wire is
reproducibility-frozen -- and dataset/ is untouched by this pass.

Verified: cargo check/clippy/doc --all-targets --all-features -D warnings clean; 943
pass / 0 fail (unchanged); boundary_len + the grown method gone; dataset/ untouched.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…not stacked

Corrects a design deviation: Stage 3b silently STACKED WithAdjacency on WithJunctions
(WithAdjacency<EPatch>, minted as IGraph) to satisfy bury's junction bound, violating
the spec's design of WithJunctions and WithAdjacency as INDEPENDENT PARALLEL wrappers
over BasicPatch. The junction requirement was a type artifact: grow runs bury edge-only
with an empty cursed set, and has_cursed_frozen short-circuits before touching junctions.

- Decouple bury from junctions: bury/branch/has_cursed_frozen drop `P: CoronaPatch` ->
  `P: Patch`, taking the cursed-prune's junction lookup as a closure `Fn(&P, usize) ->
  Option<OpenJunctionType>` threaded through the recursion. heesch passes
  `|p,i| p.junction_type_at(i)` (its P = EPatch, inherent method); grow passes `|_,_| None`.
  TileSink + all sink impls drop to `P: Patch`.
- WithAdjacency<BasicPatch> is now the grow/carve stack -- parallel to EPatch, no petals
  maintained during grow's search. The CoronaPatch trait is DELETED (nothing bounds on
  it; junction_type_at stays inherent on WithJunctions). IGraph -> pub type
  IPatch<T> = WithAdjacency<BasicPatch<T>> (the spec's canonical name). Docs on the
  layers now state the parallel design explicitly.

Verified: torus certs BYTE-IDENTICAL to the frozen baseline (sha ed6a9f85..) across k=1
and carve k=8/3/4/14 -- the adjacency is GlueDelta-projected, so inner BasicPatch vs
EPatch cannot change it; git grep of stacked/IGraph/CoronaPatch == 0; 943 pass;
clippy + doc -D warnings clean; heesch bench 1.79-1.81s/rep (flat, closure monomorphizes).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Follow-up from a full spec re-audit (decisions 1-4 + success criteria).

- Decision 3 (independent oracle): add_tile_trusted now runs an INDEPENDENT debug
  check on its result -- Snake's incremental simple-closed construction, which shares
  no machinery with BoundaryGrid -- so a bad "trusted" glue can't silently corrupt.
  (Previously the only trusted-path debug check was the grid's own polyline_all_clear,
  a self-check.) Debug-only; the Snake import is now #[cfg(debug_assertions)]-gated.
- Decision 4 (documentation): WithAdjacency carries a doc explaining it deliberately
  has NO normalize (graph patches are grown-then-consumed, never canonicalized; the
  node-remap by id_perm is unbuilt and unneeded -- nothing dedups graphs).
- Success criteria (fmt): ran cargo fmt (the stage commits were never fmt'd) -- 12
  files reformatted, no logic change.
- Fixed a DEBUG-only test regression from Stage 2, masked because every stage gated on
  the RELEASE suite only: construct_witness_guard_rejects_fully_interior_tile is
  #[should_panic]; the Stage-2 trusted-glue collision debug-assert now fires before the
  constructor's simple-closed guard, so its `expected` message no longer matched. Both
  guards reject the bad witness -- relaxed to match any panic + documented the order.
- Release build is now warning-free (the debug-gated Snake import + the debug-only
  seven_hex_full_corona fixture no longer warn under release).

Verified: cargo fmt clean; clippy + doc --all-targets --all-features -D warnings clean;
release suite 943 pass; DEBUG suite now green (the one failure above fixed; the other
934 already passed -- so the soundness debug_asserts are actually validated now).

Decisions D (Relabel omits id_perm -- documented, no consumer) and B (raw wrappers
pub(crate)) NOT changed here: B is a real fork (pub(crate) triggers private_interfaces
on the pub EPatch/IPatch typedefs + witness API) surfaced separately for a call.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…typedefs

Resolves the last audit item (decision 1 / "B"). The intent was to expose only the
canonical stacks and hide the raw wrappers, but a `pub` typedef aliasing a `pub(crate)`
type is a hard `private_interfaces` error, so pub(crate) wrappers + pub typedefs is
impossible in Rust. Since the patch API is meant to be public API on the level of
Snake/Rat (per the design), the resolution is: keep the wrappers `pub`, and DOCUMENT
that callers should name the canonical typedefs (BasicPatch / EPatch / IPatch), not the
raw WithJunctions / WithAdjacency wrappers -- the wrappers are the generic composition
mechanism, the typedefs are the intended interface + only sanctioned instantiations.

No code change; doc-only. clippy + doc --all-targets --all-features -D warnings clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…s was red)

`just check-notebooks` (which evcxr-compiles every notebook against the crate) was
failing on tilezz_examples.ipynb, using two APIs renamed by EARLIER branch commits
(pre-dating the patch-layering work): Snake::from_slice_unchecked (renamed to the
trusted/permissive family in 45bf03a) and Rat::glue (reworked into try_glue). Updated:
- from_slice_unchecked -> from_slice_permissive (the cell renders a deliberately
  self-intersecting boundary; permissive = "self-intersections allowed, nothing
  checked", the exact intent).
- rat.glue((a, b), &other) -> rat.try_glue(MatchSeed::new(a, b), &other).unwrap().

Not caused by the patch-layering refactor -- the notebooks use none of its renamed
identifiers -- but check-notebooks is a gate on this branch and should be green.

Verified: just check-notebooks -- all 3 notebooks run cleanly; just check-tools passes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…atorics rename

Bring the architecture doc up to date with the completed refactor:
- geom shape model now describes the PATCH STACK: BasicPatch core behind the `Patch`
  trait, WithJunctions (`EPatch`), checked `add_tile` vs trusted `add_tile_trusted`
  (independent Snake debug-check). Dropped the removed `GrowingPatch`/`PatchSeed`.
- `analysis` module renamed to `combinatorics` throughout (diagram, edges table,
  section, the no-cycles note); junction vocab (`junctiontypes` / `OpenJunctionType` /
  `cursed_junction_types`), with a one-line definition of a junction.
- classify: `patch_graph` now hosts `WithAdjacency` (`IPatch`), the adjacency-graph
  layer; mint's carve grows an IPatch and reads its adjacency (no recipe replay).
- New cross-cutting convention "Layered patches, pay no more than needed": the three
  parallel layers (BasicPatch/EPatch/IPatch) projected from the GlueDelta, each
  consumer on its minimal layer, public via the typedefs not the raw wrappers.

No stale identifiers remain (git grep of GrowingPatch/PatchGraph/RawBoundary/
OpenVertexType/vertextypes/PatchSeed/analysis in ARCHITECTURE.md == 0). AGENTS.md's
OpenVertexType/vertextypes mentions are historical changelog (naming a past commit) and
left intact.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The tile-classification rename standardized the type identifiers
(OpenJunctionType, CollectKind::Jtype, ...) but left "vertex type" across
prose, user-facing CLI output, and the VT/vtype abbreviations -- against the
project's JUNCTION-vs-VERTEX standard. Finish the sweep and clear the doc/label
drift a pre-merge review surfaced.

Vocab:
- VT/vtype/vt_seq/closed_vt_id/cvt/construct_witness_from_vt_sequence ->
  JT/jtype/jt_seq/... across combinatorics + geom + classify + the collect bin
  (identifiers, comments, panic/assert messages). No serde wire is frozen here
  (the jtype census is not a published artifact), so JtypeRecord fields rename
  cleanly.
- collect CLI output: "vertex-type BFS" -> "junction-type BFS", "VTYPE" ->
  "JTYPE"; tileset_collect --mode help "vtype" -> "jtype" (the accepted keyword
  is jtype; the help was stale).
- prose "vertex type" -> "junction type" in heesch/junctiontypes/vertices/
  cyclic/patch and the combinatorics module docs.

Stale docs/labels:
- ARCHITECTURE.md: drop the deleted `polyomino` bin row, the dropped `num_cpus`
  dep, and the test-only `web -> enumerate` edge; note util::parallel.
- GlueDelta doc claimed "not yet consumed" (it is) + a dangling spec-path ref;
  from_parts referenced a removed `candidates_by_start` field.
- grow module doc described the retired recipe-replay carve; store.rs orphan
  header; heesch [`filter`]->[`classify`] label, [`mark`] link, patch.rs path;
  cert [`filter`] label; enumerate test pointer; vertices compute_segments doc
  named old EdgeInfo fields.
- patch/tests.rs: rewrite five_hex_cross / square-grid / t_tetromino fixture
  docs to the pinned-literal reality (delete stale FIXMEs + the contradictory
  "invariant (2)" block); relocate the misattached keystone regression doc;
  rename the raw_boundary / junction_vertex_ids test fns.
- profile.rs: `pprof` feature -> `debug` (the fix commit missed two spots).
- vis: non-ASCII (em-dash/ellipsis/x) in touched doc lines; notebook
  `unchecked` binding -> `permissive`; cyc_explore fold panic no longer claims
  "no imaginary unit" for a divisible-but-uncompiled ring.

Behavior-neutral (identifier + user-facing text only).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- geom/matchtypes: MatchTypeIndex::new built by_start with rats[0]-sized rows
  for every tile then immediately overwrote each per-tile -- wasted allocation
  and a panic on an empty tileset. Build per-tile rows directly.
- geom/glue: junction_angle_from_sum did `sum - hturn` in i8 after casting the
  i32 junction sum to i8 -- truncates, and under the release overflow-checks
  added this branch can panic for large rings (turn >= 86). Take the sum as
  i32, reduce mod turn in i32, then normalise. Latent (unreachable for the
  compiled rings <= ZZ60) but a real regression against exact arithmetic.
- geom/snake: concat now forwards the real inner error (extend /
  extend_from_slice return &'static str, resolving the borrow FIXME); delete
  the dead, misleading is_point_inside stub (no point arg, ignored geometry,
  always true when closed, zero callers).
- classify/mint: assemble discarded its `shape` arg (`let _ = shape`); replace
  with a debug_assert that every node is the base shape (id 0), making the
  monotile graph-pure contract real instead of ignored.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Pre-merge review found three same-algorithm copies:

- classify/cert::replay_build was a hand copy of grow::replay_recipe (seed on
  base, with_tile(build[0]), add_tile the rest) plus the REPLAY_CAP guard --
  which also falsified grow's "the one place the replay protocol lives" doc.
  Delegate to replay_recipe (guard empty/oversized first, since replay_recipe
  requires a non-empty build).
- combinatorics/collect: run_collect_zz12/_zz10 and dispatch_validate_zz12/_zz10
  were byte-identical modulo the ring type param. The collect_*/validate_*
  helpers are already generic over T: IsRing, so collapse each pair to one
  generic fn (run_collect_ring / dispatch_validate); the two call sites keep
  picking the ring-specific tileset builder (ts_zz10/zz12, rebuild_tileset_*),
  which must stay split since Penrose only exists in ZZ10.
- geom/matchfinder: the ~12-line "junctions_glueable + try_glue_match ->
  TileMatch" candidate tail was copied 4x (CMI + brute-length-1 phase, in both
  valid_matches_filtered and candidates_for_pair). Extract try_candidate; each
  driver keeps its own match-source, active gate, brute-phase early prune, and
  accumulator (Vec vs BTreeMap), so hot-path ordering is unchanged.

Behavior-identical: full release suite 962 passed (unchanged), clippy, rustdoc,
fmt green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Two independent CI-red states a stale local toolchain (1.96 vs CI's 1.97) hid:

- clippy::useless_borrows_in_formatting -- a lint that landed in clippy 1.97
  (CI's stable) -- fired on three pre-existing `&x` format args:
  enumerate/boundary.rs (assert!/assert_eq!) and heesch.rs (writeln!). Drop
  the redundant `&`.
- notebooks/tilezz_examples.ipynb: the earlier `permissive`-rename edit stored
  the cell `source` as one JSON string instead of the list-of-lines every other
  cell (and notebooks/check.py's line iterator) expects, so check.py spliced it
  character-per-line into a garbage program. Restore list form.

The `ci:` commit that follows makes both of these reproducible locally so a
stale toolchain can't hide them again.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Root cause of the repeated "green locally, red in CI": my local stable was
1.96 while CI's `@stable` had moved to 1.97, and lints/behaviour differ between
them. Two changes so local == CI by construction:

- rust-toolchain.toml pins channel 1.97.0 + rustfmt/clippy. rustup auto-installs
  it on the first cargo command, so every local `cargo`/`clippy`/`fmt` uses the
  SAME compiler as CI. All three workflow jobs (+ pages) pin
  dtolnay/rust-toolchain@1.97.0 to match (bump the file and the tags together).
- justfile gains the CI recipes -- fmt-check / clippy / doc / test-debug /
  test-release, plus `check` (the static gate) and `ci` (static + both test
  profiles). CI's `checks` and `test` jobs now CALL these same recipes (via
  taiki-e/install-action@just), so the two cannot drift: `just ci` reproduces
  CI. The one CI step not in `just ci` is the release dataset round-trip (needs
  a network fetch + clean release build); it stays a CI-only integration step.

Note: CI already ran notebooks/check.py and tools/check_dataset_tools.py; those
are now `just check-notebooks` / `just check-tools` inside `just check`.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@apirogov apirogov changed the title Codebase cleanup Tile classification and codebase cleanup Jul 16, 2026
@apirogov
apirogov merged commit 0fdd4ba into main Jul 16, 2026
3 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