forked from eva/focus-flow
docs(project): commit current repository state
This commit is contained in:
parent
f7f239c3f7
commit
b4e321da02
22 changed files with 2015 additions and 38 deletions
13
AGENTS.md
13
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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
```
|
||||
|
|
@ -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
|
||||
```
|
||||
|
|
@ -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
|
||||
```
|
||||
|
|
@ -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
|
||||
```
|
||||
|
|
@ -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
|
||||
```
|
||||
|
|
@ -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:
|
||||
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
```
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
27
DOCUMENTATION_PASS_SUMMARY.md
Normal file
27
DOCUMENTATION_PASS_SUMMARY.md
Normal file
|
|
@ -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.
|
||||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
BIN
adhd_scheduling_starter_project.7z
Normal file
BIN
adhd_scheduling_starter_project.7z
Normal file
Binary file not shown.
|
|
@ -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/`.
|
||||
|
|
|
|||
|
|
@ -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<Task> 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<Task> 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<Task> 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<Task> sorted(BacklogSortKey sortKey) {
|
||||
final sortedTasks = [...backlogTasks];
|
||||
sortedTasks.sort((a, b) => _compareTasks(a, b, sortKey));
|
||||
|
|
@ -85,10 +170,15 @@ class BacklogView {
|
|||
return List<Task>.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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<LockedWeekday> 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<LockedBlockOccurrence> occurrences;
|
||||
|
||||
/// Just the intervals the scheduler needs to avoid.
|
||||
List<TimeInterval> get schedulingIntervals {
|
||||
return List<TimeInterval>.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<LockedBlock> blocks,
|
||||
required List<LockedBlockOverride> 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<LockedBlockOverride> overrides,
|
||||
) {
|
||||
|
|
@ -339,6 +482,7 @@ LockedBlockOverride? _lastReplacementOverride(
|
|||
return null;
|
||||
}
|
||||
|
||||
/// Convenience wrapper when callers only need scheduler intervals.
|
||||
List<TimeInterval> lockedSchedulingIntervalsForDay({
|
||||
required List<LockedBlock> blocks,
|
||||
required List<LockedBlockOverride> overrides,
|
||||
|
|
@ -352,6 +496,10 @@ List<TimeInterval> 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<TimeInterval> 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<TimeInterval> 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 &&
|
||||
|
|
|
|||
|
|
@ -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<BacklogTag> 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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 <BacklogTag>{},
|
||||
});
|
||||
|
||||
/// 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<BacklogTag> 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 <String>[],
|
||||
});
|
||||
|
||||
/// 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<String> 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<Task> tasks, String id) {
|
||||
for (final task in tasks) {
|
||||
if (task.id == id) {
|
||||
|
|
|
|||
|
|
@ -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 <TimeInterval>[],
|
||||
});
|
||||
|
||||
/// All tasks available to this operation. The scheduler returns a replacement
|
||||
/// list rather than mutating this one.
|
||||
final List<Task> 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<TimeInterval> 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<TimeInterval> requiredVisibleIntervals;
|
||||
|
||||
/// Tasks that the flexible movement algorithms are allowed to consider.
|
||||
List<Task> 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<Task> get lockedTasks {
|
||||
return tasks.where((task) => task.isLocked).toList(growable: false);
|
||||
}
|
||||
|
||||
/// Critical and inflexible task records that should block flexible placement.
|
||||
List<Task> get requiredVisibleTasks {
|
||||
return tasks
|
||||
.where((task) => task.isRequiredVisible)
|
||||
.toList(growable: false);
|
||||
}
|
||||
|
||||
/// Scheduled intervals for flexible tasks only. Useful for analysis/debugging.
|
||||
List<TimeInterval> 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<TimeInterval> get blockedIntervals {
|
||||
final intervals = <TimeInterval>[
|
||||
...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 <SchedulingOverlap>[],
|
||||
});
|
||||
|
||||
/// Replacement task list after the operation.
|
||||
final List<Task> tasks;
|
||||
|
||||
/// Human-readable operation messages.
|
||||
final List<SchedulingNotice> notices;
|
||||
|
||||
/// Machine-readable movements or schedule clears.
|
||||
final List<SchedulingChange> changes;
|
||||
|
||||
/// Analysis-only overlap findings.
|
||||
final List<SchedulingOverlap> 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 = <SchedulingOverlap>[];
|
||||
final notices = <SchedulingNotice>[];
|
||||
|
|
@ -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<TimeInterval> _scheduledIntervalsFor(Iterable<Task> tasks) {
|
||||
final intervals = <TimeInterval>[];
|
||||
|
||||
|
|
@ -584,6 +740,10 @@ List<TimeInterval> _scheduledIntervalsFor(Iterable<Task> tasks) {
|
|||
return List<TimeInterval>.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<Task> tasks, String taskId) {
|
||||
for (final task in tasks) {
|
||||
if (task.id == taskId) {
|
||||
|
|
@ -604,6 +765,7 @@ Task? _taskById(List<Task> 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 = <TimeInterval>[
|
||||
...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 = <String, TimeInterval>{};
|
||||
|
||||
|
|
@ -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<String> 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<String, TimeInterval> placements;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<PushDestination> 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<Task> tasks, String taskId) {
|
||||
for (final task in tasks) {
|
||||
if (task.id == taskId) {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Reference in a new issue