import type { RunIndexEntry } from "./run-index.js"; /** One lab as the labs list shows it. */ export interface LabRollup { labId: string; /** Total runs attributed to this lab. */ runs: number; /** Runs whose status record is fresh right now. */ live: number; /** The newest run of this lab, whatever its state. */ latest?: RunIndexEntry; /** Live runs, newest first — the labs list shows the first one inline. */ liveRuns: RunIndexEntry[]; } /** * Group runs by lab, newest-first within each lab and by recency between labs. Runs with no lab * attribution are collected under `unattributed` rather than invented into a lab — the honest * home for pre-contract runs and library callers. */ export declare function groupRunsByLab(entries: readonly RunIndexEntry[]): { labs: LabRollup[]; unattributed: RunIndexEntry[]; }; /** * What the launch screen may claim about a lab, and on what evidence. Every figure carries the * number of runs it came from; `sample: 0` means there is nothing to claim and the caller must * say so rather than showing an empty number. */ export interface LabExpectation { /** How many completed runs of this lab the figures are drawn from. */ sample: number; medianDurationMs?: number; /** Range across the sample, so the UI can show a span instead of a false point estimate. */ durationRangeMs?: { min: number; max: number; }; medianCostUsd?: number; /** Completed runs whose cost was declared absent — excluded from the median, reported here so * a partial sample can never masquerade as a full one. */ costUnknown: number; } /** * Derive what to expect from a lab's own history. Only FINISHED runs count: an interrupted run's * duration is the length of an accident, not of a study, and including it would quietly bias the * estimate the operator uses to decide whether to press Start. * * MODE MATTERS, and mixing modes is a lie rather than an imprecision. A dry run spends nothing and * takes no time, so a median over nine dry runs and one live one reports that a live run is free — * next to a control that spends money. Pass the mode the figure is about; omit it only for a * summary that is not attached to an action. */ export declare function expectationFor(entries: readonly RunIndexEntry[], mode?: "dry-run" | "live"): LabExpectation; /** * The one sentence the launch screen shows about time and money, with its denominator attached. * A lab with no history says so plainly instead of borrowing another lab's numbers or inventing * a range — "no runs yet" is information, and the operator can still press Start. */ export declare function expectationLine(expectation: LabExpectation): string; /** * The one line a lab may say about itself, wherever it is shown. * * Prefers LIVE figures: cost and duration are what someone reads before spending, and a median * diluted by dry runs reports a live study as cheaper and faster than it has ever been. With no * live history it reports the count and claims nothing about time or money — which is why this is * shared rather than reimplemented per surface, since the two disagreeing is the whole failure. */ export declare function labSummaryLine(row: Pick): string; /** Compact duration: seconds under a minute, then m/s, then h/m. */ export declare function formatDuration(ms: number): string; export interface NormalizedThought { lines: string[]; /** True when the source text did not fit and was cut — shown, never silent. */ truncated: boolean; } /** * Prepare a participant's recorded reasoning summary for a fixed-width surface. Provider summaries * arrive with markdown section leads (`**Thinking through setup**`) and hard newlines; a terminal * needs plain text wrapped to a line budget. * * The text is never paraphrased or shortened by meaning — only wrapped, and cut at a word boundary * with an ellipsis when it does not fit. `truncated` is how the surface says so. */ export declare function normalizeThought(text: string, options: { width: number; maxLines: number; }): NormalizedThought; /** * Which slice of a list to draw, keeping the selection visible with a little context around it. * Ink has no scroll container, so every long list windows by hand; getting this wrong is how a * selection disappears off the top of a phone screen. */ export declare function listWindow(args: { total: number; selected: number; viewport: number; /** Rows of context to keep above/below the selection when scrolling. */ margin?: number; }): { start: number; end: number; }; /** The label for a run's state, in the register the surfaces share. */ export declare function livenessLabel(entry: Pick): string; /** A lab as the labs list shows it: what is declared, joined to what actually happened. */ export interface LabRow { /** * Stable identity for THIS ROW. A manifest's path when it has one, else the lab id — because two * manifests can declare the same id, and keying rows by id makes them collapse into a pair of * indistinguishable duplicates. */ key: string; /** The declared id. Run history attributes to this, so it is NOT unique across manifests. */ labId: string; /** * The handle an operator would actually type. Lab resolution is by FILENAME, so the manifest * `\`.humanish/labs/persona-contrast-live.yaml\`` is reached as `persona-contrast-live` even when * the id inside it says something else. */ name: string; /** The manifest's own title, when it has one. */ title?: string; /** * The shortest label that is UNIQUE among the rows it is listed with: the title when no other * lab shares it, else the filename, else the full path. A list is only navigable if every row can * be told from every other one, and a title is not guaranteed to be distinct — two manifests in * this repo carry the same title AND the same declared id, differing only by filename. */ label: string; /** The manifest's own words, for the list to say what this study IS. */ description?: string; /** Repo-relative manifest path, absent for a lab known only from run history. */ path?: string; origin?: "committed" | "ignored" | "explicit"; /** * False for a lab that has runs but NO manifest here — renamed, deleted, or run from a path that * is gone. Its runs are still evidence and must stay reachable, so it is listed and marked rather * than dropped. */ declared: boolean; /** * How many OTHER manifests declare this same lab id. Above zero, the run history below is shared * between them and cannot be attributed to one file — worth saying, because it is a * misconfiguration the operator almost certainly does not know about. */ sharesIdWith: number; runs: number; live: number; latest?: RunIndexEntry; liveRuns: RunIndexEntry[]; /** Across every finished run, whatever its mode. A summary, never attached to a spend decision. */ expectation: LabExpectation; /** Live runs only — the figure that belongs beside anything that spends money. */ liveExpectation: LabExpectation; } /** The addressable handle for a manifest: its filename without directory or extension. */ export declare function labNameFromPath(manifestPath: string): string; export interface DeclaredLab { id: string; title?: string; /** The manifest's own description, so a list of studies can say what each one is. */ description?: string; path?: string; origin?: "committed" | "ignored" | "explicit"; } /** * Join declared lab manifests to run history. * * Neither side alone is the truth. A fresh project has manifests and no runs — those labs are the * whole screen, and a list built only from history would be empty on exactly the first visit that * matters. A long-lived project accumulates runs from labs whose manifest has since been renamed or * deleted — that evidence still exists on disk, so dropping those rows would make real runs * unreachable from the surface that is supposed to list them. * * ONE ROW PER MANIFEST, not per id. The two are not the same thing: a manifest is addressed by its * filename while its runs attribute to the id declared inside it, so several files can legitimately * share an id. Collapsing them hides a real file; keying by id duplicates a row with no way to tell * the copies apart. Both happen in practice — this repo has exactly that pair. * * Order puts a lab someone is working in first, then labs by how recently they ran, then declared * labs that have never run (alphabetically BY WHAT IS DISPLAYED, so the order on screen is the * order a reader can predict), then labs known only from history. */ export declare function labRows(declared: readonly DeclaredLab[], entries: readonly RunIndexEntry[]): { rows: LabRow[]; unattributed: RunIndexEntry[]; };