Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 19 additions & 2 deletions data/track-packs/test/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"researched_utc": "2026-10-11T00:00:00Z",
"attribution": "Road geometry derived from OpenStreetMap contributors; loop defined for functional testing, not for competition.",
"data_license": "ODbL-1.0",
"policy": "This pack holds functional test fixtures, not competition venues. Every definition is provisional and timer-only until its gate profile carries a validation record. A road loop has no pit lane to identify its start/finish straight and no second independent source for a line somebody chose, so independently_validated is not reachable here: promotion must come from device capture. See docs/TRACK_DATABASE.md.",
"policy": "This pack holds functional test fixtures, not competition venues. A definition is provisional and timer-only until its gate profile carries a validation record. A road loop has no pit lane to identify its start/finish straight, so its gate lines are chosen rather than inherent, and physically_validated is not reachable without driving it. independently_validated is: aerial imagery is a separate origin from OpenStreetMap's vector data, and a visual check that each line spans the carriageway is what the 12 m gate width actually depends on. Device capture remains the stronger promotion because it measures the lines with the receiver that will time them. See docs/TRACK_DATABASE.md.",
"scope_exclusions": [
{
"venue": "Public roads generally",
Expand Down Expand Up @@ -154,7 +154,24 @@
147.3,
12
],
"derivation": "Start and finish sit 30 m back along King Edward Road from the Sea Cliff Road junction, measured along the OSM centreline, because the two legs meet at the junction itself and a gate there would span both. At 30 m back the outbound leg is 26.3 m away, leaving 16.5 m between the gate end and its carriageway. Gates are 12 m: 7.62 m of road plus 2.19 m beyond each edge for lateral fix error. pit_exit faces 147.3 deg south-east-bound so the north-bound pass into the pit lane does not register."
"derivation": "Start and finish sit 30 m back along King Edward Road from the Sea Cliff Road junction, measured along the OSM centreline, because the two legs meet at the junction itself and a gate there would span both. At 30 m back the outbound leg is 26.3 m away, leaving 16.5 m between the gate end and its carriageway. Gates are 12 m: 7.62 m of road plus 2.19 m beyond each edge for lateral fix error. pit_exit faces 147.3 deg south-east-bound so the north-bound pass into the pit lane does not register.",
"validation": {
"geometry_status": "independently_validated",
"verified_utc": "2026-10-17T00:00:00Z",
"agreement_m": 5.0,
"sources": [
{
"name": "OpenStreetMap ways 262071530/262071527",
"url": "https://www.openstreetmap.org/way/262071530",
"origin": "openstreetmap"
},
{
"name": "Google Maps satellite imagery, visual check of each gate line against the carriageway",
"url": "https://www.google.com/maps/@54.169459,-4.447053,18z/data=!3m1!1e3",
"origin": "google-maps-imagery"
}
]
}
}
},
"layouts": [
Expand Down
57 changes: 46 additions & 11 deletions tests/test_test_track_pack.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,12 @@
import tempfile
import unittest

from tools.build_track_pack import TrackPackError, build_pack, validate_manifest
from tools.build_track_pack import (
MAXIMUM_SOURCE_DISAGREEMENT_M,
TrackPackError,
build_pack,
validate_manifest,
)
from tools.track_workbench import validate_definition

REPO_ROOT = Path(__file__).resolve().parents[1]
Expand Down Expand Up @@ -87,18 +92,48 @@ def test_pack_builds_one_definition_that_the_schema_accepts(self):
self.assertEqual(definition["country"], "IM")
self.assertEqual(definition["provenance"]["license"], "ODbL-1.0")

def test_the_fixture_cannot_arm_timing_until_it_is_captured(self):
"""A road loop has no route to independently_validated, so it must not claim one.

There is no pit lane to identify a start/finish straight from and no second
independent source for a line somebody chose on a road, so the only honest
promotion is a device capture. Until then this must stay timer-only, or the
provenance gate #139 built has been holed to accommodate a test fixture.
def test_the_fixture_arms_only_on_the_strength_of_its_validation_record(self):
"""The fixture is timing-ready, and the record is the only reason it is.

This replaces an assertion that the loop must stay provisional on the grounds that
a road has no second independent source. It does: OpenStreetMap supplies the vector
geometry and aerial imagery is a separate origin, and a visual check that each line
spans the carriageway is exactly what the 12 m gate width depends on. Device
capture is still the stronger promotion, because it measures the lines with the
receiver that will later time them.

What must not be lost is the gate #139 built. Promotion has to come from a
validation record that the builder has checked - two distinct origins agreeing
within MAXIMUM_SOURCE_DISAGREEMENT_M - and never from editing a status by hand.
So the record is asserted present, and removing it is asserted to put the
definition straight back to timer-only.
"""
result, definition = self._build()
self.assertEqual(result["timing_ready_count"], 0)
self.assertEqual(definition["provenance"]["geometry_status"], "provisional")
self.assertNotIn("validation", self.profile)
self.assertEqual(result["timing_ready_count"], 1)
self.assertEqual(
definition["provenance"]["geometry_status"], "independently_validated"
)

validation = self.profile["validation"]
self.assertGreaterEqual(len({source["origin"] for source in validation["sources"]}), 2)
self.assertLessEqual(validation["agreement_m"], MAXIMUM_SOURCE_DISAGREEMENT_M)
self.assertIn("2 origins", definition["provenance"]["source"])

# Without the record the same geometry is provisional again, so the status is
# earned by the evidence rather than carried by the file.
unvalidated = deepcopy(self.manifest)
del unvalidated["venues"][0]["gate_profiles"]["default"]["validation"]
with tempfile.TemporaryDirectory() as temporary:
manifest_path = Path(temporary) / "manifest.json"
manifest_path.write_text(json.dumps(unvalidated), encoding="utf-8")
bare_result = build_pack(manifest_path, Path(temporary) / "out")
bare = json.loads(
(Path(temporary) / "out" / "definitions" / "im_test_circuit.json").read_text(
encoding="utf-8"
)
)
self.assertEqual(bare_result["timing_ready_count"], 0)
self.assertEqual(bare["provenance"]["geometry_status"], "provisional")

def test_every_gate_spans_the_road_with_margin_but_not_the_other_carriageway(self):
"""12 m is the whole of the width decision, and both bounds matter.
Expand Down