Files
AgendaPro/.superpowers/sdd/task-3-report.md
T
AgendaPro Dev d796538fd9 feat: rebrand AgendaPro -> AgendaMax + calendar fixes + current-week demo seed
- Rebrand all user-facing text to AgendaMax
- Fix sidebar scrolling: card container no longer overflows html (flex h-full min-h-0)
- Fix calendar toolbar hover: active buttons keep readable contrast
- Sticky calendar toolbar (month/week/day always visible on scroll)
- Seed guarantees 2-4 appointments per day for current week (Mon-Sat)
2026-07-27 10:11:16 -06:00

3.1 KiB

Task 3 Report — scheduling.ts DB functions

Files changed

  • server/lib/scheduling.ts — appended DB-backed functions from Task 3 plan (CandidateInfo, safeArr, getCandidates, getExistingBusy, isAvailable, rankOne, pickBestSlotEmployee, AutoAssignResult, AutoAssignCtx, autoAssign, runInTransaction) + import type { DatabaseSync } from "node:sqlite".

Typecheck

npm run typecheck0 errors (after the deviation below).

test:unit

npm run test:unit15/15 pass (pure tests unaffected).

tx smoke-test output

Temp file _tx-smoke.mjs (deleted after run). It imports runInTransaction and db, exercises happy path + throw path, and verifies a real UPDATE was rolled back.

  PASS  runInTransaction returns fn value (42)
  PASS  no active tx after commit
  PASS  runInTransaction rethrows on error
  PASS  no active tx after rollback
  PASS  rollback undid the UPDATE
5/5 passed
EXIT=0

PRAGMA active_transaction was used (newer node:sqlite exposes it; falls back to a no-row / 0 object) to confirm no dangling transaction after both commit and rollback. The real-state rollback check (UPDATE _tx_probe SET v=999 inside a throwing fn) proves the ROLLBACK is functional, not just emitted.

Deviations

  1. Plan typo fix in autoAssign (line 256). The plan literally wrote:
    if (!best || ranked.score > best.score || (ranked.score === best.score && c.info.id < best.info.id)) {
    
    but inside autoAssign the loop variable c is a CandidateInfo (no .info field), so c.info.id is a type error (TS2339). Fixed to c.id < best.info.id. best is still a RankedCandidate so best.info.id is correct. The semantic intent (tie-break by lowest employee id) is preserved exactly. Note: the sibling function pickBestSlotEmployee uses c.id correctly in the plan — confirming the autoAssign line was a typo, not an intentional shape.

No other deviations. The import type { DatabaseSync } is placed mid-file (after scoreCandidate), which is valid because ES module import declarations are hoisted; tsc -b --noEmit and tsx both accept it.

Self-review

  • Does autoAssign return null when no candidate is free? YES. Two null paths:
    1. candidates.length === 0 → immediate return null (line 245).
    2. After the loop, if (!best) return null (line 260) covers the case where every candidate was filtered out (closed that day, slot outside working window, or has a conflict).
  • Does it respect per-employee working hours? YES. For each candidate it calls getWorkingHoursForDate(c.empWh, ctx.bizWh, new Date(ctx.startMs)) — employee override wins, business is the fallback, and a closed day returns null → candidate is continued. Then the slot must satisfy ctx.startMs >= wh.startMs && ctx.endMs <= wh.endMs, otherwise the candidate is skipped.
  • Does runInTransaction roll back on throw? YES. Verified by smoke test: a thrown error inside fn causes db.exec("ROLLBACK") to run, the error is re-thrown, and a real UPDATE made inside the throwing fn is undone (the probe row retained its pre-tx value).