Skip to content

feat(timetable): generate a timetable scenario that keeps every hard wish - #795

Merged
rubenvdlinde merged 6 commits into
developmentfrom
feat/timetable-generator-solver
Sep 30, 2026
Merged

rubenvdlinde merged 6 commits into
developmentfrom
feat/timetable-generator-solver

Conversation

@rubenvdlinde

Copy link
Copy Markdown
Contributor

Timetable generator, section 5: generate a scenario that keeps every hard wish

Base: development (19711e0). Sits on #794 (section 3, wishes), which sits on #793 (section 4, scorer) and #791 (section 2, input). Merge in that order, or merge this PR with a merge commit to land all four.

Change: timetabling-generator (DECISIONS rows 17 and 21), section 5 of 9 (tasks 5.1, 5.2, 5.3). The change stays open. Row moved to built: tt-hard-soft-wishes.

What the user can now do

An admin opens Timetable, Scenarios, opens a scenario and presses Generate. The run goes to the background in 60-second steps up to the time budget (timetable_generator_budget_minutes); the scenario page shows the status and the share of the budget used, then how many lessons got a place, the lessons without a place with the hard wish that blocked each one, and the soft wishes the timetable breaks. A generated scenario never breaks a hard wish: a lesson that cannot be placed without breaking one stays off the grid and names that wish.

How

  • Engine (decision 21, PHP local search now): lib/Timetabling/TimetableSolver.php interface, bound to LocalSearchSolver in Application::register (where a CP-SAT sidecar can be bound per instance later). LocalSearchSolver places lessons greedily, most constrained first, then improves with simulated annealing on TimetableScorer (section 4), seeded by SeededRandom, with a move bound per step so the same seed gives the same result, and ends with a repair pass that takes a lesson breaking a hard wish off the grid. SearchState, PlacementOptions, SolverResult hold the working state.
  • Run: POST /api/timetable/scenarios/{id}/generate (admin, AuthorizedAdminSetting plus an admin check: 403 otherwise, 400 for a scenario that has no input) queues lib/BackgroundJob/GenerateTimetableScenario.php; each step continues from the best placements of the step before (TimetableGenerationService), re-queues while it improves and time is left, and stores failed with the reason on an exception. TimetableScenarioStore reads and writes the scenario inside OpenRegister.
  • Pages: TimetableScenarios (declarative index, /timetable/scenarios) and TimetableScenarioDetail (declarative detail) whose sections slot is src/components/TimetableScenarioSections.vue. No new custom page, so the gate-69 ratchet holds. Helpers in src/utils/timetableScenarios.js. 22 new strings in all 36 locales.
  • design.md amended: the interface takes a seed and a start placement (why: repeatable runs, and each step continues the last).

Tests

  • tests/unit/Timetabling/LocalSearchSolverTest.php (7): a feasible week places everything and keeps the hard wish; an impossible hard wish leaves one lesson unplaced naming the wish; a hard limit is never broken; a soft wish is kept when it costs nothing; same seed same result; 600 lessons inside 60 seconds, with the stored result validated against the real timetableScenario fragment.
  • tests/unit/Service/TimetableGenerationServiceTest.php (6): re-queues while improving, stops at the budget, stores failed with the reason on an exception.
  • tests/unit/Controller/TimetableScenarioControllerTest.php (3): a non-admin is refused.
  • tests/vitest/timetableScenarios.spec.js (5); tests/e2e/timetable-generator.spec.ts: "generate a scenario that respects a hard wish" (nightly Playwright; not runnable from the lane clone).

Red first: all 16 PHPUnit cases failed with the classes missing (lane log lane16/red-s5.log).

Verification

At head 9f23200 (development 19711e0 + #791 + #793 + #794 + this section): composer check:strict 1 (phpcs, phpmd static access) then 0 (PHPUnit 526, 7 skipped); npm lint, stylelint, check:l10n, check:l10n-js, check:schema-l10n, check:manifest, vitest (42 files, 413 tests) and build all 0. CI-shaped Hydra gates (hydra-gates@main, full coverage, this body): gate-16 spec-coverage 1 (the job's run method) then 0: ALL 82 applicable gates passed.

Live check

  1. As an admin: Timetable, Wishes, add a hard wish "Teacher klaas, not on these periods, Wednesday 5 to 8".
  2. Timetable, Scenarios, new scenario with a window of one school week, open it, Generate. The page shows "Waiting to start", then the budget used, then "N of M lessons placed".
  3. No lesson of klaas sits on Wednesday periods 5 to 8. Lessons without a place list the blocking wish.
  4. As a non-admin: POST /apps/planninq/api/timetable/scenarios/<id>/generate answers 403.

Inherited: the non-required Newman, PHPUnit (pgsql) and Quality Report jobs have been red on every PR since #758.

🤖 Generated with Claude Code

@github-actions

Copy link
Copy Markdown
Contributor

Quality Report — ConductionNL/planninq @ a5e6e12

Check PHP Vue Security License Tests
lint ✅
phpcs ✅
phpmd ✅
psalm ✅
phpstan ✅
phpmetrics ✅
eslint ✅
stylelint ✅
build ✅
check-manifest ✅
check-l10n-js ✅
check-schema-l10n ✅
composer ✅ ✅ 107/107
npm ✅ ✅ 646/646
app:check-code ⏭️
info.xml ✅
REUSE ❌
lockfile sync ✅
PHPUnit ❌
Newman ❌
Playwright ⏭️ deferred: E2E runs locally and on the promotion path only. This pull request targets development, so the suite is asked once per promotion into beta and main rather than once per push per open pull request. Run it on any branch from the Actions tab, or locally with npx playwright test.
Hydra gates ✅

Quality workflow — 2026-09-30 05:34 UTC

Download the full PDF report from the workflow artifacts.

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