Initial commit: Grounded Flutter frontend

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
This commit is contained in:
alvocool
2026-07-27 09:11:17 +03:00
commit 16bff634b5
315 changed files with 19132 additions and 0 deletions

View File

@@ -0,0 +1,16 @@
/// Abandoning costs the most debt of all, so it is always explicit and always
/// carries a reason.
class AbandonRequest {
String commitmentId;
String reason;
AbandonRequest({this.commitmentId = "", this.reason = ""});
Map<String, dynamic> toJson() {
final Map<String, dynamic> data = <String, dynamic>{};
data['commitmentId'] = commitmentId;
data['reason'] = reason;
return data;
}
}

View File

@@ -0,0 +1,13 @@
/// Spend one of the rationed monthly tokens to wipe an item debt, no questions
/// asked.
class AmnestyRequest {
String commitmentId;
AmnestyRequest({this.commitmentId = ""});
Map<String, dynamic> toJson() {
final Map<String, dynamic> data = <String, dynamic>{};
data['commitmentId'] = commitmentId;
return data;
}
}

View File

@@ -0,0 +1,58 @@
/// Create or update a commitment.
class CommitmentRequest {
String? id;
String type;
String commitmentClass;
String title;
String category;
String dueStart;
String dueEnd;
int estMinutes;
String energy;
String proofType;
int proofTimerMinutes;
String rrule;
CommitmentRequest({
this.id,
this.type = "TASK",
this.commitmentClass = "Standard",
this.title = "",
this.category = "",
this.dueStart = "",
this.dueEnd = "",
this.estMinutes = 0,
this.energy = "Medium",
this.proofType = "Honour",
this.proofTimerMinutes = 0,
this.rrule = "",
});
Map<String, dynamic> toJson() {
final Map<String, dynamic> data = <String, dynamic>{};
data['id'] = id;
data['type'] = type;
data['commitmentClass'] = commitmentClass;
data['title'] = title;
data['category'] = category;
data['dueStart'] = dueStart;
data['dueEnd'] = dueEnd;
data['estMinutes'] = estMinutes;
data['energy'] = energy;
data['proofType'] = proofType;
data['proofTimerMinutes'] = proofTimerMinutes;
data['rrule'] = rrule;
return data;
}
}

View File

@@ -0,0 +1,38 @@
/// Completion always carries its proof. The server decides Complete vs Late
/// Complete from the window, never the client.
class CompletionRequest {
String commitmentId;
String proofType;
/// Reference to the uploaded artefact — photo id, timer session id, geofence
/// dwell id or witness confirmation id.
String proofRef;
/// Foreground seconds actually run, for Timer proof.
int timerSeconds;
double latitude;
double longitude;
CompletionRequest({
this.commitmentId = "",
this.proofType = "Honour",
this.proofRef = "",
this.timerSeconds = 0,
this.latitude = 0,
this.longitude = 0,
});
Map<String, dynamic> toJson() {
final Map<String, dynamic> data = <String, dynamic>{};
data['commitmentId'] = commitmentId;
data['proofType'] = proofType;
data['proofRef'] = proofRef;
data['timerSeconds'] = timerSeconds;
data['latitude'] = latitude;
data['longitude'] = longitude;
return data;
}
}

View File

@@ -0,0 +1,27 @@
/// Deferral always carries an excuse. Free text, minimum length enforced, no
/// template buttons — the friction is the point.
class DeferralRequest {
String commitmentId;
String excuseText;
String newDueStart;
String newDueEnd;
DeferralRequest({
this.commitmentId = "",
this.excuseText = "",
this.newDueStart = "",
this.newDueEnd = "",
});
Map<String, dynamic> toJson() {
final Map<String, dynamic> data = <String, dynamic>{};
data['commitmentId'] = commitmentId;
data['excuseText'] = excuseText;
data['newDueStart'] = newDueStart;
data['newDueEnd'] = newDueEnd;
return data;
}
}

View File

