PowerSolver Docs
Back to Wizard
This page belongs to the full documentation. You don't need it to plan: back to the essentials.

PowerSolver developer guide

How PowerSolver is built, where each piece lives, and how to change it safely. Version 2.0 (engine on Timefold Solver 2.0, Java 21, Spring Boot 4; wizard in plain JavaScript and Alpine.js).


1. Architecture

 Browser                         PowerSolver engine (powersolver-api, Java)          PowerAdmin (separate product)
 ┌──────────────────────┐        ┌──────────────────────────────────────────┐        ┌─────────────────────────┐
 │ powersolver-wizard   │  JSON  │ /api/validate  ProblemValidator          │        │ users, sign-in (tenant) │
 │  steps 0-7, Results, │──────▶ │ /api/check     FeasibilityCheck (+ trial)│        │ wizard form fields per  │
 │  Compare             │        │ /api/solve     SolverService → Timefold  │        │ problem type, config    │
 │  Excel import/export │◀────── │ /api/version, /api/health                │        └─────────────────────────┘
 └──────────────────────┘        └──────────────────────────────────────────┘                    ▲
          │ shared files (results page, rules, texts)                                              │ wizard reads
          ▼                                                                                        │ fields + config
 PowerOrchestrator UI and API (pipelines call /api/check and /api/solve)
  • The engine is stateless: each request carries the whole problem; /api/solve answers on the same request (no stored job). Up to max-concurrent-solves (default 4) solves run at once (SolveGate).
  • A problem is a JSON document (collections, variables, optional shadows and derived shadows, constraints, config). At run time the engine generates the Timefold domain classes for it (ByteBuddy) and builds the constraints from the rule catalogue. No Java is written per problem.
  • The wizard has no build step. It reads its form fields and configuration from PowerAdmin, and posts to the engine.

2. Repository layout

