Skip to content

IIT-21-Shifting-Mosaic-Solver - #21

Merged
Valle12 merged 38 commits into
mainfrom
IIT-21-Shifting-Mosaic-Solver
Jul 28, 2026
Merged

Valle12 merged 38 commits into
mainfrom
IIT-21-Shifting-Mosaic-Solver

Conversation

@Valle12

@Valle12 Valle12 commented Jul 28, 2026 •

Copy link
Copy Markdown
Owner

Summary by CodeRabbit

  • New Features

    • Delivered the Shifting Mosaic Solver end-to-end workflow, including progress/phase updates, a step-by-step solution view, and solution navigation.
    • Added config JSON upload (file drop), validation, download, and reset in the editor.
    • Expanded shipped WebAssembly solver variants, including MEMORY64 and cross-origin isolation support via a COI shim.
  • Bug Fixes

    • Improved grid pointer targeting for more reliable painting/placement.
    • Refined Phasic Dial Solver “presses” wording/formatting.
  • Tests

    • Added/reshaped Playwright coverage across multiple solvers and introduced WASM + MEMORY64 test coverage.
    • Updated CI to build and run the new native and WASM test targets.

Valle12 and others added 30 commits May 20, 2026 21:42
Runs the checks in .clang-tidy on every TU and fails the build on any
finding, under Ninja/Makefiles. Differs from the usual snippet in four
ways, each a real failure first:

  * no -checks argument, which would override .clang-tidy entirely and
    enable the contradictory llvm/fuchsia/altera families
  * find_program, since clang-tidy is not on PATH here
  * a generator gate, because the Visual Studio generator silently
    ignores CXX_CLANG_TIDY
  * --extra-arg=-EHsc on MSVC: clang-tidy discards every slash-prefixed
    flag after the `--` separator, so /EHsc never arrived and the tree
    was analysed as -fno-exceptions, making every try/throw in the tests
    a clang-diagnostic-error

Enforcement is pinned to the clang-tidy major the profile was generated
against; other hosts warn and build unchecked rather than failing on an
unknown check name. Every gate degrades to a warning, never a configure
error.

Also resolves the findings the profile surfaced, with no suppressions
added: validated env parsing in place of atoi, seeded test RNGs via
testSeed(), member initialisers, and a wasm-only #if around the heap
tripwire.

Verified: full Ninja build clean with -warnings-as-errors=* in both
Debug and Release, gate proven live by a deliberate violation, 71/71
tests in Release.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
main() was 545 lines at cognitive complexity 174 against SonarLint's
limit of 25, plus three nesting-depth findings, three nested ternaries,
four uncommented empty callbacks and a loop whose counter the body
advanced.

Moving the 41-branch flag chain would not have helped: chained branches
cost the same wherever they live. It becomes a table instead, so the
parse loop is a lookup, and the seven-rung cascade becomes a stage table
for the same reason. Engine setups move to BenchEngines.cpp, options to
BenchOptions.cpp, and main() is now the order they run in.

Behaviour is unchanged, verified against a binary built from the parent
commit: 45 differential cases (8 deterministic engines x 2 fixtures, 27
individual flags, 2 arms) produce byte-identical JSON once timing fields
are excluded; --generate, --dump and --verify produce identical files;
the four error paths keep their exit codes. Only 12 of the 27 flags were
observable on that fixture, so the flag -> field mapping of all 41 was
additionally compared mechanically against the old chain.

One intentional difference: a missing or malformed option value now also
prints the usage line before exiting 2, where before it exited 2 with
only the specific error. stdout is untouched, so the bench harnesses
that parse it are unaffected.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
runGenerate computed to roughly 50 against the limit of 25, most of it
from three lambdas whose bodies nest inside their owner. Extracting them
to free functions (randomShape, tryPlace/overlapsAt, shuffleBoard,
fixtureJson) both lowers the count and settles the depth question: the
placement retry is three levels of loop-and-test that only reads as
three once it is not also nested inside runGenerate.

The order of draws from the shared mt19937 is load-bearing, since
fuzzShiftingMosaic.ts addresses boards by seed. Verified unchanged
across 40 --generate seed/shuffle pairs (10 seeds x 4 shuffle lengths):
fixture bytes and summaries identical to the pre-refactor binary. The
45-case CLI differential still passes too.

BenchEngines.cpp had two findings the IDE reported and I had not
predicted: S1186 wants the explanatory comment INSIDE an empty body, not
above it, and a const std::string& parameter should be a string_view.
Both fixed; isKnownEngine got the same treatment for consistency. The
build's clang-tidy gate separately caught a raw loop in the new
overlapsAt that should be std::ranges::any_of.

main.cpp, BenchOptions.cpp and BenchEngines.cpp now report zero
diagnostics in the IDE.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`guard-- > 0` as the right-hand operand of && is only evaluated when the
left side holds, so the retry count could only be read correctly by
reasoning about short-circuiting. The decrement moves to the first
statement of the body.

Moving it into a for-header instead was worse: the condition also tests
cells.size(), which the body grows, and that trips the "loop is
error-prone" rule. A while loop avoids both.

Iteration count is unchanged, and --generate still reproduces byte
-identical fixtures across the 40 seed/shuffle pairs.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
tryRunPairCancellation was ~30 against the limit of 25 and, more to the
point, five levels of nesting: pair loop, pair loop, overlap loop,
copy loop, skip test. Extracting the candidate builder alone would have
fixed the count and left the depth, so the per-pair attempt becomes
tryCancelPair and the outer function is just the pair enumeration.

tryReorderMerge was ~29, almost all of it the two inline candidate
builders; they become runPulledEarlier and runPushedLater. The odd
comparison of countSteps() against runs.size() is preserved as-is --
it mixes two different notions of "step", but that is existing
behaviour and not this change's business.

Verified: 8 optimizer unit tests pass and the 45-case CLI differential
is still byte-identical, which covers this since the reported
turns/steps/dirRuns are post-processed values.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
S2245 fired on every std::mt19937 mention: 5 in BenchCommands.cpp and
12 more across the test files. Its suggested fix -- libsodium
randombytes_uniform(), Botan randomize() -- is not applicable here, and
not because of speed. Those seed themselves from the OS and cannot be
seeded deterministically, and seedability is the whole point:
--generate --seed N has to keep producing the same board because
fuzzShiftingMosaic.ts addresses boards by seed, and a failing fuzz board
is only debuggable if it re-runs from its seed. Nothing drawn here
guards a secret.

So the engine moves behind SeededRng, which reduces 17 findings to one
suppression on a single member declaration, with the reasoning stated
once. Every call site stays fully analysed rather than carrying its own
NOSONAR.

draw() forwards to dist(engine), so reusable distributions keep their
exact draw sequences. Verified: --generate is byte-identical across 40
seed/shuffle pairs, and 71/71 tests pass -- including the ones seeded
with the same value as the oracle tests, which only agree if instance
generation is unchanged.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
AStar.cpp:317 was changed to std::vector(total, UINT16_MAX) in 2c3f580
by applying a "template arguments can be deduced" suggestion. UINT16_MAX
is an int, so that deduces vector<int>, which does not match the
vector<vector<uint16_t>> member. MSVC accepts it and clang does not, so
the native build, the 71-test suite and the clang-tidy gate all stayed
green while the shipped browser build could not compile at all. The
element type is spelled out again with a comment saying why it must not
be deduced.

The other 17 CTAD sites declare the variable itself, so the deduced type
IS the variable's type; this one was an argument to assign(), where the
target type was already fixed. That is the shape to watch for.

wasm_bindings.cpp: solve() was ~228 lines. Its arms were lambdas, which
read like extraction but are measured as part of their owner, so they
become real functions over a SolveContext. The nested conditional
picking the drag entry point becomes a DragMode enum and an if-chain,
and each cascade (threaded and sequential) is its own function behind
the existing #ifdef.

Verified: em++ compiles both the pthread and non-pthread variants, the
full wasm build succeeds for all four targets, and 44/44 wasm tests pass
-- with test37 reporting the same 531->419 moves / 110->100 steps as the
native binary.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The loop counter is size_t so it cannot be too small for its bound, but
block ids are uint8_t, so passing it straight to removeBlock/floodFill
narrows implicitly. Cast once at the use, matching how the other loops
in this file already handle it.

Reported by clangd rather than SonarLint or the build gate: MSVC does
not warn here, and clang-tidy only errors on diagnostics the compiler
flags enable, so this class of finding is visible in the IDE only.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Cognitive complexity 68 against the limit of 25, plus a nesting-depth
finding and S135 (at most one break/continue per loop) on the innermost
slide loop, which had three breaks and a continue.

Three extractions:

  * slideToNextState -- the slide itself. Its two blocked exits became
    returns, so the loop keeps a single break, the one meaning "reached
    a real successor".
  * searchLimitReached -- the budget and ceiling guards. Deliberately
    NOT a per-iteration call: the mask test stays at the call site and
    the node-cap compare stays inline, so this runs once per 256
    iterations. Hoisting the whole guard set into a per-iteration call
    is what made an earlier attempt at this 8% slower. Cadences are
    unchanged, since any multiple of 4096 is also a multiple of 256.
  * expandNode -- successor generation. Required rather than optional:
    the while loop is itself nesting level 1, so the two `for`s and any
    `if` inside them were already four deep and nothing done INSIDE the
    loop could fix that. Templated on the store and heap types so
    AStar.h need not name HeapEntry, which is file-local here.

Verified by interleaved A/B, min-of-5, alternating which binary runs
first: unit +0.1%, drag -0.0%, hier +0.1% against a +5% threshold, with
drag and hier serving as untouched controls that calibrate the noise
floor. nodesExpanded is identical on every leg, so the search visits the
same states. 71/71 tests, and em++ compiles both the pthread and
non-pthread variants.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
One 460-line function carrying the whole drag-space A*. Split into named
phases declared in DragSolver.h and defined beside their only caller, so
the compiler still sees every definition and inlines the hot ones.

Two phases take a small context struct rather than a parameter list:
runAStarDrag already sits at the 7-parameter limit, so passing its locals
individually would have traded one rule for another.

The 1-in-256 budget mask test stays at the call site -- it exists so the
common iteration never pays for the call. Only the node-cap check moved.

Extracting blockIsParked surfaced a latent shift hazard the old comment
already knew about: `slept >> i` is undefined for i >= 32 and was safe
only via "por implies shapes_.size() <= 32", an invariant not visible
locally. Both shifts now share one `i < 32` guard -- behaviour-identical,
since por cannot be true when i >= 32.

Interleaved A/B, min-of-5: drag -4.6%, hier -3.2%, unit (untouched
control leg) -3.5%, so no regression; node counts identical on all three
legs, which is the correctness assertion. 71/71 Release tests, clang-tidy
gate green, all four wasm variants build.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
e12cb14 split AStar and DragSolver into several .cpp files each and added
rolling-blocks' AStarGoals.cpp, but test.yml last changed in the earlier
555b7da and still named only the pre-split files. Both em++ steps would
have failed at link time with undefined symbols on any PR to main; local
builds were unaffected because they go through src/util/buildWasm.ts,
which has the correct lists.

Mirrors buildWasm.ts exactly: 13 sources for shifting-mosaic (held in a
bash array so the list appears once across the four variants) and the
missing AStarGoals.cpp for rolling-blocks. Verified by parsing the YAML,
replaying the step's shell logic with em++ stubbed out, and checking
every listed file exists -- the only .cpp left out are the bench CLI's,
which are not part of the module.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
computeJamField 67, computeDisplacementFields 69 and computeMovableSet 36
against a limit of 25, plus eight nesting findings. Each is now a small
driver over named phases; the two geometry structs (JamGeometry,
DispGeometry) carry what the old by-reference capture lambdas closed over,
which keeps the helpers inside the 7-parameter limit.

Four loops also tripped S1994 by testing something other than their
counter -- `d < 4 && !canMove`, `i < n && !hasBlocker`, `r < rows.size()
&& clear`. Those become any_of/none_of or a helper that returns early.
The displacement BFS is the exception: it walks a worklist the body
pushes onto, so a range-for would be wrong; it is now an explicit index
walk under a while, which is what it always meant.

lockedWallRows() replaces two byte-identical inline builds of the same
locked-block wall mask, one here and one in initSearchScratch.

Benched at 9 reps after a 5-rep run showed hier +7.2%: it reproduces at
-0.3%, worst leg +0.5%, so that was noise and no bisect was needed. Added
a jam leg first -- jamGuideWeight defaults to 0, so no existing leg
called computeJamField at all -- and the harness now diffs the whole
result JSON, not just node counts, since minJamTerm is what proves the
field still computes the same values. Identical on every leg. 71/71
Release tests, clang-tidy gate green, all four wasm variants build.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
computeMovableSet 38, countPathBlockers 33 and axisAwareBlockerCost 32
against a limit of 25, plus three nesting findings in the movable
fixpoint. computeMovableSet gets the same decomposition its DragSolver
twin got in e72ea4e, so the two now read alike.

Three dedups fell out rather than being sought:
- axisAwareBlockerCost and countSweepRectangleBlockers built the SAME
  sweep rectangle and ran the SAME bbox-then-cell overlap test. Both are
  now goalCorridor() + blockOverlapsRect(); the latter shrinks to a
  count_if.
- countPathBlockers' anchorPassable lambda was the goal block's own
  inBounds() spelled out a second time, so it is gone rather than moved.
- the occupancy build is shared, with skipGoal explicit because the two
  callers genuinely differ: the path walk must exclude the goal block,
  the movable fixpoint must include it.

Two behaviour-preserving wins: `stuck` now short-circuits past the
vertical probe when the horizontal one succeeds (canStepAlong is pure),
and the occupancy index is built after the BFS instead of before, so the
unreachable `return 4` path no longer builds one it never reads. The BFS
only ever consulted bounds, never occupancy.

9 reps: unit (the affected leg) -0.2%, worst leg +0.1%, result shape
identical everywhere. 71/71 Release tests, clang-tidy gate green, all
four wasm variants build.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Complexity 57 and 29 against a limit of 25, plus a nesting finding and
another S1994 worklist loop (`head < q.size() && !found`, where the body
pushes onto q) -- now an explicit index walk under a while, same as the
displacement BFS.

Almost all of this was already written elsewhere. relevanceMaskOf's
`valid` lambda is anchorClearOfWalls() with the four captures spelled
out, so it reuses DispGeometry outright; its ring dilation and
updateSleepEnvelope's envelope dilation are both dilateRow(), which now
serves three call sites; and its path-footprint walk is the jam sweep's,
so accumulatePathFootprint() replaces both and buildJamSweepRows shrinks
to a guard plus one call. The ring-0 and ring-k passes were the same
absorb-blocks-touching-a-mask loop twice, now absorbBlocksOnMask(); its
`mask >> b & 1u` guard subsumes ring 0's explicit goal skip because the
caller pre-sets the goal bit.

Added a --por --relevant leg first: sleepSets and relevantOnly both
default to false, so NO existing leg called either refactored function.
Verified the flags actually engage -- statesStored drops 446125 -> 233591
on the same fixture -- rather than assuming the leg covers the code.

9 reps: por +0.2%, worst leg +0.2%, result shape identical on all five
legs. 71/71 Release tests, clang-tidy gate green, all four wasm variants
build.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Complexity 141 and 72 against a limit of 25 -- the two biggest functions
in the tree -- plus seven nesting findings, two S1994 loop headers, two
S135 nested-break findings and a redundant lambda return type. Both are
now drivers over named phases, with BeamState/BeamCand/BeamRound and
Frame/HierStep lifted into the header so the phases can name them.

Two loops needed the S1994/S135 conflict resolved rather than one rule
traded for the other: a for header that tests state the body writes trips
S1994, and moving those tests into the body as `break`s trips S135. Both
became while loops whose condition carries the tests -- the beam depth
loop now has zero breaks where it had four.

Frame gets an explicit noexcept move constructor. This is not cosmetic:
std::unordered_set's move is not noexcept, so the implicit one was not
either, and std::vector<Frame> was therefore deep-COPYING every frame
(banned set included) on each reallocation instead of moving it. Moving a
set whose allocator is std::allocator only steals buckets, so the promise
holds. Retiring a frame now also moves just its anchors rather than the
whole Frame, since the anchors are all the ban needs.

Added a beam leg to the bench -- hier covered searchHierarchical but
nothing covered searchBeamJam. 9 reps: beam -1.1%, worst leg +0.7%,
result shape identical on all six legs. 71/71 Release tests, clang-tidy
gate green, all four wasm variants build.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Complexity 80 against a limit of 25, plus three nesting findings, a
nested ternary, an S135 four-break loop and a C-style ROUNDS array.

Same S1994/S135 resolution as the beam arm: the four-break
`for (uint32_t r = 0;; r++)` becomes a while whose condition carries the
stop tests, leaving zero breaks. The nested ternary picking the round's
start anchors becomes jamStartAnchors(), three plain returns.

ROUNDS is now a std::array behind jamRounds(). Round is private, so a
file-scope constant in the .cpp could not name it; the table returns from
a static member function instead of moving 70 lines into the header.

The jam round's start state (elite, ridge-hopper drags, the anchors they
reached) travels as one JamRound instead of four parallel locals, which
also localises the elite-pointer lifetime: bankElite reads jr.from only
while building the new Elite, before the push_back that would invalidate
it -- the same order the inline code relied on implicitly.

beamRoundBudgetSpent was byte-identical to what this arm needed, so it is
now roundBudgetSpent and shared.

