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.
| Document | Parser | Published location |
|---|---|---|
| Profile | parseSiteForecastJson | <model>/sites/<site>.json and each history line |
| Manifest | parseForecastManifestJson | <model>/manifest.json |
| Models | parseModelCatalogueJson | models.json |
| Sites | parseSitesCatalogueJson | sites.json |
| Site context | parseSiteContextJson | site-context.json |
| Run index | parseRunsIndexJson | runs.json |
| Smoke | parseSmokeDocumentJson | <model>/sites/<site>.json for smoke models |
| Observation | parseObservationDocumentJson | <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.
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
Section titled “Site timezone propagation”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:
| API | Timezone behaviour |
|---|---|
analyzeForecast | Override, then profile.site.timeZone, then UTC with a timesAreUtc caveat |
projectForecast({ day }) | Override, then profile.site.timeZone; throws if neither exists |
buildMeteogramScene | Requires an explicit timeZone option |
groupByLocalDay / meteogramDisplayHours | Require the caller’s explicit timezone |
Scalar values
Section titled “Scalar values”Every numeric position under surface, levels, and derived is either a
number or an ensemble percentile object. Switch on shape, not a model slug:
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.
Unit boundary map
Section titled “Unit boundary map”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 family | Contract convention | Common mistake |
|---|---|---|
Sea-level pressure (surface.seaLevelPressureHpa) | hPa — one unit for the whole pressure class, shared with station documents | Reading a v1 document’s pressurePa (whole pascals) as hPa without migrating |
| Pressure levels | hPa | Multiplying named isobaric levels by 100 in labels |
| Heights and elevations | metres; profile altitude values are MSL unless explicitly AGL | Plotting model PBL depth directly on an MSL axis |
| Model PBL height | metres AGL | Comparing it to derived.boundaryLayerTopM without adding site.modelElevationM |
| Temperature and dew point | °C | Treating dew-point depression as published dew point |
| Wind speed and gust | m/s | Displaying as km/h without a named conversion |
| Wind direction | meteorological FROM, 0–359° | Using mathematical TO-direction |
| Vertical velocity | omega, Pa/s; negative is lift | Reading negative as sinking geometric velocity |
| Precipitation | mm/h with declared provider window semantics | Comparing instantaneous and window-mean rates as identical measurements |
| Cloud and cloud layers | percent | Replacing unavailable fields with 0% |
| CAPE/CIN | J/kg | Treating absent CIN as zero inhibition |
| Smoke concentrations | µg/m³ at the surface, mg/m² for columns; optical thickness dimensionless | Assuming 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 surface | Treating 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.
Building-block exports
Section titled “Building-block exports”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 role | Feeds |
|---|---|---|
scalarSchema (Scalar) | Any numeric position: a number or an ensemble percentile object | every value field below |
ensembleValueSchema (EnsembleValue) | The percentile-object arm of Scalar, including full dropout | every value field below |
forecastHourSchema (ForecastHour) | One forecast hour: validAt plus the surface, levels, derived, and optional smoke blocks below | parseSiteForecast(Json) |
forecastSurfaceSchema (ForecastSurface) | An hour’s surface block, optional capability fields absent-not-zero | parseSiteForecast(Json) |
forecastLevelSchema (ForecastLevel) | One pressure-level entry in an hour’s ascending levels array | parseSiteForecast(Json) |
forecastDerivedSchema (ForecastDerived) | The forecast engine’s derived block of an hour | parseSiteForecast(Json) |
forecastSiteSchema (ForecastSite) | Sample provenance: identity, coordinates, the model’s own terrain (modelElevationM), and the optional timezone echo — no launch elevation | parseSiteForecast(Json) |
forecastRunSchema (ForecastRun) | Publication identity: referenceTime, generatedAt, optional members | parseSiteForecast(Json) |
forecastSemanticsSchema (ForecastSemantics) | The optional gust/precipitation meaning tag stored with a document | parseSiteForecast(Json) |
forecastManifestSiteSchema (ForecastManifestSite) | One published site name/slug pair in a manifest | parseForecastManifest(Json) |
forecastManifestStatsSchema (ForecastManifestStats) | The stable accounting core plus open numeric extension keys | parseForecastManifest(Json) |
modelEntrySchema (ModelEntry) | One model catalogue entry: slug, cadence and typical publication lag, levels, lifecycle | parseModelCatalogue(Json) |
modelCapabilitiesSchema (ModelCapabilities) | A model’s declared capability set, absences included | parseModelCatalogue(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 values | parseSiteContext(Json) |
siteContextEntrySchema (SiteContextEntry) | One site’s measured ground truth — the launch elevation pick, terrain, and land cover — joined to sites.json by slug | parseSiteContext(Json) |
runsIndexEntrySchema (RunsIndexEntry) | One model’s current (referenceTime, generatedAt) pair | parseRunsIndex(Json) |
DeterministicSiteForecast is the narrowed profile type
isDeterministicProfile returns.
Compatibility rules
Section titled “Compatibility rules”- 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
semanticskeeps 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.timeZonekeeps 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.briefingtarball’sschema/directory and in the repository atforecast/schema/; the package exports them as@azohra/meteo.briefing/schema/*.json(profile.schema.jsonand its siblings), so a resolver can reach them by specifier as well as by path.
The v1 freeze and its unlock
Section titled “The v1 freeze and its unlock”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.