monarch-session-context
Standing context for the Monarch Android app at D:\Monarch — the single-fixed-text-scale decision and how it is implemented, the art generator's paper-film trap and quota behaviour, and the ANDROID_SE
SKILL.mdskills/managed/monarch-session-context/SKILL.md
GitHubMonarch standing context
Project: D:\Monarch — Kotlin + Jetpack Compose + Room fitness app. Owner's
phone is a Samsung S25 Ultra, adb serial R5GL14GXV3J; an emulator
emulator-5554 is usually also attached.
One fixed text scale (owner decision — do not re-litigate)
The owner asked for a single text scale: the Android system font setting must not affect Monarch at all.
Implemented in MainActivity.attachBaseContext:
override fun attachBaseContext(newBase: Context) {
val pinned = Configuration(newBase.resources.configuration).apply {
fontScale = FIXED_FONT_SCALE // 1f, in ui/theme/Theme.kt
}
super.attachBaseContext(newBase.createConfigurationContext(pinned))
}
Do not implement this as a CompositionLocalProvider(LocalDensity provides …)
in the theme. That covers only the tree it is provided to: a Dialog,
AlertDialog, Popup, DropdownMenu or ModalBottomSheet composes in its own
window and re-reads the platform configuration, so it keeps scaling while every
screen behind it holds still. Measured: with the theme pinned, a dialog title
still grew 409px → 756px between system 1.0x and 2.0x.
Consequences already applied — do not re-add:
- The per-screen large-type defences were deleted (nav-label cap, XP rail growth, step-dial fallback, stacked tab segments, scaled FAB clearance).
- The accessibility cost is recorded in the
docs/TODO.mddecisions table.
Proof method: set settings put system font_scale to 0.85 / 1.0 / 2.0, dump
uiautomator at each, and compare text-node widths/heights. Correct result is
zero differing nodes. Include a dialog in the comparison — the screens alone
will not reveal a leak.
Artwork generation (tools/art.py)
- Routes
gemini-3.1-flash-imagethrough the ompgoogle-antigravityprovider. - Paper film trap. Density keying maps blank paper to a LOW alpha, not zero:
78–91% of a canvas came back at alpha 3–24, invisible in isolation and plainly
a lighter box on Monarch's near-black panels. Cut at source with a soft knee
above the histogram gap;
backdrop_auditnow reports afilmpercentage. Corner checks and opaque-coverage checks both pass a film-laden image. - Sketch frame. The model draws its own faint border in the margin. Crop ~7% off each edge or it reads as the same box.
- Quota. Roughly 9–10 images per reset, then
QUOTA_EXHAUSTEDfrom cloudcode-pa. Batches must be resumable:tools/art_batches/run.pyskips ids already drawn and stops with a named reason. - Never half-wire a catalogue (e.g. 4 of 10 crests): mixed generated and procedural art looks worse than either set alone.
App icon
tools/icon_install.py turns one keyed PNG into all 20 mipmap files. It scales
art into the centre 66% (the adaptive mask keeps only 72dp of 108dp) and
writes the monochrome layer as flat white on the source alpha (Android tints it,
so colour there is meaningless). It refuses an unkeyed or empty source. Judge an
icon on the launcher, never from the PNG.
Running tests — the data-safety rule
Always pin instrumented runs to the emulator:
ANDROID_SERIAL=emulator-5554 ./gradlew :app:connectedDebugAndroidTest
The suite calls pm clear and deletes rows from the app's own database. With
the owner's phone attached, an unpinned run destroys their real training
history. The gate also uninstalls the app afterwards, so reinstall before
driving the UI or a sweep will silently measure the launcher.
Gate
ANDROID_SERIAL=emulator-5554 ./gradlew :app:assembleDebug :app:testDebugUnitTest \
:app:connectedDebugAndroidTest :app:lintRelease
Expect lint 10 (each audited) and zero compiler warnings. CI runs on GitHub via
.github/workflows/ci.yml with cancel-in-progress: true, so rapid pushes
cancel each other — "CI is green" is a statement about HEAD only.