/// Frequency-based rather than instance-based: a single miss costs nothing, /// falling below the target in the rolling window is what accrues debt. class Habit { String? id; String title; String category; /// Target completions per rolling window, e.g. 5. int targetPerWindow; /// Rolling window length in days, e.g. 7. int windowDays; /// Completions inside the current window. int completionsInWindow; /// Marked keystone once the data says its completion predicts day quality — /// the app works this out after ~60 days rather than taking your word. bool keystone; /// 0..1 correlation with overall day quality; -1 until there is enough data. double predictiveStrength; Habit({ this.id, this.title = "", this.category = "", this.targetPerWindow = 0, this.windowDays = 7, this.completionsInWindow = 0, this.keystone = false, this.predictiveStrength = -1, }); factory Habit.fromJson(Map json) { return Habit( id: json['id'], title: json['title'] ?? "", category: json['category'] ?? "", targetPerWindow: json['targetPerWindow'] ?? 0, windowDays: json['windowDays'] ?? 7, completionsInWindow: json['completionsInWindow'] ?? 0, keystone: json['keystone'] ?? false, predictiveStrength: (json['predictiveStrength'] ?? -1).toDouble(), ); } Map toJson() { final Map data = {}; data['id'] = id; data['title'] = title; data['category'] = category; data['targetPerWindow'] = targetPerWindow; data['windowDays'] = windowDays; data['completionsInWindow'] = completionsInWindow; data['keystone'] = keystone; data['predictiveStrength'] = predictiveStrength; return data; } /// Behind target for the window — the only condition under which a habit /// accrues debt. bool get behindTarget { return completionsInWindow < targetPerWindow; } int get shortfall { final int gap = targetPerWindow - completionsInWindow; return gap > 0 ? gap : 0; } }