Version 0.7 — Draft
This document defines the technical requirements for a test implementation that constructs a complete incident index from an iRacing replay file, operating without any iRacing /data REST API calls or iRacing account registration. Telemetry and replay control use the local SDK only.
The approach exploits the iRacing SDK broadcast command system to fast-forward a loaded replay at maximum speed while sampling the raw memory-mapped telemetry at 60Hz natively (bypassing SimHub's throttled update loops), detecting incident events in real time and recording their session timestamps and associated car indices.
Grafana / Loki: When the implementation runs as the SimSteward SimHub plugin (or reuses its logging stack), structured logs SHOULD be emitted to the same Loki → Grafana pipeline documented in GRAFANA-LOGGING.md so index-build phases, detections, and validation outcomes are visible in Grafana Explore and dashboards. Loki push is optional and off unless SIMSTEWARD_LOKI_URL is configured; it does not replace the on-disk JSON index (TR-019).
SimHub web dashboard: The built incident index and validation summary MUST be surfaceable in a new HTML/JavaScript page served by SimHub’s built-in HTTP server (same model as the existing SimSteward dashboard under Web/sim-steward-dash/), communicating with the plugin over the existing WebSocket bridge so users can inspect results without Grafana. Full requirements: §4.8.
The iRacing /data REST API provides per-lap incident flags server-side, but requires OAuth client credentials registered with iRacing. This approach targets a fully self-contained workflow where no internet connection, no API registration, and no user credentials are required beyond having iRacing installed and a replay loaded.
- The incident index inside
.rpyis a proprietary undocumented binary — it cannot be read directly BroadcastReplaySearch(NextIncident)has no per-car filtering — it searches globally across all carsBroadcastReplaySearchhas a minimum ~2.5 second cooldown before reliably accepting another command- All SDK broadcast commands are fire-and-forget with no return value — feedback is observation-only
CarIdxThrottlePct,CarIdxBrakePct,CarIdxClutchPctwere deliberately removed from the SDK
| Variable | Scope | Incident Signal |
|---|---|---|
PlayerCarMyIncidentCount |
Player car only | Increments on each incident point award. Delta from previous frame = new incident. |
CarIdxSessionFlags[n] |
All cars | repair (0x100000) or furled (0x80000) bit rising edge = confirmed incident for that car. |
CarIdxFastRepairsUsed[n] |
All cars | Value increment = damage confirmed. Less precise timing than flags. |
ReplaySessionTime |
Replay state | Current playback position in session seconds. Recorded at moment of detection. |
CamCarIdx |
Replay camera | Car the camera switches to after NextIncident seek. Used for post-seek car identification. |
CRITICAL: We are currently on a data finding mission to understand how the SDK behaves during accelerated replay playback. Because we do not yet know the limits of the SDK or the precise behavior of specific variables (see §7 Open Questions), verbose logging is highly desirable during the initial implementation.
- When in doubt, log it.
- When in doubt about what to include in the payload, expand and log it.
Index-build telemetry for operators and research SHOULD appear in Grafana via structured logs ingested by Loki, using the project’s four-label schema and JSON body fields (GRAFANA-LOGGING.md). While you should not spam Loki with logs on every single 60Hz poll for the entire run, you should aggressively log event-driven milestones, state transitions, unexpected variable behaviors, and each detected incident (with as much context as possible) to help us answer the open questions. Local stack setup: observability-local.md.
Operator-facing UI runs in the browser (ES6+), not Dash Studio WPF. The plugin exposes data and commands through the Fleck WebSocket server and optional broadcast messages; static assets live under SimHub Web/ per project conventions (see .cursor/rules/SimHub.mdc). Grafana remains optional; the new page is the primary local UX for the replay incident index when shipped inside SimSteward.
While SimSteward generally standardises telemetry capture through SimHub, this fast-forward incident indexing test requires a hybrid approach. SimHub abstracts away or fails to expose several critical pieces of raw SDK data needed for this specific task.
Direct SDK access MUST be used for:
- Replay control broadcast commands:
BroadcastReplaySearch,BroadcastReplaySetPlaySpeed,BroadcastReplaySearchSessionTime(SimHub does not expose these). - Raw
CarIdxSessionFlagsbitfields: Needed for per-car incident detection (SimHub only exposes flag state for the player, not the rawrepair/furledbits for all cars). - Raw frame counters:
ReplayFrameNum/ReplayFrameNumEnd. - Live high-frequency arrays:
CarIdxRPM,CarIdxGear,CarIdxSteerfor all 63 cars at 60Hz during fast-forward (SimHub's opponent model filters this to nearby cars and may not be reliable at 16x speed). - Raw YAML session string: Needed to extract unmapped fields like
ResultsPositionsentries during a replay. - Session YAML fingerprint in structured logs: When
IRacingSdk.Data.SessionInfoYamlis available, the plugin computessession_yaml_fingerprint_sha256_16(SHA-256 prefix; same helper as replay-incident index events) and merges it into logs that callMergeSessionAndRoutingFields(e.g.action_dispatched,action_result,incident_detected, dashboard bridge events). The fingerprint is recomputed whenSessionInfoUpdatechanges, not everyDataUpdatetick. PlayerCarMyIncidentCount: Polled directly to ensure delta signals are not missed during fast-forward playback.
SimHub is used for everything else: Plugin lifecycle, WebSocket server, HTML dashboard hosting, YAML DriverInfo enrichment (where sufficient), and exposing our built index/channels back out as SimHub properties.
The SDK memory map updates at 60Hz real time. During replay fast-forward at 16×, each real-time tick advances ~16× session time, so the effective sample rate relative to replay session time is approximately 60 ÷ 16 ≈ 3.75 Hz. This is acceptable for building the incident index at that speed: we do not require a higher effective session-time sampling rate than this combination yields.
- A replay can be fast-forwarded programmatically via the SDK broadcast system
- Incident events for all cars can be detected during fast-forward by monitoring
CarIdxSessionFlags ReplaySessionTimeaccurately captures the session timestamp of each detected incident- The resulting incident index matches the known incident record for the session (validated against final
Incidentscount from the YAML session string) - Total time to build a complete index for a full race replay is measured and recorded
- When integrated with SimSteward logging, index-build lifecycle and incident detections are queryable in Grafana (Loki) without exceeding project volume and cardinality rules
- The same index and validation summary are visible and usable from a new SimHub-hosted web dashboard (table, summary, and build status) without requiring Grafana
| Req ID | Priority | Requirement | Acceptance Criteria |
|---|---|---|---|
| TR-001 | MUST | Establish a direct connection to the iRacing memory-mapped file (Local\IRSDKMemMapFileName) using a raw SDK reader (e.g., IRSDKSharper or iRacingSdkWrapper). Do NOT rely solely on SimHub's DataUpdate cycle for telemetry, as SimHub does not expose the raw arrays needed for this specific feature. |
Raw SDK connection established within the SimHub plugin context; IsConnected = true verified. |
| TR-002 | MUST | Confirm the session is in replay mode by reading WeekendInfo.SimMode = 'replay' from the raw YAML session string before proceeding. |
SimMode value logged and asserted as 'replay'. |
| TR-003 | MUST | Read and store the SubSessionID from the raw YAML session string for use as a reference key for the resulting index. |
SubSessionID logged and present in output. |
| Req ID | Priority | Requirement | Acceptance Criteria |
|---|---|---|---|
| TR-004 | MUST | Before initiating fast-forward, seek the replay to the beginning using BroadcastReplaySearch(ToStart) and wait for ReplayFrameNum to stabilise at 0. |
ReplayFrameNum = 0 confirmed before fast-forward begins. |
| TR-005 | MUST | Capture a baseline snapshot of CarIdxSessionFlags for all car indices at frame 0 to correctly detect rising edges rather than false-triggering on flags present at session start. |
Baseline flags array logged for all car indices. |
| TR-006 | MUST | Capture baseline value of PlayerCarMyIncidentCount at frame 0. |
Baseline incident count logged. |
| TR-007 | SHOULD | Record the total frame count from ReplayFrameNumEnd for use in progress estimation. |
ReplayFrameNumEnd logged. |
| Req ID | Priority | Requirement | Acceptance Criteria |
|---|---|---|---|
| TR-008 | MUST | Initiate fast-forward by sending BroadcastReplaySetPlaySpeed with the maximum speed multiplier. Start at 16x and increase empirically to find the reliable ceiling. |
Replay begins advancing at faster than real-time confirmed by ReplaySessionTime increasing. |
| TR-009 | MUST | Collect data via the direct SDK connection at its native 60Hz update frequency for the duration of the fast-forward. Do not artificially throttle or rely on SimHub's UI thread DataUpdate interval, as frames will be missed at 16x speed. |
High-frequency polling loop established natively to the memory-mapped file. |
| TR-010 | MUST | Monitor IsReplayPlaying. When it transitions to false, treat the replay as complete and stop sampling. |
Completion correctly detected. No polling after replay ends. |
| TR-011 | SHOULD | Log elapsed wall-clock time from fast-forward start to completion for performance measurement. | Elapsed time recorded in test output. |
| Req ID | Priority | Requirement | Acceptance Criteria |
|---|---|---|---|
| TR-012 | MUST | On each native 60Hz SDK sample, compare current raw CarIdxSessionFlags[n] against the previous frame for all 63 car indices. Detect any frame where the repair bit (0x100000) transitions from 0 to 1. |
Rising edge correctly detected. No false positives on pre-existing flags from baseline. |
| TR-013 | MUST | On each native 60Hz SDK sample, detect any frame where the furled/meatball bit (0x80000) transitions from 0 to 1 for any car index. |
Rising edge correctly detected independently of repair bit detection. |
| TR-014 | MUST | On each native 60Hz SDK sample, compare the current raw PlayerCarMyIncidentCount against the previous value. Detect any frame where the count increments. |
All increments detected. Delta value (1, 2, or 4 points) recorded. |
| TR-015 | MUST | At the moment of any detection, record the current ReplaySessionTime as the incident timestamp. |
Timestamp within one frame (1/60s ≈ 16.7ms) of the actual incident moment. |
| TR-016 | MUST | At the moment of any detection, record the carIdx of the affected car. |
carIdx correctly identifies the car involved in each incident. |
| TR-017 | SHOULD | Detect CarIdxFastRepairsUsed[n] increments as a secondary confirmation signal. Record separately and cross-reference with flag-based detections. |
Fast repair increments logged and correlated with flag events where applicable. |
| TR-018 | MUST | Handle flag bit resets — when a repair or furled bit clears and later re-sets, treat the re-set as a new incident detection. Do not deduplicate within the same car unless within a 1-second window. | Multiple incidents on the same car correctly recorded as separate events. |
Each data point (incident index row) MUST carry a fingerprint so the same logical detection can be correlated across JSON on disk, structured logs (§4.7), the web dashboard (§4.8), and downstream storage without relying on row order. The fingerprint is a deterministic id derived only from stable fields (not wall-clock or build id).
Fingerprint (v1) — canonical string
- Let
pointsbe the JSON literal forincidentPoints: either a decimal integer string, or the two-character sequencenullwhen the value is unknown. - Build a single UTF-8 string (exact separators, no spaces):
v1|{subSessionId}|{carIdx}|{sessionTimeMs}|{detectionSource}|{points}
wheresubSessionIdis the same integer as the file summary,detectionSourceis one ofrepair_flag,furled_flag,player_incident_count. - Set
fingerprintto the lowercase hexadecimal SHA-256 digest of that UTF-8 string (64 hex characters).
If a future format needs to change inputs, bump the leading v1 token and document a new version (analogous to fingerprint_version in the broader Sim Steward data model).
| Req ID | Priority | Requirement | Acceptance Criteria |
|---|---|---|---|
| TR-019 | MUST | Produce a structured incident index as a JSON array upon completion. | Valid JSON file written to disk at test completion. |
| TR-020 | MUST | Each entry must contain: fingerprint (string, 64-char lowercase hex SHA-256 per Fingerprint (v1) above), carIdx (int), sessionTimeMs (int), detectionSource (string: repair_flag | furled_flag | player_incident_count), incidentPoints (int or null). |
All five fields present and correctly typed in every entry; fingerprint matches the v1 digest of that row’s other fields plus subSessionId. |
| TR-021 | MUST | Entries must be sorted ascending by sessionTimeMs. |
Output array is in chronological order. |
| TR-022 | SHOULD | Include a summary block: subSessionId, totalRaceIncidents, incidentCountByCarIdx, indexBuildTimeMs. |
Summary block present and values correct. |
Example output:
{
"subSessionId": 12345678,
"indexBuildTimeMs": 34200,
"totalRaceIncidents": 22,
"incidentCountByCarIdx": { "3": 2, "7": 1, "12": 4 },
"incidents": [
{
"fingerprint": "a1b2c3d4e5f6789012345678901234567890abcdef1234567890abcdef123456",
"carIdx": 3,
"sessionTimeMs": 184320,
"detectionSource": "repair_flag",
"incidentPoints": null
},
{
"fingerprint": "fedcba098765432109876543210fedcba098765432109876543210fedcba09",
"carIdx": 0,
"sessionTimeMs": 312800,
"detectionSource": "player_incident_count",
"incidentPoints": 2
}
]
}(Example fingerprint values are placeholders; implementations MUST emit the real SHA-256 hex for each row.)
| Req ID | Priority | Requirement | Acceptance Criteria |
|---|---|---|---|
| TR-023 | MUST | After index build, read the final driver Incidents value from ResultsPositions in the raw YAML session string. |
Per-driver final incident totals extracted correctly from YAML. |
| TR-024 | MUST | Cross-reference incident events detected per carIdx against final ResultsPositions.Incidents count. Log any discrepancies. |
Discrepancy report produced. Zero discrepancies is the pass condition, but discrepancies are research data not an automatic failure. |
| TR-025 | SHOULD | Seek to each detected incident timestamp using BroadcastReplaySearchSessionTime and confirm via CamCarIdx that iRacing's camera switches to the expected car. Allow 2.5 seconds per seek. |
Camera car matches expected carIdx. Match rate logged as a percentage. |
Requirements align with GRAFANA-LOGGING.md: four labels only (app, env, component, level); no high-cardinality values in labels (subsession_id, car_idx, correlation ids stay in the JSON body).
| Req ID | Priority | Requirement | Acceptance Criteria |
|---|---|---|---|
| TR-026 | SHOULD | When using the SimSteward plugin logging path, emit structured log lines for replay incident index build: at minimum replay_incident_index_started (or equivalent event name), replay_incident_index_baseline_ready, replay_incident_index_fast_forward_started, replay_incident_index_fast_forward_complete, replay_incident_index_validation_summary. |
Corresponding events appear in Loki (if push enabled) and in plugin-structured.jsonl per project pipeline. |
| TR-027 | SHOULD | While we are on a data finding mission, do not blindly emit logs on every single 60Hz SDK cycle across a 45-minute race (to avoid overwhelming the sink). However, verbose logging of state transitions, unexpected values, and key intervals is highly desired. When in doubt, log. | Flexible volume suitable for debugging and answering Open Questions without causing OOM or Loki rejections. |
| TR-028 | SHOULD | For each incident detected during fast-forward, emit one structured line with event discriminating replay index build (e.g. replay_incident_index_detection) including fingerprint (same value as TR-020 / Fingerprint (v1)), car_idx, session_time_ms or replay_session_time, detection_source (repair_flag | furled_flag | player_incident_count), incident_points when known, subsession_id, replay_frame when available, and the same session spine fields used elsewhere (track_display_name, log_env, loki_push_target where applicable). |
Each JSON index entry has a traceable log line in Grafana for debugging and cross-check; log fingerprint matches the on-disk row. |
| TR-029 | SHOULD | Log validation outcomes (TR-023–TR-025): discrepancy counts, camera seek match rate, and index_build_time_ms / total_detected summary fields in the body of replay_incident_index_validation_summary (or split events if size limits require). |
Grafana Explore can filter by subsession_id in JSON and compare to file output. |
| TR-030 | MUST | If Loki URL is unset or push fails, the index build MUST still complete and write TR-019 JSON; logging failures MUST NOT abort the test. | Graceful degradation; behaviour matches existing Loki sink semantics. |
Taxonomy note: Register new event names in GRAFANA-LOGGING.md when implemented so LogQL and dashboards stay canonical.
Deliver a dedicated dashboard page (separate HTML entry or clearly named sub-view) so the replay incident index is not buried-only in logs or disk files. Follow SimHub dashboard rules: HTML/CSS/JavaScript in Web/sim-steward-dash/ (or an adjacent path documented in deploy), loaded via SimHub’s HTTP port (e.g. http://<host>:8888/Web/...).
| Req ID | Priority | Requirement | Acceptance Criteria |
|---|---|---|---|
| TR-031 | MUST | Add a new web dashboard surface that displays the latest completed index summary (subSessionId, indexBuildTimeMs, totalRaceIncidents, per-carIdx counts per TR-022) when available. |
Summary fields visible after a successful build; empty/disabled state when no index exists for the current context. |
| TR-032 | MUST | Display the incidents array in a sortable, scannable table (or equivalent list UI) with columns matching TR-020: fingerprint, carIdx, sessionTimeMs, detectionSource, incidentPoints. |
All five fields shown for every row (fingerprint may use truncation + tooltip/copy affordance if space is tight); chronological default sort matches TR-021. |
| TR-033 | SHOULD | Show in-progress index-build status: phase (e.g. baseline, fast-forward, validation), elapsed time, and non-high-frequency progress hints (e.g. ReplaySessionTime or frame-derived estimate) without spamming the UI or WebSocket. |
User can tell build is running vs idle vs failed. |
| TR-034 | SHOULD | Provide navigation from the existing SimSteward dashboard to this page (link, tab, or menu entry) and a stable document URL path in the spec/README once chosen. | Discoverable entry point without typing a raw path from memory. |
| TR-035 | MUST | Load index payload from the plugin via the existing WebSocket bridge (broadcast snapshot and/or action request/response JSON). Do not require a second HTTP server inside the plugin. | Data matches TR-019/TR-020 semantics on the wire; one bridge connection model. |
| TR-036 | SHOULD | Allow seek/jump to a selected incident from the table (plugin action calling BroadcastReplaySearchSessionTime or equivalent) when replay mode is active, with clear feedback if seek is unavailable. |
Row action triggers seek; errors surfaced in UI. |
| TR-037 | MUST | If the iRacing SDK is disconnected or SimMode is not replay, the dashboard MUST show a clear message and MUST NOT imply an index is being built. |
Same guardrails as TR-001/TR-002, reflected in UI. |
| TR-038 | MUST | Provide a large "Record" button. When clicked, it activates high-frequency (60Hz) telemetry logging for deep data collection (the "data finding mission"). When clicked again, it stops logging. | Button exists, toggles state visually, and controls the plugin's raw high-frequency logging output. |
Since this implementation is a data finding mission, we need a dedicated Grafana dashboard to visualize the results, measure success rates, and identify SDK limits across multiple test runs.
| Req ID | Priority | Requirement | Acceptance Criteria |
|---|---|---|---|
| TR-039 | MUST | Create a Grafana Dashboard JSON model (docs/dashboards/replay-insights.json or similar) dedicated to these tests. |
Dashboard file is committed to the repository and can be imported into a local Grafana instance. |
| TR-040 | MUST | The dashboard MUST include panels visualizing: index build times vs. replay length (to deduce max fast-forward speed limits), discrepancy counts (detected vs. actual incidents), and high-frequency logging volume when the "Record" mode is active. | Panels correctly query Loki using the replay_incident_index_* events and display meaningful aggregations. |
| Req ID | Priority | Requirement | Acceptance Criteria |
|---|---|---|---|
| TR-041 | MUST | For each milestone M1–M9, when the milestone is marked complete, add a milestone summary in §9 (same pattern as M1 acceptance review): what shipped, which requirement IDs are satisfied, and evidence (source paths, test class names, structured log event names). |
Every completed milestone has a dated summary block under §9; claims map to verifiable artifacts. |
| TR-042 | MUST | Build the automated test suite for the replay incident index: unit tests with mocks/fixtures where the SDK is unavailable, integration or golden-data tests where appropriate; expectations MUST trace to this document, not ad-hoc behavior. | Tests exist, run via CI or a documented local command, and cover the behaviors implied by the linked TR/NFR IDs for M8. |
| TR-043 | MUST | All feature tests for the replay incident index pass locally and in CI. Resolve failures by fixing implementation or, when the written spec was wrong, by updating this document with rationale—not by weakening assertions, deleting cases, broadening tolerances without justification, or matching expectations to incorrect behavior. | Green suite; test edits only alongside corrected spec or post-fix tightened assertions. |
| Req ID | Priority | Requirement | Acceptance Criteria |
|---|---|---|---|
| NFR-001 | MUST | Index construction MUST NOT use the iRacing /data API or other iRacing cloud credentials. Optional: HTTPS POST to the configured Loki endpoint (SIMSTEWARD_LOKI_URL) for Grafana is permitted when enabled; with Loki disabled, no outbound observability traffic is required for the test to pass. |
No iRacing REST/OAuth traffic. Loki traffic only when explicitly configured. |
| NFR-002 | MUST | The test must fail gracefully with a clear error message if the iRacing SDK is not connected. | Graceful failure message on no connection. |
| NFR-003 | SHOULD | Total index build time for a 45-minute race replay must be measured and documented. Target is under 120 seconds — observation not a pass/fail criterion. | Build time recorded in output regardless of duration. |
| NFR-004 | SHOULD | After completion, restore the replay to its original position using the saved ReplayFrameNum from before fast-forward began. |
Replay position restored to pre-test position. |
| NFR-005 | MUST | Implement as a SimHub C# plugin OR standalone C# console application using IRSDKSharper or iRacingSdkWrapper. | Executable runs on Windows without additional runtime dependencies beyond .NET and iRacing. |
| NFR-006 | SHOULD | Document or reuse LogQL examples for the new replay_incident_index_* events. Since this is a data finding mission, ensure logging payloads are expansive enough to answer questions about flag reliability and speed limits. |
Queries reproducible from Grafana Explore; cross-ref GRAFANA-LOGGING.md § LogQL. |
| NFR-007 | SHOULD | The new dashboard page should remain usable on a LAN client (WebSocket host derived from window.location.hostname, same pattern as the main SimSteward dash). |
Remote browser on the same network can open the page and receive data. |
| NFR-008 | MUST | Treat ~3.75 Hz effective sampling vs. session time (60Hz real-time SDK polls at 16× replay speed) as acceptable for incident detection and index build. Document the chosen play-speed multiplier and implied effective rate in test output when reporting build methodology. | Spec and run logs state multiplier; no requirement to exceed SDK real-time 60Hz or to add synthetic higher session-time sampling. |
| Command | Parameters | Purpose |
|---|---|---|
BroadcastReplaySearch |
irsdk_RpySrch_ToStart |
Seek to frame 0 before fast-forward |
BroadcastReplaySetPlaySpeed |
speed (int), slowMotion (bool = false) |
Initiate fast-forward at maximum speed |
BroadcastReplaySetPlayPosition |
irsdk_RpyPos_Begin, frameNumber |
Restore original playback position after test |
BroadcastReplaySearchSessionTime |
sessionNum (int), sessionTimeMS (int) |
Validation: seek to detected incident timestamps |
- Resolved (sampling): At 16× with 60Hz real-time SDK polling, ~3.75 Hz effective vs. session time is acceptable for this project (see §2.7, NFR-008). What is the maximum reliable play speed multiplier before the SDK starts dropping frames or returning stale
CarIdxSessionFlagsvalues? - Does iRacing emit
repair/furledflag bits reliably at high playback speeds, or are flag transitions dropped if the relevant frame is not rendered? - Is
CarIdxSessionFlagspopulated correctly during replay, or are these bits only set during real-time sessions? Must be confirmed empirically. - How does
ReplaySessionTimerelate toSessionTimein the/dataAPI lap records — same time base and units? - Do incidents during caution laps, pit lane, or pre-race warm-up appear in
CarIdxSessionFlagsduring replay?
To maximise test value, use a replay that satisfies the following:
- Full race session (not a clip) with a known incident count
- At least 3 different drivers with incidents (validates per-
carIdxdetection) - At least one driver with multiple incidents on the same lap (validates deduplication logic)
- At least one DNF driver (
ReasonOutStrpopulated) - Ideally a race you participated in, so
PlayerCarMyIncidentCountprovides a second validation channel
This implementation is broken down into the following milestones (tracked in ContextStream). TR-041 applies to every milestone when marked complete (milestone summary in §9). Full criteria: §4.10.
| Milestone | Requirements | Description | Status |
|---|---|---|---|
| M1: Project Setup & SDK Connection | TR-001 – TR-003, NFR-005, TR-041 | Setup plugin structure, connect SDK, verify replay mode, extract SubSessionID. |
Complete |
| M2: Fast-Forward & Baseline Capture | TR-004 – TR-011, NFR-008, TR-041 | Seek to start, capture baseline flags, trigger 16× fast-forward, hook raw native 60Hz polling (~3.75 Hz vs. session time acceptable per §2.7), handle completion. | Complete |
| M3: Incident Detection Logic | TR-012 – TR-018, TR-041 | Detect repair/furled bit rising edges, detect player incident increments, record timestamps and carIdx with 1-second debounce. |
Complete |
| M4: Validation & JSON Output | TR-019 – TR-025, NFR-004, TR-041 | Write chronological JSON index, validate against YAML final incidents, test camera seek matching, restore replay position. | Complete |
| M5: Observability Logging | TR-026 – TR-030, TR-041 | Emit 4-label Loki structured logs for lifecycle phases, detections, and validation summary without tick spam. | Complete |
| M6: SimHub Web Dashboard | TR-031 – TR-038, TR-041 | Create HTML/JS page under Web/, stream data via WebSocket, display summary/table, add row seek actions, implement the "Record" button toggle. |
Complete |
| M7: Grafana Insights Dashboard | TR-039 – TR-040, TR-041 | Create and commit a Grafana Dashboard JSON model specifically for analyzing test data (build speeds, discrepancies, log volumes). | ⏳ Not Started |
| M8: Test suite construction | TR-041, TR-042 | Automated tests for the replay incident index (mocks/fixtures, golden data as needed); expectations trace to this spec. | Complete |
| M9: Tests passing (implementation alignment) | TR-041, TR-043 | All feature tests pass locally and in CI; fix implementation or spec—not tests—to resolve failures. | Complete |
Milestone M6 is Complete; TR-031–TR-038 and TR-041 are implemented as follows.
| Item | Evidence |
|---|---|
| TR-041 | This subsection is the M6 milestone summary. |
| TR-031 / TR-032 | src/SimSteward.Dashboard/replay-incident-index.html: summary block (subSessionId, indexBuildTimeMs, totalRaceIncidents, incidentCountByCarIdx) and sortable table columns matching TR-020. |
| TR-033 | Plugin state.replayIncidentIndex: phase (idle / seeking_start / fast_forward / camera_validating), buildElapsedMs, replaySessionTime, replayFrameNum, replayFrameEnd. |
| TR-034 | Link from src/SimSteward.Dashboard/index.html header to replay-incident-index.html; deploy copies both to Web/sim-steward-dash/. |
| TR-035 | Same Fleck WebSocket as main dash; index payload embedded in throttled state broadcast (~200ms) via BuildReplayIncidentIndexDashboardSnapshot / PluginSnapshot.ReplayIncidentIndex. |
| TR-036 | WebSocket action replay_incident_index_seek with JSON arg sessionTimeMs and optional sessionNum; uses ReplaySearchSessionTime. Row Seek buttons on replay page. |
| TR-037 | Replay page guard banners when !irsdkConnected or !isReplayMode; build/seek actions return not_connected / not_replay_mode / build_in_progress. |
| TR-038 | Action replay_incident_index_record on/off; large Record toggle; 60Hz NDJSON lines under %LocalAppData%\SimSteward\replay-incident-index\record-samples\; structured replay_incident_index_record_window ~1/s (TR-040 hook). |
Code: SimStewardPlugin.ReplayIncidentIndexDashboard.cs, PluginState.cs (ReplayIncidentIndexDashboardSnapshot), ReplayIncidentIndexOutputPaths.TryReadIndexFile, SimStewardPlugin.cs (BuildStateJson / BuildPluginSnapshot / DispatchAction), SimStewardPlugin.ReplayIncidentIndexBuild.cs (ReplayIncidentIndexDashboardNotifyIndexWritten, AppendReplayIncidentIndexRecordSampleIfEnabled). Tests: ReplayIncidentIndexOutputPathsTests, ReplayIncidentIndexBuildTests (EventRecordWindow).
Milestones M8 and M9 are Complete for the shipped replay index pipeline. TR-042 SHOULD expand when M7 (Grafana dashboard JSON) lands.
| Item | Evidence |
|---|---|
| TR-041 | This subsection is the M8/M9 milestone summary (scope, requirement mapping, evidence pointers). |
| TR-042 | ReplayIncidentIndexPrerequisitesTests (TR-001–TR-003 / §4.1); ReplayIncidentIndexBuildTests (TR-004–TR-011, NFR-008 / §4.2–§4.3, M5 event constants / TR-028 taxonomy); ReplayIncidentIndexDetectionTests (TR-012–TR-018 / §4.4); M4: ReplayIncidentIndexFingerprintTests, ReplayIncidentIndexDocumentBuilderTests, ReplayIncidentIndexResultsYamlTests, ReplayIncidentIndexValidationComparerTests, ReplayIncidentIndexOutputPathsTests (TR-019–TR-024, fingerprint §4.5); M5 Loki: AssertLokiQueries + optional ReplayIncidentIndexLokiIntegrationTests with RUN_REPLAY_INDEX_LOKI_ASSERT (observability-testing.md). Test classes reference the spec in XML docs. |
| TR-043 | dotnet test for SimSteward.Plugin.Tests (net48) passes with zero failures; project policy: resolve failures by fixing implementation or updating this document—not by weakening tests. Same suite is enforced by deploy scripts per SimHub development rules. |
Milestone M1 is Complete; TR-001–TR-003, NFR-005, TR-041, and raw session YAML fingerprinting for §2.6 are implemented as follows.
| Item | Evidence |
|---|---|
| TR-041 | This subsection is the M1 milestone summary (scope, requirement mapping, evidence pointers). |
| TR-001 | IRacingSdk (IRSDKSharper) in plugin; structured event replay_incident_index_sdk_ready on iRacing connect (irsdk_connected, update_interval_ms). |
| TR-002 | Event replay_incident_index_session_context logs sim_mode / is_replay_mode from parsed session YAML (WeekendInfo); WARN when a subsession is active but mode is not replay. |
| TR-003 | Same event logs subsession_id (string, same convention as other plugin logs) for use as the index reference key. |
| §2.6 raw YAML | IRacingSdk.Data.SessionInfoYaml fingerprint: session_yaml_fingerprint_sha256_16 (SHA-256 prefix), session_yaml_length, session_info_update (SessionInfoUpdate). Same fingerprint key merged into all spine/routing logs when YAML is available (MergeSessionAndRoutingFields in DataUpdate). |
| NFR-005 | SimHub C# plugin targeting .NET Framework 4.8 with IRSDKSharper (NuGet). |
Code: ReplayIncidentIndexPrerequisites.cs, SimStewardPlugin.ReplayIncidentIndex.cs, SimStewardPlugin.cs / OnIrsdkSessionInfo. Tests: ReplayIncidentIndexPrerequisitesTests. Log taxonomy: GRAFANA-LOGGING.md (replay_incident_index_*).
Milestone M2 is Complete; TR-004–TR-011, NFR-008, and TR-041 are implemented as follows.
| Item | Evidence |
|---|---|
| TR-041 | This subsection is the M2 milestone summary (scope, requirement mapping, evidence pointers). Milestone status synced in ContextStream when marked complete. |
| TR-004 | ReplaySearch(ToStart) from TryBeginReplayIncidentIndexBuildLocked; ReplayFrameNum stabilized at 0 (FrameZeroStableConsecutiveSamples consecutive OnTelemetryData ticks); seek failure → replay_incident_index_build_error (seek_start_timeout). Code: SimStewardPlugin.ReplayIncidentIndexBuild.cs (ProcessSeekingStartLocked). |
| TR-005 | Baseline CarIdxSessionFlags for all 64 slots via Data.GetInt("CarIdxSessionFlags", i); emitted on replay_incident_index_baseline_ready as car_idx_session_flags (full array). |
| TR-006 | PlayerCarMyIncidentCount at baseline as player_car_my_incident_count_baseline on replay_incident_index_baseline_ready. |
| TR-007 | ReplayFrameNumEnd recorded as replay_frame_num_end on baseline and completion events. |
| TR-008 | ReplaySetPlaySpeed(16, false); requested vs telemetry ReplayPlaySpeed on replay_incident_index_fast_forward_started. |
| TR-009 | Native IRSDKSharper OnTelemetryData handler (OnIrsdkTelemetryDataForReplayIndex); UpdateInterval = 1 (60Hz); not SimHub DataUpdate. |
| TR-010 | Fast-forward loop ends when IsReplayPlaying is false; completion_reason (replay_finished | paused_or_stopped) via InferCompletionReason; playback restored to 1×. |
| TR-011 | Wall-clock index_build_time_ms on replay_incident_index_fast_forward_complete (Stopwatch from fast-forward start); fast_forward_telemetry_samples counted per OnTelemetryData tick in FF phase. |
| NFR-008 | effective_sample_hz_vs_session_time (= 60 / play speed) on FF start/complete logs; play speed 16× documented in telemetry fields. |
Code: ReplayIncidentIndexBuild.cs (helpers/constants), SimStewardPlugin.ReplayIncidentIndexBuild.cs, SimStewardPlugin.cs (OnTelemetryData subscribe/unsubscribe, DispatchAction → replay_incident_index_build, ReplayIncidentIndexOnIracingDisconnected). Tests: ReplayIncidentIndexBuildTests. Actions: WebSocket replay_incident_index_build args start | cancel. Log taxonomy: GRAFANA-LOGGING.md (replay_incident_index_started, replay_incident_index_baseline_ready, replay_incident_index_fast_forward_started, replay_incident_index_fast_forward_complete, replay_incident_index_build_error, replay_incident_index_build_cancelled).
Milestone M3 is Complete; TR-012–TR-018 and TR-041 are implemented as follows.
| Item | Evidence |
|---|---|
| TR-041 | This subsection is the M3 milestone summary (scope, requirement mapping, evidence pointers). |
| TR-012 | ReplayIncidentIndexDetection.IsRisingEdge / RepairSessionFlag (0x100000); ReplayIncidentIndexDetector.Process compares each tick vs previous CarIdxSessionFlags after Reset with TR-005 baseline. |
| TR-013 | Same for FurledSessionFlag (0x80000); independent of repair (same tick can emit both). |
| TR-014 | Positive delta on PlayerCarMyIncidentCount vs previous sample → IncidentSample with detectionSource player_incident_count; incidentPoints set when delta is 1, 2, or 4, else null. |
| TR-015 | Process(replaySessionTimeSec, …) uses ReplaySessionTime (seconds) with SessionTime fallback; IncidentSample.SessionTimeMs via ToSessionTimeMs. |
| TR-016 | carIdx from affected slot (flags) or PlayerCarIdx for player channel. |
| TR-017 | CarIdxFastRepairsUsed increments append to ReplayIncidentIndexDetector.FastRepairDeltas (separate from primary IncidentSample list, not TR-020 rows). Baseline captured at frame 0 with flags/player count. |
| TR-018 | Rising edges after bit clear handled by per-frame comparison; PrimaryDebounceSessionTimeSec (1s) on replay session time per car × primary source (repair / furled / player) via TryTakePrimarySlot. |
Runtime wiring: After baseline (CaptureBaselineAndStartFastForwardLocked), the plugin calls ReplayIncidentIndexDetector.Reset with baseline CarIdxSessionFlags, PlayerCarMyIncidentCount, PlayerCarIdx, and per-slot CarIdxFastRepairsUsed. Each OnTelemetryData tick in FastForwarding (ProcessFastForwardingLocked while IsReplayPlaying) invokes Process; primary rows accumulate in _replayIndexIncidentSamples, then M4 persists TR-019 JSON and runs validation (see M4 acceptance review). Completion log: replay_incident_index_fast_forward_complete includes detected_incident_samples and fast_repair_delta_events.
Code: ReplayIncidentIndexDetection.cs (IncidentSample, FastRepairDelta, bitmasks), ReplayIncidentIndexDetector.cs, SimStewardPlugin.ReplayIncidentIndexBuild.cs (baseline fast-repair snapshot, Reset, per-tick Process). Tests: ReplayIncidentIndexDetectionTests.
Milestone M4 is Complete; TR-019–TR-025, NFR-004, and TR-041 are implemented as follows.
| Item | Evidence |
|---|---|
| TR-041 | This subsection is the M4 milestone summary (scope, requirement mapping, evidence pointers). |
| TR-019 | UTF-8 JSON written under %LocalAppData%\SimSteward\replay-incident-index\{subSessionId}.json via ReplayIncidentIndexOutputPaths (atomic temp + replace). |
| TR-020 | ReplayIncidentIndexFingerprint (v1 canonical string + SHA-256 hex); rows in ReplayIncidentIndexDocumentModel / ReplayIncidentIndexDocumentBuilder. |
| TR-021 | ReplayIncidentIndexDocumentBuilder.Build sorts by sessionTimeMs, then carIdx, then detectionSource (ordinal). |
| TR-022 | Root object includes subSessionId, indexBuildTimeMs (wall clock for full build including post-FF), totalRaceIncidents, incidentCountByCarIdx, incidents; optional validation and outputPath. |
| TR-023 | ReplayIncidentIndexResultsYaml.TryParseOfficialIncidentsByCarIdx reads ResultsPositions from raw SessionInfoYaml (prefers telemetry SessionNum captured at baseline; falls back to last non-empty block). |
| TR-024 | ReplayIncidentIndexValidationComparer.BuildDiscrepancies compares per-car detected event counts (TR-020 row counts) to YAML Incidents; list stored in JSON validation.discrepancies. |
| TR-025 | After fast-forward, CameraValidating phase: ReplaySearchSessionTime(SessionNum, sessionTimeMs) per sorted row, CameraValidationCooldownTelemetryTicks (150 ≈ 2.5s @ 60Hz), then CamCarIdx vs expected carIdx; camera_seek_match_percent in JSON and replay_incident_index_validation_summary. |
| NFR-004 | TryRestoreReplayIndexSavedFrameLocked: ReplaySetPlayPosition(Begin, saved frame) + 1× speed after finalize, cancel, disconnect, seek timeout, fast-forward speed failure, and ReplaySearch(ToStart) failure. |
Code: ReplayIncidentIndexFingerprint.cs, ReplayIncidentIndexDocumentModel.cs, ReplayIncidentIndexOutputPaths.cs, ReplayIncidentIndexResultsYaml.cs, ReplayIncidentIndexValidationComparer.cs, ReplayIncidentIndexBuild.cs (EventValidationSummary, cooldown constant), SimStewardPlugin.ReplayIncidentIndexBuild.cs (post-FF pipeline, FinalizeReplayIndexBuildLocked, ProcessCameraValidatingLocked). Tests: ReplayIncidentIndexFingerprintTests, ReplayIncidentIndexDocumentBuilderTests, ReplayIncidentIndexResultsYamlTests, ReplayIncidentIndexValidationComparerTests, ReplayIncidentIndexOutputPathsTests. Structured log: replay_incident_index_validation_summary (GRAFANA-LOGGING.md); JSON write failure → replay_incident_index_build_error (json_write_failed).
Milestone M5 is Complete; TR-026–TR-030, TR-041, and per-detection observability are implemented as follows.
| Item | Evidence |
|---|---|
| TR-041 | This subsection is the M5 milestone summary (scope, requirement mapping, evidence pointers). |
| TR-026 | Lifecycle events (M2–M3) plus M4 replay_incident_index_validation_summary; M5 adds replay_incident_index_detection so the minimum SHOULD set in §4.7 is satisfied. |
| TR-027 | Detection logs are event-driven (one line per accepted primary incident), not per 60Hz tick. |
| TR-028 | ReplayIncidentIndexBuild.EventDetection; LogReplayIncidentIndexDetectionsLocked in SimStewardPlugin.ReplayIncidentIndexBuild.cs emits fingerprint via ReplayIncidentIndexFingerprint.ComputeHexV1 (same inputs as ReplayIncidentIndexDocumentBuilder), car_idx, session_time_ms, detection_source, incident_points, replay_frame, replay_session_time, and MergeSessionAndRoutingFields spine. |
| TR-029 | Validation outcomes remain on replay_incident_index_validation_summary (M4); no split events required. |
| TR-030 | Per-detection Structured calls wrapped in try/catch so logging failures do not abort the build; Loki optional per existing pipeline. |
Code: ReplayIncidentIndexBuild.cs (EventDetection), SimStewardPlugin.ReplayIncidentIndexBuild.cs (LogReplayIncidentIndexDetectionsLocked). Tests: ReplayIncidentIndexBuildTests (event name constants). Fingerprint parity with JSON rows: ReplayIncidentIndexDocumentBuilderTests.Fingerprint_MatchesPerRowCanonicalDigest. Grafana/Loki verification: harness/SimSteward.GrafanaTestHarness emits harness replay_incident_index_detection lines (TR-020 fingerprints); tests/observability/AssertLokiQueries fails unless Loki returns those events; optional ReplayIncidentIndexLokiIntegrationTests queries Loki when RUN_REPLAY_INDEX_LOKI_ASSERT=1 (see observability-testing.md). Log taxonomy: GRAFANA-LOGGING.md (replay_incident_index_detection).
iRacing Replay Incident Index — Technical Requirements v0.7 — Draft