feat(sandbox): Placement trait with least-loaded default
Stacked on #619.
Extracts ranking out of place() behind a Placement trait (crates/preloop-sandbox/src/placement.rs). The service keeps the hard-constraint filter and builds the candidate list; Placement::rank returns a ranked list and the service takes the first. LeastLoaded reproduces today's ordering and tie-breaks exactly. Selected by PRELOOP_SANDBOX_PLACEMENT (default least-loaded); an unknown value fails startup listing the valid names.
No proto change. Tests: LeastLoaded ordering/ties, name resolution, and a conformance test proving a custom placement in ServiceConfig is honoured.
Checked: cargo clippy -p preloop-sandbox -p preloop-cli -p preloop-orchestrator --all-targets -D warnings, cargo test -p preloop-sandbox.
Summary by cubic
Introduces a Placement trait so the sandbox service's candidate ranking is configurable instead of hard-coded least-loaded.
- The service keeps the hard-constraint filter (offline providers, capability gaps, slot caps);
Placement::rankorders survivors and the service takes the first returned candidate. LeastLoadedreproduces the old ordering and tie-breaks exactly and remains the default.PRELOOP_SANDBOX_PLACEMENTselects a built-in strategy; an unknown name fails startup listing the valid ones.- Custom placements plug in via
ServiceConfig::placement. - Includes a conformance test proving a configured placement overrides the default ranking; docs updated.