@@ -0,0 +1,25 @@
class DeviceRequest {
String identifier;
String model;
String platform;
String version;
DeviceRequest({
this.identifier = "",
this.model = "",
this.platform = "",
this.version = "",
});
Map<String, dynamic> toJson() {
final Map<String, dynamic> data = <String, dynamic>{};
data['identifier'] = identifier;
data['model'] = model;
data['platform'] = platform;
data['version'] = version;
return data;
}
}

View File

@@ -0,0 +1,41 @@
class GoalRequest {
String? id;
String title;
String description;
String category;
String defaultClass;
String startDate;
String targetDate;
String colourHex;
GoalRequest({
this.id,
this.title = "",
this.description = "",
this.category = "",
this.defaultClass = "Standard",
this.startDate = "",
this.targetDate = "",
this.colourHex = "",
});
Map<String, dynamic> toJson() {
final Map<String, dynamic> data = <String, dynamic>{};
data['id'] = id;
data['title'] = title;
data['description'] = description;
data['category'] = category;
data['defaultClass'] = defaultClass;
data['startDate'] = startDate;
data['targetDate'] = targetDate;
data['colourHex'] = colourHex;
return data;
}
}

View File

@@ -0,0 +1,11 @@
class IdRequest {
String id;
IdRequest({this.id = ""});
Map<String, dynamic> toJson() {
final Map<String, dynamic> data = <String, dynamic>{};
data['id'] = id;
return data;
}
}

View File

@@ -0,0 +1,14 @@
class LoginData {
String username;
String password;
LoginData({this.username = "", this.password = ""});
Map<String, dynamic> toJson() {
final Map<String, dynamic> data = <String, dynamic>{};
data['username'] = username;
data['password'] = password;
return data;
}
}

View File

@@ -0,0 +1,14 @@
class ReportCardRequest {
String periodStart;
String periodEnd;
ReportCardRequest({this.periodStart = "", this.periodEnd = ""});
Map<String, dynamic> toJson() {
final Map<String, dynamic> data = <String, dynamic>{};
data['periodStart'] = periodStart;
data['periodEnd'] = periodEnd;
return data;
}
}

View File

@@ -0,0 +1,45 @@
class SessionLogRequest {
String? id;
String templateId;
String startedAt;
String endedAt;
double sessionRpe;
int sleepScore;
int sorenessScore;
int motivationScore;
List<Map<String, dynamic>> sets;
SessionLogRequest({
this.id,
this.templateId = "",
this.startedAt = "",
this.endedAt = "",
this.sessionRpe = 0,
this.sleepScore = 0,
this.sorenessScore = 0,
this.motivationScore = 0,
List<Map<String, dynamic>>? sets,
}) : sets = sets ?? <Map<String, dynamic>>[];
Map<String, dynamic> toJson() {
final Map<String, dynamic> data = <String, dynamic>{};
data['id'] = id;
data['templateId'] = templateId;
data['startedAt'] = startedAt;
data['endedAt'] = endedAt;
data['sessionRpe'] = sessionRpe;
data['sleepScore'] = sleepScore;
data['sorenessScore'] = sorenessScore;
data['motivationScore'] = motivationScore;
data['sets'] = sets;
return data;
}
}

View File

@@ -0,0 +1,18 @@
/// Pauses debt accrual entirely. Requires a reason and is logged in history.
class SickModeRequest {
bool enabled;
String reason;
String until;
SickModeRequest({this.enabled = false, this.reason = "", this.until = ""});
Map<String, dynamic> toJson() {
final Map<String, dynamic> data = <String, dynamic>{};
data['enabled'] = enabled;
data['reason'] = reason;
data['until'] = until;
return data;
}
}

View File

@@ -0,0 +1,11 @@
class ToneRequest {
String tone;
ToneRequest({this.tone = "Strict"});
Map<String, dynamic> toJson() {
final Map<String, dynamic> data = <String, dynamic>{};
data['tone'] = tone;
return data;
}
}