Consolidated finding register — Cadence audit
Subject: the app repository at pinned commit 03a176e72ef0075eec86b8915cbe6e93042a3b9d (v0.4.12+18).
Every file:line in this table is valid at that commit unless a cell says otherwise.
How to read this table.
- Final severity is the refuter's grade wherever the refuter re-graded. Original severity is what the
stream filed. When the two differ, the verdict cell says
SEVERITY CHANGEDand the reason is in the refuter note. Where a stream has no refutation yet, the verdict cell saysUNREFUTED — verdict provisionaland the final severity is the stream's own, carried forward unverified. - Verdict is one of
CONFIRMED,REFUTED,SEVERITY CHANGED,PARTIALLY REFUTED, orUNREFUTED — verdict provisional. AREFUTEDrow stays in the register on purpose: it records work that has already been done and must not be rediscovered. - Findings contributed by refuters (
MISS-*,S2R-*,S3R-*,R5-F1,S6R-*,S7R-*,S9R-*,M1–M4,R-F1–R-F7,S12R-*,S14R-*) are first-class rows. Their "Original severity" is— (new)because no stream filed them. - Merged rows carry every source ID in the ID cell. Independent convergence — two streams reaching the same defect from different angles — is a strength of the evidence, and the ID cell is where it is visible.
- Reassigned rows. S14's refuter proved S14 audited a scope deleted from the plan before Phase 1
(
S14R-M3). Five S14 findings are reassigned: F6→S10, F7→S6, F8→S9, F9→S9, F11→S7. The Stream column shows the corrected owner with the origin in brackets.
Unrefuted stream: S4 only. agent_reports/S8_refute.md and agent_reports/S13_refute.md landed during
compilation and are fully folded in; proof/01_findings/S4_refute/ exists, so a refutation was in flight, but no
agent_reports/S4_refute.md had landed. 15 rows are therefore provisional, all of them S4's. Where an S4
finding was independently reached by another stream whose refuter did report, the merged row carries that
refuter's verdict and is not provisional.
Severity definitions (AGENT_RULES.md R13)
| Grade | Definition |
|---|---|
| BLOCKER | Prevents store submission, or loses/corrupts a user's data, or fails to ring an alarm |
| HIGH | Wrong behaviour during service |
| MEDIUM | Quality, maintainability, or a degraded path |
| LOW | Cosmetic or stylistic |
| INFO | Recorded non-defect: the stream checked the area and found nothing, or reported a decision rather than a defect |
1. BLOCKER
| ID | Title | Final severity | Original severity | Verdict | File:line (at 03a176e) |
Evidence path | Refuter note |
|---|---|---|---|---|---|---|---|
| S1-F1 | A backward step of the wall clock silences every running timer for the length of the step | BLOCKER | BLOCKER | CONFIRMED | lib/engine/engine.dart:312; deadlines set at :177, :182; consumer lib/ui/home.dart:248 |
findings/S1_engine.md · proof/01_findings/S1/ · proof/01_findings/S1_refute/ |
Reproduced on an independently built clean copy; heartbeat proven monotonic (so the app is awake and silent); backstop proven to run on the same RTC_WAKEUP wall clock, so it does not rescue this. Three corrections: the Evidence block is a stitched composite, not verbatim — replace it with s1_facts_test.dart:92-121 as written; the silence is bounded by the size of the step, not permanent; the proposed fix is defective as written (probe R2b) and must be redesigned before implementation. |
| S2-F2 | The zone→sound migration is NOT idempotent: a second run rewrites every dish's ringtone to Bell |
BLOCKER | BLOCKER | CONFIRMED (strengthened) | lib/engine/store.dart:255-285; false claim at :249-252; mechanism lib/engine/models.dart:56-63 |
findings/S2_persistence.md · proof/01_findings/S2/ · proof/01_findings/S2_refute/ |
Reproduced twice from scratch, once with no write failure at all — the trigger needs no wrong-typed value. One wording correction: _guard (store.dart:174-180) does observe and report the write result; what it does not do is raise it as critical, retry, or let a caller await it. Delete "never verifies" before this reaches the report. |
| S3-F1 + S9-F07 (+ prior A1-1, iOS half) | The OS alarm backstop is dead on iOS: the notification plugin is initialised Android-only and no iOS sound resource is bundled, so the app boots into a permanent red banner and iOS has nothing to play | BLOCKER | BLOCKER (S3) / HIGH (S9) | CONFIRMED — S9-F07 raised HIGH→BLOCKER | lib/alarm_backstop.dart:72-76 (InitializationSettings has no DarwinInitializationSettings); _ready gate :79-96; ios/Runner.xcodeproj/project.pbxproj PBXResourcesBuildPhase block 97C146EC1CF9000F007C117D (no sound resource) |
findings/S3_audio_alarms.md · findings/S9_platform_config.md · agent_reports/S3_refute.md · agent_reports/S9_refute.md |
Found independently by S3 (audio/backstop angle) and S9 (platform-config angle). S3's refuter verified the defect against the plugin source at flutter_local_notifications_plugin.dart:142-147 and by mutation B4. S9's refuter raised its own F07 to BLOCKER and folded S9-F03's blocking condition into it. Also the still-open half of the 2026-07-19 prior audit's A1-1. |
| S3-F2 + S9-F03 | An iOS build cannot ring at all once it leaves the screen: UIBackgroundModes is absent from the whole Info.plist |
BLOCKER | BLOCKER (both) | CONFIRMED — S9-F03's proposed fix REFUTED | ios/Runner/Info.plist:4-69 — key absent from the top-level <dict> (file is 70 lines, not the 63 S3's manifest records) |
findings/S3_audio_alarms.md · findings/S9_platform_config.md · agent_reports/S3_refute.md · agent_reports/S9_refute.md |
Both refuters enumerated the plist keys themselves and confirmed the absence (grep -c UIBackgroundModes → 0; 18 top-level keys). S9's refuter split the finding: the fact holds, but adding UIBackgroundModes does not fix it — the blocking condition belongs to S9-F07 (the merged row above) and that is where the Phase-4 fix goes. Do not implement S9-F03's fix as written. |
| S3-F3 | setStreamVolume failure is swallowed in Kotlin and never verified in Dart, so the 15 % audible floor is a claim the app cannot back |
BLOCKER | BLOCKER | PARTIALLY REFUTED — BLOCKER stands, mechanism refuted | android/app/src/main/kotlin/dev/sergemio/cadence/MainActivity.kt:57-59 |
findings/S3_audio_alarms.md · agent_reports/S3_refute.md §Part 1 · proof/01_findings/S3_refute/ |
The severity survives; the justification does not. The refuter went to AOSP AudioService.java:4495-4504, :3745-3765, :4613-4626, :6724-6752 and proved the cited SecurityException cannot fire at this call site — the app passes flags = 0, and the throw is guarded by wouldToggleZenMode(getNewRingerMode(...)) which needs FLAG_ALLOW_RINGER_MODES. Consequence: fix (1) (reply result.error(...)) is inert; promote fix (2) to the whole remedy. See S3R-F2. |
| S9-F01 | The release build type signs with the debug keystore; both APK and AAB carry CN=Android Debug, which Google Play does not accept |
BLOCKER | BLOCKER | CONFIRMED | android/app/build.gradle.kts:30-36 |
findings/S9_platform_config.md · proof/01_findings/S9_refute/ |
Reproduced on the refuter's own build of both artefacts. Policy source: https://developer.android.com/studio/publish/app-signing. |
| S9-F02 + R-F1 (S11's refuter) | The iOS App Store icon set is the byte-identical Flutter placeholder logo; flutter_launcher_icons was configured to skip iOS |
BLOCKER | BLOCKER (S9) / — (new, S11R) | CONFIRMED and strengthened | pubspec.yaml:35 (ios: false); ios/Runner/Assets.xcassets/AppIcon.appiconset/*.png — all 15, not only Icon-App-1024x1024@1x.png |
findings/S9_platform_config.md · agent_reports/S9_refute.md §BLOCKER 2 · agent_reports/S11_refute.md §7 R-F1 |
Found twice, independently. S9 found it from the platform-config side and graded BLOCKER; S11's refuter found it from the asset-provenance side after discovering S11 had omitted ios/ from its inventory entirely (R-F2), and graded it BLOCKER without knowledge of S9's row. S9's refuter proved all 15 icons are template, not just the 1024. Root cause is a single line: pubspec.yaml:35. |
| S9-F04 | The iOS project has never been configured for a real signing identity, and the required Xcode/SDK toolchain is not installed | BLOCKER | BLOCKER | CONFIRMED | ios/Runner.xcodeproj/project.pbxproj:349, :469, :526 (no DEVELOPMENT_TEAM) |
findings/S9_platform_config.md · agent_reports/S9_refute.md §BLOCKER 4 |
Confirmed, and the 2026-04-28 Xcode-26 requirement date verified verbatim against https://developer.apple.com/news/upcoming-requirements/. |
BLOCKER count: 8 rows. S9's refuter's summary holds — the platform stream's BLOCKER count is unchanged at 4, but one of them changed identity (F03 → F07), and with it the Phase-4 fix.
2. HIGH
| ID | Title | Final severity | Original severity | Verdict | File:line (at 03a176e) |
Evidence path | Refuter note |
|---|---|---|---|---|---|---|---|
| S2-F4 | A timer created after v0.4.11 is forced to Bell if the migration ever re-runs |
HIGH | HIGH | CONFIRMED | lib/engine/store.dart:273-279 |
findings/S2_persistence.md · proof/01_findings/S2_refute/ |
The loop has no legacyZoneId == null guard. Unlike S2-F3 this is reachable through the S2-F2 kill window and needs no wrong-typed value. |
| S2-F5 + S5-F4 | A failed journal write discards the buffered lines permanently and tells nobody; Journal.ready keeps returning true |
HIGH | HIGH (S2) / MEDIUM (S5) | CONFIRMED | lib/journal.dart:163-180, specifically :167-168 (the _buf.clear() before the write) and :175-177 (debugPrint only); ready => _file != null at :53; Settings block stays up at lib/ui/modals.dart:673 |
findings/S2_persistence.md · findings/S5_error_handling.md · agent_reports/S2_refute.md · agent_reports/S5_refute.md |
Found twice. S2 filed the data-loss half at HIGH and reproduced it (probe J1: the two lines logged during the outage are absent, Diag.log=0, Journal.ready=true). S5 filed the reporting half at MEDIUM and its refuter recorded that S5 missed the data loss sitting in the same six lines, and that S5's proposed fix ("stamp !! ECRITURE JOURNAL IMPOSSIBLE into the buffer for the next flush") is not implementable — by the time the handler runs the buffer is already cleared. Take S2's grade and drop S5's fix. |
| S2-F6 + S5-F3 | Journal rotation erases the entire journal when the file cannot be decoded | HIGH | HIGH (both) | CONFIRMED (S2's refuter) / SEVERITY CHANGED to MEDIUM (S5's refuter) — see CONTRADICTIONS.md §5 | lib/journal.dart:192-203 (catch (_) { await f.writeAsString(''); }), triggered from :72 |
findings/S2_persistence.md · findings/S5_error_handling.md · agent_reports/S2_refute.md · agent_reports/S5_refute.md |
Found twice, and the two refuters disagree on severity. S2's refuter reproduced the destruction (probe J3: 3,146,812 bytes → 167 bytes) and kept HIGH; lcov confirms 192,194,195,196,197,199,201 are all uncovered. S5's refuter downgraded to MEDIUM on the ground that the trigger needs the file to first exceed _maxBytes = 3 MiB. Resolved HIGH — S12's refuter reconciled journal growth at 237 KiB/day, so the 3 MiB cap is crossed in 13 days of kiosk uptime, which makes the trigger reachable inside a fortnight. S2 owns journal.dart depth; S5's row is duplicated effort, not a boundary gap. |
| S2-F7 | A dead journal is completely silent: no diagnostic, no banner, and every later event vanishes | HIGH | HIGH | CONFIRMED | lib/journal.dart:108-111 and :133-134 (also :208, :233) |
findings/S2_persistence.md · proof/01_findings/S2_refute/ |
Probes J2/J7 reproduce. |
| S2-F8 | Diag.fail can itself throw, and the throw escapes the catch block that called it |
HIGH | HIGH | CONFIRMED | lib/diagnostics.dart:29 |
findings/S2_persistence.md · proof/01_findings/S2_refute/ |
Independently reproduced (test R8a): escaped=Bad state: toString exploded, Diag.log=0, critical={}. All ten store.dart call sites cited (:38,47,56,120,126,137,142,145,176,178) verified valid. |
| S2-F9 | Corrupt timers plus a wrong-typed seeded flag installs the demo kitchen over a real one | HIGH | HIGH | CONFIRMED (strengthened) | lib/engine/store.dart:300-310 |
findings/S2_persistence.md · proof/01_findings/S2_refute/ |
Reproduced (R9a), and a variant needing no wrong type at all was proven (R9b): JSON-corrupt cadence-timers-v1 plus an absent cadence-seeded-v1, on any post-migration tablet where store.dart:283 already removed the zones key, seeds the seven demo dishes. This finding stands on its own reachable trigger and does not fall with S2-F1. |
| S2R-M1 | A corrupt clones value silently destroys the run entries of every batch in flight | HIGH | — (new) | New (refuter contribution) | lib/engine/store.dart:65 and lib/engine/engine.dart:76-80 |
agent_reports/S2_refute.md §7 |
load decodes the three keys independently, but reconcile() couples them: losing the clones list deletes every run entry belonging to a batch, under a key that was never damaged, with no .corrupt backup taken. Reachable through the ordinary JSON path (row C1) — no wrong type needed. |
| S3-F4 | The TTS engine's "I do not have that language" answer is discarded, so the voice can be silently wrong for a whole service | HIGH | HIGH | CONFIRMED | android/app/src/main/kotlin/dev/sergemio/cadence/MainActivity.kt:90-94 |
findings/S3_audio_alarms.md · agent_reports/S3_refute.md §Part 2 |
The refuter read MainActivity.kt in full, re-derived the eight catch (_: Exception) sites by grep, opened each, and traced every one to its result.* reply and then to the Dart consumer. The "8 of 8, nobody informed" claim holds site by site. |
| S3-F5 | The journal records an announcement as spoken before the native side has had a chance to fail, and the native false is thrown away |
HIGH | HIGH | CONFIRMED | lib/audio/voice.dart:165-168 and :134 |
findings/S3_audio_alarms.md · agent_reports/S3_refute.md |
Both discarded booleans verified at voice.dart:134 and :167 by grep and by eye. S5's refuter independently strengthened the reachability: MainActivity.kt:132-134 wires UtteranceProgressListener.onError/onStop to completeSpeak(id, false) → result.success(false), and speak() at :145 replies false outright when the engine is not ready — all of it discarded after voice.dart:165 has already written parole "…" to the journal. |
| S3-F6 | The backstop reports itself ready when POST_NOTIFICATIONS was refused, and never re-checks the channel afterwards |
HIGH | HIGH | CONFIRMED | lib/alarm_backstop.dart:79-96, specifically _ready = true after a refused permission at :83-90; consumer :64 |
findings/S3_audio_alarms.md · agent_reports/S3_refute.md |
Verified by reading the block. |
| S3-F7 | Exact-alarm revocation on Android 12/12L cancels every backstop, and nothing in the app detects it | HIGH | HIGH | CONFIRMED (two citation corrections) | android/app/src/main/AndroidManifest.xml:13-15; lib/alarm_backstop.dart:80 |
findings/S3_audio_alarms.md · agent_reports/S3_refute.md |
Permissions verified present. Two of S3's sub-line ranges drift: it cites the boot receiver at :48-56 (actual :47-58, class name at :49) and the <queries> TTS_SERVICE action at :75-77 (actual block :68-78, action at :76). The elements are the ones it names. |
| S3-F8 | The alarm level is re-imposed only on the rising edge of a ring, so the volume rocker can mute an alarm that is already ringing | HIGH | HIGH | CONFIRMED | lib/audio/alarm_volume.dart:60-67 (onRunChanged); the only other write path is setLevel :53-56 |
findings/S3_audio_alarms.md · agent_reports/S3_refute.md |
Verified by reading the file in full. sane (:44-45) handles non-finite input correctly. S3's proposed onRepeat hook does not yet exist, so its stated red-then-green test is honest. |
| S3-F9 | lib/audio/audio.dart had 5.26 % line coverage: every path a silent alarm takes was untested |
HIGH | HIGH | CONFIRMED | lib/audio/audio.dart (whole file, 116 lines) |
findings/S3_audio_alarms.md · agent_reports/S3_refute.md §Part 6 |
Reproduced to the digit. S3's 40 new tests lift audio.dart to 97.37 % (37/38), voice.dart to 96.59 %, alarm_backstop.dart to 94.00 %, alarm_volume.dart to 100 %, whole project 71.93 % (1386/1927), suite 163 pass / 0 fail, flutter analyze 0 issues — every number reproduced exactly on the refuter's copy. But see S3R-F1: three of those 40 tests do not detect the behaviour they name, so coverage is not the same as protection. |
| S3-F17 | On iOS there is no audible floor at all, and nothing detects a muted device | HIGH | HIGH | CONFIRMED (Dart half by test; iOS half is static analysis only) | ios/Runner/AppDelegate.swift:54-59; lib/ui/home.dart:209-218 |
findings/S3_audio_alarms.md · agent_reports/S3_refute.md |
The refuter read AppDelegate.swift in full and verified every line reference in the parity table. No Xcode on this machine — the iOS half was never compiled or run, by either agent. Note that S3-F17's own proposed fix raises Diag.fail('volume-device', …, isCritical: true), a scope the operator banner cannot render — see S3R-F3. |
| S3R-F1 | Three of S3's 40 new tests assert a behaviour the whole 163-test suite cannot detect | HIGH | — (new) | New (refuter contribution) | proof/01_findings/S3/tests/s3_audio_test.dart:220-235, :321-332, :237-250; subject code lib/audio/audio.dart:62, :69, :111-115, :44-54 |
agent_reports/S3_refute.md §4 |
Each of the three names a specific behaviour in its title and then asserts something true whether or not that behaviour exists. Phase 4 is expected to land these files as test/audio_test.dart; landed as-is they carry three assertions that can never go red, in the one module whose failure mode is a silent alarm. One of the three is the test S3 cites as the proof of S3-F10. |
| S3R-F2 | The SecurityException justifying BLOCKER S3-F3 cannot fire at that call site, and 1 of its 3 proposed fixes is inert |
HIGH | — (new) | New (refuter contribution) | android/app/src/main/kotlin/dev/sergemio/cadence/MainActivity.kt:57; AOSP AudioService.java:4495-4504, :3745-3765, :4613-4626, :6724-6752 |
agent_reports/S3_refute.md §4 |
The finding S3-F3 survives; its evidence and a third of its remedy do not. Drop fix (1), promote fix (2). |
| S3R-F3 | The operator banner cannot render five of the app's critical scopes, so a critical diagnostic can be raised and shown to nobody — and two of S3's own fixes land in exactly that hole | HIGH | — (new) | New (refuter contribution) | lib/ui/home.dart:670-703 (the _criticalBanner scope map, six startsWith prefixes, no else); live offender lib/engine/store.dart:270 |
agent_reports/S3_refute.md §4 |
Overlaps and strengthens S5-F5, which found the same hole from the error-handling side and counted one scope. S3's refuter enumerated five unrenderable scopes and showed that S3-F3 fix (3) (Diag.fail('volume-set', …, isCritical: true)) and S3-F17's proposed volume-device scope both land in it. Fix the else branch before implementing either. |
| S4-F01 | The ±10 s / ✕ control row overflows its tile, and at dense boards the stop button is clipped away | HIGH | HIGH | UNREFUTED — verdict provisional | lib/ui/tile.dart:600 (the Row), sized by :573-598, hit padding :677-678 |
findings/S4_ui.md · proof/01_findings/S4/ |
No agent_reports/S4_refute.md at time of writing. |
| S4-F02 | A dish name shrinks without any floor: 24 characters render at 7.5 px on a 30-tile board | HIGH | HIGH | UNREFUTED — verdict provisional | lib/ui/tile.dart:393-415 (Flexible → FittedBox(fit: BoxFit.scaleDown) → Text) |
findings/S4_ui.md · proof/01_findings/S4/ |
Cross-reference: S10's refuter (M3) found the complementary defect — the name is silently capped at 24 characters with the counter blanked. |
| S4-F03 | 16 interactive elements are below the 48 dp minimum, including every control used during service | HIGH | HIGH | UNREFUTED — verdict provisional | lib/ui/tile.dart:431-435 (×N chip), :573-598 + :677-678 (± and ✕) |
findings/S4_ui.md · proof/01_findings/S4/ |
Corroborated from another angle by S10-F13, whose refuter re-derived the geometry independently and found S4/S10 both understated it: at tile 127×108 the ±10 s button measures 35 px and the ✕ 31.6 px including hit padding. |
| S4-F04 | The 150 ms heartbeat is gated behind three awaits with no timeout; if one never completes the board freezes silently | HIGH | HIGH | UNREFUTED — verdict provisional | lib/ui/home.dart:136 (await sounds.init()), :148 (await _initSystemVolume()), ticker created at :154 |
findings/S4_ui.md · proof/01_findings/S4/ |
Same code region as R5-F1 (a throw in _boot skips the ticker) and S12-F1 (unmount during the awaits orphans the ticker). Three distinct failure modes, three distinct fixes — do not collapse them. S14's refuter proved the sibling hang in main() by execution (EXIT_CODE=124 at 90 s), which establishes the class. |
| S6R-F1 | An out-of-set tone name reaches assetFor unvalidated and rings a silent alarm |
HIGH | — (new) | New (refuter contribution) | lib/engine/store.dart:274-277 (the v0.4.11 migration), lib/engine/models.dart:71 (fromJson), lib/engine/engine.dart:66 (soundFor), lib/audio/audio.dart:83-84 (assetFor) |
agent_reports/S6_refute.md §Findings S6 missed |
S6's negative result ("Referenced-but-missing WAVs: Zero") is true of C.tones but C.tones is not the reachable input set of assetFor. migrateZoneSounds copies any string out of the legacy zones JSON onto TimerDef.sound with no membership check, and fromJson accepts any string from prefs. Interacts with S3-F19 — see CONTRADICTIONS.md §7. |
| S7-F1 | The 150 ms heartbeat is untested, and its named constant has zero consumers | HIGH | HIGH | CONFIRMED (not individually re-graded) | lib/engine/engine.dart:32 and lib/ui/home.dart:154 |
findings/S7_tests.md · agent_reports/S7_refute.md |
The refuter reproduced grep -rn "Engine.tickMs" test/ → zero references, and re-applied S7's tickMs 150→5000 mutation itself: +123: All tests passed!, EXIT_CODE=0. The dead-constant half is the same defect as S6-F2; the untested-heartbeat half is S7's own and is not duplicated. |
| S7-F2 | Every assertion about the alarm lead is computed from the constant it claims to test | HIGH | HIGH | CONFIRMED | test/engine_test.dart:61, :145, :166, :172; test/robustness_test.dart:291 |
findings/S7_tests.md · agent_reports/S7_refute.md §10 |
Listed by the refuter under "what I did not overturn — verified verbatim at source". S1's refuter independently re-applied the alarmLeadMs → 0 mutation and got +123: All tests passed!. |
| S7-F3 | lib/ui/home.dart (722 lines) has no test, and cannot be time-tested as written |
HIGH | HIGH | CONFIRMED (not individually re-graded) | lib/ui/home.dart:248; file coverage 0.00 % |
findings/S7_tests.md · proof/00_baseline/coverage.txt |
The refuter accepted the clock-injection prerequisite as real: its audit of the home_test.dart specification says the spec is implementable "after the S7-F3 clock injection, which the spec names as a prerequisite". |
| S7-F4 | The backstop's past-deadline guard is not what makes its own test pass | HIGH | HIGH | CONFIRMED | lib/alarm_backstop.dart:181; test test/backstop_test.dart:88-96 |
findings/S7_tests.md · agent_reports/S7_refute.md §10 |
Listed under "what I did not overturn — verified verbatim at source". |
| S7-F5 | No backstop test uses a chained timer, so the final-deadline rule is unprotected | HIGH | HIGH | CONFIRMED and strengthened | lib/alarm_backstop.dart:108-112; test/backstop_test.dart:70-79 |
findings/S7_tests.md · agent_reports/S7_refute.md §10 |
"Strengthened by execution counts S7 did not measure." |
| S7-F6 | Nothing proves the backstop cancels the OS alarm of a timer that was stopped | HIGH | HIGH | CONFIRMED (not individually re-graded) | lib/alarm_backstop.dart:127-129 |
findings/S7_tests.md · agent_reports/S7_refute.md |
S10's refuter independently verified the same cancel path from the product side (:127-128 cancels the armed alarm the moment a run stops matching _desired). |
| S7-F8 | The voice queue's pacing, staleness and cancellation guards all survive mutation | HIGH | HIGH | CONFIRMED and strengthened | lib/audio/voice.dart:177 (300 ms gap), :184 (20 s staleness), :174 (generation guard) |
findings/S7_tests.md · agent_reports/S7_refute.md §10 |
"Strengthened by execution counts S7 did not measure." |
| S7-F13 | The ringtone-asset test cannot detect a filename-case mismatch on macOS | HIGH | HIGH | CONFIRMED | test/i18n_defaults_test.dart:70-78; subject lib/audio/audio.dart:83-84 |
findings/S7_tests.md · agent_reports/S7_refute.md §10 |
Verified verbatim at source. |
| S7-F16 + S4-F13 + S5-F9 | Six lib/ files — 837 of 1,927 instrumented lines, 43.4 % of the codebase — are executed by zero tests |
HIGH | HIGH (S7) / MEDIUM (S4) / MEDIUM (S5) | CONFIRMED and strengthened | lib/main.dart, lib/ui/home.dart (722), lib/ui/tile.dart (819), lib/ui/header.dart (215), lib/ui/theme.dart, lib/ui/logo.dart — all 0.00 % |
findings/S7_tests.md · findings/S4_ui.md · findings/S5_error_handling.md · proof/00_baseline/coverage.txt |
Found three times, from the test, UI and error-handling angles. S7's refuter turned the aggregate into a measured consequence: the six 0 %-coverage files hold 43.4 % of instrumented lines and took 2 of S7's 57 mutations; line-proportional sampling would have placed ~25, and every one of those would have survived by construction. S5's refuter noted S5-F9 partly double-counts a baseline fact and survives only as maintainability. |
| S7R-F4 | lib/ui/modals.dart (300 instrumented lines, 65 % covered) received zero mutations; five of six the refuter placed there survive |
HIGH | — (new) | New (refuter contribution) | lib/ui/modals.dart:217, :303, :428, :436, :523, :524 |
agent_reports/S7_refute.md §5 · proof/01_findings/S7_refute/extension_results.json |
The second-largest instrumented file, the only place a cook edits a timer, reached by two widget-test files — and nothing in the audit said whether its 65 % coverage carries any assertions. It largely does not: the editor's own duration floors and clamps are unprotected, including the 5 s per-step floor and the 0..59 seconds clamp. Those clamps also duplicate Engine.saveDef (modals.dart:520-525 vs engine.dart:368) — R7 territory that S6 missed. |
| S7R-F5 | lib/ui/tile.dart (819 lines, the largest file in the app) received zero mutations; all three the refuter placed survive |
HIGH | — (new) | New (refuter contribution) | lib/ui/tile.dart:183, :200, :498 |
agent_reports/S7_refute.md §5 · proof/01_findings/S7_refute/extension_results.json |
The three probed outputs are the idle duration, the ringing count-up and the chained step count — the single thing a cook looks at across the pass. All three can be broken outright with the suite fully green. |
| S8-F1 + S14R-M2 | The app declares no Flutter localisations, so every framework string is English whatever the app language | HIGH | HIGH (S8) / MEDIUM (S14R) | CONFIRMED at HIGH by S8's refuter | lib/main.dart:43-56 — the MaterialApp passes no localizationsDelegates, no supportedLocales, no locale; flutter_localizations absent from pubspec.yaml and pubspec.lock |
findings/S8_i18n.md · agent_reports/S8_refute.md · agent_reports/S14_refute.md §3 S14R-M2 |
Found twice, independently, and settled. S8 filed it at HIGH; S14's refuter found the identical defect at the identical lines from the app-entry side and graded it MEDIUM, with the mechanism spelled out: Flutter installs DefaultMaterialLocalizations, whose locale is fixed at en_US. S8's own refuter, working without knowledge of S14's, confirmed HIGH — that is the governing grade. See CONTRADICTIONS.md §6. Related but distinct: S9R-M03 (the iOS bundle declares English only). |
| S8-F2 | The generated spoken announcement is grammatically wrong in both languages, and v0.4.7 replaced correct English with it | HIGH | HIGH | CONFIRMED, including the regression, against git history | lib/i18n.dart:148-150; migration at lib/engine/store.dart:187-231 |
findings/S8_i18n.md · agent_reports/S8_refute.md §1 |
The refuter re-derived the v0.4.7 English regression against git history, not against the finding's prose. Interacts with S2-F10 (repairGeneratedPhrases deletes the operator's own sentence if it re-runs) — same migration, confirmed by S2's refuter — and with S8-R2, which shows the operator is shown the ungrammatical phrase live in the editor, not merely told it. |
| S8-F3 | A French first launch seeds an English board and speaks English dish names in French sentences | HIGH | HIGH | CONFIRMED | lib/engine/store.dart:326-343 |
findings/S8_i18n.md · agent_reports/S8_refute.md §1 |
Confirmed by S8's refuter, and the seed block is independently verified verbatim by S10's refuter for a different purpose (store.dart:296, 326-343, including const oven = 'Cascade', fryer = 'Chirp'). |
| S13-R2 (S13's refuter) | S13-F5 and S13-F2 compound into silent loss of the only configuration backup: roughly nine journal exports blow Android's 25 MB Auto Backup quota and cloud backup stops, unannounced | HIGH | — (new) | New (refuter contribution) | lib/journal.dart:25-26 (_maxBytes = 3 MB, _keepBytes = 1 MB), :221-231 (the export copy) |
agent_reports/S13_refute.md §3 |
Three facts each present in S13's report and never multiplied: (a) every export writes a new dated copy into getExternalStorageDirectory() and nothing deletes any of them; (b) that directory is Context.getExternalFilesDir(null), which Android's Auto Backup documentation lists among the directories it backs up by default; (c) Auto Backup is capped at 25 MB per app, past which onQuotaExceeded() fires and the system stops backing the app up entirely. Each rotated copy is 1–3 MB. A pilot kitchen exporting daily crosses the quota inside a fortnight — and this is the evidence that settles the S10-F1 / S13-F4 severity dispute (CONTRADICTIONS.md §6): the platform default S10's refuter relied on is not dependable. |
| S9-F05 | USE_FULL_SCREEN_INTENT is declared and used, but the app never requests it at runtime and has no degraded path if Play declines the auto-grant |
HIGH | HIGH | CONFIRMED | android/app/src/main/AndroidManifest.xml:16 and lib/alarm_backstop.dart:53 |
findings/S9_platform_config.md · agent_reports/S9_refute.md §Part 4 · store_readiness/01_google_play.md §5.2 |
Confirmed. The eligibility question is disputed between the Play-readiness agent and S9's refuter — both point to the same fix, so the dispute does not change what Phase 4 does. See CONTRADICTIONS.md §2. |
| S9-F06 | MODIFY_AUDIO_SETTINGS is declared but ACCESS_NOTIFICATION_POLICY is not, so the alarm-volume write throws under Do Not Disturb and the exception is swallowed |
HIGH | HIGH | CONFIRMED | android/app/src/main/AndroidManifest.xml:8 declared, ACCESS_NOTIFICATION_POLICY absent from lines 2-17; consumed at MainActivity.kt:57 |
findings/S9_platform_config.md · agent_reports/S9_refute.md §Part 4 |
Confirmed by S9's refuter against https://developer.android.com/reference/android/media/AudioManager. Read alongside S3R-F2, which proves the SecurityException from setStreamVolume with flags = 0 cannot fire — the two findings name different throw paths and Phase 4 must not conflate them. |
| S10-F2 | Every destructive action in the product is one unconfirmed tap with no undo | HIGH | BLOCKER | SEVERITY CHANGED (BLOCKER → HIGH); sub-claim REFUTED | lib/ui/tile.dart:618-625 (✕), lib/ui/modals.dart:348-351 (🗑); the HIGH is carried by lib/engine/engine.dart:378-380 (Save kills a live run and every batch of it) |
findings/S10_product.md · agent_reports/S10_refute.md §1, §3 |
"No confirmation, no undo" is CONFIRMED and total: grep -rniE "\bundo\b\|AlertDialog\|showDialog.*confirm" lib/ → exit 1, no output; grep -rn "onLongPress\|onDoubleTap" lib/ → exit 1, no output. "Four one-tap gestures" is REFUTED — only one of the four is one tap, and a different one touches the menu. A missing confirmation is none of R13's three BLOCKER conditions. |
| S10-F3 | In edit mode, tapping a ringing tile opens the editor instead of silencing the alarm | HIGH | HIGH | CONFIRMED | lib/ui/home.dart:349-353 (S10 cited :348-353), tested before r.status == RunStatus.ringing at :365; lib/ui/tile.dart:569-570 renders the ±/✕ row only for running/paused |
findings/S10_product.md · agent_reports/S10_refute.md |
editing defaults false (home.dart:40), is not persisted, and nothing auto-exits it — so the tile carries no escape while ringing. Severity stands. |
| S10-F14 | What the operator experiences in each service failure, assuming the OS backstop does not ring | HIGH | HIGH | CONFIRMED, incomplete | lib/alarm_backstop.dart:99-116, :122-149, :177-221, :238-248; lib/ui/home.dart:174-204 |
findings/S10_product.md · agent_reports/S10_refute.md |
Every row of the table checks out against the code, including the Wi-Fi row (no INTERNET permission in the manifest). Incomplete on one row: the _desired filter at :104 also excludes paused, and the table has no paused row — that gap is M1 below. |
| S10-F1 + S13-F4 | A restaurant's entire configuration exists in one copy, on one tablet, with no in-app export and no import | HIGH | BLOCKER (S10) / HIGH (S13) | S10's BLOCKER SEVERITY CHANGED and its absolute claim REFUTED; S13's HIGH CONFIRMED by S13's own refuter — HIGH governs | lib/engine/store.dart:15-24 (the only persistence keys); lib/journal.dart:207 (the only export in lib/, and it exports the flight recorder, not the configuration); Settings surface lib/ui/modals.dart:616-693 |
findings/S10_product.md · findings/S13_data.md · agent_reports/S10_refute.md §1 · agent_reports/S13_refute.md · research/01_prior_work.md A1-4 / A1-R5 |
Found three times — S10, S13, and the 2026-07-19 prior audit (A1-4 and A1-R5, both recorded STILL OPEN) — and the two refuters disagree. S10's refuter downgraded to MEDIUM because Android Auto Backup supplies backup, restore and second-device transfer by platform default for exactly the cadence-timers-v1 key. S13's refuter confirmed HIGH. Resolved HIGH: S13-R2 measured that the platform default S10's refuter relied on stops silently once roughly nine journal exports exhaust the 25 MB per-app quota, so it cannot be treated as the restaurant's backup. BLOCKER remains wrong — a missing export is none of R13's three conditions. Full resolution in CONTRADICTIONS.md §6. |
| M1 (S10's refuter) | One accidental tap on the biggest target on the board converts a cooking dish into one that will never ring, and cancels its OS safety net | HIGH | — (new) | New (refuter contribution) | lib/ui/home.dart:387-399, lib/engine/engine.dart:243-251, lib/alarm_backstop.dart:104-106 and :127-128 |
agent_reports/S10_refute.md §6 |
A single tap on a running tile pauses it. The tile is the largest touch target on the board and the same target the product asks the cook to slap to silence an alarm. pauseTimer sets endsAt = null, so the engine will never fire it, and _desired then drops it so the scheduled OS alarm is cancelled too. Both layers of protection removed by one accidental tap. |
| S11-F1 | The shipped app contains three OFL-licensed font families with no copyright notice and no licence text | HIGH | BLOCKER | SEVERITY CHANGED (BLOCKER → HIGH); REFUTED as stated | pubspec.yaml:48-69 (seven fonts declared); repository root (no licence file) |
findings/S11_asset_licensing.md · agent_reports/S11_refute.md §1 |
The factual predicate is wrong. All seven binaries ship the copyright notice in name ID 0/13, and DSEG7Classic-Bold.ttf ships the complete OFL 1.1 text inside name ID 13, byte-identical to the licensor's published copy. The real defect covers six files, not three families. Termination is further contested by the licensor's own FAQ 1.10. The release APK shipping no font notice is separately CONFIRMED, and LicenseRegistry/showLicensePage was proven from Flutter SDK source to surface nothing. Rewrite the finding before implementing. |
| S12-F9 | After a reboot nothing brings the board back; only the OS notifications survive | HIGH | HIGH | CONFIRMED | android/app/src/main/AndroidManifest.xml:22-43 — the only activity, MAIN/LAUNCHER-only intent filter |
findings/S12_lifecycle.md · agent_reports/S12_refute.md §6 |
Confirmed from the merged manifest read in full. One of the three S12 findings confirmed as written. |
| S12R-F3 | S12's own Backstop.dispose() fix leaves the OS safety net on a stale deadline |
HIGH (conditional: only if Phase 4 adopts fix_announce_timer_leak.patch as written) |
— (new) | New (refuter contribution) | The patch's second hunk, against lib/alarm_backstop.dart:266; the policy it contradicts is at lib/alarm_backstop.dart:240-242 |
agent_reports/S12_refute.md §2.1 · proof/01_findings/S12_refute/r6_backstop_dispose_WITH_s12fix.txt |
Measured both ways. Unpatched, a teardown inside the 300 ms debounce window leaves the OS holding the new deadline — correct. With S12's patch applied it holds the stale one, ten seconds later than the cook asked for. That is the BLOCKER class of failure traded for a duplicate notification that was never wrong. The correct dispose() is _flushSchedules(); _debounce?.cancel(); _debounce = null; _pending = null; — the same policy the class already applies at onBackground(). |
| S13-F1 | The exported journal carries operator free text and a device description off the device, unredacted | HIGH | HIGH | CONFIRMED | lib/journal.dart:205-237 (export) and lib/ui/modals.dart:696-717 (share) |
findings/S13_data.md · agent_reports/S13_refute.md |
Confirmed by S13's refuter, and the export path is independently verified by S5's refuter for a different defect (modals.dart:698-717 and journal.dart:233-236 verbatim-exact). Related: S10-F12 (the journal is the operator's only record and it is French-only). |
| S13-F5 | The journal writes an unmanaged second copy to external storage that nothing ever deletes | HIGH | MEDIUM | SEVERITY CHANGED (MEDIUM → HIGH, raised) — "confirmed, and under-stated" | lib/journal.dart:228-231 |
findings/S13_data.md · agent_reports/S13_refute.md §3 S13-R2 |
Raised by its own refuter. The unmanaged copies are not merely clutter: they sit in a directory Android backs up by default, each is 1–3 MB, and roughly nine of them exhaust the 25 MB per-app Auto Backup quota, at which point cloud backup stops silently for the whole app (S13-R2). The surrounding block (:226, :231) is independently verified verbatim by S5's refuter for S5-F10. |
| S13-F2 + S9-F10 + M4 (S10's refuter) | Android auto-backup is on by default, so the journal, the kitchen's entire menu and every preference are copied into the operator's Google account and restored onto any device | HIGH | HIGH (S13) / MEDIUM (S9) / — (new, S10R) | Facts CONFIRMED by three sources; S13's store-declaration rationale REFUTED; severity HIGH per S13's refuter | android/app/src/main/AndroidManifest.xml:18-21 — the <application> element (S13 cited :20-23; :22-23 are <activity / android:name=".MainActivity") declares only label, name and icon; no android:allowBackup, no android:dataExtractionRules, no android:fullBackupContent |
findings/S13_data.md · findings/S9_platform_config.md · agent_reports/S13_refute.md · agent_reports/S10_refute.md §6 M4 · agent_reports/S9_refute.md |
Found three times, from the data-privacy, platform-config and product sides, and refuted twice. S9's refuter proved the absence from its own merged manifest (0 occurrences) and confirmed MEDIUM for the config-gap framing; S13's refuter confirmed HIGH for the data-exposure framing. Take HIGH — the two grades are the same omission read as a configuration gap and as a privacy exposure, and the exposure framing is the one with the measured consequence (S13-R2). S13-R3 REFUTES S13's reasoning: Google's Data Safety guidance defines "collect" as transmitting data off a user's device by the developer, and explicitly exempts a user's own backup to their own cloud account. Following S13-F2's rationale would produce a false store filing on both stores. State the fact; do not declare it as collection. |
| S14-F1 | A failure inside Store.open() kills the app before runApp: the tablet shows a blank window forever, nothing logged, nothing on screen |
HIGH | HIGH | CONFIRMED and strengthened | lib/main.dart:22-35, specifically :24 |
findings/S14_entry_unowned.md · agent_reports/S14_refute.md §2.1 |
Test T1 re-run green; realism established from the Android plugin's Kotlin source; the blank-window claim verified against the Flutter engine's own FlutterActivity.java, i.e. reachable without mocking. |
| S14-F2 | Both awaited startup calls can hang forever with no timeout and no watchdog, producing the same blank window with no exception to catch | HIGH | HIGH | CONFIRMED — upgraded from inference to execution | lib/main.dart:24-25 |
findings/S14_entry_unowned.md · agent_reports/S14_refute.md §2.2 |
S14 argued from absence. The refuter executed it: flutter test killed at 90 s wall clock, EXIT_CODE=124. |
HIGH count: 45 rows.
3. MEDIUM
| ID | Title | Final severity | Original severity | Verdict | File:line (at 03a176e) |
Evidence path | Refuter note |
|---|---|---|---|---|---|---|---|
| S1-F2 + S5-F1 | A throwing host callback makes tick() delete the timer it has just set to ringing, and tells nobody |
MEDIUM | HIGH (both) | SEVERITY CHANGED (HIGH → MEDIUM), by both refuters independently | lib/engine/engine.dart:338-343, try at :305, reached from :286 and :328 |
findings/S1_engine.md · findings/S5_error_handling.md · agent_reports/S1_refute.md · agent_reports/S5_refute.md |
Found twice, downgraded twice, on the same ground. Both refuters enumerated every synchronous statement in the four host callbacks reachable from tick() (home.dart:252-258, :284-303, :306-312, :315-334) and proved none can throw synchronously with today's host: Journal.log is guarded, Store.saveRun → _write is all-catching, backstop.sync and the sound calls are async. Defence-in-depth, not service behaviour. Two implementation warnings: (a) test/robustness_test.dart:149 is a pre-existing repo test that enshrines this swallow-and-drop as desired behaviour — any fix must deal with it; (b) S5's proposed EngineHost.onEngineFault needs a concrete empty default body, because an abstract member breaks every implementer in lib/ and in test/. |
| S1-F3 | tick()'s own recovery path can throw, abandoning every timer after the failing one |
MEDIUM | HIGH | SEVERITY CHANGED (HIGH → MEDIUM) | lib/engine/engine.dart:342 (the unguarded re-invocation); in-try calls at :285, :327 |
findings/S1_engine.md · agent_reports/S1_refute.md |
Probe B2 reproduces and B2b measures the blast radius. Same reachability ceiling as S1-F2: persistRun has three synchronous leaves and all three are internally guarded or async. Latent robustness defect. |
| S1-F4 | saveDef silently destroys the duration of a chain reduced to one step, and the suite locks the loss in |
MEDIUM | MEDIUM | CONFIRMED | lib/engine/engine.dart:366-374 (S1 cited :370-374); UI gate lib/ui/modals.dart:505 |
findings/S1_engine.md · agent_reports/S1_refute.md |
Probe reproduces durationSec == 5 from a 5,400 s step. Mutation MUT-1 re-applied independently: the suite goes red with exactly one failing test, test/robustness_test.dart "saveDef floors (audit F7)…" — so the suite really does enforce the loss and that test must change with the fix. |
| S1-F5 | A retired lot number is handed out a second time, contradicting the model's own contract | MEDIUM | MEDIUM | CONFIRMED | lib/engine/engine.dart:156-159; contract at lib/engine/models.dart:147-149 |
findings/S1_engine.md · agent_reports/S1_refute.md |
Probe spawns and stops four times and measures ['p [lot 2]','p [lot 2]','p [lot 2]','p [lot 2]']. |
| S1-F6 | reconcile() repairs the persisted state and writes none of the repairs back |
MEDIUM | MEDIUM | CONFIRMED | lib/engine/engine.dart:74-100 |
findings/S1_engine.md · agent_reports/S1_refute.md |
Read in full: no persistDefs/persistRun/persistClones anywhere in the method. Probe measures all three counters at 0 after a reconcile() that dropped a zombie run entry and renumbered a clone. |
| S1-F7 | TimerDef.fromJson throws on a bad id, and every ?? fallback next to it is null-only |
MEDIUM | MEDIUM | CONFIRMED | lib/engine/models.dart:66, and :22, :67, :68, :71, :72, :75, :134, :139, :156-158 |
findings/S1_engine.md · agent_reports/S1_refute.md |
Ten wrong-type inputs all throw, reproduced. models.dart:66 and CloneRef's :156-157 are the only unguarded casts; RunEntry.fromJson's status (:131-132) is the sole read that silently defaults instead of throwing. |
| S1-F8 | Nothing bounds a duration: durationSec * 1000 overflows into a deadline in the past |
MEDIUM | MEDIUM | CONFIRMED | lib/engine/engine.dart:177, :182, :232, :372-374, :386, :396 |
findings/S1_engine.md · agent_reports/S1_refute.md |
Reproduces endsAt < now and an immediate ring for durationSec = 9223372036854776. The reachable arm is the one S1 names: models.dart:68 defaults a missing durationSec to 0, and 0/negative both ring on the first tick. |
| S1-F9 | A ringing entry with no nextVoiceAt passes reconcile() and is then silent forever |
MEDIUM | MEDIUM | CONFIRMED | lib/engine/engine.dart:81-91 (the invariant block) and :333-335 |
findings/S1_engine.md · agent_reports/S1_refute.md |
:84 checks running→endsAt, :85 checks paused→remainingMs, and nothing checks ringing. Probe runs 1,000 beats and measures zero repeats. |
| S1-F10 | The 1,200 ms alarm lead can be changed to 0 and the whole suite stays green | MEDIUM | MEDIUM | CONFIRMED | lib/engine/engine.dart:50, documented at :34-49 |
findings/S1_engine.md · proof/01_findings/S1_refute/remut_MUT6_alarmlead_zero.txt |
Mutation re-applied independently: +123: All tests passed!, EXIT_CODE=0. Five test sites reference alarmLeadMs, all of the form … - Engine.alarmLeadMs — the expectations move with the code. |
| MISS-1 (S1's refuter) | reconcile() validates chain→def but never def→chain, so a chain that loses its flag rings at the end of step 1 |
MEDIUM | — (new) | New (refuter contribution) | lib/engine/engine.dart:86-89; reachability hinge at lib/engine/models.dart:133 |
agent_reports/S1_refute.md §5 |
The structural invariant enforces the implication in one direction only. tick() then takes the single-timer branch at :330-331 and calls _fireAlarm at the end of step 1's duration: a twenty-one-minute dish rings after sixty seconds, with no step chime. |
| MISS-2 (S1's refuter) | remainingMs is clamped on every live path and on none of the restore paths |
MEDIUM | — (new) | New (refuter contribution) | lib/engine/engine.dart:85 (the invariant), :258 (the consumer); live clamps at :235, :247; hinge at lib/engine/models.dart:136 |
agent_reports/S1_refute.md §5 |
reconcile() accepts a paused entry whenever remainingMs is non-null and never looks at its sign; resumeTimer then computes a deadline before the resume instant and the dish rings on the next beat. |
| S2-F1 | A wrong-TYPE stored value destroys the entire kitchen configuration silently: no backup, no banner, and the next save makes it permanent | MEDIUM | BLOCKER | SEVERITY CHANGED (BLOCKER → MEDIUM) | lib/engine/store.dart:34-41 and :93 |
findings/S2_persistence.md · agent_reports/S2_refute.md §2 |
Code and behaviour verified independently (timers=0, backup=null, banner={}, then saveDefs writes []). But the trigger is not production-reachable — nothing in the app writes a wrong-typed value to those keys. S2-F3 falls with it; S2-F9 does not, because its refuter found a reachable trigger needing no wrong type. |
| S2-F3 | A wrong-TYPE cadence-zones-v1 marks the migration done without running it and then deletes the legacy tones |
MEDIUM | HIGH | SEVERITY CHANGED (HIGH → MEDIUM) | lib/engine/store.dart:256-285; contradicted invariant at :260-261 |
findings/S2_persistence.md · agent_reports/S2_refute.md |
Mechanism confirmed and the probe reproduces (flag=true, zonesKeyStillThere=false). Same unreachable wrong-type trigger class as S2-F1, so the severity moves with it. |
| S2-F10 | repairGeneratedPhrases deletes the operator's own sentence if it re-runs |
MEDIUM | MEDIUM | CONFIRMED (wording correction) | lib/engine/store.dart:219-231, flag at :229, doc comment at :215-216 |
findings/S2_persistence.md · agent_reports/S2_refute.md |
Same "_guard never verifies" wording defect as S2-F2 — correct it. Interacts with S8-F2 (the generated phrase this migration writes is grammatically wrong in both languages). |
| S2-F11 | A zone whose sound field has the wrong type is skipped in silence and its dishes fall back to Bell |
MEDIUM | MEDIUM | CONFIRMED | lib/engine/store.dart:264-268, specifically :267 (if (id is String && s is String) tones[id] = s; with no else) |
findings/S2_persistence.md · agent_reports/S2_refute.md |
Probe reproduces (diagScopes=[]). |
| S2-F12 + S13-F6 | The .corrupt preservation is write-only: nothing reads it, exports it, or restores from it; a second corruption is discarded; the siblings never expire and cannot be cleared |
MEDIUM | MEDIUM (both) | CONFIRMED | lib/engine/store.dart:119-128; the only reader is :122 |
findings/S2_persistence.md · findings/S13_data.md · agent_reports/S2_refute.md |
Found twice — S2 from the persistence side, S13 from the data-retention side. S2's refuter re-ran the whole-tree grep and got the same three hits, all prose in comments (i18n.dart:141, engine/engine.dart:70, audio/alarm_volume.dart:42). S2R-M4 makes it worse: the .corrupt write itself is unverified and non-critical. |
| S2-F13 | Every uncovered line in store.dart is a failure handler, and five of them are the write-failure paths |
MEDIUM | MEDIUM | CONFIRMED | lib/engine/store.dart:133-147 and :174-180; the README claim at README.md:14 |
findings/S2_persistence.md · agent_reports/S2_refute.md |
lcov reproduced exactly on a pristine copy: store.dart 118/125 = 94.40 %, uncovered [47,126,137,142,145,176,178]; journal.dart 85/104 = 81.73 %; diagnostics.dart 19/22 = 86.36 %, uncovered [15,16,20]; total 858/1927 = 44.53 %. The refuter executed line 137 for the first time and the branch behaves as designed. |
| S2-F14 | Deleting the migration's persistence step passes all 123 existing tests (mutation S2-M1) | MEDIUM | MEDIUM | CONFIRMED — reproduced independently | lib/engine/store.dart:280 |
findings/S2_persistence.md · agent_reports/S2_refute.md §4 |
Reproduced from scratch. |
| S2-F15 | The journal header cannot distinguish two builds of the same version, which is the exact failure this commit set out to fix | MEDIUM | MEDIUM | CONFIRMED | lib/journal.dart:83, sourced from lib/main.dart:20 (const String kAppVersion = '0.4.12';); pubspec.yaml:5 is 0.4.12+18 |
findings/S2_persistence.md · agent_reports/S2_refute.md |
version_test.dart compares .split('+').first and its own comment states the omission verbatim. |
| S2-F16 | Rotation is checked only at init, so a kiosk session that never restarts passes the 3 MiB cap |
MEDIUM | MEDIUM | CONFIRMED verdict / figures REFUTED twice | lib/journal.dart:72; cap _maxBytes = 3 * 1024 * 1024 = 3,145,728 at :25 |
findings/S2_persistence.md · agent_reports/S2_refute.md §5 · agent_reports/S12_refute.md §1 |
The cap exists, is checked only at init, and the MEDIUM grade holds. S2's "34.1 days to the cap" is wrong. S2's refuter re-measured the byte sizes and got 20.7 days; S12's refuter reconciled the whole model against production emitters and got 13.0 days at 242,270 bytes/day. Replace every digit S2 published except the 167-byte session header and the 3,145,728-byte cap. Full reconciliation in CONTRADICTIONS.md §3. |
| S2-F17 | Diag.log and DiagEntry.toString are dead in production: the in-RAM diagnostics the header advertises are read only by tests |
MEDIUM | MEDIUM | CONFIRMED | lib/diagnostics.dart:9-22 |
findings/S2_persistence.md · agent_reports/S2_refute.md |
Independent greps: Diag.log has zero readers in lib/, two in test/ (store_test.dart:104, journal_test.dart:45); DiagEntry appears only inside diagnostics.dart; lcov shows 15,16,20 uncovered. |
| S2-F18 | One failure report copies an unbounded error string into both the 50-entry buffer and the on-disk journal | MEDIUM | MEDIUM | CONFIRMED | lib/diagnostics.dart:29 and :37 |
findings/S2_persistence.md · agent_reports/S2_refute.md |
Independently reproduced: in-RAM entry length 200,000. |
| S2-C2 | An unknown or absent run status is silently coerced to running |
MEDIUM | MEDIUM | CONFIRMED with a citation correction | lib/engine/models.dart:132 — not :130-131 as S2 cited; the orElse: clause is on line 132. Reached from lib/engine/store.dart:73 |
findings/S2_persistence.md · agent_reports/S2_refute.md |
The coercion is real. Second correction: the outcome is not always "not dropped" — see the refuter's blast-radius row B6. S1's refuter independently confirmed :131-132 is the sole read that silently defaults. |
| S2R-M2 | markCleanExit is fire-and-forget, so a clean shutdown reports itself as an OS kill |
MEDIUM | — (new) | New (refuter contribution) | lib/journal.dart:186-190, called without await from lib/ui/home.dart:202 |
agent_reports/S2_refute.md §7 |
The death detector's whole value is that !clean && lastBeat != null (journal.dart:85) means the OS killed us. markCleanExit awaits a flush: true write before writing the flag, and is called unawaited from the AppLifecycleState.detached callback — the last moment before the process dies. Every death inside that window prints !! SESSION PRECEDENTE TUEE on the next boot. |
| S2R-M3 | A load- critical banner can never be cleared for the life of the session |
MEDIUM | — (new) | New (refuter contribution) | lib/diagnostics.dart:41-46 and its six call sites; raised at lib/engine/store.dart:120 |
agent_reports/S2_refute.md §7 |
Diag.clearCritical is called for backstop-init, backstop-schedule, backstop-exact, voice-init, audio-play and save-$key — never for a load- scope. Store.load runs once at home.dart:75, so nothing can retract it. S2's own probe measured this and filed no finding. |
| S2R-M4 | The .corrupt backup write is itself unverified and non-critical, so the "never silently lost" guarantee can fail silently |
MEDIUM | — (new) | New (refuter contribution) | lib/engine/store.dart:123; the guarantee is stated at :115-118; the guard's own doc is at :173 |
agent_reports/S2_refute.md §7 |
The write that delivers the guarantee uses the settings guard, documented one screen away as "comfort data — failure is logged, not critical". If it is refused, the only copy of the kitchen's configuration is gone and the failure is reported at the same level as a language preference. |
| S3-F10 + S10-F5 | All timers share one ringtone player: a ringing tone cannot be stopped, and two dishes ringing at once cannot be told apart | MEDIUM | MEDIUM (S3) / HIGH (S10) | SEVERITY CHANGED (S10 HIGH → MEDIUM); S10's sub-claim REFUTED | lib/audio/audio.dart:19 (AudioPlayer? _ring), :62, :71 (await p.stop() before :73 play), :86-87 |
findings/S3_audio_alarms.md · findings/S10_product.md · agent_reports/S10_refute.md · agent_reports/S3_refute.md |
Found twice — S3 from the audio side, S10 from the product side. S10's refuter refuted the consequence: the two runs carry independent repeat schedules (engine.dart:283-284, :293-294, each keyed to its own RunEntry.voiceGap/nextVoiceAt), so the tones alternate, each truncated at the other's next repeat — degradation, not the total loss described. The voice channel also names each dish independently (home.dart:294-295). And the test S3 cites as proof of S3-F10 does not test it (S3R-F1). |
| S3-F11 | Haptics: failures escape as unhandled async errors, and _canVibrate does not mean "has a vibrator" |
MEDIUM | MEDIUM | CONFIRMED | lib/audio/audio.dart:49-53, :96-101, :105-109 — Vibration.vibrate unawaited and uncaught at :99 and :107 |
findings/S3_audio_alarms.md · agent_reports/S3_refute.md |
Confirmed by reading the block. Overlaps S5-F8's unawaited-futures inventory, which cites the same two lines. |
| S3-F12 | A wedged TTS engine costs 12 s of silence per announcement and raises no operator banner | MEDIUM | MEDIUM | CONFIRMED | lib/audio/voice.dart:161-173; the 12 s timeout at :168, the non-critical voice-speak at :170 |
findings/S3_audio_alarms.md · agent_reports/S3_refute.md |
Verified. Distinct from S12-F6, which is the native-side channel-result leak on the same wedge. |
| S3-F13 | The speech queue is unbounded and its only trim path cannot be tested | MEDIUM | MEDIUM | CONFIRMED by S3's refuter; bound measured by S12 — see CONTRADICTIONS.md §8 | lib/audio/voice.dart:26 (_queue), :36 (staleMs as a static const), :180-188 (_dropStale) |
findings/S3_audio_alarms.md · agent_reports/S3_refute.md · findings/S12_lifecycle.md §3.4 |
S3's refuter verified the unbounded _queue and the untestable static const staleMs. S12 measured 5,000 pending entries after 5,000 enqueues with a dead engine and concluded bounded, not a leak — because in production the bound is maxBatch × repeats ≈ 210 entries per 20-second window. Both are right about different things: the queue enforces no bound of its own; the engine's maxBatch supplies one. |
| S3-F14 + S6-F18 (title half) | A background notification cannot tell the cook which batch is ready: _desired uses engine.labelFor but showNow uses raw t.name, and _nid is per-clone |
MEDIUM | MEDIUM (S3) / LOW (S6, as part of a zero-padding finding) | CONFIRMED; S6's LOW grade explicitly rejected by its refuter | lib/alarm_backstop.dart:113 (engine.labelFor(t.id)) vs :259 (raw t.name); consumer lib/ui/home.dart:300; also :186 |
findings/S3_audio_alarms.md · findings/S6_dry_deadcode.md · agent_reports/S6_refute.md |
Found twice. S6's refuter: "a backgrounded rush stacks three notifications all reading ⏰ Fries. That is a behavioural defect, not a duplication, and it does not belong at LOW inside a zero-padding finding." Take S3's MEDIUM. |
| S3-F15 + S12-F5 | SoundBox never releases its five AudioPlayers; SoundBox and VoiceBox have no disposal at all |
MEDIUM | MEDIUM (both) | PARTIALLY REFUTED — classification corrected | lib/audio/audio.dart:10-116 (whole class), players created at :44-54; lib/ui/home.dart:235-243 (dispose, which does not release them) |
findings/S3_audio_alarms.md · findings/S12_lifecycle.md · agent_reports/S12_refute.md §2 |
Found twice. S12's refuter reproduced it (AudioPlayer create calls during boot: 5 / calls after unmount: []) and refuted the "LEAK" label: SoundBox.init() is called once, from _boot(); five players exist for the process lifetime and never grow. That is a missing dispose, not an accumulation, and S12's "6 undisposed on at least one reachable path" overstates it. |
| S3-F16 | iOS speak can complete the wrong utterance's result, breaking one-announcement-at-a-time |
MEDIUM | MEDIUM (STATIC ANALYSIS ONLY — never compiled or run) | CONFIRMED (static analysis only) | ios/Runner/AppDelegate.swift:157-175 and :186-192 |
findings/S3_audio_alarms.md · agent_reports/S3_refute.md §Part 5 |
The refuter re-derived the ordering argument from the source and confirmed it, and separately discarded a pendingSpeaks key-collision hazard as unreachable. No Xcode on this machine — neither agent compiled or ran this. |
| S3R-F4 | S3's four mutation proofs do not meet R8 as written | MEDIUM | — (new) | New (refuter contribution, R8 process defect) | proof/01_findings/S3/mutations/M1_volume_floor.after.txt, M2_backstop_chain.after.txt, M3_audio_critical.after.txt, M4_voice_timeout.after.txt |
agent_reports/S3_refute.md §4 |
R8(c) and R8(d) are met; (a) and (b) are not — none of the four runs used --reporter=json, and all four were scoped with --plain-name to a single file, so the failing set was never compared against the whole suite. See EVIDENCE_INTEGRITY.md. |
| S3R-F5 | R8(b) as written can never be satisfied by a testWidgets test, which puts 7 of S3's 40 tests permanently outside the rule |
MEDIUM | — (new) | New (refuter contribution, rule defect) | AGENT_RULES.md R8 clause (b); affected artefact proof/01_findings/S3/tests/s3_volume_channel_test.dart (all 7 tests are testWidgets) |
agent_reports/S3_refute.md §4 |
flutter_test catches a TestFailure inside testWidgets through the Flutter error pipeline and re-reports it as a plain error, so the JSON reporter emits result: "error" with isFailure: false. This is a defect in the audit rule, not in the app — Phase 4 must amend R8(b) to accept isFailure: false widget-test errors, or it will reject valid mutation proofs. |
| S4-F05 + S12-F7 | The 150 ms tick rebuilds the entire screen unconditionally, including when nothing is running; one tile's countdown rebuilds every tile | MEDIUM | MEDIUM (S4) / HIGH (S12) | SEVERITY CHANGED (S12 HIGH → MEDIUM); S4 UNREFUTED | lib/ui/home.dart:154-167, specifically the unguarded setState(() {}) at :166; :559 (now: DateTime.now() handed to every tile) |
findings/S4_ui.md · findings/S12_lifecycle.md · agent_reports/S12_refute.md §3.1 |
Found twice. S12's refuter reproduced 155 builds/tick, and measured it reaching layout (16 marks) and paint (2 marks) — net 1,766 µs/tick falling to 7.5 µs with the one-line guard if (mounted && engine.run.isNotEmpty) setState(() {}), a 235× reduction. HIGH is not supported: 1,766 µs is debug-JIT Dart with asserts on, on Apple Silicon, not release AOT on an ARM tablet, and there is no power measurement. The settling artefact is named: adb shell dumpsys batterystats before and after, one hour foreground, unplugged, report the delta in mAh. |
| S4-F06 | The urgency colour is isoluminant across its first half and non-monotonic overall | MEDIUM | MEDIUM | UNREFUTED — verdict provisional | lib/ui/theme.dart:37-39 (anchors), :59-69 (fillFor) |
findings/S4_ui.md · proof/01_findings/S4/ |
S7's refuter independently verified the anchor values while auditing the theme_test.dart spec: fillFor(1.0) is 0xFF5CC79A (mint anchor) and fillFor(0.0) is 0xFFEC6A6A (red anchor), and both fillFor(0.35) and fillFor(0.15) return their anchors exactly because they mix with t = 0 at :66-67. |
| S4-F07 | Five text/background pairs fail WCAG contrast, including the multi-step phase banner | MEDIUM | MEDIUM | UNREFUTED — verdict provisional | lib/ui/tile.dart:207 (C.muted idle countdown), :506 + :536 (banner on C.amber) |
findings/S4_ui.md · proof/01_findings/S4/ |
— |
| S4-F08 | The app is invisible to a screen reader: zero semantic labels, zero button roles, and the LCD ghost digits are announced | MEDIUM | MEDIUM | UNREFUTED — verdict provisional; corroborated by prior work | Whole UI layer — grep -rn 'Semantics\|semanticLabel\|ExcludeSemantics\|tooltip' lib/ returns zero matches across 4,891 lines |
findings/S4_ui.md · research/01_prior_work.md §2.1 A1-6, §2.4 |
This is the largest single decision the Flutter line did not carry over from the v2 rewrite. Prior audit A1-6 (2026-07-19) filed it as MEDIUM and it is recorded STILL OPEN; A4 §14-15 documents the v2 work (real buttons, native dialogs, explicit move-earlier/move-later reorder controls) that was dropped. Same defect, third independent sighting. |
| S4-F09 | MediaQuery.withNoTextScaling is a total opt-out, and it is right for the board and wrong for the dialogs |
MEDIUM | MEDIUM | UNREFUTED — verdict provisional | lib/main.dart:48 (owned by S14, cited not judged), consumed by every text widget |
findings/S4_ui.md |
S7's refuter independently verified the line while auditing the main_test.dart spec: main.dart:48 is builder: (context, child) => MediaQuery.withNoTextScaling(child: child!). |
| S4-F10 | A failed save is reported to the log and to nobody else; the branch that would warn is unreachable | MEDIUM | MEDIUM | UNREFUTED — verdict provisional | lib/ui/home.dart:443-450 (the failed-save path), :679-680 (the banner mapping) |
findings/S4_ui.md |
Same unreachable-banner mechanism as S5-F5 and S3R-F3, both of which were independently confirmed. Fix all three with the one else branch in _criticalBanner. |
| S4-F11 | Every icon in the product is a text glyph that no bundled font contains | MEDIUM | MEDIUM | UNREFUTED — verdict provisional | lib/ui/tile.dart:619 (✕), :552 (✎ EDIT), lib/ui/header.dart:97 (⚙) |
findings/S4_ui.md |
Interacts with S6-F22 (uses-material-design: true ships an icon font for zero icons), which its refuter confirmed: grep -rn "Icon(\|Icons\.\|IconData\|IconButton" lib/ → zero hits. |
| S4-F12 | The operator's only failure signal is 13.5 px tall, never clears, and is placed where it steals grid height | MEDIUM | MEDIUM | UNREFUTED — verdict provisional | lib/ui/home.dart:690-702 (banner box and text style), :565 (position in the column) |
findings/S4_ui.md |
The "never clears" half is independently confirmed for one scope class by S2R-M3 (a load- critical banner can never be cleared for the life of the session) and by S14's refuter for the wakelock banner (S14-F4). |
| S4-F14 (grid half) → merged into S6-F5; S4-F14 (tick half) → merged into S6-F2 | The grid hit-test is written twice, and the tick period is declared in the engine and hard-coded in the UI | MEDIUM | MEDIUM | UNREFUTED as filed; both halves CONFIRMED via S6's refuter | lib/ui/home.dart:515-521 vs :609-613; lib/engine/engine.dart:32 vs lib/ui/home.dart:154 |
findings/S4_ui.md · agent_reports/S6_refute.md |
Row retained so the ID is not lost. Implement against S6-F5 and S6-F2, both confirmed by S6's refuter with exact line ranges (_panUpdate :509-530, _tileIndexAt :609-615). |
| S4-F15 | The UI layer's geometry lives in ~120 inline literals with no constants file | MEDIUM | MEDIUM | UNREFUTED — verdict provisional | lib/ui/tile.dart, lib/ui/header.dart, lib/ui/modals.dart, lib/ui/home.dart |
findings/S4_ui.md |
— |
| S5-F5 | A failure raised isCritical: true that cannot reach the operator banner |
MEDIUM | MEDIUM | CONFIRMED (count correction) | lib/engine/store.dart:270 raises it; lib/ui/home.dart:675-689 drops it |
findings/S5_error_handling.md · agent_reports/S5_refute.md · proof/01_findings/S5_refute/R05_critical_scopes.txt |
Both halves verbatim-exact. Correction: there are 11 distinct critical scopes, not the 12 S5 claims — audio-init, backstop-notif, backstop-init, backstop-exact, backstop-schedule, wakelock, audio-play, voice-init, load-$key, save-$key, migrate-zone-sound. Exactly one (migrate-zone-sound) matches none of the six startsWith prefixes. S3's refuter counted five unrenderable scopes when future fixes are included (S3R-F3). The else catch-all fix is correct and within R6. |
| S5-F6 | Journal export failure leaves the Settings button silently reset | MEDIUM | MEDIUM | CONFIRMED | lib/ui/modals.dart:698-717 (the return at :702 is inside the try, so the catch at :712 cannot see it, while the finally at :714 resets _sending), with lib/journal.dart:233-236 |
findings/S5_error_handling.md · agent_reports/S5_refute.md |
Reachable without contrivance (a failing getTemporaryDirectory() or a failing copy). Two-line fix, correct, within R6. |
| S5-F7 | The native side can never report an error, and Dart discards the booleans it does return | MEDIUM | MEDIUM | PARTIALLY REFUTED | MainActivity.kt:57-59, :79, and the eight catch (_: Exception) sites |
findings/S5_error_handling.md · agent_reports/S5_refute.md · proof/01_findings/S5_refute/R14_flutter_methodchannel_runtimecatch.txt |
Right half: zero result.error(...) calls in MainActivity.kt; the discarded booleans are real and production-reachable. Wrong half: the load-bearing sentence "every Dart-side .catchError on cadence/volume and cadence/tts is unreachable for native faults" is false. Flutter's embedding converts an uncaught RuntimeException from a channel handler into a PlatformException — verbatim MethodChannel.java:285-290 of the pinned 3.44.8 SDK — and MainActivity.kt has unguarded throw sites outside its try blocks at :45, :55, :83, :101-102, plus notImplemented() at :61/:109. So home.dart:211's .catchError and home.dart:227's catch are reachable. Do not write code on the refuted premise. |
| S5-F8 | Unawaited futures with nowhere for a failure to go | MEDIUM | MEDIUM | CONFIRMED (substance) / proof REFUTED | lib/main.dart:33, lib/ui/home.dart:141-144 and :202, lib/audio/audio.dart:99, :107, :114, lib/audio/voice.dart:63, :146, :177, :201, lib/journal.dart:152 |
findings/S5_error_handling.md · agent_reports/S5_refute.md |
All eleven sites verified to exist. But the cited artifact does not demonstrate the claim: proof/01_findings/S5/06_unawaited_and_timeout.txt is a grep whose pattern misses 8 of the 11 sites; calling it "the full inventory" is an R2 defect. The inventory is also incomplete — it omits home.dart:86 (_boot(), which is R5-F1) and the Timer callbacks at home.dart:277, home.dart:387 and alarm_backstop.dart:147. |
| R5-F1 (S5's refuter) | _boot() is launched unawaited and unguarded, and the 150 ms heartbeat is its last statement |
MEDIUM (the refuter records the consequence class as BLOCKER-grade) | — (new) | New (refuter contribution) | lib/ui/home.dart:86 (the call), lib/ui/home.dart:154 (the ticker) |
agent_reports/S5_refute.md §4 |
initState fires _boot(); with no await, no .catchError and no enclosing try. _boot is a 60-line async method with four awaits, and the Timer.periodic that drives every countdown, alarm and repeat is created by its last statement. Any throw earlier in _boot skips the ticker permanently: the board renders, the tiles show their stored state, and nothing counts down. Same code region as S4-F04 (hang) and S12-F1 (unmount) — three failure modes, three fixes. |
| S6-F1 | The release APK ships three CPU architectures; 61.5 % of it is dead weight on every device | MEDIUM | MEDIUM | CONFIRMED (measurement) / headline framing REFUTED | android/app/build.gradle.kts:30-36 — no splits/abiFilters; build command at README.md:25 |
findings/S6_dry_deadcode.md · agent_reports/S6_refute.md · proof/01_findings/S6_refute/apk_rebuild_independent.txt |
Rebuilt independently and reproduced to the byte: universal 53,629,091 B, arm64-only 20,623,249 B. The framing is wrong: minSdk = 24 and Google's own 64-bit requirements page permits dropping armeabi-v7a, but doing so drops 32-bit tablets — exactly the cheap hardware a restaurant buys. Prefer the App Bundle over abiFilters. Note S6R-F2: --obfuscate --split-debug-info removes a further 851,968 B and was never measured. |
| S6-F2 + S4-F14 (tick half) | Engine.tickMs is dead while the heartbeat that it describes hard-codes the same number |
MEDIUM | MEDIUM (both) | CONFIRMED | lib/engine/engine.dart:32 (declaration, zero readers) and lib/ui/home.dart:154 (the literal that should read it) |
findings/S6_dry_deadcode.md · findings/S4_ui.md · agent_reports/S6_refute.md |
Found twice. tickMs appears once in the whole tree. S7's refuter independently confirmed zero references in test/ and mutated 150→5000 with the suite still green. |
| S6-F4 (+ S6R-F4) | The 5-second minimum duration is an invariant written at seven independent sites — plus an eighth the finding missed | MEDIUM | MEDIUM | CONFIRMED, scope corrected | lib/engine/engine.dart:368, :372-374; lib/ui/modals.dart:374, :388, :418, :524; lib/ui/modals.dart:436-437 (the fourth encoding, S6R-F4). lib/engine/models.dart:22 is NOT the invariant |
findings/S6_dry_deadcode.md · agent_reports/S6_refute.md |
Six sites enforce the floor; modals.dart:418 is the picker's snap-off-zero, which is the floor in practice because the seconds stepper moves in fives (:436-437). models.dart:22 is a JSON default, not a floor — {'sec': 1} loads as 1. S6's "How to prove the fix" is also wrong: editor_layout_test.dart:76 asserts secs.first >= 5 where C.presets' smallest is 30 s, so re-pointing it at a constant set to 9 still passes. Write a new gate. Centralising without :436-437 makes the editor display one number and commit another. |
| S6-F5 + S4-F14 (grid half) | The grid hit-test is implemented twice in the same file, once inline and once as a helper | MEDIUM | MEDIUM (both) | CONFIRMED, exactly as described | lib/ui/home.dart:509-530 (_panUpdate) and :609-615 (_tileIndexAt) |
findings/S6_dry_deadcode.md · findings/S4_ui.md · agent_reports/S6_refute.md |
Found twice. _tileIndexAt is already called from onPanStart at :581-583, 30 lines above. Proposed fix is the right shape and needs no signature change. |
| S6-F6 | The EngineHost no-op implementation is copied into six test files |
MEDIUM | MEDIUM | CONFIRMED — "strongest duplication in the report" | test/announcement_test.dart:23-42, test/i18n_defaults_test.dart:13-32, test/store_test.dart:12-31, test/backstop_test.dart:12-31, test/robustness_test.dart:13-34, test/engine_test.dart:7-34 |
findings/S6_dry_deadcode.md · agent_reports/S6_refute.md |
Three of the six are md5-identical (f2198392ae74ea9e5660de7aa3bfa9d2, 20 lines each); backstop_test differs only in now(). implements EngineHost forces all nine members in every copy, so "a tenth member breaks six files" is literally true. |
| S6-F9 | RunStatus is re-encoded as bare strings throughout tile.dart |
MEDIUM | MEDIUM | PARTIALLY REFUTED — duplication confirmed, crash claim REFUTED twice over, enumeration wrong | lib/ui/tile.dart:101-105 (_status) and eleven comparison sites at :110, :111, :182, :185, :192, :205, :207, :208, :257, :569; twelfth site at lib/ui/home.dart:124-133 |
findings/S6_dry_deadcode.md · agent_reports/S6_refute.md |
S6 claims a fourth enum value "would crash rather than fail to compile". It fails to compile (flutter analyze exit 1, non_exhaustive_switch_expression at home.dart:124), and with that error patched away the tile renders the fourth value without throwing — r! is null-safe by construction. The twelfth site (S6R-F3) is the good one, an exhaustive switch with no default, and is the working example the fix should be modelled on. |
| S6-F15 + S11-F3 | Three of the sixteen WAVs have no generator; the documented source of truth cannot reproduce them | MEDIUM | MEDIUM (both) | CONFIRMED; S11's supporting inference INVALID | tools/build_ringtones.py:1-238 (13 write/write_peak calls: 12 tones + res/raw/cadence_alarm.wav); the three unreproducible files are assets/audio/step.wav, assets/audio/click-up.wav, assets/audio/click-down.wav |
findings/S6_dry_deadcode.md · findings/S11_asset_licensing.md · agent_reports/S6_refute.md · agent_reports/S11_refute.md §5 |
Found twice — S6 from the reproducibility side, S11 from the licensing side. The measurements reproduce exactly and S11's refuter ran the flip test S11 named and failed to flip it. But S11's supporting inference — "proven synthetic → no third-party licence can attach" — is a non-sequitur. The conclusion is right for a reason S11 did not give; restate it before publishing. |
| S6-F16 + S9-F11 | The backstop alarm sound is invisible to the shrinker, pinned by a single keep.xml rule with no regression test, and has already been stripped once |
MEDIUM | MEDIUM (both) | CONFIRMED with a three-state mutation proof | android/app/src/main/res/raw/cadence_alarm.wav (142,928 bytes), android/app/src/main/res/raw/keep.xml:1-6, referenced from lib/alarm_backstop.dart:55 |
findings/S6_dry_deadcode.md · findings/S9_platform_config.md · agent_reports/S6_refute.md · agent_reports/S9_refute.md §Contrarian B |
Found twice. Both refuters confirmed. build.gradle.kts sets neither isMinifyEnabled nor isShrinkResources, so the shrinker is off today and keep.xml is belt-and-braces — it becomes load-bearing the moment anyone enables shrinking. Do not delete keep.xml. S9's refuter proved all three states (shrinker off / on without keep / on with keep). |
| S7-F7 | Notification ids have no test: every timer may collapse onto one id | MEDIUM | MEDIUM | CONFIRMED (not individually re-graded) | lib/alarm_backstop.dart:67 |
findings/S7_tests.md · agent_reports/S7_refute.md |
S3's refuter independently mutated _nid (mutation B1) and the suite detected it — so the id scheme has some protection, but no test names it. Cross-reference S3-F14, where per-clone _nid is what stacks three identical notifications. |
| S7-F9 | Nine of the ten fromJson fallback defaults are unprotected |
MEDIUM | MEDIUM | CONFIRMED | lib/engine/models.dart:22 (×2), :67, :68, :72, :131-132, :134, :139, :158 |
findings/S7_tests.md · agent_reports/S7_refute.md §6 |
The refuter audited the proposed models_test.dart spec and found it implementable and killable — it kills mutations M18–M22 and M24–M28. legacyZoneId exists at models.dart:39, is read at :78, and is absent from toJson (:56-63), so spec test 3 is exactly right. |
| S7-F10 | TimerDef.isChain's >= 2 boundary has no test |
MEDIUM | MEDIUM | CONFIRMED (not individually re-graded) | lib/engine/models.dart:52 |
findings/S7_tests.md |
S1's refuter independently checked isChain at 0, 1 and ≥2 steps and confirmed a one-step def is not a chain. |
| S7-F11 | test/engine_test.dart:288 names cloning but asserts on the parent |
MEDIUM | MEDIUM | CONFIRMED | test/engine_test.dart:288-295 |
findings/S7_tests.md · agent_reports/S7_refute.md §10 |
Verified verbatim at source. |
| S7-F12 | The grid geometry ratio and aspect tests are tautological | MEDIUM | MEDIUM | CONFIRMED | test/grid_layout_test.dart:14, :15, :21, :23, :38, :72 |
findings/S7_tests.md · agent_reports/S7_refute.md §10 |
Verified verbatim at source. |
| S7-F14 | I18n.ttsLocale has no test |
MEDIUM | MEDIUM | CONFIRMED (not individually re-graded) | lib/i18n.dart:166 |
findings/S7_tests.md |
— |
| S7-F15 | Store.lang validation is unprotected for a well-typed but unsupported language |
MEDIUM | MEDIUM | CONFIRMED and strengthened | lib/engine/store.dart:158-161 |
findings/S7_tests.md · agent_reports/S7_refute.md §10 |
"Strengthened by execution counts S7 did not measure." Interacts with S8-F4 (language is chosen once and an install that never seeded is pinned to English forever), same lines. |
| S7-F19 | One test's pass depends on a 50 ms wall-clock margin | MEDIUM | MEDIUM | CONFIRMED (not individually re-graded); flakiness claim HELD | test/backstop_test.dart:169; supporting test/voice_test.dart:59, :77, :94 |
findings/S7_tests.md · agent_reports/S7_refute.md §7 |
The refuter ran five further whole-suite runs (2 identical, 2 fresh random seeds, 1 fixed seed) — 123 pass, exit 0, every time. Thirteen independent runs now agree the suite is order-independent and flake-free today; the margin remains a latent flake source. |
| S7R-F1 | The "19 of 123" headline row states as measured what §1b explicitly disclaims | MEDIUM | — (new) | New (refuter contribution, reporting defect) | findings/S7_tests.md §0 headline table, row "Distinct tests thereby individually validated" |
agent_reports/S7_refute.md §5 · proof/01_findings/S7_refute/rerun_comparison.txt |
The measured fact is "19 tests are individually pinned by a single-test assertion kill". The implication a reader takes is "104 tests are unproven". A bound S7 never computed says the opposite more strongly: 43 distinct tests of 123 went red under at least one mutation. Publish 43, not 19 alone. |
| S7R-F2 | S7 never used the --reporter=json capture R8(a) requires; every one of its 57 runs used the expanded reporter |
MEDIUM | — (new) | New (refuter contribution, R8 process defect) | All 57 of proof/01_findings/S7/mutations/M01..M57.txt |
agent_reports/S7_refute.md §5 |
The conclusions happen to be right — the refuter re-ran all 57 under json and agrees 57/57 — but S7's own records cannot demonstrate R8(a) or R8(b), because the fields are not in the capture. See EVIDENCE_INTEGRITY.md §3. |
| S7R-F3 | Not one of S7's 69 recorded files carries a TREE_STATE stamp, and R8(d) is evidenced by a single end-of-campaign check |
MEDIUM | — (new) | New (refuter contribution, R12 proof integrity) | Every .txt under proof/01_findings/S7/; proof/01_findings/S7/revert_clean_check.txt |
agent_reports/S7_refute.md §5, §8 |
R8(d) is evidenced by one aggregate git status --porcelain taken at 10:08:15Z, after all 57 runs (10:02–10:08) had finished. That proves the tree was clean at the end, not between runs. See EVIDENCE_INTEGRITY.md §1. |
| S7R-F6 | Five of the thirteen test files were never reddened by any of the 57 mutations, and S7 does not report it | MEDIUM | — (new) | New (refuter contribution) | test/announcement_test.dart (267 lines, 13 tests), test/editor_layout_test.dart (79, 3 tests), test/i18n_defaults_test.dart (117, 6), test/source_hygiene_test.dart (25, 1), test/version_test.dart (29, 1) |
agent_reports/S7_refute.md §5 |
Five of thirteen files, carrying 24 of the 123 tests, never went red once across all 57 mutations. S7's manifest reports which mutations "land" in each file but never the inverse and more useful measure. |
| S8-F4 | Language is chosen once, and an install that never seeded is pinned to English forever | MEDIUM | MEDIUM | CONFIRMED | lib/engine/store.dart:300-310 and :158-161 |
findings/S8_i18n.md · agent_reports/S8_refute.md |
Confirmed by S8's refuter; both line ranges also independently verified by S2's refuter (for S2-F9) and by S7 (for S7-F15). |
| S8-F5 | The app's own name is inconsistent across the four places an operating system reads it | MEDIUM | MEDIUM | CONFIRMED in substance; file:line REFUTED |
android/app/src/main/AndroidManifest.xml:19 — not :22, which is <activity; ios/Runner/Info.plist (CFBundleDisplayName, CFBundleName); web/manifest.json:2-3; lib/main.dart:44 |
findings/S8_i18n.md · agent_reports/S8_refute.md §2 S8-R5 |
grep -n 'android:label' android/app/src/main/AndroidManifest.xml returns 19: android:label="Cadence" and nothing else. This matters more than a normal citation slip: S8-F5 is the rename inventory, a 22-item checklist where items 3 and 7 are irreversible after first publish. The web/manifest.json:2-3 half is independently confirmed by S9-F12/S6-F21/S14-F7; the iOS half is adjacent to S9R-M03. |
| S8-F6 | The Android notification channel is English-only, in the one recovery screen the app points at | MEDIUM | MEDIUM | CONFIRMED in substance; EVIDENCE AND FIX REFUTED | lib/alarm_backstop.dart:44-59 — not :46-49; the type is AndroidNotificationDetails with channelDescription:, not AndroidNotificationChannel with description:; consumed through static const NotificationDetails _details = NotificationDetails(android: _channel) at :60-61 |
findings/S8_i18n.md · agent_reports/S8_refute.md §2 S8-R4 |
S8-F6's "verbatim" block quotes a class that appears nowhere in lib/ — grep -rn 'AndroidNotificationChannel\|createNotificationChannel' lib/ exits 1. Its claim that "the channel is created once at :78-80 inside init()" is also false: :76-82 are permission requests and a Journal.log; flutter_local_notifications creates the channel implicitly from the details on the first notification. The conclusion survives (the two strings are hardcoded English and are the recovery screen the French banner sends a chef to) but the fix does not: making it language-aware means dropping both consts and threading an I18n through Backstop and every call site, not adding one construction in init(). |
| S8-F7 | Numbers, durations and the clock are formatted by hand, with two concrete defects | MEDIUM | MEDIUM | CONFIRMED | lib/ui/theme.dart:77-82; lib/ui/header.dart:143-148; lib/ui/modals.dart:637 |
findings/S8_i18n.md · agent_reports/S8_refute.md |
Confirmed. Overlaps S6-F18 (four independent copies of a two-digit zero-pad), whose refuter REFUTED the pad2 half as a library idiom rather than logic. S8's defect is the formatting semantics, not the duplication — the two findings survive independently. Also overlaps S4-F17. |
| S8-F8 | A missing key renders its own identifier on screen | MEDIUM | LOW | SEVERITY CHANGED (LOW → MEDIUM, raised); mechanism CONFIRMED | lib/i18n.dart:143-145 |
findings/S8_i18n.md · agent_reports/S8_refute.md §1, §2 S8-R1 |
The refuter measured call(missingKeyXyz)="missingKeyXyz" and FR_MISSING_FALLS_BACK_TO_EN=false, and proved the raw identifier reaches the screen. S8 rated it LOW because "parity is perfect"; the refuter showed parity is not the guard — see S8-R1. |
| S8-R1 (S8's refuter) | A key deleted from both locale maps renders raw on screen and the whole suite stays green | MEDIUM | — (new) | New (refuter contribution) | lib/i18n.dart:143; the guard under test at test/i18n_defaults_test.dart:38-45; render site lib/ui/modals.dart:683 |
agent_reports/S8_refute.md §2 |
The parity test compares the two maps against each other, so deleting a key from both leaves parity perfect and the suite green while the identifier renders on screen. This is why S8-F8 is not LOW. |
| S8-R2 (S8's refuter) | The editor shows the operator the ungrammatical French phrase, live, next to a correct one | MEDIUM | — (new) | New (refuter contribution) | lib/ui/modals.dart:235-242, wired at :337; the correct string is at lib/i18n.dart:58 |
agent_reports/S8_refute.md §2 |
S8-F2 treats the readyPhrase grammar defect as something the operator hears. It is also something the operator sees, in the editor, immediately beside a correctly-worded string — which makes it a visible product-quality defect during a sales demo, not only an audio one. |
| S8-R4 (S8's refuter) | S8-F6's evidence quotes a class that does not exist in the codebase, and its fix targets the wrong construct | MEDIUM | — (new) | New (refuter contribution; changes the fix) | lib/alarm_backstop.dart:44-59 |
agent_reports/S8_refute.md §2 |
Detail in the S8-F6 row above. Implement the refuter's fix, not S8's. |
| S9-F08 | ITSAppUsesNonExemptEncryption is absent, forcing the export-compliance questionnaire on every single upload |
MEDIUM | MEDIUM | CONFIRMED | ios/Runner/Info.plist:4-69 — key absent from the top-level <dict> |
findings/S9_platform_config.md · agent_reports/S9_refute.md |
The refuter proved the absence independently with its own Info.plist key-set diff. |
| S9-F09 + S13-F3 (+ Apple readiness C1) | No PrivacyInfo.xcprivacy in the app bundle |
MEDIUM | MEDIUM (S9) / BLOCKER (S13) / hard blocker C1 (Apple readiness) | REFUTED — the manifest is NOT required. S13's BLOCKER refuted (its refuter re-graded it LOW); S9's MEDIUM confirmed. MEDIUM carried, for the undocumented-reason half only | Repository-wide — no .xcprivacy file exists. S13 cited the calling code at lib/journal.dart:69-72, :170-171, :194-199, :226, :230 |
findings/S9_platform_config.md · findings/S13_data.md · store_readiness/02_apple_app_store.md C1 · agent_reports/S9_refute.md §Contrarian A · agent_reports/S13_refute.md §1 |
Two refuters settled this independently, and they converged. Both quoted Apple's operative sentence, which no stream quoted: "For each executable or dynamic library in an app that uses a required reason API, the bundle that includes the executable or dynamic library needs to include a privacy manifest file that reports the API." S9's refuter enumerated nine linked iOS plugins from .flutter-plugins-dependencies (S9's own table had missed package_info_plus 10.2.1, transitive via wakelock_plus) and read the Flutter 3.44.8 engine manifest out of the built artifact: FileTimestamp (0A2A.1, C617.1), SystemBootTime (35F9.1). S13's refuter went further and disassembled the release build: libapp.so (Cadence's own AOT Dart, every File.exists()/length()/copy()/writeAsString() in journal.dart) has 0 undefined dynamic symbols and no required-reason symbols; the stat family lives entirely in libflutter.so, and nm -u Flutter.framework/Flutter shows _fstat, _lstat, _stat. Phase 4 may add an empty manifest (NSPrivacyTracking = false, empty arrays) — it must NOT populate NSPrivacyAccessedAPITypes, because Apple binds you to what you declare. Full resolution in CONTRADICTIONS.md §1. |
S13-F6 + S2-F12 → see the S2-F12 + S13-F6 row above |
Corrupt-value siblings persist forever with no expiry and no way to clear them | MEDIUM | MEDIUM | CONFIRMED by S13's refuter as well as S2's | lib/engine/store.dart:119-128 |
findings/S13_data.md · agent_reports/S13_refute.md |
Row retained so the S13 ID is not lost. Both streams' refuters confirmed the same defect at the same lines. |
| S13-R1 (S13's refuter) | The no-network proof has no layer covering native (non-Dart) network paths, and two plugins have them | MEDIUM | — (new) | New (refuter contribution) | lib/audio/audio.dart:73; audioplayers_darwin-6.5.0/darwin/…/WrappedMediaPlayer.swift:165-185; audioplayers_android-5.3.0/…/source/UrlSource.kt |
agent_reports/S13_refute.md §3 |
S13's §3 layer 3 enumerates packages importing package:http and shows each import site is unreachable. That method cannot see a plugin that opens a connection from Swift or Kotlin, because such a plugin imports no Dart HTTP library at all. The no-network conclusion survives — it is safe for a store listing — but the proof does not cover the case, and a store filing must rest on a proof that does. |
| S13-R3 (S13's refuter) | S13-F2's store-declaration rationale is wrong, and following it would over-declare on both stores | MEDIUM | — (new) | New (refuter contribution) | S13-F2 "Why it matters"; S13 §1 preamble | agent_reports/S13_refute.md §3 · proof/03_market/captures/s813r_play_data_safety_expanded.txt |
Google's published guidance, verbatim: "Collect means transmitting data from your app off a user's device", and "Developers do not have to declare data access as collection if it occurs solely on the user's device", plus an FAQ covering this exact shape (a user's own upload to their own cloud account governed by that provider's terms is not the app's collection). S13 also mis-frames its personal-data table as the input to the Data Safety form and Apple nutrition labels; it is a GDPR-controller inventory, a different question with a different answer. |
| S10-F4 | The "add a batch" chip sits in the slap zone of a ringing tile | MEDIUM | HIGH | SEVERITY CHANGED (HIGH → MEDIUM) | lib/ui/tile.dart:384 (showDup = widget.dupShow && !widget.editing), lib/ui/home.dart:624 (status != null && cnt < Engine.maxBatch, which does not exclude ringing) |
findings/S10_product.md · agent_reports/S10_refute.md |
The code fact is confirmed exactly as cited. The geometry argument is weaker than stated: on the reference 602×332 tile the chip measures ≈73×48 px including padding, sitting immediately right of the name at roughly one quarter down the tile, and Flutter resolves a touch to one point, not a palm footprint. The consequence is self-announcing — the mis-tap starts a pan and the original alarm keeps ringing — so the cook is told within a second. |
| S10-F6 | After any outage the board understates how late a dish is, by the whole outage | MEDIUM | HIGH | SEVERITY CHANGED (HIGH → MEDIUM); premise REFUTED | lib/engine/engine.dart:279 (rangAt = n), lib/ui/tile.dart:199-201 (count-up from it); driftMs computed at engine.dart:278 and journaled at home.dart:288-292 but absent from TileView's parameters (tile.dart:12-34) |
findings/S10_product.md · agent_reports/S10_refute.md |
S10's premise is its own reading and the code says the opposite verbatim: // count-up baseline = the ACTUAL ring instant, not the deadline. That is a written decision and F6's proposed fix inverts it without refuting it. The honest residue, which stands: lateness is computed, journaled and never shown anywhere in the UI. Implement the residue, not the inversion. |
| S10-F7 | Nothing on the board says which station or which cook a timer belongs to | MEDIUM | HIGH | SEVERITY CHANGED (HIGH → MEDIUM); two sub-claims corrected | lib/engine/models.dart:29-49 (the whole TimerDef surface — no station, owner or timestamp; legacyZoneId at :37-39, :78 is migration-only); lib/engine/store.dart:311-317 |
findings/S10_product.md · agent_reports/S10_refute.md §4 |
The gap is CONFIRMED. Correction to the history: what v0.4.11 deleted was not a colour dot — the removed widget printed z.name.toUpperCase() inside a coloured pill on every tile (git show 07ee62a -- lib/ui/tile.dart). Do not re-add a dot; the prior art is a text pill. |
| S10-F8 | Correcting a mistimed start costs one tap per ten seconds, and is impossible once the dish rings | MEDIUM | MEDIUM | CONFIRMED, one sub-claim REFUTED | lib/ui/tile.dart:601-616, :569-570; lib/engine/engine.dart:228-241 |
findings/S10_product.md · agent_reports/S10_refute.md |
No repeat-on-hold exists anywhere. Sub-claim refuted: S10 says the alarm_backstop.dart:36-39 debounce now "defends against a burst the UI can no longer produce". Wrong — S10's own table says +5 min costs 30 taps, and 30 taps in a few seconds is exactly the burst the 300 ms debounce collapses. Only the word holding in that comment is stale; the debounce is still load-bearing — do not remove it. |
| S10-F9 | The most urgent state on the board is the least visible one | MEDIUM | MEDIUM | CONFIRMED | lib/ui/theme.dart:16 and :25 are byte-identical Color(0xFFE7DED0); lib/ui/tile.dart:778-779 sweeps p * 2 * math.pi, so the coloured area → 0 as the deadline arrives |
findings/S10_product.md · agent_reports/S10_refute.md |
Confirmed, and the refuter notes S10 correctly names the artifact that would settle the remaining design question rather than hedging. |
| S10-F10 | The batch cap is silent: the button just disappears | MEDIUM | MEDIUM | CONFIRMED | lib/engine/engine.dart:31, :194; lib/ui/home.dart:624 |
findings/S10_product.md · agent_reports/S10_refute.md |
— |
| S10-F11 | The app ships seeded with one specific restaurant's menu, removable only one dish at a time | MEDIUM | MEDIUM | CONFIRMED | lib/engine/store.dart:296, :326-343 |
findings/S10_product.md · agent_reports/S10_refute.md · research/01_prior_work.md §2.4 |
Verified verbatim, including const oven = 'Cascade', fryer = 'Chirp'. Prior work records this as a deliberate divergence from the v2 decision to seed general examples — the code comment calls it "The pilot kitchen's real timers, in service order". These are dish names, not a restaurant identifier, so source_hygiene_test.dart passes; the store-listing question is separate. |
| S10-F12 | The operator has no in-app record of what happened; the only record is a French-only diagnostic file | MEDIUM | MEDIUM | CONFIRMED | lib/ui/modals.dart:673-689, :698-717; the hard-coded French journal lines at lib/ui/home.dart:358, :368, :383, :396, :408 are independent of store.lang |
findings/S10_product.md · agent_reports/S10_refute.md |
Confirmed. Related: S13-F1 (that same export carries operator free text and a device description off the device unredacted). |
| M2 (S10's refuter) | A single-duration dish can only be given a duration whose seconds are a multiple of five | MEDIUM | — (new) | New (refuter contribution) | lib/ui/modals.dart:436-437, against :502-503 |
agent_reports/S10_refute.md §6 |
The seconds column steps (sec + 5) % 60 up and (sec + 55) % 60 down, and the six presets (theme.dart:52-54) are all multiples of five, with no text entry for a single-mode duration. 3:07 cannot be entered at all. A chained dish can: _stepRow gives every phase a free numeric field clamped only to 0-59. The two modes disagree about what a duration is. Same two lines as S6R-F4. |
| S11-F2 | Two of the three font families ship with their embedded licence record stripped, and no record of where the binaries came from | MEDIUM | HIGH | SEVERITY CHANGED (HIGH → MEDIUM); characterisation OVERSTATED | assets/fonts/BigShouldersDisplay-{Medium,Bold,ExtraBold}.ttf and the three ChivoMono faces — six files |
findings/S11_asset_licensing.md · agent_reports/S11_refute.md §4 |
The stripping is real and reproduced. But upstream name ID 13 is a one-sentence notice plus a URL, not the OFL text — and name ID 14 carries that same URL and is already present in the bundled files. "The licence travels nowhere" is wrong. Correct the sentence before publishing. |
| S11-F4 | The web target ships the Flutter logo as the product's own icon, with no BSD-3-Clause notice | MEDIUM | MEDIUM | CONFIRMED and STRENGTHENED | web/favicon.png, web/icons/Icon-192.png, web/icons/Icon-512.png, web/icons/Icon-maskable-192.png, web/icons/Icon-maskable-512.png |
findings/S11_asset_licensing.md · agent_reports/S11_refute.md §6 · agent_reports/S9_refute.md §M02 |
All five icons are now byte-proven, not three: the two maskable icons were fetched from flutter_template_images 5.0.0 (sha256 0120589a…) and are BIT-IDENTICAL; S11's "byte comparison unavailable" limitation was not real. The trademark question is answered definitively from Google's own guidelines, which S11 never fetched. S9's refuter reached the opposite conclusion about three of the five — see CONTRADICTIONS.md §4. |
| S11-F5 | The application has no licence surface at all | MEDIUM | MEDIUM | CONFIRMED | lib/ (whole tree — no showLicensePage/LicenseRegistry call site exists); Settings dialog at lib/ui/modals.dart:616-693 |
findings/S11_asset_licensing.md · agent_reports/S11_refute.md §3.3 |
Verified from the Flutter SDK source at the pinned version, not from the API doc: LicenseRegistry/showLicensePage would surface nothing for these fonts even if called. |
| R-F2 (S11's refuter) | S11's asset inventory omits 33 tracked binaries | MEDIUM | — (new) | New (refuter contribution, coverage defect in the audit) | android/app/src/main/res/{mipmap-*,drawable-*}/*.png (15), ios/Runner/Assets.xcassets/**/*.png (18) |
agent_reports/S11_refute.md §7 |
S11 states "Total binary assets in scope: 32" and its manifest lists 36 rows; the repository tracks 65 binary image/font/audio assets. The iOS omission concealed R-F1, a BLOCKER. Any Phase-4 licence file must be rebuilt from the 65, not the 32. |
| R-F3 (S11's refuter) | The LICENSES draft reproduces the wrong Flutter copyright year for two of the five icons | MEDIUM | — (new) | New (refuter contribution) | findings/S11_LICENSES_draft.md:110 |
agent_reports/S11_refute.md §7 |
The draft reproduces Copyright 2014 The Flutter Authors (the flutter/flutter root LICENSE, which covers the three .copy.tmpl icons). The two maskable icons come from flutter_template_images 5.0.0, whose LICENSE reads Copyright 2013. BSD-3-Clause conditions binary redistribution on reproducing "the above copyright notice" — the notice attached to the work. findings/S11_LICENSES_draft.md is NOT APPROVED: 7 corrections required before Phase 4 commits it. |
| S12-F2 | Delayed announcement timers are never held and outlive dispose() |
MEDIUM | MEDIUM | CONFIRMED | lib/ui/home.dart:275-281, specifically :277 |
findings/S12_lifecycle.md · agent_reports/S12_refute.md §2 · proof/01_findings/S12_refute/repro_s12_w0_ticker_live.txt |
Reproduced red (EXIT_CODE=1). One of the three S12 findings confirmed as written: the code genuinely has no cancel path, the fix is four lines, the window is real. Latent, because _HomeScreenState.dispose() has exactly one production trigger at 03a176e and it destroys the isolate. |
| S12-F6 | A wedged TTS utterance leaks a native channel result on both platforms | MEDIUM | MEDIUM | CONFIRMED with a narrowed trigger | android/app/src/main/kotlin/dev/sergemio/cadence/MainActivity.kt:35, :143-165 (esp. :147, :160-162); ios/Runner/AppDelegate.swift:28, :173, :182, :188-189 |
findings/S12_lifecycle.md · agent_reports/S12_refute.md §2 |
The only LIVE leak of the six S12 claimed. Trigger narrowed: MainActivity.kt:150 calls speak(..., TextToSpeech.QUEUE_FLUSH, ...), so the next utterance interrupts the previous one and fires onStop(id, interrupted) → completeSpeak, clearing the stale entry. The leak therefore requires an engine that accepts speak() with SUCCESS and then emits no UtteranceProgressListener callback at all — not merely "swallows a callback". Rewrite the trigger description. |
| S12-F8 | Nothing in the app mitigates OLED burn-in on a static board held at full brightness for twelve hours | MEDIUM | MEDIUM | PARTIALLY REFUTED — grep confirmed, premise refuted, and the proposed fix violates R6 | lib/main.dart:28 (WakelockPlus.enable()), :33 |
findings/S12_lifecycle.md · agent_reports/S12_refute.md §3.2 |
The grep is confirmed (nothing mitigates burn-in; nothing names a panel). But the finding assumes an OLED panel and the audit establishes the panel nowhere — a 50/50 premise stated as certainty. The exact missing artefact: one field journal from the pilot tablet, whose fourth line is APPAREIL <manufacturer> <model> … (lib/journal.dart:119-120). The precise test: read that model number, look it up on the manufacturer's product page, record the display technology. OLED → MEDIUM stands. LCD → drops to LOW as image-persistence only. Separately: proposed fix (b) (a black overlay dismissed by touch) is a new end-user feature and violates R6, and fix (a) (shifting the grid origin ±3 px) moves every touch target and belongs to S4. |
| S12-F10 | Three setState calls run after an await with no mounted guard |
MEDIUM | MEDIUM | PARTIALLY REFUTED — code fact confirmed, harm claim not upheld | lib/ui/home.dart:448 and :456 (after await showTimerEditor at :421), and :491 |
findings/S12_lifecycle.md · agent_reports/S12_refute.md §6 |
The refuter re-verified every setState/await/mounted triple and confirmed all three sites are unguarded. It classes the finding among the seven "REFUTED in part — the code fact holds; the harm claim, the classification, or the severity does not" without stating a replacement grade, so MEDIUM is carried forward with that caveat attached. Same latency argument as the other lifecycle rows: dispose() has one production trigger and it destroys the isolate. |
| S12R-F1 | Three per-tile trigger maps in _HomeScreenState grow without bound during a session, with no teardown required |
MEDIUM | — (new) | New (refuter contribution) | lib/ui/home.dart:54-56 (declarations of _flash, _justOn, _spawn), :319, :361, :414, :415 (the only writes; zero removal sites) |
agent_reports/S12_refute.md §5 |
This refutes S12's own headline. S12 says the journal is "the one unbounded thing"; these three Map<String,int> are keyed by tile id, Engine.uid() (engine.dart:60-62) mints a fresh id per clone, and nothing ever removes an entry. They grow live, with no teardown required. |
| S12R-F4 | Every S12 proof file stamps TREE_STATE: CLEAN, including the three recorded against a mutated or patched tree |
MEDIUM | — (new) | New (refuter contribution, R12 proof integrity) | proof/01_findings/S12/*.txt — all 14; cause stated at findings/S12_lifecycle.md:542-543 |
agent_reports/S12_refute.md §5 |
S12 ran every command with CADENCE_REPO pointed at the pinned repo while working in a copy, so the stamp certifies the wrong tree. The mirror image of the S1/S2 defect: there the harness climbed out of the copy by accident; here it was pinned out deliberately. Both produce a TREE_STATE line that says nothing about the tree under test. See EVIDENCE_INTEGRITY.md §2. |
| S12R-F5 | S12's mutation record does not meet R8 on three of its four conditions | MEDIUM | — (new) | New (refuter contribution, R8 process defect) | proof/01_findings/S12/mutation_stoptimer_keeps_clone.patch, mutation_soak_goes_red.txt; claim at findings/S12_lifecycle.md:641-659 |
agent_reports/S12_refute.md §5 |
Same class as S3R-F4, S7R-F2, and S5's two failed mutations. |
| S12R-F6 | The soak's headline journal-growth figure is defended by no assertion | MEDIUM | — (new) | New (refuter contribution, R8 process defect) | proof/01_findings/S12/tests/s12_soak_test.dart:321-341 — the only journal assertion in SOAK A1 is expect(file.lengthSync(), greaterThan(0)) at line 341; the 27,585 B/h is printed at line 323 |
agent_reports/S12_refute.md §3.3, §5 |
The soak can go red, but not on the number it exists to produce. This is why the growth figure was wrong and nothing caught it. |
| S14-F4 | The operator banner promised for a failed wakelock DOES exist and is reachable — but it can never clear, and a wakelock that silently fails to take effect produces no banner at all | MEDIUM | MEDIUM | CONFIRMED — "the one I tried hardest to break" | lib/main.dart:26-32, lib/ui/home.dart:670-707 |
findings/S14_entry_unowned.md · agent_reports/S14_refute.md §2.3 |
S14 proved the banner with a hand-made Diag.fail. The refuter drove the real main() with a failing wakelock backend and proved both residual defects by test rather than grep. Note S14R-M1 found an ERROR-severity type hole on lib/main.dart:31, the exact line this finding is about. |
| S14-F8 → reassigned to S9 | .metadata registers web for template migration but not android or ios, so the two platforms that ship are excluded from every future Flutter template upgrade |
MEDIUM | MEDIUM | CONFIRMED; ownership corrected to S9 | .metadata:13-20 |
findings/S14_entry_unowned.md · agent_reports/S14_refute.md §1, §3 S14R-M3 |
Diffed against a fresh template by the refuter. Reassigned per S14R-M3: S14 audited a 47-file scope deleted from the plan before Phase 1 launched. |
| S14R-M1 | The analyser's strict type-checking modes were never measured, and turning them on surfaces six ERROR-severity type holes, one on the exact line S14 wrote F4 about | MEDIUM | — (new) | New (refuter contribution) | analysis_options.yaml:1-29 — the absence of an analyzer: block is the finding; the errors it hides include lib/main.dart:31 |
agent_reports/S14_refute.md §3 |
S14-F5 measured one dimension of the file (what sits under linter: rules:) and concluded from 75 lint infos that the analyser is "switched almost off". The other dimension — analyzer: language: controlling strict-casts, strict-inference, strict-raw-types, which are not lints — was never touched, and turning it on surfaces six ERROR-severity type holes. |
| S14R-M3 | S14 audited a 47-file scope that was deleted from the plan before Phase 1 launched, so five of its findings belong to other streams and its partition reconciliation contradicts the partition tool | MEDIUM | — (new) | New (refuter contribution, process defect) | findings/S14_entry_unowned.md header and its git ls-files reconciliation section, against PLAN.md:175, CHECKLIST.md:50 and :67, tools/check_partition.py, research/04_partition.md:46-47 |
agent_reports/S14_refute.md §3 |
This is the authority for the five reassignments applied in this register (F6→S10, F7→S6, F8→S9, F9→S9, F11→S7). It changes what Phase 2 and Phase 4 must do, not what the app does. S14's own "144 files map to exactly one stream; S14 absorbs 47 including pubspec.yaml/pubspec.lock" claim is REFUTED by four independent artefacts. |
| S9R-M03 | The app localises itself manually into French and English, but the iOS bundle declares English only | MEDIUM | — (new) | New (refuter contribution) | ios/Runner/Info.plist:4-69 (CFBundleLocalizations absent) and ios/Runner.xcodeproj/project.pbxproj:196-201 (developmentRegion = en) |
agent_reports/S9_refute.md §Part 6 |
lib/i18n.dart carries full fr and en string tables, lib/ui/modals.dart:626 offers the picker, and lib/engine/store.dart:293-294 starts a fresh install in French when the tablet is French. None of that is declared to iOS. Companion to S8-F1/S14R-M2 (the Flutter-side localisation gap) and to S8-F5 (the name is inconsistent across the four places an OS reads it). |
MEDIUM count: 88 rows.
4. LOW
| ID | Title | Final severity | Original severity | Verdict | File:line (at 03a176e) |
Evidence path | Refuter note |
|---|---|---|---|---|---|---|---|
| S1-F11 | reorder is asymmetric: dragging forward drops after the target, dragging back drops before it |
LOW | LOW | CONFIRMED | lib/engine/engine.dart:418-426, quoted lines :422-423 |
findings/S1_engine.md · agent_reports/S1_refute.md |
Probe measures [a,b,c] → reorder('a','c') → [b,c,a] and reorder('c','a') → [c,a,b]. test/engine_test.dart:335-339 asserts only the first direction. |
| S1-F12 | spawnClone will start a batch of a dish that is not running |
LOW | LOW | CONFIRMED | lib/engine/engine.dart:191-200 — parent check at :192-193, cap check at :194, no run check; startTimer at :198 runs regardless; contract at :189 |
findings/S1_engine.md · agent_reports/S1_refute.md |
Reproduced. |
| MISS-3 (S1's refuter) | One user action issues two full persist cycles, and one beat issues one per firing dish | LOW | — (new) | New (refuter contribution) | lib/engine/engine.dart:212 and :414 (delete), :212 and :403 (save), :285 (per-entry in tick) |
agent_reports/S1_refute.md §5 |
Three dishes landing on the same 150 ms beat produce three full persist cycles. |
| S2-F20 | models.dart points readers at a symbol that does not exist |
LOW | LOW | CONFIRMED | lib/engine/models.dart:13 (_kZoneSoundMigration, never declared) |
findings/S2_persistence.md · agent_reports/S2_refute.md |
grep -rn "_kZoneSoundMigration" lib/ → single hit, the comment itself. |
| S3-F19 | assetFor carries a rewrite no tone can reach |
LOW | LOW | CONFIRMED for in-set tones; contradicted for out-of-set tones by S6R-F1 |
lib/audio/audio.dart:83-84 |
findings/S3_audio_alarms.md · agent_reports/S3_refute.md · agent_reports/S6_refute.md |
S3's refuter verified assetFor :83-84. S6's refuter proved an out-of-set tone name does reach it, unvalidated, from the migration and from fromJson. Both are true of different input sets — see CONTRADICTIONS.md §7. Do not delete the rewrite branch until S6R-F1's validation is in place. |
| S4-F16 + S6-F3 | Dead symbols in the UI layer: two dead colour constants in the palette | LOW | LOW (both) | CONFIRMED | lib/ui/theme.dart:23 (C.mint) and :29 (C.logoInk); also lib/ui/grid_layout.dart:42, :94 |
findings/S4_ui.md · findings/S6_dry_deadcode.md · agent_reports/S6_refute.md |
Found twice — S4 handed it to S6, and S6's refuter proved it with an automated sweep over 123 public declarations: one line each, the declaration; raw values 0FA96A/F5F1E8 appear nowhere else, including Kotlin, Swift, XML and test/; no dart:mirrors/noSuchMethod/Function.apply anywhere, so no dynamic lookup is possible. |
| S4-F17 | A timer left ringing overnight prints a five-digit minute counter that the tile was never sized for | LOW | LOW | UNREFUTED — verdict provisional | lib/ui/theme.dart:77-82 (fmtTime / fmtUp), consumed at lib/ui/tile.dart:200 |
findings/S4_ui.md |
S7's refuter independently verified the formatter's behaviour at boundaries while auditing the theme_test.dart spec (fmtTime(-5) is '0:00'). Overlaps S8-F7. |
| S5-F10 | Two empty catch blocks | LOW | LOW | CONFIRMED (fix reasoning corrected) | lib/journal.dart:128, :231 |
findings/S5_error_handling.md · agent_reports/S5_refute.md |
Verbatim-exact. :231 genuinely is best-effort after the primary copy succeeded at :226. :128 genuinely degrades device to Platform.operatingSystem, and that string is stamped at journal.dart:84, shown at modals.dart:683, used for the export filename at journal.dart:215-223 and the share subject at modals.dart:707 — all four confirmed. The fix's reasoning is wrong in detail (_file is assigned at :73, before device at :75, so Journal.log would be admitted by the :134 guard) but the recommended remedy is fine anyway. |
| S6-F7 | The 7000 ms first voice gap is written three times, in two files that cannot see each other | LOW | MEDIUM | SEVERITY CHANGED (MEDIUM → LOW); consequence REFUTED | lib/engine/engine.dart:52, lib/engine/models.dart:114, :139 |
findings/S6_dry_deadcode.md · agent_reports/S6_refute.md · proof/01_findings/S6_refute/mutation_firstvoicegap_3000.txt |
The three literals exist. The stated mechanism is false: RunEntry.toJson writes 'voiceGap' unconditionally (models.dart:126), so a restored entry carries its stored value, and _ring overwrites voiceGap = firstVoiceGapMs on every ring (engine.dart:283). Its "How to prove the fix" is also wrong: the refuter mutated 7000→3000 and exactly one test went red — test/engine_test.dart:185 — while test/robustness_test.dart:291, the test S6 names, passed. That line's 7000 is an injected freeze duration, not a voice gap. |
| S6-F8 | The running and paused remaining-time computation is written twice in tile.dart |
LOW | LOW | CONFIRMED | lib/ui/tile.dart:185-198 |
findings/S6_dry_deadcode.md · agent_reports/S6_refute.md |
Four of six lines identical, including the t.steps! unwrap and the clamp. |
| S6-F10 | Four styling duplications inside modals.dart |
LOW | LOW | PARTIALLY REFUTED — 3 of 4 confirmed | lib/ui/modals.dart:253-258 vs :338-343 (byte-identical, CONFIRMED); :482-492 vs :535-545 (style blocks byte-identical, decorations differ by one number, CONFIRMED); _dashedAdd drawing BorderStyle.solid (CONFIRMED, a name that lies); :54-62 vs :719-727 REFUTED |
findings/S6_dry_deadcode.md · agent_reports/S6_refute.md |
_fieldLabel and _settingLabel share no value at all — size 10.9 vs 14.7, tracking 2.2 vs 2.3, colour muted vs text, gap 8 vs 12. Only the six-line shape matches. S6's proposed merge gives the function four optional parameters that all three settings call sites must override — more code and a worse API than two honest functions. Do not implement (a). |
| S6-F11 | The tile drop shadow and the corner-radius formula are each written twice in tile.dart |
LOW | LOW | CONFIRMED | lib/ui/tile.dart:441-446 vs :696-701 (shadow); :438-439 vs :598 (radius formula); :218 vs :798 (the literal 20) |
findings/S6_dry_deadcode.md · agent_reports/S6_refute.md |
Shadow blocks byte-identical (0x291C211C, blur 4, offset 0,1). math.min(10, 2.6*ch) vs math.min(10.0, 2.6*ch). BorderRadius.circular(20) vs Radius.circular(20 - inset) is a genuine cross-class coupling: the edit-mode outline must track the card's corner. |
| S6-F12 | The widget-test "open a modal" harness is copied four times, the viewport override twice | LOW | LOW | CONFIRMED | test/announcement_test.dart:200-222 and :189-197; test/editor_layout_test.dart:22-34 and :41-47; test/volume_test.dart:137-158 and :169-183 |
findings/S6_dry_deadcode.md · agent_reports/S6_refute.md |
Four MaterialApp → Builder → TextButton(Text('open')) scaffolds; two 5-line viewport blocks differing only in Size. |
| S6-F13 | The duration-preset label is duplicated across the production/test boundary | LOW | LOW | CONFIRMED | lib/ui/modals.dart:302-304 and test/editor_layout_test.dart:17-20 |
findings/S6_dry_deadcode.md · agent_reports/S6_refute.md |
The test's own comment admits the coupling. A test that reimplements the expression cannot detect a change in it. S7's refuter separately mutated the production expression and found both variants go red, so the helper "fails closed" — the duplication is a maintainability defect, not a hole. |
| S6-F14 | ChivoMono-Medium.ttf is declared, bundled and never selected |
LOW | LOW | CONFIRMED — and now proven | pubspec.yaml:61-62; file assets/fonts/ChivoMono-Medium.ttf |
findings/S6_dry_deadcode.md · agent_reports/S6_refute.md |
The refuter ran the experiment S6 only proposed: requested mono weights across the editor (both modes), the settings dialog, the tile in all five status states and the header are exactly {w400, w700}, and removing the Medium face leaves w400 and w700 rasterising to byte-identical pixels. Safe to delete. |
| S6-F17 | The alarm-acknowledgement journal line is written twice in home.dart |
LOW | LOW | CONFIRMED | lib/ui/home.dart:366-371 and :656-661 |
findings/S6_dry_deadcode.md · agent_reports/S6_refute.md |
:366-371 measures against now(), :656-661 against nowMs (the last build's timestamp, up to one 150 ms tick stale). Same headline metric, two clocks — that is a correctness difference, not only a duplication. |
| S6-F18 (pad2 half) | Four independent copies of a two-digit zero-pad | LOW | LOW | REFUTED | lib/journal.dart:57 (p), :62 (_day), :222-223; lib/ui/header.dart:145, :147; lib/ui/modals.dart:399, :502, :704; lib/ui/theme.dart:79 |
findings/S6_dry_deadcode.md · agent_reports/S6_refute.md |
n.toString().padLeft(2, '0') is a library idiom, not logic. No change to any one of the ten sites would ever have to be made at the others — padding an hour in a filename and padding a minute on a clock face are independent. S6's own fix needs two helpers (pad2 in theme.dart plus Journal._pad2, because journal.dart must not import UI), i.e. it replaces ten copies with two copies plus a layering rule. Do not implement. The title half of S6-F18 is CONFIRMED and lives in the S3-F14 row. |
| S6-F19 + S14-F6 (→S10) + S2-F19 | README.md documents the v0.1 architecture and contradicts the shipped app, including on the app's single most important safety property |
LOW | LOW (S6) / MEDIUM (S14) / LOW (S2) | CONFIRMED, and understated | README.md:1-38 (dated "Last update: 2026-07-22"; pinned commit is v0.4.12); README.md:30-34 (the safety-property contradiction); README.md:13-14 (4 keys vs the 9 static const _k declarations in lib/engine/store.dart:15-24 plus lib/journal.dart:23-24) |
findings/S6_dry_deadcode.md · findings/S14_entry_unowned.md · findings/S2_persistence.md · agent_reports/S6_refute.md · agent_reports/S14_refute.md |
Found three times. S6's refuter verified all five contradictions verbatim and found a sixth (the key count, which is S2-F19). S14's refuter confirmed it independently by re-running git log and the key/test counts, and reassigned ownership to S10. The final grade takes S6's LOW; S14's MEDIUM rested on the safety-property contradiction at README.md:30-34, which is the sentence Phase 4 must fix first. |
| S6-F20 + S14-F5 | analysis_options.yaml is the untouched Flutter template: it carries an empty linter: rules: block, and flutter analyze reporting 0 issues measures almost nothing |
MEDIUM | LOW (S6) / MEDIUM (S14) | CONFIRMED — exactly 75 issues | analysis_options.yaml:12-25 (the empty block); :1-29 (the whole file) |
findings/S6_dry_deadcode.md · findings/S14_entry_unowned.md · agent_reports/S6_refute.md · agent_reports/S14_refute.md §2.5 |
Found twice. S14's refuter reproduced the 75-issue figure twice — once with S14's saved candidate file and once with only the six named rules. S6's refuter read the block in full and confirmed it is the unmodified template. Take the MEDIUM. And add S14R-M1: the analyzer: language: dimension was never measured and hides six ERROR-severity type holes. |
| S6-F21 + S9-F12 + S14-F7 (→S6) | The web/ target is 7 files of unmodified stock Flutter template, shipping "A new Flutter project." and Flutter-blue branding, for a platform the product does not target |
MEDIUM | LOW (S6) / MEDIUM (S9) / MEDIUM (S14) | CONFIRMED — delete it, nothing in the build objects | web/index.html:21, :26, :32; web/manifest.json:2-9; web/favicon.png; web/icons/* (4 files) |
findings/S6_dry_deadcode.md · findings/S9_platform_config.md · findings/S14_entry_unowned.md · agent_reports/S6_refute.md · agent_reports/S14_refute.md §2.6 |
Found three times, from the dead-code, platform-config and app-entry sides, and the prior audit's A1-5/A1-R6 make it four. S6's refuter: git log -- web/ returns exactly one commit, 22902e0, the v0.2.0 import. S14's refuter went furthest and proved the deletion is safe: 7/7 byte-identical to a fresh template, zero references, and after deletion flutter analyze = 0, suite = 123/123, flutter build apk --release = exit 0. Deleting web/ also closes S11-F4 and S9R-M02. |
| S6-F22 | uses-material-design: true ships an icon font for zero icons |
LOW | LOW | CONFIRMED | pubspec.yaml:42 |
findings/S6_dry_deadcode.md · agent_reports/S6_refute.md |
grep -rn "Icon(\|Icons\.\|IconData\|IconButton" lib/ → zero hits. The only Material widget that renders is Slider (modals.dart:655), which draws no icon. |
| S6R-F2 | --obfuscate --split-debug-info removes 851,968 bytes and was never measured |
LOW | — (new) | New (refuter contribution) | README.md:25 (the documented build command), android/app/build.gradle.kts:30-36 |
agent_reports/S6_refute.md · proof/01_findings/S6_refute/apk_obfuscated_split_debug_info.txt |
The Dart AOT snapshot ships with its symbol table intact. The flag strips it into a side file uploaded to Play for crash de-obfuscation. Subtractive, changes no behaviour, needs no product decision — unlike the 1,664,814-byte audio saving S6 reported, which requires re-validating every tone by ear. |
| S6R-F3 | lib/ui/home.dart:124 is a twelfth RunStatus decision site and S6's map has no entry for it |
LOW | — (new) | New (refuter contribution) | lib/ui/home.dart:124-133 |
agent_reports/S6_refute.md |
The twelfth site is the good one — an exhaustive switch expression with no default. It is the reason S6-F9's proof protocol is wrong and the working example the fix should be modelled on. Omitting it makes the codebase look uniformly worse than it is. |
| S6R-F4 | The picker's five-second granularity is a fourth encoding of the 5-second floor | LOW | — (new) | New (refuter contribution) | lib/ui/modals.dart:436-437 |
agent_reports/S6_refute.md |
The seconds stepper moves in fives, so the smallest non-zero value the picker can express is five seconds. Centralise the floor without these two lines and the picker keeps offering 0:05 under a 10-second floor — the editor displays one number and commits another. Same two lines as M2. |
| S7-F17 | Three tests silently depend on the process working directory | LOW | LOW | CONFIRMED (not individually re-graded) | test/source_hygiene_test.dart:11, test/version_test.dart:16 |
findings/S7_tests.md |
— |
| S7-F18 | Global static state is reset inconsistently across test files | LOW | LOW | CONFIRMED (not individually re-graded) | test/announcement_test.dart, test/editor_layout_test.dart |
findings/S7_tests.md · agent_reports/S7_refute.md §7 |
The refuter's 13 independent runs found no order dependence today, which bounds the risk without removing it. |
| S7R-F7 | The logo_test.dart specification claims a guard its own assertion cannot deliver |
LOW | — (new) | New (refuter contribution) | findings/S7_tests.md §4.6; subject lib/ui/logo.dart:12-16, pubspec.yaml |
agent_reports/S7_refute.md §5, §6 |
The spec asserts AssetImage.assetName == 'assets/logo/mark_white.png' and says this "catches the asset being dropped from pubspec.yaml". It cannot — assetName is simply the string literal handed to Image.asset, identical whether or not the bundle declares the asset. The test is falsifiable but the failure mode it is named for would sail through. Rewrite the spec before implementing. |
| S8-R3 (S8's refuter) | The migration's own comment states an invariant the migration violates | LOW | — (new) | New (refuter contribution) | lib/engine/store.dart:213-214 |
agent_reports/S8_refute.md §2 |
The doc comment on repairGeneratedPhrases claims it "changes NOTHING about what the app says today (the same words are regenerated)". Companion to S2-F10, which its own refuter confirmed: the migration does delete the operator's own sentence if it re-runs. Two independent refuters now contradict the same comment. |
| S8-R5 (S8's refuter) | Three wrong file:line citations and five wrong line counts, all contradicting the shared code map |
LOW | — (new) | New (refuter contribution) | S8-F5 "Location"; S8 §8 coverage manifest | agent_reports/S8_refute.md §2 |
AndroidManifest.xml is 79 lines (S8 says 74), ios/Runner/Info.plist 70 (S8 says 78), web/index.html 46 (38), web/manifest.json 35 (33), pubspec.yaml 68 (69). Every one of these is correct in research/00_code_map.md:1641,1644,1647,1649,1650, the shared map every stream was told to use rather than re-derive. Every Dart line count in S8's manifest is right; only the non-Dart ones are wrong, which is consistent with their not having been measured. Note S13-R5 and S3's manifest carry the same class of slip on the same two files. |
| S8-R6 (S8's refuter) | "32 hardcoded user-visible strings" counts glyphs, operator text and unreachable fallbacks | LOW | — (new) | New (refuter contribution) | S8 §3 and the summary table | agent_reports/S8_refute.md §2 |
The refuter opened all 39 cited sites: every literal S8 lists is present at the line it gives, and no row is fabricated. But the headline count conflates three categories, and only ~15 are translation defects. Publish 15, not 32. |
| S13-R4 (S13's refuter) | The permission enumeration is wrong in both directions | LOW | — (new) | New (refuter contribution) | S13 §3 layer 2 | agent_reports/S13_refute.md §3 · proof/01_findings/S13_refute/apk_manifest_decoded.txt |
Decoded with apkanalyzer manifest print rather than a byte scan: eight android.permission.* requests plus one custom dev.sergemio.cadence.DYNAMIC_RECEIVER_NOT_EXPORTED_PERMISSION. DUMP is not a request — it is android:permission="android.permission.DUMP" guarding the androidx.profileinstaller ProfileInstallReceiver. S13 also flattens SCHEDULE_EXACT_ALARM, which ships android:maxSdkVersion="32". This is the artifact a restaurant's IT person reads on the Play listing; it has to match permission for permission. |
| S13-R5 (S13's refuter) | Two wrong file:lines, two wrong line counts, and an internal 13-vs-14 contradiction |
LOW | — (new) | New (refuter contribution) | S13-F2 "Location" (:20-23, actual :18-21); S13 §7 coverage manifest (AndroidManifest.xml 79 not 74, Info.plist 70 not 78); §1 vs §6 (14 rows vs "the 13 items in §1") |
agent_reports/S13_refute.md §3 |
Both line counts are correct in research/00_code_map.md:1644,1647. The count should read "13 items on a current install, 14 including the legacy zones key that the v0.4.11 migration removes at store.dart:283". |
| S9-F13 | Stale TODO above a correct application id invites an irreversible change |
LOW | LOW | CONFIRMED | android/app/build.gradle.kts:20-21 |
findings/S9_platform_config.md · agent_reports/S9_refute.md |
An application id cannot be changed after first publication. Delete the TODO. |
| S9-F14 + S14-F11 (→S7) | ios/RunnerTests/RunnerTests.swift is a compiled test target whose single test asserts nothing, wired into the shared scheme's test action |
LOW | LOW (both) | CONFIRMED | ios/RunnerTests/RunnerTests.swift:7-10; 19 RunnerTests references in ios/Runner.xcodeproj/project.pbxproj |
findings/S9_platform_config.md · findings/S14_entry_unowned.md · agent_reports/S9_refute.md · agent_reports/S14_refute.md |
Found twice. S14's refuter confirmed the file is STOCK-TEMPLATE and counted the 19 project references; ownership reassigned to S7 per S14R-M3. |
| S9-F15 | org.gradle.jvmargs demands 12 GB of JVM memory |
LOW | LOW | CONFIRMED | android/gradle.properties:1 |
findings/S9_platform_config.md · agent_reports/S9_refute.md |
— |
| S9-F16 | The two OEM quick-boot actions on the boot receiver cannot be delivered to a non-exported receiver | LOW | LOW | PARTIALLY REFUTED — facts confirmed, causal claim and proposed fix REFUTED | android/app/src/main/AndroidManifest.xml:48-56 |
findings/S9_platform_config.md · agent_reports/S9_refute.md §Part 5 |
The facts hold; the reason S9 gives for them and the fix it proposes are both wrong. Do not implement S9-F16's fix as written — re-derive it from the refuter's Part 5 against https://developer.android.com/guide/topics/manifest/receiver-element. |
| S9R-M01 + S14-F9 (→S9) | The iOS launch image is the untouched Flutter template on both platforms, and S9's own manifest records the opposite | LOW | — (new, S9R) / LOW (S14) | New (refuter contribution) + CONFIRMED (S14-F9) | ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png, LaunchImage@2x.png, LaunchImage@3x.png; android/app/src/main/res/drawable/launch_background.xml:4 |
agent_reports/S9_refute.md §Part 6 · findings/S14_entry_unowned.md · agent_reports/S14_refute.md |
Found twice, and one of the two sightings corrects a false negative in the audit itself. S9's coverage manifest claimed the three iOS launch images "all three differ, so the launch image was customised". Both statements are false — S9 compared against the wrong template location; measured against flutter_template_images-5.0.0 all three are byte-identical, and all three are 1×1 pixel images. S14's refuter independently found all six launch files STOCK-TEMPLATE and noted the three iOS PNGs share one SHA-256 at 68 bytes each. |
| S9R-M02 | The two maskable web icons are still the Flutter logo, while the other three web icons were replaced | LOW | — (new) | New (refuter contribution) — partly contradicted by S11's refuter | web/icons/Icon-maskable-192.png and web/icons/Icon-maskable-512.png; consumers at web/manifest.json:22-33 |
agent_reports/S9_refute.md §Part 6 · agent_reports/S11_refute.md §6.1 |
Both refuters agree the two maskable icons are byte-identical Flutter template. They disagree about the other three — see CONTRADICTIONS.md §4. The finding closes either way if web/ is deleted (S6-F21 merged row). |
| S10-F13 | Below 150 px of tile width the app removes its own touch-target floor | LOW | LOW | CONFIRMED, and understated | lib/ui/tile.dart:589-597 (S10 cited :565-597) sets minH = 0 in the bottom tier; button geometry at :678 |
findings/S10_product.md · agent_reports/S10_refute.md |
Geometry replicated independently: 800×540 crosses 150 px at n=21 (tile 127×108) and 1280×740 at n=49 (tile 136×116); the pinned 4-dish case reproduces exactly (cols 2, tileW 602, rowH 332, gap 21, pad 27). S10's "roughly 37 px" is optimistic: ±10 s measures 35 px and the ✕ 31.6 px including hit padding. Corroborates S4-F03. |
| M3 (S10's refuter) | The dish name is silently capped at 24 characters with the counter switched off | LOW | — (new) | New (refuter contribution) | lib/ui/modals.dart:251-252 (maxLength: 24 with .copyWith(counterText: '')) |
agent_reports/S10_refute.md §6 |
The field stops accepting keystrokes at 24 characters and shows nothing to say why. The cap is not a layout constraint — the tile deliberately auto-shrinks long names rather than truncating (tile.dart:393-395, "un nom coupé ne sert à rien en cuisine"), so the display would have absorbed a longer name. Complements S4-F02 (the shrink has no floor). |
| S11-F6 | Big Shoulders Display has been retired from Google Fonts, so the bundled version cannot be re-fetched from the family page | LOW | LOW | CONFIRMED | pubspec.yaml:48-56; assets/fonts/BigShouldersDisplay-*.ttf |
findings/S11_asset_licensing.md · agent_reports/S11_refute.md §2.1 |
ofl/bigshouldersdisplay still carries the family and the licence; the variable master re-instances to the bundled binaries. Provenance is recoverable. |
| R-F4 (S11's refuter) | Citation defects in S11: three pubspec.yaml line references |
LOW | — (new) | New (refuter contribution) | S11 cites pubspec.yaml:33-34 for the icon-origin comment (used 4×) — actual :31-32; S11 cites :36 for adaptive_icon_background — actual :37 |
agent_reports/S11_refute.md §7 |
Full cat -n pubspec.yaml (68 lines) recorded. Correct before publishing; pubspec.yaml:33 is flutter_launcher_icons: and :34 is android: true. |
| R-F5 (S11's refuter) | The NOTICES character count is method-dependent | LOW | — (new) | New (refuter contribution) | NOTICES — S11 and the draft both publish 1,381,653; reading the same bytes without newline translation gives 1,381,705 |
agent_reports/S11_refute.md §7 |
The file contains exactly 52 CRLF pairs; S11 read it in text mode, which collapsed them. Neither figure is wrong, but a digit-bearing claim under R1 must say which measurement it is. |
| R-F6 (S11's refuter) | assets/icon/*.png are build inputs, not shipped bundle assets |
LOW | — (new) | New (refuter contribution) | pubspec.yaml:44-46 lists only assets/audio/ and assets/logo/ |
agent_reports/S11_refute.md §7 |
The three assets/icon/ PNGs are inputs to flutter_launcher_icons and reach the APK only as Android resources. No licence consequence — they are author-original — but the distinction matters for any claim about what is "distributed". |
| R-F7 (S11's refuter) | DSEG's name ID 0 is not a copyright statement |
LOW | — (new) | New (refuter contribution) | assets/fonts/DSEG7Classic-Bold.ttf, name ID 0 (Created by Keshikan\nwith FontForge 2.0…); the copyright statement is in ID 13 |
agent_reports/S11_refute.md §7 |
S11 §2.4 asserts "All seven binaries … name a copyright holder". The conclusion holds; the supporting statement does not, per-record. |
| S12-F1 | Unmounting while _boot() is awaiting orphans the 150 ms ticker permanently |
LOW (today) — HIGH the day a second route exists | HIGH | SEVERITY CHANGED (HIGH → LOW) | lib/ui/home.dart:154 (creation; the reproduction binding names home.dart:154:21), :236-243 (dispose), :136, :141, :148, :149 (the awaits it sits behind) |
findings/S12_lifecycle.md · agent_reports/S12_refute.md §2 · proof/01_findings/S12_refute/repro_s12_w7_boot_race.txt |
Reproduced red (EXIT_CODE=1). HIGH rested on "two ticks running concurrently … a battery that drains twice as fast", and S12's own grep proves that cannot happen at this commit: one HomeScreen construction site (main.dart:55), one route, no full-screen push, showDialog pushes over without disposing, and AndroidManifest.xml:31's configChanges means no ordinary configuration change recreates the activity. _HomeScreenState.dispose() has exactly one production trigger and it destroys the isolate the orphan lives in. |
| S12-F3 | Backstop owns a 300 ms timer but has no disposal, and arms an OS alarm after teardown |
LOW | MEDIUM | REFUTED on harm | lib/alarm_backstop.dart:41 (field), :146-147 (creation); no disposal member exists |
findings/S12_lifecycle.md · agent_reports/S12_refute.md §2.1 |
The missing disposal member is confirmed. The harm claim is backwards: the alarm armed after teardown lands at the operator's new deadline for a timer that is still running and already persisted — the safety net doing its job at the moment it matters most. alarm_backstop.dart:125-126 warns about arming for a stopped timer; this is not that case, and Backstop.init() calls _plugin.cancelAll() at :89 on the next launch. Its proposed fix actively harms the product — see S12R-F3. |
| S12-F4 | Journal.init() overwrites its two periodic timers without cancelling the previous pair |
LOW | MEDIUM | SEVERITY CHANGED (MEDIUM → LOW) | lib/journal.dart:106-107; the only cancel site is :241-242, inside a @visibleForTesting member |
findings/S12_lifecycle.md · agent_reports/S12_refute.md §2 |
Reproduced (SOAK A5: timers created: 4 … STILL-LIVE periodic timers: 2). main() is the only caller and an activity recreation gives a fresh isolate; the only in-isolate second init() is a debug hot restart. |
| S12R-F2 | Journal.snapshot is a static closure over _HomeScreenState that is never cleared, so a torn-down screen and its five AudioPlayers are retained for the life of the isolate |
LOW | — (new) | New (refuter contribution) | lib/ui/home.dart:93-103 (set), lib/journal.dart:40 (the static field), lib/ui/home.dart:236-243 (dispose, which does not clear it) |
agent_reports/S12_refute.md §5 |
The retention mechanism for everything in the S3-F15/S12-F5 row. Same latency caveat as the other lifecycle findings. |
| S14-F10 | SystemUiMode.immersiveSticky is set once and never re-asserted, and no kiosk or screen-pinning mechanism exists, so a cook can leave the timer board mid-service |
LOW | LOW | CONFIRMED; ownership contested (S9/S10 for the manifest half) | lib/main.dart:33, android/app/src/main/AndroidManifest.xml:22-30 |
findings/S14_entry_unowned.md · agent_reports/S14_refute.md §1 |
Greps re-run and recorded. Not among the five findings S14R-M3 reassigns, but its refuter notes the manifest half belongs to S9/S10. |
LOW count: 44 rows.
5. INFO — recorded non-defects and reported decisions
These are not defects. They are in the register so the downstream team does not re-derive them.
| ID | Title | Final severity | Original severity | Verdict | File:line (at 03a176e) |
Evidence path | Refuter note |
|---|---|---|---|---|---|---|---|
| S2-C1 | Concurrency: checked, no defect found | INFO | n/a (checked-clear) | CONFIRMED | lib/engine/store.dart:133-154, lib/journal.dart:163-180 |
findings/S2_persistence.md · agent_reports/S2_refute.md |
Probes W2/W3 reproduce; the single-isolate reasoning and the disjoint-key argument hold for the three data keys. Scope limit: it does not cover the clones→run coupling in S2R-M1, which is a load-ordering issue, not a concurrency one. |
| S3-F18 | No defect: the past-deadline guard, the debounce, the exact-to-inexact degradation, the reboot receiver and the no-double-ring behaviour are all correct | INFO | LOW (informational) | CONFIRMED | lib/alarm_backstop.dart:181, :143-148, :208-220 |
findings/S3_audio_alarms.md · agent_reports/S3_refute.md |
The refuter mutated five of these mechanisms (_nid, Importance.max, the onForeground cancel loop, the iOS settings, the chain sum) and all five were detected. It also probed the onForeground/sync interleave for a cancel-after-rearm race and found it unreachable. Note the tension with S7-F4, which proves the past-deadline guard is not what makes its own test pass — the mechanism is correct, its test is not. |
| S4-F18 | What was checked and found sound | INFO | LOW (recorded so the coordinator can see the negative space) | UNREFUTED — verdict provisional | — | findings/S4_ui.md |
— |
| S6-F23 | REPORTED, not proposed: the 16 WAVs ship uncompressed | INFO | LOW | CONFIRMED as a non-finding | assets/audio/*.wav (15 files), android/app/src/main/res/raw/cadence_alarm.wav |
findings/S6_dry_deadcode.md · agent_reports/S6_refute.md |
Correctly framed under R6 as a product decision, not a defect. The 1,664,814-byte saving requires re-validating every tone by ear; S6R-F2 offers 851,968 bytes with no product decision at all. |
6. Prior-audit findings still open
From research/01_prior_work.md §2.5: 7 items STILL OPEN in whole or in part from the 2026-07-19 Codex
audit of sergemio/cadence-kitchen-timer (source A1) and its recommendation list. They are in the register
because they are unclosed obligations, not new discoveries.
| ID | Prior finding | Prior grade | Status at 03a176e |
Where it lands in this audit | Evidence path |
|---|---|---|---|---|---|
| A1-1 (iOS half) | "Production alarm delivery is not guaranteed when hidden or locked — critical" | critical | FIXED on Android / STILL OPEN on iOS | Merged into the BLOCKER row S3-F1 + S9-F07. alarm_backstop.dart:73-76 passes AndroidInitializationSettings only, so init() leaves _ready false on iOS and every scheduling path returns immediately (sync :123, showNow :253-256) |
research/01_prior_work.md §2.1 |
| A1-3 (CI half) | "Verification is not reproducible from the repository — high" | high | PARTLY FIXED — the CI half is STILL OPEN | No row in §1–§5 covers this — it is an open obligation with no owning stream. 13 test files / 123 tests and a dependency manifest now exist, but find . -path '*.github*' returns nothing: no .github/workflows/, no analyze job, nothing that runs the suite other than a human typing flutter test |
research/01_prior_work.md §2.1 · proof/00_baseline/test.txt |
| A1-4 / A1-R5 (export half) | "Persisted data has no schema validation or recovery — high … there is no export/import or backup" | high | PARTLY FIXED — export/import/backup STILL OPEN | Merged into S10-F1 + S13-F4. Validation and recovery are FIXED (store.dart:91-113, :119-128, :34-59, :133-147); grep -riE 'export\|backup\|restore' lib/ returns only journal.dart:207, which exports the flight recorder, not the configuration |
research/01_prior_work.md §2.1, §2.2 |
A1-5 / A1-R6 (web/ half) |
"Localization metadata and behavior disagree — medium" | medium | FIXED for the shipped app / STILL OPEN for the web/ shell |
Merged into S6-F21 + S9-F12 + S14-F7. web/manifest.json:2-7 reads "name": "cadence", "description": "A new Flutter project.", theme_color/background_color #0175C2 (Flutter blue, not the app's beige #E7DED0) |
research/01_prior_work.md §2.1, §2.2 |
| A1-6 | "Core interaction surfaces are touch-only and lack dialog semantics — medium" | medium | STILL OPEN — the largest of the seven | Same defect as S4-F08. grep -rn 'Semantics\|semanticLabel\|excludeSemantics\|tooltip' lib/ → zero matches across 4,891 lines. Tiles are raw GestureDetectors (tile.dart:372-374, :427-429, :604, :613, :620, :679); modals are showDialog → Dialog( (modals.dart:13, :16) with no semanticLabel; every control label is a glyph string (i18n.dart:41-43). This is the single largest v2 decision (A4 §14-15) the Flutter line did not carry over |
research/01_prior_work.md §2.1, §2.4 |
| A1-G2 | Device gap: "a device run that starts a short timer, backgrounds or locks the device, and records whether sound begins at the registered deadline" | — (open gap) | STILL OPEN, and explicitly so | No row in §1–§5 covers this — it needs hardware, not code. Commit f46d142 states verbatim: «Reste a valider sur tablette : le secours OS n'a jamais reellement sonne dans ce log (toujours annule avant l'echeance), aucun force-stop / ecran eteint long / eco d'energie / reboot -> les modes de panne restent a prouver.» Nothing later claims to have closed it. This is the verdict-flipping test for the whole product and it has never been run in this workspace |
research/01_prior_work.md §2.3 |
| A1-G3 | Device gap: "a device run that forces wake-lock release or power-saving mode and verifies operator-visible recovery" | — (open gap) | STILL OPEN | Touches S14-F4 (the wakelock banner exists but can never clear, and a silently ineffective wakelock produces no banner at all), which its refuter proved by driving the real main(). The device half remains unrun |
research/01_prior_work.md §2.3 |
7. Counts
| Bucket | Rows |
|---|---|
| BLOCKER | 8 |
| HIGH | 45 |
| MEDIUM | 88 |
| LOW | 44 |
| INFO | 4 |
| Register total (§1–§5) | 189 |
| Prior-audit open items (§6) | 7 |
| Grand total | 196 |
| Cut | Count |
|---|---|
| Rows contributed by refuters rather than streams | 42 |
| Rows whose severity the refuter changed | 22 |
| Rows carrying a REFUTED verdict, in whole or in part | 21 |
| — of which fully REFUTED (do not implement) | 2 (S6-F18 pad2 half; S12-F3 harm claim) |
| Merge groups (findings found independently by 2+ sources) | 25 |
| Source findings folded into those 25 rows | 57 |
Rows marked UNREFUTED — verdict provisional |
48 |
| Unrefuted streams | 3 (S4, S8, S13) |
8. What the downstream team must not do
Extracted from the refutations, because these are the fixes that would make the product worse or waste the work.
- Do not implement
S1-F1's proposed fix as written. It was tested and is defective (probe R2b). - Do not implement
S9-F03's fix. AddingUIBackgroundModesdoes not fix the blocking condition; the fix belongs toS9-F07. - Do not implement
S3-F3's fix (1) (replyresult.error(…)). Proven inert — theSecurityExceptioncannot fire at that call site. - Do not adopt
fix_announce_timer_leak.patchas written (S12R-F3). It leaves the OS alarm on a stale deadline — a BLOCKER-class trade for a duplicate notification that was never wrong. - Do not implement
S6-F18's pad2 centralisation. Ten library idioms become two copies plus a layering rule. - Do not implement
S6-F10(a)._fieldLabeland_settingLabelshare no value; merging them needs four optional parameters. - Do not implement
S12-F8's fix (b). A black overlay dismissed by touch is a new end-user feature and violates R6. - Do not implement
S10-F6's fix. It inverts a written decision (// count-up baseline = the ACTUAL ring instant, not the deadline) without refuting it. Implement the residue instead: lateness is computed and never shown. - Do not implement
S9-F16's fix. The causal claim behind it is refuted. - Do not remove the 300 ms backstop debounce (
alarm_backstop.dart:36-39).S10-F8's claim that it is now obsolete is refuted; 30 taps in a few seconds is exactly the burst it collapses. - Do not delete
keep.xml(S6-F16 + S9-F11). It is belt-and-braces today and load-bearing the moment shrinking is enabled. - Do not populate
NSPrivacyAccessedAPITypesif a privacy manifest is added. Apple binds you to what you declare; an empty declaration is safe, an inaccurate one is not. - Do not commit
findings/S11_LICENSES_draft.md. NOT APPROVED — 7 corrections required (agent_reports/S11_refute.md§8). - Do not implement the
logo_test.dartspecification as written (S7R-F7). Its assertion cannot deliver the guard it claims. - Do not treat
test/robustness_test.dart:149as a passing test to preserve when fixingS1-F2 + S5-F1. It enshrines the swallow-and-drop as desired behaviour and must change with the fix. - Do not add
EngineHost.onEngineFaultas an abstract member. It breaks every implementer inlib/andtest/; it needs a concrete empty default body.