Turn published forecasts into briefings
Give @azohra/meteo.briefing a published forecast and it answers what pilots want to know: when the day works, how high it goes, and whether the wind crosses your limits. The answers are typed data that your site renders its own way, and the Meteogram draws them as a chart.
The briefing for a launch at 1050 m
- Thermal window
- 12:00–17:00, peak climb 3.09 m/s
- Lift ceiling
- 2089.3 m, pinned to cloud base, under a boundary layer reaching 3826.1 m
- Wind check
- Over your 8 m/s limit 16:00–17:00, peaking 8.8 m/s
From URL to briefing
Fetch a published forecast, validate it, and analyze it for your launch.
import { parseSiteForecastJson } from "@azohra/meteo.briefing/contract"; import { analyzeForecast } from "@azohra/meteo.briefing/analyze"; const response = await fetch(forecastUrl); const forecast = parseSiteForecastJson(await response.text()); if (!forecast) throw new Error("failed contract validation"); const { findings } = analyzeForecast(forecast, { launch: { elevationM: 1050 }, windCeilings: { surfaceMps: 6, bandMps: 8 }, });
For the sample day, that call returns these 8 findings.
- Thermal window
thermalWindow - 12:00–17:00, 6 h of usable lift, open at both edges of the document's horizon. Peak climb 3.09 m/s; the lift tops out at 2089.3 m at 17:00 — 1039.3 m above launch.
- Lift ceiling
liftCeiling - Cloud base caps the climb from 12:00 to 17:00: usable lift peaks at 2089.3 m under a 2089.3 m cloud base, while the heated boundary layer reaches 3826.1 m.
- Wind
windSummary - Strongest wind in the climb band: 8.8 m/s from 5° at 1125 m, 17:00.
- Surface-wind ceiling
windExceedance - Above your 6 m/s ceiling 16:00–17:00 (2 h), peaking 6.9 m/s at 17:00.
- Band-wind ceiling
windExceedance - Above your 8 m/s ceiling 16:00–17:00 (2 h), peaking 8.8 m/s at 17:00.
- Wind direction
windDirection - Surface flow veers 17° across the window — 355° at 2.2 m/s opening, 12° at 6.9 m/s closing. The climb band averages 1° at 4.46 m/s.
- Shear in the climb band
bandShear - Strongest layer shear 4.04 m/s across 1125–1625 m at 17:00 — 8.08 m/s per km, from 8.8 m/s at 5° up to 4.8 m/s at 10°.
- Data caveats
dataCaveats - This model never publishes windGustMps, capeJkg, cinJkg, pblHeightM, lowCloudPercent, midCloudPercent, highCloudPercent, levels[].verticalVelocityPaS, levels[].cloudFractionPercent, smoke — those read as absent, not zero.
Each row is a typed object (a kind, its numbers, the hours it cites, and the thresholds it was computed with), so your site decides what to show and how. The two wind ceilings (6 and 8 m/s) are arguments to the call. The package ships no safe-wind numbers, so without a ceiling there is no exceedance finding. The thermal-window thresholds (0.9 m/s of climb over at least 300 m) are defaults you can override per call.
Do the models agree?
compareForecasts analyzes every model's document with your launch and thresholds, then states where they agree and how far they spread. It does not pick a winner. Here it reads two documents for the same day, where the same development arrives two hours apart.
Two controlled development timings
Two synthetic profiles can express the same daytime development at different teaching hours, making timing differences visible without treating either profile as correct.
Two synthetic profiles with matching daytime development shifted to earlier and later hours, rendered side by side from the comparison pair's documents.
- Window agreement
windowAgreement - 2 voters, unanimous: both members call a thermal window. Only later states a start the day can time: 14:00. The other window's opening edge is clipped by its document's horizon, so it reads "open since at least" and stays out of the timing envelope; startSpreadHours is null, not a number no model stated.
- Height spread
heightSpread - 0 m between the members' peak lifts: earlier puts 859.9 m above launch at 15:00; later puts 859.9 m above launch at 17:00. No mean, no consensus height: the spread is stated, weighting is yours.
- Direction spread
windDirectionSpread - 3° between the members' window-mean surface flows (earlier 4° at 2.74 m/s, later 7° at 3.04 m/s), across a 0 m model-ground delta.
Every model on one clock
The compare board draws every model's day on one shared time axis. Each row shows the thermal window as a bar, with the hours past your wind ceilings marked beneath it and cap break and rain onset at their hours. The launch, aloft, top, and storm numbers sit beside the row. Timing differences line up down the column. Where a model cannot state a value, the board shows a dash instead of a blank. Compare board documents the scene and its SVG serializer.
Two development timings, one axis
Two synthetic profiles can express the same daytime development at different teaching hours, making timing differences visible without treating either profile as correct.
Compare board with two model rows on a shared 07:00–21:00 local axis; each row draws its thermal window as a bar with launch, gust, aloft, top, and storms cells beside it.
Has the forecast settled?
compareRuns lines up what each successive run said about the same local day, newest first. loadForecastHistory reads those runs from the month archive. Every engine build appends one gzip member to it and never rewrites existing bytes.
data-sample/hrdps-continental/history/test-hill/2026-08.jsonl.gz · 2.0 kB
- gzip member @ byte 0 · 2.0 kB · 1 line→ hrdps-continental · run 2026-08-12T12:00:00Z · generated 2026-08-12T18:22:29Zthe whole published document: Test Hill, 8 hours
sidecar 2026-08.index.json lists each member's byte offset and length, so a reader can fetch only the runs it is missing with a Range request. The sidecar is advisory; the archive is authoritative
What compareRuns states
existenceTrajectorytimingTrajectorymagnitudeTrajectoryidentityDriftsettled
It reports the series and the arithmetic and leaves the interpretation to you. It does not say a forecast is "trending better", weight one run over another, or grade agreement. History and run convergence says what each kind states.
When the answer should be a chart
The Meteogram draws the same forecast the findings read. The package lays the profile out as a scene graph that no renderer depends on, then serializes it to SVG. The chart is one subpath of the package. Reading a Meteogram explains every mark, and Render your first Meteogram gets you to this chart from a URL.
Cloud base limits an otherwise deep climb
A saturated layer pins usable lift at cloud base even while the heated boundary layer extends higher.
Cloud base limits an otherwise deep climb. This Meteogram shows one atmospheric profile in Etc/UTC. A saturated layer pins usable lift at cloud base even while the heated boundary layer extends higher.
One hour, read vertically
A Meteogram covers the whole day, and a sounding covers one hour. The /sounding subpath takes the same profile document and a validAt time, and draws that hour's column as a vertical profile that stops where the model's published levels end. It shows temperature and dew point with one dot per published level, a lifted parcel with its LCL (lifted condensation level), the derived heights, and a ladder of wind barbs. The sounding is the reference for the subpath.
A complete fair-weather convective cycle, and its 15:00 UTC sounding
Morning stability gives way to a deep midday unstable column. The boundary layer, usable lift, and cloud base follow distinct arcs beneath light wind that veers gently and gains only a few km/h with height.
A complete fair-weather convective cycle. This Meteogram shows one atmospheric profile in Etc/UTC. Morning stability gives way to a deep midday unstable column. The boundary layer, usable lift, and cloud base follow distinct arcs beneath light wind that veers gently and gains only a few km/h with height. Beside the Meteogram, a sounding of the 15:00 UTC hour: temperature, dew point, and a lifted parcel against altitude, with wind barbs at each published level.
Start building
Install the package and give it a forecast URL to get findings and a chart. This example reads a sample forecast hosted on this site, so it runs as pasted.
pnpm add @azohra/meteo.briefingimport { loadForecast } from "@azohra/meteo.briefing/transport";
import { analyzeForecast } from "@azohra/meteo.briefing/analyze";
import { buildMeteogramScene, renderMeteogramSvg } from "@azohra/meteo.briefing/meteogram";
const loaded = await loadForecast({
fetch,
baseUrl: "https://meteo.azohra.com/data-sample",
modelSlug: "hrdps-continental",
siteSlug: "test-hill",
});
if ("miss" in loaded) throw new Error(`no forecast: ${loaded.miss}`);
const { findings } = analyzeForecast(loaded.profile, {
launch: { elevationM: 1225.1 }, // site-context.json's elevation pick
});
const svg = renderMeteogramSvg(
buildMeteogramScene(loaded.profile, { timeZone: "America/Vancouver" }),
);Each capability is its own subpath (contract, derive, analyze, compare, transport, history, meteogram, compare-board, sounding), and each has its own page in the briefing documentation.
This package reads forecasts. It doesn't make them.
The engine, @azohra/meteo.forecast, produces and publishes forecasts. This package reads, analyzes, compares, and draws whatever the engine publishes. The full reference is the briefing documentation.