cmp-audit — interrogate one subsystem until it confesses
Your job: take one subsystem and try to break it on paper — the audit that finds the bugs the lane structurally cannot, because they live below the JVM seam (platform scheduling, notification delivery, process lifecycle) or between clauses the spec never wrote. The output is evidence, not vibes: every finding is a file:line and a concrete failure scenario, survived a refuter pass, and lands in the project's normal change flow.
1. Scope — one subsystem, three bodies of evidence
The user names the subsystem ("the notifications", "reminders", "sync") or you infer it from their words — confirm the inference in one sentence before reading. Then read, completely:
- Its spec clauses — every
specs/*.spec.mdclause that governs the subsystem's behavior (and note what the spec is silent about; silence is where defects live). - Its implementation across ALL source sets —
commonMainANDandroidMain/iosMain. Platform code is where desktop-tier tests cannot see; an audit that reads only common code audits the part that was already testable. - Its tests — which clauses they cite, which source set they run in, and therefore which claims are actually exercised versus merely asserted in prose.
2. Interrogate — the platform-semantics question bank
Ask these against the code you just read. They are questions, not checkboxes: each one is answered with a file:line or with "not applicable because …" — never with a tick.
IDENTITY — do two logical things ever share one platform identity?
- Android
PendingIntentequality is requestCode +filterEquals— extras do not count. Do two logically distinct intents differ only in extras? - Notification identity is tag+id. Can two different logical notifications compute the same pair and overwrite each other?
- Alarm/job/work identity: is the id derived from the domain key, or from something that collides (a constant, an index, a truncated hash, an id that gets reused after delete)?
- If identity is derived from a mutable field, what happens when that field changes — does the old identity leak, unreachable?
- Is there a documented id-space partition, or could two features' ids collide?
LIFECYCLE — what does the platform do to you?
- Reboot: is everything scheduled re-registered on
BOOT_COMPLETED(alarms do not survive reboot)? From what source of truth, and is that source still correct then? - Process death: is any needed state held only in memory when the callback fires?
- App update (
MY_PACKAGE_REPLACED): same question as reboot — who re-arms? - Timezone change / DST transition / date rollover while the app is open: are "tomorrow at 08:00"-style times stored as wall-clock or epoch, and which did the user mean?
- Doze / app standby: does delivery assume the device is awake? Which windows apply?
CANCELLATION — can everything created be found and destroyed?
- For every create path, point at the cancel path. Symmetry is the claim; show it.
- Can a thing be cancelled after the DB row that spawned it is gone — or does cancel recompute an identity from data that no longer exists?
- Does "cancel all" actually enumerate all — including ids generated dynamically since the enumeration was written?
- Re-arming after an edit: is the OLD identity cancelled before the new one is armed, or do both now fire?
DELIVERY — will it actually reach the human, in the state they're in?
- Channels: whose off-switch is it? One channel for everything hands the user a single all-or-nothing switch; is the channel partition per-category as the spec implies?
- Importance vs stream routing: what happens on silent mode and DND? Does the sound come from the stream the use case demands (alarm vs notification)?
- Exactness: which alarm API is used, what lateness window does it permit, and can a "remind me BEFORE X" arrive AFTER X within that window?
- Lock screen: if the feature claims to take over the screen, does it hold the full-screen-intent capability, and what happens when that permission is denied?
- Grouping/rate limits: can a burst collapse or drop the one that mattered?
STATE RE-ASK — does delivery-time code re-check that the thing is still wanted?
- Between scheduling and firing, the world changes. At fire time, is the triggering entity re-read — or does the callback trust its years-old extras?
- Does the re-check cover EVERY kind of trigger, or only the kind that was easy?
- Completion/undo: can a fired-then-completed thing be resurrected by a later re-arm?
PERMISSIONS — which grants gate the path, and what happens ungated?
- List every permission/capability on the path (notifications, exact alarms, full-screen intent, background start). For each: what does the code do when it is denied — degrade loudly, degrade silently, or crash?
- Are grants re-checked at use time, or only at onboarding (the user can revoke any time)?
COVERAGE ARITHMETIC — force the count.
- "The re-check covers N of M" — count the instances. How many trigger kinds / reminder types / entry points exist, and how many does each guard actually cover? A guard that covers 1 of 13 is a finding with a number, not a feeling.
- Same arithmetic for tests: how many of the subsystem's clauses are exercised only from desktop-tier tests (the lane's specCoverage tier line gives the number)?
Settling PLAUSIBLE on the device — when the project ships the instrumented seam,
several of the questions above have a named state-control organ
(androidInstrumentedTest/…/testing/) that turns PLAUSIBLE into CONFIRMED; cite it in
the proposed test:
- "does it survive Doze / which windows apply?" →
DozeControlcomposed withTimeWarp(force idle, warp past the trigger, watch delivery — the flagship exemplar inRuntimeStateSeamTest). - "does a registered alarm actually deliver at T / across a DST transition?" →
TimeWarp+AlarmAsserts. - "what happens when the user denies it?" →
PermissionControl(the fresh-install denied default as a test input; its header documents why granted→denied cannot happen mid-test) +NotificationAsserts.assertNoNotificationfor the silent-drop shape. - "does state survive process death / OS reclaim?" →
ProcessControl(real OS-driven activity destroy + saved-state rebuild; honest about the in-process kill limit). - "offline / one transport down?" →
NetworkControl. Dark mode, font scale, locale →ConfigControl.
3. Discipline — non-negotiable
- Evidence-or-silence. Every finding cites file:line and states the concrete failure scenario as inputs → wrong outcome ("two reminders for the same entity compute requestCode 0 → arming the second cancels the first"). No file:line, no finding.
- Refuter pass before reporting. For each candidate finding, actively try to kill it: is there a guard elsewhere? does the platform actually behave as assumed? is the scenario reachable? Report only survivors, each marked CONFIRMED (reproducible or proven from code) or PLAUSIBLE (needs a device test to settle).
- Convert-or-cut. Each survivor lands as exactly one of: a spec-clause amendment plus a failing-test-first fix proposal, or a named human decision ("is one channel per category the intent? — decide"). No standing prose, no "consider reviewing" residue.
4. Output — findings feed the change flow
Findings enter the project's normal change flow — a feature brief / spec amendment per
docs/CHANGE-FLOW-DESIGN.md — never direct unreviewed fixes to signed artifacts. The
audit's deliverable is the interrogation record: per finding, the category, the file:line,
the scenario, CONFIRMED/PLAUSIBLE, and its convert-or-cut landing.
If the project has the instrumented seam (androidInstrumentedTest), every PLAUSIBLE
platform finding should propose the instrumented test that would settle it — naming the
state-control organ that reaches the state in question (the map at the end of §2). The
seam exists precisely so platform claims stop being unfalsifiable.
5. Record the audit — so cadence stops depending on memory
The last act of an audit is to record that it happened:
node qa/record-audit.mjs <subsystem>
This appends one line to qa/audits.jsonl naming the subsystem and the commit the audit
was run against. That record is what lets the release lane report which subsystems have
changed since their last audit — the nudge that stops this skill depending on someone
remembering to ask for it. The recorder refuses when the subsystem's files are dirty or
HEAD is unknown, because a record whose sha misattests is worse than no record.
Record only what you actually audited. One line per subsystem you interrogated — never a blanket sweep, and never before the findings have landed in §4's change flow.