Files
failwell/tasks/plan.md
Julien Calixte 0c0d555ac1 docs: add implementation plan to align code with feedback loop ADR
Vertical-slice plan with five slices across four phases, each with
acceptance criteria, manual verification, and human-review checkpoints
between phases. Surfaces and orders the inconsistencies between the
current code and the per-task loop language. Flat todo.md mirrors the
plan as a checkable task list.
2026-05-15 11:26:06 +02:00

17 KiB

Plan — Align code with the Per-Task Feedback Loop

This plan implements the decisions recorded in docs/adr/0001-per-task-feedback-loop.md and the language in CONTEXT.md. It closes the inconsistencies surfaced during the deep-design session.

Each slice is vertical: it touches model → store → UI → tests for one user-visible behaviour, rather than refactoring a layer at a time. Phases gate on checkpoints.

Goals (recap)

  1. The Failure Signal compares actual durations against the Initial Plan, defined as the Plan version active at the moment Execution begins.
  2. Re-planning (mid-Execution structural Plan changes) preserves every Step Record whose Step still exists by id.
  3. The loop closes in a dedicated Closing Ceremony that names three Failure modes: Estimation Variance, Discovered Scope, Abandoned Scope.
  4. Re-estimation (changing a number) and Re-planning (changing the shape) are distinct affordances.

Dependency graph

Phase 1 — Foundation
  ├─ Slice 1 (Initial Plan capture)     ──┐
  └─ Slice 2 (Preserve records on re-plan)─┴─▶ Phase 2

Phase 2 — Closing the loop
  └─ Slice 3 (Closing Ceremony view)      ──▶ Phase 3

Phase 3 — UX split
  └─ Slice 4 (Re-estimation inline / Re-planning modal)  ──▶ Phase 4

Phase 4 — Cleanup
  └─ Slice 5 (Model & naming hygiene)

Slice 4 is technically parallelisable with Slice 3 once Slice 2 lands, but is sequenced after Slice 3 to keep the Closing Ceremony available as the source of truth while reshaping the during-Execution UI.


Phase 1 — Foundation

Slice 1 — Anchor the Initial Plan at Start-of-Execution

Goal. The Recordable stores the Plan that was active when its first Step Record began. The Failure Signal reads from this snapshot, not from the current Plan.

