Tile classification and codebase cleanup#48
Merged
Merged
Conversation
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>
…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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
No description provided.