/** * Canonical telemetry normalization — the single source of truth for what a * lap IS across HotLap.ai (UI, Edge Functions, CLI). * * Implements RFC-0001 (hotlap.ai repo, docs/rfcs/RFC-0001-canonical-telemetry-parser.md): * - C2: official lap time from the sim's LapLastLapTime channel, with * crossing-interpolation fallback. Sample striding is never an input. * - C3: a lap is `completed` only when the sim scored it. * - C4: sectors computed at full resolution, numbered 1..N (iRacing S1..SN), * no silent rescaling — the residual is reported instead. * - C5: session metadata resolved via the SessionNum channel against the * YAML Sessions list; multi-session files split per session. * - C7: classify, don't drop — every observed lap appears with flags. * * RUNTIME NEUTRALITY: this module must not import Bun/Deno/Node APIs. * It consumes an already-parsed IBT source (see IbtSource). */ /** Structural subset of the IBT class this module needs (keeps it testable). */ export interface IbtSource { readonly tickRate: number; readonly recordCount: number; getAllTyped(key: string): Float32Array | Float64Array | Int32Array | Uint32Array | null; getAll(key: string): unknown[] | null; getSessionInfo(key?: string): T | null; } export type LapFlag = 'out-lap' | 'no-lap-last-time' | 'low-completion' | 'incident-delta' | 'unrealistic-speed' | 'too-few-samples' | 'no-sector-data'; export type TimingSource = 'lap-last-lap-time' | 'crossing-interpolation' | 'sample-span'; export interface NormalizedSector { /** 1-based, matches iRacing S1/S2/S3 display convention. */ sectorNum: number; timeSec: number | null; } export interface NormalizedLap { lapNumber: number; completed: boolean; /** Official lap time (null when the lap was never completed/scored and no fallback exists). */ lapTimeSec: number | null; timingSource: TimingSource | null; /** Diagnostic: max−min SessionTime within the lap. Never the official time. */ sampleSpanSec: number; distanceKm: number; completionPct: number | null; sectorTimes: NormalizedSector[]; /** lapTimeSec − Σ sectors; ≈0 when timing and sectors agree. Null without both. */ sectorResidualSec: number | null; incidents: number; flags: LapFlag[]; sampleCount: number; /** First/last record indices of this lap in the file (for consumers extracting points). */ firstSampleIndex: number; lastSampleIndex: number; } export interface NormalizedSession { sessionNum: number; /** Sessions[sessionNum].SessionType — full string, e.g. "Offline Testing", "Lone Qualify". */ sessionType: string | null; sessionName: string | null; /** WeekendInfo.EventType — event-level context, distinct from sessionType. */ eventType: string | null; trackId: number | null; trackName: string | null; trackConfigName: string | null; trackLengthKm: number | null; seriesId: number | null; seasonId: number | null; sessionId: number | null; subSessionId: number | null; driverCarIdx: number; driverName: string | null; carId: number | null; carScreenName: string | null; carPath: string | null; tickRate: number; laps: NormalizedLap[]; } export declare const NORMALIZE_THRESHOLDS: { readonly MIN_SAMPLES: 10; readonly MIN_COMPLETION_PCT: 0.9; readonly MAX_AVG_SPEED_KMH: 220; /** * LapLastLapTime updates shortly after the line crossing. If the recording * contains fewer than this many seconds of lap N+1, a missing update means * "recording ended before the sim published the time", not "unscored lap". */ readonly LLT_UPDATE_GRACE_SEC: 2; }; /** * Normalize a parsed .ibt file into one NormalizedSession per SessionNum * observed in the channel data (RFC C5). */ export declare function normalizeIbt(source: IbtSource): NormalizedSession[]; //# sourceMappingURL=normalize.d.ts.map