Spaces:
Sleeping
Sleeping
Commit Β·
a38dd50
1
Parent(s): 9b2479e
docs(app): document hospital crates.io workflow
Browse filesDocument the hospital app structure, planning_model manifest, retained runtime behavior, validation commands, and Docker workflow across the canonical repo docs.
Update the dependency background to published solverforge 0.9.0 and solverforge-ui 0.6.1, with no sibling path setup required.
AGENTS.md
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Repository Guidelines
|
| 2 |
+
|
| 3 |
+
## Project Structure & Module Organization
|
| 4 |
+
|
| 5 |
+
`src/` follows the current `solverforge-cli` app shape: `api/` for HTTP
|
| 6 |
+
routes, SSE, and DTOs; `solver/` for retained-job orchestration; `domain/mod.rs`
|
| 7 |
+
for the current `solverforge::planning_model!` manifest; `domain/` for the
|
| 8 |
+
exported model modules; `constraints/mod.rs` for constraint assembly; and
|
| 9 |
+
`constraints/*.rs` for the individual score rules. `data/mod.rs` is the stable
|
| 10 |
+
wrapper; `data/data_seed.rs` is the thin public data surface; and
|
| 11 |
+
`data/data_seed/` holds the deterministic sample dataset modules, including the
|
| 12 |
+
public entrypoints and the `LARGE` instance builder. `domain/plan.rs` owns
|
| 13 |
+
`Plan`, the scalar-variable `Shift`, and the scalar nearby hook functions;
|
| 14 |
+
`domain/employee.rs` and `domain/care_hub.rs` hold supporting domain types.
|
| 15 |
+
`static/` holds the browser app (`static/app/**/*.mjs`) and generated UI config.
|
| 16 |
+
Frontend tests live in `tests/frontend/`. Container packaging is defined by
|
| 17 |
+
`Dockerfile`.
|
| 18 |
+
|
| 19 |
+
This project depends on the published `solverforge` and `solverforge-ui` crates.
|
| 20 |
+
|
| 21 |
+
## Build, Test, and Development Commands
|
| 22 |
+
|
| 23 |
+
- `make help` β show the supported local development and validation commands.
|
| 24 |
+
- `make run-release` β run the app locally on `:7860`.
|
| 25 |
+
- `make test` β run the standard Rust and frontend validation surface.
|
| 26 |
+
- `make ci-local` β run the Space-oriented local CI pipeline: fmt, clippy,
|
| 27 |
+
release build, standard tests, and Docker image build.
|
| 28 |
+
- `make test-slow` β run the ignored large-demo acceptance solve.
|
| 29 |
+
- `make pre-release` β run `make ci-local` plus the slow acceptance solve.
|
| 30 |
+
- `make space-build` β build the Docker image used by the Docker-based Space
|
| 31 |
+
deployment path.
|
| 32 |
+
- `cargo run --release --bin solverforge-hospital` β run the app locally on
|
| 33 |
+
`:7860`.
|
| 34 |
+
- `cargo test` β run Rust unit and integration tests.
|
| 35 |
+
- `cargo test large_demo_solves_to_feasible_terminal_state -- --ignored --nocapture`
|
| 36 |
+
β slow end-to-end solver acceptance test.
|
| 37 |
+
- `find static/app -name '*.mjs' -print0 | xargs -0 -n1 node --check` β
|
| 38 |
+
syntax-check frontend modules.
|
| 39 |
+
- `node --test tests/frontend/*.test.js` β run frontend tests.
|
| 40 |
+
- `docker build -f Dockerfile -t solverforge-hospital .` β build the image from
|
| 41 |
+
the repository root context.
|
| 42 |
+
|
| 43 |
+
## Coding Style & Naming Conventions
|
| 44 |
+
|
| 45 |
+
Use Rust 2021 style with `cargo fmt`; keep imports and formatting
|
| 46 |
+
rustfmt-compatible. Prefer small, explicit functions over clever indirection.
|
| 47 |
+
Rust module and file names are `snake_case`; types are `UpperCamelCase`; tests
|
| 48 |
+
should describe behavior plainly. Frontend modules are plain ES modules in
|
| 49 |
+
`snake-case` filenames. Keep generator logic deterministic: do not introduce
|
| 50 |
+
random behavior without a fixed seed and an explicit reason.
|
| 51 |
+
|
| 52 |
+
## Documentation And Commenting Policy
|
| 53 |
+
|
| 54 |
+
Assume a beginner reader who is new to SolverForge and new to optimization
|
| 55 |
+
modeling.
|
| 56 |
+
|
| 57 |
+
- Treat `README.md`, `WIREFRAME.md`, this file, `solver.toml` comments, and the
|
| 58 |
+
visible API help in `static/app/shell/api-guide.mjs` as one canonical
|
| 59 |
+
documentation surface. When one changes, audit the others that describe the
|
| 60 |
+
same behavior.
|
| 61 |
+
- `PRD.md` is background/planning context only. It may be useful, but do not
|
| 62 |
+
treat it as canonical current-state documentation.
|
| 63 |
+
- Add module-level docs or comments for every new module that explain its role
|
| 64 |
+
in the app and where it sits in the data flow.
|
| 65 |
+
- Add function comments when the function does real coordination work, rebuilds
|
| 66 |
+
invariants, shapes demo data, converts between layers, or otherwise does
|
| 67 |
+
something a beginner would not infer immediately from the signature.
|
| 68 |
+
- Write comments that explain intent, domain meaning, invariants, and runtime
|
| 69 |
+
consequences. Do not write comments that merely restate syntax.
|
| 70 |
+
- Keep comments truthful. If behavior changes, update or delete the stale
|
| 71 |
+
comment in the same patch.
|
| 72 |
+
- When docs mention versions, counts, routes, solver policy, or validation
|
| 73 |
+
expectations, verify those facts against the current code and tests in the
|
| 74 |
+
same turn.
|
| 75 |
+
- Prefer present-tense current-state docs after a refactor lands. Do not leave
|
| 76 |
+
future-tense planning language in repo docs unless the file is intentionally a
|
| 77 |
+
still-pending plan.
|
| 78 |
+
- When onboarding surfaces change, keep `README.md`, `WIREFRAME.md`, and this
|
| 79 |
+
file aligned. Update `PRD.md` only if its background note would otherwise
|
| 80 |
+
become misleading.
|
| 81 |
+
|
| 82 |
+
The standard to aim for is: a new reader should be able to understand why a
|
| 83 |
+
piece of code exists before they need to understand every line of how it works.
|
| 84 |
+
|
| 85 |
+
This repo does not have a canonical hosted GitHub Actions workflow yet. Treat
|
| 86 |
+
the Makefile targets, especially `make ci-local` and `make pre-release`, as the
|
| 87 |
+
authoritative validation surface for the current Hugging Face Space-oriented
|
| 88 |
+
deployment path.
|
| 89 |
+
|
| 90 |
+
## Testing Guidelines
|
| 91 |
+
|
| 92 |
+
Add Rust tests next to the behavior they protect, usually in `src/...`
|
| 93 |
+
`#[cfg(test)]` modules. Frontend behavior belongs in `tests/frontend/` and
|
| 94 |
+
should use the existing fake DOM support in `tests/support/`. If you change
|
| 95 |
+
solver behavior, run both `cargo test` and the ignored large-demo solve. If you
|
| 96 |
+
change UI modules, run the Node syntax check and frontend tests.
|
| 97 |
+
|
| 98 |
+
## Commit & Pull Request Guidelines
|
| 99 |
+
|
| 100 |
+
Follow the workspace commit style seen upstream: conventional prefixes such as
|
| 101 |
+
`fix(...)`, `feat(...)`, `refactor(...)`, `test(...)`, and `chore(...)` (for
|
| 102 |
+
example, `fix(runtime): route pure scalar construction to descriptor path`).
|
| 103 |
+
PRs should state user-visible impact, changed config or API surface, and the
|
| 104 |
+
exact validation commands run. Include screenshots only for visible UI changes.
|
| 105 |
+
|
| 106 |
+
## Configuration & Runtime Notes
|
| 107 |
+
|
| 108 |
+
`solver.toml` is embedded from `domain/plan.rs` via
|
| 109 |
+
`#[planning_solution(..., solver_toml = "../../solver.toml")]`; treat it as
|
| 110 |
+
the runtime source of truth. Keep `solverforge.app.toml`,
|
| 111 |
+
`static/sf-config.json`, and Docker/runtime port settings aligned with any port
|
| 112 |
+
or route changes.
|
PRD.md
ADDED
|
@@ -0,0 +1,172 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# PRD: Current `solverforge-hospital` Product Contract
|
| 2 |
+
|
| 3 |
+
This file is background/planning context. It is not canonical for current
|
| 4 |
+
behavior. The current source of truth is the code, `solver.toml`, `README.md`,
|
| 5 |
+
`WIREFRAME.md`, and `AGENTS.md`.
|
| 6 |
+
|
| 7 |
+
## Summary
|
| 8 |
+
|
| 9 |
+
`solverforge-hospital` is now a `solverforge 0.9.0` and `solverforge-ui 0.6.1`
|
| 10 |
+
example. This repo demonstrates current retained-runtime integration, current
|
| 11 |
+
stock UI-shell usage, and current scalar nearby-selection support.
|
| 12 |
+
|
| 13 |
+
The objective of the repo is not to force every upstream neighborhood family
|
| 14 |
+
into the hospital example. The objective is to keep the example truthful,
|
| 15 |
+
current, teachable, and productive on its own dataset.
|
| 16 |
+
|
| 17 |
+
## Product Shape
|
| 18 |
+
|
| 19 |
+
### Backend
|
| 20 |
+
|
| 21 |
+
- Rust/Axum retained-job backend
|
| 22 |
+
- `Plan` planning solution in the current `solverforge-cli` backend shape
|
| 23 |
+
- scalar planning variable: `Shift.employee_idx`
|
| 24 |
+
- exact runtime telemetry from `SolverTelemetry`
|
| 25 |
+
- hospital-specific transport projection for UI metrics
|
| 26 |
+
|
| 27 |
+
### Frontend
|
| 28 |
+
|
| 29 |
+
- stock `SF.createBackend({ type: "axum" })`
|
| 30 |
+
- stock `SF.createHeader()`
|
| 31 |
+
- stock `SF.createStatusBar()`
|
| 32 |
+
- stock `SF.createSolver()`
|
| 33 |
+
- stock `SF.rail.createTimeline()`
|
| 34 |
+
|
| 35 |
+
### Data
|
| 36 |
+
|
| 37 |
+
- deterministic `LARGE` dataset
|
| 38 |
+
- `50` employees
|
| 39 |
+
- `688` shifts
|
| 40 |
+
- feasible and stable under the shipped solver policy
|
| 41 |
+
|
| 42 |
+
## 0.9.0 Alignment Decisions
|
| 43 |
+
|
| 44 |
+
### Versions
|
| 45 |
+
|
| 46 |
+
All surfaced versions align to:
|
| 47 |
+
|
| 48 |
+
- `solverforge` `0.9.0`
|
| 49 |
+
- `solverforge-ui` `0.6.1`
|
| 50 |
+
- app/chart metadata `0.9.0`
|
| 51 |
+
|
| 52 |
+
No active docs or package metadata should describe the app as an `0.8.x`
|
| 53 |
+
example.
|
| 54 |
+
|
| 55 |
+
### Nearby selection
|
| 56 |
+
|
| 57 |
+
Hospital exercises the scalar nearby-selection surface shipped in
|
| 58 |
+
`solverforge 0.9.0`.
|
| 59 |
+
|
| 60 |
+
To make nearby honest instead of guessed, the domain adds:
|
| 61 |
+
|
| 62 |
+
- `CareHub`
|
| 63 |
+
- `Employee.home_hub`
|
| 64 |
+
- `Shift.care_hub`
|
| 65 |
+
- scalar nearby meters on `Shift.employee_idx`
|
| 66 |
+
|
| 67 |
+
Those meters bias:
|
| 68 |
+
|
| 69 |
+
- shift -> employee by care-hub fit, required-skill fit, and obvious
|
| 70 |
+
infeasibility
|
| 71 |
+
- shift -> shift by care-hub proximity and start-time band
|
| 72 |
+
|
| 73 |
+
### Shipped solver policy
|
| 74 |
+
|
| 75 |
+
The shipped [solver.toml](./solver.toml) is:
|
| 76 |
+
|
| 77 |
+
- `construction_heuristic(cheapest_insertion)`
|
| 78 |
+
- explicit `local_search`
|
| 79 |
+
- explicit scalar nearby neighborhoods:
|
| 80 |
+
- `nearby_change_move_selector`
|
| 81 |
+
- `nearby_swap_move_selector`
|
| 82 |
+
- `late_acceptance`
|
| 83 |
+
- `accepted_count` forager
|
| 84 |
+
|
| 85 |
+
Reason:
|
| 86 |
+
|
| 87 |
+
- this uses the new 0.9.0 scalar nearby surface directly
|
| 88 |
+
- it produces real post-construction improvement on the large demo
|
| 89 |
+
- it remains feasible and fast enough for a 30-second example run
|
| 90 |
+
|
| 91 |
+
Hospital does not ship a broader scalar union here just because upstream now
|
| 92 |
+
supports it. On this dataset:
|
| 93 |
+
|
| 94 |
+
- adding pillar and ruin-recreate makes the first local-search step too large
|
| 95 |
+
- `VND` can absorb the full global time budget before later phases run
|
| 96 |
+
|
| 97 |
+
Those are runtime/product facts, not reasons to misdocument the app.
|
| 98 |
+
|
| 99 |
+
### Truthfulness about upstream surface
|
| 100 |
+
|
| 101 |
+
SolverForge `0.9.0` ships more scalar neighborhoods than hospital currently
|
| 102 |
+
uses. Hospital documents that honestly:
|
| 103 |
+
|
| 104 |
+
- upstream ships nearby, pillar, and ruin-recreate for scalar variables
|
| 105 |
+
- hospital currently opts into nearby-only local search because it is the most
|
| 106 |
+
productive configuration for this example
|
| 107 |
+
|
| 108 |
+
## Repository Surface
|
| 109 |
+
|
| 110 |
+
### Source
|
| 111 |
+
|
| 112 |
+
- `src/domain/mod.rs` is the current `planning_model!` manifest; `src/domain/plan.rs`
|
| 113 |
+
owns `Plan`, the scalar-variable `Shift`, and nearby meters;
|
| 114 |
+
`src/domain/employee.rs` and `src/domain/care_hub.rs` own the supporting
|
| 115 |
+
problem fact and proximity enum
|
| 116 |
+
- `src/constraints/mod.rs` is the canonical CLI-style assembler for
|
| 117 |
+
`create_constraints`; each hospital score rule lives in its own
|
| 118 |
+
`src/constraints/*.rs` module
|
| 119 |
+
- `src/data/mod.rs` follows the CLI stable-wrapper shape;
|
| 120 |
+
`src/data/data_seed.rs` orchestrates the generated dataset and
|
| 121 |
+
`src/data/data_seed/` owns the focused generator modules; the generator
|
| 122 |
+
assigns deterministic home hubs from workforce skill bundles
|
| 123 |
+
- `static/app/shell/config-loader.mjs` uses the explicit Axum backend adapter
|
| 124 |
+
- `solver.toml` reflects the shipped nearby-based solver policy
|
| 125 |
+
|
| 126 |
+
### Docs
|
| 127 |
+
|
| 128 |
+
- `README.md` markets the repo as `0.9.0` / `0.6.1` and acts as the
|
| 129 |
+
beginner-facing quick start
|
| 130 |
+
- `WIREFRAME.md` explains architecture and request/data flow
|
| 131 |
+
- this PRD explains why the shipped solver policy stays narrow
|
| 132 |
+
- contributor docs keep `solver.toml` as the runtime source of truth
|
| 133 |
+
- the browser-visible API guide in `static/app/shell/api-guide.mjs` stays in
|
| 134 |
+
sync with `src/api/routes.rs`
|
| 135 |
+
- inline comments explain non-obvious coordination and invariants for beginners
|
| 136 |
+
|
| 137 |
+
## Validation
|
| 138 |
+
|
| 139 |
+
Required checks:
|
| 140 |
+
|
| 141 |
+
- `cargo test --quiet`
|
| 142 |
+
- `cargo test large_demo_solves_to_feasible_terminal_state -- --ignored --nocapture`
|
| 143 |
+
- `find static/app -name '*.mjs' -print0 | xargs -0 -n1 node --check`
|
| 144 |
+
- `node --test tests/frontend/*.test.js`
|
| 145 |
+
|
| 146 |
+
Expected current outcome:
|
| 147 |
+
|
| 148 |
+
- large demo finishes `0hard`
|
| 149 |
+
- local search performs real post-construction improvement over construction
|
| 150 |
+
- docs, API guide, and config comments describe the same current product shape
|
| 151 |
+
|
| 152 |
+
## Non-Goals
|
| 153 |
+
|
| 154 |
+
This pass does not:
|
| 155 |
+
|
| 156 |
+
- redesign the hospital domain
|
| 157 |
+
- rewrite `solverforge-ui`
|
| 158 |
+
- force every `0.9.0` scalar neighborhood family into the app
|
| 159 |
+
- add benchmark tooling
|
| 160 |
+
- change the planning direction away from `shift -> employee`
|
| 161 |
+
|
| 162 |
+
## Final Intent
|
| 163 |
+
|
| 164 |
+
`solverforge-hospital` reads as a clean example of:
|
| 165 |
+
|
| 166 |
+
- current retained-runtime backend integration
|
| 167 |
+
- current stock `solverforge-ui` shell usage
|
| 168 |
+
- current scalar nearby-selection support in `solverforge 0.9.0`
|
| 169 |
+
|
| 170 |
+
Nothing in the repo should imply that hospital is pinned to an older release
|
| 171 |
+
line or that unshipped broader scalar neighborhoods are the recommended
|
| 172 |
+
hospital policy.
|
README.md
ADDED
|
@@ -0,0 +1,306 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
title: SolverForge Hospital
|
| 3 |
+
emoji: π₯
|
| 4 |
+
colorFrom: orange
|
| 5 |
+
colorTo: red
|
| 6 |
+
sdk: docker
|
| 7 |
+
app_port: 7860
|
| 8 |
+
pinned: false
|
| 9 |
+
license: apache-2.0
|
| 10 |
+
short_description: SolverForge 0.9.0 hospital scheduling example with deterministic demo data and a retained runtime
|
| 11 |
+
---
|
| 12 |
+
|
| 13 |
+
# SolverForge Hospital
|
| 14 |
+
|
| 15 |
+
`solverforge-hospital` is a beginner-friendly example of a real SolverForge app.
|
| 16 |
+
It answers one concrete question:
|
| 17 |
+
|
| 18 |
+
"Given a hospital workforce and a month of shifts, which employee should cover each shift?"
|
| 19 |
+
|
| 20 |
+
## Documentation Map
|
| 21 |
+
|
| 22 |
+
Each top-level document has a different job:
|
| 23 |
+
|
| 24 |
+
- `README.md`
|
| 25 |
+
Quick start, concepts, API surface, and the shortest learning path.
|
| 26 |
+
- `WIREFRAME.md`
|
| 27 |
+
Architecture and request/data flow across backend, runtime, and frontend.
|
| 28 |
+
- `PRD.md`
|
| 29 |
+
Background/planning notes only. It is not canonical for current behavior.
|
| 30 |
+
- `AGENTS.md`
|
| 31 |
+
Contribution rules, validation commands, and the documentation standard for future edits.
|
| 32 |
+
- `Makefile`
|
| 33 |
+
The beginner-friendly command surface for local development, validation, and
|
| 34 |
+
Docker-based Hugging Face Space preparation.
|
| 35 |
+
|
| 36 |
+
If you are new to SolverForge, read this repo as three layers:
|
| 37 |
+
|
| 38 |
+
1. The planning-model manifest and model modules in `src/domain/`
|
| 39 |
+
2. The score rules in `src/constraints/`
|
| 40 |
+
3. The runtime and browser app in `src/api/`, `src/solver/`, and `static/app/`
|
| 41 |
+
|
| 42 |
+
## What SolverForge Is Doing Here
|
| 43 |
+
|
| 44 |
+
SolverForge is the optimization engine. In this app:
|
| 45 |
+
|
| 46 |
+
- `Employee` is a problem fact: input data the solver does not move
|
| 47 |
+
- `Shift` is the planning entity: the thing the solver assigns
|
| 48 |
+
- `Shift.employee_idx` is the planning variable: the actual choice the solver makes
|
| 49 |
+
- the constraints decide whether an assignment is legal and whether it is good
|
| 50 |
+
- `solver.toml` tells the runtime how to search for a better assignment
|
| 51 |
+
|
| 52 |
+
The app ships one serious demo instance rather than many toy presets:
|
| 53 |
+
|
| 54 |
+
- fixed random seed
|
| 55 |
+
- fixed 28-day schedule horizon
|
| 56 |
+
- 50 employees
|
| 57 |
+
- 688 shifts
|
| 58 |
+
- 8-hour shifts
|
| 59 |
+
- deterministic generator
|
| 60 |
+
- retained-job runtime with pause, resume, cancel, snapshot, and analysis
|
| 61 |
+
|
| 62 |
+
## Read The Code In This Order
|
| 63 |
+
|
| 64 |
+
If you want to learn the codebase, this order is the shortest path:
|
| 65 |
+
|
| 66 |
+
1. [src/domain/employee.rs](src/domain/employee.rs)
|
| 67 |
+
`Employee` is the input fact model.
|
| 68 |
+
2. [src/domain/care_hub.rs](src/domain/care_hub.rs)
|
| 69 |
+
`CareHub` explains how the app models service-line proximity.
|
| 70 |
+
3. [src/domain/mod.rs](src/domain/mod.rs)
|
| 71 |
+
This is the `planning_model!` manifest that lists and exports the model modules.
|
| 72 |
+
4. [src/domain/plan.rs](src/domain/plan.rs)
|
| 73 |
+
`Shift`, `Plan`, nearby search meters, and transport normalization live here.
|
| 74 |
+
5. [src/constraints/mod.rs](src/constraints/mod.rs)
|
| 75 |
+
This lists every scoring rule.
|
| 76 |
+
6. [src/constraints/*.rs](src/constraints)
|
| 77 |
+
Each file explains one real scheduling rule.
|
| 78 |
+
7. [src/data/data_seed/entrypoints.rs](src/data/data_seed/entrypoints.rs)
|
| 79 |
+
This shows the public demo-data surface.
|
| 80 |
+
8. [src/data/data_seed/large.rs](src/data/data_seed/large.rs)
|
| 81 |
+
This assembles the published benchmark instance.
|
| 82 |
+
9. [src/solver/service.rs](src/solver/service.rs)
|
| 83 |
+
This is the retained-job runtime facade.
|
| 84 |
+
10. [src/api/routes.rs](src/api/routes.rs) and [src/api/sse.rs](src/api/sse.rs)
|
| 85 |
+
These expose the HTTP and live-event contracts.
|
| 86 |
+
11. [static/app/main.mjs](static/app/main.mjs)
|
| 87 |
+
This is the browser boot sequence.
|
| 88 |
+
12. [static/app/shell/](static/app/shell) and [static/app/schedule/](static/app/schedule)
|
| 89 |
+
These adapt stock `solverforge-ui` pieces to the hospital example.
|
| 90 |
+
|
| 91 |
+
## Project Shape
|
| 92 |
+
|
| 93 |
+
- `src/domain/`
|
| 94 |
+
`planning_model!` manifest, planning model, derived fields, nearby meters.
|
| 95 |
+
- `src/constraints/`
|
| 96 |
+
Hard and soft scoring rules.
|
| 97 |
+
- `src/data/`
|
| 98 |
+
Deterministic demo-data generator.
|
| 99 |
+
- `src/solver/`
|
| 100 |
+
Retained-job orchestration over `SolverManager<Plan>`.
|
| 101 |
+
- `src/api/`
|
| 102 |
+
REST routes, DTOs, and SSE endpoint.
|
| 103 |
+
- `static/app/`
|
| 104 |
+
Browser code built as plain ES modules on top of `solverforge-ui`.
|
| 105 |
+
- `tests/frontend/`
|
| 106 |
+
Browserless UI tests using the fake DOM in `tests/support/`.
|
| 107 |
+
|
| 108 |
+
## Quick Start
|
| 109 |
+
|
| 110 |
+
This repo depends only on published crates.io releases:
|
| 111 |
+
|
| 112 |
+
- `solverforge = 0.9.0`
|
| 113 |
+
- `solverforge-ui = 0.6.1`
|
| 114 |
+
|
| 115 |
+
Run the app:
|
| 116 |
+
|
| 117 |
+
```sh
|
| 118 |
+
make run-release
|
| 119 |
+
```
|
| 120 |
+
|
| 121 |
+
Then open `http://localhost:7860`.
|
| 122 |
+
|
| 123 |
+
If you want to inspect the command surface first:
|
| 124 |
+
|
| 125 |
+
```sh
|
| 126 |
+
make help
|
| 127 |
+
```
|
| 128 |
+
|
| 129 |
+
## What You Will See
|
| 130 |
+
|
| 131 |
+
The browser UI does five things:
|
| 132 |
+
|
| 133 |
+
1. Loads `static/sf-config.json`
|
| 134 |
+
2. Loads `static/generated/ui-model.json`
|
| 135 |
+
3. Fetches the `LARGE` demo dataset from `/demo-data/LARGE`
|
| 136 |
+
4. Renders two schedule views:
|
| 137 |
+
`By location` and `By employee`
|
| 138 |
+
5. Starts a retained solving job when you click Solve
|
| 139 |
+
|
| 140 |
+
The frontend is intentionally thin. It mostly uses stock `solverforge-ui` pieces:
|
| 141 |
+
|
| 142 |
+
- `SF.createBackend({ type: "axum" })`
|
| 143 |
+
- `SF.createHeader()`
|
| 144 |
+
- `SF.createStatusBar()`
|
| 145 |
+
- `SF.createSolver()`
|
| 146 |
+
- `SF.rail.createTimeline()`
|
| 147 |
+
|
| 148 |
+
The app also exposes a visible "REST API" guide inside the browser shell. That
|
| 149 |
+
guide is generated from `static/app/shell/api-guide.mjs` and is expected to
|
| 150 |
+
match the routes in `src/api/routes.rs` and the route list below.
|
| 151 |
+
|
| 152 |
+
## Run Validation
|
| 153 |
+
|
| 154 |
+
Standard local validation:
|
| 155 |
+
|
| 156 |
+
```sh
|
| 157 |
+
make test
|
| 158 |
+
```
|
| 159 |
+
|
| 160 |
+
Space-style local CI simulation:
|
| 161 |
+
|
| 162 |
+
```sh
|
| 163 |
+
make ci-local
|
| 164 |
+
```
|
| 165 |
+
|
| 166 |
+
Slow acceptance solve:
|
| 167 |
+
|
| 168 |
+
```sh
|
| 169 |
+
make test-slow
|
| 170 |
+
```
|
| 171 |
+
|
| 172 |
+
`make ci-local` is the main pre-push validation path for this repo. It checks
|
| 173 |
+
formatting, runs clippy, builds the release binary, runs the standard Rust and
|
| 174 |
+
frontend test surface, and builds the Docker image that the Space-style deploy
|
| 175 |
+
path expects. There is no hosted GitHub Actions workflow to mirror yet.
|
| 176 |
+
|
| 177 |
+
## Demo Dataset Intent
|
| 178 |
+
|
| 179 |
+
The generator is not random filler data. It is designed to publish a problem
|
| 180 |
+
that is:
|
| 181 |
+
|
| 182 |
+
- hard-feasible
|
| 183 |
+
- deterministic
|
| 184 |
+
- narrow enough that candidate choice matters
|
| 185 |
+
- rich enough that soft-score improvements still exist after construction
|
| 186 |
+
|
| 187 |
+
The hidden witness roster in `src/data/data_seed/witness.rs` is especially
|
| 188 |
+
important: it gives the generator an internal feasible assignment that is used
|
| 189 |
+
to shape unavailability and preferences, but it is never shown to the solver.
|
| 190 |
+
|
| 191 |
+
The `LARGE` dataset is built once and cached in memory because it is immutable
|
| 192 |
+
and deterministic. Each request still receives an owned `Plan`, but the server
|
| 193 |
+
does not regenerate the same public benchmark from scratch every time.
|
| 194 |
+
|
| 195 |
+
## REST API
|
| 196 |
+
|
| 197 |
+
- `GET /health`
|
| 198 |
+
- `GET /info`
|
| 199 |
+
- `GET /demo-data`
|
| 200 |
+
- `GET /demo-data/{id}`
|
| 201 |
+
- `POST /jobs`
|
| 202 |
+
- `GET /jobs/{id}`
|
| 203 |
+
- `GET /jobs/{id}/status`
|
| 204 |
+
- `GET /jobs/{id}/snapshot`
|
| 205 |
+
- `GET /jobs/{id}/analysis`
|
| 206 |
+
- `POST /jobs/{id}/pause`
|
| 207 |
+
- `POST /jobs/{id}/resume`
|
| 208 |
+
- `POST /jobs/{id}/cancel`
|
| 209 |
+
- `DELETE /jobs/{id}`
|
| 210 |
+
- `GET /jobs/{id}/events`
|
| 211 |
+
|
| 212 |
+
Lifecycle semantics:
|
| 213 |
+
|
| 214 |
+
- `pause` requests an exact runtime-managed pause and checkpoint
|
| 215 |
+
- `resume` continues from the retained checkpoint
|
| 216 |
+
- the user-facing Stop control calls `cancel` to stop a live or paused job
|
| 217 |
+
- `delete` removes a terminal retained job before the next fresh solve
|
| 218 |
+
- `GET /jobs/{id}` and `GET /jobs/{id}/status` return the same summary payload
|
| 219 |
+
- `snapshot_revision={n}` is optional on both snapshot and analysis requests
|
| 220 |
+
- reconnects bootstrap from current runtime status plus retained snapshot
|
| 221 |
+
revision, not from cached SSE text
|
| 222 |
+
|
| 223 |
+
## Payload Shape
|
| 224 |
+
|
| 225 |
+
The transport payload mirrors the domain model directly. The important field is
|
| 226 |
+
`employeeIdx`, which is the scalar planning assignment chosen for each shift.
|
| 227 |
+
|
| 228 |
+
```json
|
| 229 |
+
{
|
| 230 |
+
"employees": [
|
| 231 |
+
{
|
| 232 |
+
"id": "employee-0",
|
| 233 |
+
"name": "Alex Smith",
|
| 234 |
+
"homeHub": "critical_care",
|
| 235 |
+
"skills": ["Critical care doctor"],
|
| 236 |
+
"unavailableDates": [],
|
| 237 |
+
"undesiredDates": [],
|
| 238 |
+
"desiredDates": []
|
| 239 |
+
}
|
| 240 |
+
],
|
| 241 |
+
"shifts": [
|
| 242 |
+
{
|
| 243 |
+
"id": "shift-1",
|
| 244 |
+
"start": "2024-01-01T08:00:00",
|
| 245 |
+
"end": "2024-01-01T16:00:00",
|
| 246 |
+
"location": "Critical care",
|
| 247 |
+
"careHub": "critical_care",
|
| 248 |
+
"requiredSkill": "Critical care doctor",
|
| 249 |
+
"employeeIdx": 0
|
| 250 |
+
}
|
| 251 |
+
],
|
| 252 |
+
"score": "0hard/0soft"
|
| 253 |
+
}
|
| 254 |
+
```
|
| 255 |
+
|
| 256 |
+
Telemetry is intentionally a UI-facing projection, not the raw runtime type.
|
| 257 |
+
Fields like `elapsedMs`, `movesPerSecond`, and `acceptanceRate` are derived for
|
| 258 |
+
display.
|
| 259 |
+
|
| 260 |
+
## Solver Policy
|
| 261 |
+
|
| 262 |
+
The runtime source of truth is [solver.toml](./solver.toml).
|
| 263 |
+
|
| 264 |
+
The currently shipped policy is deliberately narrow:
|
| 265 |
+
|
| 266 |
+
- `construction_heuristic = cheapest_insertion`
|
| 267 |
+
- one local-search phase
|
| 268 |
+
- nearby scalar change/swap selectors
|
| 269 |
+
- `late_acceptance`
|
| 270 |
+
- `accepted_count`
|
| 271 |
+
|
| 272 |
+
That is not an accident. A candidate sweep against broader scalar phase-2/3
|
| 273 |
+
variants showed that the current nearby baseline remained the best balanced
|
| 274 |
+
policy for this dataset and 30-second budget.
|
| 275 |
+
|
| 276 |
+
The canonical description of current behavior is `solver.toml`, the code, this
|
| 277 |
+
README, and `WIREFRAME.md`, not `PRD.md`.
|
| 278 |
+
|
| 279 |
+
## Constraints
|
| 280 |
+
|
| 281 |
+
Hard constraints:
|
| 282 |
+
|
| 283 |
+
- Assigned shift
|
| 284 |
+
- Required skill
|
| 285 |
+
- Overlapping shift
|
| 286 |
+
- At least 10 hours between 2 shifts
|
| 287 |
+
- One shift per day
|
| 288 |
+
- Unavailable employee
|
| 289 |
+
|
| 290 |
+
Soft constraints:
|
| 291 |
+
|
| 292 |
+
- Undesired day for employee
|
| 293 |
+
- Desired day for employee
|
| 294 |
+
- Balance employee assignments
|
| 295 |
+
|
| 296 |
+
## Docker
|
| 297 |
+
|
| 298 |
+
Build from this repository root:
|
| 299 |
+
|
| 300 |
+
```sh
|
| 301 |
+
make space-build
|
| 302 |
+
make space-run
|
| 303 |
+
```
|
| 304 |
+
|
| 305 |
+
The Docker image resolves `solverforge` and `solverforge-ui` from crates.io, so
|
| 306 |
+
no extra source checkout is required in the build context.
|
WIREFRAME.md
ADDED
|
@@ -0,0 +1,276 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# solverforge-hospital WIREFRAME
|
| 2 |
+
|
| 3 |
+
This file is the architectural map for beginners.
|
| 4 |
+
|
| 5 |
+
If `README.md` tells you how to run the app, this document tells you how the
|
| 6 |
+
pieces fit together and in which order to read them.
|
| 7 |
+
|
| 8 |
+
## Documentation Roles
|
| 9 |
+
|
| 10 |
+
The docs in this repo are meant to work together rather than compete:
|
| 11 |
+
|
| 12 |
+
- `README.md`
|
| 13 |
+
Quick start, concepts, route list, and user-facing orientation.
|
| 14 |
+
- `WIREFRAME.md`
|
| 15 |
+
Architecture, execution flow, and file-map walkthrough.
|
| 16 |
+
- `PRD.md`
|
| 17 |
+
Background/planning notes only. It is not canonical for current behavior.
|
| 18 |
+
- `AGENTS.md`
|
| 19 |
+
Rules for keeping code, comments, tests, and docs aligned in future changes.
|
| 20 |
+
- `Makefile`
|
| 21 |
+
The shared developer command surface, including the local Space validation
|
| 22 |
+
pipeline.
|
| 23 |
+
|
| 24 |
+
## What This Repo Is Teaching
|
| 25 |
+
|
| 26 |
+
`solverforge-hospital` is a complete SolverForge example, not just a scoring
|
| 27 |
+
snippet.
|
| 28 |
+
|
| 29 |
+
It shows how to build a planning app where:
|
| 30 |
+
|
| 31 |
+
- the domain model is small and explicit
|
| 32 |
+
- the score rules are readable one file at a time
|
| 33 |
+
- the dataset is deterministic and intentionally shaped
|
| 34 |
+
- the solver runs as a retained background job
|
| 35 |
+
- the browser UI watches the solve through REST and SSE
|
| 36 |
+
|
| 37 |
+
The planning question is:
|
| 38 |
+
|
| 39 |
+
"For each hospital shift, which employee should be assigned?"
|
| 40 |
+
|
| 41 |
+
## SolverForge Concepts In Plain Language
|
| 42 |
+
|
| 43 |
+
- `Employee`
|
| 44 |
+
Input data. The solver reads it, but does not move it.
|
| 45 |
+
- `Shift`
|
| 46 |
+
The thing the solver is allowed to assign.
|
| 47 |
+
- `employee_idx`
|
| 48 |
+
The one real decision variable in this app. It points from a shift to one
|
| 49 |
+
employee inside `Plan.employees`.
|
| 50 |
+
- hard score
|
| 51 |
+
Rules that must not be broken, such as missing skills or overlapping shifts.
|
| 52 |
+
- soft score
|
| 53 |
+
Preferences and quality goals, such as honoring desired days or balancing
|
| 54 |
+
workload.
|
| 55 |
+
- retained job
|
| 56 |
+
A solve that keeps living in memory after it starts, so the UI can poll it,
|
| 57 |
+
pause it, resume it, stop it through runtime cancel, or inspect snapshots.
|
| 58 |
+
Delete is terminal cleanup before the next fresh Solve, not the Stop action.
|
| 59 |
+
|
| 60 |
+
## Read Order
|
| 61 |
+
|
| 62 |
+
If you are new to this repo, read files in this order:
|
| 63 |
+
|
| 64 |
+
1. `src/domain/employee.rs`
|
| 65 |
+
Learn the input facts first.
|
| 66 |
+
2. `src/domain/care_hub.rs`
|
| 67 |
+
Learn the hospital service-line grouping used by nearby search.
|
| 68 |
+
3. `src/domain/mod.rs`
|
| 69 |
+
See the `planning_model!` manifest that lists and exports the model modules.
|
| 70 |
+
4. `src/domain/plan.rs`
|
| 71 |
+
Learn the planning entity, planning variable, nearby meters, and derived
|
| 72 |
+
fields.
|
| 73 |
+
5. `src/constraints/mod.rs`
|
| 74 |
+
See the full score model at a glance.
|
| 75 |
+
6. `src/constraints/*.rs`
|
| 76 |
+
Read one scheduling rule per file.
|
| 77 |
+
7. `src/data/data_seed/entrypoints.rs`
|
| 78 |
+
See the public demo-data surface.
|
| 79 |
+
8. `src/data/data_seed/large.rs`
|
| 80 |
+
See how the published dataset is assembled.
|
| 81 |
+
9. `src/solver/service.rs`
|
| 82 |
+
See how a domain solve becomes a retained runtime job.
|
| 83 |
+
10. `src/api/routes.rs` and `src/api/sse.rs`
|
| 84 |
+
See the HTTP contract.
|
| 85 |
+
11. `static/app/main.mjs`
|
| 86 |
+
See the browser boot sequence.
|
| 87 |
+
12. `static/app/shell/` and `static/app/schedule/`
|
| 88 |
+
See how stock `solverforge-ui` components are adapted to this hospital demo.
|
| 89 |
+
|
| 90 |
+
## Runtime Flow
|
| 91 |
+
|
| 92 |
+
The shortest way to understand the app is to follow one request all the way
|
| 93 |
+
through:
|
| 94 |
+
|
| 95 |
+
1. The browser loads `static/index.html`.
|
| 96 |
+
2. `static/app/main.mjs` loads config, the generated UI model, and the `LARGE`
|
| 97 |
+
demo dataset.
|
| 98 |
+
3. The frontend turns the returned `PlanDto` into schedule rails and side-panel
|
| 99 |
+
summaries.
|
| 100 |
+
4. When the user clicks Solve, the frontend sends the current plan to
|
| 101 |
+
`POST /jobs`.
|
| 102 |
+
5. `src/api/routes.rs` converts that HTTP request into a `PlanDto`.
|
| 103 |
+
6. `PlanDto::to_domain()` rebuilds the in-memory `Plan`, including derived
|
| 104 |
+
helper fields the solver expects.
|
| 105 |
+
7. `SolverService` starts a retained solve through `SolverManager<Plan>`.
|
| 106 |
+
8. The solver emits lifecycle, phase, telemetry, best-solution, and analysis
|
| 107 |
+
events.
|
| 108 |
+
9. `src/solver/service.rs` converts those runtime events into the JSON event
|
| 109 |
+
shapes expected by the UI.
|
| 110 |
+
10. The browser consumes those events over `/jobs/{id}/events` and updates the
|
| 111 |
+
visible status, timeline, and analysis panels.
|
| 112 |
+
|
| 113 |
+
The browser shell also contains a visible REST API guide. That makes
|
| 114 |
+
`static/app/shell/api-guide.mjs` part of the documentation surface, not just a
|
| 115 |
+
UI helper.
|
| 116 |
+
|
| 117 |
+
## File Map
|
| 118 |
+
|
| 119 |
+
```text
|
| 120 |
+
.
|
| 121 |
+
βββ Cargo.toml
|
| 122 |
+
β Rust crate metadata and crates.io dependencies.
|
| 123 |
+
βββ solver.toml
|
| 124 |
+
β Embedded solver policy. This is the runtime source of truth for search.
|
| 125 |
+
βββ solverforge.app.toml
|
| 126 |
+
β App metadata used by the surrounding SolverForge tooling.
|
| 127 |
+
βββ Dockerfile
|
| 128 |
+
β Container build for running the app outside the dev checkout.
|
| 129 |
+
βββ Makefile
|
| 130 |
+
β Local build, validation, and Docker/Space workflow wrapper.
|
| 131 |
+
βββ README.md
|
| 132 |
+
β Beginner run guide, API overview, and learning path.
|
| 133 |
+
βββ PRD.md
|
| 134 |
+
β Background/planning notes; not the source of truth for current behavior.
|
| 135 |
+
βββ WIREFRAME.md
|
| 136 |
+
β This architectural walkthrough.
|
| 137 |
+
βββ AGENTS.md
|
| 138 |
+
β Repo-specific contributor and documentation rules.
|
| 139 |
+
βββ src/
|
| 140 |
+
β βββ lib.rs
|
| 141 |
+
β β Crate root and public module surface.
|
| 142 |
+
β βββ main.rs
|
| 143 |
+
β β Axum server bootstrap, CORS, static serving, and route composition.
|
| 144 |
+
β βββ domain/
|
| 145 |
+
β β `planning_model!` manifest plus problem model modules.
|
| 146 |
+
β βββ constraints/
|
| 147 |
+
β β One scheduling rule per file plus the assembler in `mod.rs`.
|
| 148 |
+
β βββ data/
|
| 149 |
+
β β Deterministic demo-data generator and published entrypoints.
|
| 150 |
+
β βββ solver/
|
| 151 |
+
β β Retained-job facade over the SolverForge runtime.
|
| 152 |
+
β βββ api/
|
| 153 |
+
β DTOs, REST routes, and SSE streaming.
|
| 154 |
+
βββ static/
|
| 155 |
+
β βββ index.html
|
| 156 |
+
β β Browser entrypoint.
|
| 157 |
+
β βββ sf-config.json
|
| 158 |
+
β β Runtime UI config for the stock frontend shell.
|
| 159 |
+
β βββ generated/ui-model.json
|
| 160 |
+
β β Generated view metadata used by `solverforge-ui`.
|
| 161 |
+
β βββ app/
|
| 162 |
+
β βββ main.mjs
|
| 163 |
+
β β Browser boot and wiring.
|
| 164 |
+
β βββ shell/
|
| 165 |
+
β β App shell, state, solver controls, and panels.
|
| 166 |
+
β βββ schedule/
|
| 167 |
+
β β Hospital-specific grouping, presentation, and rail rendering.
|
| 168 |
+
β βββ views/registry.mjs
|
| 169 |
+
β Named view registration.
|
| 170 |
+
βββ tests/
|
| 171 |
+
βββ frontend/
|
| 172 |
+
β Browserless frontend tests.
|
| 173 |
+
βββ support/
|
| 174 |
+
Fake DOM support used by the frontend tests.
|
| 175 |
+
```
|
| 176 |
+
|
| 177 |
+
## Why The Model Looks This Way
|
| 178 |
+
|
| 179 |
+
This app is intentionally narrow.
|
| 180 |
+
|
| 181 |
+
- There is one planning entity type: `Shift`.
|
| 182 |
+
- There is one scalar planning variable: `employee_idx`.
|
| 183 |
+
- Nearby search is attached directly to that scalar variable.
|
| 184 |
+
- The solver does not juggle multiple variable types, chained assignments, or
|
| 185 |
+
list planning.
|
| 186 |
+
|
| 187 |
+
That makes the example easier to learn because the optimization problem stays
|
| 188 |
+
visible:
|
| 189 |
+
|
| 190 |
+
- facts live in `Plan.employees`
|
| 191 |
+
- decisions live in `Plan.shifts[*].employee_idx`
|
| 192 |
+
- score rules read those two things and judge the assignment
|
| 193 |
+
|
| 194 |
+
## Why The Demo Data Is Structured
|
| 195 |
+
|
| 196 |
+
The demo-data generator is not filler.
|
| 197 |
+
|
| 198 |
+
It is designed to give beginners a problem that is:
|
| 199 |
+
|
| 200 |
+
- deterministic
|
| 201 |
+
- feasible
|
| 202 |
+
- interesting enough that local search still has work to do
|
| 203 |
+
- stable enough that tests and comparisons stay meaningful
|
| 204 |
+
|
| 205 |
+
One important design trick is the hidden witness roster in
|
| 206 |
+
`src/data/data_seed/witness.rs`.
|
| 207 |
+
|
| 208 |
+
That internal roster gives the generator a known feasible staffing pattern.
|
| 209 |
+
The published dataset is then shaped around that witness, while the solver only
|
| 210 |
+
sees the final public problem. This lets the repo ship a realistic-feeling
|
| 211 |
+
demo without random feasibility failures.
|
| 212 |
+
|
| 213 |
+
## Why The Runtime Uses REST And SSE
|
| 214 |
+
|
| 215 |
+
The solve is long-lived compared with a normal request/response handler, so the
|
| 216 |
+
backend splits the contract into two parts:
|
| 217 |
+
|
| 218 |
+
- REST for control and snapshots
|
| 219 |
+
- SSE for live progress
|
| 220 |
+
|
| 221 |
+
That separation keeps the frontend simple:
|
| 222 |
+
|
| 223 |
+
- create a job
|
| 224 |
+
- poll or fetch details when needed
|
| 225 |
+
- subscribe once to the live event stream
|
| 226 |
+
|
| 227 |
+
It also mirrors how a real retained SolverForge app behaves in production.
|
| 228 |
+
|
| 229 |
+
One small but important detail: `GET /jobs/{id}` and `GET /jobs/{id}/status`
|
| 230 |
+
return the same summary payload. The second route exists as a stock-compatible
|
| 231 |
+
alias for clients that expect the explicit `/status` URL shape.
|
| 232 |
+
|
| 233 |
+
## Solver Policy
|
| 234 |
+
|
| 235 |
+
`solver.toml` is embedded by `Plan` and is therefore part of the actual model,
|
| 236 |
+
not a side document.
|
| 237 |
+
|
| 238 |
+
The shipped search policy is deliberately conservative:
|
| 239 |
+
|
| 240 |
+
- `cheapest_insertion` builds a feasible first assignment
|
| 241 |
+
- local search stays in the nearby scalar neighborhood
|
| 242 |
+
- `late_acceptance` and `accepted_count` keep the search moving without blowing
|
| 243 |
+
up step cost
|
| 244 |
+
|
| 245 |
+
That narrow configuration is intentional for this demo. It is the balanced
|
| 246 |
+
30-second policy that currently performs best on the hospital dataset.
|
| 247 |
+
|
| 248 |
+
## Frontend Design
|
| 249 |
+
|
| 250 |
+
The frontend is intentionally thin.
|
| 251 |
+
|
| 252 |
+
This repo is not trying to teach a custom framework. It is showing how to take
|
| 253 |
+
stock `solverforge-ui` pieces and adapt them to one concrete planning problem.
|
| 254 |
+
|
| 255 |
+
- `shell/` owns app lifecycle, backend wiring, and side panels
|
| 256 |
+
- `schedule/` owns the hospital-specific transformation from domain data to
|
| 257 |
+
visual rails
|
| 258 |
+
- tests in `tests/frontend/` lock that presentation behavior down without a
|
| 259 |
+
full browser
|
| 260 |
+
|
| 261 |
+
## Validation Surfaces
|
| 262 |
+
|
| 263 |
+
When you change this repo, think in four separate layers:
|
| 264 |
+
|
| 265 |
+
1. Domain and constraint logic:
|
| 266 |
+
`make test-rust`
|
| 267 |
+
2. Slow end-to-end solve quality:
|
| 268 |
+
`make test-slow`
|
| 269 |
+
3. Frontend module correctness:
|
| 270 |
+
`make test-frontend-syntax`
|
| 271 |
+
4. Frontend behavior:
|
| 272 |
+
`make test-frontend`
|
| 273 |
+
5. Local deploy readiness for the Docker-based Space target:
|
| 274 |
+
`make ci-local`
|
| 275 |
+
|
| 276 |
+
If you keep those five layers green, the repo remains teachable and usable.
|