@nightowlsdev/metrics
Adapter/StorageA dimensionless, org-scoped metric-series store, `(orgId, orgScope, metric, at, dims) → value` with calendar bucketing and a fully-pinned period-over-period `compare()`, the honest home for numbers that change over time.
What it does
The metric-series store (FR-059): a small, library-shaped observation plane for numbers that move over time, exactly the shape the knowledge and graph planes silently corrupt. A point's identity is `(orgId, orgScope, metric, at, dims)`; `record()` overwrites rather than duplicating and `recordBatch()` is one all-or-nothing transaction. `compare()` is the pinned period-over-period question, half-open windows, a `dataLag`-derived cutoff that truncates only the current window (never shifts the previous one), calendar-aligned buckets flagged `partial` at every edge, totals computed over RAW observations not buckets-of-buckets, and a signed `pct` with an absolute-value denominator. Float reduction is ordered and identical in both stores (Postgres never runs a native `float8` sum, because doubles are not associative and PostgreSQL raises on an overflow JS turns into `Infinity`). Library-shaped like `@nightowlsdev/auth-sessions`, you own the `pg` Pool (a peer dependency), the package owns its `metrics_000N` migration series and never runs DDL, and the engine never reads or writes these rows, the consumers are agents and hosts. Security posture is server-only: RLS on with no policy and an explicit `REVOKE`, so the Pool must own the table or hold BYPASSRLS, and a browser gets these numbers through your server, aggregated, never as a raw row feed. `orgScope` on reads is an IDENTITY SELECTOR, not FR-055 visibility, `undefined` selects the org-wide series only (there is no unrestricted all-scopes read), because an aggregation that silently mixed departments into one number is the exact corruption a metrics store exists to prevent. NOT for this plane: engine-emitted metrics, dashboards, alerting, downsampling, cross-scope aggregate reads.
Install
pnpm add @nightowlsdev/metricsKey exports
- createMetricStore (pg-backed) / createInMemoryMetricStore (a real store, not a stub)
- MetricStore: record / recordBatch / query / compare / deleteBefore
- METRICS_MIGRATIONS / METRIC_POINTS_MIGRATION_SQL
- canonicalMetricDims / metricDimsHash
- nightOwlsPlugin
- types: MetricPoint, CompareQuery, CompareResult, TimeWindow, StepSpec, Aggregate
Usage
import { createMetricStore } from "@nightowlsdev/metrics";
import { Pool } from "pg";
// You own the Pool (a peer dep). It must OWN the table or hold BYPASSRLS — the table is RLS-on with no policy,
// so an unprivileged role reads zero rows and writes nothing, which looks exactly like an empty store.
const store = createMetricStore({ pool: new Pool({ connectionString: process.env.DATABASE_URL }) });
// record() OVERWRITES rather than duplicating. `at` is an RFC-3339 instant with a REQUIRED offset (ms precision at most).
await store.record({ orgId, metric: "seo.impressions", at: "2026-03-02T00:00:00Z", value: 1420, dims: { page: "/pricing" } });
// The pinned period-over-period question. Windows are half-open [from, to); totals come from RAW observations,
// never buckets-of-buckets; `direction` reads off totals.abs.
const { totals, direction } = await store.compare({
orgId,
metric: "seo.impressions",
current: { from: "2026-03-01T00:00:00Z", to: "2026-04-01T00:00:00Z" },
previous: { from: "2026-02-01T00:00:00Z", to: "2026-03-01T00:00:00Z" },
step: "day",
aggregate: "sum",
dataLag: "PT48H", // Search Console does not report today — truncates the CURRENT window only, never shifts previous.
});
// createInMemoryMetricStore() is a real store (same validator/identity/reducer), not a stub — good for tests.