UNPKG

agentfootprint

Version:

The explainable agent framework — backtrack a wrong answer to the exact context that caused it (evidence, not guesses). Built on footprintjs.

225 lines • 11.9 kB
/** * time/axis — a dataset's TIME AXIS: the declaration, its one judge, and * the read-side view that turns the declared column into comparable instants. * * Pattern: declared, never guessed; judged at mint, never repaired; values * read through ONE view ({@link normaliseInstants}) that never * touches the stored rows. Moved here from `artifacts/timeAxis.ts` * (which `artifacts/index.ts` still re-exports), so the axis speaks * the time layer's one grammar: `interval` is a `duration.ts` * duration under {@link AXIS_UNITS}, `zone` is a `zone.ts` zone name. * Role: L0 leaf (the time layer's one owner). Imports `instant.ts`, * `duration.ts` and `zone.ts`. Asked by `artifacts/minting.ts` · * `prepareArtifact` and `artifacts/datasetResult.ts` · * `stageDatasetArtifacts` (the judge), and by any consumer that * compares a dataset's times (the view). * Emits: N/A. * * ## Why a producer declares it * * A table of rows is a time series only when one column IS time, and the * reader of the rows cannot know that honestly: `ts` holding 1726000000 is * epoch SECONDS to the tool that wrote it and a large integer to everything * else; `'2026-09-14T10:00:00'` is UTC or the collector's wall clock. So the * producer SAYS it, on the ticket (`PutArtifactInput.timeAxis`, carried on * `ArtifactMeta.timeAxis`). A malformed declaration is REFUSED at mint * ({@link timeAxisIssues}) — never trimmed or defaulted — and the shape is * CLOSED: a misspelt `agg` is refused by name, never dropped. * * ## The view — values, not the declaration * * The judge never reads a value; {@link normaliseInstants} does, at read time. * It turns the declared column into UTC `Z` instants spelled at ONE precision, * so a lexicographic compare downstream is chronological, and it is honest * about every value it could not place: * * | Value | Under | Read as | * |-------|-------|---------| * | a number | `epoch-s` / `epoch-ms` | that instant, exactly as written (no float rounding) | * | ISO with `Z` or `±HH:MM` | `iso`, any zone | that instant (the lenient profile — a producer's spelling) | * | ISO with NO offset | `iso`, no `zone` | **naive** — counted, never read as UTC; `status: 'naive-values'`, or refused under `naive: 'refuse'` | * | ISO with no offset | `iso` + `zone` | a wall time in that zone; one instant, or — in a fall-back overlap — the row-order rule below; in a spring-forward gap, counted `dstGap` | * | anything else | — | counted `unreadable` (`missing` when the row lacks the column) | * * **The fall-back rule.** In the hour the clocks go back, one wall time names * two instants. Read in the rows' order, the values before the wall clock * steps back take the earlier offset and those from the step on the later * (a note `{ kind: 'dst-overlap', resolvedBy: 'row-order' }` per value). When * the order cannot tell — a lone `01:30`, no step back, two steps back, or a * column whose wall times are not in order anywhere — the value is counted * `dstAmbiguous` and handled like a naive one. Nothing is picked silently. * * @example * ```ts * timeAxisIssues({ column: 'ts', unit: 'epoch-s', zone: 'UTC' }); // ['timeAxis.zone is for wall-clock ISO values; …'] * normaliseInstants([{ ts: 1726300800 }, { ts: 1726304400.5 }], { column: 'ts', unit: 'epoch-s' }); * // { status: 'instants', precision: 'millisecond', * // points: [{ row: 0, at: '2024-09-14T08:00:00.000Z' }, { row: 1, at: '2024-09-14T09:00:00.500Z' }], … } * normaliseInstants([{ t: '2026-09-14T10:00:00' }], { column: 't', unit: 'iso' }); * // { status: 'naive-values', count: 1, points: [], … } — never read as UTC * ``` */ import { type InstantText } from './instant.js'; /** How the time column's values are written. */ export type TimeAxisUnit = 'iso' | 'epoch-s' | 'epoch-ms'; /** How one row summarises the interval it stands for. */ export type TimeAxisAggregate = 'avg' | 'min' | 'max' | 'sum' | 'count' | 'last'; /** The closed sets, in one place, for the judge and for its own messages. */ export declare const TIME_AXIS_UNITS: readonly TimeAxisUnit[]; export declare const TIME_AXIS_AGGREGATES: readonly TimeAxisAggregate[]; /** * A dataset's declared time axis. * * @example hourly buckets, two measures summarised differently * { column: 'hour', unit: 'iso', interval: '1h', * aggregate: { read_iops: 'avg', peak_iops: 'max' } } * @example raw samples every five minutes, epoch seconds * { column: 'ts', unit: 'epoch-s', interval: '5m', aggregate: 'raw' } */ export interface DatasetTimeAxis { /** The column that holds time. */ readonly column: string; /** `'iso'` — ISO-8601 strings; `'epoch-s'` / `'epoch-ms'` — numbers since * 1970-01-01T00:00:00Z in seconds / milliseconds. */ readonly unit: TimeAxisUnit; /** IANA zone name (`'Europe/London'`) the values are WALL-CLOCK in — never * an abbreviation such as `'PST'` or a bare offset. `'iso'` only — an epoch * is an instant and has no zone to declare. Absent: every ISO value must * carry its own offset (`Z` or `±HH:MM`); a value without one is NEVER * read as UTC — {@link normaliseInstants} counts it (`naive-values`) or, * by the reader's choice, refuses the column. */ readonly zone?: string; /** Width of the interval one row stands for: a positive whole number and * one of `s m h d w` (`'30s'`, `'5m'`, `'1h'`, `'1d'`, `'1w'`) — the time * layer's one duration grammar, any number of digits. With * `aggregate: 'raw'` it is the sampling cadence. */ readonly interval?: string; /** * How each row summarises its interval. `'raw'` — every row is a sample as * collected, nothing was reduced. One aggregate — every measure column was * reduced the same way. A record — measure column to its own aggregate * (`{ avg_iops: 'avg', peak_iops: 'max' }`); a column not named there is * not a declared measure. Any aggregate other than `'raw'` requires * `interval`, and `interval` requires `aggregate`: a summary of an interval * nobody named, or an interval with no word for what happened in it, is * half a declaration. */ readonly aggregate?: 'raw' | TimeAxisAggregate | Readonly<Record<string, TimeAxisAggregate>>; } /** * THE ONE JUDGE of a time-axis declaration. Returns every problem, in words a * producer can act on; an empty list means the declaration is well-formed. * Never throws, never repairs, never reads a value. */ export declare function timeAxisIssues(value: unknown): string[]; /** What a consumer finds on a ticket. `malformed` exists for tickets minted * outside this library's put (a foreign writer, an old store) — a consumer * shows it as an error, never as an undeclared dataset. */ export type TimeAxisReading = { readonly status: 'absent'; } | { readonly status: 'declared'; readonly axis: DatasetTimeAxis; } | { readonly status: 'malformed'; readonly issues: readonly string[]; }; /** Read the declaration off a ticket (`ArtifactMeta`, or anything shaped like * one). Absent is absent; anything present is judged. */ export declare function readTimeAxis(meta: unknown): TimeAxisReading; /** * The declared SUMMARY in words, for a chart title — the one wording every * consumer shares, so two screens never describe one ticket two ways. * * `{ interval: '1h', aggregate: { a: 'avg', b: 'max' } }` → `'hourly avg and max'` * `{ interval: '5m', aggregate: 'max' }` → `'5-minute max'` * `{ interval: '5m', aggregate: 'raw' }` → `'raw samples every 5 minutes'` * `{ aggregate: 'raw' }` → `'raw samples'` * * `undefined` when the declaration says nothing about summarising (column and * unit only) — there is no summary to put in a title, and inventing "raw" * would claim something the producer did not. */ export declare function describeTimeAxis(axis: DatasetTimeAxis): string | undefined; /** What {@link normaliseInstants} does with a naive value: count it (`'flag'`, the default) or refuse the column. */ export type NaiveValues = 'flag' | 'refuse'; /** Options of {@link normaliseInstants}. */ export interface NormaliseOptions { /** A value whose clock is unknown (naive, or a fall-back wall time the row * order cannot place): `'flag'` counts it and reads the rest; `'refuse'` * places nothing. Never read as UTC either way. Default `'flag'`. */ readonly naive?: NaiveValues; } /** The one precision every point of a view is spelled at: the finest any value was written in. */ export type AxisPrecision = 'second' | 'millisecond' | 'nanosecond'; /** One placed value: the row it came from and its UTC instant. */ export interface AxisPoint { /** The row's index in the rows as stored. */ readonly row: number; /** The instant in UTC (`Z`), at the view's one precision — so comparing two * spellings as text compares them in time. */ readonly at: InstantText; } /** A value the fall-back overlap doubled, placed by the rows' order. */ export interface AxisOverlapNote { readonly kind: 'dst-overlap'; readonly resolvedBy: 'row-order'; readonly row: number; /** Which of the two instants the wall time names: before the clock stepped back, or after. */ readonly which: 'earlier' | 'later'; } /** Every value the view could not place, by reason. */ export interface AxisCounts { /** An ISO value with no offset under an axis with no `zone`. */ readonly naive: number; /** A wall time the fall-back doubles, where the rows' order cannot tell which. */ readonly dstAmbiguous: number; /** A wall time the spring-forward skips — it names no instant. */ readonly dstGap: number; /** Present, but not a value of the declared unit (or outside years 0000–9999). */ readonly unreadable: number; /** The row lacks the column, holds `null`, or is not a record. */ readonly missing: number; } /** * The read-side view of a declared time column. * * - `instants` — every value whose clock is known was placed. * - `naive-values` — `count` values have no known clock (`naive` + * `dstAmbiguous`); they are not in `points`, and nothing should compare or * join the series with another source as if they were. * - `refused` — the same, under `naive: 'refuse'`: nothing is placed. * * `points` are sorted in time (ties by row); `unreadable`, `missing` and * `dstGap` are counted under every status. */ export type NormalisedAxis = { readonly status: 'instants'; readonly points: readonly AxisPoint[]; readonly precision: AxisPrecision; readonly notes: readonly AxisOverlapNote[]; readonly counts: AxisCounts; } | { readonly status: 'naive-values'; readonly count: number; readonly points: readonly AxisPoint[]; readonly precision: AxisPrecision; readonly notes: readonly AxisOverlapNote[]; readonly counts: AxisCounts; } | { readonly status: 'refused'; readonly reason: 'naive-values'; readonly count: number; readonly counts: AxisCounts; }; /** * THE READ-SIDE VIEW of a declared time column: every value turned into a UTC * `Z` instant at one precision, sorted in time, with every value it could not * place counted by reason (the module table). The rows are only read — never * copied into, reordered or rewritten — so the stored bytes are unchanged. * * A malformed `axis` throws a `TypeError` naming its issues (read a ticket * with `readTimeAxis` first; a declaration is never repaired). */ export declare function normaliseInstants(rows: readonly unknown[], axis: DatasetTimeAxis, options?: NormaliseOptions): NormalisedAxis; //# sourceMappingURL=axis.d.ts.map