Skip to content

feat(#233): optimiser library + endpoints (Phase 3 of Resource Optimiser)#236

Merged
NickMonrad merged 2 commits intomainfrom
feature/optimiser-phase3-endpoint
Apr 29, 2026
Merged

feat(#233): optimiser library + endpoints (Phase 3 of Resource Optimiser)#236
NickMonrad merged 2 commits intomainfrom
feature/optimiser-phase3-endpoint

Conversation

@NickMonrad
Copy link
Copy Markdown
Owner

Summary

Phase 3 of 4 for Resource Optimiser (#233). Adds the optimiser engine + API endpoints. Backend only — frontend lands in Phase 4.

Endpoints

POST /api/projects/:projectId/optimise

Runs grid search across resource-count combinations, evaluates each via the pure scheduler, returns ranked candidates by mode.

Request body:

{
  "mode": "speed" | "utilisation" | "balanced",
  "constraints": {
    "countRanges": [{ "resourceTypeId": "...", "min": 1, "max": 6 }],
    "allowRampUp": true,
    "maxBudget": 500000,         // optional
    "maxDurationWeeks": 52       // optional
  },
  "dayRates": { "rt-id": 1500 }, // optional; falls back to ResourceType.dayRate
  "topN": 3
}

Response: ranked candidates, baseline for diff display, searchStats (scenariosEvaluated, candidatesFound, durationMs, sampled), and a resourceTypes lookup [{id,name}].

POST /api/projects/:projectId/optimise/apply

Applies a candidate scenario:

  1. Creates an optimiser_apply snapshot (full v2 state) for rollback
  2. Updates ResourceType.count + NamedResource.startWeek (if rampUp suggested) in a transaction
  3. Re-materialises timeline via the pure scheduler with the new config
  4. Returns the new snapshotId so the UI can offer "Undo"

Library: server/src/lib/optimiser.ts (pure, no I/O)

  • Grid search with MAX_SCENARIOS = 5000 cap; falls back to random sampling for larger spaces (search reports sampled: true)
  • Modes:
    • speed — minimise deliveryWeeks, tiebreak by cost then utilisation
    • utilisation — maximise avg utilisation %, tiebreak by deliveryWeeks
    • balanced — multi-objective normalised score (40% speed, 40% utilisation, 20% cost; redistributes when costs absent)
  • Metrics per candidate: deliveryWeeks, avgUtilisationPct, gapWeeksByResourceTypeId, estimatedCost, parallelWarningCount
  • Constraints maxBudget / maxDurationWeeks drop scenarios entirely (not score-penalised)
  • Cost: count × dayRate × 5 × deliveryWeeks using ResourceType.dayRate or request-supplied overrides
  • Utilisation computed from raw demand vs capacity (no full resource-levelling) — orders-of-magnitude faster, makes 5000 scenarios feasible

Review fixes applied (sub-agent review cycle)

  • 🔴 HIGH — apply endpoint was using stale pre-transaction startWeek when re-materialising timeline; now overrides in memory before scheduler call
  • 🟡 MEDIUMsearchStats.scenariosEvaluated now counts scheduler invocations (was post-constraint survivors); added candidatesFound
  • 🟡 MEDIUMgapWeeksByResourceTypeId keyed on RT id (was name; collision risk since name has no unique constraint)
  • 🟡 MEDIUM — apply body now element-validates each resourceType (rejects count: "three", missing fields, negative startWeek with 400)
  • 🟢 LOWDate.now() and Math.random() made injectable (clock + seedable PRNG) for deterministic tests
  • ✅ Verified: purity, baseInput non-mutation, Cartesian product correctness, snapshot+trigger, auth, route ordering, no any casts

Tests

  • npx tsc --noEmit (server + client) — ✅ clean
  • npm test (server) — ✅ 182/182 passing (164 prior + 18 new optimiser tests)

New test coverage includes: happy path, all 3 modes producing different rankings, both constraint types, empty project, sampling fallback, single-scenario grid, topN > scenarios, manually-pinned stories counted in demand, seeded-PRNG determinism, route validation (4 cases).

Phases

Refs #233

NickMonrad and others added 2 commits April 29, 2026 19:58
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…y safety, testability

- Fix 1 (HIGH): apply endpoint now overrides namedResources[].startWeek in-memory
  alongside count, so the scheduler materialises the correct ramp-up timeline
- Fix 2 (MEDIUM): scenariosEvaluated now counts scheduler invocations (scenariosRun);
  candidatesFound tracks post-constraint survivors; OptimiserResult updated
- Fix 3 (MEDIUM): gapWeeksByType renamed to gapWeeksByResourceTypeId, keyed by rt.id
  not rt.name — eliminates silent collision for duplicate RT names; route adds
  resourceTypes: {id,name}[] lookup in response
- Fix 4 (MEDIUM): apply endpoint validates each resourceTypes element before snapshot
  creation; returns 400 with descriptive error on bad input
- Fix 5 (LOW): runOptimiser accepts optional _now injector for deterministic timing
- Fix 6 (LOW): randomSample accepts rng injector; OptimiserConfig exposes rng field
- Fix 7: added 7 new tests — topN>grid, single-scenario, manual-story demand,
  seeded PRNG determinism, 4× route element validation cases

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@NickMonrad NickMonrad merged commit 3220408 into main Apr 29, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant