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": 1,
"model": "convective-cycle"
}{
"referenceTime": "2000-01-01T06:00:00Z",
"generatedAt": "2000-01-01T06:00:00Z"
}{
"id": "synthetic-ridge",
"name": "Synthetic Ridge",
"latitude": 49,
"longitude": -123,
"altitudeM": 1050,
"modelElevationM": 900,
"timeZone": "Etc/UTC"
}{
"gust": "hourMax",
"precipitation": "instantRate"
}{
"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
}
}{
"pressureHpa": 925,
"heightM": 1100,
"temperatureC": 23.2,
"dewPointC": 11.2,
"windSpeedMs": 3.5,
"windDirectionDeg": 205
}{
"boundaryLayerTopM": 3566.7,
"thermalVelocityMs": 2.88,
"cloudBaseM": 4556.8,
"usableLiftTopM": 3946.4
}| 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.
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;}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 |
|---|---|
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 |
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 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.
Building-block exports
Section titled “Building-block exports”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.
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 v0.3-era tag does not imply a default. - A stored profile’s optional
site.timeZonekeeps 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.jsonand its four siblings) and in the repository attoolkit/schema/. They are not a package export specifier, so read them from the filesystem or the repository, not throughimport.
See Data and package versioning for the independent dataset, npm, and Python version axes.