# Task 5 Report: Backfill Candidate Rows And Final Projection ## Summary Implemented Task 5 in `go/vehicle-gateway/cmd/stats-backfill` so backfill now writes source candidates into `vehicle_daily_mileage_source` and re-projects `vehicle_daily_mileage` through the shared projector instead of writing final rows directly. ## TDD Evidence 1. Added the failing regression in `go/vehicle-gateway/cmd/stats-backfill/main_test.go`: - `TestDailySourceLastBuildsCandidateKeysBySourceIP` - Updated the existing trusted-source test to the new `normalizedSourceKey(protocol, phone, deviceID, endpoint)` signature. 2. Verified the red state with: ```bash go test ./cmd/stats-backfill -run 'TestChooseTrustedSource|TestDailySourceLastBuildsCandidateKeysBySourceIP' -count=1 ``` Observed failure: ```text too many arguments in call to normalizedSourceKey have (string, string, string, string) want (string, string) ``` 3. Implemented the production changes. 4. Verified green with: ```bash go test ./cmd/stats-backfill -run 'TestChooseTrustedSource|TestDailySourceLastBuildsCandidateKeysBySourceIP' -count=1 go test ./cmd/stats-backfill ./internal/stats -count=1 ``` Both commands passed. ## Files Changed - `go/vehicle-gateway/cmd/stats-backfill/main.go` - `go/vehicle-gateway/cmd/stats-backfill/main_test.go` ## What Changed ### 1. Source-key normalization now matches runtime semantics - Changed backfill `normalizedSourceKey` to call: ```go stats.SourceKey(envelope.Protocol(protocol), phone, deviceID, stats.NormalizeSourceIP(endpoint)) ``` - This means candidate keys now use: - protocol - phone or device ID - normalized source IP - Port changes on the same source endpoint no longer produce different source keys. ### 2. Scan-mode backfill now preserves per-source candidates - Expanded raw-frame reads to include `phone`, `device_id`, and `source_endpoint`. - Populated `envelope.FrameEnvelope` with those fields before calling `stats.SamplesFromEnvelope`. - Changed scan-mode aggregation key from: ```text vin + stat_date + protocol ``` to: ```text vin + stat_date + protocol + source_key ``` - Extended `metricAgg` to carry: - `SourceKey` - `Phone` - `DeviceID` - `SourceEndpoint` - `FirstEventTime` - `LatestEventTime` - `QualityStatus` - `QualityReason` This keeps scan-mode candidates source-separated instead of mixing sources together. ### 3. Backfill writes candidates + re-projects final rows - Replaced the direct `vehicle_daily_mileage` batch upsert path. - `writeAggregates` now, per aggregate: 1. Upserts `vehicle_data_source` using protocol + normalized source IP. 2. Upserts a `vehicle_daily_mileage_source` candidate row. 3. Calls `stats.ProjectDailyMileage` for the VIN/date/protocol. - Quality handling: - default `OK` - `INVALID_DELTA` when daily delta is `< 0` or `> 1000` - pre-set non-OK statuses from `last_diff` are preserved ### 4. `last_diff` now preserves every source candidate - `buildLastDiffAggregates` no longer picks a single trusted source. - It now creates one aggregate per current-source candidate using: ```text vin + stat_date + protocol + source_key ``` - If a same-source previous-day baseline exists: - `FirstKM = previous.TotalKM` - `LatestKM = current.TotalKM` - `QualityStatus = OK` - `QualityReason = "same_source_previous_day"` - If there is no same-source previous-day baseline: - `FirstKM = current.TotalKM` - `LatestKM = current.TotalKM` - `QualityStatus = NO_PREVIOUS_BASELINE` - `QualityReason = "missing_previous_source"` This preserves candidates without projecting them by default, matching the projector’s `quality_status = 'OK'` filter. ### 5. Reset now clears candidate rows too - Updated `resetStats` to delete both: - `vehicle_daily_mileage_source` - `vehicle_daily_mileage` within the requested date/protocol scope. This prevents stale candidates from affecting a subsequent rebuild run. ## Test Results ```bash go test ./cmd/stats-backfill -run 'TestChooseTrustedSource|TestDailySourceLastBuildsCandidateKeysBySourceIP' -count=1 ok lingniu-vehicle-ingest/go/vehicle-gateway/cmd/stats-backfill 0.572s ``` ```bash go test ./cmd/stats-backfill ./internal/stats -count=1 ok lingniu-vehicle-ingest/go/vehicle-gateway/cmd/stats-backfill 0.174s ok lingniu-vehicle-ingest/go/vehicle-gateway/internal/stats 0.557s ``` ## Concerns / Notes - `chooseTrustedSource` remains in place for its existing unit test, but the `last_diff` aggregation path no longer relies on it. - `ProjectDailyMileage` still follows the shared runtime semantics: it selects only `quality_status = 'OK'` candidates and does not synthesize cross-source deltas. ## Review Fix: Stale Final Rows ### Bug Addressed When all candidates for a backfill target `(vin, stat_date, protocol)` are non-qualifying (`NO_PREVIOUS_BASELINE` / `INVALID_DELTA`), `ProjectDailyMileage` runs but inserts no final row, leaving any prior `vehicle_daily_mileage` row in place. That violated the expectation that a run with no selected candidate should not keep a historical final row for that key. ### Fix Applied - Added helper in `cmd/stats-backfill/main.go`: - `clearBackfillFinalMileage(ctx, db, vin, statDate, protocol)` - Executes `DELETE FROM vehicle_daily_mileage WHERE vin = ? AND stat_date = ? AND protocol = ?` - Guarded against empty key values. - Updated `writeAggregates` to call the helper once per exact `(vin, stat_date, protocol)` target before calling `ProjectDailyMileage`. - Kept existing runtime writer behavior unchanged; only backfill projection sequencing changed. ### Tests Added - `TestClearBackfillFinalMileageClearsExactKey` in `cmd/stats-backfill/main_test.go` - Verifies the SQL targets exactly the intended triplet. - `TestWriteAggregatesClearsFinalBeforeProjection` in `cmd/stats-backfill/main_test.go` - Verifies backfill writes still occur and final-row clear is executed before projection. ### Updated Evidence ```bash go test ./cmd/stats-backfill -run 'TestChooseTrustedSource|TestDailySourceLastBuildsCandidateKeysBySourceIP|Test.*Reset|Test.*Projection|Test.*Final' -count=1 ok lingniu-vehicle-ingest/go/vehicle-gateway/cmd/stats-backfill 0.447s go test ./cmd/stats-backfill ./internal/stats -count=1 ok lingniu-vehicle-ingest/go/vehicle-gateway/cmd/stats-backfill 0.188s ok lingniu-vehicle-ingest/go/vehicle-gateway/internal/stats 0.547s ``` ## Follow-up Fix: Non-Reset Backfill Idempotency ### Additional Bug The prior fix only cleared `vehicle_daily_mileage` before projection. In non-reset backfills, stale `vehicle_daily_mileage_source` rows for the same `(vin, stat_date, protocol)` could still be selected by `ProjectDailyMileage`, causing old OK candidates to win against fresh backfill input. ### Additional Fix - Added `clearBackfillTargetMileage` in `go/vehicle-gateway/cmd/stats-backfill/main.go` to delete both tables for the exact target key: - `DELETE FROM vehicle_daily_mileage_source WHERE vin = ? AND stat_date = ? AND protocol = ?` - `DELETE FROM vehicle_daily_mileage WHERE vin = ? AND stat_date = ? AND protocol = ?` - Updated `writeAggregates` so this clear happens at most once per `(vin, stat_date, protocol)` and before the first candidate upsert for that target. - Kept existing backfill runtime flow intact: still upserts sources/candidates and calls `ProjectDailyMileage` for each target aggregate. ### Tests Added/Updated - `TestClearBackfillTargetMileageClearsExactKey`: - Verifies both table deletes use the exact VIN/date/protocol tuple. - `TestWriteAggregatesClearsTargetRowsBeforeStaleCandidateUpsert`: - Verifies clear operations happen before candidate writes and projection, and that the stale-candidate flow is tested by using a non-qualifying candidate status. ### Verification ```bash go test ./cmd/stats-backfill -run 'TestChooseTrustedSource|TestDailySourceLastBuildsCandidateKeysBySourceIP|Test.*Reset|Test.*Projection|Test.*Final|Test.*Candidate' -count=1 go test ./cmd/stats-backfill ./internal/stats -count=1 ```