9 reps: jam (the affected leg) +0.2%, worst leg +1.0%, result shape
identical on all six legs. 71/71 Release tests, clang-tidy gate green,
all four wasm variants build.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Complexity 149 against a limit of 25 -- the largest single function in
the tree -- plus six nesting findings, an S135 three-break loop, a
38-line lambda and an init-statement finding. It is now a driver over
named phases with AsmFrame/AsmRun/AsmOutcome in the header.

The DFS loop had three breaks meaning three different things: cancelled,
this packing is spent, everything is packed. They are now an AsmOutcome
the driver switches on, so the loop has none -- the tri-state was already
there, spelled as break plus two flags checked after the loop.

Two more duplicates fell out. applyTurns() replaces the joint endgame's
and the per-piece placement's identical anchor-stepping loops, and
postProcess() replaces the optimizer tail that searchAssembly and
finalizePlan each carried a copy of; finalizePlan already guaranteed a
non-empty plan, so its missing !empty() guard was not a difference.

The bench had no assembly leg at all, so this was the one arm whose
refactor nothing measured. It declines without real cuts and a packing,
so the leg runs test41 rather than test37 -- where it SOLVES, which makes
turns/steps/dirRuns in the shape check assertions over the entire
pipeline rather than just a node count.

9 reps: assembly -1.0%, worst leg +0.6%, result shape identical on all
seven legs. 71/71 Release tests, clang-tidy gate green (it caught
backtrackAssembly needing static), all four wasm variants build.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Valle12 and others added 6 commits July 28, 2026 12:51
floodFill walked its worklist with a for whose header tested queue_.size()
while the body pushed onto it -- the S1994 + range-for pair already fixed
in the two structurally identical BFS loops in DragSolverHeuristics and
DragSolverExpand. Now an explicit index walk under a while.

DX/DY/DIRS in AStar.h become std::array (cpp:S5945). In BitGrid.h they
deliberately do NOT: that conversion cost a measured 3.4% on the jam leg
and 3.3% on the beam leg, reproduced twice at 9 interleaved reps, and
reverting only that declaration recovered all of it (jam +3.4 -> +0.9,
beam +3.3 -> +0.1). Those two are indexed in floodFill's and
runJamDijkstra's 4-neighbour scans, the hottest loops in the solver. The
site now carries the measurement.

NOTE ON VERIFICATION: this file cannot be checked the way the .cpp files
were. SonarLint does not analyse a header standalone -- a deliberate
nested ternary planted in BitGrid.h produced no finding over four minutes
-- and the SonarQube server has never analysed this tree at all: of 78
open issues on the project, ZERO are under shifting-mosaic-solver/a-star.
So the rules applied here are the ones the sibling rolling-blocks headers
demonstrably trip (S5945 is reported on their AStar.h:54), not IDE output.

9 reps: worst leg +2.4% (assembly, 206->211ms on the shortest leg, inside
its spread today), jam +1.0%, every other leg <= 0.4%. Result shape
identical on all seven legs. 71/71 Release tests, clang-tidy gate green,
all four wasm variants build.

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

The sibling rolling-blocks NodeKey.h carries five open cpp:S5025 findings
(raw new/delete) and one cpp:S6022 (byte-oriented access); ours has the
same shape. Only one of the two is worth taking.

S6022 is free: the FNV loop now walks std::byte and folds with
std::to_integer instead of reinterpret_cast-ing to uint8_t. Benched at
+0.6% worst leg, i.e. nothing.

S5025 is not. std::unique_ptr<uint16_t[]> for the spill buffer is exactly
the same 48 bytes -- verified by compiling the struct before and after --
but it cost 3.7% on the jam leg and 2.6% on unit. A three-point bisect
separated the two changes cleanly:

  unique_ptr + byte   jam +4.1%  unit +2.2%
  unique_ptr only     jam +3.7%  unit +2.6%
  byte only           jam +0.6%  unit -0.2%

so the whole cost is the smart pointer, not the hash. NodeKey is the hash
key of every state map in every arm; its move and destroy paths are as
hot as code gets here. The raw pointer stays, with the measurement at the
declaration.

Each figure is min-of-9 interleaved reps, and the unique_ptr result was
reproduced across two independent runs before bisecting -- the first
reading was +4.1%, under the +5% threshold that would have let it through
unexamined.

71/71 Release tests, clang-tidy gate green, all four wasm variants build,
result shape identical on all seven legs throughout.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
FIRST: the wasm build was broken. A CLion CTAD quick-fix had turned
  std::vector<uint16_t>(total, UINT16_MAX)
back into
  std::vector(total, UINT16_MAX)
which deduces vector<int>. That is the exact mistake the comment directly
above the line warns about, from 9b219b3. MSVC accepts it, so the native
build, the clang-tidy gate and all 71 tests stayed green while em++
failed with "no known conversion from vector<int> to vector<unsigned
short>". Reverted, and the comment now records that it has been undone
once by a quick-fix.

Then the warnings: AStar 43, computeBlockReachability 80 and
computeGoalAnchorBfs 28, plus eight nesting findings and the
transparent-hasher suggestion. All now named phases.

The hasher finding is gone because the string-keyed unordered_map is
gone: same-shape blocks are grouped by comparing normalised cell lists
directly. There are at most a few dozen blocks, so the pairwise scan is
free at construction, and it drops the serialise-to-string step too.
Group order changes from hash order to first-appearance order, which
nothing reads -- signatureFromAnchors sorts each group independently.

lockedCellMask() replaces the identical locked-cell sweep that the
reachability pass and the goal-anchor BFS each carried.

PERFORMANCE: this costs ~3.5% on the unit leg and nothing anywhere else.
Established properly rather than guessed: a freshly-built HEAD benches
within 0.7% of the stored control, reverting only these two files
recovers the delta, and nothing that was split runs per node. The cost is
this TU's codegen shifting around lpDisplacementCost, the one per-node
function in it. Two attempts to recover it made things worse -- sharing
footprintClear() with its corridor test, and moving it to
AStarHeuristics.cpp (unit +3.7%, beam +4.2%). Accepted, unlike today's
two style-only refusals, because this one buys the actual reductions; the
file header carries the measurement.

9 reps against a freshly-built HEAD: unit +3.5%, every other leg within
+/-0.5%, result shape identical on all seven. 71/71 Release tests,
clang-tidy gate green, all four wasm variants build.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Authored in CLion, not by the refactor pass; committed here after the
same verification everything else got. All are local tidies with no
behavioural content:

- BenchEngines/BenchOptions: designated initialisers for the CASCADE
  stage table and the flag table, so each entry names its fields.
- BenchCommands: redundant parentheses dropped, and shuffleBoard's
  ShuffleStats consumed by structured binding.
- DragSolverArms: drops an include of Node.h that the searchJamRestarts
  split had already made unused.
- DragSolverAssembly / DragSolverExpand: CTAD for a
  std::unordered_set<uint64_t> and a std::vector<int32_t> whose element
  types are unambiguous from the initialiser.
- main.cpp: argc is const.

The one CTAD quick-fix that was NOT safe -- AStar.cpp's
std::vector(total, UINT16_MAX), which deduces vector<int> and breaks the
wasm build -- was reverted separately in cf811d7.

Verified as one tree: clang-tidy gate green, 71/71 Release tests, all
four wasm variants build, bench shape identical on all seven legs, and a
2000-case fuzz sweep at 2000/2000 solved with 0 invalid.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The file was already deleted in the working tree when this session
started; this only records that deletion. It is a per-machine Claude Code
permissions file -- the ".local" name is the convention for exactly the
kind of file a repo should not carry -- and nothing in the project reads
it, so tracking it only produced churn between machines.

Kept as its own commit because, unlike everything around it, this is a
tooling-config change rather than solver work: `git revert` here restores
the file without touching any code.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Follow-up to 5d0c2e4, which untracked .claude/settings.local.json: without
this the tool recreates the file and it comes straight back as untracked
noise.

Scoped to the one file rather than the .claude/ directory, so a shared
.claude/settings.json (or skills, commands) can still be tracked if the
project ever wants them. Verified with git check-ignore: the .local file
matches, .claude/settings.json and .claude/skills/ do not.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 28, 2026 •

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This PR adds a complete shifting-mosaic solver workflow, including C++ A*/drag engines, WASM builds and runtime selection, editor configuration and solution views, native benchmarking/fuzzing tools, CI coverage, fixtures, and extensive unit/E2E tests.

Changes

Shifting Mosaic solver