Files in scope.

  • src/modules/record/interfaces/recordable.ts — add initialPlan: Stepable[] | null.
  • src/modules/record/models/task-record.ts — initialize and round-trip initialPlan in fromRecordable.
  • src/modules/record/stores/useTaskRecordStore.ts:50-85 — in startStepRecord, when stepRecords is empty, snapshot task.steps into record.initialPlan (only if it isn't already set).
  • src/modules/record/components/StepRecord.vue:66-69 — replace step.value.estimation with the Initial Plan's estimation for the same Step id; fall back to step.value.estimation only when no match.
  • src/modules/record/components/RecordResume.vue:14-20 — total comparison uses sum of Initial Plan estimations, not task.totalEstimation (which reflects current Plan).
  • src/modules/record/services/compare-with-estimation.ts — keep as is (still pure); update its callers.
  • Tests: extend task-record.test.ts, compare-with-estimation.test.ts, and add new unit tests for the snapshot behaviour.

Acceptance criteria.

  • Pressing Start (first Step Record) captures task.steps into record.initialPlan. Verified in a unit test against the store.
  • Re-estimating a Step during Execution does not mutate record.initialPlan.
  • The "off by 10%" indicator on a Step compares against the Initial Plan's estimation for that Step id.
  • Existing persisted records (without initialPlan) gracefully fall back to task.stepHistory[0]. This fallback path is covered by a unit test.
  • pnpm test passes (lint + types + unit).

Manual verification.

  • Create a Task with Steps at 5 / 10 / 10 min.
  • Start recording. Re-estimate the first Step to 30 min via the existing edit modal.
  • The first row's "off" indicator continues to reference 5 min (not 30) once the Step has elapsed more than ~30 seconds.

Risks / migration.

  • Persisted localStorage data lacks initialPlan. Fallback covers display; new data will populate it. Document the fallback in a code comment.
  • RecordResume total now diverges from task.totalEstimation if Re-planning occurred. Acceptable — see Slice 3 for full surface.

Slice 2 — Preserve Step Records when the Plan changes mid-Execution

Goal. Re-planning during Execution never destroys recorded time. Step Records keyed by id survive Plan edits; Steps removed from the Plan keep their orphan records for the Closing Ceremony to inspect.

Files in scope.

  • src/modules/record/stores/useTaskRecordStore.ts:193-225 — replace cleanCurrentStepId with a non-destructive reconciliation:
    • Find the first Step in the new Plan that has no end in the existing records. That becomes the new currentStepId.
    • If that Step has no existing record yet, start a new one from the latest end-of-completed-step time (or Date.now() if none).
    • Do not delete any existing Step Record. Orphan records (no longer in the Plan) remain in stepRecords.
  • src/modules/record/stores/useTaskRecordStore.ts:20-39syncTaskRecord currently wipes the whole record when any recorded Step id is missing from the Plan. Replace with the same non-destructive reconciliation.
  • Tests: add scenarios in useTaskRecordStore.test.ts (new file if missing) for reorder, delete-in-progress, delete-completed, insert-at-start.

Acceptance criteria.

  • Reorder during recording — start times of all completed Steps preserved; in-progress Step's start preserved.
  • Delete a completed Step during recording — its record is preserved as an orphan in stepRecords (still keyed by id, not in current task.steps).
  • Delete the in-progress Step — its record preserved; the new currentStepId is the next un-finished Step in the new Plan.
  • Insert a brand-new Step at the start — no existing records mutated; the new Step has no record yet.
  • pnpm test passes.

Manual verification.

  • During recording, open the edit modal, reorder steps. After saving, recorded times are unchanged in the table.
  • Open browser devtools → Application → Local Storage. Confirm orphan records remain after deleting a Step from the Plan.

Risks / migration.

  • Orphan records change stepRecords from "subset of current Steps" to "history of all Steps that ever had a record." Any consumer that iterates stepRecords and assumes membership in task.steps must be audited:
    • RecordResume.vue → uses useTaskRecordMetadata (total duration). Acceptable since orphan time was spent.
    • StepRecord.vue is rendered per task.steps[i], so it skips orphans naturally.
    • useTaskRecordMetadata — audit during this slice.

CHECKPOINT 1 — after Phase 1

Stop. Verify:

  • pnpm test green.
  • Manual smoke: full Task lifecycle (create → record → finish) with Re-estimation and Re-planning mid-flight; localStorage shows preserved initialPlan and preserved Step Records.
  • No regression in existing per-Step "off" coloring for un-edited Tasks.

Reviewer (human): confirm Foundation matches CONTEXT.md semantics before proceeding to Phase 2.


Phase 2 — Closing the loop

Slice 3 — Closing Ceremony view (three Failure modes)

Goal. A dedicated end-of-Task view that surfaces the three Failure modes, hosts the Notes textarea for post-Execution reflection, and replaces the current adjacent-version diff view at /task/:id/history.

Files in scope.

  • New: src/modules/record/services/compute-failure-modes.ts — pure function:
    computeFailureModes({ initialPlan, finalPlan, stepRecords }) → {
      estimationVariance: Array<{ stepId, title, estimation, actual, deltaMin, isOff }>,
      discoveredScope: Array<{ stepId, title, estimation, actual? }>,
      abandonedScope: Array<{ stepId, title, estimation }>,
      totalActualMin, totalInitialMin, totalDeltaMin
    }
    
    with unit tests covering each mode in isolation and combined.
  • Rename src/views/task/TaskHistory.vueTaskClosingCeremony.vue. Body fully replaced.
  • src/router/index.ts — repoint the history-task route at the new component. Keep the path /task/:id/history for backwards-compatibility of bookmarks (rename the route name to task-closing-ceremony to match vocabulary).
  • src/modules/record/components/TaskRecord.vue — when record.end is set, surface a "Close the loop" CTA linking to the Closing Ceremony route.
  • src/modules/record/components/RecordResume.vue — keep the one-line summary as a tease, but defer the rich breakdown to the new view.
  • Tests: unit tests on compute-failure-modes, component test smoke on TaskClosingCeremony.

Open design questions (resolve during this slice).

  1. How are orphan Step Records (Steps removed from the Plan but with executed time) surfaced? Options: (a) hidden, (b) as a fourth "Discarded Work" section, (c) folded into Abandoned Scope. Default in the plan: hidden, with the time still contributing to the totals. Revisit if user feedback contradicts.
  2. Does completing a Task auto-navigate to the Closing Ceremony, or is it CTA-only? Default: CTA-only — auto-navigation disrupts the during-Execution view if the user is mid-thought. Reflection should be a deliberate transition.

Acceptance criteria.

  • computeFailureModes returns the three buckets correctly given fixture inputs covering: no re-planning (only Estimation Variance), Step added mid-flight (Discovered Scope present), Step deleted from Initial Plan (Abandoned Scope present), all three at once.
  • The new view renders all three buckets, hiding empty ones.
  • The Notes textarea on the new view binds to the same Pinia field as during-Execution Notes (single source — no duplication).
  • The route /task/:id/history no longer renders an adjacent-version diff.
  • pnpm test passes.

Manual verification.

  • Complete a Task with no Plan changes → only Estimation Variance shown.
  • Complete a Task where you added a Step mid-flight → Discovered Scope section appears with that Step.
  • Complete a Task where you deleted a planned Step before executing it → Abandoned Scope section appears.
  • Notes typed during Execution are visible on the Closing Ceremony view, and edits there persist back.

Risks / migration.

  • Removing the diff view eliminates a feature, however lightly-used. Acceptable per the deep-design session (declared "dead view").
  • Bookmark stability: route path preserved.

CHECKPOINT 2 — after Phase 2

Stop. Verify:

  • pnpm test green.
  • End-to-end loop demoable: create → start → execute → re-plan mid-flight → finish → open Closing Ceremony → three modes correct.

Reviewer (human): confirm the Closing Ceremony surface matches intent before reshaping the during-Execution UI.


Phase 3 — UX split

Slice 4 — Re-estimation inline; Re-planning deliberate

Goal. Re-estimating a Step's number does not require opening the structural-edit modal. Re-planning becomes the modal's explicit purpose with framing that matches its weight.

Files in scope.

  • src/modules/record/components/StepRecord.vue — extend the existing inline-edit pattern (already present for duration on completed Steps, see lines around the isEditing ref) to cover estimation for any Step. Pressing Enter commits; Esc cancels.
  • New store action in useTask.store.ts: reEstimateStep({ taskId, stepId, newEstimationMinutes }). Implementation: push a new entry to stepHistory containing the current Plan with only that Step's estimation updated. The new entry does not disturb record.initialPlan (which is already snapshotted per Slice 1).
  • src/modules/task/components/NewStepsForm.vue — rename heading from "Edit steps" to "Re-plan steps" (or similar weighty phrasing). Update the explanatory text to reflect that this is a structural change. Keep the existing flow for actually adding/removing/reordering.
  • src/modules/record/components/RecordControls.vue:170-176 — the "+" button: relabel/icon-update to "Re-plan" (not "+"); the action is unchanged.
  • Tests: store test for reEstimateStep; component test for StepRecord inline editor.

Acceptance criteria.

  • Tapping/clicking the estimation cell on a Step row opens an inline editor.
  • Committing the edit triggers reEstimateStep, which pushes a new stepHistory entry.
  • record.initialPlan is not mutated by re-estimation (covered by a unit test).
  • The structural-edit modal is now reached only via the explicit "Re-plan" button.
  • pnpm test passes.

Manual verification.

  • During recording, click a Step's estimation cell, type a new number, hit Enter. The number updates; no modal opens.
  • The Failure Signal (per-Step "off" indicator) continues to reference the Initial Plan estimation.
  • Clicking "Re-plan" opens the modal; the heading reflects the new framing.

Risks / migration.

  • The inline editor needs to coexist with the existing duration inline editor on completed Steps. Confirm visual layout supports both without ambiguity.
  • Step-Back Signal (variance vs latest re-estimate) is not yet rendered. Surface as a follow-up unless trivial here. Default: render as a secondary, subtler indicator next to the existing "off" color — TBD during implementation.

CHECKPOINT 3 — after Phase 3

Stop. Verify:

  • pnpm test green.
  • Re-estimation is a one-click, one-Enter interaction with no modal.
  • Re-planning still opens the modal with deliberate framing.

Reviewer (human): confirm UX feels right before final cleanup.


Phase 4 — Cleanup

Slice 5 — Model and naming hygiene

Goal. Remove the inconsistencies in the Task model and surrounding code that no longer carry their weight after Phases 1-3.

Files in scope.

  • src/modules/task/models/task.ts:
    • Remove updateSteps (line 53-56) — identical to newSteps. Update test on line 73 (task.test.ts:73) to call newSteps (or the renamed action — see below).
    • Rename newStepsrePlan to match the vocabulary established in CONTEXT.md.
    • Keep editSteps(...steps) (append semantics) if any consumer needs it; if not, remove. Verify with grep across the codebase before deletion.
    • Fix the readonly stepHistory declaration — either drop readonly from the constructor parameter (since the array is mutated via .push()), or migrate to immutable updates that produce a new array. Recommended: drop readonly; document the mutation pattern in CONTEXT.md if non-obvious.
    • Re-examine wasUpdated getter: name is misleading (it's stepHistory.length > 0, i.e., "has any steps at all"). Rename to hasSteps or remove if unused.
  • src/modules/record/stores/useTaskRecordStore.ts:
    • Rename cleanCurrentStepIdreconcileWithReplannedPlan (or similar) to match its new non-destructive contract from Slice 2.
  • src/modules/record/models/task-record.ts:
    • TaskRecord.start = toISODate(new Date()) at construction is misleading — the real start is set in startStepRecord when the first Step Record begins. Initialize as null and tighten the type: start: ISODate | null. Update consumers.
  • CONTEXT.md: resolve Task.link and Task.date semantics. Likely additions:
    • Task Date: the moment the Task was created. Used for ordering the Task list. Not editable.
    • Task Link: optional external reference (e.g., ticket URL).
  • Tests: update all references to renamed methods; add tests for TaskRecord.start === null until first Step Record.

Acceptance criteria.

  • Task model has no duplicate-bodied methods.
  • All renames propagated; pnpm test (lint + types + unit) green.
  • readonly lies removed.
  • TaskRecord.start is null until the first Step Record begins.
  • CONTEXT.md includes Task Date and Task Link definitions.

Manual verification.

  • Smoke the full Task lifecycle one more time to ensure no regressions.

Risks / migration.

  • Renames touch many files. Use rename-symbol IDE refactors where possible; verify with pnpm test:types.

CHECKPOINT 4 — after Phase 4

Stop. Verify:

  • pnpm test green.
  • pnpm build succeeds.
  • Manual: full lifecycle smoke once more on a fresh localStorage (incognito tab) to confirm no migration footgun.

Explicitly out of scope (deferred)

These were surfaced in the deep-design session but are not addressed by this plan. They can be picked up in a follow-up plan if value emerges.

  • breakTime single-pause model. Multiple pauses are silently absorbed; the fact of pause is unrecoverable. Acceptable for now; the Closing Ceremony does not surface pauses.
  • Step-Back Signal rendering. Slice 4 mentions surfacing variance vs latest re-estimate as a softer indicator; default behaviour is to render only the Failure Signal until product feedback contradicts.
  • Cross-Task patterns (e.g., "you underestimate refactors by 40%"). The per-task loop is the focus; cross-task analytics is a separate product surface.

Test command reference

  • pnpm test — full (lint + types + unit). Use at every checkpoint.
  • pnpm test:unit — fast iteration during a slice.
  • pnpm test:types — type-check only, useful after renames.
  • pnpm dev — manual UI verification.
  • pnpm vitest run path/to/file.spec.ts — single test file.