Skip to content

Contract validation

The windgram/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 six document families.

A profile document, block by block

One document carries its own contract version, publication identity, site context, semantics, and every forecast hour.

Excerpts of a real teaching profile: the schemaVersion and model identity, the run block, the 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.

schemaVersion · model

Check schemaVersion before anything else; model is an open catalogue slug, not a package enum.

{
  "schemaVersion": 1,
  "model": "convective-cycle"
}

run

Publication identity: (referenceTime, generatedAt). Ensemble documents add total membership at run.members.

{
  "referenceTime": "2000-01-01T06:00:00Z",
  "generatedAt": "2000-01-01T06:00:00Z"
}

site

Identity, coordinates, surveyed launch altitude, grid terrain, and the optional v0.4 timezone echo.

{
  "id": "synthetic-ridge",
  "name": "Synthetic Ridge",
  "latitude": 49,
  "longitude": -123,
  "altitudeM": 1050,
  "modelElevationM": 900,
  "timeZone": "Etc/UTC"
}

semantics

Optional gust and precipitation meaning, stored with the document; absence creates no default.

{
  "gust": "hourMax",
  "precipitation": "instantRate"
}

hours[5].surface

One of 10 chronological UTC hours — the peak-W* hour here. Optional capability fields are absent, never zero.

{
  "validAt": "2000-01-01T15:00:00Z",
  "surface": {
    "pressurePa": 90060,
    "temperatureC": 26,
    "dewPointC": -4,
    "windSpeedMs": 3.5,
    "windDirectionDeg": 200,
    "cloudCoverPercent": 8,
    "precipitationMmHr": 0,
    "sensibleHeatFluxWm2": 360,
    "latentHeatFluxWm2": 60,
    "windGustMs": 8,
    "capeJkg": 780,
    "cinJkg": -5,
    "pblHeightM": 2800,
    "lowCloudPercent": 3,
    "midCloudPercent": 5,
    "highCloudPercent": 10
  }
}

hours[5].levels[0]

First of 8 ascending pressure levels: height, temperature, moisture, and wind per isobaric coordinate.

{
  "pressureHpa": 925,
  "heightM": 1100,
  "temperatureC": 23.2,
  "dewPointC": 11.2,
  "windSpeedMs": 3.5,
  "windDirectionDeg": 205
}

hours[5].derived

The pipeline-owned block: boundary-layer top, thermal velocity, cloud base, and the stored 1.0 m/s usable-lift top.

{
  "boundaryLayerTopM": 3566.7,
  "thermalVelocityMs": 2.88,
  "cloudBaseM": 4556.8,
  "usableLiftTopM": 3946.4
}
Every fragment is stringified from the parsed committed profile at build time — these are the document's actual published values. The excerpted hour is hours[5] of 10; each hour repeats the surface/levels/derived shape.Units heights m MSL · temperatures °C · wind m/s · pressure Pa
Document Parser Published location
Profile parseWindgramProfileJson <model>/sites/<site>.json and each history line
Manifest parseWindgramManifestJson <model>/manifest.json
Models parseModelCatalogueJson models.json
Sites parseSitesCatalogueJson sites.json
Site context parseSiteContextJson site-context.json
Run index parseRunsIndexJson runs.json

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 {
parseWindgramProfileJson,
type WindgramProfile,
} from "windgram/contract";
export function requireProfile(text: string): WindgramProfile {
const profile = parseWindgramProfileJson(text);
if (!profile) throw new Error("unsupported or invalid windgram 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:

API Timezone behaviour
analyzeProfile Override, then profile.site.timeZone, then UTC with a timesAreUtc caveat
projectProfile({ day }) Override, then profile.site.timeZone; throws if neither exists
buildScene Requires an explicit timeZone option
groupByLocalDay / windgramDisplayHours Require 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 WindgramProfile } from "windgram/contract";
export function firstWindSpeed(profile: WindgramProfile): number | null | undefined {
const speed = profile.hours[0]?.surface.windSpeedMs;
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 block inside the five 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 windgramProfileSchema/WindgramProfile, windgramManifestSchema/WindgramManifest, modelCatalogueSchema/ModelCatalogue, sitesCatalogueSchema/SitesCatalogue, siteContextSchema/SiteContext, and runsIndexSchema/RunsIndex; SCHEMA_VERSION is the literal every root pins. 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
windgramHourSchema (WindgramHour) One forecast hour: validAt plus the three blocks below parseWindgramProfile(Json)
windgramSurfaceSchema (WindgramSurface) An hour’s surface block, optional capability fields absent-not-zero parseWindgramProfile(Json)
windgramLevelSchema (WindgramLevel) One pressure-level entry in an hour’s ascending levels array parseWindgramProfile(Json)
windgramDerivedSchema (WindgramDerived) The pipeline-owned derived block of an hour parseWindgramProfile(Json)
windgramSiteSchema (WindgramSite) Site identity, coordinates, elevations, and the optional timezone echo parseWindgramProfile(Json)
windgramRunSchema (WindgramRun) Publication identity: referenceTime, generatedAt, optional members parseWindgramProfile(Json)
windgramSemanticsSchema (WindgramSemantics) The optional gust/precipitation meaning tag stored with a document parseWindgramProfile(Json)
windgramManifestSiteSchema (WindgramManifestSite) One published site name/slug pair in a manifest parseWindgramManifest(Json)
windgramManifestStatsSchema (WindgramManifestStats) The stable accounting core plus open numeric extension keys parseWindgramManifest(Json)
modelEntrySchema (ModelEntry) One model catalogue entry: slug, cadence, levels, lifecycle parseModelCatalogue(Json)
modelCapabilitiesSchema (ModelCapabilities) A model’s declared capability set, absences included parseModelCatalogue(Json)
siteCatalogueEntrySchema (SiteCatalogueEntry) One launch entry, IANA timeZone required since v0.4 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 terrain, optional bare-earth, and land-cover context, joined to sites.json by slug parseSiteContext(Json)
runsIndexEntrySchema (RunsIndexEntry) One model’s current (referenceTime, generatedAt) pair parseRunsIndex(Json)

DeterministicWindgramProfile 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 v0.3-era tag does not imply a default.
  • A stored profile’s optional site.timeZone keeps local-time interpretation with the document; absence of that v0.4-era echo does not imply UTC.
  • The zod contract is behavioural authority. For other languages, the generated JSON Schema files ship as plain files in the npm tarball’s schema/ directory (node_modules/windgram/schema/profile.schema.json and its four siblings) and in the repository at toolkit/schema/. They are not a package export specifier, so read them from the filesystem or the repository, not through import.

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