T-D -- Validator: one rule per E_/W_/I_ code #69

Merged
bart merged 6 commits from feature/validator-rules into main 2026-09-11 11:59:21 +00:00
Collaborator

Description

Adds the plan validator: the independent check that re-derives whether a draft plan is actually feasible, separately from whatever the solver believed when it built it. Every rule from the validator table in the design docs is now a small, independently-testable function that looks only at the plan snapshot and the proposed result — shift overtime, double-booked crews or vehicles, missing skills, sites that aren't suitable for the job, jobs outside their allowed dates, windows the plan doesn't actually fit in, and a handful of informational flags like idle crews or a fallback travel matrix.

20 of the 27 documented rules are covered here; the other 7 are either explicitly owned by a later ticket (triage, scoring, locks, diffing) or, in one case, can't be checked yet because the plan data doesn't carry what's needed — flagged as a follow-up rather than guessed at.

Closes #19 (T-D). Docs: docs/04-lifecycles.md validator table.

Type of Change

  • New feature
  • Bug fix
  • Refactor / cleanup (no behaviour change)
  • Documentation
  • Chore / build / CI
  • Breaking change

Breaking Changes

None.

Test Plan

Every rule has its own hand-built bad plan that trips only that rule, plus one good plan that passes everything clean — 23 tests, all green, alongside the existing check that keeps the validator from ever depending on the solver.

Checklist

  • Conventional commit(s), one logical change per commit
  • Tests added/updated and passing
  • docs/ updated for any behavioural change
  • No hard deletes; no direct status updates outside polaris.domain
  • No business logic in routers/components
  • Migration included if the schema changed — n/a
  • No new dependency without a reason in the commit body

Additional Context

Two judgment calls worth a reviewer's eyes: the two vehicle-overlap rules read almost identically in the docs, so this PR splits them on whether the overlapping visits are at the same site or different ones; and the "including return travel" clause on one shift-length rule isn't checkable with the data the engine currently hands back, so only the outbound half is enforced for now.

🤖 Generated with Claude Code

https://claude.ai/code/session_013YioTVKBPoE6thZqbnTtnM

## Description Adds the plan validator: the independent check that re-derives whether a draft plan is actually feasible, separately from whatever the solver believed when it built it. Every rule from the validator table in the design docs is now a small, independently-testable function that looks only at the plan snapshot and the proposed result — shift overtime, double-booked crews or vehicles, missing skills, sites that aren't suitable for the job, jobs outside their allowed dates, windows the plan doesn't actually fit in, and a handful of informational flags like idle crews or a fallback travel matrix. 20 of the 27 documented rules are covered here; the other 7 are either explicitly owned by a later ticket (triage, scoring, locks, diffing) or, in one case, can't be checked yet because the plan data doesn't carry what's needed — flagged as a follow-up rather than guessed at. ## Related Issues Closes #19 (T-D). Docs: `docs/04-lifecycles.md` validator table. ## Type of Change - [x] New feature - [ ] Bug fix - [ ] Refactor / cleanup (no behaviour change) - [ ] Documentation - [ ] Chore / build / CI - [ ] Breaking change ## Breaking Changes None. ## Test Plan Every rule has its own hand-built bad plan that trips only that rule, plus one good plan that passes everything clean — 23 tests, all green, alongside the existing check that keeps the validator from ever depending on the solver. ## Checklist - [x] Conventional commit(s), one logical change per commit - [x] Tests added/updated and passing - [x] `docs/` updated for any behavioural change - [x] No hard deletes; no direct status updates outside `polaris.domain` - [x] No business logic in routers/components - [ ] Migration included if the schema changed — n/a - [x] No new dependency without a reason in the commit body ## Additional Context Two judgment calls worth a reviewer's eyes: the two vehicle-overlap rules read almost identically in the docs, so this PR splits them on whether the overlapping visits are at the same site or different ones; and the "including return travel" clause on one shift-length rule isn't checkable with the data the engine currently hands back, so only the outbound half is enforced for now. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_013YioTVKBPoE6thZqbnTtnM
Shared, stateless index-building and time-math helpers for validator rules
(_util.py) -- kept dependency-free beyond polaris.contracts and stdlib so
every rule module can import from here without pulling anything else in.

