Skip to content

Pin what frenet promises on any road with property tests - #47

Merged
edufford merged 11 commits into
mainfrom
frenet-property-tests
Sep 3, 2026
Merged

edufford merged 11 commits into
mainfrom
frenet-property-tests

Conversation

@edufford

@edufford edufford commented Sep 3, 2026 •

Copy link
Copy Markdown
Owner

The unit tests in path.rs check frenet on four roads drawn by hand. This
adds proptest as the workspace's first property-testing dependency and
six properties over generated roads lying anywhere in the plane facing
any way: a point on the road reads back its own arc length, a lane point
is recovered while the lane has room, the offset is the distance to the
road, the offset changes no faster than a point moving beside the road,
a closed road's arc length stays within one lap, and an open road's arc
length runs on past both ends with the lane offset holding.

An open road is a random walk turning by up to 150 degrees at each
vertex, and a closed one a star-shaped polygon walked in angular order.
Both keep any two segments that do not meet three lane widths apart,
since closer than that they would share lane space and a lane point
beside one would be beside the other. That clearance is what the
properties lean on: a lane has room beside a segment up to half the
distance to the nearest other segment, and beside the road, within half
the clearance, any two segments a point is nearest to must meet at a
vertex and so agree on which side of the road it is.

Two things the stress runs taught, recorded in DECISIONS.md: a
star-shaped polygon needs every angular gap under half a turn or it is
not simple, and no signed offset can be continuous far from a road that
comes back around in another direction, so the continuity property holds
the sign beside the road and the magnitude everywhere. The one boundary
it leaves unchecked is where the line a road holds past its end gives
way to a nearer segment, and frenet's docs now say that step is there
and why it stays.

The tests reach only the public API, so they live as an integration
test, and num_segments is public for the oracle's sake with one count
shared by the builder and the refusal check. A failure's seed is
persisted beside the test, committed as proptest asks, and the fix lands
with the shrunk case as a unit test, since a seed draws the same case
only while proptest and the generators stay put; CI now fetches the
dependency crates with the lock held before its first build, so a stale
lock fails the job rather than quietly building something else. The
tests cost the actors crate's clean test build under a second and run
in a tenth of a second. The demo world hash is unchanged.

The unit tests in path.rs check frenet on four roads drawn by hand. This
adds proptest as the workspace's first property-testing dependency and
six properties over generated roads, turned and shifted anywhere in the
plane: a point on the road reads back its own arc length, a lane point
round trips while it stays within the bend's reach, the offset is the
distance to the road, the offset changes no faster than a walking point
does, a loop's arc length stays within one lap, and an open road's arc
length runs on past both ends with the lane offset holding.

Every road is generated so that it cannot cross itself, since a crossing
puts a point on two stretches at once and rounding then picks between
them. An open road steps at least a meter along one axis per point; a
closed one is a star-shaped polygon walked in angular order, with no
angular gap reaching half a turn. That last bound was the one lesson the
stress runs taught: a wider gap sends a segment across the center and
through the far side, which showed up as an offset changing sign a
kilometer from the road and was the generator's fault rather than
frenet's.

The property tests reach only the public API, so they live as an
integration test. Proptest's default regression-file location assumes a
crate root the test does not have, so the config places it beside the
source. The tests cost the actors crate's clean test build under a
second and run in a few hundredths of a second. The demo world hash is
unchanged, since nothing here reaches a fingerprint.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Comment thread Cargo.toml
Comment thread crates/continuo-actors/tests/frenet_properties.rs Outdated
Comment thread crates/continuo-actors/tests/frenet_properties.rs Outdated
Comment thread crates/continuo-actors/tests/frenet_properties.rs Outdated
Comment thread crates/continuo-actors/tests/frenet_properties.rs Outdated
Comment thread crates/continuo-actors/tests/frenet_properties.rs Outdated
Comment thread crates/continuo-actors/tests/frenet_properties.rs Outdated
Comment thread crates/continuo-actors/tests/frenet_properties.rs Outdated
Comment thread crates/continuo-actors/tests/frenet_properties.rs
Comment thread crates/continuo-actors/tests/frenet_properties.rs Outdated
edufford and others added 7 commits September 2, 2026 18:58
The property tests' oracle counted a road's segments from its points and
whether it closes, which the path already knew and counted in three
places of its own: the builder, the refusal check and a private method
reading it off the arc-length table. One associated function now answers
all of them, the method is public with a unit test pinning both counts,
and the oracle calls it.

