Turn published forecasts into briefings
Point @azohra/meteo.briefing at a published forecast and get back answers you can put in front of pilots: when the day works, how high it goes, whether the wind crosses your limits — typed data your site renders its own way, plus the Meteogram when the answer should be a chart.
12:00–17:00 · launch 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
The hero card above is this call and nothing else — fetch a published forecast, validate it, analyze it against 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 }, });
Here is everything that call returned for the sample day — 8 findings, each written out from its own fields:
- 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) came from the call above: the package ships no safe-wind numbers, and with no 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 answers with statements, not a winner: it analyzes every model's document under your launch and thresholds, then reports where they agree and how far they spread. Here it reads two documents for the same day — the same development, 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.
Has the forecast settled?
compareRuns answers by lining up what each successive run said about the same local day, newest first. The runs come out of the month archive every engine build appends to — one gzip member per build, existing bytes never rewritten — via loadForecastHistory:
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 — advisory, never authoritative
What compareRuns states
existenceTrajectory- every run's vote on the day — window, quiet, or an abstention with its stated reason
timingTrajectory- window edges across runs; a horizon-clipped edge reads “open since at least”, never as timing
magnitudeTrajectory- peak climb, launch-relative peak lift, and window duration per run — the deltas state themselves
identityDrift- republications and changed model facts, so a publisher fix is never read as weather
settled- whether the newest runs' lift magnitudes sit within one stated band — stability, not skill
It never editorializes: no "trending better", no weighting one run over another, no agreement grades — just the series and the arithmetic, for you to present as you see fit. History and run convergence documents every kind.
When the answer should be a chart
The meteogram tier draws the same document the findings above read: the profile laid out as a renderer-independent scene graph, then serialized to SVG. It is one tier of the package, not 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.
Start building
Install it, point it at a forecast URL, and you have findings and a chart. This example runs against a sample forecast hosted by this site, so it works 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, transport, derive, analyze, compare, history, meteogram — and each has its own page in the briefing documentation.
This package reads forecasts. It doesn't make them.
Producing and publishing forecasts is the job of @azohra/meteo.forecast, the engine. Whatever the engine publishes, this package can read, analyze, compare, and draw — the full reference is the briefing documentation.