focus-flow/lib/src/reminder_policy.dart

334 lines
9.3 KiB
Dart

// UI/platform-independent reminder policy.
//
// This file decides whether the core would deliver, suppress, defer, or require
// acknowledgement for a reminder at a specific instant. It does not schedule OS
// notifications, run background timers, or move tasks.
import 'models.dart';
import 'occupancy_policy.dart';
/// High-level reminder directive for application/platform layers.
enum ReminderDirectiveAction {
deliver,
suppress,
defer,
requireAcknowledgement,
}
/// Stable reason codes for reminder directives.
enum ReminderDirectiveReason {
deliverNormal,
requireRequiredAcknowledgement,
deferUntilTaskWindow,
deferRequiredDuringProtectedRest,
suppressSilentProfile,
suppressProtectedRest,
suppressNoScheduledWindow,
suppressUnsupportedTaskType,
suppressInactiveStatus,
suppressHiddenLockedTime,
suppressFreeSlotRecord,
}
/// Result of resolving the effective reminder profile for one task.
class EffectiveReminderProfile {
const EffectiveReminderProfile({
required this.profile,
required this.source,
});
/// Resolved reminder profile.
final ReminderProfile profile;
/// Where [profile] came from.
final EffectiveReminderProfileSource source;
}
/// Source for an effective reminder profile.
enum EffectiveReminderProfileSource {
taskOverride,
projectDefault,
fallback,
}
/// UI/platform-independent directive for one task reminder check.
class ReminderDirective {
const ReminderDirective({
required this.taskId,
required this.action,
required this.reason,
required this.effectiveProfile,
required this.effectiveProfileSource,
required this.evaluatedAt,
this.scheduledStart,
this.scheduledEnd,
this.protectedFreeSlotTaskId,
this.nextEligibleAt,
});
/// Task evaluated for reminder behavior.
final String taskId;
/// What an application layer may do.
final ReminderDirectiveAction action;
/// Stable reason code for tests, telemetry, and later UI copy.
final ReminderDirectiveReason reason;
/// Effective reminder profile used for the decision.
final ReminderProfile effectiveProfile;
/// Source of [effectiveProfile].
final EffectiveReminderProfileSource effectiveProfileSource;
/// Instant used to evaluate the directive.
final DateTime evaluatedAt;
/// Task scheduled start, when present.
final DateTime? scheduledStart;
/// Task scheduled end, when present.
final DateTime? scheduledEnd;
/// Active protected Free Slot that affected the decision, when any.
final String? protectedFreeSlotTaskId;
/// Next known time the application may re-check, when deterministic.
final DateTime? nextEligibleAt;
}
/// Resolves reminder profiles and reminder directives.
class ReminderPolicyService {
const ReminderPolicyService({
this.fallbackProfile = ReminderProfile.gentle,
this.occupancyPolicy = const OccupancyPolicy(),
});
/// Fallback used when no task override or project profile is available.
final ReminderProfile fallbackProfile;
/// Occupancy classifier used to detect active protected Free Slots.
final OccupancyPolicy occupancyPolicy;
/// Resolve effective profile without considering learned suggestions.
EffectiveReminderProfile resolveEffectiveProfile({
required Task task,
ProjectProfile? project,
}) {
final override = task.reminderOverride;
if (override != null) {
return EffectiveReminderProfile(
profile: override,
source: EffectiveReminderProfileSource.taskOverride,
);
}
if (project != null) {
return EffectiveReminderProfile(
profile: project.defaultReminderProfile,
source: EffectiveReminderProfileSource.projectDefault,
);
}
return EffectiveReminderProfile(
profile: fallbackProfile,
source: EffectiveReminderProfileSource.fallback,
);
}
/// Resolve one reminder directive at [now].
ReminderDirective directiveForTask({
required Task task,
required DateTime now,
ProjectProfile? project,
Iterable<Task> contextTasks = const <Task>[],
}) {
final effective = resolveEffectiveProfile(task: task, project: project);
final activeFreeSlot = _activeProtectedFreeSlot(
now: now,
contextTasks: contextTasks,
);
return _directiveForTask(
task: task,
now: now,
effective: effective,
activeFreeSlot: activeFreeSlot,
);
}
ReminderDirective _directiveForTask({
required Task task,
required DateTime now,
required EffectiveReminderProfile effective,
required OccupancyEntry? activeFreeSlot,
}) {
final start = task.scheduledStart;
final end = task.scheduledEnd;
ReminderDirective build({
required ReminderDirectiveAction action,
required ReminderDirectiveReason reason,
DateTime? nextEligibleAt,
}) {
return ReminderDirective(
taskId: task.id,
action: action,
reason: reason,
effectiveProfile: effective.profile,
effectiveProfileSource: effective.source,
evaluatedAt: now,
scheduledStart: start,
scheduledEnd: end,
protectedFreeSlotTaskId: activeFreeSlot?.taskId,
nextEligibleAt: nextEligibleAt,
);
}
if (effective.profile == ReminderProfile.silent) {
return build(
action: ReminderDirectiveAction.suppress,
reason: ReminderDirectiveReason.suppressSilentProfile,
);
}
if (task.type == TaskType.locked) {
return build(
action: ReminderDirectiveAction.suppress,
reason: ReminderDirectiveReason.suppressHiddenLockedTime,
);
}
if (task.type == TaskType.freeSlot) {
return build(
action: ReminderDirectiveAction.suppress,
reason: ReminderDirectiveReason.suppressFreeSlotRecord,
);
}
if (!_isReminderEligibleStatus(task.status)) {
return build(
action: ReminderDirectiveAction.suppress,
reason: ReminderDirectiveReason.suppressInactiveStatus,
);
}
if (!_isReminderEligibleType(task.type)) {
return build(
action: ReminderDirectiveAction.suppress,
reason: ReminderDirectiveReason.suppressUnsupportedTaskType,
);
}
if (start == null || end == null || !start.isBefore(end)) {
return build(
action: ReminderDirectiveAction.suppress,
reason: ReminderDirectiveReason.suppressNoScheduledWindow,
);
}
if (now.isBefore(start)) {
return build(
action: ReminderDirectiveAction.defer,
reason: ReminderDirectiveReason.deferUntilTaskWindow,
nextEligibleAt: start,
);
}
final protectedRestActive = activeFreeSlot != null;
if (protectedRestActive && task.isFlexible) {
return build(
action: ReminderDirectiveAction.suppress,
reason: ReminderDirectiveReason.suppressProtectedRest,
nextEligibleAt: activeFreeSlot.interval?.end,
);
}
if (protectedRestActive && task.isRequiredVisible) {
return _requiredDuringProtectedRest(
task: task,
effective: effective,
build: build,
freeSlotEnd: activeFreeSlot.interval?.end,
);
}
if (task.type == TaskType.critical &&
effective.profile == ReminderProfile.strict) {
return build(
action: ReminderDirectiveAction.requireAcknowledgement,
reason: ReminderDirectiveReason.requireRequiredAcknowledgement,
);
}
return build(
action: ReminderDirectiveAction.deliver,
reason: ReminderDirectiveReason.deliverNormal,
);
}
ReminderDirective _requiredDuringProtectedRest({
required Task task,
required EffectiveReminderProfile effective,
required ReminderDirective Function({
required ReminderDirectiveAction action,
required ReminderDirectiveReason reason,
DateTime? nextEligibleAt,
}) build,
required DateTime? freeSlotEnd,
}) {
if (task.type == TaskType.critical &&
effective.profile == ReminderProfile.strict) {
return build(
action: ReminderDirectiveAction.requireAcknowledgement,
reason: ReminderDirectiveReason.requireRequiredAcknowledgement,
);
}
if (task.type == TaskType.inflexible &&
effective.profile == ReminderProfile.gentle) {
return build(
action: ReminderDirectiveAction.defer,
reason: ReminderDirectiveReason.deferRequiredDuringProtectedRest,
nextEligibleAt: freeSlotEnd,
);
}
return build(
action: ReminderDirectiveAction.deliver,
reason: ReminderDirectiveReason.deliverNormal,
);
}
OccupancyEntry? _activeProtectedFreeSlot({
required DateTime now,
required Iterable<Task> contextTasks,
}) {
for (final entry in occupancyPolicy.classify(tasks: contextTasks)) {
final interval = entry.interval;
if (entry.category != OccupancyCategory.protectedFreeSlot ||
interval == null) {
continue;
}
final startsAtOrBeforeNow =
interval.start.isBefore(now) || interval.start.isAtSameMomentAs(now);
final endsAfterNow = interval.end.isAfter(now);
if (startsAtOrBeforeNow && endsAfterNow) {
return entry;
}
}
return null;
}
}
bool _isReminderEligibleStatus(TaskStatus status) {
return status == TaskStatus.planned || status == TaskStatus.active;
}
bool _isReminderEligibleType(TaskType type) {
return type == TaskType.flexible ||
type == TaskType.critical ||
type == TaskType.inflexible;
}