/** * Timing analysis over `runstate:v1` milestone trails. * * Every milestone already stamps `at=`, so a run's per-phase durations are latent in the * trail the moment it is written — nothing needs to be measured, stored, or timed at run * time. This module is the read side of that decision: it turns the milestones * {@link ./runstate!parseMilestones} produces into per-phase spans and aggregates. * * Pure and dependency-free — no `gh`, no network, no clock, no filesystem — so the * arithmetic is unit-testable against fixture trails and the command layer only has to * render what it returns. */ import { type ParsedMilestone } from './runstate'; /** * The synthetic phase covering the gap between ship's two milestones — the PR is open and * green-pending, and nothing is being worked on. It is reported separately because it is * the one span in a run that measures waiting rather than working: folding it into `ship` * would make ship's median a function of CI queue depth rather than of the work. */ export declare const MERGE_WAIT_PHASE = "merge-wait"; /** * Row order for aggregate tables: the protocol's phase order, with `merge-wait` sitting * where it happens (between ship and report). Alphabetical order would read * `gate, implement, merge-wait, plan, …`, which tells a reader nothing about a pipeline. */ export declare const STATS_PHASE_ORDER: readonly string[]; /** The bucket a run lands in when no milestone in the run carries a `model=` key. */ export declare const UNKNOWN_MODEL = "unknown"; /** The group a milestone lands in when it carries no `run=` id. */ export declare const UNKNOWN_RUN = "unknown-run"; /** * Make a milestone-derived value safe to print. * * Control characters are the real hazard, not merely a cosmetic one: `parseMilestone` is * deliberately tolerant and validates nothing but the marker, so `status=` or `model=` can * carry ANSI escapes or a carriage return. Printed raw, those repaint or erase rows of the * table an operator reads to decide whether a run succeeded. They also make `.length` * disagree with the drawn width, which silently destroys column alignment. */ export declare function renderValue(value: string): string; /** One phase span within a run: what ran, when it ended, and how long it took. */ export interface PhaseTiming { /** Phase name, or {@link MERGE_WAIT_PHASE} for the gap between ship's two milestones. */ phase: string; status: string; /** The previous usable milestone's `at=`, or null when there is no measurable start. */ started_at: string | null; /** This milestone's `at=`. */ ended_at: string; /** Elapsed seconds, or null when `started_at` is null. May be negative under clock skew. */ seconds: number | null; } /** Every phase span of one run, plus what the whole run cost. */ export interface RunStats { issue: number; run: string; /** The `model=` recorded by the run (see {@link modelOf}), or null. */ model: string | null; phases: PhaseTiming[]; /** The last milestone's phase — how far the run actually got. */ last_phase: string; /** The last milestone's status, so an in-flight or blocked run is recognisable. */ last_status: string; /** First usable milestone's `at=`. */ started_at: string | null; /** Last usable milestone's `at=`. */ ended_at: string | null; /** * `started_at` → `ended_at`, or null when the run has fewer than TWO usable milestones. * * One usable milestone is not a zero-length run — it is a run whose length is unknown, * which is the normal state of anything still in flight. Reporting it as `0` would drag * every median it feeds toward zero with a duration nobody measured. */ total_seconds: number | null; } /** Median/min/max over every sample of one phase across the selected runs. */ export interface PhaseAggregate { phase: string; /** Spans of this phase that had a measurable duration; unmeasurable ones are excluded. */ samples: number; median_seconds: number; min_seconds: number; max_seconds: number; /** Samples below zero, i.e. from clock skew — a median over these is not meaningful. */ negative_samples: number; } /** Per-`model=` view of whole-run totals — the point of recording `model=` at the gate. */ export interface ModelAggregate { model: string; runs: number; /** Runs of this model whose total was measurable; the rest are excluded below. */ samples: number; median_total_seconds: number | null; min_total_seconds: number | null; max_total_seconds: number | null; negative_samples: number; } /** The two cross-run rollups: per-phase spread, and whole-run totals bucketed by model. */ export interface StatsAggregates { phases: PhaseAggregate[]; models: ModelAggregate[]; } /** An issue in the selection that could not be read at all, and why. */ export interface FailedIssue { issue: number; error: string; } /** Everything `runstate stats` needs to render, for one issue or many. */ export interface StatsReport { /** The repository the trails were read from, when the caller named one. */ repo: string | null; issues: number[]; runs: RunStats[]; aggregates: StatsAggregates; /** Issues in the selection whose trail had no runstate milestones at all. */ issues_without_trail: number[]; /** Issues that could not be read — distinct from an issue that simply has no trail. */ issues_failed: FailedIssue[]; /** Degraded reads: skipped milestones, clock skew, missing run ids. */ warnings: string[]; } /** True when `value` is a timestamp this module will measure against. */ export declare function isUsableTimestamp(value: string): boolean; /** * Median of a non-empty numeric list, rounded to whole seconds. * * Even counts average the two middle values — stated because the other convention (lower * middle) is equally common and the difference shows up in exactly the small sample sizes * these trails produce. * * @throws when `values` is empty — an empty median is `NaN`, which renders as a * plausible-looking `NaN (NaNs)` cell rather than announcing itself. */ export declare function median(values: number[]): number; /** One issue's trail, as the command layer hands it to {@link buildStatsReport}. */ export interface IssueTrail { issue: number; milestones: ParsedMilestone[]; } /** One aggregate row's skew note, or null when every sample was forwards. */ export declare function skewNote(negativeSamples: number): string | null; export interface StatsInput { trails: IssueTrail[]; /** Issues that could not be read at all. */ failed?: FailedIssue[]; /** The repository the trails came from, for unambiguous issue references. */ repo?: string; } /** * Build the whole report from already-fetched trails. * * Takes trails rather than issue numbers so every rule here — grouping, pairing, skipping, * aggregating — is testable against fixture milestones without a `gh` in sight. */ export declare function buildStatsReport(input: StatsInput): StatsReport; //# sourceMappingURL=runstate-stats.d.ts.map