Guard that every drawable has a drawable-night twin #42

Closed
opened 2026-08-18 22:47:09 -05:00 by null · 1 comment
Owner

The light/dark pairing is protected by a preview and nothing else.

What is true now

app/src/main/kotlin/dev/privacyllc/period/feature/onboarding/OnboardingPreviews.kt renders all eight illustrations in a light/dark @Preview pair, with one entry per illustration, explicitly so that a missing night asset is visible. Its own KDoc says why: "a set of eight where seven have a night variant looks completely fine in light mode."

But a preview is not a test. Nothing fails a build, there are no screenshot tests in the repository, and the three existing guards — checkModuleBoundaries, checkPermissions and checkNoHealthLogging in the root build.gradle.kts — none of them inspect resources.

What it costs

The theme swap is pure resource resolution, which is what makes it robust — and also what makes a gap silent. A missing drawable-night file does not fail, does not warn, and renders the light illustration on a dark screen. Nobody sees it unless they open the app in dark mode on the one affected screen.

What to do

Add checkThemedDrawables to the root build.gradle.kts beside the other three, wired into check the same way. Every file in drawable*/ must have a counterpart of the same name in the matching -night folder, and vice versa.

Write the guard and prove it fails before believing it passes. docs/architecture/GUARDS.md §1 requires it and scripts/prove-guard.sh performs it. Three guards in this project have been confidently green over exactly the failure they claimed to catch — assume this one is broken until it has been watched failing.

Traps

  • Never a silent pass. If the walk finds no drawables at all — a renamed folder, a changed module path — the task must fail rather than report success, which is how checkModuleBoundaries spent its first day green.
  • Exclude /build/ and /bin/ outputs. checkNoHealthLogging failed its own first run by reading a stale copy of a test file in a gitignored bin/ directory.
  • Do not require a night twin for every drawable in the repository — ic_launcher_monochrome.xml is a themed vector with no night variant and must not fail the build. Scope the rule to the illustration set, or allow an explicit exemption list with reasons.
  • Configuration cache: resolve paths at configuration time, not inside doLast, or the build fails with a cache error instead of a guard result.

Verify: bash scripts/prove-guard.sh reports exactly one failure when a drawable-night file is deleted and green when it is restored, and ./gradlew check runs the guard.

The light/dark pairing is protected by a preview and nothing else. ## What is true now `app/src/main/kotlin/dev/privacyllc/period/feature/onboarding/OnboardingPreviews.kt` renders all eight illustrations in a light/dark `@Preview` pair, with one entry per illustration, explicitly so that a missing night asset is visible. Its own KDoc says why: "a set of eight where seven have a night variant looks completely fine in light mode." But a preview is not a test. Nothing fails a build, there are no screenshot tests in the repository, and the three existing guards — `checkModuleBoundaries`, `checkPermissions` and `checkNoHealthLogging` in the root `build.gradle.kts` — none of them inspect resources. ## What it costs The theme swap is pure resource resolution, which is what makes it robust — and also what makes a gap silent. A missing `drawable-night` file does not fail, does not warn, and renders the light illustration on a dark screen. Nobody sees it unless they open the app in dark mode on the one affected screen. ## What to do Add `checkThemedDrawables` to the root `build.gradle.kts` beside the other three, wired into `check` the same way. Every file in `drawable*/` must have a counterpart of the same name in the matching `-night` folder, and vice versa. **Write the guard and prove it fails before believing it passes.** `docs/architecture/GUARDS.md` §1 requires it and `scripts/prove-guard.sh` performs it. Three guards in this project have been confidently green over exactly the failure they claimed to catch — assume this one is broken until it has been watched failing. ## Traps - **Never a silent pass.** If the walk finds no drawables at all — a renamed folder, a changed module path — the task must fail rather than report success, which is how `checkModuleBoundaries` spent its first day green. - Exclude `/build/` and `/bin/` outputs. `checkNoHealthLogging` failed its own first run by reading a stale copy of a test file in a gitignored `bin/` directory. - Do not require a night twin for every drawable in the repository — `ic_launcher_monochrome.xml` is a themed vector with no night variant and must not fail the build. Scope the rule to the illustration set, or allow an explicit exemption list with reasons. - Configuration cache: resolve paths at configuration time, not inside `doLast`, or the build fails with a cache error instead of a guard result. Verify: `bash scripts/prove-guard.sh` reports exactly one failure when a `drawable-night` file is deleted and green when it is restored, and `./gradlew check` runs the guard.
null added this to the Batch 08 — Polish milestone 2026-08-18 22:47:09 -05:00
null added the
P2
label 2026-08-18 22:47:10 -05:00
null closed this issue 2026-08-19 21:37:46 -05:00
Author
Owner

Done in 255808f.

