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,159 @@
import 'dart:math';
import '../about/external/data/Commitment.dart';
import '../about/external/data/Habit.dart';
import '../about/internal/application/CommitmentClass.dart';
import '../about/internal/application/CommitmentStatus.dart';
/// The single number the whole system runs on.
///
/// ```
/// debt = Sum over open/missed commitments:
/// w(class) x severity(days_overdue) x recency_decay(t)
///
/// w(class): non-negotiable 5.0 | standard 2.0 | elective 0.0
/// severity(d): 1 + log2(1 + d)
/// recency_decay: 0.5 ^ (days_since / half_life)
/// abandonment: one-time +2x w(class), no decay for 30 days
/// late_complete: debt reduced to 30% of accrued, not 0
/// ```
///
/// Sublinear severity is the load-bearing choice: with linear growth a single
/// ancient task swamps the score and the number stops meaning anything.
class DebtEngine {
/// Half-life of the recency decay, in days.
static const double halfLifeDays = 14;
/// Multiplier applied once when a commitment is abandoned.
static const double abandonmentMultiplier = 2.0;
/// Days an abandonment resists decay.
static const int abandonmentProtectionDays = 30;
/// What a late completion leaves behind. Never zero — otherwise you learn
/// that everything is negotiable.
static const double lateCompleteRetention = 0.30;
/// Debt weight per habit shortfall unit.
static const double habitShortfallWeight = 1.0;
/// `severity(d) = 1 + log2(1 + d)`.
static double severity(int daysOverdue) {
final int days = daysOverdue < 0 ? 0 : daysOverdue;
return 1 + (log(1 + days) / ln2);
}
/// `recency_decay(t) = 0.5 ^ (days_since / half_life)`.
static double recencyDecay(int daysSince) {
final int days = daysSince < 0 ? 0 : daysSince;
return pow(0.5, days / halfLifeDays).toDouble();
}
/// Debt contributed by one commitment, as of [now].
static double commitmentDebt(Commitment commitment, {DateTime? now}) {
final DateTime moment = now ?? DateTime.now();
final double weight = classWeight(commitment.commitmentClass);
// Electives never accrue debt, whatever happens to them.
if (weight == 0) {
return 0;
}
// Cleanly completed and archived items are settled.
if (commitment.status == CommitmentStatus.Completed ||
commitment.status == CommitmentStatus.Archived) {
return 0;
}
if (commitment.dueEnd == null) {
return 0;
}
final int daysSince = moment.difference(commitment.dueEnd!).inDays;
// The window is still open — nothing is owed yet.
if (daysSince < 0) {
return 0;
}
if (commitment.status == CommitmentStatus.Abandoned) {
// Abandonment is the most expensive outcome and resists decay for a
// month, so it cannot be waited out.
final double base = weight * abandonmentMultiplier * severity(daysSince);
if (daysSince <= abandonmentProtectionDays) {
return base;
}
return base * recencyDecay(daysSince - abandonmentProtectionDays);
}
final double accrued =
weight * severity(daysSince) * recencyDecay(daysSince);
if (commitment.status == CommitmentStatus.LateCompleted) {
return accrued * lateCompleteRetention;
}
return accrued;
}
/// Debt contributed by a habit. A single miss costs nothing — only falling
/// below the frequency target in the rolling window does.
static double habitDebt(Habit habit) {
if (!habit.behindTarget) {
return 0;
}
return habit.shortfall * habitShortfallWeight;
}
/// The whole score, as of [now].
static double totalDebt(
List<Commitment> commitments, {
List<Habit> habits = const <Habit>[],
DateTime? now,
}) {
final DateTime moment = now ?? DateTime.now();
double total = 0;
for (Commitment commitment in commitments) {
total = total + commitmentDebt(commitment, now: moment);
}
for (Habit habit in habits) {
total = total + habitDebt(habit);
}
return total;
}
/// Open items past their window — the count that [Thresholds.maxOpenOverdue]
/// caps.
static int openOverdueCount(List<Commitment> commitments) {
return commitments
.where((commitment) =>
commitment.status == CommitmentStatus.Overdue ||
(commitment.status == CommitmentStatus.Open &&
commitment.windowClosed))
.length;
}
/// Missed non-negotiables — three of these force Grounded regardless of the
/// numeric score.
static int missedNonNegotiables(List<Commitment> commitments) {
return commitments
.where((commitment) =>
commitment.commitmentClass == CommitmentClass.NonNegotiable &&
(commitment.status == CommitmentStatus.Overdue ||
commitment.status == CommitmentStatus.Abandoned ||
(commitment.status == CommitmentStatus.Open &&
commitment.windowClosed)))
.length;
}
/// What clearing this item would remove from the score — used to show the
/// user the actual price of each row in the overdue queue.
static double reliefFromClearing(Commitment commitment, {DateTime? now}) {
return commitmentDebt(commitment, now: now);
}
}