feat: guard §45's logging rules, and stop the leak that needed no log call
closes #38
checkNoHealthLogging fails the build on any logging call in a module that can
see a cycle date. It runs in `./gradlew check`.
WHY IT IS A GUARD AND NOT A GREP
Both traps were already live in this repository. PeriodApplication passes
android.util.Log.WARN to WorkManager as a CONSTANT, which is not a log call.
ReminderWorker's KDoc says "a Log.d in a worker is the kind that survives",
explaining why there isn't one — a naive grep fails the build on the clearest
possible explanation, and the obvious fix is to delete the explanation. So it
matches a call shape, and strips comments first.
Proved both directions per GUARDS.md §1: an injected Log.d in CycleRepository
produced exactly one failure; a comment containing Log.d( and println( stayed
green. It also failed its own first run by walking domain/*/bin/, a gitignored
IDE output holding stale copies of test files — a guard that fails on untracked
build output is one somebody switches off.
THE LEAK IT WAS NOT LOOKING FOR
Prediction's init block interpolated dates into its require messages:
require(!windowStart.isAfter(windowEnd)) { "window start $windowStart is..." }
Five predicted dates across three messages, inside an IllegalArgumentException —
the one string a crash reporter collects without anybody choosing to log it.
§45 forbids exactly this and no logging statement was involved.
The same applies to every data class, since toString() renders every field into
any string that touches it. PeriodRecord, SpottingRecord, CycleRecord,
Prediction and NotYetObservation now override it: ids and cycle lengths survive,
dates do not. NoDatesInDiagnosticsTest pins seven cases and was itself proved to
fail.
R8 -assumenosideeffects strips android.util.Log from release, covering what a
source guard cannot reach: a dependency logging on our behalf, and a module
added without being listed in the guard.
VERIFIED ON A RELEASE BUILD, NOT REASONED ABOUT
assembleRelease signed with the debug keystore, installed, driven from
onboarding to a forecast and then logging a period: zero ISO dates in logcat,
zero health words, and the only mentions of the package are the system's own. A
screenshot confirms it reached a real forecast, because "no logs" is trivially
true of an app that did nothing.
201 tests pass. All three guards green.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 22:01:33 -05:00
|
|
|
# Release builds are minified.
|
chore: adopt the project template and add the Kotlin/Compose skeleton
Period was a bare directory holding one 2,527-line specification, with no git
repository, no tracker and no documentation convention. This is the adoption
from Projects/Template/START-HERE-New-Project.md, plus a project that compiles
so the hooks and future guards have something real to run against.
Documents. scaffold.sh created 19 paths, 0 skipped. The specification moved to
docs/planning/PRODUCT_PLAN.md unchanged in substance, with a status header; the
capitalised Docs/ is gone. Every scaffolded document was filled in for Period.
docs/OPERATIONS.md deleted — an offline app is not a deployed service.
DOC_TRUST_MAP.md written last, describing what is actually here, including what
this project deliberately does not have.
Code. Four Gradle modules. domain/cycle and domain/prediction are kotlin("jvm")
and cannot see the Android SDK, so the engine is testable without an emulator —
17 tests pass, 12 of them the acceptance cases from PRODUCT_PLAN.md §51.
BaselinePredictionEngine is a robust-median prototype and explicitly not the
product; it exists so Batch 02's replacement can be shown to be better rather
than merely different.
Versions verified against their official sources today rather than inherited
from the specification's own numbers, which that document asks for: Kotlin
2.4.10, AGP 9.3.1, Gradle 9.7.0, Compose BOM 2026.08.00, Room 2.8.4, Hilt
2.60.1. AGP 9 ships Kotlin built in, so org.jetbrains.kotlin.android is no
longer applied. compileSdk is 37 because current AndroidX requires it; targetSdk
stays 36, Play's floor from 2026-08-31, and the difference is deliberate.
Six scripts taken into scripts/; the rest declined and named in docs/TOOLS.md.
Three hooks in .githooks/, with pre-commit adapted to Gradle.
closes #1
closes #2
2026-08-18 02:16:47 -05:00
|
|
|
#
|
feat: guard §45's logging rules, and stop the leak that needed no log call
closes #38
checkNoHealthLogging fails the build on any logging call in a module that can
see a cycle date. It runs in `./gradlew check`.
WHY IT IS A GUARD AND NOT A GREP
Both traps were already live in this repository. PeriodApplication passes
android.util.Log.WARN to WorkManager as a CONSTANT, which is not a log call.
ReminderWorker's KDoc says "a Log.d in a worker is the kind that survives",
explaining why there isn't one — a naive grep fails the build on the clearest
possible explanation, and the obvious fix is to delete the explanation. So it
matches a call shape, and strips comments first.
Proved both directions per GUARDS.md §1: an injected Log.d in CycleRepository
produced exactly one failure; a comment containing Log.d( and println( stayed
green. It also failed its own first run by walking domain/*/bin/, a gitignored
IDE output holding stale copies of test files — a guard that fails on untracked
build output is one somebody switches off.
THE LEAK IT WAS NOT LOOKING FOR
Prediction's init block interpolated dates into its require messages:
require(!windowStart.isAfter(windowEnd)) { "window start $windowStart is..." }
Five predicted dates across three messages, inside an IllegalArgumentException —
the one string a crash reporter collects without anybody choosing to log it.
§45 forbids exactly this and no logging statement was involved.
The same applies to every data class, since toString() renders every field into
any string that touches it. PeriodRecord, SpottingRecord, CycleRecord,
Prediction and NotYetObservation now override it: ids and cycle lengths survive,
dates do not. NoDatesInDiagnosticsTest pins seven cases and was itself proved to
fail.
R8 -assumenosideeffects strips android.util.Log from release, covering what a
source guard cannot reach: a dependency logging on our behalf, and a module
added without being listed in the guard.
VERIFIED ON A RELEASE BUILD, NOT REASONED ABOUT
assembleRelease signed with the debug keystore, installed, driven from
onboarding to a forecast and then logging a period: zero ISO dates in logcat,
zero health words, and the only mentions of the package are the system's own. A
screenshot confirms it reached a real forecast, because "no logs" is trivially
true of an app that did nothing.
201 tests pass. All three guards green.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 22:01:33 -05:00
|
|
|
# When a rule is added here it must say what breaks without it — a proguard file
|
chore: adopt the project template and add the Kotlin/Compose skeleton
Period was a bare directory holding one 2,527-line specification, with no git
repository, no tracker and no documentation convention. This is the adoption
from Projects/Template/START-HERE-New-Project.md, plus a project that compiles
so the hooks and future guards have something real to run against.
Documents. scaffold.sh created 19 paths, 0 skipped. The specification moved to
docs/planning/PRODUCT_PLAN.md unchanged in substance, with a status header; the
capitalised Docs/ is gone. Every scaffolded document was filled in for Period.
docs/OPERATIONS.md deleted — an offline app is not a deployed service.
DOC_TRUST_MAP.md written last, describing what is actually here, including what
this project deliberately does not have.
Code. Four Gradle modules. domain/cycle and domain/prediction are kotlin("jvm")
and cannot see the Android SDK, so the engine is testable without an emulator —
17 tests pass, 12 of them the acceptance cases from PRODUCT_PLAN.md §51.
BaselinePredictionEngine is a robust-median prototype and explicitly not the
product; it exists so Batch 02's replacement can be shown to be better rather
than merely different.
Versions verified against their official sources today rather than inherited
from the specification's own numbers, which that document asks for: Kotlin
2.4.10, AGP 9.3.1, Gradle 9.7.0, Compose BOM 2026.08.00, Room 2.8.4, Hilt
2.60.1. AGP 9 ships Kotlin built in, so org.jetbrains.kotlin.android is no
longer applied. compileSdk is 37 because current AndroidX requires it; targetSdk
stays 36, Play's floor from 2026-08-31, and the difference is deliberate.
Six scripts taken into scripts/; the rest declined and named in docs/TOOLS.md.
Three hooks in .githooks/, with pre-commit adapted to Gradle.
closes #1
closes #2
2026-08-18 02:16:47 -05:00
|
|
|
# of unexplained -keep lines is a file nobody can ever safely shrink again.
|
feat: guard §45's logging rules, and stop the leak that needed no log call
closes #38
checkNoHealthLogging fails the build on any logging call in a module that can
see a cycle date. It runs in `./gradlew check`.
WHY IT IS A GUARD AND NOT A GREP
Both traps were already live in this repository. PeriodApplication passes
android.util.Log.WARN to WorkManager as a CONSTANT, which is not a log call.
ReminderWorker's KDoc says "a Log.d in a worker is the kind that survives",
explaining why there isn't one — a naive grep fails the build on the clearest
possible explanation, and the obvious fix is to delete the explanation. So it
matches a call shape, and strips comments first.
Proved both directions per GUARDS.md §1: an injected Log.d in CycleRepository
produced exactly one failure; a comment containing Log.d( and println( stayed
green. It also failed its own first run by walking domain/*/bin/, a gitignored
IDE output holding stale copies of test files — a guard that fails on untracked
build output is one somebody switches off.
THE LEAK IT WAS NOT LOOKING FOR
Prediction's init block interpolated dates into its require messages:
require(!windowStart.isAfter(windowEnd)) { "window start $windowStart is..." }
Five predicted dates across three messages, inside an IllegalArgumentException —
the one string a crash reporter collects without anybody choosing to log it.
§45 forbids exactly this and no logging statement was involved.
The same applies to every data class, since toString() renders every field into
any string that touches it. PeriodRecord, SpottingRecord, CycleRecord,
Prediction and NotYetObservation now override it: ids and cycle lengths survive,
dates do not. NoDatesInDiagnosticsTest pins seven cases and was itself proved to
fail.
R8 -assumenosideeffects strips android.util.Log from release, covering what a
source guard cannot reach: a dependency logging on our behalf, and a module
added without being listed in the guard.
VERIFIED ON A RELEASE BUILD, NOT REASONED ABOUT
assembleRelease signed with the debug keystore, installed, driven from
onboarding to a forecast and then logging a period: zero ISO dates in logcat,
zero health words, and the only mentions of the package are the system's own. A
screenshot confirms it reached a real forecast, because "no logs" is trivially
true of an app that did nothing.
201 tests pass. All three guards green.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 22:01:33 -05:00
|
|
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
# PRODUCT_PLAN.md §45: "Disable verbose logging in release builds."
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
#
|
|
|
|
|
# `checkNoHealthLogging` in the root build.gradle.kts already fails the build on
|
|
|
|
|
# any logging call in a module that can see a cycle date, and that is the real
|
|
|
|
|
# protection because it stops the line being written at all.
|
|
|
|
|
#
|
|
|
|
|
# This is the second layer, and it exists for the two cases a source guard
|
|
|
|
|
# cannot reach:
|
|
|
|
|
#
|
|
|
|
|
# - **A dependency logging on our behalf.** Room, WorkManager and the AndroidX
|
|
|
|
|
# libraries log, and a stack trace or a query they print can carry a value
|
|
|
|
|
# that came from a cycle record. The guard cannot see inside a library; R8
|
|
|
|
|
# can remove the call sites.
|
|
|
|
|
# - **A future module.** The guard reads a list of directories. Somebody adding
|
|
|
|
|
# a module and forgetting to list it gets no warning, because absence of a
|
|
|
|
|
# finding looks exactly like a clean result.
|
|
|
|
|
#
|
|
|
|
|
# `assumenosideeffects` lets R8 delete these calls entirely, arguments and all,
|
|
|
|
|
# rather than leaving a stripped string constant in the dex. It is safe here
|
|
|
|
|
# only because nothing in this app uses a Log return value — `Log.d` returns an
|
|
|
|
|
# int nobody reads. If that ever stops being true, R8 will assume it is zero.
|
|
|
|
|
#
|
|
|
|
|
# Warning removed deliberately: `-dontwarn` is not used, because a warning here
|
|
|
|
|
# would mean the class shape changed and this rule stopped matching, which is
|
|
|
|
|
# exactly what we would want to hear about.
|
|
|
|
|
-assumenosideeffects class android.util.Log {
|
|
|
|
|
public static int v(...);
|
|
|
|
|
public static int d(...);
|
|
|
|
|
public static int i(...);
|
|
|
|
|
public static int w(...);
|
|
|
|
|
public static int e(...);
|
|
|
|
|
public static int wtf(...);
|
|
|
|
|
public static int println(...);
|
|
|
|
|
}
|