Cadence — Code Map (Phase 0.7)
Subject: the app repository
Pinned commit: 03a176e72ef0075eec86b8915cbe6e93042a3b9d — v0.4.12 : le journal annoncait la mauvaise build
Version: 0.4.12+18 (pubspec.yaml:5)
Toolchain: Flutter 3.44.8 / Dart 3.12.2
Scope read: 18 files in lib/ (4,853 lines), 13 in test/ (2,313 lines), plus pubspec.yaml,
analysis_options.yaml, android/app/build.gradle.kts, android/app/src/main/AndroidManifest.xml,
android/app/src/main/kotlin/dev/sergemio/cadence/MainActivity.kt, ios/Runner/AppDelegate.swift,
ios/Runner/Info.plist, tools/build_ringtones.py, web/.
This document is descriptive only. It records structure, symbols, call graph, and neutral structural
facts. It contains no judgements, severities, or recommendations. Source comments are French; the
map is written in English and quotes code verbatim where the exact text matters.
1. System overview
Cadence is a single-screen Android/iOS Flutter app that runs a board of independent kitchen timers,
each rendered as a tile whose background is a full-tile pie wedge counting down (lib/ui/tile.dart).
A timer is either single (one duration) or chained (two or more named steps), and any running
dish can be duplicated into up to two extra "batch" clones sharing the parent's definition
(Engine.maxBatch = 3, lib/engine/engine.dart:31). main() (lib/main.dart:22) initialises the
Flutter binding, opens SharedPreferences through Store.open(), boots the persistent flight
recorder Journal.init(store.prefs, kAppVersion), requests a wakelock, sets immersive system UI,
and runs CadenceApp, whose only route is HomeScreen(store: store). _HomeScreenState
(lib/ui/home.dart:31) owns everything runtime: it constructs the Engine, loads persisted state,
runs two one-shot data migrations, and starts a 150 ms Timer.periodic heartbeat
(lib/ui/home.dart:154) that calls engine.tick() and setState.
The layering is: a pure-Dart engine (lib/engine/engine.dart, lib/engine/models.dart) with no
Flutter imports, which never decrements time but recomputes it from absolute epoch-millisecond
deadlines (RunEntry.endsAt, lib/engine/models.dart:97); a persistence layer
(lib/engine/store.dart) that writes every mutation immediately to SharedPreferences; an audio
layer (lib/audio/audio.dart ringtones/haptics, lib/audio/voice.dart speech queue,
lib/audio/alarm_volume.dart the loudness rule); a UI layer (lib/ui/*); and a platform layer
reached through two MethodChannels that the app defines itself. The engine never performs side
effects: it calls back through the EngineHost interface (lib/engine/engine.dart:10), which
_HomeScreenState implements (lib/ui/home.dart:33 implements EngineHost).
A timer fires alarmLeadMs = 1200 milliseconds before its nominal deadline
(lib/engine/engine.dart:50), so tick() compares n < r.endsAt! - alarmLeadMs
(lib/engine/engine.dart:312). On firing, _fireAlarm (lib/engine/engine.dart:265) records drift
and rangAt, sets status ringing, schedules the first voice repeat at firstVoiceGapMs = 7000,
then calls host.onAlarmFire(t). _HomeScreenState.onAlarmFire (lib/ui/home.dart:284) writes a
journal line, plays the timer's WAV through SoundBox.ringtone, queues the spoken announcement
900 ms later via _announceIfStill, fires the haptic pattern, and — if backgrounded — posts the
notification immediately. Repeats shrink the gap by voiceGapFactor = 0.72 down to
minVoiceGapMs = 2000 (lib/engine/engine.dart:293).
Alongside the in-app alarm there is an OS-level backstop (lib/alarm_backstop.dart): every run
mutation funnels through persistRun() (lib/ui/home.dart:252), which calls backstop.sync() to
diff the desired set of flutter_local_notifications scheduled alarms against what is already
armed, arming new ones immediately and debouncing deadline changes by 300 ms
(lib/alarm_backstop.dart:40). Backstop alarms are placed at the chain's final deadline plus a
1500 ms grace (lib/alarm_backstop.dart:175). Speech goes out over the app's own cadence/tts
channel rather than a package, because the native side attaches USAGE_ALARM audio attributes so
speech rides the alarm stream (MainActivity.kt:122-127); loudness is written to the device
STREAM_ALARM over cadence/volume (MainActivity.kt:43-63), with iOS returning nil from
getAlarmVolume so Dart falls back to app-level gain (AppDelegate.swift:54-59). Every failure
path routes into Diag.fail (lib/diagnostics.dart:28), which both feeds a ValueNotifier the
home screen turns into an operator banner (lib/ui/home.dart:670) and appends to the persistent
journal file.
2. Per-file entries
2.1 lib/main.dart — 58 lines
Purpose. Application entry point: binding init, store/journal boot, wakelock, immersive UI mode,
and the MaterialApp root.
Public surface.
| Symbol | Signature |
|---|---|
| kAppVersion | const String kAppVersion = '0.4.12' (line 20) |
| main | Future<void> main() async (line 22) |
| CadenceApp | class CadenceApp extends StatelessWidget (line 37) |
| CadenceApp.store | final Store store (line 38) |
| CadenceApp ctor | const CadenceApp({super.key, required this.store}) (line 39) |
| CadenceApp.build | Widget build(BuildContext context) (line 42) |
Imports from within the project. diagnostics.dart (6), engine/store.dart (7), journal.dart (8), ui/home.dart (9), ui/theme.dart (10).
Imported by. No file in lib/ imports it. Proof:
grep -rn "import 'main.dart'\|import '../main.dart'\|package:cadence/main.dart" lib/ → exit 1, no output.
One test imports it: test/version_test.dart:11: import 'package:cadence/main.dart' show kAppVersion;.
External packages. flutter/material.dart, flutter/services.dart, wakelock_plus.
Persistent state touched. None directly; passes store.prefs to Journal.init.
Structural notes. main is async and awaits Store.open() (24) and Journal.init (25).
WakelockPlus.enable() is a fire-and-forget .then(...).catchError(...) chain, not awaited
(28-32); the catchError at line 30 is the only error handler in the file, and it reports
isCritical: true. runApp is called at line 34 with no await between it and the unawaited
wakelock future. child! force-unwrap at line 48 inside the MediaQuery.withNoTextScaling
builder. No timers, streams, or controllers; no dispose.
2.2 lib/engine/models.dart — 160 lines
Purpose. Plain data model — step, timer definition, run entry, clone reference — with JSON
round-trip, ported from the webapp's localStorage objects.
Public surface.
| Symbol | Signature |
|---|---|
| StepDef | class StepDef (15) — fields String name (16), int sec (17) |
| StepDef ctor | StepDef({required this.name, required this.sec}) (18) |
| StepDef.toJson | Map<String, dynamic> toJson() (20) |
| StepDef.fromJson | factory StepDef.fromJson(Map<String, dynamic> j) (21) |
| kDefaultSound | const String kDefaultSound = 'Chirp' (27) |
| TimerDef | class TimerDef (29) — String id, name; int durationSec; String sound, phrase; List<StepDef>? steps; final String? legacyZoneId (30-39) |
| TimerDef ctor | TimerDef({required this.id, required this.name, required this.durationSec, this.sound = kDefaultSound, this.phrase = '', this.steps, this.legacyZoneId}) (41) |
| TimerDef.isChain | bool get isChain => steps != null && steps!.length >= 2 (52) |
| TimerDef.totalSec | int get totalSec (53) |
| TimerDef.toJson | Map<String, dynamic> toJson() (56) |
| TimerDef.fromJson | factory TimerDef.fromJson(Map<String, dynamic> j) (65) |
| TimerDef.copyWithId | TimerDef copyWithId(String newId) (81) |
| RunStatus | enum RunStatus { running, paused, ringing } (91) |
| RunEntry | class RunEntry (93) — RunStatus status; bool chain; int stepIndex; int? endsAt, remainingMs, rangAt, armedAt, driftMs; int voiceGap; int? nextVoiceAt (94-103) |
| RunEntry ctor | RunEntry({required this.status, this.chain = false, this.stepIndex = 0, this.endsAt, this.remainingMs, this.rangAt, this.armedAt, this.driftMs, this.voiceGap = 7000, this.nextVoiceAt}) (105) |
| RunEntry.toJson | Map<String, dynamic> toJson() (118) |
| RunEntry.fromJson | factory RunEntry.fromJson(Map<String, dynamic> j) (130) |
| CloneRef | class CloneRef (144) — String id, parentId; int batchNo (145-150) |
| CloneRef ctor | CloneRef({required this.id, required this.parentId, this.batchNo = 0}) (151) |
| CloneRef.toJson | Map<String, dynamic> toJson() (153) |
| CloneRef.fromJson | factory CloneRef.fromJson(Map<String, dynamic> j) (155) |
Imports from within the project. None (no imports at all — zero import statements).
Imported by (lib). alarm_backstop.dart:24, ui/tile.dart:8, ui/home.dart:13, ui/modals.dart:7, engine/engine.dart:6, engine/store.dart:12.
Imported by (test). store_test.dart:9, announcement_test.dart:18, backstop_test.dart:10, i18n_defaults_test.dart:8, engine_test.dart:5, robustness_test.dart:9.
External packages. None.
Persistent state touched. None directly; defines the JSON shapes written under the Store keys.
Structural notes. No async, no error handling, no timers/controllers, no dispose. Force-unwrap
sites: steps! at 52, 54, 62. fromJson at 65 casts j['id'] as String with no null guard (a
missing id throws — this is the path exercised by test/robustness_test.dart:41). legacyZoneId
is read in fromJson (78) but deliberately absent from toJson (56-63). RunEntry.fromJson
defaults voiceGap to the literal 7000 (139), duplicating the constructor default (114) and
Engine.firstVoiceGapMs (engine.dart:52).
2.3 lib/engine/engine.dart — 432 lines
Purpose. The timer state machine: start/stop/pause/resume/adjust, chain-step advance, alarm fire
and repeat, batch clones, definition edits, reorder — pure Dart, no Flutter.
Public surface.
| Symbol | Signature |
|---|---|
| EngineHost | abstract class EngineHost (10) |
| EngineHost.now | int now() (11) |
| EngineHost.persistDefs | void persistDefs() (12) |
| EngineHost.persistRun | void persistRun() (13) |
| EngineHost.persistClones | void persistClones() (14) |
| EngineHost.onAlarmFire | void onAlarmFire(TimerDef t) (15) |
| EngineHost.onAlarmRepeat | void onAlarmRepeat(TimerDef t) (16) |
| EngineHost.onStepAdvance | void onStepAdvance(TimerDef t, int advanced, int stepIndex) (20) |
| EngineHost.onStopped | void onStopped(String id) (21) |
| EngineHost.onClick | void onClick(bool up) (22) |
| Engine | class Engine (25) |
| Engine.host | final EngineHost host (26) |
| Engine.timers | List<TimerDef> timers = [] (27) |
| Engine.run | Map<String, RunEntry> run = {} (28) |
| Engine.clones | List<CloneRef> clones = [] (29) |
| Engine.maxBatch | static const int maxBatch = 3 (31) |
| Engine.tickMs | static const int tickMs = 150 (32) |
| Engine.alarmLeadMs | static const int alarmLeadMs = 1200 (50) |
| Engine.dblMs | static const int dblMs = 260 (51) |
| Engine.firstVoiceGapMs | static const int firstVoiceGapMs = 7000 (52) |
| Engine.voiceGapFactor | static const double voiceGapFactor = 0.72 (53) |
| Engine.minVoiceGapMs | static const int minVoiceGapMs = 2000 (54) |
| Engine ctor | Engine(this.host) (58) |
| Engine.uid | String uid() (60) |
| Engine.soundFor | String soundFor(TimerDef t) => t.sound (66) |
| Engine.reconcile | void reconcile() (74) |
| Engine.viewList | List<TimerDef> viewList() (116) |
| Engine.labelFor | String labelFor(String id) (132) |
| Engine.batchNoFor | int batchNoFor(String id) (144) |
| Engine.nextBatchNo | int nextBatchNo(String pid) (154) |
| Engine.isClone | bool isClone(String id) (161) |
| Engine.parentIdOf | String parentIdOf(String id) (162) |
| Engine.batchCount | int batchCount(String pid) (165) |
| Engine.startTimer | void startTimer(TimerDef t) (170) |
| Engine.spawnClone | String? spawnClone(String pid) (191) |
| Engine.removeClonesOf | void removeClonesOf(String pid) (203) |
| Engine.stopTimer | void stopTimer(String id, {bool click = true}) (215) |
| Engine.adjustTimer | void adjustTimer(String id, int deltaSec) (228) |
| Engine.pauseTimer | void pauseTimer(String id) (243) |
| Engine.resumeTimer | void resumeTimer(String id) (253) |
| Engine.tick | void tick() (300) |
| Engine.saveDef | bool saveDef({String? editingId, required String name, required String phrase, String sound = kDefaultSound, int? durationSec, List<StepDef>? steps}) (355) |
| Engine.deleteDef | void deleteDef(String id) (408) |
| Engine.reorder | void reorder(String fromId, String toId) (418) |
| private | _defFor (103), _nextBatchNo (156), _fireAlarm (265), _alarmRepeat (289), final math.Random _rng (56) |
| file-private extension | extension<T> on Iterable<T> { T? get firstOrNull } (430-432) |
Imports from within the project. models.dart (6). Plus dart:math as math (5).
Imported by (lib). alarm_backstop.dart:23, engine/store.dart:11, ui/home.dart:12.
Imported by (test). store_test.dart:8, backstop_test.dart:9, announcement_test.dart:17, i18n_defaults_test.dart:7, engine_test.dart:4, robustness_test.dart:8.
External packages. None (Dart core only).
Persistent state touched. None directly — every write is delegated to host.persistDefs(),
host.persistRun(), host.persistClones().
Structural notes.
- No async/await anywhere; the class is fully synchronous.
- Error handling: one try { … } catch (_) { … } inside tick() — try at 305, catch (_) at
338; the handler removes the offending run entry and calls host.persistRun() (341-342).
- Force-unwrap sites: def.steps!.length (88), t.steps![0].sec (177), r.endsAt! (232, 247, 312,
318, 320, 324), run[t.id]! (266), t.steps! (317, 320, 323), r.nextVoiceAt! (335),
durationSec! (389, 396).
- firstOrNull extension (430) is used at 133, 135, 145, 192, 376 in place of firstWhere; the
comment at 348-354 records that firstWhere without orElse used to throw.
- No timers, streams, controllers, or dispose.
- saveDef returns false on a missing editingId (378) and mutates timers in place otherwise.
- The reconcile() loop at 97-99 mutates c.batchNo while iterating clones.
2.4 lib/engine/store.dart — 354 lines
Purpose. SharedPreferences persistence with typed reads, entry-level salvage on corrupt JSON,
corrupt-value preservation, settings (lang, vol), and two one-shot migrations plus the
first-launch seed.
Public surface.
| Symbol | Signature |
|---|---|
| Store | class Store (14) |
| Store.prefs | final SharedPreferences prefs (26) |
| Store ctor | Store(this.prefs) (27) |
| Store.open | static Future<Store> open() async (29) |
| Store.load | void load(Engine e) (63) |
| Store.saveDefs | void saveDefs(Engine e) (149) |
| Store.saveRun | void saveRun(Engine e) (151) |
| Store.saveClones | void saveClones(Engine e) (153) |
| Store.lang | String get lang (158) / set lang(String v) (163) |
| Store.vol | double get vol (168) / set vol(double v) (170) |
| Store.wasGeneratedByUs | static bool wasGeneratedByUs(String name, String phrase) (201) |
| Store.repairGeneratedPhrases | int repairGeneratedPhrases(Engine e) (219) |
| Store.migrateZoneSounds | int migrateZoneSounds(Engine e) (255) |
| Store.seedLangFor | static String seedLangFor(String? deviceLang) (293) |
| Store.seedIfFresh | bool seedIfFresh(Engine e, {String? deviceLang}) (300) |
| private constants | _kDefs (15), _kRun (16), _kZones (18), _kZoneSound (19), _kClones (20), _kSeeded (21), _kPhraseRepair (22), _kLang (23), _kVol (24), _seededPhrases (187), _legacyFallbackSound = 'Bell' (238) |
| private methods | _readString (34), _readBool (43), _readDouble (52), _readList<T> (91), _preserveCorrupt (119), _write (133), _guard (174) |
Imports from within the project. ../audio/alarm_volume.dart (8), ../diagnostics.dart (9), ../i18n.dart (10), engine.dart (11), models.dart (12). Plus dart:convert (5), dart:ui show PlatformDispatcher (6).
Imported by (lib). main.dart:7, ui/home.dart:14.
Imported by (test). store_test.dart:10, volume_test.dart:10, announcement_test.dart:19, i18n_defaults_test.dart:9, robustness_test.dart:10.
External packages. shared_preferences.
Persistent state touched (exact key strings).
cadence-timers-v1 (15), cadence-run-v1 (16), cadence-zones-v1 (18, legacy, removed at 283),
cadence-zone-sound-v1 (19), cadence-clones-v1 (20), cadence-seeded-v1 (21),
cadence-phrase-repair-v1 (22), cadence-lang (23), cadence-vol (24), plus dynamically-named
siblings '$key.corrupt' written at line 123 and read at 122.
Structural notes.
- Async boundaries: Store.open() (29) is the only async member. All writes are fire-and-forget
futures handled by .then(...).catchError(...) — _write (135-142) and _guard (175-179).
- catch sites: 37, 46, 55 (typed reads); 74 (per-run-entry salvage, catch (_)); 82 (run JSON,
catch (err)); 100 (per-list-entry salvage, catch (_)); 109 (list JSON, catch (err)); 125
(_preserveCorrupt inner); 141 (.catchError on setString); 144 (_write outer); 177
(.catchError in _guard); 269 (migrateZoneSounds JSON). 12 sites total in this file.
- Force-unwrap: v! at line 160 (return (v == 'fr' || v == 'en') ? v! : 'en';).
- seedIfFresh reads the device locale via PlatformDispatcher.instance.locale.languageCode
(347) when deviceLang is null.
- Seeded timer names are English literals at 327-342 (Manouche, Mozzarella sticks, Fries,
Crispy, Melt cheese, Dough, Cook chicken, steps Cook/Flip/Cook).
- No timers, streams, controllers, or dispose.
- _readList (91) and the run-decode block (69-85) are structurally parallel but not textually
identical.
2.5 lib/diagnostics.dart — 54 lines
Purpose. Bounded in-memory failure log, per-scope one-shot debugPrint, a ValueNotifier set of
critical scopes for the operator banner, and a single choke point routing every failure into the
persistent journal.
Public surface.
| Symbol | Signature |
|---|---|
| DiagEntry | class DiagEntry (9) — final DateTime at; final String scope; final String message (10-12) |
| DiagEntry ctor | DiagEntry(this.at, this.scope, this.message) (13) |
| DiagEntry.toString | String toString() (16) |
| Diag | class Diag with private ctor Diag._() (19-20) |
| Diag.log | static final List<DiagEntry> log = [] (22) |
| Diag.critical | static final ValueNotifier<Set<String>> critical = ValueNotifier(const {}) (26) |
| Diag.fail | static void fail(String scope, Object e, {bool isCritical = false}) (28) |
| Diag.clearCritical | static void clearCritical(String scope) (41) |
| Diag.reset | @visibleForTesting static void reset() (48-49) |
| private | static const int _max = 50 (21), static final Set<String> _warned = {} (23) |
Imports from within the project. journal.dart (7).
Imported by (lib). main.dart:6, alarm_backstop.dart:22, ui/modals.dart:6, audio/audio.dart:8, ui/home.dart:11, engine/store.dart:9, audio/voice.dart:13.
Imported by (test). store_test.dart:7, voice_test.dart:8, backstop_test.dart:8, journal_test.dart:9, robustness_test.dart:7.
External packages. flutter/foundation.dart (for ValueNotifier, debugPrint, @visibleForTesting).
Persistent state touched. Indirect: every fail writes a Journal.log line (37).
Structural notes. All state is static and process-global; reset() (48) is the only way to
clear it and is annotated @visibleForTesting. Diag.critical is a ValueNotifier that is never
disposed anywhere in lib/ (it is a static singleton). No try/catch, no async, no timers.
log is capped at 50 entries by removeAt(0) (30). _warned grows unbounded (one entry per
distinct scope string). Across lib/, Diag.fail and Diag.clearCritical are called at 41 sites.
2.6 lib/journal.dart — 250 lines
Purpose. On-device flight recorder: an append-only text file that survives process death, with a
session header, previous-session kill detection, a 60 s heartbeat, 3 s auto-flush, size rotation,
and an export/share copy.
Public surface.
| Symbol | Signature |
|---|---|
| Journal | class Journal (22) |
| Journal.device | static String device = 'unknown-device' (35) |
| Journal.appVersion | static String appVersion = '' (36) |
| Journal.snapshot | static String Function()? snapshot (39) |
| Journal.beatQuietMs | static const int beatQuietMs = 5 * 60 * 1000 (49) |
| Journal.ready | static bool get ready => _file != null (53) |
| Journal.init | static Future<void> init(SharedPreferences prefs, String version) async (65) |
| Journal.log | static void log(String scope, [String msg = '']) (133) |
| Journal.beatNow | @visibleForTesting static void beatNow() (156-157) |
| Journal.flushNow | static Future<void> flushNow() (184) |
| Journal.markCleanExit | static Future<void> markCleanExit() async (186) |
| Journal.exportCopy | static Future<String?> exportCopy() async (207) |
| Journal.disableForTests | @visibleForTesting static void disableForTests() (239-240) |
| private | _kLastBeat (23), _kClean (24), _maxBytes (25), _keepBytes (26), _file (28), _prefs (29), _buf (30), _beat (31), _autoFlush (32), _chain (33), _lastBeatSnap (50), _lastBeatAt (51), _ts (55), _day (61), _describeDevice (114), _heartbeat (142), _flush (163), _rotate (192) |
Imports from within the project. None. Plus dart:async (15), dart:io (16).
Imported by (lib). main.dart:8, alarm_backstop.dart:25, diagnostics.dart:7, ui/home.dart:16, ui/modals.dart:9, audio/alarm_volume.dart:23, audio/voice.dart:14.
Imported by (test). journal_test.dart:10, volume_test.dart:12.
External packages. device_info_plus, flutter/foundation.dart, path_provider, shared_preferences.
Persistent state touched.
- SharedPreferences keys: cadence-journal-beat (23), cadence-journal-clean (24).
- Files: '${dir.path}/cadence-journal.txt' where dir = await getApplicationDocumentsDirectory()
(69-70); export copy '${tmp.path}/$name' with tmp = await getTemporaryDirectory() (224-225);
best-effort second copy '${ext.path}/$name' with ext = await getExternalStorageDirectory()
(229-230). Export filename pattern: 'cadence-log-$slug-${_day(now)}-${hh}h${mm}.txt' (221-223).
Structural notes.
- Async boundaries: init (65), _describeDevice (114), _flush (163, returns the serialised
_chain future), flushNow (184), markCleanExit (186), _rotate (192), exportCopy (207).
Writes are serialised on static Future<void> _chain (33, 165).
- catch sites: 108 (init, sets _file = null), 128 (_describeDevice, catch (_) {} — empty),
175 (_flush), 200 (_rotate, catch (_) then writes an empty string), 231 (exportCopy
external copy, catch (_) {} — empty), 233 (exportCopy outer). 6 sites.
- Force-unwrap: _file! at 170, 226, 230.
- Timers created: _beat = Timer.periodic(const Duration(seconds: 60), …) (106) and
_autoFlush = Timer.periodic(const Duration(seconds: 3), …) (107). They are cancelled only in
disableForTests() (241-242); there is no other cancel site and no dispose on this class.
- log() (133) returns immediately when _file == null (134), then calls _flush() on every
event (139).
2.7 lib/alarm_backstop.dart — 279 lines
Purpose. OS-level safety-net alarms through flutter_local_notifications: diff-based scheduling
against each running timer's final deadline, debounced re-arms, exact→inexact degradation,
immediate show on background, and clear on foreground.
Public surface.
| Symbol | Signature |
|---|---|
| Backstop | class Backstop (27) |
| Backstop.ready | bool get ready => _ready (64) |
| Backstop.init | Future<void> init() async (69) |
| Backstop.sync | void sync(Engine engine, String body) (122) |
| Backstop.onBackground | Future<void> onBackground(Engine engine, String body) async (238) |
| Backstop.showNow | Future<void> showNow(TimerDef t, String body) async (252) |
| Backstop.onForeground | Future<void> onForeground() async (268) |
| private | _plugin (28), _scheduled (32), _ready (33), _exactOk (34), _debounceMs = 300 (40), _debounce (41), _pending (42), _channel (44), _details (61), _nid (67), _desired (100), _flushSchedules (154), _graceMs = 1500 (175), _schedule (177), _cancel (223) |
Imports from within the project. diagnostics.dart (22), engine/engine.dart (23), engine/models.dart (24), journal.dart (25). Plus dart:async (16).
Imported by (lib). ui/home.dart:7.
Imported by (test). backstop_test.dart:7.
External packages. flutter/services.dart (for PlatformException), flutter_local_notifications, timezone/data/latest.dart as tzdata, timezone/timezone.dart as tz.
Persistent state touched. No SharedPreferences. OS notification state:
- Android notification channel id 'cadence-alarms', name 'Timer alarms' (46-47).
- Raw resource sound RawResourceAndroidNotificationSound('cadence_alarm') (55) → file
android/app/src/main/res/raw/cadence_alarm.wav (present on disk; generated by
tools/build_ringtones.py:236).
- Android init icon '@mipmap/ic_launcher' (74).
- Notification ids derived by _nid(String id) => id.hashCode & 0x7fffffff (67).
Structural notes.
- Async boundaries: init (69), _schedule (177), onBackground (238), showNow (252),
onForeground (268). sync (122) is synchronous and calls the async _schedule without
awaiting (138, 166). _cancel (223) calls _plugin.cancel(...).catchError(...) unawaited (230).
- catch sites: 92 (init, sets _ready = false), 204 (on ArgumentError catch (e)), 208
(generic, contains the exact_alarms_not_permitted branch and a recursive retry at 216), 230
(.catchError on cancel), 261 (showNow), 275 (onForeground). 6 sites.
- Force-unwrap: r.endsAt! (107), t.steps! (109, 110), n.id! (273).
- Timer created: _debounce = Timer(const Duration(milliseconds: _debounceMs), _flushSchedules)
(146-147). Cancelled at 145 (before re-arm) and 155 (inside _flushSchedules). The class has
no dispose/close method, so a pending _debounce is not cancelled at teardown from this
file.
- _schedule recurses into itself at line 216 (return _schedule(id, at, name, body);) after
setting _exactOk = false.
- Guard against past deadlines at 181: if (at <= DateTime.now().millisecondsSinceEpoch + 500) return;.
2.8 lib/audio/audio.dart — 116 lines
Purpose. Ringtone/click playback on the alarm audio context and vibration patterns.
Public surface.
| Symbol | Signature |
|---|---|
| SoundBox | class SoundBox (10) |
| SoundBox.vol | double vol = 1.0 (20) |
| SoundBox.init | Future<void> init() async (44) |
| SoundBox.assetFor | static String assetFor(String tone) (83) |
| SoundBox.ringtone | Future<void> ringtone(String name) (86) |
| SoundBox.stepChime | Future<void> stepChime() (89) |
| SoundBox.click | Future<void> click(bool up) (91) |
| SoundBox.hapticFire | void hapticFire() (103) |
| SoundBox.hapticRepeat | void hapticRepeat() (104) |
| SoundBox.hapticStep | void hapticStep() (105) |
| SoundBox.hapticClick | void hapticClick() (111) |
| private | _pool (12), _next (13), _ring (19), _canVibrate (21), _alarmCtx (23), _newPlayer (37), _play (58), _vibratePattern (96) |
Imports from within the project. ../diagnostics.dart (8).
Imported by (lib). ui/home.dart:9.
Imported by (test). i18n_defaults_test.dart:6 (uses SoundBox.assetFor only, at line 74).
External packages. audioplayers, flutter/services.dart (for HapticFeedback), vibration.
Persistent state touched. Bundled assets only, via AssetSource('audio/$asset') (73).
Structural notes.
- Async boundaries: _newPlayer (37), init (44), _play (58), and the three thin wrappers
ringtone/stepChime/click which return the _play future.
- catch sites: 51 (Vibration.hasVibrator() probe) and 75 (_play, reports with
isCritical: critical). 2 sites.
- Force-unwrap: none.
- AudioPlayer instantiation: 4 pooled instances created in the loop at 45-47 plus one dedicated
_ring at 48, all via _newPlayer() (38). There is no dispose, release, or stop-on-teardown
method on SoundBox — grep for dispose in this file returns nothing. _pool and _ring live
for the process lifetime.
- assetFor (83-84) is '${tone.toLowerCase().replaceAll('buzzer', 'buzz')}.wav'; because
toLowerCase() runs first, the replaceAll only matches a tone literally named Buzzer. The
tone key in C.tones is 'Buzz'; the FR/EN label is 'Buzzer' (i18n.dart:20, 34).
- _play selects (ring ? _ring : null) ?? (_pool.isEmpty ? null : _pool[_next]) (62) and reports
'player pool not ready' through Diag.fail when both are null (66).
2.9 lib/audio/voice.dart — 204 lines
Purpose. Serial speech queue over the app's own cadence/tts platform channel, with
per-timer cancellation, staleness dropping, cold-start draining, and voice selection.
Public surface.
| Symbol | Signature |
|---|---|
| VoiceBox | class VoiceBox (24) |
| VoiceBox.staleMs | static const int staleMs = 20000 (36) |
| VoiceBox.vol | double vol = 1.0 (30) |
| VoiceBox.ready | bool get ready => _ready (40) |
| VoiceBox.pending | int get pending => _queue.length (43) |
| VoiceBox.init | Future<void> init(String locale) async (45) |
| VoiceBox.setLocale | Future<void> setLocale(String locale) async (80) |
| VoiceBox.enqueue | void enqueue(String id, String text) (144) |
| VoiceBox.stopFor | void stopFor(String id) (191) |
| file-private | class _QueueItem (16) with final String id, text; final int at (17-19) |
| private | _ch (25), _queue (26), _speaking (27), _currentId (28), _gen (29), _locale (31), _ready (32), _discardQueue (73), _pickVoice (104), _drain (149), _dropStale (180) |
Imports from within the project. ../diagnostics.dart (13), ../journal.dart (14). Plus dart:async (11).
Imported by (lib). ui/home.dart:10.
Imported by (test). voice_test.dart:7.
External packages. flutter/services.dart (MethodChannel, TimeoutException comes from dart:async).
Persistent state touched. Platform channel 'cadence/tts' (25). Methods invoked: 'init' (48),
'setLanguage' (54, 84), 'setRate' (55), 'getVoices' (106), 'setVoice' (134), 'speak' (167),
'stop' (196).
Structural notes.
- Async boundaries: init (45), setLocale (80), _pickVoice (104), _drain (149). enqueue
(144) calls _drain() without awaiting (146). stopFor (191) is synchronous but calls the async
channel and schedules a Timer.
- catch sites: 64 (init), 86 (setLocale), 139 (_pickVoice), 169 (} on TimeoutException {),
171 (_drain generic), 196 (.catchError on stop). 6 sites.
- Timeout: .timeout(const Duration(seconds: 12)) on the speak invocation (168).
- Force-unwrap: none (raw is! List at 107 is a type test, not a force-unwrap).
- Timers created: Timer(const Duration(milliseconds: 300), _drain) (177) and
Timer(const Duration(milliseconds: 60), _drain) (201). Neither is stored in a field and neither
is cancellable. No dispose/close method on VoiceBox.
- Generation guard _gen (29): incremented in stopFor (194) and captured as myGen in _drain
(160), compared at 174 with if (myGen != _gen) return; — the early return at 174 skips the
_speaking = false reset at 175.
- _queue is an unbounded List<_QueueItem>; _dropStale (180) removes entries older than
staleMs.
2.10 lib/audio/alarm_volume.dart — 68 lines
Purpose. The alarm-loudness rule as a pure, injectable object: a hard floor, boot assertion,
write-through on slider move, and re-assertion only on the rising edge of a ring.
Public surface.
| Symbol | Signature |
|---|---|
| AlarmVolume | class AlarmVolume (25) |
| AlarmVolume.floor | static const double floor = 0.15 (28) |
| AlarmVolume.apply | final void Function(double level) apply (33) |
| AlarmVolume ctor | AlarmVolume(this.apply, {double stored = 1.0}) : _level = sane(stored) (38) |
| AlarmVolume.level | double get level => _level (40) |
| AlarmVolume.sane | static double sane(double v) => v.isFinite ? v.clamp(floor, 1.0).toDouble() : 1.0 (44-45) |
| AlarmVolume.assertLevel | void assertLevel() => apply(_level) (49) |
| AlarmVolume.setLevel | void setLevel(double v) (53) |
| AlarmVolume.onRunChanged | void onRunChanged({required bool anyRinging}) (60) |
| private | _level (35), _wasRinging (36) |
Imports from within the project. ../journal.dart (23).
Imported by (lib). ui/home.dart:8, ui/modals.dart:5, engine/store.dart:8.
Imported by (test). volume_test.dart:9.
External packages. None.
Persistent state touched. None directly; the value it guards is stored by Store under
cadence-vol (store.dart:24).
Structural notes. Fully synchronous. No try/catch, no force-unwrap, no timers or
controllers, no dispose. One journal write at 62-64, emitted only on the rising edge.
onRunChanged (60) is the single place _wasRinging is updated (66).
2.11 lib/i18n.dart — 167 lines
Purpose. FR/EN string tables, tone labels, the generated "ready" announcement, and the TTS
locale mapping.
Public surface.
| Symbol | Signature |
|---|---|
| I18n | class I18n (4) |
| I18n.toneLabels | static const toneLabels = {'fr': {...}, 'en': {...}} (8-37) — 12 tones per language |
| I18n.strings | @visibleForTesting static Map<String, Map<String, String>> get strings (135-136) |
| I18n.lang | String lang (138) |
| I18n ctor | I18n(this.lang) (139) |
| I18n.call | String call(String key) (143) |
| I18n.toneLabel | String toneLabel(String tone) (144) |
| I18n.readyPhrase | String readyPhrase(String name) (148) |
| I18n.announcementFor | String announcementFor(String name, String phrase) (163) |
| I18n.ttsLocale | String get ttsLocale => lang == 'fr' ? 'fr-FR' : 'en-US' (166) |
| private | static const _strings (39-132) — 37 keys per language (FR block 40-87, EN block 88-131) |
Key set (both languages, _strings): edit, done, new, emptyTitle, newTimer, editTimer,
nameLabel, namePh, typeLabel, single, multi, durationLabel, stepsLabel, addStep,
ringtoneLabel, voiceLabel, voicePh, cancel, save, stepsWord, close, settingsTitle,
languageLabel, volumeLabel, volumeFloor, silentNote, voiceDown, audioDown, saveFail,
loadFail, screenDown, backstopDown, notifRinging, journalLabel, journalSend,
journalSending, journalHint.
Imports from within the project. None.
Imported by (lib). ui/modals.dart:8, ui/header.dart:6, ui/home.dart:15, engine/store.dart:10.
Imported by (test). volume_test.dart:11, i18n_defaults_test.dart:10, announcement_test.dart:20, editor_layout_test.dart:10, robustness_test.dart:11.
External packages. flutter/foundation.dart (for @visibleForTesting).
Persistent state touched. None.
Structural notes. No async, no try/catch, no timers. Force-unwrap sites at 143 and 145,
both of the form (_strings[lang] ?? _strings['en'])![key] ?? key — the ! applies to the
null-coalesced map, and a missing key returns the key itself rather than throwing. readyPhrase
(148) hard-codes the two grammar forms '$name est prêt' and 'The ${name.toLowerCase()} is ready'.
2.12 lib/ui/theme.dart — 82 lines
Purpose. Colour palette, font-family names, tone list, duration presets, the continuous urgency
colour, and the two time formatters.
Public surface.
| Symbol | Signature |
|---|---|
| C | abstract class C (4) |
| colours | bg (5), panel (6), panel2 (7), line (8), tileIdle (16), tileEdge (17), tileEdgeW = 1.5 (18), text (19), muted (20), ember (21), amber (22), mint (23), red (24), track (25), onAccent (26), headerBg (27), headerInk (28), logoInk (29), pausedFill (30), pausedText (31), ringInnerTop (32), ringInnerBottom (33), ringName (34) |
| urgency anchors | fMint = [92, 199, 154] (37), fAmber = [237, 178, 78] (38), fRed = [236, 106, 106] (39) |
| C.tones | static const tones = [...] (45-48) — 12 entries |
| C.presets | static const presets = [[0,30],[1,0],[3,0],[5,0],[10,0],[15,0]] (52-54) |
| fillFor | Color fillFor(double p) (59) |
| F | abstract class F (71) — display = 'Big Shoulders Display' (72), mono = 'Chivo Mono' (73), dseg7 = 'DSEG7 Classic' (74) |
| fmtTime | String fmtTime(double s) (77) |
| fmtUp | String fmtUp(double s) => '+${fmtTime(s)}' (82) |
Imports from within the project. None.
Imported by (lib). main.dart:10, ui/tile.dart:9, ui/home.dart:20, ui/modals.dart:10, ui/header.dart:8.
Imported by (test). editor_layout_test.dart:12 (uses C.presets), i18n_defaults_test.dart:11 (uses C.tones).
External packages. flutter/material.dart.
Persistent state touched. None.
Structural notes. No async, no error handling, no force-unwrap, no timers. fillFor (59)
allocates a local closure mix on every call (60). C.logoInk (29) and C.mint (23) are declared;
grep for C.logoInk and C.mint outside this file returns no hits in lib/. Baseline coverage
for this file is 0.00%, so fillFor, fmtTime, and fmtUp are not executed by the suite.
2.13 lib/ui/grid_layout.dart — 109 lines
Purpose. Pure grid geometry: choose the column count that maximises tile size, break ties toward
a full grid, and derive tile width, row height, gap, and padding.
Public surface.
| Symbol | Signature |
|---|---|
| GridLayout | class GridLayout (16) |
| GridLayout.gapRatio | static const gapRatio = 0.035 (18) |
| GridLayout.marginRatio | static const marginRatio = 0.045 (21) |
| GridLayout.maxAspect | static const maxAspect = 0.85 (24) |
| GridLayout.fullGridTolerance | static const fullGridTolerance = 0.05 (35) |
| fields | final int cols, rows; final double tileW, rowH, gap, pad (37-42) |
| ctor | const GridLayout({required this.cols, required this.rows, required this.tileW, required this.rowH, required this.gap, required this.pad}) (44) |
| GridLayout.gridW | double get gridW => cols * tileW + (cols - 1) * gap (54) |
| GridLayout.gridH | double get gridH => rows * rowH + (rows - 1) * gap (57) |
| GridLayout.solve | static GridLayout solve(double wrapW, double wrapH, int n) (62) |
| private | static ({double tw, double th, double g, double pad}) _measure(double wrapW, double wrapH, int count, int c) (100) |
Imports from within the project. None. Plus dart:math as math (14).
Imported by (lib). ui/home.dart:17.
Imported by (test). grid_layout_test.dart:5.
External packages. None.
Persistent state touched. None.
Structural notes. Fully synchronous and pure. No try/catch, no force-unwrap, no timers, no
dispose. solve runs two O(n) loops over candidate column counts (66-84). Negative geometry is
clamped by math.max(0.0, …) at 91-94. Baseline coverage 100.00%.
2.14 lib/ui/logo.dart — 18 lines
Purpose. The header mark widget — a single Image.asset.
Public surface.
| Symbol | Signature |
|---|---|
| CadenceMark | class CadenceMark extends StatelessWidget (6) |
| CadenceMark.height | final double height (7) |
| ctor | const CadenceMark({super.key, required this.height}) (8) |
| build | Widget build(BuildContext context) (11) |
Imports from within the project. None.
Imported by (lib). ui/header.dart:7 — the only importer. Proof:
grep -rn "import '.*logo.dart'" lib/ → lib/ui/header.dart:7:import 'logo.dart'; (single hit).
No test imports it: grep -rn "package:cadence/ui/logo.dart" test/ → no output.
External packages. flutter/material.dart.
Persistent state touched. Asset 'assets/logo/mark_white.png' (13).
Structural notes. No async, error handling, force-unwrap, timers, or dispose. Baseline
coverage 0.00%.
Purpose. The dark top bar: logo lockup, centred blinking DSEG clock, and the Settings / Edit /
New buttons, with a width-based degradation ladder.
Public surface.
| Symbol | Signature |
|---|---|
| Header | class Header extends StatelessWidget (10) |
| fields | final I18n i18n (11), final bool editing (12), final DateTime now (13), final VoidCallback onSettings (14), final VoidCallback onEdit (16), final VoidCallback onNew (17) |
| ctor | const Header({super.key, required this.i18n, required this.editing, required this.now, required this.onSettings, required this.onEdit, required this.onNew}) (19) |
| build | Widget build(BuildContext context) (31) |
| file-private | class _Clock extends StatelessWidget (127) — final DateTime now; final bool reduced (128-129) |
| file-private | class _HBtn extends StatelessWidget (152) — label, iconOnly, compact, narrow, primary, active, fontScale, onTap (153-160) |
Imports from within the project. ../i18n.dart (6), logo.dart (7), theme.dart (8).
Imported by (lib). ui/home.dart:18 — the only importer. Proof:
grep -rn "import '.*header.dart'" lib/ → lib/ui/home.dart:18:import 'header.dart'; (single hit).
No test imports it: grep -rn "package:cadence/ui/header.dart" test/ → no output.
External packages. flutter/material.dart.
Persistent state touched. None.
Structural notes. Stateless throughout; no async, no try/catch, no force-unwrap, no timers or
controllers, no dispose. Breakpoint constants are inline literals at 33-37 (470, 820, 800,
960, 560). Blink logic at 143: final colonOn = reduced || now.millisecond < 500;. The clock
re-renders from the now value passed by HomeScreen.build (home.dart:559), which is refreshed by
the 150 ms ticker. Baseline coverage 0.00%.
2.16 lib/ui/tile.dart — 819 lines
Purpose. One timer tile: pie painter, DSEG digits with LCD ghost, ±10 s / ✕ control row, chained-
phase banner, ×N batch chip, edit-mode badge/veil/dashed outline, and six animation controllers.
Public surface.
| Symbol | Signature |
|---|---|
| TileView | class TileView extends StatefulWidget (11) |
| fields | def (12), isClone (13), r (14), nowMs (15), editing (17), isDropTarget (18), dragOffset (19), dupShow (20), dupLabel (21), batchNo (24), stepsWord (25), flashTick (27), justOnTick (28), spawnTick (29), onTap (30), onDup (31), onPlus (32), onMinus (33), onStop (34) |
| ctor | const TileView({super.key, required this.def, required this.isClone, required this.r, required this.nowMs, required this.editing, required this.isDropTarget, required this.dragOffset, required this.dupShow, required this.dupLabel, required this.batchNo, required this.stepsWord, required this.flashTick, required this.justOnTick, required this.spawnTick, required this.onTap, required this.onDup, required this.onPlus, required this.onMinus, required this.onStop}) (36) |
| createState | State<TileView> createState() => _TileViewState() (61) |
| file-private | _TileViewState (64), _CtlBtn (643) with static const double signScale = 1.33 (668), _PiePainter (759), _DashedOutline (788) |
Imports from within the project. ../engine/models.dart (8), theme.dart (9). Plus dart:math as math (6).
Imported by (lib). ui/home.dart:21 — the only importer. Proof:
grep -rn "import '.*tile.dart'" lib/ → lib/ui/home.dart:21:import 'tile.dart'; (single hit).
No test imports it: grep -rn "package:cadence/ui/tile.dart" test/ → no output.
External packages. flutter/material.dart.
Persistent state touched. None.
Structural notes.
- late final sites: six AnimationController fields at 65, 66, 67, 68, 69, 70.
- AnimationController instantiation: _pulse (75, 900 ms), _breath (77, 1800 ms), _jiggle
(79, 320 ms), _flash (81, 750 ms), _appear (83, 300 ms, value: 1), _spawn (85, 300 ms,
value: 1). All six are disposed in dispose() at 147-152, super.dispose() at 153.
- No try/catch anywhere in the file, no async/await, no timers, no streams.
- addPostFrameCallback used at 89 (initState) and 108 (_syncLoops), both guarded by
if (!mounted) return; (90, 109).
- _reduced (99) reads MediaQuery.of(context).disableAnimations; it is invoked from initState's
post-frame callback (90) and from didUpdateWidget (137, 138, 141).
- Force-unwrap sites: r!.chain (186), t.steps![r.stepIndex] (186), r.endsAt! (187), r!.chain
and t.steps![r.stepIndex] (193), r!.rangAt (200), widget.dragOffset! (364),
t.steps!.length (498, 500), t.steps![r.stepIndex] (501), sign! (731).
- _PiePainter.shouldRepaint (785) and _DashedOutline.shouldRepaint (817) are both implemented.
- AnimatedBuilder at 161 merges all six controllers via Listenable.merge (162).
- Baseline coverage 0.00%.
2.17 lib/ui/modals.dart — 746 lines
Purpose. Two dialogs — the timer editor (single/chain, ringtone picker, announcement field) and
Settings (language, volume slider, journal export) — plus shared modal chrome widgets.
Public surface.
| Symbol | Signature |
|---|---|
| TimerEditorResult | class TimerEditorResult (150) — final bool delete; final String name, phrase, sound; final int? durationSec; final List<StepDef>? steps (151-156) |
| ctor | TimerEditorResult({this.delete = false, this.name = '', this.phrase = '', this.sound = kDefaultSound, this.durationSec, this.steps}) (157) |
| showTimerEditor | Future<TimerEditorResult?> showTimerEditor(BuildContext context, {required I18n i18n, TimerDef? existing, required void Function(String sound) previewSound}) (167) |
| showSettings | Future<void> showSettings(BuildContext context, {required I18n i18n, required double vol, required void Function(String lang) onLang, required void Function(double vol) onVol, required VoidCallback onVolReleased}) (572) |
| file-private | _showSheet<T> (12), _h2 (35), _fieldLabel (54), _inputDeco (64), _ChipBtn (79), _modalBtn (119), _TimerEditor (179), _TimerEditorState (192), _Settings (590), _SettingsState (607) |
Imports from within the project. ../audio/alarm_volume.dart (5), ../diagnostics.dart (6), ../engine/models.dart (7), ../i18n.dart (8), ../journal.dart (9), theme.dart (10).
Imported by (lib). ui/home.dart:19.
Imported by (test). volume_test.dart:13, announcement_test.dart:21, editor_layout_test.dart:11.
External packages. flutter/material.dart, share_plus.
Persistent state touched. No direct SharedPreferences access. Calls Journal.exportCopy()
(701), reads Journal.ready (673), Journal.device (683, 707), Journal.appVersion (709), and
hands the exported file to SharePlus.instance.share(ShareParams(files: [XFile(path)], …)) (705).
Structural notes.
- late sites: _name (193), _phrase (194), mode (195), min, sec (196), sound (197),
steps (198), and late double vol = AlarmVolume.sane(widget.vol) (610).
- Async boundaries: _showSheet returns showDialog<T> (13); _sendJournal is
Future<void> _sendJournal() async (698).
- catch sites: 712 (_sendJournal, Diag.fail('journal-export', e)), with a finally at 714
guarded by if (mounted) (715). 1 site in this file.
- TextEditingController instantiation: _name (204), _phrase (205). Listener added at 207
(_name.addListener(_onNameTyped)), removed at 225, and both controllers disposed at 226-227 in
dispose() (224).
- Force-unwrap: t.steps! (211).
- Early return at 702 inside _sendJournal (if (path == null) return;) exits before the
SharePlus call; the finally block at 714 still runs.
- The editor's duration min is clamped to [0, 180] at 428 and sec cycles %60 at 436-437;
bump (416) forces sec = 5 when both reach 0 (418).
- _commitStep (520) floors a step at 5 seconds (524), mirroring Engine.saveDef (engine.dart:368).
- Two near-identical _inputDeco().copyWith(counterText: '', contentPadding: …, fillColor: C.panel)
blocks at 482-491 and 535-544 (see §3.8).
2.18 lib/ui/home.dart — 722 lines
Purpose. The single screen. Owns the Engine, the 150 ms heartbeat, edit mode, drag-to-reorder,
the auto-scaled grid, the critical banner, and implements EngineHost (audio, voice, haptics,
persistence, backstop sync).
Public surface.
| Symbol | Signature |
|---|---|
| HomeScreen | class HomeScreen extends StatefulWidget (23) |
| HomeScreen.store | final Store store (24) |
| ctor | const HomeScreen({super.key, required this.store}) (25) |
| createState | State<HomeScreen> createState() => _HomeScreenState() (28) |
| file-private | class _HomeScreenState extends State<HomeScreen> with WidgetsBindingObserver implements EngineHost (31-33) |
_HomeScreenState members (all file-private but load-bearing): engine (34), i18n (35),
sounds (36), voice (37), backstop (38), _ticker (39), editing (40), _foreground (41),
alarmVol (44), _lastTickMs (48), _leftAt (49), _phrasesRepaired (50), _soundsMigrated (51),
_flash/_justOn/_spawn (55-57), _tapPending (59), _dragId/_dragDelta/_dropTargetId
(62-64), _volumeChannel (67), _systemVolumeOk (68), initState (71), _boot (92),
didChangeAppLifecycleState (175), _applyAlarmLevel (209), _initSystemVolume (220), dispose
(236), now (248), persistDefs (250), persistRun (252), _reconcileAlarmVolume (263),
persistClones (267), _phraseOf (269), _announceIfStill (275), onAlarmFire (284),
onAlarmRepeat (306), onStepAdvance (315), onStopped (338), onClick (341), _tapTile (348),
_dup (403), _openEditor (420), _toggleEdit (459), _openSettings (465), _panStart (500),
_panUpdate (509), _panEnd (532), build (549), _tileIndexAt (609), _buildTile (617),
_criticalBanner (670), _empty (706).
Imports from within the project. ../alarm_backstop.dart (7), ../audio/alarm_volume.dart (8),
../audio/audio.dart (9), ../audio/voice.dart (10), ../diagnostics.dart (11),
../engine/engine.dart (12), ../engine/models.dart (13), ../engine/store.dart (14),
../i18n.dart (15), ../journal.dart (16), grid_layout.dart (17), header.dart (18),
modals.dart (19), theme.dart (20), tile.dart (21). Plus dart:async (4). It is the widest
importer in the codebase (15 project imports).
Imported by (lib). main.dart:9 — the only importer. Proof:
grep -rn "import '.*home.dart'" lib/ → lib/main.dart:9:import 'ui/home.dart'; (single hit).
No test imports it: grep -rn "package:cadence/ui/home.dart" test/ → no output.
External packages. flutter/material.dart, flutter/services.dart (MethodChannel).
Persistent state touched.
- Platform channel 'cadence/volume' (67), methods 'setAlarmVolume' (211) and 'getAlarmVolume'
(226).
- SharedPreferences indirectly through widget.store.saveDefs/saveRun/saveClones (250, 253, 267)
and widget.store.lang = code (474), widget.store.vol = alarmVol.level (482).
- Journal file indirectly via 30 Journal.log sites and Journal.flushNow() (200),
Journal.markCleanExit() (202); Journal.snapshot is assigned at 93.
Structural notes.
- late final sites: engine (34), i18n (35), alarmVol (44).
- Async boundaries: _boot (92, async), _initSystemVolume (220), _openEditor (420),
_openSettings (465). initState (71) calls _boot() without awaiting (86). voice.init(...)
is a non-awaited .then chain (141-144). backstop.onForeground() (189) and
backstop.onBackground(...) (199) are async and not awaited.
- catch sites: 138 (_boot, around sounds.init()), 211 (.catchError on the volume channel),
227 (_initSystemVolume). 3 sites.
- Force-unwrap sites: r.endsAt! (126), t.steps!.length (127), _leftAt! (184), t.steps! (317,
332, 333), t.steps!.length (359), t!.name (432), res.steps! (452, 453), _dragId! (534).
- Timers created: _ticker = Timer.periodic(const Duration(milliseconds: 150), …) (154), cancelled
in dispose() at 238; Timer(delay, …) inside _announceIfStill (277) — not stored, not
cancellable; _tapPending[id] = Timer(const Duration(milliseconds: Engine.dblMs), …) (387),
cancelled at 379 (on the second tap) and in the dispose() loop at 239-241.
- dispose() (236) removes the observer (237), cancels _ticker (238), cancels every pending tap
timer (239-241), then super.dispose() (242). It does not tear down sounds, voice, or
backstop — none of those three classes expose a teardown method.
- setState(() {}) is called from the ticker callback at 166 guarded by if (mounted), and
unguarded at 362, 373, 385, 417, 456, 491, 649, 654, 663 (inside synchronous user-gesture
handlers) and guarded at 168, 191, 398.
- _criticalBanner (670) maps Diag.critical scope prefixes to i18n keys with a chain of
startsWith tests at 675-687 (voice, audio, save, load, wakelock, backstop).
- Baseline coverage 0.00%.
2.19 test/engine_test.dart — 341 lines
Purpose. Behavioural unit tests of Engine against a recording FakeHost with a controllable
clock. 21 test( in 5 group(.
Public surface. class FakeHost implements EngineHost (7) with fields t (8), fired (9),
repeated (10), steps (11), stopped (12), saves (13); helpers TimerDef single(String id,
int sec) (36) and TimerDef chain(String id, List<int> secs) (39); void main() (46).
Imports from within the project. package:cadence/engine/engine.dart (4),
package:cadence/engine/models.dart (5).
Imported by. Nothing — test entry points are not imported. Proof:
grep -rn "engine_test" lib/ test/ → no output.
External packages. flutter_test.
Persistent state touched. None (no SharedPreferences, no files, no channels).
Structural notes. No TestWidgetsFlutterBinding.ensureInitialized() in this file — it is the only
test/ file that omits it besides source_hygiene_test.dart and grid_layout_test.dart. Uses
setUp (50) to rebuild host and engine. Force-unwraps assertions like e.run['a']! throughout.
Primary lib/ file exercised: lib/engine/engine.dart (baseline coverage 94.47%).
2.20 test/store_test.dart — 220 lines
Purpose. Persistence tests: first-launch seed runs once, startup never overwrites customised
state, corrupt data loads safely and is preserved, saves round-trip, and the v0.4.11 zone→sound
migration. 10 test( in 1 group(.
Public surface. class NullHost implements EngineHost (12); Map<String, Object>
_preZoneRemovalTablet() (35); void main() (50).
Imports from within the project. package:cadence/diagnostics.dart (7),
package:cadence/engine/engine.dart (8), package:cadence/engine/models.dart (9),
package:cadence/engine/store.dart (10).
Imported by. Nothing (grep -rn "store_test" lib/ test/ → no output).
External packages. flutter_test, shared_preferences, dart:convert.
Persistent state touched. SharedPreferences.setMockInitialValues with keys
cadence-timers-v1 (36, 74, 91), cadence-zones-v1 (42, 76, 179, 209), cadence-seeded-v1 (47, 77,
93), cadence-run-v1 (92), cadence-lang (78); asserts on cadence-timers-v1.corrupt (101),
cadence-run-v1.corrupt (102), cadence-zone-sound-v1 (169, 185).
Structural notes. TestWidgetsFlutterBinding.ensureInitialized() (51), setUp(Diag.reset) (53).
Async settling is done with await Future<void>.delayed(Duration.zero) at 115, 152, 167. Primary
lib/ file exercised: lib/engine/store.dart (94.40%).
2.21 test/backstop_test.dart — 191 lines
Purpose. OS-backstop tests through a mocked flutter_local_notifications channel: past deadlines
are never scheduled, a future deadline schedules an exact alarmClock alarm at deadline + 1500 ms,
only exact_alarms_not_permitted degrades to inexact, a random error does not poison exact mode,
and the 300 ms debounce collapses a burst into one re-arm (and flushes on background). 6 test(,
no group(.
Public surface. class FakeHost implements EngineHost (12); const _ch =
MethodChannel('dexterous.com/flutter/local_notifications') (33); void main() (35) with local
helpers engineWith (70), scheduled (81), modeOf (84).
Imports from within the project. package:cadence/alarm_backstop.dart (7),
package:cadence/diagnostics.dart (8), package:cadence/engine/engine.dart (9),
package:cadence/engine/models.dart (10).
Imported by. Nothing.
External packages. flutter/services.dart, flutter_local_notifications, flutter_test.
Persistent state touched. Mocks the plugin channel 'dexterous.com/flutter/local_notifications'
(33) via setMockMethodCallHandler (48-49); calls
AndroidFlutterLocalNotificationsPlugin.registerWith() (44).
Structural notes. TestWidgetsFlutterBinding.ensureInitialized() (36). Uses real wall-clock
DateTime.now() (14, 76) rather than an injected clock. Real await Future<void>.delayed(const
Duration(milliseconds: 350)) at 169 to let the 300 ms debounce fire. Primary lib/ file exercised:
lib/alarm_backstop.dart (78.00%).
2.22 test/voice_test.dart — 197 lines
Purpose. Speech-queue tests over a mocked cadence/tts channel: stopFor cancels the in-flight
utterance and its own queue only, voice selection prefers language then quality then offline, a
pre-init announcement is held and drained, and a dead engine is contained, discards the queue, and is
reported critical. 9 test(, no group(.
Public surface. void main() (10) with local helpers mock({bool initOk, bool throwOnInit})
(18) and mockVoices(List<Map<String, Object>> voices, List<String> chosen) (101).
Imports from within the project. package:cadence/audio/voice.dart (7),
package:cadence/diagnostics.dart (8).
Imported by. Nothing.
External packages. dart:async, flutter/services.dart, flutter_test.
Persistent state touched. Channel 'cadence/tts' (12) mocked with setMockMethodCallHandler.
Structural notes. TestWidgetsFlutterBinding.ensureInitialized() (11). Uses a Completer<void>?
speaking (16) to hold the mocked speak open, so the test controls when an utterance "finishes".
Real delays at 59, 77, 94 (100 ms / 400 ms) to let the 60 ms and 300 ms re-drain timers run. Primary
lib/ file exercised: lib/audio/voice.dart (86.36%).
2.23 test/volume_test.dart — 188 lines
Purpose. Alarm-loudness tests: the 15 % floor at every entry point, the v0.4.4/v0.4.5
regressions (boot imposes the stored level; a ring never forces max; mid-ring lowering is written
through), and two widget tests on the Settings slider. 11 test( + 2 testWidgets( in 3 group(.
Public surface. void main() (15) with local helper ({AlarmVolume vol, List<double> written})
make({double stored = 1.0}) (20) and Future<double?> openAndDragFullLeft(WidgetTester tester)
(136).
Imports from within the project. package:cadence/audio/alarm_volume.dart (9),
package:cadence/engine/store.dart (10), package:cadence/i18n.dart (11),
package:cadence/journal.dart (12), package:cadence/ui/modals.dart (13).
Imported by. Nothing.
External packages. flutter/material.dart, flutter_test, shared_preferences.
Persistent state touched. SharedPreferences.setMockInitialValues({'cadence-vol': …}) (50, 56,
63); asserts store.prefs.getDouble('cadence-vol') (67).
Structural notes. TestWidgetsFlutterBinding.ensureInitialized() (16), setUp(Journal.
disableForTests) (17). The two testWidgets (160, 168) drive the real showSettings dialog.
Primary lib/ files exercised: lib/audio/alarm_volume.dart (100.00%) and the slider portion of
lib/ui/modals.dart.
2.24 test/journal_test.dart — 193 lines
Purpose. Flight-recorder tests with real files in a temp directory: no-op before init, session
header, events and failures land on disk, kill detection, clean-exit path, kill-proof write-through,
heartbeat de-duplication, the death stamp advancing on unwritten beats, markCleanExit, and export.
11 test(, no group(.
Public surface. class _FakePaths extends PathProviderPlatform with MockPlatformInterfaceMixin
(12); void main() (23); local helper File logFile() (39).
Imports from within the project. package:cadence/diagnostics.dart (9),
package:cadence/journal.dart (10).
Imported by. Nothing.
External packages. dart:io, flutter_test, path_provider_platform_interface,
plugin_platform_interface, shared_preferences.
Persistent state touched. Real files under Directory.systemTemp.createTemp('cadence-journal-
test') (30), overriding PathProviderPlatform.instance (31). SharedPreferences keys
cadence-journal-beat (80, 93, 119, 158, 163) and cadence-journal-clean (81, 95, 120, 171).
Structural notes. TestWidgetsFlutterBinding.ensureInitialized() (24); setUp (27) resets
Diag and calls Journal.disableForTests(); tearDown (34) disables again and deletes the temp
directory. Real delays at 115 (50 ms) and 160 (20 ms). Primary lib/ file exercised:
lib/journal.dart (81.73%).
2.25 test/robustness_test.dart — 314 lines
Purpose. Adversarial-review non-regressions: entry-level salvage, type-corrupt prefs, the seed
guard, run invariants and tick() backstop, batch labelling and collision, bounded drift, and
saveDef floors. 16 test( in 7 group(.
Public surface. class FakeHost implements EngineHost (13); void main() (36).
Imports from within the project. package:cadence/diagnostics.dart (7),
package:cadence/engine/engine.dart (8), package:cadence/engine/models.dart (9),
package:cadence/engine/store.dart (10), package:cadence/i18n.dart (11).
Imported by. Nothing.
External packages. dart:convert, flutter_test, shared_preferences.
Persistent state touched. SharedPreferences.setMockInitialValues with cadence-timers-v1 (44,
62, 109), cadence-seeded-v1 (49, 69, 85, 117), cadence-run-v1 (65), cadence-lang (83),
cadence-vol (84); asserts cadence-timers-v1.corrupt (56).
Structural notes. TestWidgetsFlutterBinding.ensureInitialized() (37), setUp(Diag.reset) (38).
Primary lib/ files exercised: lib/engine/engine.dart, lib/engine/store.dart, lib/i18n.dart.
2.26 test/announcement_test.dart — 267 lines
Purpose. Announcement-ownership tests: operator text is spoken verbatim in both languages, an
empty field generates the default in the current language, the seed writes no phrase, the one-shot
phrase-repair migration recognises only app-written text, first-launch language selection, and three
editor widget tests showing the live placeholder. 9 test( + 4 testWidgets( in 4 group(.
Public surface. class NullHost implements EngineHost (23); void main() (44); local helper
Future<TimerEditorResult?> runEditor(WidgetTester tester, I18n i18n, {required Future<void>
Function(WidgetTester t) act}) (200).
Imports from within the project. package:cadence/engine/engine.dart (17),
package:cadence/engine/models.dart (18), package:cadence/engine/store.dart (19),
package:cadence/i18n.dart (20), package:cadence/ui/modals.dart (21).
Imported by. Nothing.
External packages. flutter/material.dart, flutter_test, shared_preferences.
Persistent state touched. SharedPreferences.setMockInitialValues({}) (68, 172, 180) and
{'cadence-seeded-v1': true} (102, 137).
Structural notes. TestWidgetsFlutterBinding.ensureInitialized() (45). The widget group sets a
physicalSize = const Size(1600, 1400) and devicePixelRatio = 1.0 in setUp (191-193) with an
addTearDown reset (194-197). Primary lib/ files exercised: lib/i18n.dart,
lib/engine/store.dart, lib/ui/modals.dart.
2.27 test/i18n_defaults_test.dart — 117 lines
Purpose. Translation/key parity, tone-label coverage and uniqueness, every picker tone having a
non-empty .wav on disk, unknown-key behaviour, and seed self-sufficiency. 6 test( in 3 group(.
Public surface. class NullHost implements EngineHost (13); void main() (34).
Imports from within the project. package:cadence/audio/audio.dart (6),
package:cadence/engine/engine.dart (7), package:cadence/engine/models.dart (8),
package:cadence/engine/store.dart (9), package:cadence/i18n.dart (10),
package:cadence/ui/theme.dart (11).
Imported by. Nothing.
External packages. dart:io, flutter_test, shared_preferences.
Persistent state touched. Reads real asset files with File('assets/audio/${SoundBox.assetFor(t)}')
(74) — relative to the package root, so it depends on the test working directory.
SharedPreferences.setMockInitialValues({}) (88).
Structural notes. TestWidgetsFlutterBinding.ensureInitialized() (35). The asset test asserts
f.existsSync() (75) and f.lengthSync() > 1000 (76). Primary lib/ file exercised:
lib/i18n.dart (90.00%); it is also the only test asserting anything about lib/audio/audio.dart
(the assetFor pure function).
2.28 test/grid_layout_test.dart — 152 lines
Purpose. Grid-geometry tests: the 3.5 %/4.5 % ratios hold across timer counts and screen sizes,
the grid never overflows, enough cells for every card, the 0.85 aspect cap, the exact column split
per count, full-grid preference, a guard against tiny cards, the reference 602×332 tile, degenerate
frames, and zero timers. 13 test( in 4 group( (one bare test( at file level, line 145).
Public surface. const boardW = 1280.0, boardH = 740.0 (8); void main() (10).
Imports from within the project. package:cadence/ui/grid_layout.dart (5).
Imported by. Nothing.
External packages. flutter_test.
Persistent state touched. None.
Structural notes. No TestWidgetsFlutterBinding.ensureInitialized() — the subject is pure Dart.
Several tests iterate nested loops over sizes and counts (46-48, 123-128). Primary lib/ file
exercised: lib/ui/grid_layout.dart (100.00%).
2.29 test/editor_layout_test.dart — 79 lines
Purpose. Editor-layout tests: the six duration presets must render on one line at the narrowest
supported surface, in both languages, and must be distinct and increasing. 2 testWidgets(
declarations; the first sits inside for (final lang in ['fr', 'en']) (36) so it produces 2 runs —
3 executed tests from 2 declarations.
Public surface. void main() (14) with local helpers String presetLabel(List<int> p) (18) and
Future<void> openEditor(WidgetTester tester, String lang) (22).
Imports from within the project. package:cadence/i18n.dart (10),
package:cadence/ui/modals.dart (11), package:cadence/ui/theme.dart (12).
Imported by. Nothing.
External packages. flutter/material.dart, flutter_test.
Persistent state touched. None.
Structural notes. TestWidgetsFlutterBinding.ensureInitialized() (15). Sets
physicalSize = const Size(600, 1400) and devicePixelRatio = 1.0 (42-43) with addTearDown
resets (44-47). presetLabel (18) duplicates the chip-label expression in
lib/ui/modals.dart:302-304 (see §3.8). Primary lib/ files exercised: lib/ui/modals.dart,
lib/ui/theme.dart (C.presets).
2.30 test/source_hygiene_test.dart — 25 lines
Purpose. A single repository-wide text rule: no restaurant identifier may appear anywhere in
lib/. 1 test(, no group(.
Public surface. void main() (7).
Imports from within the project. None (it reads lib/ as text, not as code).
Imported by. Nothing.
External packages. dart:io, flutter_test.
Persistent state touched. Reads every lib/**/*.dart file from disk via
Directory('lib').listSync(recursive: true) (11) — relative to the working directory.
Structural notes. No binding initialisation. Banned pattern:
RegExp(r'sezam|modern\s*leb', caseSensitive: false) (9). Exercises no lib/ symbol.
2.31 test/version_test.dart — 29 lines
Purpose. Asserts kAppVersion matches the semantic part of version: in pubspec.yaml. 1
test(, no group(.
Public surface. void main() (14).
Imports from within the project. package:cadence/main.dart' show kAppVersion (11).
Imported by. Nothing.
External packages. dart:io, flutter_test.
Persistent state touched. Reads File('pubspec.yaml') (16) relative to the working directory.
Structural notes. No binding initialisation. Parses with line.split(':')[1].trim().split('+')
.first (24). It is the only test that imports lib/main.dart, and it touches one constant — the
file's baseline coverage remains 0.00%.
3. Cross-cutting inventories
App-owned channels: 2.
| Channel |
Dart call site(s) |
Kotlin handler |
Swift handler |
cadence/volume |
declared lib/ui/home.dart:67; invokeMethod('setAlarmVolume', level) lib/ui/home.dart:211; invokeMethod<double>('getAlarmVolume') lib/ui/home.dart:226 |
MainActivity.kt:43 (channel), MainActivity.kt:47 (getAlarmVolume), MainActivity.kt:51 (setAlarmVolume), MainActivity.kt:61 (notImplemented) |
AppDelegate.swift:51 (channel), :54 (getAlarmVolume → result(nil)), :58 (setAlarmVolume → result(nil)), :60 (FlutterMethodNotImplemented) |
cadence/tts |
declared lib/audio/voice.dart:25; 'init' :48; 'setLanguage' :54 and :84; 'setRate' :55; 'getVoices' :106; 'setVoice' :134; 'speak' :167; 'stop' :196 |
MainActivity.kt:65 (channel), :68 (init→initTts :114), :69 (getVoices), :82 (setVoice), :90 (setLanguage), :95 (setRate), :100 (speak→speak() :143), :104 (stop), :109 (notImplemented) |
AppDelegate.swift:67 (channel), :71 (init→initTts :138), :74 (getVoices), :93 (setVoice), :102 (setLanguage), :109 (setRate), :120 (speak→speak() :157), :127 (stop), :132 (FlutterMethodNotImplemented) |
Third-party channel used but not owned by app code (listed for completeness, mocked in tests):
dexterous.com/flutter/local_notifications — mocked at test/backstop_test.dart:33; the Dart side
is FlutterLocalNotificationsPlugin at lib/alarm_backstop.dart:28-29; there is no first-party
handler for it in MainActivity.kt or AppDelegate.swift.
3.2 SharedPreferences keys
11 statically-named keys plus one dynamic family.
| Key string |
Declared at |
Written at |
Read at |
cadence-timers-v1 |
lib/engine/store.dart:15 |
:150 (via _write :133) |
:64 (via _readList :91) |
cadence-run-v1 |
lib/engine/store.dart:16 |
:152 |
:67 |
cadence-zones-v1 (legacy) |
lib/engine/store.dart:18 |
removed at :283 (prefs.remove) |
:257, :305 |
cadence-zone-sound-v1 |
lib/engine/store.dart:19 |
:282 |
:256 |
cadence-clones-v1 |
lib/engine/store.dart:20 |
:154 |
:65 |
cadence-seeded-v1 |
lib/engine/store.dart:21 |
:308, :351 |
:301 |
cadence-phrase-repair-v1 |
lib/engine/store.dart:22 |
:229 |
:220 |
cadence-lang |
lib/engine/store.dart:23 |
:163 |
:159 |
cadence-vol |
lib/engine/store.dart:24 |
:171 |
:168 |
cadence-journal-beat |
lib/journal.dart:23 |
:99, :152, :174 |
:78 |
cadence-journal-clean |
lib/journal.dart:24 |
:96, :189 |
:79 |
'$key.corrupt' (dynamic, one per corrupt key) |
— |
lib/engine/store.dart:123 |
lib/engine/store.dart:122 |
3.3 Error-handling sites in lib/ (38 total)
| Site |
Form |
Enclosing member |
lib/main.dart:30 |
.catchError((e) {…}) |
main — wakelock chain |
lib/alarm_backstop.dart:92 |
} catch (e) (try at :70) |
init |
lib/alarm_backstop.dart:204 |
} on ArgumentError catch (e) (try at :183) |
_schedule |
lib/alarm_backstop.dart:208 |
} catch (e) |
_schedule |
lib/alarm_backstop.dart:230 |
.catchError((e) {…}) |
_cancel |
lib/alarm_backstop.dart:261 |
} catch (e) (try at :257) |
showNow |
lib/alarm_backstop.dart:275 |
} catch (e) (try at :270) |
onForeground |
lib/journal.dart:108 |
} catch (e) (try at :68) |
init |
lib/journal.dart:128 |
} catch (_) {} (empty; try at :115) |
_describeDevice |
lib/journal.dart:175 |
} catch (e) (try at :169) |
_flush |
lib/journal.dart:200 |
} catch (_) (try at :193) |
_rotate |
lib/journal.dart:231 |
} catch (_) {} (empty; try at :228) |
exportCopy — external copy |
lib/journal.dart:233 |
} catch (e) (try at :213) |
exportCopy |
lib/ui/home.dart:138 |
} catch (e) (try at :135) |
_boot |
lib/ui/home.dart:211 |
.catchError((e) {…}) |
_applyAlarmLevel |
lib/ui/home.dart:227 |
} catch (e) (try at :221) |
_initSystemVolume |
lib/engine/engine.dart:338 |
} catch (_) (try at :305) |
tick |
lib/ui/modals.dart:712 |
} catch (e) (try at :700, finally at :714) |
_SettingsState._sendJournal |
lib/audio/audio.dart:51 |
} catch (e) (try at :49) |
init |
lib/audio/audio.dart:75 |
} catch (e) (try at :70) |
_play |
lib/audio/voice.dart:64 |
} catch (e) (try at :47) |
init |
lib/audio/voice.dart:86 |
} catch (e) (try at :83) |
setLocale |
lib/audio/voice.dart:139 |
} catch (e) (try at :105) |
_pickVoice |
lib/audio/voice.dart:169 |
} on TimeoutException { (try at :161) |
_drain |
lib/audio/voice.dart:171 |
} catch (e) |
_drain |
lib/audio/voice.dart:196 |
.catchError((e) {…}) |
stopFor |
lib/engine/store.dart:37 |
} catch (e) (try at :35) |
_readString |
lib/engine/store.dart:46 |
} catch (e) (try at :44) |
_readBool |
lib/engine/store.dart:55 |
} catch (e) (try at :53) |
_readDouble |
lib/engine/store.dart:74 |
} catch (_) (try at :72) |
load — per-run-entry |
lib/engine/store.dart:82 |
} catch (err) (try at :69) |
load — run JSON |
lib/engine/store.dart:100 |
} catch (_) (try at :98) |
_readList — per-entry |
lib/engine/store.dart:109 |
} catch (err) (try at :94) |
_readList — list JSON |
lib/engine/store.dart:125 |
} catch (e) (try at :121) |
_preserveCorrupt |
lib/engine/store.dart:141 |
.catchError((e) {…}) |
_write |
lib/engine/store.dart:144 |
} catch (e) (try at :134) |
_write |
lib/engine/store.dart:177 |
.catchError((e) {…}) |
_guard |
lib/engine/store.dart:269 |
} catch (err) (try at :263) |
migrateZoneSounds |
Per-file totals: store.dart 12, alarm_backstop.dart 6, journal.dart 6, voice.dart 6,
home.dart 3, audio.dart 2, main.dart 1, engine.dart 1, modals.dart 1.
Files with zero error handling: lib/diagnostics.dart, lib/i18n.dart,
lib/engine/models.dart, lib/audio/alarm_volume.dart, lib/ui/theme.dart,
lib/ui/grid_layout.dart, lib/ui/header.dart, lib/ui/logo.dart, lib/ui/tile.dart.
.timeout(...): one site, lib/audio/voice.dart:168 (12 s).
3.4 Timers, controllers, players — creation and disposal
| Object |
Created at |
Stored in |
Cancelled/disposed at |
dispose in same file? |
Timer (debounce, 300 ms) |
lib/alarm_backstop.dart:146-147 |
Backstop._debounce (:41) |
:145, :155 |
No — Backstop has no dispose/close member |
Timer.periodic (60 s heartbeat) |
lib/journal.dart:106 |
Journal._beat (:31) |
lib/journal.dart:241 (inside disableForTests) |
Only @visibleForTesting disableForTests() (:239) |
Timer.periodic (3 s auto-flush) |
lib/journal.dart:107 |
Journal._autoFlush (:32) |
lib/journal.dart:242 (inside disableForTests) |
Only disableForTests() |
Timer.periodic (150 ms ticker) |
lib/ui/home.dart:154 |
_HomeScreenState._ticker (:39) |
lib/ui/home.dart:238 |
Yes — dispose() at :236 |
Timer (delayed announcement) |
lib/ui/home.dart:277 |
not stored |
never cancelled |
n/a |
Timer (tap disambiguation, 260 ms) |
lib/ui/home.dart:387 |
_tapPending[id] (:59) |
lib/ui/home.dart:379 (second tap), :240 (dispose loop) |
Yes — dispose() at :236 |
Timer (300 ms re-drain) |
lib/audio/voice.dart:177 |
not stored |
never cancelled |
No dispose on VoiceBox |
Timer (60 ms re-drain) |
lib/audio/voice.dart:201 |
not stored |
never cancelled |
No dispose on VoiceBox |
AnimationController _pulse |
lib/ui/tile.dart:75 |
:65 (late final) |
lib/ui/tile.dart:147 |
Yes — dispose() at :146 |
AnimationController _breath |
lib/ui/tile.dart:77 |
:66 |
:148 |
Yes |
AnimationController _jiggle |
lib/ui/tile.dart:79 |
:67 |
:149 |
Yes |
AnimationController _flash |
lib/ui/tile.dart:81 |
:68 |
:150 |
Yes |
AnimationController _appear |
lib/ui/tile.dart:83 |
:69 |
:151 |
Yes |
AnimationController _spawn |
lib/ui/tile.dart:85 |
:70 |
:152 |
Yes |
AudioPlayer ×4 (pool) |
lib/audio/audio.dart:45-47 via _newPlayer() (:38) |
SoundBox._pool (:12) |
no release/dispose site in lib/ |
No — SoundBox has no dispose |
AudioPlayer ×1 (_ring) |
lib/audio/audio.dart:48 via _newPlayer() |
SoundBox._ring (:19) |
no release/dispose site in lib/ |
No |
TextEditingController _name |
lib/ui/modals.dart:204 |
:193 (late final) |
:226 (plus removeListener :225) |
Yes — dispose() at :224 |
TextEditingController _phrase |
lib/ui/modals.dart:205 |
:194 |
:227 |
Yes |
ValueNotifier<Set<String>> Diag.critical |
lib/diagnostics.dart:26 |
static field |
never disposed |
No — static singleton |
StreamController: zero instances. Proof: grep -rn "StreamController\|\.listen(" lib/ → no
output.
3.5 Assets referenced from code
| Asset path referenced |
Reference site |
On disk? |
assets/logo/mark_white.png |
lib/ui/logo.dart:13 (Image.asset) |
yes (1 file in assets/logo/) |
audio/<tone>.wav (via AssetSource('audio/$asset')) |
lib/audio/audio.dart:73; name computed by assetFor :83-84 from C.tones (lib/ui/theme.dart:45-48) |
12 files, all present |
audio/step.wav |
lib/audio/audio.dart:89 |
yes |
audio/click-up.wav |
lib/audio/audio.dart:92 |
yes |
audio/click-down.wav |
lib/audio/audio.dart:92 |
yes |
cadence_alarm (Android raw resource) |
lib/alarm_backstop.dart:55 (RawResourceAndroidNotificationSound) |
android/app/src/main/res/raw/cadence_alarm.wav |
@mipmap/ic_launcher |
lib/alarm_backstop.dart:74 (AndroidInitializationSettings) |
res/mipmap-*/ic_launcher.png + mipmap-anydpi-v26/ic_launcher.xml |
Diff against the 15 WAVs on disk: the 12 tone names in C.tones map through
assetFor(t) = '${t.toLowerCase().replaceAll('buzzer','buzz')}.wav' to exactly
beep, bell, bowl, buzz, cascade, chime, chirp, coin, fanfare, marimba, ping, pop .wav; adding
step.wav, click-up.wav, click-down.wav gives 15 of 15 — no unreferenced WAV and no
referenced-but-missing WAV. The 15 files on disk are asserted non-empty (> 1000 bytes) for the 12
tones only, at test/i18n_defaults_test.dart:74-77; step.wav, click-up.wav, click-down.wav are
not covered by that assertion.
Fonts: Dart code references family names only — F.display = 'Big Shoulders Display',
F.mono = 'Chivo Mono', F.dseg7 = 'DSEG7 Classic' (lib/ui/theme.dart:72-74). The 7 .ttf files
are bound to those three families in pubspec.yaml:48-69; no Dart file names a .ttf path. Family
usage sites: F.display at lib/main.dart:52, lib/ui/home.dart:713, lib/ui/header.dart:69, 82,
205, lib/ui/modals.dart:47, 107, 138, 255, 340, lib/ui/tile.dart:407, 734; F.mono at
lib/ui/modals.dart:58, 107, 401, 409, 434, 472, 488, 501, 541, 561, 640, 668, 685, 723,
lib/ui/tile.dart:456, 531, 555, 708, 745; F.dseg7 at lib/ui/header.dart:136,
lib/ui/tile.dart:477, 744.
Declared but unreferenced from Dart: assets/icon/ic_foreground.png, ic_legacy.png,
ic_monochrome.png are consumed by the flutter_launcher_icons config (pubspec.yaml:33-39), not
at runtime. pubspec.yaml:44-46 declares only assets/audio/ and assets/logo/ as bundled asset
directories — assets/icon/ is not bundled.
3.6 User-visible strings NOT routed through lib/i18n.dart
| String (verbatim) |
Site |
Where it appears |
'Cadence — Kitchen Timer' |
lib/main.dart:44 |
MaterialApp.title |
'CADENCE' |
lib/ui/header.dart:67 |
header wordmark |
' — Kitchen Timer' |
lib/ui/header.dart:79 |
header descriptor (>960 px) |
'⚙' |
lib/ui/header.dart:97 |
Settings button label |
':' |
lib/ui/header.dart:146 |
clock colon |
'◷' |
lib/ui/home.dart:708 |
empty-board glyph |
'✎ EDIT' |
lib/ui/tile.dart:552 |
edit-mode badge |
'10' (×2) |
lib/ui/tile.dart:603, :612 |
±10 s button digits |
'✕' |
lib/ui/tile.dart:619 |
stop button |
'+' / '−' |
lib/ui/tile.dart:602, :611 |
± signs |
'🗑' |
lib/ui/modals.dart:348 |
delete button |
':' (×2) |
lib/ui/modals.dart:432, :500 |
duration/step separators |
'✕' |
lib/ui/modals.dart:510 |
step-remove glyph |
'▲' / '▼' |
lib/ui/modals.dart:395, :413 |
duration steppers |
'Phase' |
lib/ui/modals.dart:482 |
step-name field placeholder |
'🇬🇧', 'English' |
lib/ui/modals.dart:624 |
language button |
'🇫🇷', 'Français' |
lib/ui/modals.dart:626 |
language button |
'${(vol * 100).round()} %' |
lib/ui/modals.dart:637 |
volume readout (unit % hard-coded) |
'Sear', 'Rest' |
lib/ui/modals.dart:276-277 |
default chain step names |
'Step' |
lib/ui/modals.dart:312, :373 |
default/blank step name |
'Timer' |
lib/ui/modals.dart:364 |
default timer name (blank field) |
'Timer' |
lib/engine/engine.dart:365 |
engine-side default name |
'Step' / 'Timer' |
lib/engine/models.dart:22, :67 |
JSON-decode fallbacks |
'⏰ $name' |
lib/alarm_backstop.dart:186 |
scheduled notification title |
'⏰ ${t.name}' |
lib/alarm_backstop.dart:259 |
immediate notification title |
'Timer alarms' |
lib/alarm_backstop.dart:47 |
Android channel name (Settings UI) |
'Rings when a timer expires while the app is not on screen' |
lib/alarm_backstop.dart:48-49 |
Android channel description |
'Manouche', 'Mozzarella sticks', 'Fries', 'Crispy', 'Melt cheese', 'Dough', 'Cook chicken', 'Cook', 'Flip' |
lib/engine/store.dart:327-341 |
first-launch seed timer and step names |
'[lot ${c.batchNo}]' |
lib/engine/engine.dart:137 |
journal/label suffix |
'?' |
lib/engine/engine.dart:135 |
unknown-id label fallback |
'#${engine.nextBatchNo(pid)}' |
lib/ui/home.dart:638 |
×N chip label |
'${t.name.toUpperCase()} #${widget.batchNo}' |
lib/ui/tile.dart:402 |
tile name + batch number |
'Cadence log — … — dd/MM HHhmm' |
lib/ui/modals.dart:707-708 |
share-sheet subject |
'Journal de bord Cadence v…\nAppareil : …\n' |
lib/ui/modals.dart:709-710 |
share-sheet body |
The whole journal/log surface is also user-reachable (the operator exports and sends the file from
Settings) and is French-only, not routed through i18n: 43 Journal.log(...) call sites across
lib/main.dart:29, lib/alarm_backstop.dart:81, 197, 228, 254, lib/diagnostics.dart:37, 44,
lib/audio/alarm_volume.dart:62, lib/ui/home.dart:104, 108, 113, 133, 137, 142, 150, 161, 185, 197,
289, 301, 307, 316, 358, 368, 383, 393, 396, 408, 428, 432, 451, 461, 476, 485, 542, 648, 653, 658,
lib/audio/voice.dart:75, 135, 137, 165, 185, plus the session-header lines built in
lib/journal.dart:82-94, 187, 198, 211.
3.7 Numeric literals appearing more than once in lib/
Generated by scanning non-comment code for integers ≥ 2 digits and all decimals, excluding
identifiers and hex colours. Values with two or more occurrences:
| Value |
× |
Sites |
10 |
26 |
audio/voice.dart:116; ui/header.dart:47,48,49,50; ui/home.dart:647,648,652,653,709; ui/modals.dart:39,43,124,287,310,352,357,450,461,682; ui/theme.dart:53; ui/tile.dart:435,439,576,603,612 |
60 |
23 |
audio/voice.dart:201; journal.dart:49,89,106; ui/modals.dart:218,219,277,292(×2),312,336,381,436,437,480,496,502,521,522,524(×2); ui/theme.dart:79(×2) |
1.0 |
15 |
audio/alarm_volume.dart:38,45(×2); audio/audio.dart:20,59,72; audio/voice.dart:30,55; engine/store.dart:168; ui/tile.dart:188,195,232,259,368,388 |
12 |
14 |
audio/voice.dart:168; ui/header.dart:193; ui/modals.dart:70,72,75,97(×2),100,464,555,636,720; ui/tile.dart:458,582 |
1000 |
13 |
alarm_backstop.dart:110,198; engine/engine.dart:177,182,232,235,320; journal.dart:49; ui/home.dart:126,129,131,371,661 |
14 |
11 |
ui/header.dart:103,112; ui/home.dart:693; ui/modals.dart:70,128,422,425,431,552; ui/tile.dart:409,411 |
0.0 |
9 |
audio/audio.dart:72; ui/grid_layout.dart:65,91,92,93,94; ui/tile.dart:188,195,807 |
100 |
9 |
audio/alarm_volume.dart:63; audio/audio.dart:103; audio/voice.dart:118,125; ui/home.dart:486; ui/modals.dart:637; ui/tile.dart:124,160(×2) |
15 |
7 |
i18n.dart:67,115; ui/modals.dart:96,124,739; ui/theme.dart:53; ui/tile.dart:266 |
22 |
7 |
ui/modals.dart:27,628,630,674,676; ui/tile.dart:478,480 |
16 |
6 |
ui/header.dart:47,49; ui/modals.dart:108,453,490,543 |
18 |
6 |
ui/modals.dart:260,283,315,332,468; ui/tile.dart:253 |
150 |
5 |
audio/audio.dart:104(×2); engine/engine.dart:32; ui/home.dart:154; ui/tile.dart:581 |
2.4 |
5 |
ui/tile.dart:507,542,575,583,591 |
20 |
5 |
ui/modals.dart:19,21,36; ui/tile.dart:218,798 |
26 |
5 |
ui/modals.dart:27(×3),40; ui/tile.dart:578 |
300 |
5 |
alarm_backstop.dart:40; audio/voice.dart:125,177; ui/tile.dart:84,86 |
0.15 |
4 |
audio/alarm_volume.dart:28; ui/theme.dart:67(×2); ui/tile.dart:350 |
0.6 |
4 |
ui/tile.dart:248(×3),421 |
1.5 |
4 |
ui/modals.dart:670,687; ui/theme.dart:18; ui/tile.dart:542 |
1024 |
4 |
journal.dart:25(×2),26(×2) |
180 |
4 |
ui/modals.dart:217,428(×2); ui/tile.dart:358 |
2.6 |
4 |
ui/home.dart:716; ui/tile.dart:423,439,598 |
20.8 |
4 |
ui/header.dart:137,138; ui/modals.dart:256,341 |
0.04 |
3 |
ui/header.dart:72,138,207 |
0.4 |
3 |
main.dart:20 (inside '0.4.12'); ui/tile.dart:248,421 |
0.5 |
3 |
ui/tile.dart:233,357,513 |
1000.0 |
3 |
ui/tile.dart:187,194,200 |
11 |
3 |
ui/modals.dart:552; ui/tile.dart:556,558 |
24 |
3 |
ui/modals.dart:251,345,690 |
260 |
3 |
engine/engine.dart:51; engine/store.dart:329; ui/home.dart:326 |
30 |
3 |
audio/voice.dart:110; ui/grid_layout.dart:76; ui/theme.dart:53 |
4.8 |
3 |
ui/tile.dart:576,584,592 |
7000 |
3 |
engine/engine.dart:52; engine/models.dart:114,139 |
0.02 |
2 |
ui/tile.dart:480,535 |
0.045 |
2 |
ui/grid_layout.dart:21; ui/header.dart:72 |
0.06 |
2 |
ui/tile.dart:411,635 |
0.28 |
2 |
ui/tile.dart:242,258 |
0.35 |
2 |
ui/theme.dart:66(×2) |
0.45 |
2 |
ui/tile.dart:487,504 |
0.85 |
2 |
ui/grid_layout.dart:24; ui/tile.dart:350 |
1.2 |
2 |
ui/tile.dart:358,678 |
1.8 |
2 |
ui/tile.dart:319,513 |
10.0 |
2 |
ui/header.dart:181; ui/tile.dart:598 |
106 |
2 |
ui/theme.dart:39(×2) |
12.8 |
2 |
ui/header.dart:179; ui/modals.dart:669 |
120 |
2 |
audio/audio.dart:107; ui/modals.dart:276 |
14.4 |
2 |
ui/header.dart:179; ui/modals.dart:474 |
14.7 |
2 |
ui/modals.dart:641,724 |
1500 |
2 |
alarm_backstop.dart:175; ui/home.dart:160 |
2.2 |
2 |
ui/modals.dart:60,140 |
200 |
2 |
audio/audio.dart:103(×2) |
3.5 |
2 |
ui/tile.dart:266(×2) |
34 |
2 |
ui/tile.dart:243,249 |
36 |
2 |
engine/engine.dart:61,62 |
360 |
2 |
engine/store.dart:339,341 |
500 |
2 |
alarm_backstop.dart:181; ui/header.dart:143 |
560 |
2 |
ui/header.dart:37; ui/modals.dart:24 |
9.0 |
2 |
ui/header.dart:180,181 |
900 |
2 |
ui/home.dart:294; ui/tile.dart:76 |
999 |
2 |
ui/modals.dart:100; ui/tile.dart:545 |
Pairs where the two sites are semantically linked and the value is written twice rather than shared:
7000 (Engine.firstVoiceGapMs vs the RunEntry.voiceGap default and its JSON fallback);
260 (Engine.dblMs vs the seed duration for Fries vs an unrelated tile padding);
1500 (Backstop._graceMs vs the freeze threshold in the home ticker);
0.15 (AlarmVolume.floor vs the urgency-band boundary in fillFor vs a tile scale factor);
0.85 (GridLayout.maxAspect vs a tile spawn scale);
150 (Engine.tickMs vs the literal 150 in Timer.periodic at home.dart:154, vs haptic
durations, vs a tile width tier);
560 (header clock breakpoint vs modal max width);
900 (announcement delay vs _pulse duration).
3.8 Duplicated blocks of 5+ lines in 2+ places
Detected by hashing 5-line windows of non-blank, non-comment-only, whitespace-normalised lines, then
extending each match to its maximal length. Locations only, no judgement.
| Block |
Length (substantive lines) |
Locations |
_inputDeco().copyWith(counterText: '', contentPadding: EdgeInsets.symmetric(...), fillColor: C.panel) + style: const TextStyle(fontFamily: F.mono, fontWeight: FontWeight.w700, fontSize: 16, color: C.text) |
7 |
lib/ui/modals.dart:486-492; lib/ui/modals.dart:539-545 |
boxShadow: const [BoxShadow(color: Color(0x291C211C), blurRadius: 4, offset: Offset(0, 1))] inside a BoxDecoration with borderRadius/border |
5 |
lib/ui/tile.dart:441-445; lib/ui/tile.dart:696-700 |
style: const TextStyle(fontFamily: F.display, fontWeight: FontWeight.w700, fontSize: 20.8, letterSpacing: 1, color: C.text) on a TextField |
5 |
lib/ui/modals.dart:253-257; lib/ui/modals.dart:338-342 |
_HBtn/_modalBtn trailing Text(...toUpperCase(), style: TextStyle(fontFamily: F.display, fontWeight: …, fontSize: …, letterSpacing: …, color: …)) closing shape |
5 |
lib/ui/header.dart:200-204; lib/ui/modals.dart:133-137 |
class NullHost implements EngineHost { @override int now() => 0; @override void persistDefs() {} … } — the full 20-line no-op host |
20 |
test/announcement_test.dart:23-42; test/i18n_defaults_test.dart:13-32; test/store_test.dart:12-31 |
The same no-op EngineHost body minus the class header (a FakeHost variant) |
15 |
test/backstop_test.dart:17-31; test/robustness_test.dart:20-34 |
import 'dart:convert'; import 'package:flutter_test/…'; import 'package:shared_preferences/…'; import 'package:cadence/diagnostics.dart'; import 'package:cadence/engine/engine.dart'; import 'package:cadence/engine/models.dart'; import 'package:cadence/engine/store.dart'; |
7 |
test/robustness_test.dart:4-10; test/store_test.dart:4-10 |
SharedPreferences.setMockInitialValues(_preZoneRemovalTablet()); final store = await Store.open(); final e = Engine(NullHost()); store.load(e); store.migrateZoneSounds(e); |
5 |
test/store_test.dart:127-131; test/store_test.dart:146-150; test/store_test.dart:161-165 |
'cadence-timers-v1': jsonEncode([{'id': 'a', 'name': 'Fries', 'durationSec': 60, 'phrase': ''}, …]), 'cadence-seeded-v1': true, fixture |
5 |
test/robustness_test.dart:49-53; test/robustness_test.dart:69-73 |
final store = await Store.open(); final e = Engine(NullHost()); store.load(e); expect(store.seedIfFresh(e), isFalse); shape |
5 |
test/store_test.dart:79-83; test/store_test.dart:212-216 |
child: const Text('open'), ), ), )); await tester.tap(find.text('open')); await tester.pumpAndSettle(); — dialog-opening harness |
5 |
test/announcement_test.dart:213-217; test/editor_layout_test.dart:28-32; test/volume_test.dart:149-153; test/volume_test.dart:178-182 |
v.physicalSize = const Size(…); v.devicePixelRatio = 1.0; addTearDown(() { v.resetPhysicalSize(); v.resetDevicePixelRatio(); }); |
5 |
test/announcement_test.dart:193-197; test/editor_layout_test.dart:43-47 |
Additional single-expression duplication that the 5-line window does not catch, recorded because it
spans files: the preset chip label
p[1] != 0 ? '${p[0]}:${p[1].toString().padLeft(2, '0')}' : '${p[0]}' appears at
lib/ui/modals.dart:302-304 and, verbatim, at test/editor_layout_test.dart:18-20; the test's
comment at line 17 records that it is "kept in sync with the chip builder".
4. Test map
| Test file |
Lines |
test( |
testWidgets( |
group( |
Executed tests |
Primary lib/ file exercised |
Baseline coverage of that file |
test/announcement_test.dart |
267 |
9 |
4 |
4 |
13 |
lib/i18n.dart (+ engine/store.dart, ui/modals.dart) |
90.00% / 94.40% / 65.33% |
test/backstop_test.dart |
191 |
6 |
0 |
0 |
6 |
lib/alarm_backstop.dart |
78.00% |
test/editor_layout_test.dart |
79 |
0 |
2 |
0 |
3 (one declaration runs per language) |
lib/ui/modals.dart (+ ui/theme.dart C.presets) |
65.33% / 0.00% |
test/engine_test.dart |
341 |
21 |
0 |
5 |
21 |
lib/engine/engine.dart |
94.47% |
test/grid_layout_test.dart |
152 |
13 |
0 |
4 |
13 |
lib/ui/grid_layout.dart |
100.00% |
test/i18n_defaults_test.dart |
117 |
6 |
0 |
3 |
6 |
lib/i18n.dart (+ audio/audio.dart::assetFor, ui/theme.dart::tones) |
90.00% / 5.26% / 0.00% |
test/journal_test.dart |
193 |
11 |
0 |
0 |
11 |
lib/journal.dart |
81.73% |
test/robustness_test.dart |
314 |
16 |
0 |
7 |
16 |
lib/engine/store.dart + lib/engine/engine.dart |
94.40% / 94.47% |
test/source_hygiene_test.dart |
25 |
1 |
0 |
0 |
1 |
none (text scan of lib/) |
n/a |
test/store_test.dart |
220 |
10 |
0 |
1 |
10 |
lib/engine/store.dart |
94.40% |
test/version_test.dart |
29 |
1 |
0 |
0 |
1 |
lib/main.dart (kAppVersion only) |
0.00% |
test/voice_test.dart |
197 |
9 |
0 |
0 |
9 |
lib/audio/voice.dart |
86.36% |
test/volume_test.dart |
188 |
11 |
2 |
3 |
13 |
lib/audio/alarm_volume.dart (+ ui/modals.dart slider) |
100.00% / 65.33% |
| Total |
2,313 |
114 |
8 |
27 |
123 |
— |
— |
Reconciliation with the baseline: 114 + 8 = 122 declarations, but
test/editor_layout_test.dart:37 sits inside for (final lang in ['fr', 'en']) (line 36) and
therefore registers two tests, giving 123 executed tests — the exact figure in
proof/00_baseline/SUMMARY.md §5 (00:01 +123: All tests passed!).
4.1 lib/ files with no dedicated test file (neutral fact)
A "dedicated test file" here means a file in test/ whose name and subject match the lib/ file.
Eight lib/ files have one: engine.dart↔engine_test.dart, store.dart↔store_test.dart,
alarm_backstop.dart↔backstop_test.dart, voice.dart↔voice_test.dart,
alarm_volume.dart↔volume_test.dart, journal.dart↔journal_test.dart,
grid_layout.dart↔grid_layout_test.dart, i18n.dart↔i18n_defaults_test.dart.
Ten lib/ files have none:
lib/ file |
Lines |
Baseline coverage |
Exercised indirectly by |
lib/main.dart |
58 |
0.00% |
version_test.dart reads kAppVersion only; main() and CadenceApp are never run |
lib/diagnostics.dart |
54 |
86.36% |
store_test, voice_test, backstop_test, journal_test, robustness_test (assert on Diag.log / Diag.critical) |
lib/engine/models.dart |
160 |
72.13% |
engine_test, store_test, robustness_test, backstop_test, announcement_test, i18n_defaults_test |
lib/audio/audio.dart |
116 |
5.26% |
i18n_defaults_test.dart:74 calls SoundBox.assetFor only; no test constructs a SoundBox |
lib/ui/modals.dart |
746 |
65.33% |
announcement_test (editor), editor_layout_test (presets), volume_test (slider) |
lib/ui/home.dart |
722 |
0.00% |
nothing — no test imports it |
lib/ui/tile.dart |
819 |
0.00% |
nothing — no test imports it |
lib/ui/header.dart |
215 |
0.00% |
nothing — no test imports it |
lib/ui/theme.dart |
82 |
0.00% |
editor_layout_test and i18n_defaults_test read C.presets / C.tones as compile-time constants; fillFor, fmtTime, fmtUp are never executed |
lib/ui/logo.dart |
18 |
0.00% |
nothing — no test imports it |
Six of the eighteen lib/ files have 0.00% line coverage in the baseline: main.dart,
ui/header.dart, ui/home.dart, ui/logo.dart, ui/theme.dart, ui/tile.dart — 1,914 of the
4,853 lib/ lines by file size, and 837 of the 1,927 lcov-instrumented lines (17 + 70 + 390 + 4 +
12 + 344, from proof/00_baseline/SUMMARY.md §6).
5. Coverage manifest
Every file listed was read end to end at commit 03a176e72ef0075eec86b8915cbe6e93042a3b9d.
5.1 lib/ — 18 files, 4,853 lines (complete)
| # |
File |
Lines |
| 1 |
lib/alarm_backstop.dart |
279 |
| 2 |
lib/audio/alarm_volume.dart |
68 |
| 3 |
lib/audio/audio.dart |
116 |
| 4 |
lib/audio/voice.dart |
204 |
| 5 |
lib/diagnostics.dart |
54 |
| 6 |
lib/engine/engine.dart |
432 |
| 7 |
lib/engine/models.dart |
160 |
| 8 |
lib/engine/store.dart |
354 |
| 9 |
lib/i18n.dart |
167 |
| 10 |
lib/journal.dart |
250 |
| 11 |
lib/main.dart |
58 |
| 12 |
lib/ui/grid_layout.dart |
109 |
| 13 |
lib/ui/header.dart |
215 |
| 14 |
lib/ui/home.dart |
722 |
| 15 |
lib/ui/logo.dart |
18 |
| 16 |
lib/ui/modals.dart |
746 |
| 17 |
lib/ui/theme.dart |
82 |
| 18 |
lib/ui/tile.dart |
819 |
|
Total |
4,853 |
5.2 test/ — 13 files, 2,313 lines (complete)
| # |
File |
Lines |
| 1 |
test/announcement_test.dart |
267 |
| 2 |
test/backstop_test.dart |
191 |
| 3 |
test/editor_layout_test.dart |
79 |
| 4 |
test/engine_test.dart |
341 |
| 5 |
test/grid_layout_test.dart |
152 |
| 6 |
test/i18n_defaults_test.dart |
117 |
| 7 |
test/journal_test.dart |
193 |
| 8 |
test/robustness_test.dart |
314 |
| 9 |
test/source_hygiene_test.dart |
25 |
| 10 |
test/store_test.dart |
220 |
| 11 |
test/version_test.dart |
29 |
| 12 |
test/voice_test.dart |
197 |
| 13 |
test/volume_test.dart |
188 |
|
Total |
2,313 |
Both totals match proof/00_baseline/SUMMARY.md §10 exactly (lib 18 files / 4,853 lines; test 13
files / 2,313 lines).
Line counts here are wc -l (newline count), the same method the baseline used for lib/ and
test/; a file whose final line has no trailing newline therefore reads one lower than its last
line number.
| File |
Lines |
Read in full? |
Notes recorded above |
pubspec.yaml |
68 |
yes |
10 direct deps, 5 dev deps, launcher-icon config (33-39), asset dirs assets/audio/ + assets/logo/ (44-46), 3 font families / 7 .ttf (48-69) |
analysis_options.yaml |
28 |
yes |
include: package:flutter_lints/flutter.yaml (10); the linter: rules: block (23-25) contains only commented-out examples — no rule is enabled or disabled |
android/app/build.gradle.kts |
51 |
yes |
namespace/applicationId dev.sergemio.cadence (8, 21); Java 17 (13-14); isCoreLibraryDesugaringEnabled = true (16) with desugar_jdk_libs:2.1.4 (46); release build signed with the debug signing config (34), flagged TODO at 32-33; minSdk/targetSdk/compileSdk all inherited from the Flutter plugin (9, 24-25) |
android/app/src/main/AndroidManifest.xml |
79 |
yes |
Permissions: VIBRATE (2), WAKE_LOCK (3), MODIFY_AUDIO_SETTINGS (8), POST_NOTIFICATIONS (12), USE_EXACT_ALARM (13), SCHEDULE_EXACT_ALARM maxSdk 32 (14-15), USE_FULL_SCREEN_INTENT (16), RECEIVE_BOOT_COMPLETED (17). Activity launchMode="singleTop", taskAffinity="" (25-26). Two flutter_local_notifications receivers (46-56). <queries> includes TTS_SERVICE (76) |
android/app/src/main/kotlin/dev/sergemio/cadence/MainActivity.kt |
176 |
yes |
Both channels, TextToSpeech with USAGE_ALARM (122-127), utterance-completion bookkeeping (160-169), onDestroy shuts TTS down (171-175). Eight catch (_: Exception) sites, all with an empty or constant-fallback body: 58, 79, 86, 92, 97, 105, 151, 172 |
ios/Runner/AppDelegate.swift |
207 |
yes |
Mirror of the two channels; header comment at 12-13 states verbatim ⚠️ NOT YET COMPILED — written on Windows, no Xcode available.; getAlarmVolume returns nil by design (54-57); .playback session category (145) |
ios/Runner/Info.plist |
70 |
yes |
CFBundleDisplayName Cadence (10); portrait + both landscapes on iPhone (56-61), all four on iPad (62-68); scene manifest present (29-49) |
tools/build_ringtones.py |
238 |
yes |
numpy-only WAV generator; writes the 12 tones into assets/audio/ and cadence_alarm.wav into android/.../res/raw/ (236). Two normalisation paths: write() RMS + tanh limiter (84) for the v0.4.4 six and the backstop, write_peak() peak-only (175) for the v0.4.10 six. It does not generate step.wav, click-up.wav, or click-down.wav — those three of the fifteen WAVs on disk have no generator in this file |
web/index.html |
46 |
yes |
Stock Flutter template, unmodified; <meta name="description" content="A new Flutter project."> (21), apple-mobile-web-app-title cadence (26), <title>cadence</title> (32), loader <script src="flutter_bootstrap.js" async> (44) |
web/manifest.json |
35 |
yes |
Stock template: "name": "cadence", "description": "A new Flutter project.", background_color/theme_color #0175C2, orientation: portrait-primary |
web/favicon.png, web/icons/Icon-192.png, Icon-512.png, Icon-maskable-192.png, Icon-maskable-512.png |
— |
binary, not read as text |
Present; enumerated by find web -type f |
No file in lib/ or test/ was skipped or partially read.
6. Reproduction
Every grep quoted above was run from the app repository at HEAD
03a176e72ef0075eec86b8915cbe6e93042a3b9d. Later streams should record their own commands with:
proof/run_and_record.sh [not published] <out-file> <command...>
and refresh the checksum index with:
proof/make_manifest.sh [not published]