summaryrefslogtreecommitdiff
path: root/docs/superpowers/specs/2026-07-15-recurring-maintenance-buckets-design.md
diff options
context:
space:
mode:
Diffstat (limited to 'docs/superpowers/specs/2026-07-15-recurring-maintenance-buckets-design.md')
-rw-r--r--docs/superpowers/specs/2026-07-15-recurring-maintenance-buckets-design.md70
1 files changed, 70 insertions, 0 deletions
diff --git a/docs/superpowers/specs/2026-07-15-recurring-maintenance-buckets-design.md b/docs/superpowers/specs/2026-07-15-recurring-maintenance-buckets-design.md
new file mode 100644
index 0000000..5072643
--- /dev/null
+++ b/docs/superpowers/specs/2026-07-15-recurring-maintenance-buckets-design.md
@@ -0,0 +1,70 @@
+# Recurring Maintenance Buckets — Design
+
+**Status:** Backlog idea, refined via interview 2026-07-15. Not yet approved for implementation.
+
+## Context
+
+For longer-term items that need doing "every so often" but don't each deserve their own due date (e.g. "clean the gutters," "check the water heater," "review the emergency kit") — a bucket holds a pool of such items, and every M days/weeks the system activates N of them as real, actionable tasks. Completing (or deferring) an active item returns it to the pool; the interview's own framing calls for "room for fanciness" in the selection.
+
+**Depends on:** the task-recurrence-and-detail-editing feature (already shipped) for its periodic-scheduler infrastructure, and optionally on [[doot-future-task-scheduling-ideas]] items 1 (availability) and 2 (labels) for scoring inputs, per the interview.
+
+## Architecture Note: Different Shape Than Due-Date Recurrence
+
+The existing recurrence system (shipped 2026-07-14) creates a **new row per iteration** — each occurrence is a distinct, permanent history entry. That model fits a task that repeats indefinitely on its own schedule.
+
+A bucket is different: it's a **finite, fixed pool of items** that get **repeatedly activated and deactivated** — the item itself doesn't need a fresh row each cycle, it needs a state flip. So this feature reuses the *scheduler infrastructure* (a periodic background check, same pattern as `scheduler.RunRecurrenceCheck`) but not the *new-row-per-iteration* mechanism. This is the "new recurrence variant layered onto the existing system" from the interview — same periodic-check shape, different iteration semantics.
+
+## Data Model
+
+```sql
+CREATE TABLE maintenance_buckets (
+ id TEXT PRIMARY KEY,
+ name TEXT NOT NULL,
+ cycle_days INTEGER NOT NULL, -- M, expressed in days (a "weekly" bucket = 7, "every 3 weeks" = 21)
+ pick_n INTEGER NOT NULL, -- N items activated per cycle
+ last_cycle_at DATETIME,
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP
+);
+
+ALTER TABLE native_tasks ADD COLUMN bucket_id TEXT DEFAULT '';
+ALTER TABLE native_tasks ADD COLUMN bucket_state TEXT DEFAULT ''; -- '', 'dormant', 'active'
+ALTER TABLE native_tasks ADD COLUMN bucket_last_active_at DATETIME; -- drives staleness scoring
+```
+
+Bucket items are ordinary `native_tasks` rows (so they appear in existing lookups/UI naturally) with `bucket_id` set and `bucket_state` cycling between `'dormant'` (in the pool, no `due_date`, invisible to normal task views) and `'active'` (selected this cycle, has a computed `due_date`, shows up everywhere a normal task would).
+
+Per the interview: no fixed due dates on dormant items — the cycle length is the only timing concept. Exactly one bucket per item (`bucket_id` is a single value, not a list).
+
+## Selection Mechanism
+
+Every `cycle_days`, a periodic check (extending the existing scheduler) for each bucket:
+1. Finds all `'dormant'` items with that `bucket_id`.
+2. Scores each — staleness (time since `bucket_last_active_at`; never-activated items score highest), task priority, and (if the item has a label/project with an availability budget, per [[doot-future-task-scheduling-ideas]] item 1) how well it fits current capacity. Exact scoring formula is an implementation-time decision, not fixed here — the interview confirmed "weighted/scored," not a specific weighting.
+3. Selects the top `pick_n`, flips them to `'active'`, sets `due_date = now + cycle_days` (giving the full cycle window to complete it), sets `bucket_last_active_at = now`.
+4. Updates `last_cycle_at` on the bucket.
+
+**Deferring** an active item (a new "Defer" action distinct from complete/reschedule): flips it back to `'dormant'` with `bucket_last_active_at` left at its prior value (so it's still relatively stale and likely to be reselected soon), then **immediately triggers a fresh selection** for that bucket to backfill the freed slot — matching the interview's "defer one and get a new selection" requirement.
+
+**Completing** an active item: flips it back to `'dormant'` too, but sets `bucket_last_active_at = now` (just done, so it naturally cools down in the staleness scoring before being picked again) — distinct from deferring, where nothing was actually accomplished. This distinction wasn't explicitly asked in the interview; it's a reasonable design choice to revisit if it doesn't match intuition in practice.
+
+## API
+
+- `GET/POST /api/widget/buckets` — CRUD for buckets (name, cycle_days, pick_n).
+- `POST /api/widget/buckets/{id}/items` — add an existing or new task to a bucket's pool (sets `bucket_id`, `bucket_state = 'dormant'`).
+- `POST /api/widget/task/defer` — `{id}`, the new defer action (distinct from complete/reschedule) for active bucket items.
+- No dedicated "trigger selection" endpoint needed for normal operation (the periodic check handles it) — but the defer action needs to synchronously trigger a re-selection for its bucket, reusing the same selection logic as a library function, not a new job.
+
+## UI
+
+Not designed in detail here — the interview didn't cover UI specifics for buckets. At minimum: a bucket needs a management view (create bucket, add/remove pool items, see cycle settings) and active bucket items need a visible "Defer" action alongside the existing Complete action, likely in the task-detail popup ([[doot-future-task-scheduling-ideas]] item 2's popup, or a bucket-specific variant).
+
+## Testing
+
+Store-layer tests for the selection algorithm (given a pool with known staleness/priority values, confirm the expected N are selected), the defer-triggers-reselection flow, and complete-vs-defer's differing `bucket_last_active_at` handling. Scheduler test for the periodic per-bucket cycle check, mirroring the existing `AdvanceDueRecurringTasks` test style.
+
+## Out of Scope
+
+- Exact scoring formula/weights — left as an implementation-time decision.
+- UI design.
+- Multi-bucket item membership (exactly one bucket per item, per the interview).
+- Any interaction with the linear-task-chains feature ([[doot-future-task-scheduling-ideas]] item 4) — buckets and chains are independent concepts, not designed to compose.