A to-do app that doesn't believe you — an enforcement layer rather than a
neutral ledger.
Architecture ported from Autoreceptives/Frontend/Receptive: stacked MVVM with
the mandatory 4-file screen pattern, one ParentViewModel owning the loading /
network / error overlays and the handleError decision tree, one AppDataManager
gateway, dio comms carrying the three identity headers, secure storage with
random-suffixed keys, and a single-chokepoint Navigator. Package root and Dart
package name are both Grounded; org is nya.
The enforcement engine, one unit per formula in utils/:
- DebtEngine w(class) x severity(d) x decay(t), sublinear severity so old
misses cannot swamp the score; abandonment 2x with 30-day
decay immunity; late complete retains 30%
- StandingEngine Good -> Warned -> Grounded -> Lockdown, derived not set;
Grounded replaces home with the overdue queue
- CapacityEngine blocks over-scheduling against p50 of historically completed
minutes, with a learned per-category estimation multiplier
- IntegrityEngine session integrity, weekly volume, plyometric contact ceiling
and enforced recovery gaps
- ExcuseAnalyser on-device excuse clustering plus the confrontation copy
- GuardrailEngine distress detection and rationed amnesty
- ToneEngine all enforcement copy, so the tone cap lives in one place
CommitmentEvent is append-only and is the source of truth rather than the
status field, which is what makes honest history and excuse analysis possible.
Goals contain commitments via parentId, and a task can be run from a
full-screen runner that derives elapsed time from wall-clock so screen-off
cannot lose time. Backgrounding pauses the clock and is counted. The runner is
mirrored into an ongoing notification, with alarm-class full-screen intents
reserved for non-negotiables.
Design language, fonts, icon and native splash are in place; Mason bricks are
retargeted to this project and verified end-to-end.
flutter analyze lib/ reports no errors.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Mfu2gQLSFN21YRBcU2NrTt
6.5 KiB
Grounded — Flutter Frontend
A to-do app that doesn't believe you.
Architecture ported from Documents/Autoreceptives/Frontend/Receptive. Same
structure, conventions and infrastructure; the business logic is the debt and
enforcement engine.
1. Stack
Flutter (Dart ^3.6.2), stacked MVVM, dio, flutter_secure_storage,
flutter_dotenv, fl_chart, flutter_local_notifications.
Package name and lib/ root are both Grounded, so imports read
package:Grounded/Grounded/.... Org is nya (nya.grounded).
Codegen is via Mason — screens, endpoints and storage fields are generated, never hand-scaffolded.
2. Layout (lib/Grounded/)
about/external/data/ responses & domain models (+ pages/ for pagination)
about/external/initial/ request bodies
about/internal/application/ enums and app-internal models
about/internal/file/ FileStorage
comms/ CommsDirections · ConnectComms · Comms
informatics/ DataManager · AppDataManager
memory/ ConnectInternalMemory · InternalMemory
configs/ Navigator · Env · NotificationServiceConfig
designs/ Component · Shell · Responsive · buttons/ input/ text/
see/<feature>/ screens, 4 files each
utils/ Colors · CommonUtils · engines · validators
3. The 4-file screen pattern (mandatory)
| File | Role |
|---|---|
Foo.dart |
StatefulWidget shell — creates FooState only. |
FooState.dart |
All UI + local state; implements ConnectFoo. |
ViewFoo.dart |
extends ParentViewModel — business logic, calls DataManager, pushes results back through ConnectFoo. |
ConnectFoo.dart |
Abstract callbacks so the ViewModel never touches widgets. |
ViewModelBuilder<ViewFoo>.reactive wires them. onViewModelReady assigns
_model and calls one named _initiate(). No logic in builder: before the
return.
Generate with: mason make mvvc_template --project Grounded --screen Foo
4. ParentViewModel
Constructs the single AppDataManager and owns everything cross-cutting:
showLoading / closeLoading, hasNetwork(retry), showError,
showApplicationNotification, and handleError — whose decision tree routes
401 → sessionExpired() and error code 5000.901 → updateMe().
Guard every network call:
void loadThing(ThingRequest request) async {
if (!await hasNetwork(() => loadThing(request))) return;
showLoading('Loading…');
try {
final response = await getDataManager().getThing(request);
closeLoading();
connection.onThingLoaded(Thing.fromJson(response.data));
} catch (e) {
handleError(e, () => loadThing(request), () => dismissError(), 'Retry');
}
}
5. Domain — the enforcement engine
Three primitives: Commitment (what you said you'd do), Debt (weighted,
decaying score), Standing (Good → Warned → Grounded → Lockdown).
Goals contain commitments via Commitment.parentId — "Workout" holds
"Monday shoulders". Goals never carry debt; the tasks inside them do.
Engines in utils/, each mapping 1:1 to the spec's formulas:
| Engine | Owns |
|---|---|
DebtEngine |
w(class) × severity(d) × decay(t); abandonment 2×, no decay 30d; late complete retains 30% |
StandingEngine |
tier derivation; what each tier permits |
CapacityEngine |
blocks over-scheduling against p50 of historical completed minutes × 0.85 |
IntegrityEngine |
session integrity, weekly volume, plyo contact ceiling, recovery gate |
ExcuseAnalyser |
on-device excuse clustering + the confrontation copy |
GuardrailEngine |
distress detection, amnesty tokens |
ToneEngine |
all enforcement copy — the one place the tone cap is enforced |
Thresholds live in Thresholds.dart. Never inline a limit.
CommitmentEvent is append-only and is the source of truth — not the
status field. That's what makes honest history and excuse analysis possible.
6. Design language
See design.md. In short: black chrome → white sheet with a 28 radius →
oversized Light display title → tiny grey all-caps labels above values.
- Every screen is built from
Sheet(designs/Shell.dart). - Display titles are
TextType.Light. Bold is for labels and metrics only. - Never raw
Text/TextStyle— usetext()fromdesigns/text/Text.dart. - Never inline hex — use
utils/Colors.dart. - Standing owns a colour used consistently everywhere (chrome, chip, meter).
- One primary button per screen; everything else outlined or text.
Font: General Sans (Fontshare, FFL — see fonts/LICENSE-GeneralSans.txt),
wrapped as TextType.{Bold,Medium,Regular,Light}.
7. Live task runner
see/live/ is the full-screen runner: a task being run takes over the screen.
Elapsed time is derived from wall-clock, not counted by the ticker, so a
screen-off period cannot lose time. Backgrounding pauses the clock and
increments backgroundedCount — that's what makes Timer proof mean anything.
LocalNotificationEngine mirrors it into an ongoing notification and fires
alarm-class full-screen intents for non-negotiables. Android manifest
carries USE_FULL_SCREEN_INTENT, SCHEDULE_EXACT_ALARM, and the activity is
showWhenLocked + turnScreenOn.
8. Hard conventions
- Named event handlers only —
onPressed: _onSave, never() => _save(). selectFieldloads on tap; never pre-load picker options.- List parsing via
utils/ObjectConvertors.dart— never inline.map(...fromJson). - Pagination:
HistoryRequest { PageAndSort }; responses are*Page;last == truestops loading. - Reload-on-return: child
Navigator.pop(context, true), parent refreshes on the result. - New endpoint →
mason make api_endpoint(injects into all four HTTP files). - New secure field →
mason make internal_memory. - One class per file, named after the class.
9. Environment
.env is a bundled asset, loaded in main() before runApp:
GROUNDED_PRODUCTION_PATH · GROUNDED_LOCAL_PATH · LOCALISED_APP_VERSION
isProd in CommsDirections.dart flips local↔production. Requests carry the
three identity headers: what, whom, version.
10. Commands
flutter pub get
flutter analyze lib/ # bricks/ are excluded — templates aren't valid Dart
flutter run
mason get
mason make mvvc_template --project Grounded --screen MyScreen
dart run flutter_launcher_icons
dart run flutter_native_splash:create