checkThemedDrawables is in the root build.gradle.kts beside the other three and wired into every module's check, verified in the task graph.

themed drawables: 64 resource(s) across 5 folder pair(s), all paired.

It walks both directions — a light asset with no night twin, and a night asset with no light one. The second is the same defect from the other side, and it renders as nothing at all rather than as the wrong picture.

ic_launcher_monochrome is the single exemption, in a named map with its reason: the launcher tints it from the system palette, so a night copy would be a second source of truth for one shape. An exemption list rather than a narrowed scope, because a scope listing today's eight illustrations would not cover tomorrow's — and the defect being guarded against is a file somebody forgot.

Proved to fail, four ways

prove-guard.sh cannot drive this guard, and that is worth recording rather than working around: it breaks a target by replacing a string inside a file, and this guard's failure mode is a file that is not there, in a set that is entirely .webp. There is no string to replace in an absent file and no text in the present ones.

So GUARDS.md §1's manual recipe was run instead, to the same standard:

Mutation Result
drawable-night-xhdpi/art_welcome.webp deleted exit 1, exactly one violation, naming art_welcome
a dark-only art_orphan.webp added exit 1, exactly one violation, naming art_orphan
both restored green — 64 resources, 5 folder pairs
themedResourceRoots pointed at a folder that does not exist no drawables were found, so no pairing was checked. This is not a pass

The fourth is the one this repo has been bitten by twice, so it gets proved too rather than assumed.

That limitation is now GUARDS.md §9 — "some guards cannot be driven by prove-guard.sh, and must still be proved" — because the next guard whose subject is a missing file will hit the same wall, and the tempting response is to skip §1 rather than run it by hand.

Traps from the issue, each addressed

  • Never a silent pass — the fourth proof above.
  • /build/ and /bin/ excluded, the trap checkNoHealthLogging fell into on its own first run.
  • ic_launcher_monochrome.xml does not fail the build — exempted with a reason rather than by narrowing the rule.
  • Configuration cache — roots and repo root are resolved at configuration time; doLast never touches layout, which would capture the Project and fail with a cache error instead of a guard result.
Done in `255808f`. `checkThemedDrawables` is in the root `build.gradle.kts` beside the other three and wired into every module's `check`, verified in the task graph. ``` themed drawables: 64 resource(s) across 5 folder pair(s), all paired. ``` It walks **both** directions — a light asset with no night twin, and a night asset with no light one. The second is the same defect from the other side, and it renders as nothing at all rather than as the wrong picture. `ic_launcher_monochrome` is the single exemption, in a named map with its reason: the launcher tints it from the system palette, so a night copy would be a second source of truth for one shape. An exemption list rather than a narrowed scope, because a scope listing today's eight illustrations would not cover tomorrow's — and the defect being guarded against is a file somebody forgot. ## Proved to fail, four ways `prove-guard.sh` **cannot drive this guard**, and that is worth recording rather than working around: it breaks a target by replacing a string inside a file, and this guard's failure mode is a file that is *not there*, in a set that is entirely `.webp`. There is no string to replace in an absent file and no text in the present ones. So GUARDS.md §1's manual recipe was run instead, to the same standard: | Mutation | Result | | --- | --- | | `drawable-night-xhdpi/art_welcome.webp` deleted | exit 1, **exactly one** violation, naming `art_welcome` | | a dark-only `art_orphan.webp` added | exit 1, **exactly one** violation, naming `art_orphan` | | both restored | green — 64 resources, 5 folder pairs | | `themedResourceRoots` pointed at a folder that does not exist | `no drawables were found, so no pairing was checked. This is not a pass` | The fourth is the one this repo has been bitten by twice, so it gets proved too rather than assumed. That limitation is now **`GUARDS.md` §9** — "some guards cannot be driven by prove-guard.sh, and must still be proved" — because the next guard whose subject is a missing file will hit the same wall, and the tempting response is to skip §1 rather than run it by hand. ## Traps from the issue, each addressed - **Never a silent pass** — the fourth proof above. - **`/build/` and `/bin/` excluded**, the trap `checkNoHealthLogging` fell into on its own first run. - **`ic_launcher_monochrome.xml` does not fail the build** — exempted with a reason rather than by narrowing the rule. - **Configuration cache** — roots and repo root are resolved at configuration time; `doLast` never touches `layout`, which would capture the `Project` and fail with a cache error instead of a guard result.
Sign in to join this conversation.
No Label
P0
P1
P2
release-blocker
No project
No Assignees
1 Participants
Notifications
Due Date
The due date is invalid or out of range. Please use the format 'yyyy-mm-dd'.

No due date set.

Dependencies

No dependencies set.

Reference: null/Privacy-Period-Tracker#42
No description provided.