Skip to content

Contract validation

The @azohra/meteo.briefing/contract export is the executable authority for published JSON. Its zod schemas, inferred TypeScript types, nullable parse guards, and generated JSON Schemas describe the same eight document families.

A profile document, block by block Excerpts of a real teaching profile: the schemaVersion and model identity, the run block, the sample-provenance site block with its timezone echo, the semantics tag, and the peak-W* hour's surface, first level, and derived blocks, quoted verbatim from the committed document.

DocumentParserPublished location
ProfileparseSiteForecastJson<model>/sites/<site>.json and each history line
ManifestparseForecastManifestJson<model>/manifest.json
ModelsparseModelCatalogueJsonmodels.json
SitesparseSitesCatalogueJsonsites.json
Site contextparseSiteContextJsonsite-context.json
Run indexparseRunsIndexJsonruns.json
SmokeparseSmokeDocumentJson<model>/sites/<site>.json for smoke models
ObservationparseObservationDocumentJson<model>/sites/<site>.json for observation datasets

Each parser also has an already-parsed counterpart without the Json suffix. All return the typed document or null; rejected input is not patched into shape.

validate-profile.ts
import {
parseSiteForecastJson,
type SiteForecast,
} from "@azohra/meteo.briefing/contract";
export function requireProfile(text: string): SiteForecast {
const profile = parseSiteForecastJson(text);
if (!profile) throw new Error("unsupported or invalid profile");
return profile;
}

The optional site.timeZone echo and its semantics — absence means the document predates the echo, never “the launch uses UTC” — are defined in the profile reference. For an older document without the echo, consumers either supply a known fallback or use an API’s documented fallback behaviour:

APITimezone behaviour
analyzeForecastOverride, then profile.site.timeZone, then UTC with a timesAreUtc caveat
projectForecast({ day })Override, then profile.site.timeZone; throws if neither exists
buildMeteogramSceneRequires an explicit timeZone option
groupByLocalDay / meteogramDisplayHoursRequire the caller’s explicit timezone

Every numeric position under surface, levels, and derived is either a number or an ensemble percentile object. Switch on shape, not a model slug:

read-scalar.ts
import { isEnsembleDropout, isEnsembleValue, type SiteForecast } from "@azohra/meteo.briefing/contract";
export function firstWindSpeed(profile: SiteForecast): number | null | undefined {
const speed = profile.hours[0]?.surface.windSpeedMps;
return speed === undefined
? undefined
: isEnsembleValue(speed)
? isEnsembleDropout(speed) ? null : speed.p50
: speed;
}

Full ensemble dropout is members: 0 with every percentile null. isEnsembleDropout distinguishes it from a populated percentile block.

For a consumer that supports only deterministic documents, isDeterministicProfile(profile) narrows every scalar position to number after one checked guard. Run it once per document.

Every document computes in the platform’s shared physical vocabulary — Units, angles, one wind sign defines the wind sign convention and conversion helpers each package uses. These are the unit boundaries most likely to cause integration errors:

Quantity familyContract conventionCommon mistake
Sea-level pressure (surface.seaLevelPressureHpa)hPa — one unit for the whole pressure class, shared with station documentsReading a v1 document’s pressurePa (whole pascals) as hPa without migrating
Pressure levelshPaMultiplying named isobaric levels by 100 in labels
Heights and elevationsmetres; profile altitude values are MSL unless explicitly AGLPlotting model PBL depth directly on an MSL axis
Model PBL heightmetres AGLComparing it to derived.boundaryLayerTopM without adding site.modelElevationM
Temperature and dew point°CTreating dew-point depression as published dew point
Wind speed and gustm/sDisplaying as km/h without a named conversion
Wind directionmeteorological FROM, 0–359°Using mathematical TO-direction
Vertical velocityomega, Pa/s; negative is liftReading negative as sinking geometric velocity
Precipitationmm/h with declared provider window semanticsComparing instantaneous and window-mean rates as identical measurements
Cloud and cloud layerspercentReplacing unavailable fields with 0%
CAPE/CINJ/kgTreating absent CIN as zero inhibition
Smoke concentrationsµg/m³ at the surface, mg/m² for columns; optical thickness dimensionlessAssuming provider units — RAQDPS GRIBs carry kg/m³ and kg/m² with no units metadata (verified in the smoke reference); builders convert at fetch
Measured shortwave (observations)W/m², instantaneous at the surfaceTreating an absent instant as zero output, or provider DQF 0 as validity — night pixels are fill with DQF 0

The contract JSDoc and generated schemas define each field. The profile guide maps document blocks; model capabilities explains declared absence and semantics.

A worked height boundary: to place surface.pblHeightM beside MSL series, add profile.site.modelElevationM. Do not add a launch elevation — the model’s PBL depth is referenced to model terrain. For pure unit conversions, use package exports such as msToKmh from @azohra/meteo.briefing/derive; the deprecated scene-subpath re-export was retained through the Windgram-era v0.3 line and removed in v0.4.0, before the @azohra packages existed.

