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 (224 loc) • 11.9 kB
TypeScript
/**
* 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;