PowerSolver/
├── powersolver-api/            engine (Maven)
│   ├── src/main/java/com/powersolver/
│   │   ├── controller/         SolverController (/api/validate, /api/check, /api/solve), ProblemController (/api/problems…), ResponseEnvelope, SolveGate, Health, Version
│   │   ├── store/              Database (SQLite + migrations), ProblemStore, AuditLog, Ids, StoreConfig
│   │   ├── schema/             ProblemParser → Problem, VariableDefinition, ConstraintDefinition, shadows
│   │   ├── validation/         ProblemValidator → ValidationIssue (code, path, message)
│   │   ├── rules/              RuleCatalog (reads resources/rules/catalog.json), ParamSpec, Values (value conventions)
│   │   ├── generation/         DomainAssembler and generators: element, host, value pools, solution, derived shadows
│   │   ├── expression/         derived-shadow expression language (parser, evaluator, interceptor)
│   │   ├── constraint/         ConstraintBuilder (one builder per rule), dynamic ConstraintProvider factory
│   │   ├── service/            SolverService, SearchSettings, InitialSolutionBuilder, ScoreBreakdown,
│   │   │                       SolutionResultBuilder, FeasibilityCheck + FeasibilityChains + FeasibilityCounting
│   │   └── adapter/, model/, config/
│   ├── src/main/resources/     application.yml (limits, store path), rules/catalog.json (the 19 rules), db/migrations/*.sql
│   ├── test-cases/             corpus of problems + assertions (run against a deployed server)
│   └── Dockerfile, docker-compose.yml
├── powersolver-verify/         independent checker in Python: verify_solution.py, rule examples, generators, pytest
├── powersolver-wizard/         wizard, Results, Compare (static files)
│   ├── js/steps/               step0 (AI intake) … step7 (check and plan)
│   ├── js/components/          data-import (Excel/CSV), field-renderer, card-grid, slot-generator, modals, PowerAI UI
│   ├── js/*.js                 see §4
│   ├── locales/                en, fr, de, es, nl
│   ├── tests/                  node tests (§6)
│   └── tools/                  check-i18n, stamp-version, sync-shared, make-demo-kit
├── powersolver-demos/          20 demos: generators, problems, solutions, Excel kits (input/), scenarios, expected results
├── powersolver-template/       problem-type starter definitions
├── timefold-templates/         sequence-mode starter specs
├── scripts/quality-gate.*      full gate (§6.4)
└── documentation/

3. Engine

3.1 Request flow

  1. ProblemParser.parse turns the JSON into a Problem and runs ProblemValidator. Any issue → HTTP 400 with a list of {code, path, message} (on /api/check: verdict INVALID).
  2. /api/check → FeasibilityCheck.check(problem) (no solving): data that breaks a rule by itself, empty windows, dependency loops, pins that break a rule, items with no option, chains that cannot fit, supply against demand (Hall's condition). Each finding has a code, the rule, an English message, items and params (the facts, used by clients to write the sentence in their language). With ?trialSeconds=N a short solve runs when nothing is impossible.
  3. /api/solve → SolverService: DomainAssembler generates the classes, ConstraintBuilder builds one Timefold constraint per rule, SearchSettings configures the search (construction heuristic, local search, combined moves for 2+ decisions, unimprovedSeconds, maxSteps), InitialSolutionBuilder applies pins, Timefold solves, SolutionResultBuilder and ScoreBreakdown write assignments, shadows and the per-rule score.
  4. ResponseEnvelope wraps every answer: jobId, status (success | partial | failed), errors[], payload, trace.

3.2 Rules — three implementations that must agree

Every rule is implemented three times, on purpose:

WhereRole
ConstraintBuilderwhat the solver optimises (Timefold constraint streams)
ScoreBreakdowncounts each rule's violations in a solution (Results page, feasibility of pins)
powersolver-verify/verify_solution.pyan independent Python re-implementation that re-checks solutions

catalog.json is the single description of each rule (parameters, kinds, counting text); RULE_REFERENCE.md is generated from it (python -I powersolver-verify/render_rule_reference.py).

Rules that accept resource_property (resource_unavailable, skill_match, capacity) can point at a resource fixed on each item (a reference list) instead of a decision; ProblemValidator.checkResourceSource requires exactly one of variable_field / resource_property.

3.3 Adding a rule — checklist

  1. rules/catalog.json: id, label, sentence, counting, default severity, variable types, parameters (kind, value type, required, missing-on-item behaviour).
  2. ConstraintBuilder: the builder, registered in the switch. RuleCatalogTest checks that required/optional parameters match what the builder reads.
  3. ScoreBreakdown: the counting, identical to the builder.
  4. verify_solution.py: the same counting in Python; add examples in powersolver-verify/rule-examples/.
  5. ProblemValidator: any data check that makes the rule meaningless if wrong.
  6. FeasibilityCheck: add the rule to MONOTONE_RULES if a violation among pinned items can never be repaired; add a check if the data alone can make the rule impossible.
  7. Wizard js/rule-catalog.js: the rule, its parameters and value types (rule-catalog.test.mjs compares with the engine catalogue); labels and descriptions rules.<id>.* in the 5 locales.
  8. Regenerate RULE_REFERENCE.md; run node tools/sync-shared.mjs (the catalogue is shared with PowerOrchestrator).
  9. Run the quality gate (§6.4).

3.4 Limits (application.yml)

max-items 5,000 per collection (→ 400 TOO_LARGE), max-timeout-seconds 600 (→ 400 BAD_CONFIG), max-concurrent-solves 4 (→ 429 BUSY, Retry-After: 30).

4. Wizard

ModuleRole
wizard-state.jsAlpine store: problem type, entities, variables (decisions) with their values, reference lists, constraints, pins, config
app.jsboot, navigation, importProblem (JSON → store), exportProblem, step bar
steps/step1..7.jsthe steps; step7._buildJson writes the problem JSON
spec-shape.jsreads the shape of a problem (entity collection, decisions) from JSON or from the store
rule-catalog.jsthe rules the wizard may offer, their parameters and pickers (mirrors the engine catalogue)
components/data-import.jsExcel/CSV/paste import: header mapping, value conversion as the engine reads values, row/column problems, workbook import
workbook-setup.jsthe plan workbook: Setup sheet ↔ problem skeleton, what a loaded workbook holds per step, Fill all steps now, step banners, template sheets (also used by tools/make-demo-kit.mjs)
rule-import.jsRules sheet ↔ constraints, checked against steps 1-3 with the same options as the step 4 form
services/plans-api.jsPlansAPI: the stored plans (/api/problems…); Plan it stores the problem (sessionStorage.psProblemId) and solves it through /api/problems/{id}/solve, falling back to /api/solve on an engine without stored plans
feasibility-check.jsthe Can this work? panel: calls /api/check, writes each finding from its params in the user's language, quick plan
feedback.jsone registry of every user-facing check and error: severity (block / review / info), the wizard step that fixes it, words in locales/*.json → feedback; turns engine JSON paths into "step 4: Rules › rule 'X' › setting 'y'"
result-model.js, result-view.jsResults page: verdict, KPIs, checks (Check before you use it), plan grid; shared with PowerOrchestrator
plan-calendar.jscalendar over the planning period, Excel export of grids
replan-diff.jsChanges tab and compare.html: what changed between plans and in their data
solution-common.js, run-files.jsshared helpers, downloading problem/solution files

Form fields per problem type (PowerAdmin wizard_fields): one row per field the wizard shows, keyed by problem_type, time_grain (assignment or sequence), step_number (2 = items, 3 = options) and entity_or_value_type (Standard, or Temporal for time-slot lists); then field_name, field_type (text, number, select, checkbox, datetime-local), label, placeholder, help text, is_required, is_visible, sort_order. A required field makes the Excel import refuse a sheet without that column. Columns that match no field are kept as extra properties. Changing the fields is a PowerAdmin data change, not a wizard release.

User-facing messages: never write a sentence for a check or an engine code in a page. Add the code to feedback.js (severity, fix step) and its words under feedback.<code> in the 5 locales; feedback.test.mjs fails otherwise (it also reads the engine sources for codes the engine can send).

Shared with PowerOrchestrator: node tools/sync-shared.mjs copies the shared files and the results and feedback texts to PowerOrchestrator/powerorchestrator-ui, and the English feedback texts to powerorchestrator-api/app/engine/solver_feedback_en.json. --check only reports drift; shared-files.test.mjs fails on drift.

Themes and design tokens (full reference: Layout Guide): two themes, Midnight + Violet (violet, default) and Charcoal + Amber (amber), meant as the design system of all PowerXXXX products (applied to PowerSolver only so far).

  • css/theme.css is the only place colours, the font (Plus Jakarta Sans), type sizes, spacing, radii and shadows are defined: palette tokens per theme (--ps-bg, --ps-surface, --ps-surface-raised, --ps-surface-selected, --ps-border, --ps-border-strong, --ps-border-input, --ps-text, --ps-text-secondary, --ps-text-muted, --ps-accent and its hover / active / soft / text / on-accent variants), status colours, and Bootstrap mapped onto them. Older names (--wizard-*, --primary-color...) are aliases. Components use tokens only, never a raw colour.
  • js/theme.js (loaded in every page's <head>) sets <html data-theme> before the page paints, keeps the choice in localStorage (ppt_theme, like ppt_language), updates the open pages and mounts the switch (an accessible radio group) in the sidebar. A theme changes only the look, never the plan or its settings.
  • Logo: one mark per theme (logo-violet.svg, logo-amber.svg; inline symbols in index.html, shown by theme).
  • Contrast (WCAG AA) is designed in: text 4.5:1 on every surface (--ps-accent-text for accent-coloured text), form field edges 3:1 (--ps-border-input).
  • Files shared with PowerOrchestrator write var(--ps-x, <old colour>) or color-mix(... var(--ps-x, <old colour>) ...), so they follow the theme here and look unchanged there; canvas charts read tokens at draw time (themeColour in solution-common.js). Data palettes (category and priority colours) stay theme-independent.

Before shipping wizard changes: node tools/check-i18n.mjs (every key used exists in every locale) and node tools/stamp-version.mjs (cache-busting ?v= on script tags).

Stored plans (SQLite)

store/Database opens powersolver.store.path (default /data/powersolver.db) with WAL, foreign keys and a 5 s busy timeout on every connection, and applies db/migrations/NNNN_name.sql in order at start (table schema_migrations). A released migration is never edited: a change is a new numbered file. Writes go through Database.inTransaction (one writer at a time). Ids are UUIDv7 text, dates ISO-8601 UTC, every business row has created_at/by, updated_at/by, owner, deleted_at/by; audit_log records every change (actor_verified 0 until the product has its own users). These conventions are shared with PowerForecast and PowerDecision, so the users / roles and AI tables planned later can be added the same way in the three products.

5. Running locally

cd powersolver-api
mvn -q -o package -DskipTests
java -jar target/powersolver-api-2.0.0.jar --server.port=8091

Serve the wizard with any static server (python -m http.server 8092 in powersolver-wizard), sign in with a PowerAdmin account, and point it at a local engine with ?solver=http://localhost:8091 or localStorage.setItem('powersolver_solver_url_override', 'http://localhost:8091').

Build a demo workbook from any problem: node powersolver-wizard/tools/make-demo-kit.mjs <problem.json>.

6. Tests

WhatCommandCovers
Enginecd powersolver-api && mvn -q -o testparser, validator, every rule, feasibility checks (incl. real demo cases), controller, search
Engine, all demos solved and exportedmvn -o -q test -Ddifferential=true -Dtest=DifferentialExportTestsolves every shipped and generated problem
Independent checkercd powersolver-verify && python -m pytest -qre-checks every exported solution and the rule examples; the rule reference is up to date
Wizardcd powersolver-wizard && for t in tests/*.test.mjs; do node $t; doneimports, rules sheet, workbook kits (every demo kit gives its shipped problem), messages on every demo in 5 languages, feasibility panel, Results, replan diff, shared files
Full gatescripts/quality-gate.sh (or .ps1)generates 500 problems, engine tests, differential export, independent checker

Details: TESTS_USER_GUIDE.md.

7. Conventions

  • Java 21; no Lombok; constraint code mirrors the catalogue's counting text.
  • JavaScript: no framework beyond Alpine; pure functions where possible so that node tests can load them in a vm.
  • Every user-facing sentence goes through the locales (5 languages); the English fallback in code is the same text.
  • A plan or a check that cannot see a problem says so (e.g. Can this work? "no problem found — not a guarantee").
  • Timefold upgrades follow TIMEFOLD_UPGRADES.md.
  • Documentation: the Markdown in documentation/ is the only source; after editing it run python -I documentation/tools/build_html.py to regenerate documentation/html/. A temporary plan or study does not belong in documentation/: fold its lasting content into the guides.