/** * The long run: many reports over many periods, as one series. * * Every comparison in Trazum is between two logs, and a product's cost * problem is rarely visible in two — it is visible in twenty. This module * takes *stored reports* (the `--json` documents a team already keeps) and * builds the series no pairwise comparison can see: the workload that grew a * little every week, the model share that has been climbing since a date, * the cache hit rate decaying slowly enough that no single week's report * called it a finding. * * **Still no forecasts.** Twenty points make a trend visible; they do not * make next month knowable. The series is stated, the shape is named as * consecutive movement — never a line fitted through the points — and where * it goes next remains the reader's to judge, the same refusal * `modelMixDrift` has carried since 1.27. * * **Derived from stored reports, not re-parsed logs**, so a year of `--json` * output is enough and the raw logs can be thrown away — which is what the * privacy story requires anyway. Browser-safe: documents in, series out. */ import type { PlanActionKind, PlanDocument } from './plan.js'; /** The slice of a stored profile document this module actually reads. */ export interface StoredReport { /** Where it came from — a file name, shown so a finding can be traced. */ name: string; span: { fromMs: number; toMs: number; } | null; totalUsd: number; /** * null when the source serves no request count — a bucketed usage API. Zero * would read as "no traffic" against real spend, which is the reading this * product refuses everywhere it can occur. */ calls: number | null; /** Label → dollars this period. */ byLabel: Map; /** Model → dollars this period. */ byModel: Map; /** Share of input tokens served from cache, or null when unknowable. */ cacheReadShare: number | null; } /** * A run of consecutive movement, named — never extrapolated. * * `periods` counts the *rises* (or falls), so a run of 3 spans 4 reports. * The floor is 3: two rises is what `--against` already shows, and one is * noise wearing a trend's clothes. */ export interface HistoryRun { kind: 'label-spend-climbing' | 'model-share-climbing' | 'cache-share-decaying'; subject: string; /** Consecutive rises (falls, for decay). */ periods: number; /** The report the run started in, by name — "climbing since ". */ sinceName: string; /** First and last values of the run, so the reader judges the size. */ from: number; to: number; /** * Days inside this run's stretch that no report covers. * * A run is *consecutive reports*, and until this field existed it read as * *consecutive time*. Four reports with a three-week hole between the second * and the third is not "climbing for four periods" in any sense a reader * would act on — the climb may have reversed and come back inside the hole, * and nothing here can see it. The caveat travels with the finding rather * than living in another field the reader has to cross-reference, because a * finding that needs a second lookup to be read correctly will be read * incorrectly. */ unmeasuredDays: number; } /** * A stretch of calendar time no stored report covers. * * Arithmetic on the spans, not an inference about a schedule: this module has * no idea how often somebody meant to run anything, and guessing a cadence in * order to call a gap late would be the tool deciding what the reader's * routine is. A hole between one report's end and the next one's start is a * fact about the reports. * * It is also the whole of what "nobody produced this series on a schedule" * looks like from inside the data: a cron that died three weeks ago does not * announce itself, it just stops adding points — and a shorter series and a * series with a hole in it read identically until somebody says which. */ export interface UnmeasuredStretch { fromMs: number; toMs: number; days: number; /** The report that ends where the hole begins. */ afterName: string; /** The report that begins where the hole ends. */ beforeName: string; } /** * Two reports covering some of the same calendar time. * * `history` never sums `totalUsd` across periods, but a reader with the * document in a spreadsheet will, and two reports over the same fortnight * count that fortnight twice. Named rather than merged: which of the two is * the better measurement is not knowable from here, and picking one would be * throwing away money on a guess. */ export interface OverlappingReports { a: string; b: string; days: number; } /** The same action planned again and again: a decision nobody is executing. */ export interface RepeatedPlanAction { kind: PlanActionKind; label: string; model: string; appearances: number; firstPlanned: string | null; lastPlanned: string | null; } export interface HistoryDocument { schemaVersion: 1; /** Ordered oldest first by span start. */ periods: { name: string; fromMs: number; toMs: number; totalUsd: number; calls: number | null; }[]; /** Per label, dollars per period — null where the label had no traffic. */ labelSeries: { label: string; points: (number | null)[]; }[]; /** Per model, share of that period's total — null where absent. */ modelShareSeries: { model: string; points: (number | null)[]; }[]; /** Cache read share per period, null where unknowable. */ cacheShareSeries: (number | null)[]; /** The findings only a series can make. Shapes, never forecasts. */ runs: HistoryRun[]; /** Plans in the same directory, held against each other. */ repeatedPlanActions: RepeatedPlanAction[]; /** * Reports that carry no span cannot be placed on a timeline; they are * named here and in no series above, never silently absorbed. */ undatedReports: string[]; /** * Calendar time between the first report's start and the last one's end * that no report covers, oldest first. * * The series is stated with its holes rather than closed over them: a * scheduled run that stopped three weeks ago produces a series that looks * exactly like a shorter one, and only this field tells the two apart. */ unmeasured: UnmeasuredStretch[]; /** The same calendar time covered by two reports. Named, never merged. */ overlappingReports: OverlappingReports[]; } export declare const MIN_RUN = 3; export declare function buildHistory(reports: StoredReport[], plans?: (PlanDocument & { createdAt?: string; name?: string; })[]): HistoryDocument; /** * Reads one stored `profile --json` document into the slice history needs. * Returns null when the JSON is not a profile document — the caller names * the file rather than absorbing it. */ export declare function storedReportFrom(name: string, parsed: unknown): StoredReport | null; //# sourceMappingURL=history.d.ts.map