Refs #19 (T-D).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013YioTVKBPoE6thZqbnTtnM
E_SHIFT_EXCEEDED, E_TRAVEL_IMPOSSIBLE, W_WAITING, W_IDLE_CREW, W_DST
(route.py) and E_OUTSIDE_WINDOW, W_TIGHT_WINDOW (windows.py), per the
docs/04-lifecycles.md validator table. Route rules compare the engine's own
travel_from_prev_min/wait_min against the shift clock rather than
recomputing travel independently -- the validator has no OSRM/haversine
access. The window rules share one E01 tolerance check so a tight-but-close
window degrades E_OUTSIDE_WINDOW to W_TIGHT_WINDOW instead of double-firing.

Refs #19 (T-D). S: E01, E06.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013YioTVKBPoE6thZqbnTtnM
E_CREW_DOUBLE_BOOKED, E_VEHICLE_DOUBLE_BOOKED, E_DOUBLE_PRESENCE
(booking.py); E_SKILL_MISSING, E_NOT_SUITABLE, W_LOW_CONFIDENCE,
W_LARGE_SITE (eligibility.py).

The two vehicle-overlap codes split on site equality: same vehicle, same
site overlapping in time is E_VEHICLE_DOUBLE_BOOKED (E04); different sites
is E_DOUBLE_PRESENCE (E03) -- docs/04 doesn't spell out the split since
both codes use near-identical wording, so this is a deliberate
interpretation, documented in booking.py's module docstring.
E_CREW_DOUBLE_BOOKED groups by the shift's crew_id rather than shift_id
alone, so two shifts for the same crew on the same day are also caught.

Refs #19 (T-D). S: E03, E04, E07, S16.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013YioTVKBPoE6thZqbnTtnM
E_JOB_STATE, E_PAST_DUE, E_BEFORE_EARLIEST, E_SHIFT_CANCELLED (job_state.py);
W_SITE_GONE (presence.py); W_TRAVEL_FALLBACK (plan_level.py). Adds the
validate(snapshot, result) -> list[Finding] registry in __init__.py that
runs every implemented rule and concatenates.

E_JOB_STATE can only be re-derived as "assignment.job_id missing from
snapshot.jobs" -- SnapshotJobStatus is closed to open/planned, so a job in
any other status is never in the snapshot to begin with; documented on the
rule itself.

This ticket intentionally does not implement E_UNTRIAGED, I_GAP_UNFILLED,
I_REJECTED_ELSEWHERE (PH3.2), W_CONF_DROP (PH4.3), E_LOCK_VIOLATED (PH5.1)
or I_DISPLACED (PH5.5) -- each is a named deliverable of a later
implementation-plan row that owns a specific validator/rules/*.py file --
nor W_PATTERN_STALE, a genuine contract gap (no per-vehicle last-feed
timestamp exists on PlanningSnapshot). See issue #19 comments.

Refs #19 (T-D). S: S12, S16.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013YioTVKBPoE6thZqbnTtnM
One PlanningSnapshot/PlanResult pair per implemented E_/W_/I_ code (20
total), each a minimal mutation of a shared "everything passes" baseline
(base.py) that trips exactly that one rule and no other -- CLAUDE.md:
"validator rules get hand-built bad-plan fixtures". base.py's baseline
also doubles as the "good plan yields no findings" fixture.

Refs #19 (T-D). S: E01, E03, E04, E06, E07, S12, S16.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013YioTVKBPoE6thZqbnTtnM
test_rules.py: table-driven -- each fixture's own rule function fires only
its expected code, and validate() run over the whole registry on that same
fixture also yields only that one code (checks fixtures don't accidentally
trip a second rule). test_validate.py: a good plan validates clean, and
the covered-code set equals every docs/04 code minus the ones deferred to
later tickets and the one contract gap (see polaris.validator's docstring).

Refs #19 (T-D).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013YioTVKBPoE6thZqbnTtnM
bart merged commit 14f7191ec3 into main 2026-09-11 11:59:21 +00:00
bart deleted branch feature/validator-rules 2026-09-11 11:59:21 +00:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
patrick/Polaris!69
No description provided.