Layer / File(s) Summary
Solver engines and search infrastructure
src/pages/shifting-mosaic-solver/a-star/*
Adds unit and drag-space A* search, heuristics, deadlock handling, symmetry reduction, memory limits, parallel cascades, solution optimization, and native benchmark tooling.
Browser editor and solution workflow
src/pages/shifting-mosaic-solver/*
Adds configuration validation, upload/download and drag-and-drop support, responsive board sizing, WASM solver orchestration, and step-by-step solution rendering.
WASM, build, and isolation integration
src/common/*, src/util/build*, src/util/serve.ts, .github/workflows/test.yml, e2e/coi.ts
Builds and serves standard, threaded, MEMORY64, and threaded-MEMORY64 variants; adds COOP/COEP service-worker support, an isolation-wait E2E helper, and CI execution paths.
Validation and benchmark coverage
test/shiftingMosaicSolver/*, e2e/shiftingMosaicSolver*, e2e/shiftingMosaicWasmVariants.test.ts, e2e/shiftingMosaicHeapLimit.slow.test.ts, test/resources/*, src/pages/shifting-mosaic-solver/a-star/bench/*
Adds solver, editor, WASM, MEMORY64, fuzz, benchmark, and browser coverage (including WASM variant selection and heap-limit slow tests) with shifting-mosaic fixtures and recorded benchmark outputs.
Related solver and test maintenance
src/pages/rolling-blocks-solver/*, e2e/rollingBlocksSolver*, test/rollingBlocksSolver/*, src/pages/phasic-dial-solver/*, test/phasic-dial-solver/*
Extracts shared rolling-block goal-cluster utilities, adds progress wiring and ownership updates, expands solver E2E coverage, and adjusts phasic-dial output and assertions.

Estimated code review effort: 5 (Critical) | ~120 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title is directly related to the main change, naming the Shifting Mosaic Solver work, though it’s ticket-style rather than descriptive.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch IIT-21-Shifting-Mosaic-Solver

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 17

🧹 Nitpick comments (35)
.github/workflows/test.yml (2)

27-38: 🧹 Nitpick | 🔵 Trivial

New shifting-mosaic C++ test step mirrors rolling-blocks pattern.

Correctly wired build+ctest steps. Note per the comment, this makes CI hard-require a 4-8 min fixture (test37) plus ~25s (test41) plus 42 others on every run — a real CI duration cost, but the author has explicitly documented and accepted this tradeoff, so no action needed unless CI times become a problem.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/test.yml around lines 27 - 38, No code changes are
required; the shifting-mosaic C++ build and ctest steps are correctly
configured, and the documented fixture runtime tradeoff is explicitly accepted.

128-135: 🧹 Nitpick | 🔵 Trivial

The heap-limit regression guard is opt-in and never runs in this CI workflow.

e2e/shiftingMosaicHeapLimit.slow.test.ts (the test that specifically guards against the RuntimeError: Aborted() regression on the threads-mem64 variant) is skipped unless SM_SLOW_E2E is set, and this workflow never sets it. The threads+mem64 build is only compile-checked here (per the comment at lines 118-120), so the actual runtime regression this test protects against currently has no automated coverage. Consider adding a scheduled/nightly job that runs with SM_SLOW_E2E=1.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/test.yml around lines 128 - 135, Add scheduled/nightly CI
coverage for the heap-limit regression by introducing a job or workflow step
that sets SM_SLOW_E2E=1 and runs the relevant slow end-to-end test, including
the threads-mem64 runtime path. Keep the existing compile and standard test
steps unchanged, and ensure the scheduled job executes
e2e/shiftingMosaicHeapLimit.slow.test.ts rather than leaving it opt-in only.
e2e/rollingBlocksSolver.placement.test.ts (1)

3-31: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Duplicate, unused setupSolvedPuzzle helper.

This helper is byte-identical to the one in e2e/rollingBlocksSolver.solution.test.ts but is never called by the single test in this file. Extract to a shared test-helper module, or drop it here.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@e2e/rollingBlocksSolver.placement.test.ts` around lines 3 - 31, Remove the
unused setupSolvedPuzzle helper from this placement test, since it duplicates
the helper in rollingBlocksSolver.solution.test.ts and is not called here.
e2e/rollingBlocksSolver.solution.test.ts (1)

3-31: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Duplicate helper — candidate for extraction.

Same setupSolvedPuzzle implementation is duplicated verbatim in e2e/rollingBlocksSolver.placement.test.ts (where it's unused). Move to a shared helper module imported by both.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@e2e/rollingBlocksSolver.solution.test.ts` around lines 3 - 31, Extract the
duplicated setupSolvedPuzzle helper into a shared e2e helper module, then import
and use it from both rollingBlocksSolver.solution.test.ts and
rollingBlocksSolver.placement.test.ts. Preserve the existing setup behavior and
remove the unused local duplicate from the placement test.
e2e/shiftingMosaicSolver.config.test.ts (1)

1-59: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Duplicate test fixtures/helpers, one unused.

Same block as e2e/shiftingMosaicSolver.blocks.test.ts lines 1-59 — see that comment. paintWithGap is unused here too.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@e2e/shiftingMosaicSolver.config.test.ts` around lines 1 - 59, Remove the
duplicated SAMPLE_CONFIG, Cell type, and cellAt, cellCenter, paintShape, and
unused paintWithGap helpers from this test file; reuse the corresponding shared
fixtures and helpers from shiftingMosaicSolver.blocks.test.ts instead, updating
imports or references as needed.
e2e/shiftingMosaicSolver.blocks.test.ts (1)

1-59: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Duplicate test fixtures/helpers, one unused.

SAMPLE_CONFIG, Cell, cellAt, cellCenter, paintShape, and paintWithGap are byte-identical to the same block in e2e/shiftingMosaicSolver.config.test.ts. Additionally, paintWithGap is not called anywhere in either file. Recommend extracting the shared pieces to a common e2e helper module and dropping paintWithGap if it's genuinely unused, or adding a test that exercises it (e.g., verifying gap-drag placement behavior) if it was meant to be used.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@e2e/shiftingMosaicSolver.blocks.test.ts` around lines 1 - 59, Extract the
duplicated SAMPLE_CONFIG, Cell type, cellAt, cellCenter, and paintShape
fixtures/helpers from the shifting mosaic tests into a shared e2e helper module,
then import and reuse them in both shiftingMosaicSolver.blocks.test.ts and
shiftingMosaicSolver.config.test.ts. Remove paintWithGap if it is not needed;
only retain it by adding a test that exercises its gap-drag behavior.
src/pages/shifting-mosaic-solver/shiftingMosaicSolver.css (1)

591-649: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Solution-view colors are hardcoded literals rather than theme tokens.

#c84ad8, #1f9d57, #b88600 and #ffffff bypass the --md-sys-color-* system used elsewhere in this file, so these markers won't adapt to a dark/high-contrast theme and their contrast against --md-sys-color-surface-* is unverified. Consider mapping them to theme tokens (or defining them once as custom properties with per-scheme overrides).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/pages/shifting-mosaic-solver/shiftingMosaicSolver.css` around lines 591 -
649, Replace the hardcoded solution-view marker colors in .sm-moving-label,
.sm-dest-label, .sm-moving, .sm-dest::after, and .sm-goal-edge-* with existing
--md-sys-color-* theme tokens, or define shared custom properties with
light/dark/high-contrast overrides. Preserve the current visual roles while
ensuring marker contrast adapts to the surrounding surface colors.
e2e/shiftingMosaicWasmVariants.test.ts (1)

209-216: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Substring assertions on emscripten glue are brittle.

maximum:131072n, address:"i64", shared:true assume the exact minified spacing/quoting emscripten currently emits. A toolchain bump that emits maximum: 131072n or single quotes breaks this test without any real regression. Tolerant regexes keep the same guarantee.

♻️ Suggested loosening
-    expect(glue).toContain("maximum:131072n");
-    expect(glue).toContain('address:"i64"');
-    expect(glue).toContain("shared:true");
+    expect(glue).toMatch(/maximum:\s*131072n/);
+    expect(glue).toMatch(/address:\s*['"]i64['"]/);
+    expect(glue).toMatch(/shared:\s*true/);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@e2e/shiftingMosaicWasmVariants.test.ts` around lines 209 - 216, Update the
glue assertions in the test around the fetched astar.threads.mem64.mjs content
to use whitespace- and quote-tolerant regular expressions instead of exact
substring matches for maximum, address, and shared. Preserve validation of the
same values and flags while allowing equivalent Emscripten formatting changes.
test/shiftingMosaicSolver/wasm.test.ts (1)

183-224: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

No internal deadline: a test-level timeout leaves every arm worker alive.

racePortfolio only settles on done/error/exit. If an arm neither finishes nor exits, the Bun per-test timeout fires while the promise is still pending, so finish() never runs and the nine workers (each holding a wasm heap) survive into the next fixture — the exact leak the exit handler at Line 220 was added to avoid. A watchdog slightly under the test timeout closes the remaining path.

🛡️ Suggested guard
     return new Promise(resolve => {
       const workers = arms.map(() => new ArmWorker(workerPath));
       let pending = workers.length;
       let settled = false;
+      let watchdog: ReturnType<typeof setTimeout>;
       const finish = (turns: WasmTurn[]) => {
         if (settled) return;
         settled = true;
+        clearTimeout(watchdog);
         for (const w of workers) void w.terminate();
         resolve(turns);
       };
+      // Slightly under the enclosing test timeout so the arms are always
+      // torn down by us rather than abandoned by the runner.
+      watchdog = setTimeout(() => finish([]), budgetMs * 2);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/shiftingMosaicSolver/wasm.test.ts` around lines 183 - 224, Add an
internal watchdog within racePortfolio that fires slightly before the test-level
timeout, calls finish([]) to terminate all still-running arm workers, and
settles the promise. Ensure the watchdog is cleared when finish completes, while
preserving the existing done/error/exit retirement behavior.
src/pages/shifting-mosaic-solver/a-star/CMakeLists.txt (1)

54-62: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Hardcoded CLion paths and pinned LLVM 23 make linting effectively local-only.

The hint list pins one machine's CLion install, and SM_CLANG_TIDY_PROFILE_LLVM 23 (a dev snapshot no distro ships) means every CI host takes the skip branch — the "tree cannot drift out of compliance" goal isn't enforced anywhere automated. Consider letting users point at their own binary, e.g. honoring a cache/env override before the hints.

♻️ Optional override hook
     find_program(SM_CLANG_TIDY_EXE
         NAMES clang-tidy
-        HINTS "$ENV{ProgramFiles}/JetBrains/CLion 2024.1.4/bin/clang/win/x64/bin"
+        HINTS "$ENV{SM_CLANG_TIDY_DIR}"
+              "$ENV{ProgramFiles}/JetBrains/CLion 2024.1.4/bin/clang/win/x64/bin"
               "$ENV{ProgramFiles}/JetBrains/CLion/bin/clang/win/x64/bin"
     )
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/pages/shifting-mosaic-solver/a-star/CMakeLists.txt` around lines 54 - 62,
Update the SM_CLANG_TIDY_EXE discovery in CMakeLists.txt to honor a
user-provided cache or environment override for the clang-tidy executable before
applying CLion-specific HINTS, and avoid making lint enforcement depend solely
on the pinned LLVM 23 profile. Preserve the existing fallback discovery while
allowing CI and other hosts to supply their own compatible clang-tidy binary.
e2e/rollingBlocksSolver.tools.test.ts (1)

922-922: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Remove the unused setupSolvedPuzzle helper
e2e/rollingBlocksSolver.tools.test.ts:3 no longer references it.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@e2e/rollingBlocksSolver.tools.test.ts` at line 922, Remove the unused
setupSolvedPuzzle helper from rollingBlocksSolver.tools.test.ts, including its
definition and any associated import or setup code, since no references remain.
test/shiftingMosaicSolver/board.pointer.test.ts (1)

1-31: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Extract shared Board test harness instead of triplicating it.

The editorMock shape, DOM stub, and Board construction are copy-pasted identically across three files. A shared helper would reduce upkeep when the Board/editor interface changes.

  • test/shiftingMosaicSolver/board.pointer.test.ts#L1-L31: import a shared createBoardTestContext() helper instead of inlining the mock/Board setup.
  • test/shiftingMosaicSolver/board.render.test.ts#L1-L31: same — replace the duplicated beforeEach block with the shared helper.
  • test/shiftingMosaicSolver/board.state.test.ts#L1-L31: same — replace the duplicated beforeEach block with the shared helper.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/shiftingMosaicSolver/board.pointer.test.ts` around lines 1 - 31, Extract
the duplicated editor mock, DOM setup, and Board initialization into a shared
createBoardTestContext() helper, then update the beforeEach setup in
test/shiftingMosaicSolver/board.pointer.test.ts:1-31,
test/shiftingMosaicSolver/board.render.test.ts:1-31, and
test/shiftingMosaicSolver/board.state.test.ts:1-31 to use it while preserving
each test’s board and internal context access.
src/pages/shifting-mosaic-solver/a-star/AStar.h (1)

16-16: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚖️ Poor tradeoff

Class AStar exceeds the 20-field cohesion threshold (22 fields, per SonarCloud).

Consider grouping the board-analysis caches (blockValidAnchorMask_, initialGoalPathCells_, blockSafeAnchorDist_, goalAnchorBfsDist_) into a small nested struct to reduce the member count and improve cohesion. Given AStar.cpp's own notes that restructuring this file has measurably regressed performance before, treat this as low priority and re-measure if attempted.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/pages/shifting-mosaic-solver/a-star/AStar.h` at line 16, Optionally
reduce AStar’s 22-member count by grouping blockValidAnchorMask_,
initialGoalPathCells_, blockSafeAnchorDist_, and goalAnchorBfsDist_ into a small
nested cache struct within AStar, updating all AStar.cpp accesses accordingly.
Treat this as low priority, preserve behavior and performance, and re-measure
before keeping the change.

Source: Linters/SAST tools

src/pages/shifting-mosaic-solver/a-star/AStarOptimizer.cpp (1)

239-266: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

tryReorderMerge's improvement check compares against the wrong step metric (currently safe, but fragile).

baseSteps is computed via computeRuns(turns).size() — which splits on direction changes within a block — while the accept condition compares it against countSteps(cand), which only splits on block-ID changes (the coarser, actual player-step metric per this function's own doc comment: "the player-step count drops by one"). runs.size() >= countSteps(turns) always, so this check is a looser bound than intended.

Right now this can't produce a wrong accept: reaching this branch requires an intervening different-block run between ri/rj (proven by the prevRunOfBlock tracking), which guarantees the merge always yields exactly countSteps(original) - 1. But the check doesn't actually verify that invariant — comparing against countSteps(turns) directly would make the code self-validating against future changes to the run-tracking logic instead of relying on an implicit proof.

♻️ Proposed fix to compare against the actual pre-merge step count
 bool AStar::tryReorderMerge(std::vector<Turn> &turns) const {
   const auto runs = computeRuns(turns);
-  const size_t baseSteps = runs.size();
+  const size_t baseSteps = countSteps(turns);
   std::vector prevRunOfBlock(shapes_.size(), -1);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/pages/shifting-mosaic-solver/a-star/AStarOptimizer.cpp` around lines 239
- 266, Update tryReorderMerge to compute the baseline using countSteps(turns)
rather than runs.size(), then compare both runPulledEarlier and runPushedLater
candidates against that pre-merge player-step count while retaining replaySolves
validation and the existing successful replacement behavior.
src/pages/shifting-mosaic-solver/a-star/test/DragSolverTestOracle.h (1)

205-220: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

alone is dead — confirm the goal-target check is intentionally bbox-only.

alone is built and assigned but never read; only the bbox test below decides the target. If the leftover was meant to feed an oracleCanPlace(p, 0, t, alone) check, that check is missing; otherwise drop the two lines.

♻️ Remove the unused vector
     const Position t = {.x = static_cast<int8_t>(rng.draw(xDist)),
                         .y = static_cast<int8_t>(rng.draw(yDist))};
-    std::vector<Position> alone(p.anchors.size(), {.x = 127, .y = 127});
-    alone[0] = t;
     // Target just needs the goal's bbox in-grid.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/pages/shifting-mosaic-solver/a-star/test/DragSolverTestOracle.h` around
lines 205 - 220, Remove the unused alone vector and its assignment in the
goal-target generation loop around p.goalAnchor. Keep the existing
bounding-box-only validity check and target selection behavior unchanged.
src/pages/shifting-mosaic-solver/a-star/test/dragsolver_search_test.cpp (1)

159-173: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Wall-clock budget with a hard success assertion can flake on loaded CI.

ASSERT_FALSE(turns.empty()) for every oracle-solvable board is bounded by a 10s wall-clock budget, exactly the machine-speed dependence you avoided in JamRoundShapingStaysValidAndDefaultsUnchanged by switching to a node cap (Lines 116-120). Bounding these by nodes instead (searchHierarchical(0, N)) would make the pass/fail deterministic. Same applies to JamGuideAndJitterStayValidAndComplete.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/pages/shifting-mosaic-solver/a-star/test/dragsolver_search_test.cpp`
around lines 159 - 173, The solvable-board tests use a wall-clock search budget,
making success assertions machine-speed dependent. In
HierarchicalSolvesSolvableBoards and JamGuideAndJitterStayValidAndComplete,
replace the time-based searchHierarchical limit with a sufficiently large node
cap via the node-budget argument (for example, searchHierarchical(0, N)),
preserving the existing solvability and replay-validity assertions.
src/pages/shifting-mosaic-solver/a-star/BenchFixture.cpp (1)

138-146: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

writeTurnsFile silently swallows write failures.

Unlike writeFixtureAt, this never checks is_open()/stream state, so an unwritable path loses the plan with no signal — awkward when the turns file is the artifact a fuzz repro depends on.

♻️ Return success like `writeFixtureAt`
-void writeTurnsFile(const std::vector<Turn> &turns, const std::string &path) {
+bool writeTurnsFile(const std::vector<Turn> &turns, const std::string &path) {
   json tj = json::array();
   for (const auto &[blockId, direction] : turns)
     tj.push_back(
         {{"blockId", blockId},
          {"direction", static_cast<int>(std::to_underlying(direction))}});
   std::ofstream f(path);
+  if (!f.is_open())
+    return false;
   f << tj.dump();
+  return f.good();
 }

Header signature in BenchFixture.h needs the matching update.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/pages/shifting-mosaic-solver/a-star/BenchFixture.cpp` around lines 138 -
146, Update writeTurnsFile to return a success status like writeFixtureAt:
verify the output stream opened successfully and remains valid after writing,
returning false on failure and true only when the turns JSON is written
successfully. Update the matching declaration in BenchFixture.h and adjust all
call sites to handle the returned status.
src/pages/shifting-mosaic-solver/a-star/BenchFixture.h (1)

6-8: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Make BenchFixture.h self-contained. Add <cstddef> and change the size_t return types to std::size_t instead of relying on transitive/global declarations.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/pages/shifting-mosaic-solver/a-star/BenchFixture.h` around lines 6 - 8,
Add <cstddef> to BenchFixture.h and update all size_t return types in the
BenchFixture declarations to std::size_t, ensuring the header is self-contained
and no longer depends on transitive or global declarations.
src/pages/shifting-mosaic-solver/a-star/BenchOptions.cpp (2)

57-57: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Drop the hard-coded array size.

std::array<FlagSpec, 41> must be updated by hand whenever a flag is added/removed; a mismatch is a compile error at best, and it is pure bookkeeping. constexpr FlagSpec FLAGS[]{...} (or CTAD via std::to_array) deduces it.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/pages/shifting-mosaic-solver/a-star/BenchOptions.cpp` at line 57, Update
the FLAGS declaration in BenchOptions to use a size-deduced array instead of the
hard-coded std::array<FlagSpec, 41>, such as a deduced constexpr array or
std::to_array, while preserving the existing flag entries and constexpr
behavior.

209-226: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider bailing out as soon as the cursor records an error.

cur.error() is only inspected after the whole argv is consumed, so a malformed value keeps applying later flags (and value() returning "" writes empty paths into opt). Behaviour is still correct because parseBenchOptions returns false and main.cpp aborts before use, but an early if (!cur.error().empty()) break; inside the loop makes the contract obvious.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/pages/shifting-mosaic-solver/a-star/BenchOptions.cpp` around lines 209 -
226, Update parseBenchOptions to check cur.error() immediately after each
spec->apply(opt, cur) call and stop processing further arguments when an error
is recorded. Keep the existing final error reporting and false return unchanged,
while preventing later flags from modifying opt after a malformed value.
src/pages/shifting-mosaic-solver/a-star/DragSolverBeam.cpp (1)

130-137: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Verify the score bit-packing has room for the jam term.

jamChild is capped at 1u << 19 and shifted left 14, so the top field occupies bits 14–33 of a uint64_t — fine — but h << 10 only leaves bits 10–13 for a value that is documented as 0/1, and the jitter is masked to 10 bits. Worth a brief comment recording the field widths so a future change to h (or a wider jitter) doesn't silently collide.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/pages/shifting-mosaic-solver/a-star/DragSolverBeam.cpp` around lines 130
- 137, Add a concise comment immediately above the score construction
documenting the bit layout: jamChild uses bits 14–33, h uses bits 10–13, and
beamJitter uses the low 10 bits. Keep the existing packing unchanged and note
the expected h range so future width changes are checked for overlap.
src/pages/shifting-mosaic-solver/a-star/DragSolverAssembly.cpp (2)

53-66: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Name the 40 reachability threshold.

Every other tuned constant in this file is a named constexpr with a rationale comment (UNFREEZE_K, ENDGAME_K, MAX_JOINT, MAX_BACKTRACKS); this one is a bare literal that silently decides "big enough free region ⇒ approachable".

♻️ Suggested change
+  // A flood region this large is effectively open space; treat the piece as
+  // approachable without checking its current anchor.
+  constexpr size_t OPEN_REGION_CELLS = 40;
   if (const auto &reached = grid_.floodFill(piece, slotPos(piece));
-      reached.size() >= 40)
+      reached.size() >= OPEN_REGION_CELLS)
     return true;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/pages/shifting-mosaic-solver/a-star/DragSolverAssembly.cpp` around lines
53 - 66, Name the flood-fill reachability threshold used by
DragSolver::approachable as a file-level constexpr, replacing the bare 40
literal. Add a concise rationale comment documenting that it represents the
minimum free-region size considered large enough for approachability, and use
the named constant in the reached.size() comparison.

206-218: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚖️ Poor tradeoff

Solver progress goes to raw std::cout.

This code also runs inside the wasm build where the arms race concurrently; unconditional std::cout from every arm interleaves and cannot be silenced by callers. A verbosity flag on Config (or a small logging hook alongside onProgress) would keep the diagnostics without forcing them on embedders.

Also applies to: 274-279, 440-466

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/pages/shifting-mosaic-solver/a-star/DragSolverAssembly.cpp` around lines
206 - 218, The solver’s progress messages in the joint-endgame and other cited
paths write unconditionally to std::cout, preventing callers from suppressing or
safely collecting output. Route these diagnostics through a Config verbosity
flag or logging/progress hook alongside onProgress, and guard or dispatch all
affected messages—including the locations around jointEndgame and lines
440-466—through that mechanism instead of direct std::cout.
src/pages/shifting-mosaic-solver/a-star/DragSolver.cpp (1)

211-252: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low value

Consider avoiding std::function for the hot packing DFS.

dfs is called up to PACK_NODE_CAP (5M) times and every recursive step pays an indirect call through std::function plus the type-erasure barrier that blocks inlining. A small local recursive struct (or a template <typename Self> lambda / explicit worklist) keeps the same shape without the indirection.

♻️ Sketch
-  std::function<bool(size_t)> dfs = [&](const size_t fromRank) -> bool {
+  const auto dfs = [&](this const auto &self, const size_t fromRank) -> bool {

(deducing-this recursive lambda if the toolchain is C++23; otherwise a local struct Dfs { ... bool run(size_t); }.)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/pages/shifting-mosaic-solver/a-star/DragSolver.cpp` around lines 211 -
252, Replace the std::function-based recursive dfs in the packing search with a
directly callable recursive implementation, such as a self-passing templated
lambda or local recursive struct, while preserving the existing DFS behavior,
state updates, and PACK_NODE_CAP handling. Ensure recursive calls avoid
std::function type erasure and indirect dispatch.

Source: Linters/SAST tools

src/pages/shifting-mosaic-solver/a-star/DragSolverArms.cpp (1)

176-178: 🩺 Stability & Availability | 🔵 Trivial | 💤 Low value

Guard the unsigned subtraction in the remaining-node computation.

maxNodes - stats_.nodesExpanded wraps if expansions ever overshoot maxNodes (rounds are capped, not clamped), and the wrapped value makes std::min a no-op — the opposite of the intent. The loop guard currently makes this unreachable, but a saturating form removes the dependency.

♻️ Proposed change
-  if (maxNodes != 0)
-    cap = std::min<uint32_t>(cap, maxNodes - stats_.nodesExpanded);
+  if (maxNodes != 0)
+    cap = std::min<uint32_t>(cap, maxNodes > stats_.nodesExpanded
+                                      ? maxNodes - stats_.nodesExpanded
+                                      : 0);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/pages/shifting-mosaic-solver/a-star/DragSolverArms.cpp` around lines 176
- 178, Update the remaining-node calculation near the return cap logic to
saturate maxNodes - stats_.nodesExpanded at zero when expansions meet or exceed
maxNodes, then apply std::min with cap only to the non-underflowing remainder.
Preserve the existing unlimited behavior when maxNodes is zero.
src/pages/shifting-mosaic-solver/a-star/DragSolverSearch.cpp (3)

40-49: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

operator== on DragHeapEntry appears dead code.

The heap only needs ordering (via std::greater<>, which is satisfiable from operator<=>); operator== isn't otherwise invoked in this file. SonarCloud's "unused member function" flag looks correct here.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/pages/shifting-mosaic-solver/a-star/DragSolverSearch.cpp` around lines 40
- 49, Remove the unused operator== overload from DragHeapEntry, leaving
operator<=> unchanged so std::greater<> continues to provide the heap ordering.

Source: Linters/SAST tools


300-318: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Reference-identity shortcut for mask comparison — fragile if cutSuffixRows_ is ever resized mid-search.

if (&maskN == &exp.maskP)
    return 1 + exp.baseCount;

This relies on cutSuffixRows_ elements never moving between the time exp.maskP was bound and this call. Given it's a member fixed up-front per computeDisplacementFields, this is presumably safe today, but it's a non-obvious invariant for future maintainers touching cutSuffixRows_'s lifetime.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/pages/shifting-mosaic-solver/a-star/DragSolverSearch.cpp` around lines
300 - 318, Replace the address-based mask comparison in the displacement-count
logic with a value-based comparison between maskN and exp.maskP, preserving the
existing 1 + exp.baseCount result when the masks are equivalent. Update the code
in the method containing the DragExpansion parameter without changing the
surrounding counting behavior.

20-51: 📐 Maintainability & Code Quality | 🔵 Trivial

Align the analyzer with C++23
src/pages/shifting-mosaic-solver/a-star/CMakeLists.txt already sets CMAKE_CXX_STANDARD 23, and this codebase deliberately uses <=>, std::ranges, and .contains(). The static-analysis job needs the same standard flag instead of reporting these as source defects.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/pages/shifting-mosaic-solver/a-star/DragSolverSearch.cpp` around lines 20
- 51, Update the analyzer configuration in the a-star CMake setup to use C++23,
matching the existing CMAKE_CXX_STANDARD 23 setting. Ensure the static-analysis
invocation receives the equivalent C++23 standard flag so valid usage of
DragHeapEntry’s operator<=> and other C++23 APIs is analyzed correctly.
test/shiftingMosaicSolver/aStar.search.test.ts (1)

216-216: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Prefer toHaveLength for length assertions.

SonarCloud suggests expect(turns).toHaveLength(1) over expect(turns.length).toBe(1) for clearer failure reporting.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/shiftingMosaicSolver/aStar.search.test.ts` at line 216, Update the
assertion in the relevant A* search test to use toHaveLength directly on turns,
preserving the expected length of 1.

Source: Linters/SAST tools

src/pages/shifting-mosaic-solver/aStar.ts (4)

106-110: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Move field declarations above the constructor.

movableBlockIndices, unsolvableAtStart, and goalAnchorBfsDist are declared with initializers after the constructor that populates them (lines 75-92). This is safe (field initializers run before the constructor body), but it reads as if the constructor's work gets reset — worth moving these declarations above the constructor for clarity.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/pages/shifting-mosaic-solver/aStar.ts` around lines 106 - 110, Move the
field declarations movableBlockIndices, unsolvableAtStart, and goalAnchorBfsDist
above the constructor in the solver class, preserving their existing
initializers and types. Do not change the constructor’s initialization logic or
field behavior.

152-259: 📐 Maintainability & Code Quality | 🔵 Trivial | 🏗️ Heavy lift

Reduce cognitive complexity of search.

SonarCloud flags this function at cognitive complexity 39 (limit 15). Logic itself checks out (dedup via closedSet/gScore, timeout budget, successor generation), but consider extracting the successor-expansion inner loop (Lines 220-254) and/or the timeout/goal checks into named helpers to aid readability and future maintenance.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/pages/shifting-mosaic-solver/aStar.ts` around lines 152 - 259, Reduce
cognitive complexity in the AStar search method by extracting the
successor-generation logic from the movableBlockIndices/direction loops into a
named helper, and extract additional timeout or goal-check handling if needed to
meet the configured threshold. Preserve the existing closedSet/gScore
deduplication, cameFrom updates, heap insertion, timeout behavior, deadlock
pruning, and goal-path reconstruction exactly.

Source: Linters/SAST tools


377-429: 📐 Maintainability & Code Quality | 🔵 Trivial | 🏗️ Heavy lift

Reduce cognitive complexity of computeMovableSet.

SonarCloud flags this function at cognitive complexity 37 (limit 15). The fixpoint logic is correct, but consider extracting the per-block "can this direction be freed" check (Lines 405-414) into a helper method to shrink nesting depth.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/pages/shifting-mosaic-solver/aStar.ts` around lines 377 - 429, Reduce
cognitive complexity in computeMovableSet by extracting the per-direction
occupancy check into a focused helper method that determines whether a block can
move in a given direction, using the existing anchors, shape, occupancy, and
movable set. Keep the fixpoint iteration and its current behavior unchanged, and
have computeMovableSet delegate the directional checks to the helper.

Source: Linters/SAST tools


436-465: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Reduce cognitive complexity of isDeadlocked.

SonarCloud flags cognitive complexity 21 (limit 15). The "does block i occupy a goal-final cell" check is duplicated between the hasBlocker scan and the final loop — extracting it into a helper both fixes the duplication and reduces nesting.

♻️ Proposed refactor
+  private blockOccupiesGoalFinalCells(shapeIndex: number, anchor: Position): boolean {
+    for (const cell of this.shapes[shapeIndex]!) {
+      const enc = (anchor.x + cell.x) * this.gridHeight + (anchor.y + cell.y);
+      if (this.goalBlockFinalCells.has(enc)) return true;
+    }
+    return false;
+  }
+
   private isDeadlocked(anchors: Position[]): boolean {
-    const stride = this.gridHeight;
-    let hasBlocker = false;
-    for (let i = 0; i < anchors.length; i++) {
-      if (i === this.goalIndex) continue;
-      const a = anchors[i]!;
-      for (const cell of this.shapes[i]!) {
-        const enc = (a.x + cell.x) * stride + (a.y + cell.y);
-        if (this.goalBlockFinalCells.has(enc)) {
-          hasBlocker = true;
-          break;
-        }
-      }
-      if (hasBlocker) break;
-    }
-    if (!hasBlocker) return false;
+    const hasBlocker = anchors.some(
+      (a, i) => i !== this.goalIndex && this.blockOccupiesGoalFinalCells(i, a),
+    );
+    if (!hasBlocker) return false;

     const movable = this.computeMovableSet(anchors);

-    for (let i = 0; i < anchors.length; i++) {
-      if (i === this.goalIndex) continue;
-      if (movable.has(i)) continue;
-      const a = anchors[i]!;
-      for (const cell of this.shapes[i]!) {
-        const enc = (a.x + cell.x) * stride + (a.y + cell.y);
-        if (this.goalBlockFinalCells.has(enc)) return true;
-      }
-    }
-    return false;
+    return anchors.some(
+      (a, i) =>
+        i !== this.goalIndex &&
+        !movable.has(i) &&
+        this.blockOccupiesGoalFinalCells(i, a),
+    );
   }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/pages/shifting-mosaic-solver/aStar.ts` around lines 436 - 465, Reduce
cognitive complexity in isDeadlocked by extracting the duplicated check for
whether a block occupies any cell in goalBlockFinalCells into a private helper.
Reuse that helper in both the initial hasBlocker scan and the final
immovable-block scan, preserving the existing goalIndex exclusions and deadlock
behavior.

Source: Linters/SAST tools

test/shiftingMosaicSolver/aStar.collision.test.ts (2)

55-81: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Empty test asserts nothing.

This test builds shapes, comments through several false starts, then only does void shapeA; void shapeB; void shapeBReal; — no blocksCollide call, no expect(...). It always passes without testing anything, which is misleading since the next test (Lines 83-112) already covers the intended bbox-overlap-but-no-cell-collision scenario. Recommend removing this test.

♻️ Proposed fix
-    test("two L-shapes whose bboxes overlap but whose cells do not are not colliding", () => {
-      // Shape A: L pointing top-right
-      //   X X
-      //   X .
-      const shapeA: Position[] = [
-        { x: 0, y: 0 },
-        { x: 1, y: 0 },
-        { x: 0, y: 1 },
-      ];
-      // Shape B: just bottom-right cell
-      //   . X
-      const shapeB: Position[] = [{ x: 1, y: 1 }];
-      // We use a 2x2 bbox shape B by also including {x:0,y:0} for bbox
-      // ... but we want a shape that fits the (1,1) corner exactly.
-      // Easier: use a real bbox-overlapping case where the cell at (1,1) is empty.
-      const shapeBReal: Position[] = [
-        { x: 1, y: 1 },
-        { x: 0, y: 1 },
-      ];
-      // shapeBReal's bbox is 2x2 (because we have x∈{0,1}, y∈{0,1}... wait y=1 only)
-      // Actually bbox is 2 wide, 1 tall (since min(y)=1, but bbox calc uses max(y)+1 = 2)
-      // Hmm bbox calc is just max+1 — let me redo with normalized shape.
-      // Skip this — use a more direct test below instead.
-      void shapeA;
-      void shapeB;
-      void shapeBReal;
-    });
-
     test("L-shape and single cell — bbox overlaps, cells overlap", () => {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/shiftingMosaicSolver/aStar.collision.test.ts` around lines 55 - 81,
Remove the empty test case that defines shapeA, shapeB, and shapeBReal but
performs no collision check or assertion. Keep the subsequent test covering bbox
overlap without cell collision unchanged.

12-51: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Duplicated test helpers across two files. makeAStar and applyTurns are copy-pasted verbatim between the two test files; extracting them into a shared test-utils module (e.g. test/shiftingMosaicSolver/testHelpers.ts) avoids drift as more AStar unit-test files are added to this suite.

  • test/shiftingMosaicSolver/aStar.collision.test.ts#L12-L51: import makeAStar/applyTurns from a new shared helper module instead of defining them locally.
  • test/shiftingMosaicSolver/aStar.search.test.ts#L12-L51: same — import from the shared helper module instead of the duplicate local definitions.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/shiftingMosaicSolver/aStar.collision.test.ts` around lines 12 - 51,
Extract the duplicated makeAStar and applyTurns helpers into a shared test
utility module. In test/shiftingMosaicSolver/aStar.collision.test.ts lines 12-51
and test/shiftingMosaicSolver/aStar.search.test.ts lines 12-51, remove the local
definitions and import both helpers from the shared module, preserving their
existing behavior and required dependencies.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 302fa3b5-73de-4b7d-b3b1-287957c48865

📥 Commits

Reviewing files that changed from the base of the PR and between 068e4fb and c306249.

📒 Files selected for processing (182)
  • .claude/settings.local.json
  • .github/workflows/test.yml
  • .gitignore
  • e2e/phasicDialSolver.solvable.test.ts
  • e2e/phasicDialSolver.states.test.ts
  • e2e/rollingBlocksSolver.placement.test.ts
  • e2e/rollingBlocksSolver.solution.test.ts
  • e2e/rollingBlocksSolver.tools.test.ts
  • e2e/shiftingMosaicHeapLimit.slow.test.ts
  • e2e/shiftingMosaicSolver.blocks.test.ts
  • e2e/shiftingMosaicSolver.config.test.ts
  • e2e/shiftingMosaicSolver.editing.test.ts
  • e2e/shiftingMosaicSolver.solving.test.ts
  • e2e/shiftingMosaicSolver.test.ts
  • e2e/shiftingMosaicWasmVariants.test.ts
  • package.json
  • playwright.config.ts
  • src/common/coi-serviceworker.js
  • src/common/coiRegister.ts
  • src/common/common.css
  • src/pages/phasic-dial-solver/phasicDialSolver.ts
  • src/pages/rolling-blocks-solver/a-star/AStar.cpp
  • src/pages/rolling-blocks-solver/a-star/AStar.h
  • src/pages/rolling-blocks-solver/a-star/AStarGoals.cpp
  • src/pages/rolling-blocks-solver/a-star/CMakeLists.txt
  • src/pages/rolling-blocks-solver/a-star/GoalCluster.h
  • src/pages/rolling-blocks-solver/a-star/Node.h
  • src/pages/rolling-blocks-solver/a-star/NodeKey.h
  • src/pages/rolling-blocks-solver/a-star/test/CMakeLists.txt
  • src/pages/rolling-blocks-solver/a-star/wasm_bindings.cpp
  • src/pages/rolling-blocks-solver/aStar.ts
  • src/pages/rolling-blocks-solver/bfs.ts
  • src/pages/rolling-blocks-solver/board.ts
  • src/pages/rolling-blocks-solver/goalClusters.ts
  • src/pages/rolling-blocks-solver/rollingBlocksSolver.ts
  • src/pages/shifting-mosaic-solver/a-star/.clang-tidy
  • src/pages/shifting-mosaic-solver/a-star/AStar.cpp
  • src/pages/shifting-mosaic-solver/a-star/AStar.h
  • src/pages/shifting-mosaic-solver/a-star/AStarHeuristics.cpp
  • src/pages/shifting-mosaic-solver/a-star/AStarOptimizer.cpp
  • src/pages/shifting-mosaic-solver/a-star/AStarSearch.cpp
  • src/pages/shifting-mosaic-solver/a-star/BenchCommands.cpp
  • src/pages/shifting-mosaic-solver/a-star/BenchCommands.h
  • src/pages/shifting-mosaic-solver/a-star/BenchEngines.cpp
  • src/pages/shifting-mosaic-solver/a-star/BenchEngines.h
  • src/pages/shifting-mosaic-solver/a-star/BenchFixture.cpp
  • src/pages/shifting-mosaic-solver/a-star/BenchFixture.h
  • src/pages/shifting-mosaic-solver/a-star/BenchOptions.cpp
  • src/pages/shifting-mosaic-solver/a-star/BenchOptions.h
  • src/pages/shifting-mosaic-solver/a-star/BitGrid.h
  • src/pages/shifting-mosaic-solver/a-star/CMakeLists.txt
  • src/pages/shifting-mosaic-solver/a-star/ClangdCompat.h
  • src/pages/shifting-mosaic-solver/a-star/DragSolver.cpp
  • src/pages/shifting-mosaic-solver/a-star/DragSolver.h
  • src/pages/shifting-mosaic-solver/a-star/DragSolverArms.cpp
  • src/pages/shifting-mosaic-solver/a-star/DragSolverAssembly.cpp
  • src/pages/shifting-mosaic-solver/a-star/DragSolverBeam.cpp
  • src/pages/shifting-mosaic-solver/a-star/DragSolverConfig.h
  • src/pages/shifting-mosaic-solver/a-star/DragSolverExpand.cpp
  • src/pages/shifting-mosaic-solver/a-star/DragSolverHeuristics.cpp
  • src/pages/shifting-mosaic-solver/a-star/DragSolverSearch.cpp
  • src/pages/shifting-mosaic-solver/a-star/MemoryProbe.h
  • src/pages/shifting-mosaic-solver/a-star/Node.h
  • src/pages/shifting-mosaic-solver/a-star/NodeKey.h
  • src/pages/shifting-mosaic-solver/a-star/PARALLEL.md
  • src/pages/shifting-mosaic-solver/a-star/ParallelCascade.h
  • src/pages/shifting-mosaic-solver/a-star/SeededRng.h
  • src/pages/shifting-mosaic-solver/a-star/SolverClock.cpp
  • src/pages/shifting-mosaic-solver/a-star/SolverClock.h
  • src/pages/shifting-mosaic-solver/a-star/StateTable.h
  • src/pages/shifting-mosaic-solver/a-star/Types.h
  • src/pages/shifting-mosaic-solver/a-star/bench/HARD-BOARDS.md
  • src/pages/shifting-mosaic-solver/a-star/bench/after-cascade.json
  • src/pages/shifting-mosaic-solver/a-star/bench/after-luby.json
  • src/pages/shifting-mosaic-solver/a-star/bench/baseline-unit.json
  • src/pages/shifting-mosaic-solver/a-star/bench/before-cascade.json
  • src/pages/shifting-mosaic-solver/a-star/bench/drag-settled-w2.json
  • src/pages/shifting-mosaic-solver/a-star/bench/fuzz-1022-hard.json
  • src/pages/shifting-mosaic-solver/a-star/bench/fuzz-10276-hard.json
  • src/pages/shifting-mosaic-solver/a-star/bench/fuzz-11386-hard.json
  • src/pages/shifting-mosaic-solver/a-star/bench/fuzz-12366-hard.json
  • src/pages/shifting-mosaic-solver/a-star/bench/fuzz-23444-hard.json
  • src/pages/shifting-mosaic-solver/a-star/bench/fuzz-29534-hard.json
  • src/pages/shifting-mosaic-solver/a-star/bench/fuzz-3058-hard.json
  • src/pages/shifting-mosaic-solver/a-star/bench/fuzz-3086-hard.json
  • src/pages/shifting-mosaic-solver/a-star/bench/fuzz-3870-hard.json
  • src/pages/shifting-mosaic-solver/a-star/bench/fuzz-4722-hard.json
  • src/pages/shifting-mosaic-solver/a-star/bench/fuzz-8697-hard.json
  • src/pages/shifting-mosaic-solver/a-star/bench/fuzz-9164-hard.json
  • src/pages/shifting-mosaic-solver/a-star/main.cpp
  • src/pages/shifting-mosaic-solver/a-star/test/CMakeLists.txt
  • src/pages/shifting-mosaic-solver/a-star/test/DragSolverTestOracle.h
  • src/pages/shifting-mosaic-solver/a-star/test/astar_unit_test.cpp
  • src/pages/shifting-mosaic-solver/a-star/test/dragsolver_search_test.cpp
  • src/pages/shifting-mosaic-solver/a-star/test/dragsolver_unit_test.cpp
  • src/pages/shifting-mosaic-solver/a-star/test/json_test.cpp
  • src/pages/shifting-mosaic-solver/a-star/test/optimizer_test.cpp
  • src/pages/shifting-mosaic-solver/a-star/wasm_bindings.cpp
  • src/pages/shifting-mosaic-solver/aStar.ts
  • src/pages/shifting-mosaic-solver/board.ts
  • src/pages/shifting-mosaic-solver/boardGeometry.ts
  • src/pages/shifting-mosaic-solver/config.ts
  • src/pages/shifting-mosaic-solver/directions.ts
  • src/pages/shifting-mosaic-solver/editorHelpers.ts
  • src/pages/shifting-mosaic-solver/index.html
  • src/pages/shifting-mosaic-solver/layout.ts
  • src/pages/shifting-mosaic-solver/node.ts
  • src/pages/shifting-mosaic-solver/shiftingMosaicSolver.css
  • src/pages/shifting-mosaic-solver/shiftingMosaicSolver.ts
  • src/pages/shifting-mosaic-solver/solutionView.ts
  • src/pages/shifting-mosaic-solver/turn.ts
  • src/pages/shifting-mosaic-solver/wasm/astar.worker.js
  • src/pages/shifting-mosaic-solver/wasmBridge.ts
  • src/util/benchShiftingMosaic.ts
  • src/util/benchWasmSimd.ts
  • src/util/build.ts
  • src/util/buildWasm.ts
  • src/util/fuzzShiftingMosaic.ts
  • src/util/minHeap.ts
  • src/util/plugins.ts
  • src/util/serve.ts
  • src/util/types.ts
  • test/phasic-dial-solver/phasicDialSolver.test.ts
  • test/resources/shiftingMosaicTest.json
  • test/resources/shiftingMosaicTest1.json
  • test/resources/shiftingMosaicTest10.json
  • test/resources/shiftingMosaicTest11.json
  • test/resources/shiftingMosaicTest12.json
  • test/resources/shiftingMosaicTest13.json
  • test/resources/shiftingMosaicTest14.json
  • test/resources/shiftingMosaicTest15.json
  • test/resources/shiftingMosaicTest16.json
  • test/resources/shiftingMosaicTest17.json
  • test/resources/shiftingMosaicTest18.json
  • test/resources/shiftingMosaicTest19.json
  • test/resources/shiftingMosaicTest2.json
  • test/resources/shiftingMosaicTest20.json
  • test/resources/shiftingMosaicTest21.json
  • test/resources/shiftingMosaicTest22.json
  • test/resources/shiftingMosaicTest23.json
  • test/resources/shiftingMosaicTest24.json
  • test/resources/shiftingMosaicTest25.json
  • test/resources/shiftingMosaicTest26.json
  • test/resources/shiftingMosaicTest27.json
  • test/resources/shiftingMosaicTest28.json
  • test/resources/shiftingMosaicTest29.json
  • test/resources/shiftingMosaicTest3.json
  • test/resources/shiftingMosaicTest30.json
  • test/resources/shiftingMosaicTest31.json
  • test/resources/shiftingMosaicTest32.json
  • test/resources/shiftingMosaicTest33.json
  • test/resources/shiftingMosaicTest34.json
  • test/resources/shiftingMosaicTest35.json
  • test/resources/shiftingMosaicTest36.json
  • test/resources/shiftingMosaicTest37.json
  • test/resources/shiftingMosaicTest38.json
  • test/resources/shiftingMosaicTest39.json
  • test/resources/shiftingMosaicTest4.json
  • test/resources/shiftingMosaicTest40.json
  • test/resources/shiftingMosaicTest41.json
  • test/resources/shiftingMosaicTest42.json
  • test/resources/shiftingMosaicTest43.json
  • test/resources/shiftingMosaicTest5.json
  • test/resources/shiftingMosaicTest6.json
  • test/resources/shiftingMosaicTest7.json
  • test/resources/shiftingMosaicTest8.json
  • test/resources/shiftingMosaicTest9.json
  • test/rollingBlocksSolver/block.musttouch.test.ts
  • test/rollingBlocksSolver/block.roll.test.ts
  • test/rollingBlocksSolver/block.validity.test.ts
  • test/shiftingMosaicSolver/aStar.collision.test.ts
  • test/shiftingMosaicSolver/aStar.primitives.test.ts
  • test/shiftingMosaicSolver/aStar.search.test.ts
  • test/shiftingMosaicSolver/board.geometry.test.ts
  • test/shiftingMosaicSolver/board.pointer.test.ts
  • test/shiftingMosaicSolver/board.render.test.ts
  • test/shiftingMosaicSolver/board.state.test.ts
  • test/shiftingMosaicSolver/board.test.ts
  • test/shiftingMosaicSolver/config.test.ts
  • test/shiftingMosaicSolver/mem64.node.test.mjs
  • test/shiftingMosaicSolver/portfolio.worker.ts
  • test/shiftingMosaicSolver/wasm.test.ts
💤 Files with no reviewable changes (2)
  • e2e/shiftingMosaicSolver.test.ts
  • .claude/settings.local.json

Comment thread src/common/common.css Resolved
Comment thread src/util/benchShiftingMosaic.ts Resolved
Comment thread src/util/benchShiftingMosaic.ts Resolved
Comment thread src/util/benchShiftingMosaic.ts Resolved

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Inline review comments failed to post. This is likely due to GitHub's internal server error or limits when posting large numbers of comments. If you are seeing this consistently it is likely a permissions issue. Please check "Moderation" -> "Code review limits" under your organization settings.

Actionable comments posted: 17

🧹 Nitpick comments (35)
.github/workflows/test.yml (2)

27-38: 🧹 Nitpick | 🔵 Trivial

New shifting-mosaic C++ test step mirrors rolling-blocks pattern.

Correctly wired build+ctest steps. Note per the comment, this makes CI hard-require a 4-8 min fixture (test37) plus ~25s (test41) plus 42 others on every run — a real CI duration cost, but the author has explicitly documented and accepted this tradeoff, so no action needed unless CI times become a problem.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/test.yml around lines 27 - 38, No code changes are
required; the shifting-mosaic C++ build and ctest steps are correctly
configured, and the documented fixture runtime tradeoff is explicitly accepted.

128-135: 🧹 Nitpick | 🔵 Trivial

The heap-limit regression guard is opt-in and never runs in this CI workflow.

e2e/shiftingMosaicHeapLimit.slow.test.ts (the test that specifically guards against the RuntimeError: Aborted() regression on the threads-mem64 variant) is skipped unless SM_SLOW_E2E is set, and this workflow never sets it. The threads+mem64 build is only compile-checked here (per the comment at lines 118-120), so the actual runtime regression this test protects against currently has no automated coverage. Consider adding a scheduled/nightly job that runs with SM_SLOW_E2E=1.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/test.yml around lines 128 - 135, Add scheduled/nightly CI
coverage for the heap-limit regression by introducing a job or workflow step
that sets SM_SLOW_E2E=1 and runs the relevant slow end-to-end test, including
the threads-mem64 runtime path. Keep the existing compile and standard test
steps unchanged, and ensure the scheduled job executes
e2e/shiftingMosaicHeapLimit.slow.test.ts rather than leaving it opt-in only.
e2e/rollingBlocksSolver.placement.test.ts (1)

3-31: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Duplicate, unused setupSolvedPuzzle helper.

This helper is byte-identical to the one in e2e/rollingBlocksSolver.solution.test.ts but is never called by the single test in this file. Extract to a shared test-helper module, or drop it here.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@e2e/rollingBlocksSolver.placement.test.ts` around lines 3 - 31, Remove the
unused setupSolvedPuzzle helper from this placement test, since it duplicates
the helper in rollingBlocksSolver.solution.test.ts and is not called here.
e2e/rollingBlocksSolver.solution.test.ts (1)

3-31: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Duplicate helper — candidate for extraction.

Same setupSolvedPuzzle implementation is duplicated verbatim in e2e/rollingBlocksSolver.placement.test.ts (where it's unused). Move to a shared helper module imported by both.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@e2e/rollingBlocksSolver.solution.test.ts` around lines 3 - 31, Extract the
duplicated setupSolvedPuzzle helper into a shared e2e helper module, then import
and use it from both rollingBlocksSolver.solution.test.ts and
rollingBlocksSolver.placement.test.ts. Preserve the existing setup behavior and
remove the unused local duplicate from the placement test.
e2e/shiftingMosaicSolver.config.test.ts (1)

1-59: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Duplicate test fixtures/helpers, one unused.

Same block as e2e/shiftingMosaicSolver.blocks.test.ts lines 1-59 — see that comment. paintWithGap is unused here too.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@e2e/shiftingMosaicSolver.config.test.ts` around lines 1 - 59, Remove the
duplicated SAMPLE_CONFIG, Cell type, and cellAt, cellCenter, paintShape, and
unused paintWithGap helpers from this test file; reuse the corresponding shared
fixtures and helpers from shiftingMosaicSolver.blocks.test.ts instead, updating
imports or references as needed.
e2e/shiftingMosaicSolver.blocks.test.ts (1)

1-59: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Duplicate test fixtures/helpers, one unused.

SAMPLE_CONFIG, Cell, cellAt, cellCenter, paintShape, and paintWithGap are byte-identical to the same block in e2e/shiftingMosaicSolver.config.test.ts. Additionally, paintWithGap is not called anywhere in either file. Recommend extracting the shared pieces to a common e2e helper module and dropping paintWithGap if it's genuinely unused, or adding a test that exercises it (e.g., verifying gap-drag placement behavior) if it was meant to be used.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@e2e/shiftingMosaicSolver.blocks.test.ts` around lines 1 - 59, Extract the
duplicated SAMPLE_CONFIG, Cell type, cellAt, cellCenter, and paintShape
fixtures/helpers from the shifting mosaic tests into a shared e2e helper module,
then import and reuse them in both shiftingMosaicSolver.blocks.test.ts and
shiftingMosaicSolver.config.test.ts. Remove paintWithGap if it is not needed;
only retain it by adding a test that exercises its gap-drag behavior.
src/pages/shifting-mosaic-solver/shiftingMosaicSolver.css (1)

591-649: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Solution-view colors are hardcoded literals rather than theme tokens.

#c84ad8, #1f9d57, #b88600 and #ffffff bypass the --md-sys-color-* system used elsewhere in this file, so these markers won't adapt to a dark/high-contrast theme and their contrast against --md-sys-color-surface-* is unverified. Consider mapping them to theme tokens (or defining them once as custom properties with per-scheme overrides).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/pages/shifting-mosaic-solver/shiftingMosaicSolver.css` around lines 591 -
649, Replace the hardcoded solution-view marker colors in .sm-moving-label,
.sm-dest-label, .sm-moving, .sm-dest::after, and .sm-goal-edge-* with existing
--md-sys-color-* theme tokens, or define shared custom properties with
light/dark/high-contrast overrides. Preserve the current visual roles while
ensuring marker contrast adapts to the surrounding surface colors.
e2e/shiftingMosaicWasmVariants.test.ts (1)

209-216: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Substring assertions on emscripten glue are brittle.

maximum:131072n, address:"i64", shared:true assume the exact minified spacing/quoting emscripten currently emits. A toolchain bump that emits maximum: 131072n or single quotes breaks this test without any real regression. Tolerant regexes keep the same guarantee.

♻️ Suggested loosening
-    expect(glue).toContain("maximum:131072n");
-    expect(glue).toContain('address:"i64"');
-    expect(glue).toContain("shared:true");
+    expect(glue).toMatch(/maximum:\s*131072n/);
+    expect(glue).toMatch(/address:\s*['"]i64['"]/);
+    expect(glue).toMatch(/shared:\s*true/);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@e2e/shiftingMosaicWasmVariants.test.ts` around lines 209 - 216, Update the
glue assertions in the test around the fetched astar.threads.mem64.mjs content
to use whitespace- and quote-tolerant regular expressions instead of exact
substring matches for maximum, address, and shared. Preserve validation of the
same values and flags while allowing equivalent Emscripten formatting changes.
test/shiftingMosaicSolver/wasm.test.ts (1)

183-224: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

No internal deadline: a test-level timeout leaves every arm worker alive.

racePortfolio only settles on done/error/exit. If an arm neither finishes nor exits, the Bun per-test timeout fires while the promise is still pending, so finish() never runs and the nine workers (each holding a wasm heap) survive into the next fixture — the exact leak the exit handler at Line 220 was added to avoid. A watchdog slightly under the test timeout closes the remaining path.

🛡️ Suggested guard
     return new Promise(resolve => {
       const workers = arms.map(() => new ArmWorker(workerPath));
       let pending = workers.length;
       let settled = false;
+      let watchdog: ReturnType<typeof setTimeout>;
       const finish = (turns: WasmTurn[]) => {
         if (settled) return;
         settled = true;
+        clearTimeout(watchdog);
         for (const w of workers) void w.terminate();
         resolve(turns);
       };
+      // Slightly under the enclosing test timeout so the arms are always
+      // torn down by us rather than abandoned by the runner.
+      watchdog = setTimeout(() => finish([]), budgetMs * 2);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/shiftingMosaicSolver/wasm.test.ts` around lines 183 - 224, Add an
internal watchdog within racePortfolio that fires slightly before the test-level
timeout, calls finish([]) to terminate all still-running arm workers, and
settles the promise. Ensure the watchdog is cleared when finish completes, while
preserving the existing done/error/exit retirement behavior.
src/pages/shifting-mosaic-solver/a-star/CMakeLists.txt (1)

54-62: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Hardcoded CLion paths and pinned LLVM 23 make linting effectively local-only.

The hint list pins one machine's CLion install, and SM_CLANG_TIDY_PROFILE_LLVM 23 (a dev snapshot no distro ships) means every CI host takes the skip branch — the "tree cannot drift out of compliance" goal isn't enforced anywhere automated. Consider letting users point at their own binary, e.g. honoring a cache/env override before the hints.

♻️ Optional override hook
     find_program(SM_CLANG_TIDY_EXE
         NAMES clang-tidy
-        HINTS "$ENV{ProgramFiles}/JetBrains/CLion 2024.1.4/bin/clang/win/x64/bin"
+        HINTS "$ENV{SM_CLANG_TIDY_DIR}"
+              "$ENV{ProgramFiles}/JetBrains/CLion 2024.1.4/bin/clang/win/x64/bin"
               "$ENV{ProgramFiles}/JetBrains/CLion/bin/clang/win/x64/bin"
     )
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/pages/shifting-mosaic-solver/a-star/CMakeLists.txt` around lines 54 - 62,
Update the SM_CLANG_TIDY_EXE discovery in CMakeLists.txt to honor a
user-provided cache or environment override for the clang-tidy executable before
applying CLion-specific HINTS, and avoid making lint enforcement depend solely
on the pinned LLVM 23 profile. Preserve the existing fallback discovery while
allowing CI and other hosts to supply their own compatible clang-tidy binary.
e2e/rollingBlocksSolver.tools.test.ts (1)

922-922: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Remove the unused setupSolvedPuzzle helper
e2e/rollingBlocksSolver.tools.test.ts:3 no longer references it.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@e2e/rollingBlocksSolver.tools.test.ts` at line 922, Remove the unused
setupSolvedPuzzle helper from rollingBlocksSolver.tools.test.ts, including its
definition and any associated import or setup code, since no references remain.
test/shiftingMosaicSolver/board.pointer.test.ts (1)

1-31: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Extract shared Board test harness instead of triplicating it.

The editorMock shape, DOM stub, and Board construction are copy-pasted identically across three files. A shared helper would reduce upkeep when the Board/editor interface changes.

  • test/shiftingMosaicSolver/board.pointer.test.ts#L1-L31: import a shared createBoardTestContext() helper instead of inlining the mock/Board setup.
  • test/shiftingMosaicSolver/board.render.test.ts#L1-L31: same — replace the duplicated beforeEach block with the shared helper.
  • test/shiftingMosaicSolver/board.state.test.ts#L1-L31: same — replace the duplicated beforeEach block with the shared helper.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/shiftingMosaicSolver/board.pointer.test.ts` around lines 1 - 31, Extract
the duplicated editor mock, DOM setup, and Board initialization into a shared
createBoardTestContext() helper, then update the beforeEach setup in
test/shiftingMosaicSolver/board.pointer.test.ts:1-31,
test/shiftingMosaicSolver/board.render.test.ts:1-31, and
test/shiftingMosaicSolver/board.state.test.ts:1-31 to use it while preserving
each test’s board and internal context access.
src/pages/shifting-mosaic-solver/a-star/AStar.h (1)

16-16: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚖️ Poor tradeoff

Class AStar exceeds the 20-field cohesion threshold (22 fields, per SonarCloud).

Consider grouping the board-analysis caches (blockValidAnchorMask_, initialGoalPathCells_, blockSafeAnchorDist_, goalAnchorBfsDist_) into a small nested struct to reduce the member count and improve cohesion. Given AStar.cpp's own notes that restructuring this file has measurably regressed performance before, treat this as low priority and re-measure if attempted.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/pages/shifting-mosaic-solver/a-star/AStar.h` at line 16, Optionally
reduce AStar’s 22-member count by grouping blockValidAnchorMask_,
initialGoalPathCells_, blockSafeAnchorDist_, and goalAnchorBfsDist_ into a small
nested cache struct within AStar, updating all AStar.cpp accesses accordingly.
Treat this as low priority, preserve behavior and performance, and re-measure
before keeping the change.

Source: Linters/SAST tools

src/pages/shifting-mosaic-solver/a-star/AStarOptimizer.cpp (1)

239-266: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

tryReorderMerge's improvement check compares against the wrong step metric (currently safe, but fragile).

baseSteps is computed via computeRuns(turns).size() — which splits on direction changes within a block — while the accept condition compares it against countSteps(cand), which only splits on block-ID changes (the coarser, actual player-step metric per this function's own doc comment: "the player-step count drops by one"). runs.size() >= countSteps(turns) always, so this check is a looser bound than intended.

Right now this can't produce a wrong accept: reaching this branch requires an intervening different-block run between ri/rj (proven by the prevRunOfBlock tracking), which guarantees the merge always yields exactly countSteps(original) - 1. But the check doesn't actually verify that invariant — comparing against countSteps(turns) directly would make the code self-validating against future changes to the run-tracking logic instead of relying on an implicit proof.

♻️ Proposed fix to compare against the actual pre-merge step count
 bool AStar::tryReorderMerge(std::vector<Turn> &turns) const {
   const auto runs = computeRuns(turns);
-  const size_t baseSteps = runs.size();
+  const size_t baseSteps = countSteps(turns);
   std::vector prevRunOfBlock(shapes_.size(), -1);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/pages/shifting-mosaic-solver/a-star/AStarOptimizer.cpp` around lines 239
- 266, Update tryReorderMerge to compute the baseline using countSteps(turns)
rather than runs.size(), then compare both runPulledEarlier and runPushedLater
candidates against that pre-merge player-step count while retaining replaySolves
validation and the existing successful replacement behavior.
src/pages/shifting-mosaic-solver/a-star/test/DragSolverTestOracle.h (1)

205-220: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

alone is dead — confirm the goal-target check is intentionally bbox-only.

alone is built and assigned but never read; only the bbox test below decides the target. If the leftover was meant to feed an oracleCanPlace(p, 0, t, alone) check, that check is missing; otherwise drop the two lines.

♻️ Remove the unused vector
     const Position t = {.x = static_cast<int8_t>(rng.draw(xDist)),
                         .y = static_cast<int8_t>(rng.draw(yDist))};
-    std::vector<Position> alone(p.anchors.size(), {.x = 127, .y = 127});
-    alone[0] = t;
     // Target just needs the goal's bbox in-grid.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/pages/shifting-mosaic-solver/a-star/test/DragSolverTestOracle.h` around
lines 205 - 220, Remove the unused alone vector and its assignment in the
goal-target generation loop around p.goalAnchor. Keep the existing
bounding-box-only validity check and target selection behavior unchanged.
src/pages/shifting-mosaic-solver/a-star/test/dragsolver_search_test.cpp (1)

159-173: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Wall-clock budget with a hard success assertion can flake on loaded CI.

ASSERT_FALSE(turns.empty()) for every oracle-solvable board is bounded by a 10s wall-clock budget, exactly the machine-speed dependence you avoided in JamRoundShapingStaysValidAndDefaultsUnchanged by switching to a node cap (Lines 116-120). Bounding these by nodes instead (searchHierarchical(0, N)) would make the pass/fail deterministic. Same applies to JamGuideAndJitterStayValidAndComplete.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/pages/shifting-mosaic-solver/a-star/test/dragsolver_search_test.cpp`
around lines 159 - 173, The solvable-board tests use a wall-clock search budget,
making success assertions machine-speed dependent. In
HierarchicalSolvesSolvableBoards and JamGuideAndJitterStayValidAndComplete,
replace the time-based searchHierarchical limit with a sufficiently large node
cap via the node-budget argument (for example, searchHierarchical(0, N)),
preserving the existing solvability and replay-validity assertions.
src/pages/shifting-mosaic-solver/a-star/BenchFixture.cpp (1)

138-146: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

writeTurnsFile silently swallows write failures.

Unlike writeFixtureAt, this never checks is_open()/stream state, so an unwritable path loses the plan with no signal — awkward when the turns file is the artifact a fuzz repro depends on.

♻️ Return success like `writeFixtureAt`
-void writeTurnsFile(const std::vector<Turn> &turns, const std::string &path) {
+bool writeTurnsFile(const std::vector<Turn> &turns, const std::string &path) {
   json tj = json::array();
   for (const auto &[blockId, direction] : turns)
     tj.push_back(
         {{"blockId", blockId},
          {"direction", static_cast<int>(std::to_underlying(direction))}});
   std::ofstream f(path);
+  if (!f.is_open())
+    return false;
   f << tj.dump();
+  return f.good();
 }

Header signature in BenchFixture.h needs the matching update.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/pages/shifting-mosaic-solver/a-star/BenchFixture.cpp` around lines 138 -
146, Update writeTurnsFile to return a success status like writeFixtureAt:
verify the output stream opened successfully and remains valid after writing,
returning false on failure and true only when the turns JSON is written
successfully. Update the matching declaration in BenchFixture.h and adjust all
call sites to handle the returned status.
src/pages/shifting-mosaic-solver/a-star/BenchFixture.h (1)

6-8: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Make BenchFixture.h self-contained. Add <cstddef> and change the size_t return types to std::size_t instead of relying on transitive/global declarations.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/pages/shifting-mosaic-solver/a-star/BenchFixture.h` around lines 6 - 8,
Add <cstddef> to BenchFixture.h and update all size_t return types in the
BenchFixture declarations to std::size_t, ensuring the header is self-contained
and no longer depends on transitive or global declarations.
src/pages/shifting-mosaic-solver/a-star/BenchOptions.cpp (2)

57-57: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Drop the hard-coded array size.

std::array<FlagSpec, 41> must be updated by hand whenever a flag is added/removed; a mismatch is a compile error at best, and it is pure bookkeeping. constexpr FlagSpec FLAGS[]{...} (or CTAD via std::to_array) deduces it.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/pages/shifting-mosaic-solver/a-star/BenchOptions.cpp` at line 57, Update
the FLAGS declaration in BenchOptions to use a size-deduced array instead of the
hard-coded std::array<FlagSpec, 41>, such as a deduced constexpr array or
std::to_array, while preserving the existing flag entries and constexpr
behavior.

209-226: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider bailing out as soon as the cursor records an error.

cur.error() is only inspected after the whole argv is consumed, so a malformed value keeps applying later flags (and value() returning "" writes empty paths into opt). Behaviour is still correct because parseBenchOptions returns false and main.cpp aborts before use, but an early if (!cur.error().empty()) break; inside the loop makes the contract obvious.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/pages/shifting-mosaic-solver/a-star/BenchOptions.cpp` around lines 209 -
226, Update parseBenchOptions to check cur.error() immediately after each
spec->apply(opt, cur) call and stop processing further arguments when an error
is recorded. Keep the existing final error reporting and false return unchanged,
while preventing later flags from modifying opt after a malformed value.
src/pages/shifting-mosaic-solver/a-star/DragSolverBeam.cpp (1)

130-137: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Verify the score bit-packing has room for the jam term.

jamChild is capped at 1u << 19 and shifted left 14, so the top field occupies bits 14–33 of a uint64_t — fine — but h << 10 only leaves bits 10–13 for a value that is documented as 0/1, and the jitter is masked to 10 bits. Worth a brief comment recording the field widths so a future change to h (or a wider jitter) doesn't silently collide.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/pages/shifting-mosaic-solver/a-star/DragSolverBeam.cpp` around lines 130
- 137, Add a concise comment immediately above the score construction
documenting the bit layout: jamChild uses bits 14–33, h uses bits 10–13, and
beamJitter uses the low 10 bits. Keep the existing packing unchanged and note
the expected h range so future width changes are checked for overlap.
src/pages/shifting-mosaic-solver/a-star/DragSolverAssembly.cpp (2)

53-66: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Name the 40 reachability threshold.

Every other tuned constant in this file is a named constexpr with a rationale comment (UNFREEZE_K, ENDGAME_K, MAX_JOINT, MAX_BACKTRACKS); this one is a bare literal that silently decides "big enough free region ⇒ approachable".

♻️ Suggested change
+  // A flood region this large is effectively open space; treat the piece as
+  // approachable without checking its current anchor.
+  constexpr size_t OPEN_REGION_CELLS = 40;
   if (const auto &reached = grid_.floodFill(piece, slotPos(piece));
-      reached.size() >= 40)
+      reached.size() >= OPEN_REGION_CELLS)
     return true;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/pages/shifting-mosaic-solver/a-star/DragSolverAssembly.cpp` around lines
53 - 66, Name the flood-fill reachability threshold used by
DragSolver::approachable as a file-level constexpr, replacing the bare 40
literal. Add a concise rationale comment documenting that it represents the
minimum free-region size considered large enough for approachability, and use
the named constant in the reached.size() comparison.

206-218: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚖️ Poor tradeoff

Solver progress goes to raw std::cout.

This code also runs inside the wasm build where the arms race concurrently; unconditional std::cout from every arm interleaves and cannot be silenced by callers. A verbosity flag on Config (or a small logging hook alongside onProgress) would keep the diagnostics without forcing them on embedders.

Also applies to: 274-279, 440-466

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/pages/shifting-mosaic-solver/a-star/DragSolverAssembly.cpp` around lines
206 - 218, The solver’s progress messages in the joint-endgame and other cited
paths write unconditionally to std::cout, preventing callers from suppressing or
safely collecting output. Route these diagnostics through a Config verbosity
flag or logging/progress hook alongside onProgress, and guard or dispatch all
affected messages—including the locations around jointEndgame and lines
440-466—through that mechanism instead of direct std::cout.
src/pages/shifting-mosaic-solver/a-star/DragSolver.cpp (1)

211-252: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low value

Consider avoiding std::function for the hot packing DFS.

dfs is called up to PACK_NODE_CAP (5M) times and every recursive step pays an indirect call through std::function plus the type-erasure barrier that blocks inlining. A small local recursive struct (or a template <typename Self> lambda / explicit worklist) keeps the same shape without the indirection.

♻️ Sketch
-  std::function<bool(size_t)> dfs = [&](const size_t fromRank) -> bool {
+  const auto dfs = [&](this const auto &self, const size_t fromRank) -> bool {

(deducing-this recursive lambda if the toolchain is C++23; otherwise a local struct Dfs { ... bool run(size_t); }.)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/pages/shifting-mosaic-solver/a-star/DragSolver.cpp` around lines 211 -
252, Replace the std::function-based recursive dfs in the packing search with a
directly callable recursive implementation, such as a self-passing templated
lambda or local recursive struct, while preserving the existing DFS behavior,
state updates, and PACK_NODE_CAP handling. Ensure recursive calls avoid
std::function type erasure and indirect dispatch.

Source: Linters/SAST tools

src/pages/shifting-mosaic-solver/a-star/DragSolverArms.cpp (1)

176-178: 🩺 Stability & Availability | 🔵 Trivial | 💤 Low value

Guard the unsigned subtraction in the remaining-node computation.

maxNodes - stats_.nodesExpanded wraps if expansions ever overshoot maxNodes (rounds are capped, not clamped), and the wrapped value makes std::min a no-op — the opposite of the intent. The loop guard currently makes this unreachable, but a saturating form removes the dependency.

♻️ Proposed change
-  if (maxNodes != 0)
-    cap = std::min<uint32_t>(cap, maxNodes - stats_.nodesExpanded);
+  if (maxNodes != 0)
+    cap = std::min<uint32_t>(cap, maxNodes > stats_.nodesExpanded
+                                      ? maxNodes - stats_.nodesExpanded
+                                      : 0);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/pages/shifting-mosaic-solver/a-star/DragSolverArms.cpp` around lines 176
- 178, Update the remaining-node calculation near the return cap logic to
saturate maxNodes - stats_.nodesExpanded at zero when expansions meet or exceed
maxNodes, then apply std::min with cap only to the non-underflowing remainder.
Preserve the existing unlimited behavior when maxNodes is zero.
src/pages/shifting-mosaic-solver/a-star/DragSolverSearch.cpp (3)

40-49: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

operator== on DragHeapEntry appears dead code.

The heap only needs ordering (via std::greater<>, which is satisfiable from operator<=>); operator== isn't otherwise invoked in this file. SonarCloud's "unused member function" flag looks correct here.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/pages/shifting-mosaic-solver/a-star/DragSolverSearch.cpp` around lines 40
- 49, Remove the unused operator== overload from DragHeapEntry, leaving
operator<=> unchanged so std::greater<> continues to provide the heap ordering.

Source: Linters/SAST tools


300-318: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Reference-identity shortcut for mask comparison — fragile if cutSuffixRows_ is ever resized mid-search.

if (&maskN == &exp.maskP)
    return 1 + exp.baseCount;

This relies on cutSuffixRows_ elements never moving between the time exp.maskP was bound and this call. Given it's a member fixed up-front per computeDisplacementFields, this is presumably safe today, but it's a non-obvious invariant for future maintainers touching cutSuffixRows_'s lifetime.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/pages/shifting-mosaic-solver/a-star/DragSolverSearch.cpp` around lines
300 - 318, Replace the address-based mask comparison in the displacement-count
logic with a value-based comparison between maskN and exp.maskP, preserving the
existing 1 + exp.baseCount result when the masks are equivalent. Update the code
in the method containing the DragExpansion parameter without changing the
surrounding counting behavior.

20-51: 📐 Maintainability & Code Quality | 🔵 Trivial

Align the analyzer with C++23
src/pages/shifting-mosaic-solver/a-star/CMakeLists.txt already sets CMAKE_CXX_STANDARD 23, and this codebase deliberately uses <=>, std::ranges, and .contains(). The static-analysis job needs the same standard flag instead of reporting these as source defects.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/pages/shifting-mosaic-solver/a-star/DragSolverSearch.cpp` around lines 20
- 51, Update the analyzer configuration in the a-star CMake setup to use C++23,
matching the existing CMAKE_CXX_STANDARD 23 setting. Ensure the static-analysis
invocation receives the equivalent C++23 standard flag so valid usage of
DragHeapEntry’s operator<=> and other C++23 APIs is analyzed correctly.
test/shiftingMosaicSolver/aStar.search.test.ts (1)

216-216: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Prefer toHaveLength for length assertions.

SonarCloud suggests expect(turns).toHaveLength(1) over expect(turns.length).toBe(1) for clearer failure reporting.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/shiftingMosaicSolver/aStar.search.test.ts` at line 216, Update the
assertion in the relevant A* search test to use toHaveLength directly on turns,
preserving the expected length of 1.

Source: Linters/SAST tools

src/pages/shifting-mosaic-solver/aStar.ts (4)

106-110: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Move field declarations above the constructor.

movableBlockIndices, unsolvableAtStart, and goalAnchorBfsDist are declared with initializers after the constructor that populates them (lines 75-92). This is safe (field initializers run before the constructor body), but it reads as if the constructor's work gets reset — worth moving these declarations above the constructor for clarity.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/pages/shifting-mosaic-solver/aStar.ts` around lines 106 - 110, Move the
field declarations movableBlockIndices, unsolvableAtStart, and goalAnchorBfsDist
above the constructor in the solver class, preserving their existing
initializers and types. Do not change the constructor’s initialization logic or
field behavior.

152-259: 📐 Maintainability & Code Quality | 🔵 Trivial | 🏗️ Heavy lift

Reduce cognitive complexity of search.

SonarCloud flags this function at cognitive complexity 39 (limit 15). Logic itself checks out (dedup via closedSet/gScore, timeout budget, successor generation), but consider extracting the successor-expansion inner loop (Lines 220-254) and/or the timeout/goal checks into named helpers to aid readability and future maintenance.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/pages/shifting-mosaic-solver/aStar.ts` around lines 152 - 259, Reduce
cognitive complexity in the AStar search method by extracting the
successor-generation logic from the movableBlockIndices/direction loops into a
named helper, and extract additional timeout or goal-check handling if needed to
meet the configured threshold. Preserve the existing closedSet/gScore
deduplication, cameFrom updates, heap insertion, timeout behavior, deadlock
pruning, and goal-path reconstruction exactly.

Source: Linters/SAST tools


377-429: 📐 Maintainability & Code Quality | 🔵 Trivial | 🏗️ Heavy lift

Reduce cognitive complexity of computeMovableSet.

SonarCloud flags this function at cognitive complexity 37 (limit 15). The fixpoint logic is correct, but consider extracting the per-block "can this direction be freed" check (Lines 405-414) into a helper method to shrink nesting depth.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/pages/shifting-mosaic-solver/aStar.ts` around lines 377 - 429, Reduce
cognitive complexity in computeMovableSet by extracting the per-direction
occupancy check into a focused helper method that determines whether a block can
move in a given direction, using the existing anchors, shape, occupancy, and
movable set. Keep the fixpoint iteration and its current behavior unchanged, and
have computeMovableSet delegate the directional checks to the helper.

Source: Linters/SAST tools


436-465: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Reduce cognitive complexity of isDeadlocked.

SonarCloud flags cognitive complexity 21 (limit 15). The "does block i occupy a goal-final cell" check is duplicated between the hasBlocker scan and the final loop — extracting it into a helper both fixes the duplication and reduces nesting.

♻️ Proposed refactor
+  private blockOccupiesGoalFinalCells(shapeIndex: number, anchor: Position): boolean {
+    for (const cell of this.shapes[shapeIndex]!) {
+      const enc = (anchor.x + cell.x) * this.gridHeight + (anchor.y + cell.y);
+      if (this.goalBlockFinalCells.has(enc)) return true;
+    }
+    return false;
+  }
+
   private isDeadlocked(anchors: Position[]): boolean {
-    const stride = this.gridHeight;
-    let hasBlocker = false;
-    for (let i = 0; i < anchors.length; i++) {
-      if (i === this.goalIndex) continue;
-      const a = anchors[i]!;
-      for (const cell of this.shapes[i]!) {
-        const enc = (a.x + cell.x) * stride + (a.y + cell.y);
-        if (this.goalBlockFinalCells.has(enc)) {
-          hasBlocker = true;
-          break;
-        }
-      }
-      if (hasBlocker) break;
-    }
-    if (!hasBlocker) return false;
+    const hasBlocker = anchors.some(
+      (a, i) => i !== this.goalIndex && this.blockOccupiesGoalFinalCells(i, a),
+    );
+    if (!hasBlocker) return false;

     const movable = this.computeMovableSet(anchors);

-    for (let i = 0; i < anchors.length; i++) {
-      if (i === this.goalIndex) continue;
-      if (movable.has(i)) continue;
-      const a = anchors[i]!;
-      for (const cell of this.shapes[i]!) {
-        const enc = (a.x + cell.x) * stride + (a.y + cell.y);
-        if (this.goalBlockFinalCells.has(enc)) return true;
-      }
-    }
-    return false;
+    return anchors.some(
+      (a, i) =>
+        i !== this.goalIndex &&
+        !movable.has(i) &&
+        this.blockOccupiesGoalFinalCells(i, a),
+    );
   }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/pages/shifting-mosaic-solver/aStar.ts` around lines 436 - 465, Reduce
cognitive complexity in isDeadlocked by extracting the duplicated check for
whether a block occupies any cell in goalBlockFinalCells into a private helper.
Reuse that helper in both the initial hasBlocker scan and the final
immovable-block scan, preserving the existing goalIndex exclusions and deadlock
behavior.

Source: Linters/SAST tools

test/shiftingMosaicSolver/aStar.collision.test.ts (2)

55-81: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Empty test asserts nothing.

This test builds shapes, comments through several false starts, then only does void shapeA; void shapeB; void shapeBReal; — no blocksCollide call, no expect(...). It always passes without testing anything, which is misleading since the next test (Lines 83-112) already covers the intended bbox-overlap-but-no-cell-collision scenario. Recommend removing this test.

♻️ Proposed fix
-    test("two L-shapes whose bboxes overlap but whose cells do not are not colliding", () => {
-      // Shape A: L pointing top-right
-      //   X X
-      //   X .
-      const shapeA: Position[] = [
-        { x: 0, y: 0 },
-        { x: 1, y: 0 },
-        { x: 0, y: 1 },
-      ];
-      // Shape B: just bottom-right cell
-      //   . X
-      const shapeB: Position[] = [{ x: 1, y: 1 }];
-      // We use a 2x2 bbox shape B by also including {x:0,y:0} for bbox
-      // ... but we want a shape that fits the (1,1) corner exactly.
-      // Easier: use a real bbox-overlapping case where the cell at (1,1) is empty.
-      const shapeBReal: Position[] = [
-        { x: 1, y: 1 },
-        { x: 0, y: 1 },
-      ];
-      // shapeBReal's bbox is 2x2 (because we have x∈{0,1}, y∈{0,1}... wait y=1 only)
-      // Actually bbox is 2 wide, 1 tall (since min(y)=1, but bbox calc uses max(y)+1 = 2)
-      // Hmm bbox calc is just max+1 — let me redo with normalized shape.
-      // Skip this — use a more direct test below instead.
-      void shapeA;
-      void shapeB;
-      void shapeBReal;
-    });
-
     test("L-shape and single cell — bbox overlaps, cells overlap", () => {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/shiftingMosaicSolver/aStar.collision.test.ts` around lines 55 - 81,
Remove the empty test case that defines shapeA, shapeB, and shapeBReal but
performs no collision check or assertion. Keep the subsequent test covering bbox
overlap without cell collision unchanged.

12-51: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Duplicated test helpers across two files. makeAStar and applyTurns are copy-pasted verbatim between the two test files; extracting them into a shared test-utils module (e.g. test/shiftingMosaicSolver/testHelpers.ts) avoids drift as more AStar unit-test files are added to this suite.

  • test/shiftingMosaicSolver/aStar.collision.test.ts#L12-L51: import makeAStar/applyTurns from a new shared helper module instead of defining them locally.
  • test/shiftingMosaicSolver/aStar.search.test.ts#L12-L51: same — import from the shared helper module instead of the duplicate local definitions.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/shiftingMosaicSolver/aStar.collision.test.ts` around lines 12 - 51,
Extract the duplicated makeAStar and applyTurns helpers into a shared test
utility module. In test/shiftingMosaicSolver/aStar.collision.test.ts lines 12-51
and test/shiftingMosaicSolver/aStar.search.test.ts lines 12-51, remove the local
definitions and import both helpers from the shared module, preserving their
existing behavior and required dependencies.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 302fa3b5-73de-4b7d-b3b1-287957c48865

📥 Commits

Reviewing files that changed from the base of the PR and between 068e4fb and c306249.

📒 Files selected for processing (182)
  • .claude/settings.local.json
  • .github/workflows/test.yml
  • .gitignore
  • e2e/phasicDialSolver.solvable.test.ts
  • e2e/phasicDialSolver.states.test.ts
  • e2e/rollingBlocksSolver.placement.test.ts
  • e2e/rollingBlocksSolver.solution.test.ts
  • e2e/rollingBlocksSolver.tools.test.ts
  • e2e/shiftingMosaicHeapLimit.slow.test.ts
  • e2e/shiftingMosaicSolver.blocks.test.ts
  • e2e/shiftingMosaicSolver.config.test.ts
  • e2e/shiftingMosaicSolver.editing.test.ts
  • e2e/shiftingMosaicSolver.solving.test.ts
  • e2e/shiftingMosaicSolver.test.ts
  • e2e/shiftingMosaicWasmVariants.test.ts
  • package.json
  • playwright.config.ts
  • src/common/coi-serviceworker.js
  • src/common/coiRegister.ts
  • src/common/common.css
  • src/pages/phasic-dial-solver/phasicDialSolver.ts
  • src/pages/rolling-blocks-solver/a-star/AStar.cpp
  • src/pages/rolling-blocks-solver/a-star/AStar.h
  • src/pages/rolling-blocks-solver/a-star/AStarGoals.cpp
  • src/pages/rolling-blocks-solver/a-star/CMakeLists.txt
  • src/pages/rolling-blocks-solver/a-star/GoalCluster.h
  • src/pages/rolling-blocks-solver/a-star/Node.h
  • src/pages/rolling-blocks-solver/a-star/NodeKey.h
  • src/pages/rolling-blocks-solver/a-star/test/CMakeLists.txt
  • src/pages/rolling-blocks-solver/a-star/wasm_bindings.cpp
  • src/pages/rolling-blocks-solver/aStar.ts
  • src/pages/rolling-blocks-solver/bfs.ts
  • src/pages/rolling-blocks-solver/board.ts
  • src/pages/rolling-blocks-solver/goalClusters.ts
  • src/pages/rolling-blocks-solver/rollingBlocksSolver.ts
  • src/pages/shifting-mosaic-solver/a-star/.clang-tidy
  • src/pages/shifting-mosaic-solver/a-star/AStar.cpp
  • src/pages/shifting-mosaic-solver/a-star/AStar.h
  • src/pages/shifting-mosaic-solver/a-star/AStarHeuristics.cpp
  • src/pages/shifting-mosaic-solver/a-star/AStarOptimizer.cpp
  • src/pages/shifting-mosaic-solver/a-star/AStarSearch.cpp
  • src/pages/shifting-mosaic-solver/a-star/BenchCommands.cpp
  • src/pages/shifting-mosaic-solver/a-star/BenchCommands.h
  • src/pages/shifting-mosaic-solver/a-star/BenchEngines.cpp
  • src/pages/shifting-mosaic-solver/a-star/BenchEngines.h
  • src/pages/shifting-mosaic-solver/a-star/BenchFixture.cpp
  • src/pages/shifting-mosaic-solver/a-star/BenchFixture.h
  • src/pages/shifting-mosaic-solver/a-star/BenchOptions.cpp
  • src/pages/shifting-mosaic-solver/a-star/BenchOptions.h
  • src/pages/shifting-mosaic-solver/a-star/BitGrid.h
  • src/pages/shifting-mosaic-solver/a-star/CMakeLists.txt
  • src/pages/shifting-mosaic-solver/a-star/ClangdCompat.h
  • src/pages/shifting-mosaic-solver/a-star/DragSolver.cpp
  • src/pages/shifting-mosaic-solver/a-star/DragSolver.h
  • src/pages/shifting-mosaic-solver/a-star/DragSolverArms.cpp
  • src/pages/shifting-mosaic-solver/a-star/DragSolverAssembly.cpp
  • src/pages/shifting-mosaic-solver/a-star/DragSolverBeam.cpp
  • src/pages/shifting-mosaic-solver/a-star/DragSolverConfig.h
  • src/pages/shifting-mosaic-solver/a-star/DragSolverExpand.cpp
  • src/pages/shifting-mosaic-solver/a-star/DragSolverHeuristics.cpp
  • src/pages/shifting-mosaic-solver/a-star/DragSolverSearch.cpp
  • src/pages/shifting-mosaic-solver/a-star/MemoryProbe.h
  • src/pages/shifting-mosaic-solver/a-star/Node.h
  • src/pages/shifting-mosaic-solver/a-star/NodeKey.h
  • src/pages/shifting-mosaic-solver/a-star/PARALLEL.md
  • src/pages/shifting-mosaic-solver/a-star/ParallelCascade.h
  • src/pages/shifting-mosaic-solver/a-star/SeededRng.h
  • src/pages/shifting-mosaic-solver/a-star/SolverClock.cpp
  • src/pages/shifting-mosaic-solver/a-star/SolverClock.h
  • src/pages/shifting-mosaic-solver/a-star/StateTable.h
  • src/pages/shifting-mosaic-solver/a-star/Types.h
  • src/pages/shifting-mosaic-solver/a-star/bench/HARD-BOARDS.md
  • src/pages/shifting-mosaic-solver/a-star/bench/after-cascade.json
  • src/pages/shifting-mosaic-solver/a-star/bench/after-luby.json
  • src/pages/shifting-mosaic-solver/a-star/bench/baseline-unit.json
  • src/pages/shifting-mosaic-solver/a-star/bench/before-cascade.json
  • src/pages/shifting-mosaic-solver/a-star/bench/drag-settled-w2.json
  • src/pages/shifting-mosaic-solver/a-star/bench/fuzz-1022-hard.json
  • src/pages/shifting-mosaic-solver/a-star/bench/fuzz-10276-hard.json
  • src/pages/shifting-mosaic-solver/a-star/bench/fuzz-11386-hard.json
  • src/pages/shifting-mosaic-solver/a-star/bench/fuzz-12366-hard.json
  • src/pages/shifting-mosaic-solver/a-star/bench/fuzz-23444-hard.json
  • src/pages/shifting-mosaic-solver/a-star/bench/fuzz-29534-hard.json
  • src/pages/shifting-mosaic-solver/a-star/bench/fuzz-3058-hard.json
  • src/pages/shifting-mosaic-solver/a-star/bench/fuzz-3086-hard.json
  • src/pages/shifting-mosaic-solver/a-star/bench/fuzz-3870-hard.json
  • src/pages/shifting-mosaic-solver/a-star/bench/fuzz-4722-hard.json
  • src/pages/shifting-mosaic-solver/a-star/bench/fuzz-8697-hard.json
  • src/pages/shifting-mosaic-solver/a-star/bench/fuzz-9164-hard.json
  • src/pages/shifting-mosaic-solver/a-star/main.cpp
  • src/pages/shifting-mosaic-solver/a-star/test/CMakeLists.txt
  • src/pages/shifting-mosaic-solver/a-star/test/DragSolverTestOracle.h
  • src/pages/shifting-mosaic-solver/a-star/test/astar_unit_test.cpp
  • src/pages/shifting-mosaic-solver/a-star/test/dragsolver_search_test.cpp
  • src/pages/shifting-mosaic-solver/a-star/test/dragsolver_unit_test.cpp
  • src/pages/shifting-mosaic-solver/a-star/test/json_test.cpp
  • src/pages/shifting-mosaic-solver/a-star/test/optimizer_test.cpp
  • src/pages/shifting-mosaic-solver/a-star/wasm_bindings.cpp
  • src/pages/shifting-mosaic-solver/aStar.ts
  • src/pages/shifting-mosaic-solver/board.ts
  • src/pages/shifting-mosaic-solver/boardGeometry.ts
  • src/pages/shifting-mosaic-solver/config.ts
  • src/pages/shifting-mosaic-solver/directions.ts
  • src/pages/shifting-mosaic-solver/editorHelpers.ts
  • src/pages/shifting-mosaic-solver/index.html
  • src/pages/shifting-mosaic-solver/layout.ts
  • src/pages/shifting-mosaic-solver/node.ts
  • src/pages/shifting-mosaic-solver/shiftingMosaicSolver.css
  • src/pages/shifting-mosaic-solver/shiftingMosaicSolver.ts
  • src/pages/shifting-mosaic-solver/solutionView.ts
  • src/pages/shifting-mosaic-solver/turn.ts
  • src/pages/shifting-mosaic-solver/wasm/astar.worker.js
  • src/pages/shifting-mosaic-solver/wasmBridge.ts
  • src/util/benchShiftingMosaic.ts
  • src/util/benchWasmSimd.ts
  • src/util/build.ts
  • src/util/buildWasm.ts
  • src/util/fuzzShiftingMosaic.ts
  • src/util/minHeap.ts
  • src/util/plugins.ts
  • src/util/serve.ts
  • src/util/types.ts
  • test/phasic-dial-solver/phasicDialSolver.test.ts
  • test/resources/shiftingMosaicTest.json
  • test/resources/shiftingMosaicTest1.json
  • test/resources/shiftingMosaicTest10.json
  • test/resources/shiftingMosaicTest11.json
  • test/resources/shiftingMosaicTest12.json
  • test/resources/shiftingMosaicTest13.json
  • test/resources/shiftingMosaicTest14.json
  • test/resources/shiftingMosaicTest15.json
  • test/resources/shiftingMosaicTest16.json
  • test/resources/shiftingMosaicTest17.json
  • test/resources/shiftingMosaicTest18.json
  • test/resources/shiftingMosaicTest19.json
  • test/resources/shiftingMosaicTest2.json
  • test/resources/shiftingMosaicTest20.json
  • test/resources/shiftingMosaicTest21.json
  • test/resources/shiftingMosaicTest22.json
  • test/resources/shiftingMosaicTest23.json
  • test/resources/shiftingMosaicTest24.json
  • test/resources/shiftingMosaicTest25.json
  • test/resources/shiftingMosaicTest26.json
  • test/resources/shiftingMosaicTest27.json
  • test/resources/shiftingMosaicTest28.json
  • test/resources/shiftingMosaicTest29.json
  • test/resources/shiftingMosaicTest3.json
  • test/resources/shiftingMosaicTest30.json
  • test/resources/shiftingMosaicTest31.json
  • test/resources/shiftingMosaicTest32.json
  • test/resources/shiftingMosaicTest33.json
  • test/resources/shiftingMosaicTest34.json
  • test/resources/shiftingMosaicTest35.json
  • test/resources/shiftingMosaicTest36.json
  • test/resources/shiftingMosaicTest37.json
  • test/resources/shiftingMosaicTest38.json
  • test/resources/shiftingMosaicTest39.json
  • test/resources/shiftingMosaicTest4.json
  • test/resources/shiftingMosaicTest40.json
  • test/resources/shiftingMosaicTest41.json
  • test/resources/shiftingMosaicTest42.json
  • test/resources/shiftingMosaicTest43.json
  • test/resources/shiftingMosaicTest5.json
  • test/resources/shiftingMosaicTest6.json
  • test/resources/shiftingMosaicTest7.json
  • test/resources/shiftingMosaicTest8.json
  • test/resources/shiftingMosaicTest9.json
  • test/rollingBlocksSolver/block.musttouch.test.ts
  • test/rollingBlocksSolver/block.roll.test.ts
  • test/rollingBlocksSolver/block.validity.test.ts
  • test/shiftingMosaicSolver/aStar.collision.test.ts
  • test/shiftingMosaicSolver/aStar.primitives.test.ts
  • test/shiftingMosaicSolver/aStar.search.test.ts
  • test/shiftingMosaicSolver/board.geometry.test.ts
  • test/shiftingMosaicSolver/board.pointer.test.ts
  • test/shiftingMosaicSolver/board.render.test.ts
  • test/shiftingMosaicSolver/board.state.test.ts
  • test/shiftingMosaicSolver/board.test.ts
  • test/shiftingMosaicSolver/config.test.ts
  • test/shiftingMosaicSolver/mem64.node.test.mjs
  • test/shiftingMosaicSolver/portfolio.worker.ts
  • test/shiftingMosaicSolver/wasm.test.ts
💤 Files with no reviewable changes (2)
  • e2e/shiftingMosaicSolver.test.ts
  • .claude/settings.local.json
🛑 Comments failed to post (1)
test/rollingBlocksSolver/block.validity.test.ts (1)

365-385: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Test correctness depends on cross-iteration execution order.

The shared index variable is set by the first test.each iteration and consumed by the second. This only works if both iterations run strictly in sequence and order — fragile under reordering/parallel execution, and would throw (blocks[index]! undefined) or silently check the wrong block otherwise.

♻️ Proposed fix: pair each case with its own index
-    const nonOverlappingBlocksCase = [
+    const nonOverlappingBlocksCase: [Block[], number][] = [
       [
         [
           new Block(1, 0, 0, 4, 1, 1),
           new Block(2, 4, 0, 1, 4, 1),
           new Block(3, 1, 4, 4, 1, 1),
           new Block(4, 0, 1, 1, 4, 1),
         ],
+        0,
       ],
       [
         [
           new Block(1, 0, 0, 1, 1, 1),
           // ...unchanged...
         ],
+        13,
       ],
     ];
-
-    let index = 0;

     test.each(nonOverlappingBlocksCase)(
       "there should be no overlaps between blocks (%#)",
-      blocks => {
+      (blocks, index) => {
         checkOutOfBoundsSpy.mockReturnValue(true);
         checkBlockingCellsSpy.mockReturnValue(true);

         const result = blocks[index]!.checkValidity(
           5,
           5,
           cells,
           blocks,
           mustTouchCellsSatisfied,
         );

         expect(result).toBeTrue();
         expect((blocks[index]! as any).checkBlockingCells).toHaveBeenCalled();
-        index = 13;
       },
     );
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/rollingBlocksSolver/block.validity.test.ts` around lines 365 - 385,
Remove the shared index state from the test.each block and include the intended
block index as part of each nonOverlappingBlocksCase test case. Destructure the
case-specific index in the test callback and use it when selecting
blocks[index], so each iteration is independent of execution order and
parallelism.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
src/pages/shifting-mosaic-solver/shiftingMosaicSolver.ts (2)

533-545: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Dispose the active solution view when applying a config.

applyLoadedConfig() hides the solution view without calling SolutionView.dispose() or clearing this.solutionView. Because drag-and-drop works globally, this path is reachable while a solution is open, leaving its listeners or animation resources alive until another solution is entered.

Proposed fix
   private applyLoadedConfig(config: ShiftingMosaicTest) {
+    this.solutionView?.dispose();
+    this.solutionView = null;
     this.gridWidth = config.gridWidth;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/pages/shifting-mosaic-solver/shiftingMosaicSolver.ts` around lines 533 -
545, Update applyLoadedConfig() to dispose the currently active solution view
before hiding the solution UI. Call SolutionView.dispose() through
this.solutionView when present, then clear this.solutionView so global
drag-and-drop listeners and animation resources are released before loading the
new configuration.

376-408: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Ignore callbacks from canceled solver runs.

stopCurrentWorker() only clears the bookkeeping field; the onDone/onError closures do not verify that they belong to the active solve. A late callback after editing, reset, config loading, or starting another solve can show an obsolete solution or error for the current board. Add a monotonically increasing solve token and check it before handling callbacks or clearing currentWorker.

Proposed fix
   private currentWorker: SolverHandle | null = null;
+  private solveGeneration = 0;

     this.stopCurrentWorker();
+    const generation = ++this.solveGeneration;

       onDone: turns => {
+        if (generation !== this.solveGeneration) return;
         this.currentWorker = null;
         this.handleSolution(puzzle, turns);
       },
       onError: error => {
+        if (generation !== this.solveGeneration) return;
         this.currentWorker = null;
         this.showSolverError(error);
       },

   private stopCurrentWorker() {
+    this.solveGeneration++;
     if (this.currentWorker) {

Also applies to: 477-483, 585-589

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/pages/shifting-mosaic-solver/shiftingMosaicSolver.ts` around lines 376 -
408, Update calculateSolution and the related solver callback paths to use a
monotonically increasing solve token for each new solve. Capture the token in
each searchShiftingMosaicWasm callback and ignore onProgress, onPhase, onDone,
and onError when it no longer matches the active token; only the current run may
clear currentWorker or update progress, solution, or error state. Increment the
token when canceling or replacing a solve so callbacks from stopped runs are
invalidated.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@src/pages/shifting-mosaic-solver/shiftingMosaicSolver.ts`:
- Around line 533-545: Update applyLoadedConfig() to dispose the currently
active solution view before hiding the solution UI. Call SolutionView.dispose()
through this.solutionView when present, then clear this.solutionView so global
drag-and-drop listeners and animation resources are released before loading the
new configuration.
- Around line 376-408: Update calculateSolution and the related solver callback
paths to use a monotonically increasing solve token for each new solve. Capture
the token in each searchShiftingMosaicWasm callback and ignore onProgress,
onPhase, onDone, and onError when it no longer matches the active token; only
the current run may clear currentWorker or update progress, solution, or error
state. Increment the token when canceling or replacing a solve so callbacks from
stopped runs are invalidated.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 47fffde6-be92-48c4-a795-fea7fb8e59f5

📥 Commits

Reviewing files that changed from the base of the PR and between c306249 and 7b97518.

📒 Files selected for processing (30)
  • .github/workflows/test.yml
  • src/common/common.css
  • src/pages/rolling-blocks-solver/a-star/AStar.h
  • src/pages/rolling-blocks-solver/a-star/AStarGoals.cpp
  • src/pages/shifting-mosaic-solver/a-star/AStarSearch.cpp
  • src/pages/shifting-mosaic-solver/a-star/BenchFixture.cpp
  • src/pages/shifting-mosaic-solver/a-star/BenchFixture.h
  • src/pages/shifting-mosaic-solver/a-star/BitGrid.h
  • src/pages/shifting-mosaic-solver/a-star/DragSolver.cpp
  • src/pages/shifting-mosaic-solver/a-star/DragSolverBeam.cpp
  • src/pages/shifting-mosaic-solver/a-star/PARALLEL.md
  • src/pages/shifting-mosaic-solver/a-star/ParallelCascade.h
  • src/pages/shifting-mosaic-solver/a-star/test/json_test.cpp
  • src/pages/shifting-mosaic-solver/editorHelpers.ts
  • src/pages/shifting-mosaic-solver/index.html
  • src/pages/shifting-mosaic-solver/shiftingMosaicSolver.css
  • src/pages/shifting-mosaic-solver/shiftingMosaicSolver.ts
  • src/pages/shifting-mosaic-solver/solutionView.ts
  • src/pages/shifting-mosaic-solver/wasmBridge.ts
  • src/util/benchShiftingMosaic.ts
  • src/util/benchWasmSimd.ts
  • src/util/buildWasm.ts
  • src/util/fuzzShiftingMosaic.ts
  • src/util/wasmModule.ts
  • test/rollingBlocksSolver/aStar.test.ts
  • test/shiftingMosaicSolver/aStar.collision.test.ts
  • test/shiftingMosaicSolver/aStar.search.test.ts
  • test/shiftingMosaicSolver/mem64.node.test.mjs
  • test/shiftingMosaicSolver/portfolio.worker.ts
  • test/shiftingMosaicSolver/wasm.test.ts
🚧 Files skipped from review as they are similar to previous changes (23)
  • src/common/common.css
  • test/shiftingMosaicSolver/aStar.collision.test.ts
  • test/shiftingMosaicSolver/mem64.node.test.mjs
  • test/shiftingMosaicSolver/portfolio.worker.ts
  • src/util/benchWasmSimd.ts
  • src/pages/shifting-mosaic-solver/editorHelpers.ts
  • test/shiftingMosaicSolver/aStar.search.test.ts
  • src/pages/shifting-mosaic-solver/a-star/BenchFixture.h
  • .github/workflows/test.yml
  • src/pages/shifting-mosaic-solver/shiftingMosaicSolver.css
  • test/shiftingMosaicSolver/wasm.test.ts
  • src/pages/shifting-mosaic-solver/a-star/PARALLEL.md
  • src/util/buildWasm.ts
  • src/pages/shifting-mosaic-solver/a-star/test/json_test.cpp
  • src/util/fuzzShiftingMosaic.ts
  • src/util/benchShiftingMosaic.ts
  • src/pages/shifting-mosaic-solver/index.html
  • src/pages/shifting-mosaic-solver/wasmBridge.ts
  • src/pages/shifting-mosaic-solver/a-star/DragSolver.cpp
  • src/pages/shifting-mosaic-solver/a-star/BitGrid.h
  • src/pages/shifting-mosaic-solver/a-star/ParallelCascade.h
  • src/pages/shifting-mosaic-solver/a-star/AStarSearch.cpp
  • src/pages/shifting-mosaic-solver/a-star/DragSolverBeam.cpp

@sonarqubecloud

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/util/buildWasm.ts (1)

130-169: 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Make the shifting-mosaic WASM builds atomic. Promise.all won’t stop the other em++ processes, so a failure in one variant can still overwrite files in src/pages/shifting-mosaic-solver/wasm and leave that directory in a mixed state. Build each variant in a temp dir and swap it in on success, or terminate the sibling processes as soon as one fails.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/util/buildWasm.ts` around lines 130 - 169, Update the
SHIFTING_MOSAIC_VARIANTS build flow so all variant outputs are produced
atomically rather than directly in the shared wasm directory. Use a temporary
output directory for the builds and replace the existing directory only after
every build succeeds, or otherwise terminate sibling builds and clean up on
failure; preserve the existing variant configuration and output filenames.
🧹 Nitpick comments (1)
.github/workflows/test.yml (1)

70-74: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Pin the Emscripten SDK version for reproducible builds.

The new WASM step relies on the unversioned SDK setup above, while the MEMORY64 flags and comments assume a specific Emscripten generation. Select the exact SDK version validated by the repository instead of allowing toolchain drift. Emscripten supports explicit SDK version selection. (github.com)

       - name: Setup Emscripten
         uses: emscripten-core/setup-emsdk@v16
+        with:
+          version: "6.0.4" # or the exact version validated by the project
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/test.yml around lines 70 - 74, Pin the Emscripten SDK
version in the “Build shifting-mosaic WASM” workflow step using the exact
repository-validated version, rather than relying on the unversioned SDK setup.
Keep the existing MEMORY64 build configuration unchanged.

Source: MCP tools

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.github/workflows/test.yml:
- Around line 134-150: Update the MEMORY64 CI test invocation around
mem64.node.test.mjs so unsupported wasm64 runtimes cause a nonzero exit instead
of silently passing due to the test’s skip condition. Preserve skipping for
local runs by adding an explicit CI-only capability check or failure path, while
ensuring the workflow imports and exercises astar.mem64.mjs when support is
available.
- Line 14: Update the actions/checkout step to set persist-credentials to false,
and scope the workflow or job permissions to contents: read so subsequent
repository-controlled install and test steps cannot reuse a write-capable
checkout token.

---

Outside diff comments:
In `@src/util/buildWasm.ts`:
- Around line 130-169: Update the SHIFTING_MOSAIC_VARIANTS build flow so all
variant outputs are produced atomically rather than directly in the shared wasm
directory. Use a temporary output directory for the builds and replace the
existing directory only after every build succeeds, or otherwise terminate
sibling builds and clean up on failure; preserve the existing variant
configuration and output filenames.

---

Nitpick comments:
In @.github/workflows/test.yml:
- Around line 70-74: Pin the Emscripten SDK version in the “Build
shifting-mosaic WASM” workflow step using the exact repository-validated
version, rather than relying on the unversioned SDK setup. Keep the existing
MEMORY64 build configuration unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: f0156333-17eb-48fc-b2de-7fda498b90b6

📥 Commits

Reviewing files that changed from the base of the PR and between 7b97518 and 5d81d08.

📒 Files selected for processing (13)
  • .github/workflows/test.yml
  • e2e/coi.ts
  • e2e/index.test.ts
  • e2e/shiftingMosaicHeapLimit.slow.test.ts
  • e2e/shiftingMosaicSolver.blocks.test.ts
  • e2e/shiftingMosaicSolver.config.test.ts
  • e2e/shiftingMosaicSolver.editing.test.ts
  • e2e/shiftingMosaicSolver.solving.test.ts
  • e2e/shiftingMosaicWasmVariants.test.ts
  • src/pages/shifting-mosaic-solver/shiftingMosaicSolver.ts
  • src/pages/shifting-mosaic-solver/wasmBridge.ts
  • src/util/buildWasm.ts
  • test/shiftingMosaicSolver/mem64.node.test.mjs
🚧 Files skipped from review as they are similar to previous changes (9)
  • test/shiftingMosaicSolver/mem64.node.test.mjs
  • e2e/shiftingMosaicHeapLimit.slow.test.ts
  • e2e/shiftingMosaicSolver.solving.test.ts
  • e2e/shiftingMosaicSolver.config.test.ts
  • e2e/shiftingMosaicWasmVariants.test.ts
  • e2e/shiftingMosaicSolver.blocks.test.ts
  • e2e/shiftingMosaicSolver.editing.test.ts
  • src/pages/shifting-mosaic-solver/shiftingMosaicSolver.ts
  • src/pages/shifting-mosaic-solver/wasmBridge.ts

steps:
- name: Checkout code
uses: actions/checkout@v6
uses: actions/checkout@v7

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

git ls-files .github/workflows/test.yml
echo '---'
cat -n .github/workflows/test.yml

Repository: Valle12/islands-of-insight-tools

Length of output: 7350


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect checkout action defaults and any explicit permissions in this workflow.
python3 - <<'PY'
from pathlib import Path
p = Path('.github/workflows/test.yml')
text = p.read_text()
for needle in ['permissions:', 'persist-credentials', 'actions/checkout@v7', 'bun ci', 'e2e', 'unit']:
    print(f'### {needle}')
    for i, line in enumerate(text.splitlines(), 1):
        if needle in line:
            start = max(1, i-3)
            end = min(len(text.splitlines()), i+3)
            for j in range(start, end+1):
                print(f'{j:4}: {text.splitlines()[j-1]}')
            print()
PY

Repository: Valle12/islands-of-insight-tools

Length of output: 1476


🏁 Script executed:

#!/bin/bash
set -euo pipefail
cat -n .github/workflows/test.yml

Repository: Valle12/islands-of-insight-tools

Length of output: 7319


🌐 Web query:

actions/checkout@v7 persist-credentials default documentation

💡 Result:

In the actions/checkout version 7 action, the persist-credentials input maintains a default value of true [1][2]. When persist-credentials is set to its default (true), the action configures the authentication token or SSH key with the local git configuration [3][4][1]. This enables subsequent steps in your workflow to perform authenticated git commands (such as git fetch or git push) [3][4][5]. The credentials are removed during the post-job cleanup [3][5]. If you prefer to disable this behavior, you must explicitly set persist-credentials to false in your workflow step [3][5]: - uses: actions/checkout@v7 with: persist-credentials: false While there has been community discussion regarding changing this default to false to improve security [6][7], it remains true as of version 7 [1][2]. Notably, starting in version 6, the action was updated to store these credentials in a separate file under $RUNNER_TEMP rather than directly within the.git/config file, which improved credential security [8][5].

Citations:


Disable persisted checkout credentials. actions/checkout@v7 still defaults persist-credentials to true, and this job runs repository-controlled install/test steps afterward; set it to false (and scope permissions to contents: read) so later scripts can’t reuse the checkout token.

🧰 Tools
🪛 zizmor (1.26.1)

[warning] 13-14: credential persistence through GitHub Actions artifacts (artipacked): does not set persist-credentials: false

(artipacked)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/test.yml at line 14, Update the actions/checkout step to
set persist-credentials to false, and scope the workflow or job permissions to
contents: read so subsequent repository-controlled install and test steps cannot
reuse a write-capable checkout token.

Sources: MCP tools, Linters/SAST tools

Comment on lines +134 to +150
# bun cannot instantiate a Memory64 module, so the mem64 build is tested
# under node instead of in `bun test`.
#
# The node version is PINNED rather than taken from the runner image: the
# ubuntu-24.04 default validates a 64-bit MEMORY but rejects the 64-bit
# TABLE that emscripten 6.x emits for every wasm64 build, so the module
# fails to compile with "invalid table elements limits flags". The test's
# own probe now covers tables too, so an under-capable runtime SKIPS —
# which means without this pin the gate would quietly stop testing
# anything at all.
- name: Setup node
uses: actions/setup-node@v7
with:
node-version: 24

- name: Run MEMORY64 wasm test (node)
run: node --test test/shiftingMosaicSolver/mem64.node.test.mjs

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n## Workflow excerpt\n'
sed -n '120,170p' .github/workflows/test.yml

printf '\n## Test file outline\n'
ast-grep outline test/shiftingMosaicSolver/mem64.node.test.mjs --view expanded || true

printf '\n## Test file excerpt\n'
cat -n test/shiftingMosaicSolver/mem64.node.test.mjs

Repository: Valle12/islands-of-insight-tools

Length of output: 6901


Fail the MEMORY64 gate when wasm64 support is missing

{ skip: !memory64 } lets node --test exit 0 on an unsupported runtime, so this step can turn green without importing or exercising astar.mem64.mjs. Make the CI path fail explicitly and keep the skip only for local runs.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/test.yml around lines 134 - 150, Update the MEMORY64 CI
test invocation around mem64.node.test.mjs so unsupported wasm64 runtimes cause
a nonzero exit instead of silently passing due to the test’s skip condition.
Preserve skipping for local runs by adding an explicit CI-only capability check
or failure path, while ensuring the workflow imports and exercises
astar.mem64.mjs when support is available.

Source: MCP tools

@Valle12
Valle12 merged commit 79670c4 into main Jul 28, 2026
7 checks passed
@Valle12
Valle12 deleted the IIT-21-Shifting-Mosaic-Solver branch July 28, 2026 16:36
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