feat(#233): optimiser library + endpoints (Phase 3 of Resource Optimiser)#236
Merged
NickMonrad merged 2 commits intomainfrom Apr 29, 2026
Merged
feat(#233): optimiser library + endpoints (Phase 3 of Resource Optimiser)#236NickMonrad merged 2 commits intomainfrom
NickMonrad merged 2 commits intomainfrom
Conversation
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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/optimiseRuns 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,baselinefor diff display,searchStats(scenariosEvaluated,candidatesFound,durationMs,sampled), and aresourceTypeslookup[{id,name}].POST /api/projects/:projectId/optimise/applyApplies a candidate scenario:
optimiser_applysnapshot (full v2 state) for rollbackResourceType.count+NamedResource.startWeek(if rampUp suggested) in a transactionsnapshotIdso the UI can offer "Undo"Library:
server/src/lib/optimiser.ts(pure, no I/O)MAX_SCENARIOS = 5000cap; falls back to random sampling for larger spaces (search reportssampled: true)speed— minimisedeliveryWeeks, tiebreak by cost then utilisationutilisation— maximise avg utilisation %, tiebreak by deliveryWeeksbalanced— multi-objective normalised score (40% speed, 40% utilisation, 20% cost; redistributes when costs absent)deliveryWeeks,avgUtilisationPct,gapWeeksByResourceTypeId,estimatedCost,parallelWarningCountmaxBudget/maxDurationWeeksdrop scenarios entirely (not score-penalised)count × dayRate × 5 × deliveryWeeksusingResourceType.dayRateor request-supplied overridesReview fixes applied (sub-agent review cycle)
startWeekwhen re-materialising timeline; now overrides in memory before scheduler callsearchStats.scenariosEvaluatednow counts scheduler invocations (was post-constraint survivors); addedcandidatesFoundgapWeeksByResourceTypeIdkeyed on RT id (was name; collision risk since name has no unique constraint)count: "three", missing fields, negative startWeek with 400)Date.now()andMath.random()made injectable (clock + seedable PRNG) for deterministic testsanycastsTests
npx tsc --noEmit(server + client) — ✅ cleannpm 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