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.

Earlier developmentWindow 12:00–17:00, open at the first published hour, peak 2.97 m/s
w* m/s 3 0 900m 2953ft 1599m 5245ft 2298m 7538ft 2996m 9831ft 3695m 12123ft 4394m 14416ft 12 13 14 15 16 17 launch 1050 m
Later developmentWindow 14:00–17:00, start stated, peak 2.97 m/s
w* m/s 3 0 900m 2953ft 1599m 5245ft 2298m 7538ft 2996m 9831ft 3695m 12123ft 4394m 14416ft 12 13 14 15 16 17 launch 1050 m
Both documents share one launch marker and one lift scale. The development is the same, and only its hours move.Units time UTC · altitude m MSL · thermal strength as shaded columns
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.

Compare board — 2000-01-01 (Etc/UTC), 2 membersMODELLAUNCH m/sGUST m/sALOFT m/sTOP mSTORMS08121620window open since at least 12:00, still open at 17:00earliersurface wind, window open 12:00 → close 17:00; at peak lift 5° 2.9 m/s (15:00); a "·" direction sits under the analysis's 1 m/s direction floor355° 2 → 12° 3.5 m/sveer 17°no statement—strongest wind in the climb band during the window, 17:00, from 10°4.8 @ 1625 mpeak usable-lift top, 15:00 — 860 m above launch; cloud base sets the top at the cited hour — the number IS cloud base1910 mcloud baseno statement—window opens 14:00, still open at 17:00latersurface wind, window open 14:00 → close 17:00; at peak lift 12° 3.5 m/s (17:00); a "·" direction sits under the analysis's 1 m/s direction floor2° 2.6 → 12° 3.5 m/sveer 10°no statement—strongest wind in the climb band during the window, 17:00, from 10°4.8 @ 1625 mpeak usable-lift top, 17:00 — 860 m above launch; cloud base sets the top at the cited hour — the number IS cloud base1910 mcloud baseno statement—
Read down the column: earlier 12:00–17:00 against later 14:00–17:00, the same development two hours apart.Units time local · winds m/s · altitude m MSL

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

One run so far this month. Each new build appends one more member.

What compareRuns states

  • existenceTrajectory
  • timingTrajectory
  • magnitudeTrajectory
  • identityDrift
  • settled

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.

Precip mm/h 0.5 0 Cloud % 100 0 w* m/s 3.1 0 900m 2953ft 1599m 5245ft 2298m 7538ft 2996m 9831ft 3695m 12123ft 4394m 14416ft 12 13 14 15 16 17 23° 24° 26° 27° 26° 24° launch 1050 m 0° 10° 20°
Usable lift Cloud base Boundary layer 0 °C Condensation
Usable lift stays at the 2089.3 m cloud base all afternoon while the boundary layer rises far above it, to 3826.1 m at the peak-lift hour. This is the liftCeiling finding drawn as a chart.Units altitude m and ft · wind km/h · temperature °C · precipitation mm · w* m/s

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.

Precip mm/h 0.5 0 Cloud % 100 0 w* m/s 3 0 900m 2953ft 1604m 5262ft 2308m 7572ft 3012m 9882ft 3716m 12192ft 4420m 14501ft 10 11 12 13 14 15 16 17 18 19 7° 9° 11° 16° 23° 25° 24° 17° 13° 9° launch 1050 m 0° 10° 20°
900m 1604m 2308m 3012m 3716m 4420m hPa 925 900 850 800 750 700 650 600 -10° 0° 10° 20° 30° °C km/h launch 1050 m LCL 3507 m cloud base 3490 m usable lift 3082 m BL top 2829 m Temperature Dew point Parcel 8 published levels · top of column 4250 m
Model's published levels
The tinted Meteogram column is 15:00 UTC, the hour of the day's strongest lift, and the sounding draws that column vertically. The 8 dots on each trace are the model's published levels. The straight segments between them are interpolation, not data. The dashed parcel is the only derived trace, and its LCL at 3507 m sits just under the 3490 m cloud base.Units altitude m · temperature °C · wind km/h · w* m/s

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.briefing
import { 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.