B6/RAG-5: cross-game habit aggregation (the Pro story on every platform) - #136
Open
ber4444 wants to merge 2 commits into
Open
B6/RAG-5: cross-game habit aggregation (the Pro story on every platform)#136ber4444 wants to merge 2 commits into
ber4444 wants to merge 2 commits into
Conversation
…ds on Adds cross-game habit aggregation for Android's Pro story now that Android ships no on-device model: with no model, deterministic cross-game habit detection is the only proposed Pro feature that means anything there, and it inherits the always-correct/always-instant properties of the coach line for the same reason DeterministicCoach does. HabitAggregator (pure, :app) walks the player's assessed MISTAKE/BLUNDER moves across the most recent games in GameHistoryRepository, grouped by a recurring costly motif (currently hangs-piece, the one motif DeterministicCoach treats as describing the player's own mistake) with a general-rate fallback when no specific tactic recurs. Requires the pattern to recur across multiple games, not multiple times in one bad game. HabitNarrator turns that into deterministic text -- code detects the habit, nothing narrates it into being. Practice positions are the FEN before each flagged move plus the engine's alternative, both already computed by MoveAssessor/MotifDetector when the ply was assessed -- nothing new to compute. New Habits screen wired into AppRoot, Pro-gated like Rules Q&A. Prerequisite fix found while building this: GameActions hardcoded White as the human regardless of AppSettings.playerSide (a leftover from before the player-side setting existed), so a Black-side game's PGN mislabelled the colours and GameHistoryBackfiller had no way to know which plies were the player's -- it would have backfilled the engine's moves instead on any Black-side game. Added SavedGame.playerSide (defaults to "WHITE", correct for pre-existing data) and threaded it through GameActions and the backfiller. Also fixed a second, independent bug found in the same function: the backfiller's evaluate() fallback negated a value that ChessEngine.evaluate() already returns from White's perspective, inverting every assessment that hit it (any game played without a live-attached engine). Verified: chess-core:check, ondeviceai:check, app:check (including iosSimulatorArm64Test and wasmJsBrowserTest), androidApp:assembleDebug -- all green. No :onDeviceAi public API touched. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
#137 took the model-phrased coach off iOS as well as Android, so this feature is no longer "the Android Pro story" — it is the Pro story everywhere, and for a better reason than an absent runtime. Both phone runtimes were measured on the same 100 golden positions and neither was more truthful than the deterministic line, so "what Pro adds" had to stop meaning "a model phrases it" and start meaning a surface the free tier genuinely does not have. The gap that reframing exposed: Screen.HABITS was Pro-gated but PaywallScreen.proFeatures() never listed it, so a purchase unlocked a surface the paywall didn't sell. That is the same class of bug #137 fixed in the other direction (selling a surface the build can't show), and it wants the same shape of fix — the entry is keyed off LocalHabitsManager, the same signal Screen.HABITS branches on, so the list cannot drift from what a purchase actually unlocks. Adds LocalHabitsManager for that one reason; availability is a property of the build (gameHistory != null per entry point), which LocalEntitlements cannot know. Also notes in HabitNarrator why the "code detects, the model narrates" contract binds harder on an aggregate than on a single move: more claims per sentence, and both failures the device run found were a supplied fact reattached to the wrong subject. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
ber4444
force-pushed
the
b6-habit-aggregation
branch
from
August 15, 2026 14:55
165fa06 to
df637f5
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Cross-game habit aggregation (B6/RAG-5): the app already stores a
MoveAssessmenton every ply of every saved game, and nothing has ever readacross them. This does — "You've hung a piece in 4 of your last 10 games" —
and it is the sentence no single-move coach can produce, on the axis the field
is weakest on (DecodeChess and Dr. Wolf both lose the thread between sessions).
It is also what Pro adds now. The Move Coach is deterministic on every
platform (#137 measured both phone runtimes against the same 100 golden
positions; neither was more truthful than the computed line), so Pro can no
longer mean "a model phrases the coach" anywhere. This is the replacement, and a
better one: it is a surface the free tier genuinely doesn't have, rather than a
nicer rendering of one it does.
Nothing here runs a model or touches the network, on any platform.
What's in it
HabitAggregator— a pure function overGameHistoryRepository.games.Walks the player's assessed MISTAKE/BLUNDER moves across the most recent
games, groups by a recurring costly motif (currently
hangs-piece— the onemotif
DeterministicCoachtreats as describing the player's own mistakerather than something going right), with a general-rate fallback when no
specific tactic clears its own bar. Requires the pattern to recur across
multiple games, not multiple times in one bad game — a single rough game is
not a habit.
HabitNarrator— deterministic text from aHabitSummary. Code detects;nothing narrates it into being. A model, where one exists, could later rephrase
the two strings this produces, but the counts and the motif are never its to
invent. That contract binds harder here than on a single move: an aggregate
carries more claims per sentence, and both failures ML Kit on Android: availability was a client-config bug, and the "repetition loop" was ours #137's device run found
were a supplied fact reattached to the wrong subject.
HabitOccurrencecarriesfenBefore(the exactposition) and
bestMoveSan(the engine's alternative), both already computedby
MoveAssessor/MotifDetectorwhen the ply was assessed. Nothing new iscomputed; data already persisted is reprojected.
HabitsScreen+HabitsManager, wired intoAppRootasScreen.HABITSwith a "Habits" button next to History in the game screen's menu row.
Pro-gated the way Rules Q&A is (an explicit branch rather than
ProGate,since the screen supplies its own
SubScreenScaffold), withavailable=gameHistory != null.HabitsManagerneeds no platform runtime, model or engine, so unlike theAI managers
AppRootconstructs it directly from thegameHistoryit alreadyhas. No entry-point wiring changed on any platform.
LocalHabitsManager— the samesignal
Screen.HABITSbranches on, so the advertised list cannot drift fromwhat a purchase actually unlocks.
PaywallScreen.proFeatures()already had toavoid selling a surface the build can't show (ML Kit on Android: availability was a client-config bug, and the "repetition loop" was ours #137's coach line); gating one
the build can show while never listing it is the same bug pointing the other
way, and both are refund-shaped. Availability is a property of the build, which
LocalEntitlementscannot know, so it stays a separate signal.A prerequisite bug found and fixed
GameActionshardcoded White as the human regardless ofAppSettings.playerSide— a leftover from before the player-side setting(#112) existed, never updated when it shipped. Two consequences:
SavedGamemislabelled the colours for a Black-side game(
[White "Player"]even when the human played Black).GameHistoryBackfillerhad no way to know which plies were the player's,so on a Black-side game it silently backfilled assessments for the engine's
moves — fatal for this feature, which would then have reported the engine's
mistakes as the user's habits.
Fixed by adding
SavedGame.playerSide(defaulting to"WHITE", which is correctfor data saved before the field existed — that was the only side selectable then)
and threading it through
GameActions.pgnTags/toSavedGameandGameHistoryBackfiller.A second, independent bug surfaced while fixing the backfiller: its
evaluate()fallback path negated a value thatChessEngine.evaluate()alreadyreturns from White's perspective (confirmed against both UCI transports —
UciEvaluation.toWhitePerspectiveis applied before the value ever reaches thecaller), silently inverting every assessment that hit it — any game played
without a live-attached engine, e.g. the CPU fallback.
GameHistoryBackfillerTestpins both fixes.What I could not verify
HabitsScreen. Comparable coverage(
GameHistoryScreen) lives inandroidDeviceTestand needs an emulator thisenvironment doesn't have running. The aggregation and narration logic — the
part that can be wrong in a way that matters — is covered by
HabitAggregatorTest/HabitNarratorTestincommonTest, which did run.CLAUDE.md,isProUnlocked()treats a nullLocalEntitlementsas unrestricted, so noCompose UI test can catch a paywall regression on any surface.
GameHistoryDetail'sexisting PGN-block precedent, not loaded into an interactive practice board —
there is no FEN-import/practice-mode flow to hand them to, and building one is
a materially larger feature. That is the honest scope of "suggest a practice
position" here.
[move-N]-style citation for cross-game occurrences.ui/CitationSanitizerdeliberately preserves that syntax for the currentgame's board-jump affordance (B16); a habit spans multiple saved games, so
reusing it would be ambiguous — and this feature never routes through a model
or the sanitizer at all, so nothing forced the question. Occurrences are listed
in plain text with a recency label.
Composes with the Maia-3 spike (not wired here)
HabitSummary/HabitOccurrenceare plain data so a future Maia-3 signal("players at your level play X here") can sit alongside a detected habit without
a redesign, per that spike's own note that the two compose: habits say you keep
doing X, Maia says players above you do Y. Nothing Maia-related is wired in.
Verification
Green, including
:ondeviceai:iosSimulatorArm64Test,:app:iosSimulatorArm64Testand:app:wasmJsBrowserTest. No:onDeviceAipublic API touched.
🤖 Generated with Claude Code