Skip to content

Contract validation

@azohra/meteo.briefing/contract defines what published JSON may contain, in code. Its zod schemas, inferred TypeScript types, parse guards, and generated JSON Schemas all describe the same nine document families.

Every guard normalizes up. It parses each version its family has ever published and returns the newest shape. It refuses only versions newer than the package. Archived documents stay readable forever, and the only version that reads as invalid is one published by a newer writer. Compatibility sets out the rollout rule.

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
Manifest (forecast and smoke)parseForecastManifestJson<model>/manifest.json
Manifest (observation)parseObservationManifestJson<model>/manifest.json for observation datasets; the same path carries a different manifest shape (firstObservedAt, lastObservedAt, observationCount in place of the forecast-hour extent)
Manifest (either)parseManifestJson<model>/manifest.json when the caller does not know the dataset kind: the union of the two shapes above
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 a counterpart without the Json suffix that takes an already-parsed value. All of them return the typed document or null. They never patch rejected input 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;
}

Site timezone propagation

The profile reference defines the optional site.timeZone echo. An older document simply lacks it, and that does not put the launch on UTC. For such a document, supply a known timezone yourself or rely on the API’s documented fallback:

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

Scalar values

Decide how to read a value from its shape. A model slug does not tell you. The profile reference has a worked example of narrowing.

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

If your code supports only deterministic documents, run isDeterministicProfile(profile) once per document. After that one check, every scalar position is typed as number.

Unit boundary map

Every document uses the platform’s shared units and conventions. Units, angles, one wind sign defines the wind sign convention and the conversion helpers. These are the unit boundaries that most often cause integration errors:

Quantity familyContract conventionCommon mistake
Sea-level pressure (surface.seaLevelPressureHpa)hPa, the one pressure unit, shared with station documentsAssuming whole pascals; the wire carries hPa
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 the generated schemas define each field. The profile guide maps the document blocks, and Model capabilities explains declared absence and semantics.

For example, to plot surface.pblHeightM beside MSL series, add profile.site.modelElevationM. Do not add the launch elevation, because the model’s PBL depth is measured from the model’s own terrain. For plain unit conversions, use package exports such as msToKmh from @azohra/meteo.briefing/derive.

Building-block exports

Every block inside the nine document families is also exported as a zod schema with an inferred type. You can validate or type one fragment, such as a single hour, a capability declaration, or a manifest stats block, without handling a whole document. Each schema and type pair feeds exactly one parse entry point.

The document roots are siteForecastSchema/SiteForecast, forecastManifestSchema/ForecastManifest, observationManifestSchema/ObservationManifest (parsed by parseObservationManifest(Json)), modelCatalogueSchema/ModelCatalogue, sitesCatalogueSchema/SitesCatalogue, siteContextSchema/SiteContext, runsIndexSchema/RunsIndex, smokeDocumentSchema/SmokeDocument, and observationDocumentSchema/ObservationDocument. manifestSchema/Manifest, parsed by parseManifest(Json), is the union of the two manifest roots, for callers that read <model>/manifest.json without knowing the dataset kind.

Each document family pins its own exported version constant, so a breaking change to one family cannot invalidate readers of another. Manifests pin MANIFEST_SCHEMA_VERSION (1), the model catalogue MODEL_CATALOGUE_SCHEMA_VERSION (1), the run index RUNS_INDEX_SCHEMA_VERSION (1), smoke documents SMOKE_SCHEMA_VERSION (1), observation documents OBSERVATION_SCHEMA_VERSION (1), the profile SITE_FORECAST_SCHEMA_VERSION (2), and sites and site context SITES_SCHEMA_VERSION (2) and SITE_CONTEXT_SCHEMA_VERSION (3). The pieces are:

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 declared-capability fields are absent when unpublished, never 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.

Compatibility rules

  • Check schemaVersion. Filenames say nothing about compatibility.
  • A model is identified by an open slug that you discover from the catalogue. The package has no enum of models.
  • An absent optional field means the value was not published there. It is never zero.
  • A stored profile’s optional semantics and site.timeZone travel with the document, and leaving either out does not imply a default (profile reference).
  • The zod contract decides behaviour. For other languages, the generated JSON Schema files ship in the schema/ directory of the @azohra/meteo.briefing tarball and live in the repository at briefing/schema/. The package also 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.

Compatibility lists the document families and the rule for reading across versions. Package versioning covers npm versions.