Complete Phase 1: accumulator, exports, and full parity coverage

T05 — ride_accumulator.dart with the cross-batch anchor intact (12 tests).
T06 — ride_export.dart, GPX 1.1 and GeoJSON (15 tests), parsed with real
parsers rather than substring matching.
T07 — parity harness extended to cover export byte output.

GPX and GeoJSON are byte-identical across Kotlin and Dart: same length, same
FNV hash, including the escaped hostile name and all %.7f/%.1f formatting.

Two harness bugs found and fixed while building it. String.hashCode is not
comparable across Java and Dart, so text comparison used FNV-1a instead. And a
missing jar let a failed Kotlin compile pass as a green run -- run.sh now checks
for the artifact and exits non-zero, the same failure mode as the v2 sweep that
hid a compile error behind /dev/null.

Phase 1 done: 79 tests passing, analyze clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-15 01:08:30 -05:00
parent 4af4e3411a
commit 398157b1f5
12 changed files with 918 additions and 6 deletions

View File

@@ -3,6 +3,9 @@ import com.rippr.data.Segment
import com.rippr.geo.Geo
import com.rippr.geo.LatLon
import com.rippr.stats.RideStatistics
import com.rippr.data.Trip
import com.rippr.data.TripState
import com.rippr.export.RideExport
// A deterministic LCG implemented identically in Kotlin and Dart, so both languages
// see the SAME noise sequence. Neither language's built-in Random can do this.
@@ -14,6 +17,14 @@ class Lcg(private var s: Long) {
}
}
// FNV-1a 32-bit. String.hashCode() differs between Java and Dart, so it cannot be used
// to compare text across languages -- this can.
fun fnv1a(s: String): Long {
var h = 2166136261L
for (ch in s) { h = h xor ch.code.toLong(); h = (h * 16777619L) and 0xFFFFFFFFL }
return h
}
fun p(seg: Long, ts: Long, lat: Double, lon: Double, sp: Float, alt: Double, id: Long) =
TrackPoint(id = id, tripId = 1, segmentId = seg, timestamp = ts,
latitude = lat, longitude = lon, speedKmh = sp, altitudeM = alt)
@@ -55,5 +66,19 @@ fun main() {
val hist = RideStatistics.speedHistogram(run, 10)
out("hist_buckets", hist.size)
// --- export byte output -------------------------------------------------
val etrip = Trip(id = 1, startedAt = 1_700_000_000_000L, endedAt = 1_700_000_600_000L,
state = TripState.COMPLETED, distanceM = 12_345.6, maxSpeedKmh = 118.4f)
val esegs = listOf(Segment(id = 1, tripId = 1, startedAt = 0, endedAt = 1),
Segment(id = 2, tripId = 1, startedAt = 2, endedAt = 3))
val epts = esegs.flatMap { sg -> (0 until 5).map {
p(sg.id, 1_700_000_000_000L + it * 1000L, 51.0447 + it * 0.0001, -114.0719, 72f, 1045.0, it.toLong() + 1) } }
val g = RideExport.gpx(etrip, esegs, epts, "Sam & Dave's <ride>")
val j = RideExport.geoJson(etrip, esegs, epts, "Sam & Dave's <ride>")
out("gpx_len", g.length)
out("gpx_fnv", fnv1a(g))
out("geojson_len", j.length)
out("geojson_fnv", fnv1a(j))
out("hist_first_millis", hist.first().millis)
}