agentfootprint
Version:
The explainable agent framework — backtrack a wrong answer to the exact context that caused it (evidence, not guesses). Built on footprintjs.
148 lines (147 loc) • 7.32 kB
TypeScript
/**
* time/zone — IANA zone names and offset arithmetic, through `Intl`.
*
* Pattern: the platform's tz database behind two small questions — "what is
* the offset of this zone at this instant?" and "which instant is
* this wall time in this zone?" — with Temporal's four DST words for
* the second. No dependency: `Intl.DateTimeFormat` carries the
* database the runtime ships.
* Role: L0 leaf (the time layer's one owner). Imports `instant.ts` only.
* Emits: N/A.
*
* ## A zone is an IANA name, never an abbreviation
*
* `Intl` is more forgiving than this layer: it accepts `PST` (and reads it as
* `America/Los_Angeles`, whose October offset is -07:00, not PST's -08:00),
* `EST` (as `America/Panama`), and a bare offset such as `+05:30`. Each of
* those turns a person's words into a zone without saying so, so
* {@link isZoneName} refuses them: a name is accepted when it has an
* `Area/Location` shape (`America/Los_Angeles`, `Etc/GMT+5`) or is `UTC` /
* `GMT`, AND `Intl` knows it. An abbreviation reaches a zone only through a
* recorded map (a later step), never here.
*
* ## Cost — one `formatToParts` per (zone, instant), memoised
*
* Asking `Intl` is the whole cost of this module (a `formatToParts` is
* microseconds; everything else is arithmetic), and a resolution asks the
* same instants again: every candidate a DST probe validates is then spelled
* with its offset. So the formatter is built once per zone and the reading of
* each (zone, instant) is kept — the tz database cannot change inside a
* process — in a bounded memo cleared when full, like the formatter cache.
* `readWall` also skips the one probe it can prove redundant: when the offset
* a day before, at, and a day after the wall time agree, no change falls in
* that window, so the one candidate needs no check. What this buys is pinned
* as a COUNT by the performance case of test/time/resolve.test.ts, not a
* duration.
*
* ## DST — the four words
*
* | Wall time | `compatible` | `earlier` | `later` | `reject` |
* |-----------|--------------|-----------|---------|----------|
* | exists once | it | it | it | it |
* | exists twice (fall back, `01:30`) | the first | the first | the second | `undefined` |
* | never exists (spring forward, `02:30`) | shifted forward by the gap (`03:30`) | shifted back (`01:30`) | shifted forward | `undefined` |
*
* @example
* ```ts
* isZoneName('America/Los_Angeles'); // true
* isZoneName('PST'); // false — an abbreviation
* offsetAt('America/Los_Angeles', Date.parse('2026-10-09T15:00:00Z')); // -420
* readWall({ year: 2026, month: 11, day: 1, hour: 1, minute: 30 }, 'America/Los_Angeles');
* // { kind: 'overlap', earlier: …08:30Z, later: …09:30Z }
* ```
*/
/** An IANA zone name, checked through `Intl`. Never an abbreviation, never a bare offset. */
export type ZoneName = string;
/** A wall-clock reading with no zone. `second` and `millisecond` default to 0. */
export interface WallTime {
readonly year: number;
readonly month: number;
readonly day: number;
readonly hour: number;
readonly minute: number;
readonly second?: number;
readonly millisecond?: number;
}
/** Temporal's four words for a wall time a DST change doubles or skips. */
export type Disambiguation = 'compatible' | 'earlier' | 'later' | 'reject';
/** What a wall time names in a zone: one instant, two (fall back), or none (spring forward). */
export type WallReading = {
readonly kind: 'unique';
readonly ms: number;
} | {
readonly kind: 'overlap';
readonly earlier: number;
readonly later: number;
} | {
readonly kind: 'gap';
readonly earlier: number;
readonly later: number;
};
/** Whether `value` is an IANA zone name this runtime's tz database knows — see the module header for what is refused. */
export declare function isZoneName(value: unknown): value is ZoneName;
/** The runtime's canonical spelling of a zone (`america/los_angeles` → `America/Los_Angeles`), or `undefined` when it is not a zone name. */
export declare function canonicalZone(value: unknown): ZoneName | undefined;
/** The wall time an instant shows in `zone` (milliseconds kept). Frozen: the reading is shared through the memo. */
export declare function wallAt(zone: ZoneName, ms: number): WallTime;
/**
* The offset of `zone` at the instant `ms`, in minutes east of UTC
* (`America/Los_Angeles` in October: -420). A historical local-mean-time
* offset can be a fraction of a minute.
*/
export declare function offsetAt(zone: ZoneName, ms: number): number;
/** Whether a wall time's fields name a real calendar time (any zone aside). */
export declare function isWallTime(value: unknown): value is WallTime;
/**
* What `wall` names in `zone`: one instant, two (a fall-back overlap:
* `earlier`, `later`), or none (a spring-forward gap: `earlier` is the wall
* time read with the offset after the change, `later` with the offset before
* it — Temporal's two readings). Throws a `TypeError` on a wall time that is
* not a calendar time or a zone that is not a zone name.
*/
export declare function readWall(wall: WallTime, zone: ZoneName): WallReading;
/**
* The instant `wall` names in `zone` under `disambiguation` (the module
* table), or `undefined` under `'reject'` when the wall time is doubled or
* skipped.
*/
export declare function wallToInstant(wall: WallTime, zone: ZoneName, disambiguation: Disambiguation): number | undefined;
/**
* The tz database version this runtime's `Intl` carries — Node names it
* (`process.versions.tz`, e.g. `'2025b'`); any other runtime answers
* `'unknown'`. Recorded on every `time-reading` row: zone data changes
* between releases, so a reading is re-derivable only with it.
*/
export declare function tzdataVersion(): string;
/**
* The ONE zone a place name names in this runtime's tz database, or
* `undefined` when it names none or more than one. `London` → `Europe/London`,
* `New York` → `America/New_York`; `India`, `Pacific`, `Eastern` → `undefined`
* (no zone ends in them — a region is not a place). Looked up two ways, both
* the tz database's own data: the zones whose last segment is the place, and
* `Area/Place` under each geographic area (which admits a renamed city's link,
* `Kyiv`). Every hit is compared by its canonical name, so a link and its
* target are one zone. Memoised; the runtime's database does not change.
*
* @example
* ```ts
* zoneOfPlace('London'); // 'Europe/London'
* zoneOfPlace('New York'); // 'America/New_York'
* zoneOfPlace('India'); // undefined — asked, never guessed
* ```
*/
export declare function zoneOfPlace(place: string): ZoneName | undefined;
/**
* The IANA zone that is a fixed offset, for a whole-hour offset the tz
* database names (`Etc/GMT+8` is −08:00 — POSIX signs, inverted), else
* `undefined`. How a window read at a literal offset, or typed with one no
* other zone shows, names its zone without claiming a place's.
*
* @example
* ```ts
* fixedOffsetZone(-480); // 'Etc/GMT+8'
* fixedOffsetZone(0); // 'UTC'
* fixedOffsetZone(330); // undefined — no Etc zone for +05:30
* ```
*/
export declare function fixedOffsetZone(minutes: number): ZoneName | undefined;