`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/003 tag until planning confirms them against the owner's stated availability. Dates are re-derived at the previous sprint's retro if it slipped.

Provisional commitment

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:

  1. tasknotes_create_task with the title, projects, tags and priority shown. MCP quirks: create drops the description and defaults scheduled to today, so follow with tasknotes_update_task to set details (the body below) and scheduled: null.
  2. Wire blockedBy by editing the task file's YAML frontmatter directly (no MCP tool exposes it). A story lists all upstream dependencies, including done ones.
  3. Verify with tasknotes_get_task: isBlocked: true on the story while its upstreams are open, and isBlocking on each upstream. After any later update_task, re-check that blockedBy survived.
  4. If the epic E4 is missing, recreate it first from the spec in oxmpl - sprint-003 § Epic E4.
  5. At this sprint's planning (owner confirms availability): add the oxmpl/sprint/NNN tag, set scheduled/due to the sprint dates, remove sprint-status: provisional from 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

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)

Acceptance criteria

Tasks

Risks

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)

Acceptance criteria

Tasks

Risks

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)

Acceptance criteria

Tasks

Open question (resolve at implementation, flag to owner)

Progress log

Review & retro

At sprint end: done / carried over / dropped, what was over- or underestimated, and at most one process change for the next sprint.