/** * @fileoverview Shared date-range handling for the GDELT tools that accept an explicit * startDatetime/endDatetime window — the YYYYMMDDHHMMSS field pattern, the both-or-neither * pairing predicate, record timestamps against a window, and the window partitioning and * resume boundary the record-list tools hand back as a continuation contract. * @module mcp-server/tools/date-range */ import { z } from '@cyanheads/mcp-ts-core'; /** * GDELT's datetime wire format: exactly 14 digits, YYYYMMDDHHMMSS, no separators. * * Applied as a field-level Zod `.regex()` so it serializes into the advertised JSON * Schema as `pattern`, letting a caller see the constraint before it calls. */ export declare const GDELT_DATETIME_PATTERN: RegExp; /** * Relative DOC timespan validator. It rejects only syntax the server can parse reliably; * unknown/upstream-evolved syntax remains available to GDELT and its response classifier. */ export declare const gdeltDocTimespanSchema: z.ZodString; /** A GDELT query window, both boundaries in the 14-digit YYYYMMDDHHMMSS wire format. */ export type GdeltWindow = { startDatetime: string; endDatetime: string; }; /** Format a Date as GDELT's 14-digit YYYYMMDDHHMMSS wire format (UTC). */ export declare function toGdeltDatetime(date: Date): string; /** * The window a call actually ran against, in GDELT's wire format. * * Mirrors `applyTimeRange()`'s precedence: an explicit boundary pair wins, a timespan * is resolved against now, and a call that pinned neither gets `undefined` rather than * a guessed window — GDELT's own default is not something this server can observe. */ export declare function resolveEffectiveWindow(args: { timespan?: string | undefined; startDatetime?: string | undefined; endDatetime?: string | undefined; }): GdeltWindow | undefined; /** * The ` Timespan "…" resolved to – .` sentence an empty-result notice carries when * a call ran on a relative timespan, so the caller sees which dates it actually covered; `''` * when an explicit window was pinned or the timespan does not parse. */ export declare function describeResolvedTimespan(args: { timespan?: string | undefined; startDatetime?: string | undefined; endDatetime?: string | undefined; }): string; /** * Split a window into two halves whose union covers it exactly, or `undefined` when it * is already too narrow to divide. * * The second half deliberately **overlaps** the first by one second rather than resuming * at the shared midpoint. GDELT documents both boundaries as exclusive — STARTDATETIME * considers "only articles published *after* this date/time stamp" and ENDDATETIME "only * articles published *before*" it (DOC 2.0 and TV 2.0 API docs alike) — so halves that * merely touch at the midpoint would drop every record timestamped exactly there, silently. * * Reaching back one second closes that seam: under the documented exclusive reading the * two halves tile the original window with no gap and no repeat, and if the boundaries * turn out to behave inclusively instead, the overlap costs at most two seconds of * duplicates — which a caller can see and de-duplicate. Gap-free either way. */ export declare function splitWindow(window: GdeltWindow): [GdeltWindow, GdeltWindow] | undefined; /** The next-call windows and the prose explaining them, for a record cap at its ceiling. */ export type WindowContinuation = { windows?: [GdeltWindow, GdeltWindow]; guidance: string; }; /** * How a caller retrieves records left behind once `maxRecords` is already at its ceiling, or * once a page under a sort with no resume point is cut to the response byte budget. * * GDELT exposes no offset or cursor, so narrowing the time window is the only lever, and * each outcome is stated rather than implied: halves to re-query when the window divides, * how to pin a window when the call never set one, and — when the window is already too * narrow to divide — that the remaining records are simply unreachable. * * Callers own the record-noun prose; this covers only the window reasoning both tools share. */ export declare function planWindowContinuation(window: GdeltWindow | undefined): WindowContinuation; /** * The window to send GDELT TV for a caller's window: the start floored to the clock hour and * the end kept, or stretched to the end of that first hour when the window is shorter. GDELT * answers the same whole hours either way — this never adds an hour the caller's window does * not reach — but the widened span is one GDELT accepts, so a window of any width works. The * caller's exact window is what the out-of-window drop then keeps. * * An end exactly on the clock hour is sent one second earlier: sent as is, it would have GDELT * answer that whole next hour for the window's one final second, spending maxRecords on clips * the drop then discards. That final second is left out — as GDELT documents ENDDATETIME, the * end is exclusive there. */ export declare function tvRequestWindow(window: GdeltWindow): GdeltWindow; /** Whether GDELT capped the page that needs a continuation, and against which limit. */ export type TvCap = 'none' | 'below-ceiling' | 'at-ceiling'; /** * The TV counterpart of {@link planWindowContinuation}. GDELT TV answers whole clock hours and * the handler trims its answer to the requested window, so halves need no overlapping second: * they share no second and cover the window exactly. * * The split falls on the clock hour nearest the middle whenever one lies inside the window, * which gives the two halves disjoint hour sets — the split that helps when the cap left * records behind, since any window inside one hour fetches that same capped hour. Inside a * single hour a page cut to the byte budget still splits at the second: its withheld clips were * fetched and each half fetches the hour again and keeps its own seconds. When that page was * also capped, only clips past the cap stay out of reach — a higher maxRecords reaches them * below the 3000 ceiling, nothing does at it. An uncut page capped at the ceiling with no hour * inside has nothing a narrower window can add. */ export declare function planTvWindowContinuation(window: GdeltWindow | undefined, { cut, cap }: { cut: boolean; cap: TvCap; }): WindowContinuation; /** Sort orders whose last emitted record is a point a follow-up window can resume from. */ export type DateSort = 'dateDesc' | 'dateAsc'; /** * The boundary that resumes a date-sorted run after the last record a response emitted: under * `dateDesc` an `endDatetime` one second past that record, under `dateAsc` a `startDatetime` * one second before it. The other boundary stays whatever the caller's window had. * * Reaching one second past the record keeps it inside the resumed window under GDELT's * documented exclusive boundaries, so records that share its second — and were cut — are * returned again rather than skipped. Records from that second the response already emitted * come back too, so callers de-duplicate on re-assembly. */ export declare function resumeBoundary(sort: DateSort, lastEmittedMs: number): Pick | Pick; /** * The boundary that skips past the last emitted record's second: under `dateDesc` an * `endDatetime` one second before it, under `dateAsc` a `startDatetime` one second after it. * Offered when resuming at that second cannot make progress; records at the skipped second * that the response did not emit are left behind. */ export declare function skipPastBoundary(sort: DateSort, lastEmittedMs: number): Pick | Pick; /** * Epoch milliseconds of a record timestamp — DOC's `seendate` (`20240115T120000Z`) or TV's ISO * 8601 clip date (`2024-01-15T12:00:00Z`) — or `undefined` when it is neither. */ export declare function parseRecordTimestamp(value: string): number | undefined; /** * True when `ms` falls inside `window`, boundaries included. Inclusive on purpose: GDELT * documents exclusive boundaries, but a record on a boundary second is one the caller's * window names, and dropping it would open a gap if the boundaries behave inclusively. */ export declare function isWithinWindow(window: GdeltWindow, ms: number): boolean; /** * True when exactly one of the two boundaries is present. * * GDELT honors an explicit date range only when both boundaries are set, so a lone * boundary is dropped during URL construction and the query silently runs against a * different window than the caller asked for. * * The rule is cross-field, so each tool handler enforces it rather than a Zod * object-level refinement: a schema-level rejection is raised before the handler runs, * which returns a raw Zod issue dump with no `structuredContent` — dropping the * `reason` + `recovery.hint` contract every other error path on these tools carries. */ export declare function isUnpairedDateRange(startDatetime?: string, endDatetime?: string): boolean; /** * Why an explicit window cannot be used, or `undefined` when it can. * * One reason covers every way a date argument fails, so each handler stays a single guard and * a caller has one `invalid_date_range` branch rather than three. The field regex upstream has * already established 14 digits; what it cannot see is whether those digits name a real * instant, or which boundary comes first — `applyTimeRange` sets both parameters verbatim, so * an impossible or reversed window reaches GDELT, which normalizes it and answers successfully * for dates nobody asked about. * * Both boundaries are fixed-width zero-padded digits by the time ordering is compared, so * lexical order is calendar order. */ export declare function describeDateRangeFault(startDatetime?: string, endDatetime?: string): string | undefined; //# sourceMappingURL=date-range.d.ts.map