@exodus/datapoints
v3.3.0
Published
Provenance-backed datapoint contract: schemas, formula evaluation, registry validation, and data report rendering
Maintainers
Keywords
Readme
@exodus/datapoints
The contract for provenance-backed datapoints: typed definitions, deterministic
formula evaluation, fail-closed registry validation, value formatting,
render-by-reference data reports, and the payload schemas consumers validate
tool responses against. Zero I/O — data sources live behind the
DatapointSource interface, not in this package.
A datapoint is a typed value with an auditable origin chain: which source and
query produced it, when it was retrieved, and for derived values the formula
and each input's chain. A data report is agent-authored text rendered by
reference: the model writes {{dp:datapoint_id}} placeholders, the renderer
substitutes evaluated values, and bare numbers are rejected — the model never
types the final figures.
Install
npm install @exodus/datapointsValidating a report payload
import { reportPayloadSchema } from '@exodus/datapoints';
const parsed = reportPayloadSchema.safeParse(structuredContent.report);
if (parsed.success) {
for (const datapoint of parsed.data.datapoints) {
console.log(datapoint.id, datapoint.formatted, datapoint.tier, datapoint.provenance);
}
}evaluatedDatapointSchema validates single values, and evaluateResultsSchema
validates an evaluate response. Every value carries value, formatted,
unit, tier (human-reviewed for curated values, human-approved for an
AI-written custom definition a human approved in a report review, or
ai-reviewed; a formula takes the most cautious tier of its inputs),
retrievedAt, and
provenance; lookups may carry query ({ language, text }, where language
is sql, promql, logql or opal; a stored plain string parses as SQL), description, and freshness, and derived values
carry formulaProvenance. A lookup's provenance entry carries asOf
(YYYY-MM-DD, the last day the value covers) when the source reports one, and
args when the lookup was read with arguments: each enum argument as its value
and each time range as the resolved { from, to } window.
Rendering placeholders
import { collectPlaceholderIds, renderDataReport } from '@exodus/datapoints';
const text = 'Pay reached {{dp:pay_mtu_30d}} users, up {{dp:pay_mtu_growth_30d}}.';
const ids = collectPlaceholderIds(text);
const report = renderDataReport(text, evaluatedValues);renderDataReport fails closed: a submission containing any digit outside a
placeholder is rejected in full with the offending digits listed. Use
{{lit:3}} for numbers that are ordinary prose rather than data claims.
Series and charts
A lookup with valueType: 'series' holds an ordered list of { label, value }
points (at most MAX_SERIES_POINTS); formatValue summarizes it as
"12 points · Jan 2026 – Dec 2026". Series cannot feed a formula.
A report puts series on a chart with a fenced block the renderer draws:
```chart
kind: line
title: Monthly MTU
series:
- {{dp:custom_mtu_monthly_1a2b3c4d}}
```Kinds and options:
| Key | Values | Applies to |
| ------------- | ----------------------------- | ----------------------------------------------------------------------------------------------------- |
| kind | line, bar, area, stat | required. stat renders one KPI tile per entry; a series entry shows its last value with a sparkline |
| title | text | any |
| series | list of {{dp:<id>}} | required; series datapoints for plotted kinds, scalars or series for stat |
| stacked | true / false | bar, area; only count or USD series |
| orientation | vertical, horizontal | bar |
| layout | overlay, multiples | plotted kinds; multiples draws one panel per series and allows mixed units |
| target | {{dp:<scalar id>}} | plotted kinds without multiples; a reference line sharing the series unit |
renderDataReport parses every block with parseCharts (unclosed fences,
unknown keys, kinds or values, duplicate keys or ids, and option conflicts are
chart_invalid) and validates it with validateChart: plotted entries are
series datapoints with the same number of points and, unless layout:
multiples, one shared unit. Renderers reuse the same parser via
matchChartBlocks so the gateway and the UI agree on the grammar.
rendered is plain text: every placeholder, including those inside a chart
block, is replaced by its formatted value. A renderer that draws charts needs
the placeholder source, which the payload carries as the optional text
field; persist the whole payload so a dashboard can redraw it. It also carries
the optional title and summary sources, with placeholders, in the same form.
Surface
| Export | Purpose |
| ------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- |
| datapointDefinitionSchema | Validates a lookup, derived, or constant definition; optional policies names the consumer's access policies |
| argumentSpecSchema / argumentSpecsSchema | The args a live lookup declares: enum values or a time_range with modes and maxRange |
| rawArgumentsSchema / resolvedArgumentsSchema / timeWindowSchema | Argument values as a caller passes them and as they resolve |
| resolveArguments | Checks raw arguments against a lookup's specs and resolves rolling ranges against now |
| takesArguments | Whether a definition is a lookup that declares at least one argument; narrows it to ArgumentLookup |
| ARGUMENT_NAME_PATTERN / MAX_ARGUMENT_NAMES / MAX_ARGUMENT_VALUE_LENGTH / TIME_RANGE_MODES | Limits on argument names, their count, values, and time range modes |
| MAX_LIVE_FRESHNESS | The longest freshness a live lookup may declare |
| evaluatedDatapointSchema / reportPayloadSchema / evaluateResultsSchema | Payload schemas for tool responses |
| buildRegistry | Validates a definition set against the available source ids, and optionally the live ones, and fails closed |
| resolutionOrder | Dependency order for evaluating a derived datapoint |
| validateFormula / evaluateFormula | Restricted arithmetic over declared inputs |
| evaluateLookup / evaluateConstant / evaluateDerivedDatapoint | Assemble EvaluatedDatapoint values with provenance |
| formatValue | Deterministic en-US formatting for USD, percent, count |
| collectPlaceholderIds / renderDataReport | Render-by-reference data reports |
| parseCharts / validateChart / matchChartBlocks | Chart block grammar shared by the gateway and renderers |
| isSeriesValue / valueTypeOf | Narrow a DatapointValue to its ValueType |
| displayDatapointId / displayFormula | Custom ids without their trailing hash, for display only |
| DatapointSource | Interface every source implementation satisfies; live: true marks a source that reads an operational system |
| LookupOptions | notBefore: a caching source must not answer from older data; args: the resolved arguments a live source reads with |
Source caches and fresh reads
lookup(queryId, options) takes an optional LookupOptions. A source that keeps its own cache
must not answer from data it started retrieving before options.notBefore; lookups that pass the
same notBefore may share one fresh read. A caller honoring a fresh request passes the time the
evaluation started. Without options the source applies its own cache policy. A live source reads
with options.args, the resolved arguments; sources that are not live ignore it.
A source that reads an operational system declares live: true. Pass those ids to
buildRegistry(definitions, { sourceIds, liveSourceIds }) and the registry keeps the two in
step: a lookup that is live or declares args must target a live source
(live_on_non_live_source), and a lookup on a live source must be live
(non_live_on_live_source). Passing a plain set of source ids skips both checks.
Formula evaluation
Formulas are arithmetic over declared inputs — rev_2026_q1 / rev_2025_q4 - 1.
An id may carry one entity dot (monavate_onchain.gross_spend_usd_30d), and
formulas use such ids directly; a number still starts with a digit, so a.b is
never read as a decimal.
They are evaluated by a restricted mathjs build and constrained by a character
allowlist, so assignment, function definition, indexing, comparison,
exponentiation, string literals, and units are rejected at registry load.
