export interface Vec3 { x: number; y: number; z: number; } /** MIRRORS @helix/engine-core's NpcSample — structural, so this repo takes no engine dependency. */ export interface NpcSample { id: string; /** Monotonic page clock, MILLISECONDS — what rows are ordered and differenced by. */ t: number; position: Vec3 | null; arrived?: boolean; hasPath: boolean; pathLength?: number; speed?: number; locomotionState?: string; dead?: boolean; ragdolled?: boolean; state?: Record; } /** A sample re-clocked onto seconds-since-the-run-started, which is what every reported time is in. */ export type TimedSample = NpcSample & { tS: number; }; export declare const STUCK_WINDOW_S = 1.5; export declare const STUCK_MIN_PROGRESS_M = 0.35; /** Below this over the whole run the NPC is reported as never having moved (sensor noise, not travel). */ export declare const MOVED_EPSILON_M = 0.05; export interface StuckWindow { fromS: number; toS: number; durationS: number; /** The MOST any 1.5 s sub-window inside this run made — an upper bound on the progress it achieved. */ progressM: number; } export interface Transition { atS: number; /** null on the first observed value — the timeline opens with what the NPC started on. */ from: string | null; to: string; } export interface ArrivedEdge { atS: number; arrived: boolean; } export interface TargetProgress { target: Vec3; startM: number; finalM: number; closestM: number; closestAtS: number; /** startM − finalM: positive means the NPC closed distance, negative means it lost ground. */ closedM: number; } export type NpcFlag = 'never-moved' | 'dead-throughout' | 'no-position-reads' | 'moving-without-a-path'; export interface NpcMetrics { id: string; samples: number; /** Seconds between this NPC's first and last row (it may appear late — ids are re-scanned per sample). */ spanS: number; firstSeenAtS: number; position: { first: Vec3 | null; last: Vec3 | null; }; pathTracedM: number; netDisplacementM: number; /** net / traced. 1.00 = a straight run, near 0 = it went nowhere. null when nothing was traced. */ progressRatio: number | null; averageSpeedMps: number; /** Max of the blackboard `speed` reads, when the world's NPCs publish one. */ topReportedSpeedMps: number | null; target: TargetProgress | null; stuckWindows: StuckWindow[]; stuckSeconds: number; arrivedEdges: ArrivedEdge[]; /** What `arrived` read on the first row that carried it — the baseline the edges move away from. */ arrivedInitial: boolean | null; /** Fraction of rows whose driver was following a planned path (nav) rather than steering direct. */ hasPathRatio: number; maxPathLength: number | null; locomotionTimeline: Transition[]; stateTimeline: Transition[]; deadThroughout: boolean; ragdolledAnySample: boolean; flags: NpcFlag[]; } export interface NpcInspection { seconds: number; sampleHz: number; settleMs: number; /** How many times npcSample() was actually called (a slow page yields fewer than seconds × sampleHz). */ sampleCount: number; /** The id the run was filtered to, or null for every NPC the world reported. */ npcFilter: string | null; target: Vec3 | null; /** Every id seen across the run, in first-seen order — including ones the filter excluded. */ roster: string[]; npcs: NpcMetrics[]; notes: string[]; } /** Re-clocks raw rows onto seconds since `t0Ms` — the shared origin every NPC's timeline is read against. */ export declare function timeline(rows: readonly NpcSample[], t0Ms: number): TimedSample[]; /** Sum of successive position deltas — how far the NPC actually walked, not how far it got. */ export declare function pathTraced(rows: readonly TimedSample[]): number; /** First position to last — how far it GOT. Traced minus this is what pacing and circling cost. */ export declare function netDisplacement(rows: readonly TimedSample[]): number; /** * Windows where the NPC made less than STUCK_MIN_PROGRESS_M across at least STUCK_WINDOW_S — AIDriver's * own rule, evaluated on a SLIDING window (the driver resets its anchor, so its non-overlapping windows * depend on when the seek began; sliding finds the same stalls regardless of phase). Overlapping hits * merge into one reported window so a 6 s grind reads as one problem, not thirty. */ export declare function stuckWindows(rows: readonly TimedSample[]): StuckWindow[]; /** Value changes over time, opening with the first value observed (from: null). */ export declare function transitionsOf(rows: readonly TimedSample[], read: (row: TimedSample) => string | undefined): Transition[]; /** Edges of the driver's `arrived` flag — the seek's completion, which no position number states. */ export declare function arrivedEdges(rows: readonly TimedSample[]): ArrivedEdge[]; /** Rows whose driver was on a planned path. 0 while the NPC moves = the NavGrid degraded to direct steering. */ export declare function hasPathRatio(rows: readonly TimedSample[]): number; export declare function targetProgress(rows: readonly TimedSample[], target: Vec3): TargetProgress | null; /** Everything above for one NPC's rows. `rows` must be that id's samples, in sample order. */ export declare function summarizeNpc(id: string, rows: readonly TimedSample[], target: Vec3 | null): NpcMetrics; export interface SummarizeOptions { seconds: number; sampleHz: number; settleMs: number; npc?: string | null; target?: Vec3 | null; } /** Frames (one npcSample() call each) → the whole inspection. Ids are grouped in first-seen order. */ export declare function summarizeSamples(frames: readonly (readonly NpcSample[])[], opts: SummarizeOptions): NpcInspection;