# 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 typecheck` → **0 errors** (after the deviation below). ## test:unit `npm run test:unit` → **15/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: ```ts 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 `continue`d. 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).