spb/earth-now Public License
earth-now.co — real-time planetary dashboard: live world metrics modeled, not streamed.
TypeScript 93%
Shell 2.3%
SQL 1.4%
JavaScript 1.3%
Dockerfile 1.2%
CSS 0.8%
1/**2 * earth-now.co3 * Author: Simon-Pierre Boucher4 * Contact: contact@spboucher.ai5 * File: packages/registry/src/schema.ts6 * Purpose: Zod schemas for the metric registry YAML and the data fixtures — a metric does not exist until it validates here7 */89import { z } from "zod";1011export const sourceSchema = z.object({12 id: z.string().min(1),13 name: z.string().min(1),14 url: z.string().url(),15 license: z.string().min(1),16 /** Set when the license needs a human re-check before public launch. */17 licenseNote: z.string().optional(),18 cadence: z.string().min(1),19 variants: z.array(z.string()).optional(),20});2122export const displaySchema = z.object({23 decimals: z.number().int().min(0).max(6),24 sigFigs: z.number().int().min(1).max(15).optional(),25 /** Display conversion factor (t → Gt = 1e-9). NEVER applied in the pipeline. */26 scale: z.number().positive().optional(),27 unit: z.object({ fr: z.string(), en: z.string() }),28});2930export const derivedSchema = z.object({31 op: z.enum(["linear-combination", "window", "rate-of", "depletion-countdown"]),32 inputs: z.array(z.object({ id: z.string(), weight: z.number().default(1) })).min(1),33 constant: z.number().optional(),34 /** For op=window: which window is this metric's primary reading. */35 window: z.enum(["today", "ytd", "session"]).optional(),36});3738export const metricSchema = z.object({39 id: z.string().regex(/^[a-z0-9_]+$/),40 name: z.object({ fr: z.string().min(1), en: z.string().min(1) }),41 domain: z.enum([42 "population",43 "climate",44 "emissions",45 "forest",46 "ocean",47 "energy",48 "society",49 "health",50 "economy",51 "tech",52 "realtime",53 "space",54 ]),55 priority: z.enum(["mvp", "v1", "v2"]),56 kind: z.enum(["stock", "cumulative", "event", "derived"]),57 /** Model level from the catalog: 0/1/2 statistical, "rt" true event-driven, "derived". */58 level: z.union([z.literal(0), z.literal(1), z.literal(2), z.literal("rt"), z.literal("derived")]),59 /** Model family (must match a builder in packages/models), or "derived"/"rt". */60 model: z.string().min(1),61 /** Data-layer unit (SI or domain standard); display conversions live in display. */62 unit: z.string().min(1),63 sources: z.array(sourceSchema).min(1),64 refresh: z.string().min(1),65 display: displaySchema,66 constraints: z67 .object({68 maxAbsRatePerSec: z.number().positive().optional(),69 maxJumpOnRefit: z.number().positive().optional(),70 })71 .default({}),72 windows: z.array(z.enum(["total", "today", "ytd", "session"])).default(["total"]),73 derived: derivedSchema.optional(),74 /** Relative 90 % CI to surface in the UI ("estimation" label mandatory when set). */75 uncertaintyFraction: z.number().positive().max(1).optional(),76 editorialNote: z.object({ fr: z.string(), en: z.string() }).optional(),77});7879export const metricsFileSchema = z.object({ metrics: z.array(metricSchema).min(1) });8081export type MetricEntry = z.infer<typeof metricSchema>;82export type SourceEntry = z.infer<typeof sourceSchema>;8384const timeValueSchema = z.object({ time: z.string(), value: z.number() });8586export const fixtureSchema = z.discriminatedUnion("family", [87 z.object({88 family: z.literal("seasonal-spline-v2"),89 observations: z.array(timeValueSchema).min(1),90 forecasts: z.array(timeValueSchema).min(1),91 uncertainty: z.object({ low: z.number(), high: z.number() }).optional(),92 observedAt: z.string(),93 }),94 z.object({95 family: z.literal("keeling-fusion-v1"),96 observations: z.array(timeValueSchema).min(6),97 observedAt: z.string(),98 }),99 z.object({100 family: z.literal("seasonal-ytd-v1"),101 year: z.number().int(),102 annualTotal: z.number().nonnegative(),103 shape: z.array(104 z.object({105 period: z.enum(["year", "week", "day"]),106 order: z.number().int().min(1),107 relativeAmplitude: z.number(),108 phase: z.number(),109 }),110 ),111 observedAt: z.string(),112 }),113 z.object({114 family: z.literal("linear-ytd-v1"),115 year: z.number().int(),116 annualTotal: z.number().nonnegative(),117 observedAt: z.string(),118 }),119 z.object({120 family: z.literal("linear-stock-v1"),121 at: z.string(),122 value: z.number(),123 perSecond: z.number(),124 observedAt: z.string(),125 }),126 z.object({127 family: z.literal("static-rt-v1"),128 at: z.string(),129 value: z.number(),130 observedAt: z.string(),131 }),132]);133134export type MetricFixture = z.infer<typeof fixtureSchema>;135136export const fixturesFileSchema = z.record(z.string(), fixtureSchema);137