scheduler: pre-1.0 public-surface decisions — phantom fields, docstring drift, percentile convention, exception wrapping
Severity:
Found by: pre-release scheduler-engine audit (2026-05-28). Each sub-item below is a decision (often "remove or document") not a fix.
Sub-items
A. Phantom fields on Task: `planned_finish`, `percent_complete`
- Location: `packages/scheduler/src/trueppm_scheduler/models.py:55,69`
- Both are public dataclass fields serialized in `to_dict`/`from_dict` but never read by `schedule()` or `monte_carlo()`. Only `planned_start` is consumed.
- Decision: (a) honour them (planned_finish = SNLT finish constraint; percent_complete = remaining-duration driver) or (b) remove. Either is breaking at 1.0 — pick pre-1.0.
B. Unused Calendar fields: `hours_per_day`, `timezone`
- Location: `models.py:146-147`
- Engine works entirely in whole-day units; both fields are no-ops.
- Decision: (a) consume them or (b) remove. `hours_per_day` is a particularly common knob users will assume affects calculation.
C. `monte_carlo()` default cap vs docstring claim
- Location: `engine.py:810` (`runs=1_000, max_runs=1_000`) vs docstring/README "10k runs/sec" example.
- PyPI users following the example will trip `SimulationCapExceeded`.
- Fix: raise the default or correct the prose.
D. Percentile indexing convention undocumented
- Location: `engine.py:974-976`
- Uses lower-median nearest-rank with `-1` offset (so `runs=10 → P50 = all_dates[4]`). No convention documented (nearest-rank vs linear vs C=1 vs C=0).
- Fix: switch to `numpy.percentile(distribution, [50, 80, 95])` (de-facto Python standard) and document.
E. `DateRange.from_dict` / `Project.from_json` leak `KeyError`/`ValueError`
- Location: `models.py:38-42`
- README documents `InvalidScheduleInput`/`CyclicDependencyError`/`SimulationCapExceeded` only.
- Fix: wrap deserialisation errors in `InvalidScheduleInput` (or a sibling exception) so the public exception surface is complete.
F. `ScheduleResult.tasks` — mutable result, no copy on construction
- Location: `engine.py:103, 725`
- A consumer mutating result tasks mutates the internal state. Sharing a `ScheduleResult` across threads is unsafe.
- Decision: document immutability or use `@dataclass(frozen=True)` + tuple.
Acceptance per sub-item
- A — decision recorded; fields either removed or honoured + docstring updated
- B — same as A
- C — default and docstring agree
- D — percentile convention chosen, documented, tests pin it
- E — deserialisation errors wrapped; README exception list complete
- F — immutability stance documented in code + README