Every block inside the eight document families is itself an exported zod schema with an inferred type, so a consumer can validate or type one fragment — a single hour, a capability declaration, a manifest stats block — without handling a whole document. Each pair feeds exactly one parse entry point.

The document roots are siteForecastSchema/SiteForecast, forecastManifestSchema/ForecastManifest, modelCatalogueSchema/ModelCatalogue, sitesCatalogueSchema/SitesCatalogue, siteContextSchema/SiteContext, runsIndexSchema/RunsIndex, smokeDocumentSchema/SmokeDocument, and observationDocumentSchema/ObservationDocument. Manifest, models, runs, smoke, and observation pin SCHEMA_VERSION (1); the profile pins its own exported SITE_FORECAST_SCHEMA_VERSION (2 — wire v2), and sites and site context pin SITES_SCHEMA_VERSION and SITE_CONTEXT_SCHEMA_VERSION (both 2). Their pieces:

Schema (type)One-line roleFeeds
scalarSchema (Scalar)Any numeric position: a number or an ensemble percentile objectevery value field below
ensembleValueSchema (EnsembleValue)The percentile-object arm of Scalar, including full dropoutevery value field below
forecastHourSchema (ForecastHour)One forecast hour: validAt plus the surface, levels, derived, and optional smoke blocks belowparseSiteForecast(Json)
forecastSurfaceSchema (ForecastSurface)An hour’s surface block, optional capability fields absent-not-zeroparseSiteForecast(Json)
forecastLevelSchema (ForecastLevel)One pressure-level entry in an hour’s ascending levels arrayparseSiteForecast(Json)
forecastDerivedSchema (ForecastDerived)The forecast engine’s derived block of an hourparseSiteForecast(Json)
forecastSiteSchema (ForecastSite)Sample provenance: identity, coordinates, the model’s own terrain (modelElevationM), and the optional timezone echo — no launch elevationparseSiteForecast(Json)
forecastRunSchema (ForecastRun)Publication identity: referenceTime, generatedAt, optional membersparseSiteForecast(Json)
forecastSemanticsSchema (ForecastSemantics)The optional gust/precipitation meaning tag stored with a documentparseSiteForecast(Json)
forecastManifestSiteSchema (ForecastManifestSite)One published site name/slug pair in a manifestparseForecastManifest(Json)
forecastManifestStatsSchema (ForecastManifestStats)The stable accounting core plus open numeric extension keysparseForecastManifest(Json)
modelEntrySchema (ModelEntry)One model catalogue entry: slug, cadence and typical publication lag, levels, lifecycleparseModelCatalogue(Json)
modelCapabilitiesSchema (ModelCapabilities)A model’s declared capability set, absences includedparseModelCatalogue(Json)
siteCatalogueEntrySchema (SiteCatalogueEntry)One catalogued site — identity only since sites schemaVersion 2 (slug, name, coordinates, required IANA timeZone; nothing physical)parseSitesCatalogue(Json)
siteContextSourceSchema (SiteContextSource)One upstream terrain/land-cover source, with the licence attribution that travels with its valuesparseSiteContext(Json)
siteContextEntrySchema (SiteContextEntry)One site’s measured ground truth — the launch elevation pick, terrain, and land cover — joined to sites.json by slugparseSiteContext(Json)
runsIndexEntrySchema (RunsIndexEntry)One model’s current (referenceTime, generatedAt) pairparseRunsIndex(Json)

DeterministicSiteForecast is the narrowed profile type isDeterministicProfile returns.

  • Check schemaVersion; do not infer compatibility from filenames.
  • Model identity is an open slug discovered from the catalogue, not a package enum.
  • An absent optional field means not published there, never zero.
  • A stored profile’s own semantics keeps gust and precipitation meaning with the document; absence of that Windgram-era v0.3 tag does not imply a default.
  • A stored profile’s optional site.timeZone keeps local-time interpretation with the document; absence of that Windgram-era v0.4 echo does not imply UTC.
  • The zod contract is behavioural authority. For other languages, the generated JSON Schema files ship in the @azohra/meteo.briefing tarball’s schema/ directory and in the repository at forecast/schema/; the package exports them as @azohra/meteo.briefing/schema/*.json (profile.schema.json and its siblings), so a resolver can reach them by specifier as well as by path.

The v1 wire was frozen — field names, schema $ids, units, declared semantics, and every URL under the published dataset root — with one documented escape: wording and vocabulary change only at a schemaVersion event. Wire v2 is that event, fired. Site-forecast and history documents now carry the v2 quantity vocabulary (the Mps suffix grammar, seaLevelPressureHpa), the JSON Schema titles and descriptions track the current contract rather than any frozen wording, and stored v1 documents reach v2 through the forecast engine’s wire migrator (meteo forecast migrate).

See Data and package versioning for the independent dataset and npm version axes.