5acd8e4cd0
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>
242 lines
13 KiB
Markdown
242 lines
13 KiB
Markdown
# 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.
|
||
|
||
- Release: https://git.aleshym.co/funman300/Ferrous-Solitaire/releases/tag/v0.40.0
|
||
|
||
| 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.11579264` → `30.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 error** — `poll_opt_in_task` / `poll_opt_out_task` error
|
||
branches now fire `WarningToastEvent` instead of `InfoToastEvent`.
|
||
|
||
2. **Display name not pushed to server on change** — `Settings` 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 change** — `LeaderboardPublicNameText` 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`.
|