feat(engine,api): manual re-plan mode and POST /plans/versions/{id}/reoptimise #94

Merged
patrick merged 3 commits from feature/engine-manual-mode into main 2026-09-11 14:07:32 +00:00
Collaborator

Description

A planner can now trigger "Re-optimise" on a specific scope — a date range plus a chosen set of crews — against an existing plan version. Behind the scenes this runs the solver only over the crews and dates the planner picked; every other crew's schedule on those same dates, and anything already under way, is left exactly as it was. The result comes back as a new draft version built on top of the one the planner started from, so it can be reviewed and published the same way any other draft can.

Closes #35. Row PH5.3 in docs/12-implementation-plan.md.

Also files #93, a follow-up seam request: the worker doesn't yet dispatch a manual re-optimise run to this new engine behaviour end-to-end (it currently always uses the same engine the nightly run uses). That's flagged in detail on both issues and doesn't block this PR — this ticket's own scope (the engine behaviour itself, and the endpoint's validation/enqueue/child-version wiring) is complete and independently tested.

Type of Change

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

Breaking Changes

None.

Test Plan

Tests green: new engine-level unit tests cover out-of-scope crew-days and out-of-scope dates being returned unchanged, already-started work staying put, the planner's soft-lock-vs-free choice, and an existing lock still taking effect. New API-level integration tests (real Postgres) cover scope validation (unknown version, bad date range, range too wide, unknown crew), the missing-reason-header case, role gating, and the happy path enqueuing a run and returning a child version. Full backend suite (ruff check, ruff format --check, mypy --strict, pytest -m "not smoke") passes.

Checklist

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

Additional Context

Two judgment calls worth a reviewer's eyes:

  • The endpoint path itself: docs/09-api.md's table never spelled out a literal /reoptimise row — only the generic POST /plans/runs. This PR mounts it at POST /plans/versions/{id}/reoptimise, matching the ticket's own file name and prose and the shape of its sibling per-version actions (/recalculate, /overrides, /publish, /discard), and adds the missing row to docs/09-api.md.
  • "Freeze always hard" and "planner chooses confirmed soft-locked or free" (both from the re-plan policy table) needed modeling decisions the docs didn't pin down — documented in docs/05-planning-engine.md. The "free" choice isn't reachable from the API request body yet, since the underlying task contract has no field for it; that's called out as a known gap rather than something this PR works around.

No new dependencies.

🤖 Generated with Claude Code

https://claude.ai/code/session_013YioTVKBPoE6thZqbnTtnM

## Description A planner can now trigger "Re-optimise" on a specific scope — a date range plus a chosen set of crews — against an existing plan version. Behind the scenes this runs the solver only over the crews and dates the planner picked; every other crew's schedule on those same dates, and anything already under way, is left exactly as it was. The result comes back as a new draft version built on top of the one the planner started from, so it can be reviewed and published the same way any other draft can. ## Related Issues Closes #35. Row PH5.3 in docs/12-implementation-plan.md. Also files #93, a follow-up seam request: the worker doesn't yet dispatch a manual re-optimise run to this new engine behaviour end-to-end (it currently always uses the same engine the nightly run uses). That's flagged in detail on both issues and doesn't block this PR — this ticket's own scope (the engine behaviour itself, and the endpoint's validation/enqueue/child-version wiring) is complete and independently tested. ## Type of Change - [x] New feature - [ ] Bug fix - [ ] Refactor / cleanup (no behaviour change) - [ ] Documentation - [ ] Chore / build / CI - [ ] Breaking change ## Breaking Changes None. ## Test Plan Tests green: new engine-level unit tests cover out-of-scope crew-days and out-of-scope dates being returned unchanged, already-started work staying put, the planner's soft-lock-vs-free choice, and an existing lock still taking effect. New API-level integration tests (real Postgres) cover scope validation (unknown version, bad date range, range too wide, unknown crew), the missing-reason-header case, role gating, and the happy path enqueuing a run and returning a child version. Full backend suite (`ruff check`, `ruff format --check`, `mypy --strict`, `pytest -m "not smoke"`) passes. ## Checklist - [x] Conventional commit(s), one logical change per commit - [x] Tests added/updated and passing - [x] `docs/` updated for any behavioural change (ADR added if a prior decision was reversed) - [x] No hard deletes; no direct status updates outside `polaris.domain` - [x] No business logic in routers/components - [x] Migration included if the schema changed (one per PR max, reversible downgrade) - [x] No new dependency without a reason in the commit body ## Additional Context Two judgment calls worth a reviewer's eyes: - The endpoint path itself: docs/09-api.md's table never spelled out a literal `/reoptimise` row — only the generic `POST /plans/runs`. This PR mounts it at `POST /plans/versions/{id}/reoptimise`, matching the ticket's own file name and prose and the shape of its sibling per-version actions (`/recalculate`, `/overrides`, `/publish`, `/discard`), and adds the missing row to docs/09-api.md. - "Freeze always hard" and "planner chooses confirmed soft-locked or free" (both from the re-plan policy table) needed modeling decisions the docs didn't pin down — documented in docs/05-planning-engine.md. The "free" choice isn't reachable from the API request body yet, since the underlying task contract has no field for it; that's called out as a known gap rather than something this PR works around. No new dependencies. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_013YioTVKBPoE6thZqbnTtnM
Adds polaris.engine.modes.manual.solve_manual(snapshot, ManualScope) for
the "Re-optimise" re-plan mode (docs/05-planning-engine.md "Re-plan
policy" manual row): scope is a planner-chosen date range plus an
explicit crew set, unlike every other mode's date-only scope.

Reuses the shared, mode-agnostic VRP pipeline (polaris.engine.solve)
unchanged for the in-scope sub-problem instead of duplicating any model
building. Two scope-enforcing steps wrap that call: shifts and jobs
outside scope (or already started before the snapshot was generated,
i.e. "freeze always hard") are excluded before solving so they are
never even offered to the solver, and their published assignments are
carried into the result byte-for-byte afterwards so the plan_version
this run writes stays a complete picture of the horizon rather than
only the touched crew-days.

PH5.3, issue #35.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013YioTVKBPoE6thZqbnTtnM
Lets a planner trigger "Re-optimise" on an explicit scope (dates,
crews) against an existing plan version. Validates the scope (version
exists, dates are sane, crews exist), resolves it to the concrete
scope_shift_ids, and enqueues a manual-mode solve run as a child
version of the one being re-optimised, via the same
domain.planning.runs.start_run machinery POST /plans/runs already uses
- no reimplementation.

docs/09-api.md's table only documents the generic POST /plans/runs;
this ticket's own row names the file api/routers/plans/reoptimise.py
and the ticket body's prose names the path, so it mounts under
/plans/versions/{id} alongside its sibling per-version actions
(recalculate, overrides, publish, discard) rather than as a variant of
/plans/runs.

PH5.3, issue #35.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013YioTVKBPoE6thZqbnTtnM
Records the modeling decisions PH5.3 (issue #35) made where the
re-plan policy table didn't pin one down: what "freeze always hard"
means with no event-style trigger instant to derive a cutoff from, how
out-of-scope crew-days survive as pass-through rows so a re-optimised
plan_version stays a complete picture, and that the planner's
soft-locked-vs-free choice isn't reachable from the API yet. Adds the
POST /plans/versions/{id}/reoptimise row docs/09-api.md's table was
missing.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013YioTVKBPoE6thZqbnTtnM
patrick deleted branch feature/engine-manual-mode 2026-09-11 14:07:32 +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!94
No description provided.