feat(goals): first-class goals with per-cycle point targets (add-goals OpenSpec change) #36

Merged
forgejo-admin merged 5 commits from add-goals into main 2026-08-02 07:50:51 +00:00

Implements the add-goals OpenSpec change end to end: first-class goals with per-cycle point targets, standing tasks and one-off/idea items rolling up into goal progress. All 23 tasks in openspec/changes/add-goals/tasks.md are checked off; the design contract in designs/goal-rollup/README.md is implemented without deviations.

What shipped

Schema (additive): goal, goal_user, goal_month tables; nullable goalId on templateTask (standing task) and task (one-off/idea). Migration 0012_add-goals is purely additive.

Server:

  • Progress is a pure rollup query (goal-rollup.ts), shared by getGoals and getVelocityByMonth — no denormalized counter, no direct ticks on goals.
  • Rollover is three lanes via a pure planner (rollover.ts): template instances stay as history; engaged-but-uncompleted goal one-offs silently return to idea (monthId cleared, pending schedules dropped); goal-less tasks keep today's backlog sweep byte-for-byte.
  • goal_month rows are written only by the commit flow (startCycle now carries per-goal point targets), never by rollover. Velocity sources goal commitments from goal_month; goal-linked rows are excluded from targetCount-based committed sums.
  • Engagement is the pull-in: scheduling or completing an idea stamps monthId. Goal-linked tasks have no completion ceiling (targetCount ignored where goalId is set).
  • Items require ≥1 assignee at creation (unassigned tasks are invisible to user-filtered queries). Ideas are excluded from the backlog query — they live in their goal's bar.
  • Task 1.3 resolved: taskCompletion.scheduleId's FK tolerates schedule deletion (bun:sqlite never enables PRAGMA foreign_keys; verified empirically, and reconcileSchedules already relies on it). Rollover still deletes only status='scheduled' rows — recorded as a comment in rollover.ts.

