Files
Ferrous-Solitaire/SESSION_HANDOFF.md
funman300 5acd8e4cd0 docs(architecture): bring source-of-truth docs to post-migration reality
ARCHITECTURE.md predated the card_game migration (PR #88, 2026-06-22)
and still documented the pre-migration core: local Card with face_up,
PileType, stored score/undo/recycle, and a snapshot undo stack. Rewrites
the Core Game Models section around the upstream card_game/klondike
types and the derived-stats/replay-undo model, updates the
solitaire_core crate section (deps + ownership), adds solitaire_web and
solitaire_assetgen to the workspace tree, and corrects the
solitaire_app and Settings entries. Version bumped to 1.4.

Also adds the two missing crates to the CLAUDE.md crate map (AGENTS.md,
its gitignored local twin, was regenerated in place) and records the
full device checklist passing on the Fold 7 in SESSION_HANDOFF.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 15:26:06 -07:00

13 KiB
Raw Permalink Blame History

Ferrous Solitaire — Session Handoff

Last updated: 2026-07-06 — v0.41.0 + v0.41.1 released and verified on a physical Galaxy Fold 7; issue tracker empty except low-priority #130.


Current state

  • Branch state: master pushed to origin; latest work is the 2026-07-06 arc (PRs #121#131): scripted repo review, five issues filed and fixed, plugin module splits, two releases.
  • Latest tags: v0.41.1 (pile-marker child-resize fix + Android relayout logging) on top of v0.41.0 (consolidated release for everything since v0.39.0). Both released via tag push → CI signed APK; both verified installed on hardware (versionCode 4101).
  • Working tree: clean. Local scripts/*.go helpers are intentionally gitignored (.gitignore:43); scripts/watch_deploy.sh is now committed.
  • Latest verification: workspace clippy --all-targets -D warnings, full test suite, cargo ndk clippy for aarch64-linux-android, CI release builds green, and an on-device pass on the Fold 7 (fold/unfold layout, safe-area resume, marker fix).
  • Issue tracker: #116/#117/#118/#119/#120 all closed 2026-07-06. #130 (transient tableau clip after fold) open at low priority — did not reproduce in repeat testing; v0.41.1's relayout logging is the evidence channel if it recurs.

2026-07-06 session summary (v0.40.3 → v0.41.1)

  • Scripted repo review (cratemap/todoctx/cargoclip/testfail): clippy clean, tests green, error/SQL policies compliant. Five issues filed and all resolved same-day.
  • #116 safe-area re-poll after resume (PR #121): refresh_insets now gates on the poll counter, settles per cycle, and rewrites insets only on change. Verified on Fold 7 — note both Fold screens report identical insets (top=110 bottom=0), so the re-poll path is a no-op on this device.
  • #117 Draw-1 recycle (PR #122): unlimited recycling documented as an intentional rules decision (ARCHITECTURE.md "Rules decisions" + draw_one_recycling_is_unlimited_by_design lock-in test). A hard limit would invalidate the difficulty seed catalog and the winnable-deal solver.
  • #118 module splits (PRs #124/#125/#127/#128/#129): card, hud, settings, game, and input plugins are now module directories; tests in sibling tests.rs, runtime code split along system boundaries (pub(super) items, mod.rs glob-imports children). Largest runtime file is now card_plugin/sync.rs at 748 lines (was card_plugin.rs at 4,129).
  • v0.41.1 pile-marker fix (PR #131): marker outline + "A"/"K" watermark children are re-derived from the layout on every resize — previously spawn-time-sized, rendering as oversized grey slabs on empty foundations after fold/unfold. Found via photo evidence, fixed, re-verified on device.
  • Obtainium note: reported "no suitable release" for v0.41.0 even though the anonymous releases API, releases/latest, and the APK download were all verified fine — client-side issue; sideload via adb was used instead.

v0.40.0 release (2026-06-25)

Released via tag push → .gitea/workflows/android-release.yml built and signed the arm64-v8a release APK (release keystore, versionCode 4000 / versionName 0.40.0, 29.2 MB) and published it to the Gitea release. Obtainium clients tracking the repo pick it up automatically.

PR Summary
#106 fix(engine): Draw-Three waste fan hit-test now shares the renderer's fan step (card_plugin::waste_fan_step / tableau_col_step). The two had diverged under Android's tighter column spacing (H_GAP_DIVISOR=32), shifting the top fanned waste card's click target onto the card beneath it — so dragging the visible top card played the wrong one. Desktop/web were unaffected (the formulas already coincided there).
#105 test(engine): waste-card draggability regression tests (find_draggable_at picks the waste top with multiple cards and as a lone card).
#108 docs(android): NDK reference updated 26.3.1157926430.0.14904198; noted versions are not load-bearing and build_android_apk.sh auto-discovers the newest NDK/build-tools.

Pre-release validation performed locally this session: workspace clippy/test/build gates; aarch64-linux-android cross-compile + clippy clean (covers the #[cfg(target_os = "android")] paths that host CI never lints); release manifest sanity (solitaire_app/android/AndroidManifest.xml has no version fields so CI injection works; lib_name matches [lib].name); and a full signed local APK proving the build_android_apk.sh packaging pipeline end-to-end.


What shipped since v0.39.0

  • Browser Bevy canvas route and window.__FERROUS_DEBUG__ automation bridge landed, with Playwright coverage for /play.
  • In-place card_game / klondike rewrite phases are complete through the latest follow-up:
    • 5e87358 integrates upstream deps cleanly.
    • ae1ecc8 unifies Suit / Rank with upstream card_game types.
    • d864d98 routes klondike/card imports through solitaire_core.
    • 9bcf13d, 56e3b62, 26f1b00 finish schema-v3 migration coverage, undo/recycle score correctness, and rewrite-plan docs.
  • Android keystore wiring, Android build-script hardening, server auth/runtime hardening, and modal safe-area centering have landed.
  • CHANGELOG.md has been caught up from v0.34.0 through current unreleased work and committed in 7fe6ac6.
  • Matomo analytics was re-reviewed: MatomoClient and AnalyticsPlugin are wired through CoreGamePlugin on non-wasm targets, and targeted tests now cover opt-in client creation, event encoding, buffer trimming, and analytics mode labels.
  • Native analytics and Android physical-device validation now have runbooks in docs/analytics-validation.md and docs/ANDROID.md.

Historical notes before v0.39.0

See git log and CHANGELOG.md. The changelog now includes v0.34.0 through v0.39.0, plus current unreleased work.


What shipped since the last handoff (v0.23.0 → v0.35.1)

v0.34.0 — Android polish + code-quality sweep (2026-05-16/17)

Commit Summary
9623bde Wire FiraMono to Android corner label; CardImageSet load tests
980312c Fix wrong bottom-right suit symbol on JS/QS/KS card assets
04e99a8 Correct Android waste fan overlap and resume layout desync
3bb3ddb Eliminate panics, fix dismiss hit-test scope, guard home respawn
f8f1f26 Adaptive drop zones, touch event correctness, modal lifecycle guards
1eb4043 Auth-guard avatar serving; atomic write; user_id assertion in merge
69c6e88 Deterministic pile serialization, undo skip, url-encode bytes, merge_at
aa7b0f6 Gate frame-hot ECS systems on resource changes (perf)
6727126 Consolidate APP_DIR_NAME; add #[must_use] on pure fns
a4dfb0c Differentiate leaderboard opt-in vs opt-out error toasts (M-12)
7fc98f8 WASM: state() and step() return Result, errors throw JS exceptions (CR-6)
ffed6b2 Share Tokio runtime across all network tasks (M-16)
fa84152 Correct Android help hint label to ! (M-17)
18d7937 Derive Copy for DrawMode; drop redundant .clone() calls (M-18)
132fea9 Use saturating_add for move_count increments (M-19)
0ecc1a9 Add missing derives to AchievementContext (M-20)
2301cc6 Align android_keystore temp extension with cleanup glob (M-21)
2e52f54 Enforce 32-char display_name limit at sync client boundary (M-22)
c8878d6 Fix stale FOCUS_RING colour comment (M-23)
4aafc0a Name HUD popover Z-layers; replace raw Z arithmetic (M-24)

v0.35.0 — Accessibility + sync reliability (2026-05-18)

Commit Summary
eb6c93f Silence B0004 by adding Transform to ModalScrim
6f5cebd Fire WarningToastEvent on sync pull failure (was InfoToastEvent)
87aec5b Gate all decorative motion animations under reduce_motion_mode

reduce_motion_mode now gates: score pulse, score floater, streak flourish (hud_plugin), card-shake on rejected move, foundation completion flourish (feedback_anim_plugin). Pattern: gate at the trigger/start system, never at the tick system — if the component isn't inserted, the tick path never runs.

v0.35.1 — Leaderboard bug fixes (2026-05-18)

Commit Summary
8f86d66 Fix three leaderboard bugs: wrong toast type, stale label, name not synced

Three bugs fixed:

  1. Wrong toast type on errorpoll_opt_in_task / poll_opt_out_task error branches now fire WarningToastEvent instead of InfoToastEvent.

  2. Display name not pushed to server on changeSettings gains leaderboard_opted_in: bool (serde-defaulted false). Set true/false when opt-in/out tasks succeed and persisted to settings.json. handle_display_name_confirm now spawns an opt_in_leaderboard task when already opted in — the server's upsert endpoint updates only display_name without re-opting-in.

  3. "Public name" label stale after name changeLeaderboardPublicNameText marker component added to the label node. update_leaderboard_public_name_label system rewrites the text each frame the panel is open; O(0) cost when panel is closed.

5 new regression tests cover all three bugs.


Open punch list

1. Physical-device smoke test — DONE (2026-07-06, Galaxy Fold 7)

v0.41.1 installed via adb and the full device checklist passed on hardware: fold/unfold layout on both screens (incl. the #116 resume path and the pile-marker fix), safe-area inset resolution, Draw-Three waste fan tap accuracy (#106), modal centring on both screens, drag-and-drop across all pile types, text rendering, kill-and-restore, and the sync token flow. Reminder for future gates: AVD is not a substitute — adb shell input tap doesn't deliver real touch events.

2. Matomo analytics live validation (independent — NOT a release blocker)

Separate, ongoing task unrelated to the Android release. Settings has analytics_enabled, matomo_url, and matomo_site_id; the engine consumes them via AnalyticsPlugin on non-wasm targets. Remaining work is live validation against the deployed Matomo instance. Use docs/analytics-validation.md for the native validation checklist and the current web/WASM decision notes.


Architectural notes for next session

  • Plugin submodule pattern (2026-07-06 splits): big plugins are module directories: mod.rs holds types/markers/plugin-build and glob-imports the children (use input::*;); children hold pub(super) systems and start with use super::*; plus their own external imports. Tests (tests.rs) may need explicit imports for names no longer used by mod.rs itself.

  • Marker child-resize rule: anything spawned as a child of a layout-sized entity (outline frames, watermark text) must be re-derived in on_window_resized too — resizing only the parent sprite leaves children at spawn-time size (the v0.41.1 foldable bug).

  • Fold 7 quirks: both screens report identical safe-area insets (top=110, bottom=0), so inset-driven relayout never fires on fold; layout correctness across folds rides entirely on WindowResized. winit 0.30 logs TODO: find a way to notify application of content rect change on resume — see #130 if a stale-width layout ever reproduces; v0.41.1 logs every Android relayout (layout: resize to WxH) for exactly this.

  • Reduce-motion pattern: always gate in the start_* / detect_* system (the trigger), not the tick_* system. If the component is never inserted, the tick path never runs. See hud_plugin.rs::detect_score_change and feedback_anim_plugin.rs::start_shake_anim for the canonical pattern.

  • Leaderboard server upsert: POST /api/leaderboard/opt-in is idempotent — calling it when already opted in just updates display_name. Safe to call from handle_display_name_confirm without tracking a separate "needs update" flag.

  • Messages<T> API (Bevy 0.18.1): write with resource_mut::<Messages<T>>().write(value); read in tests with msgs.get_cursor() + cursor.read(msgs).next().

  • Test input-state pitfall: MinimalPlugins has no input-tick system, so ButtonInput::just_pressed state persists across frames unless explicitly cleared with input.release(key); input.clear() between updates.

  • /play debug bridge design: play.html runs two independent WASM instances in Promise.all([bootstrap(), init()]). bootstrap() sets window.__FERROUS_DEBUG__ (logic layer via solitaire_wasm.js); init() starts the Bevy canvas. The bridge operates its own SolitaireGame — moves applied through the bridge do NOT affect the Bevy visual game. This is intentional for automation/invariant checking.

  • HiDPI Bevy canvas: WindowResolution::default().with_scale_factor_override(1.0) is set in the canvas app. Without this, physical pixels exceed WebGL2's 2048px limit on HiDPI displays, causing an immediate wgpu panic on the first resize event.

  • /play-classic vs /play in e2e: smoke.spec.js + gameplay_review.spec.js target /play-classic (DOM-heavy game.html); play_canvas.spec.js targets /play using only the __FERROUS_DEBUG__ bridge (no DOM selectors). cycle_metrics.js supports both via --route play-classic|play.