`oxmpl` - sprint-003
Sprint 003 — Validation foundations (provisional)
Provisional scaffold for release 0.9.0 (E4), written 2026-10-08 so the release isn't lost. Nothing here is committed: the stories carry no
oxmpl/sprint/003tag until planning confirms them against the owner's stated availability. Dates are re-derived at the previous sprint's retro if it slipped.
- Dates (provisional): Thu 2026-10-29 → Wed 2026-11-18 (three weeks)
- Epic: E4 — Validation (0.9.0)
- Goal (provisional): PRM roadmaps reproducible; validation fallible and thread-safe; spaces own their resolution — main green, nothing released.
Provisional commitment
- S4.1 — PRM milestone budget (size-M; after E3)
- S4.2 — Fallible, thread-safe validation core (size-M; after E3)
- S4.3 — Space resolution: valid segment count (size-S; after E3)
Order: S4.1 first (stabilises PRM tests before resolution changes), then S4.2 / S4.3 in either order.
Entry criteria: E3 done and 0.8.0 published (S4.x are gated on E3). If sprint-002 carried work over, re-derive these dates at its retro.
Blind pickup: how to (re)generate the tickets
Tickets for these stories already exist in TaskNotes (created 2026-10-08). Once a ticket exists, its TaskNotes details are authoritative and this page is only the regeneration spec. If a ticket is missing or was lost, recreate it from the spec below:
tasknotes_create_taskwith the title, projects, tags and priority shown. MCP quirks: create drops the description and defaultsscheduledto today, so follow withtasknotes_update_taskto setdetails(the body below) andscheduled: null.- Wire
blockedByby editing the task file's YAML frontmatter directly (no MCP tool exposes it). A story lists all upstream dependencies, including done ones. - Verify with
tasknotes_get_task:isBlocked: trueon the story while its upstreams are open, andisBlockingon each upstream. After any laterupdate_task, re-check thatblockedBysurvived. - If the epic E4 is missing, recreate it first from the spec in
oxmpl- sprint-003 § Epic E4. - At this sprint's planning (owner confirms availability): add the
oxmpl/sprint/NNNtag, setscheduled/dueto the sprint dates, removesprint-status: provisionalfrom this page's frontmatter, and rewrite the "Provisional commitment" heading as "Commitment".
Conventions: oxmpl - planning. Repo context: ADR docs/planning/adr/0003-motion-validation.md (decisions), CONTEXT.md (glossary). Line references are as of origin/main c558556 (2026-10-08), before E3. E3 renames things (rand Rng→RngExt, PyO3 with_gil→attach, edition 2024), so re-grep before trusting a line number.
Epic E4 (regeneration spec)
# TaskNotes task — file: task_notes/tasks/E4 Validation — 0.9.0.md
title: "E4: Validation — 0.9.0"
status: open
priority: normal
projects: ["[[`oxmpl` - A Rust-based Motion Planning Library]]"]
tags: [oxmpl, oxmpl/epic]
scheduled: 2026-10-29 # re-derive if sprint-002 slipped
due: 2027-01-20 # 0.9.0 target; re-derive at each retro
reminders: [absolute 2026-10-28T09:00+04:00 "OxMPL: sprint-002 retro + plan sprint-003"]
blockedBy: # every epic is gated on all its stories
- uid: "[[S4.1 — PRM milestone budget]]"
reltype: FINISHTOSTART
- uid: "[[S4.2 — Fallible, thread-safe validation core]]"
reltype: FINISHTOSTART
- uid: "[[S4.3 — Space resolution valid segment count]]"
reltype: FINISHTOSTART
- uid: "[[S4.4 — Motion validator trait and discrete validator]]"
reltype: FINISHTOSTART
- uid: "[[S4.5 — SpaceInformation migration]]"
reltype: FINISHTOSTART
- uid: "[[S4.6 — Binding error hierarchy]]"
reltype: FINISHTOSTART
- uid: "[[S4.7 — User motion validators in the bindings]]"
reltype: FINISHTOSTART
- uid: "[[S4.8 — Resolution exposed in the bindings]]"
reltype: FINISHTOSTART
- uid: "[[S4.9 — Docs, migration guide, ADR-0003 accepted]]"
reltype: FINISHTOSTART
- uid: "[[S4.10 — Release 0.9.0]]"
reltype: FINISHTOSTART
details body:
Goal
Release 0.9.0 "Validation": replace the four private check_motion copies with a pluggable motion-validation model after OMPL — SpaceInformation (space + state validity checker + motion validator) as the single source of validity; fallible, thread-safe validation; resolution owned by the state space (max-of-component counts, default fraction 0.01, no hidden ×0.1); typed binding errors; user motion validators in Python/JS (one cross-language call per motion); reproducible PRM roadmaps via a milestone budget.
Decisions: repo docs/planning/adr/0003-motion-validation.md; glossary: CONTEXT.md. Supersedes E2. Gated on E3 (0.8.0).
Stories
- S4.1 — PRM milestone budget (size-M) — supersedes S2.1
- S4.2 — Fallible, thread-safe validation core (size-M)
- S4.3 — Space resolution: valid segment count (size-S)
- S4.4 — Motion validator trait and discrete validator (size-M; after S4.2, S4.3) — supersedes S2.2
- S4.5 — SpaceInformation migration (size-L; after S4.4)
- S4.6 — Binding error hierarchy (size-M; after S4.2)
- S4.7 — User motion validators in the bindings (size-M; after S4.5, S4.6)
- S4.8 — Resolution exposed in the bindings (size-M; after S4.3)
- S4.9 — Docs, migration guide, ADR-0003 accepted (size-M; after all)
- S4.10 — Release 0.9.0 (size-S; after all)
Parallel: S4.1–S4.3 have no intra-epic dependencies; S4.6 and S4.8 can run alongside S4.5.
Full gate table (E4)
| Story | blockedBy |
|---|---|
| S4.1 | E3 |
| S4.2 | E3 |
| S4.3 | E3 |
| S4.4 | E3, S4.2, S4.3 |
| S4.5 | E3, S4.4 |
| S4.6 | E3, S4.2 |
| S4.7 | E3, S4.5, S4.6 |
| S4.8 | E3, S4.3 |
| S4.9 | E3, S4.1, S4.2, S4.3, S4.4, S4.5, S4.6, S4.7, S4.8 |
| S4.10 | E3, S4.1, S4.2, S4.3, S4.4, S4.5, S4.6, S4.7, S4.8, S4.9 |
| E4 | S4.1 … S4.10 |
E3 itself is gated on S3.1–S3.6 (sprint-002).
Story specs
S4.1 — PRM milestone budget (size-M)
# TaskNotes task — file: task_notes/tasks/S4.1 — PRM milestone budget.md
title: "S4.1 — PRM milestone budget"
status: open
priority: normal
projects: ["[[E4 Validation — 0.9.0]]"]
tags: [oxmpl, oxmpl/epic, oxmpl/story/size-M] # add oxmpl/sprint/003 only when committed at planning
scheduled: null # set to sprint start when committed
due: null # set to sprint end when committed
blockedBy: # wire in frontmatter by hand (no MCP tool exposes it)
- uid: "[[E3 Toolchain refresh — 0.8.0]]"
reltype: FINISHTOSTART
details body:
Intent
PRM builds its roadmap until a wall-clock timeout, so roadmap density depends on machine speed and on how expensive each validity call is. In the bindings every check crosses into Python/JS (~122k calls/s measured on pyo3), which caused the deterministic py SO(3) failure and the JS compound flake root-caused during S1.4. Add an additive milestone budget: with a seed, a budget and a deterministic checker, a roadmap becomes reproducible. Supersedes S2.1 (cancelled). Goes first in E4 because S4.3/S4.4 change the motion-check resolution, which would destabilise wall-clock-bound PRM tests.
Decisions (grilling 2026-10-08, Q3/Q17/Q19)
- Additive, not breaking: Rust
PRMhas private fields (no struct-literal construction), so a new public field is safe. - A milestone is a valid sample added to the roadmap; rejected samples do not count (glossary:
CONTEXT.md§ Roadmaps). - Construction stops at the budget or the timeout, whichever first. The timeout stays the safety cap. This amends S1.3's "timeout-only, no iteration API" for PRM construction only; no ADR (additive and easy to reverse).
milestone_count()getter, as OMPL'sPRM::milestoneCount().
Acceptance criteria
Tasks
Risks
- A budget too high for the timeout silently falls back to timeout behaviour; tests should assert the budget was reached, not just that a path was found.
S4.2 — Fallible, thread-safe validation core (size-M)
# TaskNotes task — file: task_notes/tasks/S4.2 — Fallible, thread-safe validation core.md
title: "S4.2 — Fallible, thread-safe validation core"
status: open
priority: normal
projects: ["[[E4 Validation — 0.9.0]]"]
tags: [oxmpl, oxmpl/epic, oxmpl/story/size-M] # add oxmpl/sprint/003 only when committed at planning
scheduled: null # set to sprint start when committed
due: null # set to sprint end when committed
blockedBy: # wire in frontmatter by hand (no MCP tool exposes it)
- uid: "[[E3 Toolchain refresh — 0.8.0]]"
reltype: FINISHTOSTART
details body:
Intent
User-supplied validation code can fail, and today that failure is invisible. Probe against oxmpl-py 0.7.0 (2026-10-08): a Python checker that raised was printed and treated as "invalid", so solve() still returned a path; KeyboardInterrupt was swallowed; a typo (s.valuez) produced 286k traceback lines over the full timeout and then a misleading "No solution found". OMPL's Python bindings raise immediately because C++ exceptions unwind through the planner; Rust has no such channel (and the wasm build is panic="abort"), so the error must be in the signature. Make failure a first-class error and make the validation traits thread-safe in one breaking change.
Decisions (ADR-0003; grilling Q4/Q5/Q6/Q7/Q18)
UserError = Box<dyn std::error::Error + Send + Sync>: any Rust error converts with?; bindings box the originalPyErr/JsValueso S4.6 can re-raise it.- Fallible:
StateValidityChecker::is_valid -> Result<bool, UserError>,Goal::is_satisfied -> Result<bool, UserError>,GoalRegion::distance_goal -> Result<f64, UserError>.GoalSampleableRegion::sample_goalkeeps its signature (alreadyResult<S, StateSamplingError>). PlanningError: addUser(UserError); removePartialEq(boxed errors are not comparable; tests already usematches!); add#[non_exhaustive].StateSamplingError: addUser(UserError); add#[non_exhaustive](and dropPartialEqifUsermakes it impossible).- Thread safety:
StateValidityChecker,StateSpace(includingAnyStateSpaceand the boxed compound subspaces), and the goal traits all becomeSend + Sync. Needed because the motion validator (S4.4) holds the space and checker and must beSend + Sync; adding the bound later would be a second breaking change. - JS:
js_sys::Function/JsValueare neverSend/Sync. Adapters useunsafe impl Send + Sync, which is sound only on single-threaded wasm32; guard with#[cfg(target_feature = "atomics")] compile_error!(...). - Python:
Py<T>is alreadySend + Sync(pyo3), so no unsafe needed. - Planners abort on the first user error with
PlanningError::User(..); no retry, no logging-and-continue.
Acceptance criteria
Tasks
Risks
- Highest: the
Send + Synccascade throughBox<dyn AnyStateSpace>(compound_state_space.rs:53) may collide with the ADR-0002 two-tierState/AnyStatesplit. If it spreads beyond adding bounds, stop and raise it with the owner before redesigning. ?inside iterator chains in rrt_star.rs needs local refactors.
S4.3 — Space resolution: valid segment count (size-S)
# TaskNotes task — file: task_notes/tasks/S4.3 — Space resolution valid segment count.md
title: "S4.3 — Space resolution: valid segment count"
status: open
priority: normal
projects: ["[[E4 Validation — 0.9.0]]"]
tags: [oxmpl, oxmpl/epic, oxmpl/story/size-S] # add oxmpl/sprint/003 only when committed at planning
scheduled: null # set to sprint start when committed
due: null # set to sprint end when committed
blockedBy: # wire in frontmatter by hand (no MCP tool exposes it)
- uid: "[[E3 Toolchain refresh — 0.8.0]]"
reltype: FINISHTOSTART
details body:
Intent
Resolution belongs to the state space, not to the planners. Today compound, SE2 and SE3 merge their components' longest valid segment lengths by weighted RMS, which under-samples rotation: SE2 on [0,10]² at default settings would inspect a 180° turn every 36° once the ×0.1 factor goes (rotation alone needs 9°). Adopt OMPL's rule (max of per-component segment counts) and OMPL's default fraction. Supersedes part of S2.2 (cancelled).
Decisions (ADR-0003; grilling Q2/Q3/Q13)
- New provided method
StateSpace::valid_segment_count(&self, s1, s2) -> usize, defaultceil(distance(s1, s2) / longest_valid_segment_length()). Custom spaces keep working unchanged. CompoundStateSpace,SE2StateSpace,SE3StateSpaceoverride it with the max over components of each component's own count (OMPLCompoundStateSpace::validSegmentCount).- Default longest-valid-segment fraction 0.01 (was 0.05) in RealVector, SO2, SO3. Single spaces become 2× coarser than today's effective spacing; compound spaces become finer than today.
- Migration note for users: set the fraction to 0.005 to reproduce the old single-space spacing.
- OMPL's extra
longestValidSegmentCountFactoris not adopted (the fraction covers it; can be added later as a provided method). - The ×0.1 factor itself is removed in S4.4, not here: S4.3 only adds the method and changes defaults, so behaviour shifts in two observable steps.
Acceptance criteria
Tasks
Open question (resolve at implementation, flag to owner)
- The fraction setters clamp to
[0, 1](real_vector_state_space.rs:121,so2:82,so3:87), so a fraction of 0 gives a zero length and a division by zero invalid_segment_count. OMPL throws for fractions outside (ε, 1−ε). Options: reject non-positive fractions (breaking: setter becomes fallible) or guard the division (count =usize::MAX/error). Breaking is acceptable in 0.9.0; pick one and record it in the PR.
Progress log
- 2026-10-08 — Scaffolded as provisional during sprint-002 planning (owner request); expanded the same day into a full regeneration spec (owner request: "enough that a blind pickup can generate all the tickets"). Not yet planned or committed.
Review & retro
At sprint end: done / carried over / dropped, what was over- or underestimated, and at most one process change for the next sprint.