Web: GoalBar in CategorySection (fuel gauge with scheduled segment and displayed overshoot, expandable THIS CYCLE / IDEAS rows, inline slim add-item form per design D8); DatePicker day cap lifted for goal-linked items only; Template page goal authoring (EditGoalDialog + standing-task management); Goals page points-based commit with suggestGoalTargetPoints (avg of last 3 committed cycles' completed points); 🎯 calendar labels for goal items.

Seed: defaultScenario now includes the garden example — goal at 10 pts committed, two standing tasks (3 completions on "Garden session"), a done 5-pt one-off, a scheduled 5-pt one-off, and two ideas.

Two things reviewers should look at

  1. Migration meta repair (0011). Migrations 0009/0010 were hand-written without snapshots, so the snapshot chain stopped describing reality (missing user.groups, task FKs) and any drizzle-kit generate produced a broken diff (it even re-emitted ALTER TABLE user ADD groups, which fails). 0011_resync-after-handwritten-migrations is a deliberate no-op migration whose drizzle-generated snapshot resyncs the chain; drizzle.config.ts is added (there was none — db:generate could not run at all). bun run db:recreate runs the full chain cleanly.
  2. determineSuggestedTarget. Task 3.4 says "replace", but deleting the count heuristic would change the goal-less recurring commit flow, which design decision 9 declares untouched. Resolution: the new points-based suggestGoalTargetPoints drives goal commits; the count heuristic stays for goal-less recurring cards only.

Verification

  • bun run test:unit: 73 passed (new: rollup mixed-completion spec scenario, all three rollover lanes, suggestion heuristic)
  • bun run test:playwright: 32 passed, 0 failed (new goals.spec: bar render/expand, standing completion ticks progress, idea schedule pull-in, idea done logs points, idea delete — with DB assertions via /api/test-query)
  • bun run test:seed: green; lint: 0 errors; server typecheck (tsc -p apps/lunacycle-server/tsconfig.json --noEmit --allowImportingTsExtensions): clean (root typecheck checks zero files)

🤖 Generated with Claude Code

Implements the `add-goals` OpenSpec change end to end: first-class goals with per-cycle point targets, standing tasks and one-off/idea items rolling up into goal progress. All 23 tasks in `openspec/changes/add-goals/tasks.md` are checked off; the design contract in `designs/goal-rollup/README.md` is implemented without deviations. ## What shipped **Schema (additive):** `goal`, `goal_user`, `goal_month` tables; nullable `goalId` on `templateTask` (standing task) and `task` (one-off/idea). Migration `0012_add-goals` is purely additive. **Server:** - Progress is a pure rollup query (`goal-rollup.ts`), shared by `getGoals` and `getVelocityByMonth` — no denormalized counter, no direct ticks on goals. - Rollover is three lanes via a pure planner (`rollover.ts`): template instances stay as history; engaged-but-uncompleted goal one-offs silently return to idea (monthId cleared, pending schedules dropped); goal-less tasks keep today's backlog sweep byte-for-byte. - `goal_month` rows are written only by the commit flow (`startCycle` now carries per-goal point targets), never by rollover. Velocity sources goal commitments from `goal_month`; goal-linked rows are excluded from targetCount-based committed sums. - Engagement is the pull-in: scheduling or completing an idea stamps `monthId`. Goal-linked tasks have no completion ceiling (`targetCount` ignored where `goalId` is set). - Items require ≥1 assignee at creation (unassigned tasks are invisible to user-filtered queries). Ideas are excluded from the backlog query — they live in their goal's bar. - Task 1.3 resolved: `taskCompletion.scheduleId`'s FK tolerates schedule deletion (bun:sqlite never enables `PRAGMA foreign_keys`; verified empirically, and `reconcileSchedules` already relies on it). Rollover still deletes only `status='scheduled'` rows — recorded as a comment in `rollover.ts`. **Web:** `GoalBar` in `CategorySection` (fuel gauge with scheduled segment and displayed overshoot, expandable THIS CYCLE / IDEAS rows, inline slim add-item form per design D8); `DatePicker` day cap lifted for goal-linked items only; Template page goal authoring (`EditGoalDialog` + standing-task management); Goals page points-based commit with `suggestGoalTargetPoints` (avg of last 3 committed cycles' completed points); 🎯 calendar labels for goal items. **Seed:** `defaultScenario` now includes the garden example — goal at 10 pts committed, two standing tasks (3 completions on "Garden session"), a done 5-pt one-off, a scheduled 5-pt one-off, and two ideas. ## Two things reviewers should look at 1. **Migration meta repair (0011).** Migrations 0009/0010 were hand-written without snapshots, so the snapshot chain stopped describing reality (missing `user.groups`, task FKs) and any `drizzle-kit generate` produced a broken diff (it even re-emitted `ALTER TABLE user ADD groups`, which fails). `0011_resync-after-handwritten-migrations` is a deliberate no-op migration whose drizzle-generated snapshot resyncs the chain; `drizzle.config.ts` is added (there was none — `db:generate` could not run at all). `bun run db:recreate` runs the full chain cleanly. 2. **`determineSuggestedTarget`.** Task 3.4 says "replace", but deleting the count heuristic would change the goal-less recurring commit flow, which design decision 9 declares untouched. Resolution: the new points-based `suggestGoalTargetPoints` drives goal commits; the count heuristic stays for goal-less recurring cards only. ## Verification - `bun run test:unit`: 73 passed (new: rollup mixed-completion spec scenario, all three rollover lanes, suggestion heuristic) - `bun run test:playwright`: 32 passed, 0 failed (new goals.spec: bar render/expand, standing completion ticks progress, idea schedule pull-in, idea done logs points, idea delete — with DB assertions via /api/test-query) - `bun run test:seed`: green; lint: 0 errors; server typecheck (`tsc -p apps/lunacycle-server/tsconfig.json --noEmit --allowImportingTsExtensions`): clean (root typecheck checks zero files) 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Implements the server side of the add-goals OpenSpec change:

- goal / goal_user / goal_month tables plus nullable goalId on
  templateTask (standing task) and task (one-off/idea), with a clean
  additive migration (0012).
- Migration meta repair: 0009/0010 were hand-written without snapshots,
  so drizzle-kit diffs were computed against a snapshot that no longer
  described reality (missing user.groups and task FKs). 0011 is a
  deliberate no-op migration whose snapshot resyncs the meta chain;
  drizzle.config.ts is added so `bun run db:generate` works at all.
- Progress is a pure rollup query (goal-rollup.ts), no denormalized
  counters, shared by getGoals and getVelocityByMonth. Committed points
  for goals come from goal_month, written only by the commit flow
  (startCycle), never by rollover.
- Rollover rewritten as three lanes via a pure planner (rollover.ts):
  template instances stay put, engaged-but-uncompleted goal one-offs
  silently return to idea (pending schedules dropped; the
  taskCompletion.scheduleId FK tolerates schedule deletion — bun:sqlite
  never enables PRAGMA foreign_keys — but only status='scheduled' rows
  are deleted anyway), goal-less tasks keep today's backlog sweep.
- Engagement is the pull-in: scheduling or completing an idea stamps
  monthId with the active cycle. Goal-linked tasks have no targetCount
  ceiling; completions are unbounded.
- Goal CRUD + addGoalItem procedures (items require ≥1 assignee since
  unassigned tasks are invisible to user-filtered queries). deleteGoal
  detaches items rather than deleting them.
- Ideas are excluded from the backlog query — they live in their goal.
- defaultScenario seeds the garden example (goal, standing tasks, done
  and scheduled one-offs, ideas) for dev and e2e.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- GoalBar: full-width collapsed bar per goal inside its category section
  (name, fuel gauge with scheduled segment and displayed — not clamped —
  overshoot, item/idea counts), expanding to THIS CYCLE and IDEAS rows
  with inline complete / schedule / delete-idea actions and a slim
  add-item form (standing vs one-off, title, points, assignees — design
  D8 resolved toward the slim form: without targetCount, focus and
  backlog, almost none of EditTaskDialog survives).
- Goal items render only in their bar; the goal-less card grid is
  untouched alongside.
- DatePicker: targetCount becomes nullable — null means no day cap,
  passed only for goal-linked items.
- Template page: goal authoring per category (EditGoalDialog for
  name/description/default target/users, standing-task management).
- Goals page: per-goal point target commit with a points-based
  suggestion (suggestGoalTargetPoints from goal_month + completion
  history; the count-based determineSuggestedTarget stays for the
  goal-less recurring flow, which keeps today's behavior per design
  decision 9). startCycle now carries the goal commitments.
- Calendar entries for goal items get a 🎯 prefix (implementer's choice).
- CacheArg becomes Partial — a clear always targeted a subset of caches;
  requiring every key forced an `as` cast server-side and made every
  client clearCache call a type error.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
test(goals): rollup/rollover/suggestion unit tests, goal-bar e2e; check off tasks
All checks were successful
pr-validate / validate (pull_request) Successful in 17s
495be2a60f
- goal-rollup.test.ts covers the spec's mixed-completion scenario
  (1-pt standing ×3 + 5-pt one-off = 8), goal isolation, scheduled
  points, ideas contributing nothing, and per-month bucketing.
- rollover.test.ts covers all three lanes including the silent return
  to idea and byte-for-byte goal-less sweep parity.
- goals.spec.ts (e2e): bar renders/expands, standing completion ticks
  progress, scheduling an idea pulls it in, completing an idea logs its
  points, deleting an idea removes the row — asserted in the DB via
  /api/test-query.
- suggestGoalTargetPoints unit tests.
- All 23 tasks in the OpenSpec change checked off.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
fix(goals): mobile-first item rows — quiet row + detail sheet, item editing
All checks were successful
pr-validate / validate (pull_request) Successful in 17s
49c968a5ce
Mobile review found item titles crushed to zero width and trailing
buttons bleeding past the card: a single flex line carried ~7 fixed-width
controls, leaving the title as the only shrinkable element. There was
also no way to edit an item at all.

Rework (same behavior at all widths, one code path):
- Item rows are quiet: icon, title (the only flexible element, so it
  stays readable), check count, points badge, and one inline quick
  action ([+] log progress / [✓] complete).
- Tapping a row opens a detail sheet (ResponsiveDialog): points,
  assignees, status, description, and the remaining actions — schedule
  (DatePicker), edit, delete (ideas only, replacing the row ⋯ menu).
- New slim edit form in the sheet (title, points, users, description).
  Standing tasks edit their template and sync this cycle's instance via
  the new updateGoalItem procedure; one-offs/ideas edit the task row.

Tests: goals e2e updated to the sheet interactions plus a new edit
scenario asserting both template and instance carry the change; new
mobile regression test (3 device profiles) asserting titles stay
readable and rows don't bleed past the card. Full suite: 36 passed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
fix(goals): merge the item view/edit split into one editable dialog
All checks were successful
pr-validate / validate (pull_request) Successful in 17s
7568be95af
Design review: the detail sheet's view mode was a read-only page whose
only content was buttons — for a done one-off it was a full-screen sheet
holding four words, and Edit just swapped in a form with the same
fields. A goal item is five fields; there is nothing to "view" that the
form doesn't already show.

Row tap now opens a single dialog, matching the app's existing
TaskCard → EditTaskDialog idiom: status line in the header, fields live
immediately (title, points, assignees, description), Delete (ideas) /
Schedule / Save in the footer. Saving closes the dialog. Log/Complete
remains the row's inline quick action only, so a mutating button never
sits inside an unsaved form. design.md D8 records the resolution.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign in to join this conversation.
No description provided.