From b4e321da02a9766bd051c3f0dbd9c05a345eb856 Mon Sep 17 00:00:00 2001 From: Ashley Venn Date: Wed, 24 Jun 2026 11:11:15 -0700 Subject: [PATCH] docs(project): commit current repository state --- AGENTS.md | 13 + .../V1_BLOCK_07_Child_Tasks.md | 132 ++++++++ .../V1_BLOCK_08_Today_Timeline_State.md | 133 ++++++++ .../V1_BLOCK_09_Persistence_Preparation.md | 122 +++++++ ..._BLOCK_10_Testing_Documentation_Handoff.md | 127 +++++++ ..._Task_Actions_State_Transitions_UPDATED.md | 312 ++++++++++++++++++ .../V1_BLOCK_07_Child_Tasks.md | 62 +++- .../V1_BLOCK_08_Today_Timeline_State.md | 49 +++ .../V1_BLOCK_09_Persistence_Preparation.md | 95 +++++- ..._BLOCK_10_Testing_Documentation_Handoff.md | 66 +++- DOCUMENTATION_PASS_SUMMARY.md | 27 ++ .../Starter Architecture Notes.md | 15 +- README.md | 5 +- adhd_scheduling_starter_project.7z | Bin 0 -> 74361 bytes lib/scheduler_core.dart | 14 + lib/src/backlog.dart | 107 ++++++ lib/src/locked_time.dart | 163 +++++++++ lib/src/models.dart | 176 +++++++++- lib/src/quick_capture.dart | 75 +++++ lib/src/scheduling_engine.dart | 239 +++++++++++++- lib/src/task_actions.dart | 68 ++++ lib/src/task_statistics.dart | 53 ++- 22 files changed, 2015 insertions(+), 38 deletions(-) create mode 100644 Codex Documentation/Current Software Plan/V1_BLOCKS_07_10_UPDATED/V1_BLOCK_07_Child_Tasks.md create mode 100644 Codex Documentation/Current Software Plan/V1_BLOCKS_07_10_UPDATED/V1_BLOCK_08_Today_Timeline_State.md create mode 100644 Codex Documentation/Current Software Plan/V1_BLOCKS_07_10_UPDATED/V1_BLOCK_09_Persistence_Preparation.md create mode 100644 Codex Documentation/Current Software Plan/V1_BLOCKS_07_10_UPDATED/V1_BLOCK_10_Testing_Documentation_Handoff.md create mode 100644 Codex Documentation/Current Software Plan/V1_BLOCK_06_Task_Actions_State_Transitions_UPDATED.md create mode 100644 DOCUMENTATION_PASS_SUMMARY.md create mode 100644 adhd_scheduling_starter_project.7z diff --git a/AGENTS.md b/AGENTS.md index 58ee171..cd77f0f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -49,6 +49,18 @@ Codex Documentation/Archived plans/ This is firm. New plans may be uploaded, generated, or pointed at from elsewhere, but once Codex completes a plan, it must move the completed plan document into `Codex Documentation/Archived plans/`. Do not leave completed plans in `Current Software Plan/`. + +## Persistence target rules + +MongoDB is the committed persistence target for this project. Treat MongoDB document storage as the future database direction when designing repository interfaces, serialization helpers, and persistence boundaries. + +Rules: + +- Do not introduce alternative database assumptions unless the user explicitly reverses this decision. +- V1 planning may prepare MongoDB-friendly repository interfaces and document-shaped model mappings without adding a MongoDB driver yet. +- Do not add MongoDB connection strings, Atlas/cloud setup, local server requirements, sync behavior, accounts, or network/background behavior unless an active plan explicitly calls for that implementation work. +- Keep the scheduling core independent from MongoDB APIs; persistence adapters should sit behind repository interfaces. + ## Planning document execution rules Planning documents are organized into blocks, chunks, and optional stages. @@ -103,6 +115,7 @@ Do not make vague commits such as `update files`, `changes`, or `work`. - Start with pure Dart domain logic and tests. - Add Flutter UI only when the plan calls for it. - Avoid network/sync/background behavior unless explicitly planned. +- Use MongoDB as the committed future persistence target; do not add alternative database assumptions. - Prefer immutable models or copy/update patterns. - Avoid hidden side effects in scheduling functions. - Every rule that changes task placement should have tests. diff --git a/Codex Documentation/Current Software Plan/V1_BLOCKS_07_10_UPDATED/V1_BLOCK_07_Child_Tasks.md b/Codex Documentation/Current Software Plan/V1_BLOCKS_07_10_UPDATED/V1_BLOCK_07_Child_Tasks.md new file mode 100644 index 0000000..7767658 --- /dev/null +++ b/Codex Documentation/Current Software Plan/V1_BLOCKS_07_10_UPDATED/V1_BLOCK_07_Child_Tasks.md @@ -0,0 +1,132 @@ +# V1 Block 07 — Child Tasks + +Status: Planned + +Purpose: Support breaking a large task into smaller owned child tasks without adding dependency complexity. + +## Chunk 7.1 — Parent/child model rules + +Recommended Codex level: medium + +Tasks: + +Implement parent/child ownership fields and helpers: + +- parent task id on child tasks +- list/query children by parent +- parent can be incomplete while children are planned/completed +- parent/child helpers must remain domain-only and UI-independent + +Rules: + +- This is not full task dependency support. +- Do not add arbitrary DAG/dependency logic. +- Children are owned by parent only. +- Child ownership does not block scheduling unless a later planned feature explicitly adds dependency behavior. + +Acceptance criteria: + +- Test: child references parent. +- Test: parent can query/aggregate children through helper. +- Test: child ownership does not create dependency/blocking behavior. + +## Chunk 7.2 — Child entry defaults + +Recommended Codex level: medium + +Tasks: + +Support row-style child entry data: + +- child title +- priority up/down/dropdown value +- reward up/down/dropdown value +- time required +- optional project override + +Rules: + +- If no priority is set, children are inserted in the order added. +- Children can have their own reward, priority, difficulty, and time. +- Children inherit project from parent unless overridden. +- Reward can remain `not set`; do not treat missing reward as very low reward. +- Preserve original entry order for children that have no explicit priority. + +Acceptance criteria: + +- Tests cover child creation with explicit fields. +- Tests cover no priority preserving insertion order. +- Tests cover inherited project and overridden project. +- Tests cover reward `not set` remaining distinct from very low reward. + +BREAKPOINT: Stop here. Confirm `extra high` mode before implementing the child-task edge-case regression suite. + +## Chunk 7.3 — Child-task edge-case regression test suite + +Recommended Codex level: extra high + +Tasks: + +Before implementing parent auto-completion, add or expand tests for edge cases that could make child tasks behave like unwanted dependency logic. + +Cover these cases: + +- parent with zero children does not auto-complete by accident +- parent with planned children remains incomplete +- child completion updates child state without forcing parent completion until all children are complete +- parent completion can force-complete remaining children only through the explicit parent-complete action +- child tasks keep their parent id when scheduled, pushed, moved to backlog, or marked complete +- children without priority preserve row insertion order +- children with priority can be sorted by priority without losing stable insertion order within same-priority groups +- parent project inheritance works +- child project override works +- child task reward `not set` remains distinct from very low reward +- no arbitrary dependency/DAG fields or scheduling-blocking behavior are introduced + +Rules: + +- Tests should name the business rule being protected. +- Prefer small fixtures over large scenario setup. +- Do not mark this chunk complete unless `dart analyze` and `dart test` pass. +- Do not implement new user-facing behavior in this chunk unless needed to make the edge-case tests compile against planned domain APIs. + +Acceptance criteria: + +- A dedicated child-task test group exists. +- Tests cover ownership, ordering, inheritance, and non-dependency behavior. +- Existing scheduling and backlog tests still pass. + +BREAKPOINT: Stop here. Confirm `high` mode before implementing auto-completion propagation. + +## Chunk 7.4 — Parent auto-completion + +Recommended Codex level: high + +Tasks: + +Implement completion rules: + +- Parent auto-completes when all children complete. +- Marking parent complete force-completes remaining children. +- Completing from any child can provide a domain-level option/result to mark entire parent complete. +- Parent/child completion should update relevant task statistics without duplicating events. + +Rules: + +- Do not silently complete sibling child tasks when one child completes. +- Do not complete parent until every child is complete unless the explicit parent-complete action is selected. +- Do not add generalized dependency resolution. + +Acceptance criteria: + +- Test: all children completed completes parent. +- Test: parent complete force-completes children. +- Test: partial child completion does not complete parent. +- Test: completing one child does not complete siblings. +- Test: explicit parent-complete action records the correct completion state for parent and remaining children. + +Commit suggestion: + +```text +feat(tasks): support parent-owned child tasks +``` diff --git a/Codex Documentation/Current Software Plan/V1_BLOCKS_07_10_UPDATED/V1_BLOCK_08_Today_Timeline_State.md b/Codex Documentation/Current Software Plan/V1_BLOCKS_07_10_UPDATED/V1_BLOCK_08_Today_Timeline_State.md new file mode 100644 index 0000000..8dc9588 --- /dev/null +++ b/Codex Documentation/Current Software Plan/V1_BLOCKS_07_10_UPDATED/V1_BLOCK_08_Today_Timeline_State.md @@ -0,0 +1,133 @@ +# V1 Block 08 — Today Timeline State + +Status: Planned + +Purpose: Prepare domain/view-state data for the Today timeline without building full Flutter UI yet unless explicitly requested. + +## Chunk 8.1 — Timeline item view model + +Recommended Codex level: medium + +Tasks: + +Create a UI-independent timeline item model with fields for: + +- display title +- start/end or duration +- task type +- project color token +- background type token +- reward icon token +- difficulty icon token +- explicit time display flag +- quick actions available +- item category that distinguishes task cards from overlays + +Rules: + +- Thick border color is project class. +- Translucent background is task type. +- Name is text. +- Icon 1 is reward. +- Icon 2 is difficulty. +- Flexible task cards show duration; timeline position shows start/stop. +- Inflexible, critical, and locked show explicit start/end times. +- Timeline state must remain UI-framework-independent. + +Acceptance criteria: + +- Tests or snapshots can verify field mapping from task to timeline item. +- Tests cover flexible duration display. +- Tests cover explicit time display for inflexible, critical, and locked items. + +## Chunk 8.2 — Hidden locked block overlay state + +Recommended Codex level: medium + +Tasks: + +Create overlay state for locked blocks: + +- hidden by default +- temporary reveal mode +- named blocked-time overlay +- not a normal task block +- overlay item category distinct from task-card category + +Acceptance criteria: + +- Locked block can produce an overlay item only when reveal mode is active. +- Overlay item is distinguishable from task card item. +- Overlay item shows a name and explicit start/end times. +- Locked overlay does not expose normal task quick actions. + +BREAKPOINT: Stop here. Confirm `low` mode before implementing compact mode state. + +## Chunk 8.3 — Compact mode state + +Recommended Codex level: low + +Tasks: + +Add domain/app state for manual compact mode: + +- Manual toggle only. +- Show current task. +- Show next required task. +- Optionally show next flexible task. +- Hide full timeline unless expanded. + +Rules: + +- The app must not automatically switch into compact mode after missed/pushed tasks. +- Locked blocks stay hidden in compact mode unless explicitly revealed by a separate overlay action. +- Compact mode should use existing timeline item mapping instead of duplicate display logic where practical. + +Acceptance criteria: + +- Compact mode flag controls compact output selection. +- Tests cover manual toggle behavior. +- Tests cover compact mode not activating automatically after pushed/missed tasks. +- Tests cover compact mode showing the next required item when one exists. + +BREAKPOINT: Stop here. Confirm `medium` mode before implementing the timeline regression test suite. + +## Chunk 8.4 — Timeline state regression test suite + +Recommended Codex level: medium + +Tasks: + +Add or expand tests that protect the timeline mapping rules before Flutter UI work begins. + +Cover these cases: + +- flexible task card exposes duration, not forced explicit start/end text +- inflexible task card exposes explicit start/end text +- critical task card exposes explicit start/end text +- locked block overlay exposes explicit start/end text only when reveal mode is active +- locked block overlay is not treated as a task card +- project color token maps from project class/border token +- background token maps from task type +- reward icon token handles all five reward levels plus `not set` +- difficulty icon token handles all five difficulty levels +- quick actions map correctly for flexible tasks +- compact mode returns only the intended reduced item set + +Rules: + +- Do not implement Flutter widgets in this block unless explicitly requested later. +- Timeline models should be easy for Flutter to consume but must not import Flutter. +- Do not mark this chunk complete unless `dart analyze` and `dart test` pass. + +Acceptance criteria: + +- Timeline test group exists. +- Mapping rules are tested without depending on a UI framework. +- Existing scheduling, backlog, locked-block, and task-action tests still pass. + +Commit suggestion: + +```text +feat(timeline): add today timeline view state models +``` diff --git a/Codex Documentation/Current Software Plan/V1_BLOCKS_07_10_UPDATED/V1_BLOCK_09_Persistence_Preparation.md b/Codex Documentation/Current Software Plan/V1_BLOCKS_07_10_UPDATED/V1_BLOCK_09_Persistence_Preparation.md new file mode 100644 index 0000000..9ac3e22 --- /dev/null +++ b/Codex Documentation/Current Software Plan/V1_BLOCKS_07_10_UPDATED/V1_BLOCK_09_Persistence_Preparation.md @@ -0,0 +1,122 @@ +# V1 Block 09 — Persistence Preparation + +Status: Planned + +Purpose: Prepare the domain layer for local persistence without overbuilding sync or database implementation too early. + +## Chunk 9.1 — Repository interface + +Recommended Codex level: medium + +Tasks: + +Define repository interfaces for: + +- tasks +- projects +- locked blocks +- scheduling operations/state snapshots + +Rules: + +- Do not couple scheduling engine directly to SQLite. +- Use interfaces that a future Drift/SQLite layer can implement. +- Keep current implementation in-memory if needed. +- Repository interfaces should preserve domain rules rather than bypassing services. + +Acceptance criteria: + +- Domain services can depend on interfaces. +- Tests can use fake/in-memory repositories. +- Interfaces do not import Drift, SQLite, Flutter, or platform-specific APIs. + +BREAKPOINT: Stop here. Confirm `extra high` mode before implementing the persistence edge-case test suite. + +## Chunk 9.2 — Persistence edge-case regression test suite + +Recommended Codex level: extra high + +Tasks: + +Before model serialization changes, add or expand tests that define persistence-sensitive behavior clearly. + +Cover these cases: + +- task IDs remain stable across copy/update operations +- project IDs remain stable across copy/update operations +- locked block IDs remain stable across copy/update operations +- DateTime values are stored and compared using the documented convention +- nullable fields can be intentionally preserved +- nullable fields that need clearing have explicit clear behavior or documented mapping behavior +- enum values have stable persistence names or a clearly tested mapping plan +- `RewardLevel.notSet` remains distinct from `RewardLevel.veryLow` +- backlog age behavior has a documented source timestamp or TODO before database work +- in-memory fake repositories can round-trip saved records without losing scheduling fields +- repository operations do not mutate input objects unexpectedly + +Rules: + +- This chunk may add tests and small model helpers needed to make persistence behavior testable. +- Do not add Drift/SQLite implementation yet. +- Do not add sync, networking, cloud accounts, or background services. +- Do not mark this chunk complete unless `dart analyze` and `dart test` pass. + +Acceptance criteria: + +- Persistence-sensitive test group exists. +- Tests capture stable IDs, enum mapping, nullable-field behavior, and DateTime convention. +- Any unresolved persistence detail is documented as a TODO in the plan or architecture notes. + +BREAKPOINT: Stop here. Confirm `high` mode before model serialization changes. + +## Chunk 9.3 — Serialization-safe models + +Recommended Codex level: high + +Tasks: + +Make models persistence-friendly: + +- stable IDs +- enum serialization helpers or clear mapping plan +- DateTime handling convention +- nullable fields documented +- nullable field clear helpers where needed +- migration-safe field names where possible +- map/json-like conversion helpers only if they do not over-couple the domain layer + +Acceptance criteria: + +- Models can round-trip through map/json-like structures or have a clear TODO for Drift mappings. +- Tests cover at least task serialization if implemented. +- Tests cover enum persistence mapping if implemented. +- Tests cover intentional clearing of nullable fields if helper behavior is added. + +BREAKPOINT: Stop here. Confirm `low` mode before documenting the explicit non-sync boundary. + +## Chunk 9.4 — Explicit non-sync boundary + +Recommended Codex level: low + +Tasks: + +Document that V1 persistence is local-first and sync is out of scope. + +Rules: + +- No network sync code. +- No cloud assumptions. +- No background service implementation. +- No mobile notification or background reconciliation implementation in this block. + +Acceptance criteria: + +- README or architecture doc states sync is future work. +- Docs distinguish local persistence preparation from actual sync. +- Wishlist/future notes contain sync as a later feature, not a V1 requirement. + +Commit suggestion: + +```text +feat(data): prepare persistence interfaces for scheduling core +``` diff --git a/Codex Documentation/Current Software Plan/V1_BLOCKS_07_10_UPDATED/V1_BLOCK_10_Testing_Documentation_Handoff.md b/Codex Documentation/Current Software Plan/V1_BLOCKS_07_10_UPDATED/V1_BLOCK_10_Testing_Documentation_Handoff.md new file mode 100644 index 0000000..d25ea12 --- /dev/null +++ b/Codex Documentation/Current Software Plan/V1_BLOCKS_07_10_UPDATED/V1_BLOCK_10_Testing_Documentation_Handoff.md @@ -0,0 +1,127 @@ +# V1 Block 10 — Testing, Documentation, and Handoff + +Status: Planned + +Purpose: Ensure the V1 scheduling core is understandable, tested, and ready for future UI work. + +## Chunk 10.1 — Final scheduling rule coverage audit + +Recommended Codex level: high + +Tasks: + +Audit the full V1 test suite and fill remaining coverage gaps for: + +- flexible insert into free slot +- flexible insert pushing other flexible tasks +- locked blocks never moved +- inflexible tasks never moved +- critical tasks remain visible/required +- push to next available +- push to tomorrow top of queue +- push to backlog +- end-of-day rollover +- required task states +- surprise task overlaps +- child task parent completion +- timeline state mapping +- persistence-sensitive model behavior + +Rules: + +- This is a final coverage audit, not a place to add large new features. +- If a missing behavior is found, implement the smallest domain fix needed and test it. +- Do not duplicate tests already added in earlier block-specific regression chunks unless the duplicate provides clearer final coverage. +- Tests should name the business rule being verified. +- Avoid brittle tests that only check implementation details. + +Acceptance criteria: + +- Test matrix exists as either a Markdown checklist or clearly grouped test names. +- Every V1 implemented behavior has at least one meaningful test. +- Edge-case tests added in Blocks 06–09 are represented in the final coverage audit. +- `dart analyze` and `dart test` pass. + +BREAKPOINT: Stop here. Confirm `medium` mode before final verification and documentation sync. + +## Chunk 10.2 — Full verification pass + +Recommended Codex level: medium + +Tasks: + +Run and record the standard verification commands: + +```bash +dart pub get +dart format lib test +dart analyze +dart test +git diff --check +``` + +Rules: + +- Do not mark this chunk complete unless all commands pass. +- If formatting changes files, include those changes in the same work block. +- If any command cannot run because of the local environment, document the exact reason and do not claim it passed. + +Acceptance criteria: + +- Verification results are recorded in the completed plan note or commit summary. +- No failing analyze/test/format/diff-check issues remain. + +## Chunk 10.3 — Documentation sync + +Recommended Codex level: medium + +Tasks: + +Update docs to reflect implemented behavior: + +- README summary. +- Human Documentation product/design notes if affected. +- Relevant current plan statuses. +- Any architecture notes. +- Any TODOs moved to wishlist/future notes. +- Any known limitations that remain after V1. + +Acceptance criteria: + +- No docs claim V2 features are implemented in V1. +- MVP exclusions remain clear. +- Plan files accurately distinguish complete, active, and future work. +- Breakpoints and recommended Codex levels remain accurate after any renumbering. + +BREAKPOINT: Stop here. Confirm `low` mode before archiving completed plans. + +## Chunk 10.4 — Archive completed plans + +Recommended Codex level: low + +Tasks: + +For every completed block plan: + +- Mark status complete. +- Move completed plan to `Codex Documentation/Archived plans/`. +- Leave active/incomplete plans in Current Software Plan. +- Commit archive moves. + +Rules: + +- Completed plans must not remain in `Codex Documentation/Current Software Plan/`. +- Incomplete or partially complete plans must not be archived. +- All completed work must be committed with a conventional commit message. + +Acceptance criteria: + +- Completed plans are not left in Current Software Plan. +- Archive folder contains completed plans. +- Commit message follows conventional commit style. + +Commit suggestion: + +```text +docs(plan): archive completed v1 planning blocks +``` diff --git a/Codex Documentation/Current Software Plan/V1_BLOCK_06_Task_Actions_State_Transitions_UPDATED.md b/Codex Documentation/Current Software Plan/V1_BLOCK_06_Task_Actions_State_Transitions_UPDATED.md new file mode 100644 index 0000000..6f757f1 --- /dev/null +++ b/Codex Documentation/Current Software Plan/V1_BLOCK_06_Task_Actions_State_Transitions_UPDATED.md @@ -0,0 +1,312 @@ +# V1 Block 06 — Task Actions and State Transitions + +Status: Planned + +Purpose: Define and implement the safe, low-friction actions available from task cards. + +This block must keep task actions predictable, testable, and aligned with the MVP product rules. Do not add UI, sync, persistence, reports, child-task implementation, or timeline rendering here unless a chunk explicitly asks for it. + +## Chunk 6.1 — Flexible task quick actions + +Recommended Codex level: medium + +Status: Completed + +Tasks: + +Support these quick actions for flexible task cards: + +- Done +- Push +- Backlog +- Break up + +Rules: + +- Done marks completed. +- Push opens simple push destinations. +- Backlog moves to unified backlog and does not preserve original schedule order. +- Break up starts child task flow; full implementation in Block 07. + +Acceptance criteria: + +- Tests cover done and backlog state transitions. +- Push destination selection is represented in domain layer. + +Execution notes: + +- Added `FlexibleTaskActionService` for flexible card quick actions. +- Added `FlexibleTaskQuickAction` and `PushDestination` domain enums. +- Done marks a flexible task completed without changing its schedule placement. +- Backlog uses the scheduling engine backlog transition and clears schedule placement. +- Push returns explicit domain destinations: next available slot, tomorrow/top of queue, and backlog. +- Break up returns an explicit child-task-flow intent without creating children yet. +- Added tests for done, backlog, push destination representation, and break-up flow intent. +- `dart format lib test`, `dart analyze`, `dart test`, and `git diff --check` passed. + +## Chunk 6.2 — Push destination behavior + +Recommended Codex level: high + +Status: Completed + +Tasks: + +Implement push destinations: + +- Next available slot. +- Tomorrow/top of queue. +- Backlog. + +Explicitly exclude: + +- Later today. + +Acceptance criteria: + +- Test: next available uses scheduling engine. +- Test: tomorrow sets tomorrow/top-of-queue metadata. +- Test: backlog clears schedule placement. + +Execution notes: + +- Added `PushDestinationResult` to carry selected destination metadata with the scheduling result. +- Added `FlexibleTaskActionService.applyPushDestination`. +- Next available delegates to `SchedulingEngine.pushFlexibleTaskToNextAvailableSlot`. +- Tomorrow/top-of-queue delegates to `SchedulingEngine.pushFlexibleTaskToTomorrowTopOfQueue` and reports `placesAtTomorrowTopOfQueue`. +- Backlog destination updates the task list with `SchedulingEngine.moveToBacklog`, clears placement, and records a scheduling change. +- `PushDestination` remains limited to next available slot, tomorrow/top of queue, and backlog; later today is not represented. +- Added tests for next available, tomorrow/top-of-queue metadata, backlog clearing schedule placement, and the excluded later-today destination. +- `dart format lib test`, `dart analyze`, `dart test`, and `git diff --check` passed. + +BREAKPOINT: Stop here. Confirm `extra high` mode before implementing Chunk 6.3, because the next chunk expands the regression and edge-case test suite and may require small correctness fixes. + +## Chunk 6.3 — Edge-case regression test suite + +Recommended Codex level: extra high + +Status: Planned + +Purpose: + +Before adding more state-transition behavior, harden the existing implementation with a dedicated regression test suite. The goal is to prove that Blocks 02 through 06.2 are reliable before layering required-task states, surprise-task logging, child tasks, timeline state, or persistence on top of them. + +Scope: + +- Add tests first where possible. +- Make the smallest code changes needed to satisfy tests when tests expose real behavior gaps. +- Do not implement Required Task States from Chunk 6.4. +- Do not implement Surprise Task Logging from Chunk 6.5. +- Do not add Flutter UI. +- Do not add persistence/database code. +- Do not add sync/network behavior. + +Required test organization: + +- Keep existing tests passing. +- Split tests into clearer files if useful, for example: + - `test/models_test.dart` + - `test/scheduling_engine_test.dart` + - `test/backlog_test.dart` + - `test/quick_capture_test.dart` + - `test/locked_time_test.dart` + - `test/task_actions_test.dart` +- If tests remain in one file temporarily, group them with clear `group(...)` blocks. + +Required edge-case coverage: + +1. Domain model and copy behavior + + Add or confirm tests for: + + - `Task.quickCapture` trims titles. + - `Task.quickCapture` rejects empty/whitespace-only titles. + - `Task.copyWith(clearSchedule: true)` clears both `scheduledStart` and `scheduledEnd`. + - `Task.copyWith` preserves existing values when fields are not supplied. + - Any new nullable-field clear behavior, if added, is explicitly tested. + - `ProjectProfile.createTask` rejects empty/whitespace-only task titles, matching quick-capture behavior. + +2. Flexible scheduling and push behavior + + Add or confirm tests for: + + - Next-available insertion uses the earliest flexible slot where the task fits. + - Inserting a backlog task pushes later flexible tasks forward in order. + - Flexible task order is preserved after pushing. + - Locked intervals are never moved. + - Inflexible intervals are never moved. + - Critical intervals are never moved. + - If no available slot exists, the result leaves the task unscheduled/backlogged rather than corrupting the plan. + - `PushDestination.nextAvailableSlot` delegates to the scheduling engine. + - `PushDestination.tomorrowTopOfQueue` places the task at the top of tomorrow’s flexible queue. + - `PushDestination.backlog` clears schedule placement and does not preserve original active-plan order. + +3. End-of-day rollover safety + + Add tests that specifically prevent accidental rollover corruption: + + - Only unfinished flexible tasks from the intended source day roll over. + - Completed flexible tasks do not roll over. + - Cancelled tasks do not roll over. + - Backlogged tasks do not roll over. + - Critical tasks do not roll over through the flexible rollover path. + - Inflexible tasks do not roll over through the flexible rollover path. + - Future flexible tasks outside the source-day window are not pulled into tomorrow accidentally. + - Rolled-over tasks appear at the top of tomorrow’s flexible queue and preserve relative order among themselves. + + If the current rollover API lacks enough source-day context, adjust the API minimally so the source-day boundary is explicit and covered by tests. + +4. Backlog behavior and staleness + + Add or confirm tests for: + + - Unified backlog filtering by status/category works as expected. + - Pushed, critical-missed, wishlist, inbox, stale, and no-reward-set filters do not overlap incorrectly. + - Sorting by priority is stable and deterministic. + - Sorting by reward-vs-effort treats `RewardLevel.notSet` as its own neutral/unknown category, not as equivalent to very low reward. + - Sorting by age is deterministic when multiple tasks have the same age marker. + - Staleness marker thresholds match the configured/default rules: + - younger than/equal to 7 days = fresh/green + - older than 7 days through 30 days = aging/blue + - older than 30 days = stale/purple + - Stale filtering and staleness marker behavior use consistent timestamp semantics. + + If the current model cannot distinguish task creation age from backlog age, do not add database persistence yet. Either document the limitation in code comments/tests or add a minimal in-memory/domain field only if necessary and safe for MVP. + +5. Quick capture behavior + + Add or confirm tests for: + + - Quick capture defaults to backlog/inbox/unsorted behavior. + - Priority defaults to medium. + - Reward defaults to not set. + - Duration can remain unset when the task is not immediately scheduled. + - Checking “add to next available slot” requires or collects enough scheduling data to place the task. + - Quick capture with immediate scheduling uses normal flexible insertion/push rules. + - Quick capture cannot create blank tasks. + +6. Locked block and overlay-domain behavior + + Add or confirm tests for: + + - Recurring locked blocks expand into concrete blocked intervals for the requested day. + - One-day remove override removes only that day’s instance. + - One-day replace override changes only that day’s interval. + - One-day add override adds surprise locked time without changing the recurring rule. + - Locked blocks are hidden by default in normal task-style output/domain views. + - Overlay/reveal data can expose the locked block name as blocked time, not as a normal task. + - Completed-during-locked-hours counters increment correctly. + - Tasks completed outside locked hours do not increment locked-hour counters. + +7. Flexible action service behavior + + Add or confirm tests for: + + - `Done` marks flexible task completed. + - `Backlog` moves the task to backlog and clears schedule. + - `Push` returns only supported destinations. + - `Break up` returns an intent only and does not create child tasks in Block 06. + - Unsupported action/type combinations fail safely and predictably. + +8. Regression and safety checks + + Add tests or assertions for: + + - No duplicate task IDs are created by task-action helpers unless explicitly intended. + - Scheduling results include enough change metadata for later UI/reporting. + - Required/inflexible/locked blocks are never silently converted into flexible tasks. + - All public domain services avoid mutating input lists in place unless explicitly documented. + +Acceptance criteria: + +- Existing test count increases meaningfully beyond the current baseline. +- The new tests cover edge cases from Blocks 02 through 06.2. +- Any code changes are minimal and directly tied to failing edge-case tests. +- No Required Task States implementation is added in this chunk. +- No Surprise Task Logging implementation is added in this chunk. +- `dart format lib test` passes. +- `dart analyze` passes. +- `dart test` passes. +- `git diff --check` passes. +- Commit with a conventional commit message. + +Commit suggestion: + +```text +test(tasks): add edge-case regression coverage before state transitions +``` + +BREAKPOINT: Stop here. Confirm `medium` mode before implementing Chunk 6.4 required task state transitions. + +## Chunk 6.4 — Required task states + +Recommended Codex level: medium + +Status: Planned + +Tasks: + +Implement required task state meanings: + +- Done: completed. +- Missed: did not happen; visible as missed but not aggressively styled. +- Cancel: did not/will not happen; remove from active plan. +- No longer relevant: separate from cancelled. + +Rules: + +- Critical missed tasks move to backlog. +- Required/inflexible missed tasks remain in place/history and are marked missed. +- Cancelled and noLongerRelevant are distinct statuses. +- Locked blocks are not normal tasks and should not be handled by this state-transition service as task cards. + +Acceptance criteria: + +- Tests cover critical missed to backlog. +- Tests cover inflexible missed stays in schedule/history. +- Tests cover cancelled vs noLongerRelevant distinction. +- Tests cover done state for required/inflexible task cards. +- Tests cover safe handling of unsupported task types. +- `dart format lib test`, `dart analyze`, `dart test`, and `git diff --check` pass. + +Commit suggestion: + +```text +feat(tasks): implement required task state transitions +``` + +BREAKPOINT: Stop here. Confirm `high` mode before implementing Chunk 6.5 surprise task logging. + +## Chunk 6.5 — Surprise task logging + +Recommended Codex level: high + +Status: Planned + +Tasks: + +Implement `I did something unplanned` behavior: + +- Create surprise completed task. +- Optional fields: time used, project, reward, priority. +- Surprise task occupies the time it happened. +- Flexible tasks in that time are pushed using normal rules. +- Inflexible/critical/locked blocks are not moved. +- Inflexible/critical overlaps are reported. +- Locked overlaps remain hidden by default unless reveal mode exists. + +Acceptance criteria: + +- Test: surprise task creates completed task. +- Test: overlapping flexible task is pushed. +- Test: overlapping inflexible/critical task is reported, not moved. +- Test: locked overlap can be tracked without rendering as task. +- Test: optional fields can be omitted without blocking save. +- Test: normal push rules are reused rather than duplicated. +- `dart format lib test`, `dart analyze`, `dart test`, and `git diff --check` pass. + +Commit suggestion: + +```text +feat(tasks): implement surprise task logging +``` diff --git a/Codex Documentation/Current Software Plan/V1_BLOCK_07_Child_Tasks.md b/Codex Documentation/Current Software Plan/V1_BLOCK_07_Child_Tasks.md index 2663f7d..7767658 100644 --- a/Codex Documentation/Current Software Plan/V1_BLOCK_07_Child_Tasks.md +++ b/Codex Documentation/Current Software Plan/V1_BLOCK_07_Child_Tasks.md @@ -15,17 +15,20 @@ Implement parent/child ownership fields and helpers: - parent task id on child tasks - list/query children by parent - parent can be incomplete while children are planned/completed +- parent/child helpers must remain domain-only and UI-independent Rules: - This is not full task dependency support. - Do not add arbitrary DAG/dependency logic. - Children are owned by parent only. +- Child ownership does not block scheduling unless a later planned feature explicitly adds dependency behavior. Acceptance criteria: - Test: child references parent. - Test: parent can query/aggregate children through helper. +- Test: child ownership does not create dependency/blocking behavior. ## Chunk 7.2 — Child entry defaults @@ -39,19 +42,63 @@ Support row-style child entry data: - priority up/down/dropdown value - reward up/down/dropdown value - time required +- optional project override Rules: - If no priority is set, children are inserted in the order added. -- Children can have their own reward, priority, and time. +- Children can have their own reward, priority, difficulty, and time. - Children inherit project from parent unless overridden. +- Reward can remain `not set`; do not treat missing reward as very low reward. +- Preserve original entry order for children that have no explicit priority. Acceptance criteria: - Tests cover child creation with explicit fields. - Tests cover no priority preserving insertion order. +- Tests cover inherited project and overridden project. +- Tests cover reward `not set` remaining distinct from very low reward. -## Chunk 7.3 — Parent auto-completion +BREAKPOINT: Stop here. Confirm `extra high` mode before implementing the child-task edge-case regression suite. + +## Chunk 7.3 — Child-task edge-case regression test suite + +Recommended Codex level: extra high + +Tasks: + +Before implementing parent auto-completion, add or expand tests for edge cases that could make child tasks behave like unwanted dependency logic. + +Cover these cases: + +- parent with zero children does not auto-complete by accident +- parent with planned children remains incomplete +- child completion updates child state without forcing parent completion until all children are complete +- parent completion can force-complete remaining children only through the explicit parent-complete action +- child tasks keep their parent id when scheduled, pushed, moved to backlog, or marked complete +- children without priority preserve row insertion order +- children with priority can be sorted by priority without losing stable insertion order within same-priority groups +- parent project inheritance works +- child project override works +- child task reward `not set` remains distinct from very low reward +- no arbitrary dependency/DAG fields or scheduling-blocking behavior are introduced + +Rules: + +- Tests should name the business rule being protected. +- Prefer small fixtures over large scenario setup. +- Do not mark this chunk complete unless `dart analyze` and `dart test` pass. +- Do not implement new user-facing behavior in this chunk unless needed to make the edge-case tests compile against planned domain APIs. + +Acceptance criteria: + +- A dedicated child-task test group exists. +- Tests cover ownership, ordering, inheritance, and non-dependency behavior. +- Existing scheduling and backlog tests still pass. + +BREAKPOINT: Stop here. Confirm `high` mode before implementing auto-completion propagation. + +## Chunk 7.4 — Parent auto-completion Recommended Codex level: high @@ -62,14 +109,21 @@ Implement completion rules: - Parent auto-completes when all children complete. - Marking parent complete force-completes remaining children. - Completing from any child can provide a domain-level option/result to mark entire parent complete. +- Parent/child completion should update relevant task statistics without duplicating events. + +Rules: + +- Do not silently complete sibling child tasks when one child completes. +- Do not complete parent until every child is complete unless the explicit parent-complete action is selected. +- Do not add generalized dependency resolution. Acceptance criteria: - Test: all children completed completes parent. - Test: parent complete force-completes children. - Test: partial child completion does not complete parent. - -BREAKPOINT: Stop here. Confirm `high` mode before implementing auto-completion propagation. +- Test: completing one child does not complete siblings. +- Test: explicit parent-complete action records the correct completion state for parent and remaining children. Commit suggestion: diff --git a/Codex Documentation/Current Software Plan/V1_BLOCK_08_Today_Timeline_State.md b/Codex Documentation/Current Software Plan/V1_BLOCK_08_Today_Timeline_State.md index a2e92c2..8dc9588 100644 --- a/Codex Documentation/Current Software Plan/V1_BLOCK_08_Today_Timeline_State.md +++ b/Codex Documentation/Current Software Plan/V1_BLOCK_08_Today_Timeline_State.md @@ -21,6 +21,7 @@ Create a UI-independent timeline item model with fields for: - difficulty icon token - explicit time display flag - quick actions available +- item category that distinguishes task cards from overlays Rules: @@ -31,10 +32,13 @@ Rules: - Icon 2 is difficulty. - Flexible task cards show duration; timeline position shows start/stop. - Inflexible, critical, and locked show explicit start/end times. +- Timeline state must remain UI-framework-independent. Acceptance criteria: - Tests or snapshots can verify field mapping from task to timeline item. +- Tests cover flexible duration display. +- Tests cover explicit time display for inflexible, critical, and locked items. ## Chunk 8.2 — Hidden locked block overlay state @@ -48,11 +52,16 @@ Create overlay state for locked blocks: - temporary reveal mode - named blocked-time overlay - not a normal task block +- overlay item category distinct from task-card category Acceptance criteria: - Locked block can produce an overlay item only when reveal mode is active. - Overlay item is distinguishable from task card item. +- Overlay item shows a name and explicit start/end times. +- Locked overlay does not expose normal task quick actions. + +BREAKPOINT: Stop here. Confirm `low` mode before implementing compact mode state. ## Chunk 8.3 — Compact mode state @@ -71,11 +80,51 @@ Add domain/app state for manual compact mode: Rules: - The app must not automatically switch into compact mode after missed/pushed tasks. +- Locked blocks stay hidden in compact mode unless explicitly revealed by a separate overlay action. +- Compact mode should use existing timeline item mapping instead of duplicate display logic where practical. Acceptance criteria: - Compact mode flag controls compact output selection. - Tests cover manual toggle behavior. +- Tests cover compact mode not activating automatically after pushed/missed tasks. +- Tests cover compact mode showing the next required item when one exists. + +BREAKPOINT: Stop here. Confirm `medium` mode before implementing the timeline regression test suite. + +## Chunk 8.4 — Timeline state regression test suite + +Recommended Codex level: medium + +Tasks: + +Add or expand tests that protect the timeline mapping rules before Flutter UI work begins. + +Cover these cases: + +- flexible task card exposes duration, not forced explicit start/end text +- inflexible task card exposes explicit start/end text +- critical task card exposes explicit start/end text +- locked block overlay exposes explicit start/end text only when reveal mode is active +- locked block overlay is not treated as a task card +- project color token maps from project class/border token +- background token maps from task type +- reward icon token handles all five reward levels plus `not set` +- difficulty icon token handles all five difficulty levels +- quick actions map correctly for flexible tasks +- compact mode returns only the intended reduced item set + +Rules: + +- Do not implement Flutter widgets in this block unless explicitly requested later. +- Timeline models should be easy for Flutter to consume but must not import Flutter. +- Do not mark this chunk complete unless `dart analyze` and `dart test` pass. + +Acceptance criteria: + +- Timeline test group exists. +- Mapping rules are tested without depending on a UI framework. +- Existing scheduling, backlog, locked-block, and task-action tests still pass. Commit suggestion: diff --git a/Codex Documentation/Current Software Plan/V1_BLOCK_09_Persistence_Preparation.md b/Codex Documentation/Current Software Plan/V1_BLOCK_09_Persistence_Preparation.md index e15a61d..8ae4c82 100644 --- a/Codex Documentation/Current Software Plan/V1_BLOCK_09_Persistence_Preparation.md +++ b/Codex Documentation/Current Software Plan/V1_BLOCK_09_Persistence_Preparation.md @@ -2,7 +2,9 @@ Status: Planned -Purpose: Prepare the domain layer for local persistence without overbuilding sync or database implementation too early. +Purpose: Prepare the domain layer for future MongoDB-backed persistence without adding a MongoDB driver, network behavior, sync behavior, or database runtime dependency too early. + +MongoDB is the committed persistence target. This block should make the core persistence-friendly and MongoDB-document-friendly while keeping the scheduling engine independent from database APIs. ## Chunk 9.1 — Repository interface @@ -19,56 +21,123 @@ Define repository interfaces for: Rules: -- Do not couple scheduling engine directly to SQLite. -- Use interfaces that a future Drift/SQLite layer can implement. -- Keep current implementation in-memory if needed. +- Do not couple the scheduling engine directly to MongoDB APIs. +- Use interfaces that a future MongoDB adapter can implement. +- Keep the current implementation in-memory if needed. +- Repository interfaces should preserve domain rules rather than bypassing services. +- Do not introduce alternative database assumptions. Acceptance criteria: - Domain services can depend on interfaces. - Tests can use fake/in-memory repositories. +- Interfaces do not import MongoDB, Flutter, platform-specific APIs, or network/client APIs. +- Interface names and method shapes are compatible with document-style persistence. -## Chunk 9.2 — Serialization-safe models +BREAKPOINT: Stop here. Confirm `extra high` mode before implementing the persistence edge-case test suite. + +## Chunk 9.2 — Persistence edge-case regression test suite + +Recommended Codex level: extra high + +Tasks: + +Before model serialization changes, add or expand tests that define persistence-sensitive behavior clearly. + +Cover these cases: + +- task IDs remain stable across copy/update operations +- project IDs remain stable across copy/update operations +- locked block IDs remain stable across copy/update operations +- DateTime values are stored and compared using the documented convention +- nullable fields can be intentionally preserved +- nullable fields that need clearing have explicit clear behavior or documented mapping behavior +- enum values have stable persistence names or a clearly tested mapping plan +- `RewardLevel.notSet` remains distinct from `RewardLevel.veryLow` +- backlog age behavior has a documented source timestamp or TODO before database work +- in-memory fake repositories can round-trip saved records without losing scheduling fields +- repository operations do not mutate input objects unexpectedly +- document-shaped maps preserve all fields needed by future MongoDB persistence +- generated document field names are stable and migration-friendly + +Rules: + +- This chunk may add tests and small model helpers needed to make persistence behavior testable. +- Do not add a MongoDB driver, MongoDB client, Atlas setup, connection string, local MongoDB service requirement, or database adapter yet. +- Do not add alternative database mappings or SQL-specific terminology. +- Do not add sync, networking, cloud accounts, or background services. +- Do not mark this chunk complete unless `dart analyze` and `dart test` pass. + +Acceptance criteria: + +- Persistence-sensitive test group exists. +- Tests capture stable IDs, enum mapping, nullable-field behavior, DateTime convention, and document-shaped mapping expectations. +- Any unresolved persistence detail is documented as a TODO in the plan or architecture notes. +- No alternative-database references remain in the active persistence plan. + +BREAKPOINT: Stop here. Confirm `high` mode before model serialization changes. + +## Chunk 9.3 — MongoDB-document-safe models Recommended Codex level: high Tasks: -Make models persistence-friendly: +Make models persistence-friendly and MongoDB-document-friendly: - stable IDs - enum serialization helpers or clear mapping plan - DateTime handling convention - nullable fields documented -- migration-safe field names where possible +- nullable field clear helpers where needed +- migration-safe document field names where possible +- document-shaped map conversion helpers only if they do not over-couple the domain layer +- clear mapping rules for nested task statistics and future child-task ownership fields + +Rules: + +- Keep model helpers independent from MongoDB client libraries. +- Prefer plain Dart map/document shapes that a later adapter can translate into MongoDB documents. +- Do not introduce database connection logic in this chunk. +- Do not add SQL-style table assumptions, joins, or relational schema requirements. Acceptance criteria: -- Models can round-trip through map/json-like structures or have a clear TODO for Drift mappings. +- Models can round-trip through document-shaped map/json-like structures or have a clear TODO for MongoDB document mappings. - Tests cover at least task serialization if implemented. +- Tests cover enum persistence mapping if implemented. +- Tests cover intentional clearing of nullable fields if helper behavior is added. +- Tests confirm `not set` reward remains distinct from very low reward through mapping. -BREAKPOINT: Stop here. Confirm `high` mode before model serialization changes. +BREAKPOINT: Stop here. Confirm `low` mode before documenting the explicit non-sync boundary. -## Chunk 9.3 — Explicit non-sync boundary +## Chunk 9.4 — Explicit non-sync / no-database-runtime boundary Recommended Codex level: low Tasks: -Document that V1 persistence is local-first and sync is out of scope. +Document that V1 persistence work prepares for MongoDB but does not implement runtime database access, sync, accounts, or background services. Rules: - No network sync code. - No cloud assumptions. +- No MongoDB connection strings. +- No Atlas setup. +- No local MongoDB server requirement. - No background service implementation. +- No mobile notification or background reconciliation implementation in this block. Acceptance criteria: -- README or architecture doc states sync is future work. +- README or architecture doc states MongoDB is the committed persistence target. +- README or architecture doc states this block does not require a running MongoDB instance. +- Docs distinguish MongoDB persistence preparation from actual sync or database adapter implementation. +- Wishlist/future notes contain sync as a later feature, not a V1 requirement. Commit suggestion: ```text -feat(data): prepare persistence interfaces for scheduling core +feat(data): prepare MongoDB-friendly persistence interfaces ``` diff --git a/Codex Documentation/Current Software Plan/V1_BLOCK_10_Testing_Documentation_Handoff.md b/Codex Documentation/Current Software Plan/V1_BLOCK_10_Testing_Documentation_Handoff.md index 017a150..d25ea12 100644 --- a/Codex Documentation/Current Software Plan/V1_BLOCK_10_Testing_Documentation_Handoff.md +++ b/Codex Documentation/Current Software Plan/V1_BLOCK_10_Testing_Documentation_Handoff.md @@ -4,13 +4,13 @@ Status: Planned Purpose: Ensure the V1 scheduling core is understandable, tested, and ready for future UI work. -## Chunk 10.1 — Scheduling rule test matrix +## Chunk 10.1 — Final scheduling rule coverage audit Recommended Codex level: high Tasks: -Create a test matrix covering: +Audit the full V1 test suite and fill remaining coverage gaps for: - flexible insert into free slot - flexible insert pushing other flexible tasks @@ -21,15 +21,57 @@ Create a test matrix covering: - push to tomorrow top of queue - push to backlog - end-of-day rollover +- required task states - surprise task overlaps - child task parent completion +- timeline state mapping +- persistence-sensitive model behavior + +Rules: + +- This is a final coverage audit, not a place to add large new features. +- If a missing behavior is found, implement the smallest domain fix needed and test it. +- Do not duplicate tests already added in earlier block-specific regression chunks unless the duplicate provides clearer final coverage. +- Tests should name the business rule being verified. +- Avoid brittle tests that only check implementation details. Acceptance criteria: -- Tests clearly name the business rule being verified. -- Avoid brittle tests that only check implementation details. +- Test matrix exists as either a Markdown checklist or clearly grouped test names. +- Every V1 implemented behavior has at least one meaningful test. +- Edge-case tests added in Blocks 06–09 are represented in the final coverage audit. +- `dart analyze` and `dart test` pass. -## Chunk 10.2 — Documentation sync +BREAKPOINT: Stop here. Confirm `medium` mode before final verification and documentation sync. + +## Chunk 10.2 — Full verification pass + +Recommended Codex level: medium + +Tasks: + +Run and record the standard verification commands: + +```bash +dart pub get +dart format lib test +dart analyze +dart test +git diff --check +``` + +Rules: + +- Do not mark this chunk complete unless all commands pass. +- If formatting changes files, include those changes in the same work block. +- If any command cannot run because of the local environment, document the exact reason and do not claim it passed. + +Acceptance criteria: + +- Verification results are recorded in the completed plan note or commit summary. +- No failing analyze/test/format/diff-check issues remain. + +## Chunk 10.3 — Documentation sync Recommended Codex level: medium @@ -38,18 +80,22 @@ Tasks: Update docs to reflect implemented behavior: - README summary. +- Human Documentation product/design notes if affected. - Relevant current plan statuses. - Any architecture notes. - Any TODOs moved to wishlist/future notes. +- Any known limitations that remain after V1. Acceptance criteria: - No docs claim V2 features are implemented in V1. - MVP exclusions remain clear. +- Plan files accurately distinguish complete, active, and future work. +- Breakpoints and recommended Codex levels remain accurate after any renumbering. -BREAKPOINT: Stop here. Confirm mode switch if needed before final cleanup. +BREAKPOINT: Stop here. Confirm `low` mode before archiving completed plans. -## Chunk 10.3 — Archive completed plans +## Chunk 10.4 — Archive completed plans Recommended Codex level: low @@ -62,6 +108,12 @@ For every completed block plan: - Leave active/incomplete plans in Current Software Plan. - Commit archive moves. +Rules: + +- Completed plans must not remain in `Codex Documentation/Current Software Plan/`. +- Incomplete or partially complete plans must not be archived. +- All completed work must be committed with a conventional commit message. + Acceptance criteria: - Completed plans are not left in Current Software Plan. diff --git a/DOCUMENTATION_PASS_SUMMARY.md b/DOCUMENTATION_PASS_SUMMARY.md new file mode 100644 index 0000000..eed2089 --- /dev/null +++ b/DOCUMENTATION_PASS_SUMMARY.md @@ -0,0 +1,27 @@ +# Documentation Pass Summary + +This archive is the same starter Dart scheduling core with expanded inline documentation added to the code files under `lib/`. + +## What changed + +- Added file-level reading guides to each Dart file. +- Expanded class, enum, field, getter, method, and helper documentation. +- Added inline comments in the scheduling engine to explain validation, placement planning, and application flow. +- Documented how core concepts are meant to be used by UI, persistence, tests, and future planner features. + +## Files documented + +- `lib/scheduler_core.dart` +- `lib/src/models.dart` +- `lib/src/backlog.dart` +- `lib/src/locked_time.dart` +- `lib/src/quick_capture.dart` +- `lib/src/scheduling_engine.dart` +- `lib/src/task_actions.dart` +- `lib/src/task_statistics.dart` + +## Behavior check + +Only comments and documentation text were added to the Dart files. A comparison that strips comments and blank lines reports the non-comment code as unchanged from the uploaded archive. + +`dart format` was not run because the Dart SDK is not installed in this execution environment. The comments were inserted to preserve the existing formatting style as closely as possible. diff --git a/Human Documentation/Starter Architecture Notes.md b/Human Documentation/Starter Architecture Notes.md index ff1df1b..208ad31 100644 --- a/Human Documentation/Starter Architecture Notes.md +++ b/Human Documentation/Starter Architecture Notes.md @@ -15,9 +15,9 @@ Pure Dart scheduling core ↓ Repository interfaces ↓ -Local persistence, likely SQLite/Drift later +MongoDB persistence adapter (planned later) ↓ -Future sync layer +Future sync layer, if explicitly planned ``` ## Why pure Dart first @@ -30,3 +30,14 @@ Future sync layer ## Key invariant The scheduling core must never move locked or inflexible blocks during automatic rescheduling. + + +## Persistence direction + +MongoDB is the committed persistence target. The V1 scheduling core should still +remain persistence-independent and testable without a running database. Repository +interfaces should be designed so a later MongoDB adapter can persist document-shaped +models without importing MongoDB APIs into scheduling logic. + +Do not add alternative database assumptions to this project unless +the product owner explicitly changes the persistence decision. diff --git a/README.md b/README.md index 8a0401f..71fea25 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,7 @@ The repository intentionally starts with a **pure Dart scheduling core** before - V1/MVP: Today view, backlog/wishlist, quick capture, flexible task pushing, recurring hidden locked blocks, task-state transitions, and a testable scheduling core. - V2.0: Week/month views, reports, overwhelm shield, drag-and-drop, task history panels. +- Persistence direction: MongoDB is the committed database target, but the current core remains database-independent until the active plan adds a MongoDB adapter. - Wishlist/future: Dependencies, context tags, advanced sync, long-running task behavior decisions. ## Repository layout @@ -38,7 +39,9 @@ The repository intentionally starts with a **pure Dart scheduling core** before ## Basic commands Install a Dart SDK that satisfies `pubspec.yaml` first. This starter is a pure -Dart package, so Flutter is not required for the current core/test loop. +Dart package, so Flutter is not required for the current core/test loop. MongoDB +is the planned persistence target, but no database service is required for the +current in-memory domain/test loop. ```bash dart pub get diff --git a/adhd_scheduling_starter_project.7z b/adhd_scheduling_starter_project.7z new file mode 100644 index 0000000000000000000000000000000000000000..5d4c631f66e220cd2143ca7a32ba92920a74586a GIT binary patch literal 74361 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 literal 0 HcmV?d00001 diff --git a/lib/scheduler_core.dart b/lib/scheduler_core.dart index d63fb0a..1f423b0 100644 --- a/lib/scheduler_core.dart +++ b/lib/scheduler_core.dart @@ -1,3 +1,17 @@ +// Public library entry point for the ADHD scheduling core. +// +// Import this file from app/UI code instead of importing individual files from +// `lib/src/`. That keeps the public API explicit while allowing the internal +// file layout to change later. At the moment this library exposes pure domain +// objects and scheduling helpers only; it does not perform persistence, UI work, +// notifications, or network calls. +// +// Typical usage: +// 1. Build or load `Task`, `ProjectProfile`, and locked-time objects. +// 2. Create a `SchedulingInput` for the day being planned. +// 3. Call `SchedulingEngine` or one of the action services. +// 4. Store the returned immutable task list and surface notices/changes in UI. + // Public exports for the ADHD scheduling core. // // Keep this file small. Implementation details belong in `lib/src/`. diff --git a/lib/src/backlog.dart b/lib/src/backlog.dart index 0a3cef5..101e58e 100644 --- a/lib/src/backlog.dart +++ b/lib/src/backlog.dart @@ -1,41 +1,100 @@ + // Backlog query helpers for the ADHD scheduling core. +// +// The backlog is not a separate database table in this starter model. It is a +// view over tasks whose `Task.status` is `TaskStatus.backlog`. This file keeps +// display-oriented backlog filtering, sorting, and age markers out of the main +// scheduler so the engine can focus on moving tasks through time. + import 'models.dart'; /// Derived backlog filters for a unified backlog list. +/// +/// These filters do not store separate task collections. They are projections +/// over the same master task list. That is important because a task can move +/// between today's timeline and backlog by changing [Task.status], without +/// needing to copy it between separate stores. enum BacklogFilter { + /// Uncategorized captured tasks in the default inbox project. inbox, + + /// Tasks that have been manually or automatically pushed at least once. pushed, + + /// Critical tasks that have missed at least once and need recovery attention. criticalMissed, + + /// Someday/maybe tasks that are intentionally kept out of normal pressure. wishlist, + + /// Tasks whose [Task.updatedAt] age exceeds the configured stale threshold. stale, + + /// Tasks still missing a reward estimate. Useful during cleanup/review. noRewardSet, } /// Sort options for a unified backlog list. +/// +/// Sort keys are intentionally product-facing rather than database-facing. For +/// example, `rewardVsEffort` maps to a simple derived score instead of a stored +/// field. Persistence can later index the underlying fields if needed. enum BacklogSortKey { + /// Highest priority first. priority, + + /// Best simple reward-minus-difficulty score first. rewardVsEffort, + + /// Oldest created task first. age, + + /// Lexicographic project id grouping. Future UI can replace this with project + /// display order while keeping the same public key. project, + + /// Most frequently pushed tasks first. timesPushed, } /// Visual age bucket for backlog display. +/// +/// This supports the design rule that old backlog items should visually age +/// from green to blue to purple. The enum names describe semantic buckets; UI +/// code should translate them into actual theme colors. enum BacklogStalenessMarker { + /// Fresh backlog item. Default: created within seven days. green, + + /// Aging backlog item. Default: created within thirty days. blue, + + /// Old/stale backlog item. Default: created more than thirty days ago. purple, } /// Configurable thresholds for backlog age markers. +/// +/// The defaults match the current design spec: less than a week is fresh, less +/// than a month is aging, and anything older is stale. Keeping the thresholds in +/// a value object makes future settings/preferences easy to inject in tests or +/// user configuration. class BacklogStalenessSettings { const BacklogStalenessSettings({ this.greenMaxAge = const Duration(days: 7), this.blueMaxAge = const Duration(days: 30), }); + /// Maximum age that still counts as fresh/green. final Duration greenMaxAge; + + /// Maximum age that still counts as aging/blue. Anything older is purple. final Duration blueMaxAge; + /// Return the visual age marker for [task] relative to [now]. + /// + /// This uses [Task.createdAt], not [Task.updatedAt], because the marker is + /// meant to show how long the idea has existed in the system. A task edited + /// yesterday but created two months ago should still feel old in the backlog. BacklogStalenessMarker markerFor({ required Task task, required DateTime now, @@ -55,6 +114,11 @@ class BacklogStalenessSettings { } /// Read-only backlog projection over the unified task list. +/// +/// [BacklogView] is a query/helper object. It does not mutate tasks or own data; +/// it receives the current task list and exposes common backlog slices for UI. +/// That keeps backlog display logic out of widgets and avoids duplicating the +/// same filtering rules in multiple screens. class BacklogView { const BacklogView({ required this.tasks, @@ -63,21 +127,42 @@ class BacklogView { this.stalenessSettings = const BacklogStalenessSettings(), }); + /// Master task list supplied by the caller. Only `status == backlog` items are + /// shown by this view. final List tasks; + + /// Clock value supplied by the caller so age/staleness behavior is testable. final DateTime now; + + /// Age since [Task.updatedAt] that qualifies for the `stale` filter. final Duration staleAfter; + + /// Color-bucket threshold configuration for backlog aging indicators. final BacklogStalenessSettings stalenessSettings; + /// All tasks currently in backlog status. + /// + /// The returned list is a snapshot. It is not intended to be modified and then + /// written back; state changes should go through scheduling/action services. List get backlogTasks { return tasks.where((task) => task.isBacklog).toList(growable: false); } + /// Return backlog tasks matching a single user-facing filter. + /// + /// Filtering always starts from [backlogTasks], so a completed or planned task + /// will never appear here even if it has matching statistics. List filter(BacklogFilter filter) { return backlogTasks.where((task) => _matchesFilter(task, filter)).toList( growable: false, ); } + /// Return all backlog tasks sorted by a user-facing ordering. + /// + /// A new list is created before sorting so the original [tasks] list is never + /// reordered by a read operation. The final list is unmodifiable to make that + /// intent explicit to callers. List sorted(BacklogSortKey sortKey) { final sortedTasks = [...backlogTasks]; sortedTasks.sort((a, b) => _compareTasks(a, b, sortKey)); @@ -85,10 +170,15 @@ class BacklogView { return List.unmodifiable(sortedTasks); } + /// Return the green/blue/purple marker for one task. BacklogStalenessMarker stalenessMarkerFor(Task task) { return stalenessSettings.markerFor(task: task, now: now); } + /// Private predicate implementing every [BacklogFilter] option. + /// + /// Keeping this as a switch expression makes new filters obvious: add the enum + /// value and the compiler forces this method to handle it. bool _matchesFilter(Task task, BacklogFilter filter) { return switch (filter) { BacklogFilter.inbox => task.projectId == 'inbox', @@ -102,6 +192,10 @@ class BacklogView { }; } + /// Comparison callback used by [sorted]. + /// + /// Sort directions are encoded here. Higher priority/reward/push counts should + /// appear earlier, while older age uses the earliest [Task.createdAt] first. int _compareTasks(Task a, Task b, BacklogSortKey sortKey) { return switch (sortKey) { BacklogSortKey.priority => @@ -115,6 +209,10 @@ class BacklogView { } } +/// Convert nullable priority into a stable numeric rank for sorting. +/// +/// Null priority is treated like medium so partially imported data behaves like +/// normal starter tasks instead of sinking to the bottom. int _priorityRank(PriorityLevel? priority) { return switch (priority) { PriorityLevel.veryLow => 0, @@ -125,6 +223,7 @@ int _priorityRank(PriorityLevel? priority) { }; } +/// Convert reward enum values to numeric ranks for derived scoring. int _rewardRank(RewardLevel reward) { return switch (reward) { RewardLevel.notSet => 0, @@ -136,6 +235,7 @@ int _rewardRank(RewardLevel reward) { }; } +/// Convert difficulty enum values to numeric ranks for derived scoring. int _difficultyRank(DifficultyLevel difficulty) { return switch (difficulty) { DifficultyLevel.notSet => 0, @@ -147,10 +247,17 @@ int _difficultyRank(DifficultyLevel difficulty) { }; } +/// Simple motivation score: reward minus difficulty. +/// +/// Positive scores suggest high payoff for lower activation cost. Negative scores +/// suggest high effort for lower payoff. This is deliberately simple for V1 and +/// can be replaced by richer heuristics later without changing the public sort +/// key. int _rewardVsEffortScore(Task task) { return _rewardRank(task.reward) - _difficultyRank(task.difficulty); } +/// Total manual and automatic pushes recorded on the task. int _timesPushed(Task task) { return task.stats.manuallyPushedCount + task.stats.autoPushedCount; } diff --git a/lib/src/locked_time.dart b/lib/src/locked_time.dart index 4ab41d1..30a7f78 100644 --- a/lib/src/locked_time.dart +++ b/lib/src/locked_time.dart @@ -1,6 +1,17 @@ +// Locked-time modeling and expansion. +// +// Locked time is anything the flexible scheduler should treat as unavailable: +// work, appointments, streams, sleep boundaries, relationship blocks, or one-off +// interruptions. This file converts human-friendly locked block definitions into +// concrete `TimeInterval`s that the scheduler can avoid. + import 'models.dart'; /// Weekday value using DateTime's Monday-first convention. +/// +/// Dart represents weekdays as integers where Monday is `1` and Sunday is `7`. +/// This enum wraps those integers so the rest of the code can talk in readable +/// names while still comparing directly to [DateTime.weekday]. enum LockedWeekday { monday(DateTime.monday), tuesday(DateTime.tuesday), @@ -12,10 +23,15 @@ enum LockedWeekday { const LockedWeekday(this.dateTimeValue); + /// Matching [DateTime.weekday] integer value. final int dateTimeValue; } /// Time of day without a calendar date. +/// +/// Locked blocks are often defined as "every weekday from 8:00 to 17:00" rather +/// than as one specific timestamp. [ClockTime] stores just the hour/minute part +/// and can later be projected onto a concrete date with [onDate]. class ClockTime { const ClockTime({ required this.hour, @@ -23,28 +39,53 @@ class ClockTime { }) : assert(hour >= 0 && hour < 24, 'hour must be 0-23'), assert(minute >= 0 && minute < 60, 'minute must be 0-59'); + /// 24-hour clock hour, 0 through 23. final int hour; + + /// Minute within the hour, 0 through 59. final int minute; + /// Combine this time-of-day with [date]. + /// + /// Only the year, month, and day from [date] are used. Seconds and smaller + /// units are intentionally reset because locked blocks in this starter project + /// operate at minute precision. DateTime onDate(DateTime date) { return DateTime(date.year, date.month, date.day, hour, minute); } } /// Recurrence rule for locked time. +/// +/// The current starter implementation only supports weekly recurrence because +/// that covers the product's fixed work/stream/relationship blocks. This object +/// keeps recurrence separate from [LockedBlock] so monthly or custom recurrence +/// rules can be added later without changing the rest of the locked-time model. class LockedBlockRecurrence { const LockedBlockRecurrence.weekly({ required this.weekdays, }); + /// Weekdays when this recurrence should produce an occurrence. final Set weekdays; + /// Whether this recurrence applies to [date]. bool occursOn(DateTime date) { return weekdays.any((weekday) => weekday.dateTimeValue == date.weekday); } } /// Scheduling constraint that reserves time without becoming a task card. +/// +/// Locked blocks represent time the flexible scheduler should avoid: work hours, +/// appointments, sleep boundaries, streams, relationship blocks, or any other +/// commitment that should not be automatically rearranged. They are modeled +/// separately from normal tasks because the UI may show them as subtle overlays +/// rather than actionable task cards. +/// +/// A block is either: +/// - one-off, with [date] set and [recurrence] null; or +/// - recurring, with [recurrence] set and [date] usually null. class LockedBlock { const LockedBlock({ required this.id, @@ -62,23 +103,46 @@ class LockedBlock { 'date is required for one-off locked blocks', ); + /// Stable id for persistence and one-day overrides. final String id; + + /// User-facing label, such as `Work`, `Stream`, or `Relationship block`. final String name; + + /// Start time-of-day. Combined with a date during expansion. final ClockTime startTime; + + /// End time-of-day. Combined with a date during expansion. final ClockTime endTime; /// Calendar date for one-off locked blocks. Recurring blocks leave this null. final DateTime? date; + + /// Optional weekly recurrence. Null means this is a one-off block. final LockedBlockRecurrence? recurrence; + + /// Whether UI should keep this block visually quiet by default. Scheduling still + /// treats hidden blocks as blocked time. final bool hiddenByDefault; + + /// Optional project/category association for UI colors or reports. final String? projectId; + + /// Creation timestamp for persistence/auditing. final DateTime createdAt; + + /// Last update timestamp for persistence/auditing. final DateTime updatedAt; + /// Convenience check for whether this block expands through recurrence. bool get isRecurring => recurrence != null; } /// Concrete locked-time occurrence for one calendar day. +/// +/// [LockedBlock] is a rule; [LockedBlockOccurrence] is the actual result on a +/// specific date. The scheduler only needs occurrences/time intervals, while UI +/// can use ids and visibility flags to explain where the blocked time came from. class LockedBlockOccurrence { const LockedBlockOccurrence({ required this.name, @@ -89,14 +153,29 @@ class LockedBlockOccurrence { this.projectId, }); + /// Display/debug label for this occurrence. final String name; + + /// Concrete start/end on one calendar date. final TimeInterval interval; + + /// Visibility hint for UI; does not affect scheduling. final bool hiddenByDefault; + + /// Source recurring/one-off block id, when this came from a base block. final String? lockedBlockId; + + /// Source override id, when this was replaced or added for one date. final String? overrideId; + + /// Optional project/category association for UI colors or reports. final String? projectId; /// Scheduler-facing interval. Visibility only affects future UI overlays. + /// + /// A fresh [TimeInterval] is returned with [name] as the label so scheduling + /// notices/debugging can identify the blocked source without depending on the + /// richer occurrence object. TimeInterval get schedulingInterval { return TimeInterval( start: interval.start, @@ -107,13 +186,30 @@ class LockedBlockOccurrence { } /// Type of one-day override applied to locked time. +/// +/// Overrides let the app handle holidays, one-off appointments, or changed work +/// hours without editing the base recurring rule. enum LockedBlockOverrideType { + /// Suppress one occurrence of a recurring locked block. remove, + + /// Replace one occurrence with different details. replace, + + /// Add a one-off locked occurrence that has no base recurring block. add, } /// One-day change to locked time that leaves the base recurrence unchanged. +/// +/// Overrides are applied during expansion for a single date only. This is safer +/// than modifying the recurring block because the normal schedule remains intact +/// for every other day. +/// +/// Use the named factories to create valid override shapes: +/// - [remove] references a base block and suppresses that occurrence. +/// - [replace] references a base block and swaps its details for one day. +/// - [add] creates an extra locked occurrence that has no base block. class LockedBlockOverride { const LockedBlockOverride({ required this.id, @@ -201,18 +297,40 @@ class LockedBlockOverride { ); } + /// Stable id for persistence and debugging. final String id; + + /// Base block id affected by remove/replace overrides. Null for add overrides. final String? lockedBlockId; + + /// Date the override applies to. Time components are ignored by date checks. final DateTime date; + + /// Kind of override operation. final LockedBlockOverrideType type; + + /// Optional replacement/addition name. final String? name; + + /// Optional replacement/addition start time. final ClockTime? startTime; + + /// Optional replacement/addition end time. final ClockTime? endTime; + + /// Visibility hint for UI; scheduling still blocks this time. final bool hiddenByDefault; + + /// Optional project/category association for UI colors or reports. final String? projectId; + + /// Creation timestamp for persistence/auditing. final DateTime createdAt; + + /// Last update timestamp for persistence/auditing. final DateTime updatedAt; + /// Whether this override targets [blockId] on [occurrenceDate]. bool appliesTo({ required String blockId, required DateTime occurrenceDate, @@ -220,6 +338,11 @@ class LockedBlockOverride { return lockedBlockId == blockId && _sameDate(date, occurrenceDate); } + /// Build a concrete interval for [targetDate] when this override has enough + /// time data and applies to that date. + /// + /// Remove overrides intentionally return null because they do not create a new + /// interval. Replacement/add overrides need both [startTime] and [endTime]. TimeInterval? intervalForDate(DateTime targetDate) { final start = startTime; final end = endTime; @@ -237,15 +360,23 @@ class LockedBlockOverride { } /// Expands locked blocks and one-day overrides into concrete intervals. +/// +/// This object is the boundary between human-friendly locked block definitions +/// and scheduler-friendly intervals. It retains the occurrence details for UI +/// while exposing [schedulingIntervals] for placement algorithms. class LockedScheduleExpansion { const LockedScheduleExpansion({ required this.date, required this.occurrences, }); + /// Calendar date represented by this expansion, normalized to year/month/day. final DateTime date; + + /// Concrete locked occurrences active on [date]. final List occurrences; + /// Just the intervals the scheduler needs to avoid. List get schedulingIntervals { return List.unmodifiable( occurrences.map((occurrence) => occurrence.schedulingInterval), @@ -254,6 +385,14 @@ class LockedScheduleExpansion { } /// Returns concrete locked-time occurrences for [date]. +/// +/// Expansion order: +/// 1. Sort overrides deterministically by creation time, then id. +/// 2. For each base block that occurs on the date, collect its overrides. +/// 3. Skip the occurrence if any remove override applies. +/// 4. Use the latest replacement override if one exists. +/// 5. Add one-off `add` overrides for the date. +/// 6. Sort occurrences by start time for predictable UI/scheduler behavior. LockedScheduleExpansion expandLockedBlocksForDay({ required List blocks, required List overrides, @@ -327,6 +466,10 @@ LockedScheduleExpansion expandLockedBlocksForDay({ ); } +/// Return the last replacement override from an already sorted override list. +/// +/// Later-created replacements win, which lets a user correct a one-day override +/// without deleting older history first. LockedBlockOverride? _lastReplacementOverride( List overrides, ) { @@ -339,6 +482,7 @@ LockedBlockOverride? _lastReplacementOverride( return null; } +/// Convenience wrapper when callers only need scheduler intervals. List lockedSchedulingIntervalsForDay({ required List blocks, required List overrides, @@ -352,6 +496,10 @@ List lockedSchedulingIntervalsForDay({ } /// Returns [task] with locked-hour completion statistics applied when relevant. +/// +/// This does not decide whether completion is allowed. It only records that a +/// completion overlapped locked time, which future reports can surface as a +/// boundary-leak signal. Task trackCompletedDuringLockedHours({ required Task task, required List lockedIntervals, @@ -371,6 +519,9 @@ Task trackCompletedDuringLockedHours({ } /// Calculates how many scheduled task minutes overlap locked intervals. +/// +/// Multiple locked intervals may overlap each other. To avoid double-counting, +/// intersections are merged before minutes are summed. int completedDuringLockedHoursMinutes({ required Task task, required List lockedIntervals, @@ -424,6 +575,7 @@ int completedDuringLockedHoursMinutes({ return total.inMinutes; } +/// Whether a base block should produce an occurrence on [date]. bool _blockOccursOn(LockedBlock block, DateTime date) { final recurrence = block.recurrence; if (recurrence != null) { @@ -433,6 +585,7 @@ bool _blockOccursOn(LockedBlock block, DateTime date) { return _sameDate(block.date!, date); } +/// Convert a base block rule into a concrete occurrence for [date]. LockedBlockOccurrence _occurrenceFromBlock( LockedBlock block, DateTime date, @@ -450,6 +603,10 @@ LockedBlockOccurrence _occurrenceFromBlock( ); } +/// Convert a replacement override into a concrete occurrence. +/// +/// Missing override fields fall back to the original block, so a one-day change +/// can replace only the time, only the name, or only the project association. LockedBlockOccurrence _occurrenceFromReplacement({ required LockedBlock block, required LockedBlockOverride override, @@ -473,6 +630,7 @@ LockedBlockOccurrence _occurrenceFromReplacement({ ); } +/// Convert an add override into a concrete occurrence when it is complete. LockedBlockOccurrence? _occurrenceFromAddedOverride( LockedBlockOverride override, DateTime date, @@ -497,10 +655,12 @@ LockedBlockOccurrence? _occurrenceFromAddedOverride( ); } +/// Whether this task state/type should be checked for locked-hour completion. bool _shouldTrackLockedHourCompletion(Task task) { return task.status == TaskStatus.completed || task.type == TaskType.surprise; } +/// Build a valid scheduled interval for a task, or null if the task is unplaced. TimeInterval? _scheduledIntervalForTask(Task task) { final start = task.scheduledStart; final end = task.scheduledEnd; @@ -512,14 +672,17 @@ TimeInterval? _scheduledIntervalForTask(Task task) { return TimeInterval(start: start, end: end, label: task.id); } +/// Return whichever timestamp is earlier. DateTime _earliest(DateTime first, DateTime second) { return first.isBefore(second) ? first : second; } +/// Return whichever timestamp is later. DateTime _latest(DateTime first, DateTime second) { return first.isAfter(second) ? first : second; } +/// Compare only the calendar date parts of two timestamps. bool _sameDate(DateTime first, DateTime second) { return first.year == second.year && first.month == second.month && diff --git a/lib/src/models.dart b/lib/src/models.dart index fad47ef..2bd7909 100644 --- a/lib/src/models.dart +++ b/lib/src/models.dart @@ -1,6 +1,28 @@ +// Core domain model for the ADHD scheduling starter project. +// +// This file intentionally contains small immutable value objects and enums. It +// should stay free of UI, persistence, notification, and platform code. Keeping +// this layer plain makes the scheduler easy to test and easy to move between a +// command-line prototype, Flutter UI, or future backend service. +// +// Reading order for humans: +// 1. `TaskType` and `TaskStatus` explain the two main axes of a task. +// 2. `Task` shows the actual data carried through the scheduling engine. +// 3. `ProjectProfile` explains how project defaults create tasks. +// 4. `TimeInterval` is the shared time-span helper used by scheduling logic. + import 'task_statistics.dart'; /// Scheduling behavior category. +/// +/// This enum is one of the central concepts in the planner. The type answers +/// the question: "how should the scheduler treat this item?" It is separate +/// from [TaskStatus], which answers "where is this item in its lifecycle?" +/// +/// For example, a flexible task can be planned, completed, missed, or moved to +/// backlog. A locked item, by contrast, acts more like a calendar constraint +/// than a normal task card. Keeping behavior and lifecycle separate makes later +/// UI and persistence logic easier to reason about. enum TaskType { /// Movable planned work. flexible, @@ -22,6 +44,12 @@ enum TaskType { } /// Current lifecycle state of a task. +/// +/// Status is intentionally data-oriented: it describes what happened to a task, +/// not how important it is or how the scheduler should move it. Scheduler rules +/// combine [TaskStatus] with [TaskType]. For instance, a planned flexible task +/// can be pushed, while a planned critical task should remain visible and become +/// backlog if missed. enum TaskStatus { /// Scheduled or queued work that has not started. planned, @@ -46,6 +74,12 @@ enum TaskStatus { } /// User-facing importance level. +/// +/// Priority is a relative ordering hint. It should help decide what rises to the +/// top of a queue, but it should not be treated as an absolute promise that the +/// task must happen at a specific time. The scheduling engine currently ranks +/// this with simple numeric helpers in `backlog.dart`; more advanced heuristics +/// can build on the same enum later. enum PriorityLevel { /// Lowest priority. veryLow, @@ -64,6 +98,10 @@ enum PriorityLevel { } /// Expected reward or payoff from completing a task. +/// +/// Reward is meant to capture motivational payoff, not objective value. In this +/// app design it supports ADHD-friendly planning: a small, easy, high-reward task +/// may be a better momentum starter than a large low-reward task. enum RewardLevel { /// No reward level has been captured; this is not equivalent to low reward. notSet, @@ -85,6 +123,11 @@ enum RewardLevel { } /// Expected effort or activation difficulty for a task. +/// +/// Difficulty is not the same as duration. A five-minute phone call might be +/// very hard to start, while an hour of familiar maintenance may be easy. The +/// backlog view uses this with [RewardLevel] to expose a simple +/// reward-versus-effort sort. enum DifficultyLevel { /// No difficulty has been captured yet. notSet, @@ -106,6 +149,10 @@ enum DifficultyLevel { } /// Reminder intensity preference. +/// +/// This is currently stored as project metadata rather than enforced by the core +/// scheduler. Future notification/UI layers can use it to decide how aggressive +/// reminders should be without adding reminder-specific logic to [Task]. enum ReminderProfile { /// No reminder nudges. silent, @@ -121,6 +168,10 @@ enum ReminderProfile { } /// Lightweight backlog-only metadata. +/// +/// Tags here are deliberately narrow. They are not meant to replace a general +/// tagging system. They identify special backlog behavior that the planner needs +/// to understand, such as "wishlist/someday" items. enum BacklogTag { /// Task is intentionally saved as a someday/wishlist item. wishlist, @@ -128,8 +179,25 @@ enum BacklogTag { /// Starter task model for the scheduling core. /// -/// This is a public placeholder API for early V1 chunks. Keep behavior changes -/// explicit and covered by tests as the model becomes more complete. +/// [Task] is the main domain object passed through the scheduler. It is written +/// as an immutable value object: operations do not mutate an existing task, they +/// return a copied task with changed fields. That approach makes scheduling +/// actions easier to test because every function receives an input list and +/// returns a new output list. +/// +/// Important modeling choices: +/// - [type] controls scheduling behavior: flexible, critical, locked, etc. +/// - [status] controls lifecycle state: planned, completed, backlog, etc. +/// - [scheduledStart] and [scheduledEnd] are optional because backlog items and +/// unscheduled captures do not have timeline placement yet. +/// - [stats] records quiet metadata for future reports. It should not clutter +/// the everyday UI unless a report or filter specifically needs it. +/// - [parentTaskId] links child tasks to a larger parent task without requiring +/// a nested object graph. That keeps persistence simple and avoids recursive +/// scheduling structures. +/// +/// This is still a starter V1 model, so behavior changes should be explicit and +/// backed by tests as the product rules settle. class Task { const Task({ required this.id, @@ -151,6 +219,14 @@ class Task { }); /// Create a minimal captured task without requiring planning details. + /// + /// Quick capture is intentionally forgiving: the user can enter only a title + /// and the system can still create a valid backlog item. Defaults route the + /// item into the inbox project as a medium-priority flexible backlog task. + /// + /// The only hard validation here is that [title] must contain non-whitespace + /// text. Scheduling validation, such as requiring a positive duration, happens + /// in `quick_capture.dart` because that depends on the requested capture flow. factory Task.quickCapture({ required String id, required String title, @@ -183,29 +259,81 @@ class Task { ); } + /// Stable identifier used by persistence, UI selection, and scheduler changes. final String id; + + /// User-facing task title. The model expects this to already be trimmed. final String title; + + /// Owning project/profile id. `inbox` is used for uncategorized captures. final String projectId; + + /// Scheduling behavior category. See [TaskType] for rule-level meaning. final TaskType type; + + /// Current lifecycle state. See [TaskStatus] for state-level meaning. final TaskStatus status; + + /// Optional importance. Most creation paths default this to medium, but it is + /// nullable to leave room for imports or legacy data that have not set it yet. final PriorityLevel? priority; + + /// Motivational payoff used by backlog sorting and future planning hints. final RewardLevel reward; + + /// Activation/effort estimate used by backlog sorting and future planning hints. final DifficultyLevel difficulty; + + /// Estimated task length. Required for most scheduling operations, optional for + /// backlog capture because not every captured thought has an estimate yet. final int? durationMinutes; + + /// Inclusive scheduled start time. Null means the task is not currently placed. final DateTime? scheduledStart; + + /// Exclusive scheduled end time. Null means the task is not currently placed. final DateTime? scheduledEnd; + + /// Parent task id when this task is a child/subtask. Null means top-level task. final String? parentTaskId; + + /// Backlog-specific flags, such as wishlist/someday behavior. final Set backlogTags; + + /// Creation timestamp used for age/staleness sorting. final DateTime createdAt; + + /// Last domain-level update timestamp. Scheduling actions set this when moving + /// or changing tasks so persistence and reports can detect recent activity. final DateTime updatedAt; + + /// Quiet counters for reporting and later heuristics. final TaskStatistics stats; + /// Convenience predicate for the task type most scheduler movement operates on. bool get isFlexible => type == TaskType.flexible; + + /// Critical and inflexible tasks are both visible to the user and treated as + /// blocked time by flexible scheduling. bool get isRequiredVisible => type == TaskType.critical || type == TaskType.inflexible; + + /// Locked tasks behave as timeline constraints, not normal interactive cards. bool get isLocked => type == TaskType.locked; + + /// Backlog status means the task is stored for later and has no active slot. bool get isBacklog => status == TaskStatus.backlog; + /// Return a copy with selected fields changed. + /// + /// The core uses this instead of mutation. That matters because scheduling + /// operations often need to produce an auditable before/after result, including + /// [SchedulingChange]-style records elsewhere. + /// + /// [clearSchedule] is a deliberate escape hatch for nullable schedule fields. + /// Without it, passing null would be ambiguous: it could mean "do not change" + /// or "clear this value." When [clearSchedule] is true, both schedule fields + /// are removed even if [scheduledStart] or [scheduledEnd] are omitted. Task copyWith({ String? id, String? title, @@ -248,6 +376,14 @@ class Task { } /// Starter project defaults used when creating or scheduling tasks. +/// +/// A project profile represents reusable defaults for a group of tasks. UI code +/// can let the user pick a project, then call [createTask] so new tasks inherit +/// a color, default priority, reward, difficulty, reminder profile, and duration. +/// +/// The scheduler itself mostly cares about the resulting [Task] fields. Keeping +/// project defaults separate prevents every scheduling function from needing to +/// know project configuration details. class ProjectProfile { const ProjectProfile({ required this.id, @@ -260,16 +396,35 @@ class ProjectProfile { this.defaultDurationMinutes, }); + /// Stable project id stored on tasks. final String id; + + /// User-facing project name. final String name; + + /// Theme/color token for UI rendering. This is a key, not a raw color value. final String colorKey; + + /// Default importance assigned when a task does not override priority. final PriorityLevel defaultPriority; + + /// Default motivational payoff assigned when a task does not override reward. final RewardLevel defaultReward; + + /// Default activation difficulty assigned when a task does not override effort. final DifficultyLevel defaultDifficulty; + + /// Default reminder behavior for future notification/UI layers. final ReminderProfile defaultReminderProfile; + + /// Optional duration estimate used for newly created tasks. final int? defaultDurationMinutes; /// Create a task using project defaults while allowing explicit overrides. + /// + /// This keeps capture and project-default behavior in one place. Callers can + /// pass only the fields the user explicitly set; everything else falls back to + /// this profile. The created task receives this profile's [id] as [Task.projectId]. Task createTask({ required String id, required String title, @@ -307,6 +462,11 @@ class ProjectProfile { } /// Starter time range value used by scheduling helpers. +/// +/// [TimeInterval] is the scheduler's neutral representation of a time span. It +/// is used for scheduled task slots, locked blocks, required visible blocks, and +/// candidate placements. The interval convention is start-inclusive and +/// end-exclusive, which avoids treating two back-to-back blocks as overlapping. class TimeInterval { const TimeInterval({ required this.start, @@ -314,12 +474,24 @@ class TimeInterval { this.label, }); + /// Inclusive beginning of the interval. final DateTime start; + + /// Exclusive ending of the interval. final DateTime end; + + /// Optional debug/UI label, often a task id or locked block name. final String? label; + /// Raw duration between [start] and [end]. Callers are responsible for only + /// constructing meaningful positive intervals when required by a rule. Duration get duration => end.difference(start); + /// Whether this interval shares any actual time with [other]. + /// + /// Adjacent intervals do not overlap: `9:00-10:00` and `10:00-11:00` are safe + /// to place back-to-back because the first interval's end is the second + /// interval's start. bool overlaps(TimeInterval other) { return start.isBefore(other.end) && end.isAfter(other.start); } diff --git a/lib/src/quick_capture.dart b/lib/src/quick_capture.dart index 0fbd4e5..60565a6 100644 --- a/lib/src/quick_capture.dart +++ b/lib/src/quick_capture.dart @@ -1,14 +1,35 @@ +// Low-friction capture flow. +// +// Quick capture is the "dump the thought before it disappears" path. The goal +// is to accept minimal data, preserve the user's input, and only ask for more +// structure when the user wants immediate scheduling. + import 'models.dart'; import 'scheduling_engine.dart'; /// Outcome of a quick-capture request. +/// +/// The UI can use this status to decide whether to show a passive success, draw +/// a scheduled card on the timeline, or display validation messages. It is not +/// an exception-based flow because quick capture should fail gently and keep the +/// user's typed task available. enum QuickCaptureStatus { + /// Capture succeeded and the task remains unscheduled in backlog. addedToBacklog, + + /// Capture succeeded and the task was placed on the timeline. scheduled, + + /// Capture could not complete the requested flow; see result messages. validationError, } /// Input for low-friction task capture. +/// +/// This object represents what the UI knows at the moment of capture. It mirrors +/// the product goal: adding a thought should require as little structure as +/// possible, but the user can optionally provide enough detail to immediately +/// schedule it into the next open flexible slot. class QuickCaptureRequest { const QuickCaptureRequest({ required this.id, @@ -24,20 +45,47 @@ class QuickCaptureRequest { this.backlogTags = const {}, }); + /// Caller-generated id. Keeping id generation outside this service makes the + /// domain layer independent from persistence/database choices. final String id; + + /// Raw user-entered title. The [Task.quickCapture] factory trims it. final String title; + + /// Capture timestamp supplied by the caller for testability. final DateTime createdAt; + + /// Whether capture should attempt immediate timeline placement. final bool addToNextAvailableSlot; + + /// Project id to assign; defaults to the inbox for uncategorized thoughts. final String projectId; + + /// Initial priority used by backlog/scheduler heuristics. final PriorityLevel priority; + + /// Initial reward estimate. final RewardLevel reward; + + /// Initial difficulty estimate. final DifficultyLevel difficulty; + + /// Captured task type. Immediate scheduling currently requires flexible tasks. final TaskType type; + + /// Optional duration estimate. Required only when scheduling immediately. final int? durationMinutes; + + /// Optional backlog flags such as wishlist/someday. final Set backlogTags; } /// Result of a quick-capture request. +/// +/// The result always carries a [task], even on validation failure, so the UI can +/// preserve the user's input and show what needs to be fixed. When scheduling +/// was attempted, [schedulingResult] exposes the lower-level engine notices and +/// changes for debugging or timeline updates. class QuickCaptureResult { const QuickCaptureResult({ required this.task, @@ -46,20 +94,43 @@ class QuickCaptureResult { this.messages = const [], }); + /// Captured task, scheduled or unscheduled depending on [status]. final Task task; + + /// High-level outcome of the capture attempt. final QuickCaptureStatus status; + + /// Detailed scheduling output when immediate placement was attempted. final SchedulingResult? schedulingResult; + + /// Human-readable validation or scheduling messages. final List messages; + /// Convenience check for UI branches that only care whether capture succeeded. bool get isValid => status != QuickCaptureStatus.validationError; } /// Coordinates quick capture defaults and optional scheduling. +/// +/// This service is intentionally thin: it builds a [Task], validates the extra +/// requirements for immediate scheduling, and delegates placement to +/// [SchedulingEngine]. It keeps quick-capture UI code from needing to understand +/// every scheduler precondition. class QuickCaptureService { const QuickCaptureService({this.engine = const SchedulingEngine()}); + /// Scheduling dependency. Defaults to the starter engine but can be swapped in + /// tests or future implementations. final SchedulingEngine engine; + /// Capture a task and optionally place it into the next available slot. + /// + /// Flow: + /// 1. Build the task using lightweight defaults. + /// 2. If immediate scheduling was not requested, return a backlog success. + /// 3. Validate the requirements for immediate scheduling. + /// 4. Call [SchedulingEngine.insertBacklogTaskIntoNextAvailableSlot]. + /// 5. Convert the lower-level scheduling result into a capture result. QuickCaptureResult capture( QuickCaptureRequest request, { SchedulingInput? schedulingInput, @@ -138,6 +209,10 @@ class QuickCaptureService { } } +/// Find a task in a returned scheduling result. +/// +/// This duplicates a small helper rather than exposing scheduler internals. The +/// capture service only needs to retrieve the newly created task after placement. Task? _taskById(List tasks, String id) { for (final task in tasks) { if (task.id == id) { diff --git a/lib/src/scheduling_engine.dart b/lib/src/scheduling_engine.dart index 5c4c081..ed1c014 100644 --- a/lib/src/scheduling_engine.dart +++ b/lib/src/scheduling_engine.dart @@ -1,6 +1,24 @@ +// Scheduling engine for the ADHD scheduling starter project. +// +// This file is the core timeline manipulation layer. It takes task data plus a +// planning window and returns a new task list, notices, changes, and analysis +// findings. The implementation is deliberately side-effect free so a first-time +// reader can trace each operation from input validation, to placement planning, +// to application of the plan. +// +// Human reading map: +// 1. Data wrappers: `SchedulingWindow`, `SchedulingInput`, result classes. +// 2. Public engine methods: the operations UI/actions can call. +// 3. Private planning helpers: calculate intervals without changing tasks. +// 4. Private apply helpers: convert plans into updated tasks and notices. + import 'models.dart'; /// Category for scheduler notices. +/// +/// Notices are human-readable summaries attached to a [SchedulingResult]. They +/// are not exceptions. The scheduler returns them alongside the task list so UI +/// can explain what happened without losing the successfully computed output. enum SchedulingNoticeType { /// General informational notice. info, @@ -19,17 +37,31 @@ enum SchedulingNoticeType { } /// Window of time available to a scheduling operation. +/// +/// Most engine methods operate on one planning window: "today", "tomorrow", +/// or any other bounded range supplied by the caller. The window constrains where +/// flexible tasks can be placed. Anything outside this range is treated as out of +/// scope for the operation. class SchedulingWindow { const SchedulingWindow({ required this.start, required this.end, }); + /// Inclusive beginning of the scheduling range. final DateTime start; + + /// Exclusive ending of the scheduling range. final DateTime end; + /// The window as a [TimeInterval], useful for overlap checks. TimeInterval get interval => TimeInterval(start: start, end: end); + /// Whether [interval] is completely inside this window. + /// + /// A task that starts before the window or ends after the window is considered + /// outside the operation. The engine may treat such tasks as fixed blocks + /// instead of moving them. bool contains(TimeInterval interval) { final startsInWindow = interval.start.isAfter(start) || interval.start.isAtSameMomentAs(start); @@ -41,6 +73,11 @@ class SchedulingWindow { } /// In-memory input for scheduling operations. +/// +/// This is the complete snapshot the pure scheduling engine needs. It contains +/// tasks plus the fixed intervals the scheduler must avoid. It deliberately does +/// not know where the data came from: UI state, a database, tests, or generated +/// examples can all build this same object. class SchedulingInput { const SchedulingInput({ required this.tasks, @@ -49,29 +86,49 @@ class SchedulingInput { this.requiredVisibleIntervals = const [], }); + /// All tasks available to this operation. The scheduler returns a replacement + /// list rather than mutating this one. final List tasks; + + /// Date/time range that the operation is allowed to plan inside. final SchedulingWindow window; + + /// External locked time intervals, usually produced by `locked_time.dart`. final List lockedIntervals; + + /// Extra fixed visible intervals supplied by the caller. This lets UI/backend + /// code reserve required time even when that time is not represented as a + /// [Task] in the current list. final List requiredVisibleIntervals; + /// Tasks that the flexible movement algorithms are allowed to consider. List get flexibleTasks { return tasks.where((task) => task.isFlexible).toList(growable: false); } + /// Locked task records in [tasks], if the caller represents locked time as + /// tasks instead of only passing [lockedIntervals]. List get lockedTasks { return tasks.where((task) => task.isLocked).toList(growable: false); } + /// Critical and inflexible task records that should block flexible placement. List get requiredVisibleTasks { return tasks .where((task) => task.isRequiredVisible) .toList(growable: false); } + /// Scheduled intervals for flexible tasks only. Useful for analysis/debugging. List get flexibleIntervals { return _scheduledIntervalsFor(flexibleTasks); } + /// All intervals that flexible scheduling must avoid. + /// + /// This combines explicit locked intervals, locked task records, caller-supplied + /// required-visible intervals, and scheduled critical/inflexible tasks. The + /// result is sorted to make interval scanning deterministic. List get blockedIntervals { final intervals = [ ...lockedIntervals, @@ -85,6 +142,10 @@ class SchedulingInput { } /// Exact placement change made by a scheduling operation. +/// +/// Changes are machine-readable before/after records. UI can use notices for +/// display text, but persistence, undo, analytics, or tests should inspect these +/// fields to know exactly which task moved and where. class SchedulingChange { const SchedulingChange({ required this.taskId, @@ -94,14 +155,27 @@ class SchedulingChange { required this.nextEnd, }); + /// Task that moved or had its schedule cleared. final String taskId; + + /// Previous scheduled start, or null if the task was previously unplaced. final DateTime? previousStart; + + /// Previous scheduled end, or null if the task was previously unplaced. final DateTime? previousEnd; + + /// New scheduled start, or null if the task was moved out of the timeline. final DateTime? nextStart; + + /// New scheduled end, or null if the task was moved out of the timeline. final DateTime? nextEnd; } /// Overlap between a scheduled task and blocked time. +/// +/// Analysis uses this to report problems without moving anything. This is useful +/// when loading persisted data, debugging imports, or validating a day before the +/// UI presents it as clean. class SchedulingOverlap { const SchedulingOverlap({ required this.taskId, @@ -109,12 +183,21 @@ class SchedulingOverlap { required this.blockedInterval, }); + /// Flexible task that overlaps blocked time. final String taskId; + + /// The task's scheduled interval. final TimeInterval taskInterval; + + /// The blocked interval it overlaps. final TimeInterval blockedInterval; } /// Starter notice type returned by scheduling operations. +/// +/// A notice is presentation-friendly context about an operation. It intentionally +/// carries both text and a structured [type] so the UI can decide whether to show +/// it as neutral info, movement, overlap, or failure. class SchedulingNotice { const SchedulingNotice( this.message, { @@ -122,12 +205,22 @@ class SchedulingNotice { this.taskId, }); + /// Human-readable message safe to surface in UI or logs. final String message; + + /// Structured category for UI styling and tests. final SchedulingNoticeType type; + + /// Optional task related to this notice. Null means the notice applies to the + /// whole operation. final String? taskId; } /// Starter result wrapper for scheduling operations. +/// +/// Every engine operation returns a [SchedulingResult], even when nothing moved. +/// This keeps the call pattern predictable: always inspect `tasks`, then surface +/// any `notices`, `changes`, or `overlaps` relevant to the UI. class SchedulingResult { const SchedulingResult({ required this.tasks, @@ -136,16 +229,37 @@ class SchedulingResult { this.overlaps = const [], }); + /// Replacement task list after the operation. final List tasks; + + /// Human-readable operation messages. final List notices; + + /// Machine-readable movements or schedule clears. final List changes; + + /// Analysis-only overlap findings. final List overlaps; } /// Starter scheduling engine. /// -/// This is intentionally small. Codex should expand this according to the V1 -/// plan documents and add tests for every scheduling rule. +/// The engine is a pure domain service: it receives immutable-ish input values +/// and returns new values. It does not persist data, render UI, send reminders, +/// or read the clock except where an optional [updatedAt] timestamp is omitted. +/// +/// Current V1 responsibilities: +/// - insert backlog tasks into the earliest available flexible slot; +/// - push flexible tasks later today; +/// - move flexible tasks to tomorrow's queue; +/// - roll unfinished flexible tasks into a new planning window; +/// - analyze overlaps against locked/required time; +/// - perform small state transitions such as missed/backlog handling. +/// +/// Important rule vocabulary: +/// - `fixedBlocks` are intervals the engine will not move. +/// - `queue` is the ordered set of flexible tasks that may be placed or shifted. +/// - `placement` is a map from task id to the interval chosen by the planner. class SchedulingEngine { const SchedulingEngine(); @@ -154,11 +268,17 @@ class SchedulingEngine { /// Locked, inflexible, and critical time is treated as fixed. Planned /// flexible tasks at or after the insertion point may shift later, preserving /// their relative order. + /// + /// The selected task must already exist in [input.tasks], be flexible, be in + /// backlog status, and have a positive duration. If any precondition fails, the + /// original task list is returned with a no-fit/overflow notice. SchedulingResult insertBacklogTaskIntoNextAvailableSlot({ required SchedulingInput input, required String taskId, DateTime? updatedAt, }) { + // Step 1: resolve and validate the task. The engine does not create tasks; + // quick capture or persistence code is responsible for adding it to the list. final task = _taskById(input.tasks, taskId); if (task == null) { return _unchangedResult( @@ -194,6 +314,8 @@ class SchedulingEngine { ); } + // Step 2: compute placements without mutating any task. Planning returns null + // if the inserted task and shifted queue cannot fit inside the window. final placement = _planBacklogInsertion( input: input, task: task, @@ -223,11 +345,17 @@ class SchedulingEngine { /// /// The selected task moves after its current slot. Planned flexible tasks /// after it may shift later, preserving their relative order. + /// + /// This is the "not now, later today" action. Anything before the pushed + /// task's current end time becomes fixed for this operation, while later + /// planned flexible tasks may be shifted if necessary. SchedulingResult pushFlexibleTaskToNextAvailableSlot({ required SchedulingInput input, required String taskId, DateTime? updatedAt, }) { + // Resolve the selected task by id so UI code only needs to pass a stable + // identifier, not object references. final task = _taskById(input.tasks, taskId); if (task == null) { return _unchangedResult( @@ -304,6 +432,9 @@ class SchedulingEngine { /// /// The input window represents tomorrow's scheduling window. Existing planned /// flexible tasks in that window may shift later, preserving their order. + /// + /// The method name says "tomorrow" because that is the product action, but the + /// engine only trusts [input.window]. Tests can pass any future window. SchedulingResult pushFlexibleTaskToTomorrowTopOfQueue({ required SchedulingInput input, required String taskId, @@ -376,10 +507,17 @@ class SchedulingEngine { /// The input window represents tomorrow's scheduling window. Only planned and /// active flexible tasks are rolled; required, locked, completed, and /// cancelled tasks remain unchanged. + /// + /// Rollover is bulk push behavior for day-end recovery. It collects unfinished + /// flexible tasks outside the target window, preserves their relative order, + /// then places them at the start of the new window while shifting already + /// planned flexible tasks as needed. SchedulingResult rollOverUnfinishedFlexibleTasks({ required SchedulingInput input, DateTime? updatedAt, }) { + // Build the explicit queue of tasks to roll before asking the planner to + // place anything. This keeps selection separate from placement. final rolledItems = <_PlacementItem>[]; final rolloverTasks = input.flexibleTasks .where((task) => _shouldRollOver(task, input.window)) @@ -441,6 +579,10 @@ class SchedulingEngine { } /// Analyze the current in-memory schedule without moving tasks. + /// + /// This is a validation/debugging helper. It scans scheduled flexible tasks and + /// reports any overlap with blocked intervals. It deliberately returns the + /// original task list unchanged. SchedulingResult analyzeSchedule(SchedulingInput input) { final overlaps = []; final notices = []; @@ -483,7 +625,9 @@ class SchedulingEngine { /// Move a task to backlog. /// - /// Backlog does not preserve original schedule/order placement. + /// Backlog does not preserve original schedule/order placement. The task's + /// schedule is cleared and its moved-to-backlog counter is incremented so + /// reports can distinguish this from a task that was never scheduled. Task moveToBacklog(Task task, {DateTime? updatedAt}) { return task.copyWith( status: TaskStatus.backlog, @@ -494,6 +638,9 @@ class SchedulingEngine { } /// Mark a flexible task pushed manually. + /// + /// This updates statistics only. Use the push methods above when the task's + /// actual scheduled slot should change. Task markManuallyPushed(Task task, {DateTime? updatedAt}) { return task.copyWith( updatedAt: updatedAt ?? DateTime.now(), @@ -503,7 +650,9 @@ class SchedulingEngine { /// Mark missed according to the current MVP rules. /// - /// Critical missed tasks go to backlog. Inflexible missed tasks stay in place. + /// Critical missed tasks go to backlog so they remain actionable. Inflexible + /// missed tasks stay in place as missed because they represented a fixed event + /// or time block that cannot simply be rescheduled automatically. Task markMissed(Task task, {DateTime? updatedAt}) { final nextStats = task.stats.incrementMissed(); final now = updatedAt ?? DateTime.now(); @@ -527,8 +676,10 @@ class SchedulingEngine { /// Finds the first interval that can fit the requested duration while avoiding /// blocked intervals. /// - /// This helper is deliberately simple. Full flexible bump behavior belongs in - /// later plan chunks. + /// This public helper is deliberately simple and does not shift existing + /// flexible tasks. It is useful for UI previews or tests that only need to know + /// the first open gap. Full bump/queue behavior lives in the private planning + /// helpers below. TimeInterval? findFirstOpenInterval({ required DateTime windowStart, required DateTime windowEnd, @@ -560,6 +711,10 @@ class SchedulingEngine { } } +/// Convert a scheduled task into an interval, or null if it is unplaced. +/// +/// This helper does not validate positive duration; callers that require a valid +/// duration check that separately. TimeInterval? _scheduledIntervalFor(Task task) { final start = task.scheduledStart; final end = task.scheduledEnd; @@ -571,6 +726,7 @@ TimeInterval? _scheduledIntervalFor(Task task) { return TimeInterval(start: start, end: end, label: task.id); } +/// Convert all placed tasks in [tasks] into intervals. List _scheduledIntervalsFor(Iterable tasks) { final intervals = []; @@ -584,6 +740,10 @@ List _scheduledIntervalsFor(Iterable tasks) { return List.unmodifiable(intervals); } +/// Return the original task list with one explanatory notice. +/// +/// Most validation failures use this so callers can keep rendering the existing +/// schedule while showing why the requested action did not apply. SchedulingResult _unchangedResult( SchedulingInput input, SchedulingNotice notice, @@ -594,6 +754,7 @@ SchedulingResult _unchangedResult( ); } +/// Find a task by stable id. Task? _taskById(List tasks, String taskId) { for (final task in tasks) { if (task.id == taskId) { @@ -604,6 +765,7 @@ Task? _taskById(List tasks, String taskId) { return null; } +/// Convert a nullable minute estimate into a positive [Duration]. Duration? _durationFromMinutes(int? minutes) { if (minutes == null || minutes <= 0) { return null; @@ -612,11 +774,17 @@ Duration? _durationFromMinutes(int? minutes) { return Duration(minutes: minutes); } +/// Plan insertion of a backlog task plus any flexible tasks that must shift. +/// +/// This function only calculates intervals. It does not update task objects. The +/// returned plan is later applied by [_applyPlacement], which creates notices, +/// changes, and updated task copies. _BacklogInsertionPlan? _planBacklogInsertion({ required SchedulingInput input, required Task task, required Duration taskDuration, }) { + // Start with intervals that the algorithm is not allowed to move. final fixedBlocks = [ ...input.blockedIntervals, ]; @@ -628,6 +796,9 @@ _BacklogInsertionPlan? _planBacklogInsertion({ ), ]; + // Existing flexible tasks are inspected in timeline order. Planned tasks inside + // the movable portion become part of the placement queue; everything else is + // treated as fixed. final scheduledFlexibleTasks = input.flexibleTasks .where((flexibleTask) => flexibleTask.id != task.id) .toList(growable: false) @@ -669,6 +840,9 @@ _BacklogInsertionPlan? _planBacklogInsertion({ fixedBlocks.sort((a, b) => a.start.compareTo(b.start)); + // The cursor tracks the earliest point after the previously placed queue item. + // Each queued task is placed no earlier than both the cursor and its own + // original earliest start. var cursor = input.window.start; final placements = {}; @@ -692,6 +866,11 @@ _BacklogInsertionPlan? _planBacklogInsertion({ return _BacklogInsertionPlan(placements: placements); } +/// Plan the "push later today" behavior for one flexible task. +/// +/// Items before the pushed task's current end are fixed. The pushed task starts +/// the queue at its current end, followed by later planned flexible tasks that +/// may need to move to preserve order. _BacklogInsertionPlan? _planFlexiblePush({ required SchedulingInput input, required Task task, @@ -772,6 +951,7 @@ _BacklogInsertionPlan? _planFlexiblePush({ return _BacklogInsertionPlan(placements: placements); } +/// Plan putting a single task at the start of the supplied future window. _BacklogInsertionPlan? _planTomorrowQueueInsertion({ required SchedulingInput input, required Task task, @@ -792,6 +972,11 @@ _BacklogInsertionPlan? _planTomorrowQueueInsertion({ ); } +/// Plan a queue of flexible tasks at the beginning of [input.window]. +/// +/// This is shared by tomorrow push and bulk rollover. [excludeTaskIds] identifies +/// tasks already represented in the incoming [queue] so they are not also pulled +/// from existing scheduled flexible tasks. _BacklogInsertionPlan? _planQueueAtWindowStart({ required SchedulingInput input, required List<_PlacementItem> queue, @@ -865,6 +1050,11 @@ _BacklogInsertionPlan? _planQueueAtWindowStart({ return _BacklogInsertionPlan(placements: placements); } +/// Apply a backlog insertion plan to the task list. +/// +/// The inserted backlog task becomes planned and increments +/// `restoredFromBacklogCount`; any existing flexible tasks moved to make room +/// increment `autoPushedCount`. SchedulingResult _applyPlacement({ required SchedulingInput input, required Task insertedTask, @@ -930,6 +1120,11 @@ SchedulingResult _applyPlacement({ ); } +/// Apply a push/tomorrow placement plan to the task list. +/// +/// The explicitly pushed task increments `manuallyPushedCount`; other moved +/// flexible tasks increment `autoPushedCount` because the scheduler moved them as +/// a side effect. SchedulingResult _applyPushPlacement({ required SchedulingInput input, required Task pushedTask, @@ -992,6 +1187,10 @@ SchedulingResult _applyPushPlacement({ ); } +/// Apply a bulk rollover placement plan. +/// +/// Rolled tasks are set back to planned status in the target window. Existing +/// tasks moved to make room receive normal movement notices. SchedulingResult _applyRolloverPlacement({ required SchedulingInput input, required Set rolledTaskIds, @@ -1066,6 +1265,10 @@ SchedulingResult _applyRolloverPlacement({ ); } +/// Whether [task] belongs in the rollover queue. +/// +/// Planned/active flexible tasks already inside the target window are not rolled +/// again; they are handled as existing tasks that may shift to make room. bool _shouldRollOver(Task task, SchedulingWindow tomorrowWindow) { final interval = _scheduledIntervalFor(task); final isTomorrowTask = @@ -1076,6 +1279,7 @@ bool _shouldRollOver(Task task, SchedulingWindow tomorrowWindow) { (task.status == TaskStatus.planned || task.status == TaskStatus.active); } +/// Return whichever timestamp is later. DateTime _laterOf(DateTime first, DateTime second) { if (first.isAfter(second)) { return first; @@ -1084,6 +1288,7 @@ DateTime _laterOf(DateTime first, DateTime second) { return second; } +/// Null-safe exact timestamp comparison. bool _sameDateTime(DateTime? first, DateTime? second) { if (first == null || second == null) { return first == null && second == null; @@ -1092,6 +1297,12 @@ bool _sameDateTime(DateTime? first, DateTime? second) { return first.isAtSameMomentAs(second); } +/// Find the first candidate interval at or after [earliestStart]. +/// +/// The scan assumes [blocked] is sorted by start time. When a candidate overlaps +/// a blocked interval, the cursor jumps to that blocked interval's end and tries +/// again. This makes the algorithm easy to follow and adequate for the starter +/// in-memory engine. TimeInterval? _firstOpenIntervalFrom({ required DateTime earliestStart, required DateTime windowEnd, @@ -1133,6 +1344,11 @@ TimeInterval? _firstOpenIntervalFrom({ } } +/// One item in a placement queue. +/// +/// [earliestStart] preserves a task's natural ordering constraint. For existing +/// scheduled tasks, this is usually their current start; for a pushed task, it is +/// the earliest time the push operation allows. class _PlacementItem { const _PlacementItem({ required this.task, @@ -1140,13 +1356,24 @@ class _PlacementItem { required this.earliestStart, }); + /// Task represented by this queue entry. final Task task; + + /// Duration the planner must reserve. final Duration duration; + + /// Earliest allowed start time for this item. final DateTime earliestStart; } +/// Planned task intervals keyed by task id. +/// +/// The name is historical from the first insertion feature; it now also supports +/// push and rollover placement plans. It remains private so it can be renamed or +/// expanded later without affecting callers. class _BacklogInsertionPlan { const _BacklogInsertionPlan({required this.placements}); + /// Chosen interval for each task that should be scheduled or moved. final Map placements; } diff --git a/lib/src/task_actions.dart b/lib/src/task_actions.dart index 0f219b5..9b4ad33 100644 --- a/lib/src/task_actions.dart +++ b/lib/src/task_actions.dart @@ -1,22 +1,53 @@ +// Flexible task card actions. +// +// The main scheduling engine moves tasks through time. This file models the +// small user actions that appear on a flexible task card, such as done, push, +// backlog, and break-up. Keeping these in a service makes UI button handlers +// thin and keeps task-type safety checks in one place. + import 'models.dart'; import 'scheduling_engine.dart'; /// Quick actions available from a flexible task card. +/// +/// These are the low-friction card controls the UI can expose directly on a +/// planned flexible task. The service below translates each button into either a +/// direct task update, a scheduling operation, or a follow-up flow. enum FlexibleTaskQuickAction { + /// Mark the task completed. done, + + /// Ask the user where the task should be pushed. push, + + /// Move the task out of today's plan and into backlog. backlog, + + /// Start a flow that splits the task into child tasks. breakUp, } /// Explicit push destinations shown after choosing the push quick action. +/// +/// Push starts as a simple quick action, but the actual destination requires one +/// more choice. Keeping destinations as a separate enum prevents the initial card +/// action list from becoming too crowded. enum PushDestination { + /// Move the task later within the current planning window. nextAvailableSlot, + + /// Move the task to the beginning of the supplied tomorrow/future window. tomorrowTopOfQueue, + + /// Remove the task from the active timeline and store it for later. backlog, } /// Domain result for a flexible task quick action. +/// +/// This result deliberately supports three outcomes: the task changed, the user +/// must choose a push destination, or the UI should start the child-task flow. +/// That keeps card code from guessing how to interpret each action. class FlexibleTaskActionResult { const FlexibleTaskActionResult({ required this.action, @@ -25,37 +56,65 @@ class FlexibleTaskActionResult { this.startsChildTaskFlow = false, }); + /// Action the user selected. final FlexibleTaskQuickAction action; + + /// Current or updated task, depending on the action. final Task task; + + /// Destination choices to show after `push`; empty for direct actions. final List pushDestinations; + + /// Whether the UI should open a child-task creation flow. final bool startsChildTaskFlow; + /// True when the action directly produced an updated [task]. bool get changedTask => !startsChildTaskFlow && pushDestinations.isEmpty; } /// Result from applying a selected push destination. +/// +/// The selected destination is included next to the [SchedulingResult] so UI and +/// tests can distinguish "moved later today" from "moved to tomorrow" even if +/// the low-level scheduling change shape is similar. class PushDestinationResult { const PushDestinationResult({ required this.destination, required this.schedulingResult, }); + /// Destination that was applied. final PushDestination destination; + + /// Full scheduler output: updated tasks, notices, changes, and overlaps. final SchedulingResult schedulingResult; + /// Convenience flag for UI copy or persistence behavior that cares about the + /// tomorrow queue specifically. bool get placesAtTomorrowTopOfQueue { return destination == PushDestination.tomorrowTopOfQueue; } } /// Applies low-friction quick actions for flexible task cards. +/// +/// This service is the adapter between small UI button presses and domain logic. +/// It intentionally only accepts flexible tasks; required/locked/surprise items +/// should have their own action rules so the UI cannot accidentally apply a +/// flexible-only behavior to a fixed commitment. class FlexibleTaskActionService { const FlexibleTaskActionService({ this.schedulingEngine = const SchedulingEngine(), }); + /// Scheduling dependency used for actions that need timeline changes. final SchedulingEngine schedulingEngine; + /// Apply the first-stage quick action. + /// + /// Direct actions (`done`, `backlog`) return a changed task. `push` returns the + /// list of destinations the UI should present. `breakUp` signals that the UI + /// should start a child-task flow rather than changing the task immediately. FlexibleTaskActionResult apply({ required Task task, required FlexibleTaskQuickAction action, @@ -99,6 +158,10 @@ class FlexibleTaskActionService { } } + /// Apply the second-stage destination selected after the `push` action. + /// + /// This needs the full [SchedulingInput] because pushing can shift other + /// flexible tasks and must avoid locked/required intervals. PushDestinationResult applyPushDestination({ required PushDestination destination, required SchedulingInput input, @@ -131,6 +194,10 @@ class FlexibleTaskActionService { ); } + /// Move one planned flexible task to backlog inside a scheduling result. + /// + /// This mirrors the shape of other push destination results so callers can + /// handle every destination through the same `SchedulingResult` interface. SchedulingResult _moveTaskToBacklog({ required SchedulingInput input, required String taskId, @@ -198,6 +265,7 @@ class FlexibleTaskActionService { } } +/// Find one task by id in a list. Task? _taskById(List tasks, String taskId) { for (final task in tasks) { if (task.id == taskId) { diff --git a/lib/src/task_statistics.dart b/lib/src/task_statistics.dart index 7ccae3a..e9e379e 100644 --- a/lib/src/task_statistics.dart +++ b/lib/src/task_statistics.dart @@ -1,8 +1,20 @@ +// Quiet per-task history counters. +// +// The app design calls for recovery/reporting features that can notice repeated +// pushes, backlog moves, burnout skips, and locked-hour leakage. This file keeps +// that history as immutable counters attached to each task. + /// Internal counters used for future filtering, reporting, and scheduling hints. /// -/// These are intentionally quiet metadata. They should not become noisy UI by -/// default. This is a public starter placeholder while persistence and reporting -/// needs are still being shaped by the V1 plan. +/// These counters answer "what has happened to this task over time?" without +/// changing the primary task state. They are intentionally quiet metadata: daily +/// task cards should not show all of this by default, but reports and filters can +/// use it to identify patterns such as tasks repeatedly pushed, skipped during +/// burnout, or completed inside locked hours. +/// +/// Like [Task], this is immutable. Increment helpers return a new +/// [TaskStatistics] value so calling code can update a task through `copyWith` +/// while retaining predictable before/after behavior. class TaskStatistics { const TaskStatistics({ this.skippedDuringBurnoutCount = 0, @@ -17,17 +29,40 @@ class TaskStatistics { this.completedDuringLockedHoursMinutes = 0, }); + /// Number of times this task was skipped during Shield/recovery behavior. final int skippedDuringBurnoutCount; + + /// Number of times the user explicitly pushed this task. final int manuallyPushedCount; + + /// Number of times the scheduler moved this task to make room for something. final int autoPushedCount; + + /// Number of times this task was moved from schedule/today into backlog. final int movedToBacklogCount; + + /// Number of times this task came back from backlog into a planned slot. final int restoredFromBacklogCount; + + /// Number of times this task missed its intended timing. final int missedCount; + + /// Number of times this task was intentionally cancelled. final int cancelledCount; + + /// Number of times this task was completed after its scheduled window. final int completedLateCount; + + /// Number of completion events that overlapped locked hours. final int completedDuringLockedHoursCount; + + /// Total minutes completed while overlapping locked hours. final int completedDuringLockedHoursMinutes; + /// Return a copy with selected counters changed. + /// + /// Counters default to their current values when omitted, which keeps small + /// increment helpers concise and avoids direct mutation. TaskStatistics copyWith({ int? skippedDuringBurnoutCount, int? manuallyPushedCount, @@ -58,38 +93,50 @@ class TaskStatistics { ); } + /// Record that the task was removed from active planning and stored for later. TaskStatistics incrementMovedToBacklog() { return copyWith(movedToBacklogCount: movedToBacklogCount + 1); } + /// Record a recovery/Shield skip. This is distinct from manual cancellation. TaskStatistics incrementSkippedDuringBurnout() { return copyWith(skippedDuringBurnoutCount: skippedDuringBurnoutCount + 1); } + /// Record an explicit user push action. TaskStatistics incrementManualPush() { return copyWith(manuallyPushedCount: manuallyPushedCount + 1); } + /// Record scheduler-driven movement caused by another placement. TaskStatistics incrementAutoPush() { return copyWith(autoPushedCount: autoPushedCount + 1); } + /// Record that a backlog item was scheduled again. TaskStatistics incrementRestoredFromBacklog() { return copyWith(restoredFromBacklogCount: restoredFromBacklogCount + 1); } + /// Record a missed intended time or required task handling event. TaskStatistics incrementMissed() { return copyWith(missedCount: missedCount + 1); } + /// Record that the task was deliberately cancelled. TaskStatistics incrementCancelled() { return copyWith(cancelledCount: cancelledCount + 1); } + /// Record that the task was completed after its planned end. TaskStatistics incrementCompletedLate() { return copyWith(completedLateCount: completedLateCount + 1); } + /// Record completion that overlapped locked time by [minutes]. + /// + /// Both count and minutes are tracked because reports may want either "how + /// often did this happen?" or "how much time leaked into locked blocks?" TaskStatistics incrementCompletedDuringLockedHours(int minutes) { return copyWith( completedDuringLockedHoursCount: completedDuringLockedHoursCount + 1,