/** * earth-now.co * Author: Simon-Pierre Boucher * Contact: contact@spboucher.ai * File: packages/registry/src/schema.ts * Purpose: Zod schemas for the metric registry YAML and the data fixtures — a metric does not exist until it validates here */ import { z } from "zod"; export const sourceSchema = z.object({ id: z.string().min(1), name: z.string().min(1), url: z.string().url(), license: z.string().min(1), /** Set when the license needs a human re-check before public launch. */ licenseNote: z.string().optional(), cadence: z.string().min(1), variants: z.array(z.string()).optional(), }); export const displaySchema = z.object({ decimals: z.number().int().min(0).max(6), sigFigs: z.number().int().min(1).max(15).optional(), /** Display conversion factor (t → Gt = 1e-9). NEVER applied in the pipeline. */ scale: z.number().positive().optional(), unit: z.object({ fr: z.string(), en: z.string() }), }); export const derivedSchema = z.object({ op: z.enum(["linear-combination", "window", "rate-of", "depletion-countdown"]), inputs: z.array(z.object({ id: z.string(), weight: z.number().default(1) })).min(1), constant: z.number().optional(), /** For op=window: which window is this metric's primary reading. */ window: z.enum(["today", "ytd", "session"]).optional(), }); export const metricSchema = z.object({ id: z.string().regex(/^[a-z0-9_]+$/), name: z.object({ fr: z.string().min(1), en: z.string().min(1) }), domain: z.enum([ "population", "climate", "emissions", "forest", "ocean", "energy", "society", "health", "economy", "tech", "realtime", "space", ]), priority: z.enum(["mvp", "v1", "v2"]), kind: z.enum(["stock", "cumulative", "event", "derived"]), /** Model level from the catalog: 0/1/2 statistical, "rt" true event-driven, "derived". */ level: z.union([z.literal(0), z.literal(1), z.literal(2), z.literal("rt"), z.literal("derived")]), /** Model family (must match a builder in packages/models), or "derived"/"rt". */ model: z.string().min(1), /** Data-layer unit (SI or domain standard); display conversions live in display. */ unit: z.string().min(1), sources: z.array(sourceSchema).min(1), refresh: z.string().min(1), display: displaySchema, constraints: z .object({ maxAbsRatePerSec: z.number().positive().optional(), maxJumpOnRefit: z.number().positive().optional(), }) .default({}), windows: z.array(z.enum(["total", "today", "ytd", "session"])).default(["total"]), derived: derivedSchema.optional(), /** Relative 90 % CI to surface in the UI ("estimation" label mandatory when set). */ uncertaintyFraction: z.number().positive().max(1).optional(), editorialNote: z.object({ fr: z.string(), en: z.string() }).optional(), }); export const metricsFileSchema = z.object({ metrics: z.array(metricSchema).min(1) }); export type MetricEntry = z.infer; export type SourceEntry = z.infer; const timeValueSchema = z.object({ time: z.string(), value: z.number() }); export const fixtureSchema = z.discriminatedUnion("family", [ z.object({ family: z.literal("seasonal-spline-v2"), observations: z.array(timeValueSchema).min(1), forecasts: z.array(timeValueSchema).min(1), uncertainty: z.object({ low: z.number(), high: z.number() }).optional(), observedAt: z.string(), }), z.object({ family: z.literal("keeling-fusion-v1"), observations: z.array(timeValueSchema).min(6), observedAt: z.string(), }), z.object({ family: z.literal("seasonal-ytd-v1"), year: z.number().int(), annualTotal: z.number().nonnegative(), shape: z.array( z.object({ period: z.enum(["year", "week", "day"]), order: z.number().int().min(1), relativeAmplitude: z.number(), phase: z.number(), }), ), observedAt: z.string(), }), z.object({ family: z.literal("linear-ytd-v1"), year: z.number().int(), annualTotal: z.number().nonnegative(), observedAt: z.string(), }), z.object({ family: z.literal("linear-stock-v1"), at: z.string(), value: z.number(), perSecond: z.number(), observedAt: z.string(), }), z.object({ family: z.literal("static-rt-v1"), at: z.string(), value: z.number(), observedAt: z.string(), }), ]); export type MetricFixture = z.infer; export const fixturesFileSchema = z.record(z.string(), fixtureSchema);