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
180 lines
6.5 KiB
Markdown
180 lines
6.5 KiB
Markdown
# 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:
|
||
|
||
```dart
|
||
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` — use `text()` from `designs/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
|
||
|
||
1. Named event handlers only — `onPressed: _onSave`, never `() => _save()`.
|
||
2. `selectField` loads on tap; never pre-load picker options.
|
||
3. List parsing via `utils/ObjectConvertors.dart` — never inline `.map(...fromJson)`.
|
||
4. Pagination: `HistoryRequest { PageAndSort }`; responses are `*Page`; `last == true` stops loading.
|
||
5. Reload-on-return: child `Navigator.pop(context, true)`, parent refreshes on the result.
|
||
6. New endpoint → `mason make api_endpoint` (injects into all four HTTP files).
|
||
7. New secure field → `mason make internal_memory`.
|
||
8. 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
|
||
|
||
```bash
|
||
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
|
||
```
|