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 0000000..5d4c631 Binary files /dev/null and b/adhd_scheduling_starter_project.7z differ 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,