Inert by intent: the demo world hash is unchanged.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
An open road was a walk that always advanced along one axis, which
cannot cross itself but cannot double back either. It is now a random
walk turning by up to 150 degrees at each vertex, held to one rule: any
two segments that do not meet stay three lane widths apart, since closer
than that they would share lane space and a lane point beside one would
be beside the other. Closed roads keep the same clearance. About a third
of open roads are redrawn for it, so proptest's redraw allowance is
raised to suit a stress run.

Roads that double back showed the continuity property was overstated.
Half a kilometer from a hairpin, a point switches from the right of the
outbound leg to the outside of a right turn, and the offset flips sign
with no zero crossing; no signed offset can be continuous far from a
road facing both ways. The property now holds the sign only beside the
road, within half the clearance, where any two segments a point is
nearest to are close enough that they must meet at a vertex, and holds
the magnitude everywhere. The one pair it leaves unchecked is a walk
changing stretch and nearest segment at once, where the line a road
holds past its end gives way to a nearer segment, which a short return
leg ending within lane distance of the outbound leg can bring beside
the road.

The same sentences carry "rotated" for "turned", a wider point range
with its reason, and "segment" wherever a stretch of road was meant, so
that the word names only the parts of an open road's arc length.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Two tests each worked out, in their own words and with a bare cap of
fifty or a hundred meters, how far a point could sit off a segment
before another segment was nearer. One helper now answers both: half
the distance to the nearest other segment, and never more than the
widest road, ten lanes. The tests that recover a point from its arc
length and offset say so in their names rather than claiming an inverse
the docs deny, the distance test names its three stretches outright
instead of matching the last with a wildcard, and the past-the-ends
test calls the offset it places the expected one and says a negative
one lies to the right.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The docs on frenet said the offset goes on measuring across the line
the road held past its end, and left unsaid what happens where the road
bends back toward that line: any segment nearer than the line still
wins, so the offset steps from the line's distance to the road's as a
car crosses between them. Review settled that this stays as it is, so
the docs now say it. The property tests' notion of a stretch is trimmed
to what it classifies, an arc length, and the function naming one binds
its result before returning it.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The property that a closed road's arc length stays within one lap
allows the total itself, and its comment explained why in a sentence
nobody could read. Outside the corner where the road closes, the last
segment and the first are the same distance away in exact arithmetic,
rounding decides which answers, and the last answers with the total
where the first answers with zero. The test and its neighbor now say
closed road rather than loop, as the rest of the file does.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The config comment said a failure's seed lands in a directory beside
the test, where proptest's WithSource setting writes a sibling file
with the test's name and a new extension. Saying the real name is what
lets the file be found, committed, or cleaned up on purpose.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
A persisted proptest seed replays its case only while proptest and the
generators that drew it stay as they are, and nothing says when that
stops. So the seed file is committed as proptest asks, the fix it led
to lands as a unit test with the shrunk case written out, and the file
is deleted whenever proptest moves in the lock or a generator changes.
The config comment says so.

Proptest stays at a caret requirement pinned through Cargo.lock like
libm, and CI now fetches the dependency crates with the lock held
before its first build. A stale lock used to be updated quietly and
built against whatever it resolved, which for libm is a different
fingerprint and for proptest a seed that no longer means its case;
now it fails the job at the cause.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@edufford
edufford marked this pull request as ready for review September 3, 2026 06:47
Comment thread .github/workflows/ci.yml Outdated
Comment thread crates/continuo-actors/Cargo.toml Outdated
edufford and others added 3 commits September 2, 2026 21:24
The pull request description was carrying the reasons, which is
DECISIONS.md's job. One dated entry now holds them: the offset steps
where a nearer segment takes over from the line an open road holds past
its end, the signed offset is continuous beside the road rather than
everywhere, and a persisted proptest seed is committed with its fix
landing as a unit test and the file deleted once proptest or a
generator moves. CLAUDE.md's list of barriers names the CI step that
fetches the dependency crates with the lock held.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The locked fetch sat after every install step, so a stale lock was
reported only once the packager and the Python environment were set up.
It now follows the rust-cache restore directly, the first moment cargo
can run, and the crates it fetches land in the restored cache.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The dev-dependency comment tied proptest to the property tests on the
road, which would read as stale the day a second file of property tests
appeared. It names the kind of test now and nothing more.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@edufford
edufford merged commit 920da3a into main Sep 3, 2026
6 checks passed
@edufford
edufford deleted the frenet-property-tests branch September 3, 2026 15:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant