Builder contract
This page is for someone writing or reviewing a builder module. An operator running the CLI does not need it. The errors a failed build prints are quoted in Run one model.
A builder translates one verified provider feed into the shared source shape, then hands profile derivation and publication to shared code. It does not redefine the public JSON shape or renderer behaviour.
Required invariants
Section titled “Required invariants”- The builder agrees with the catalogue. The slug, kind, levels, cadence,
horizon, capability presence, and field semantics match
models.json. - The builder selects only complete runs. It selects a provider run only
after the run’s declared final product is available, and it skips an
already-published
referenceTimewithout rewriting output. - The builder is honest about its domain. A sample too far from a site signals out-of-domain clamping, and the build fails instead of publishing a boundary value.
- Units and directions are normalized. Source values are converted to the profile contract’s units and meteorological FROM-direction before publication.
- Absence stays absent. Missing records, masked sentinels, and unsupported optional fields are omitted. They are not converted to zero.
- Semantics are supplied by the builder and are not inferred. The verified
builder passes gust, precipitation, and (where the model carries smoke)
smoke semantics into
deriveSiteForecast. - Derived values have one authority. Builders supply source fields, and
forecast/src/derive.tssuppliesderived.*. - Publication is deterministic at the edge. Shared rounding and JSON writers own precision and serialized shape.
The shared machinery
Section titled “The shared machinery”A builder implements only what is specific to its model (field tables, URLs,
and provider quirks). For the steps every build shares, it uses
builders/common.ts, which exports the following.
BuilderHourandBuilderLevelare the source-hour and level shapes.emptyHour(validAt)seeds every numeric field with NaN, so a fetch task that never ran leaves a value the serializer refuses.isCompleteLevel(level)requires all six level fields before a level publishes.requiredValue(provider, value, fieldName, site)andmemberRequiredValue(value, field, site, member)reject a missing or non-finite required sample by name.withDewPointDepression(level)swaps a level’s relative humidity for the derived dew-point depression.validTime(referenceTime, forecastHour),manifestInstant(), andprofileInstant()produce the shared timestamp forms.runConcurrent(tasks, maxWorkers)is the bounded fetch pool.parseCycleStamp(referenceTime, runHours, name)validates a pinned cycle, andrunReferenceTime(run)is its inverse.KELVINand theNamedSitetype complete the module.
Publication authority lives in derive.ts, publish.ts, and
builders/publication.ts. Every run builder hands its model-specific parts
to publishRun, which resolves the run, applies the already-published gate,
and owns the profile, history, and manifest writes. A builder does not
write these itself.
A semantics declaration (invariant 6) is computed from the model descriptor.
modelSemantics in forecast/src/builders/eccc.ts reads the gust pairing
and precipitation transport from the descriptor’s fields, and each builder’s
contract test pins the result against models.json. HRDPS West, for
example, resolves to { gust: "hourMax", precipitation: "instantRate" }
because its descriptor names a gust-max variable and carries precipitation
as a plain surface rate (PRATE) instead of an accumulation.
What failure looks like
Section titled “What failure looks like”An invariant violation fails the build, and no document is published. The exact error strings an operator sees are quoted in Run one model. Optional declared-capability fields are the one sanctioned absence. They stay absent only where the catalogue declaration and builder behaviour agree (invariant 5).
Source hour versus published hour
Section titled “Source hour versus published hour”builder source published profiletemperatureC ─┐ surface.temperatureCdewPointDepressionC ├─→ surface.dewPointCheat fluxes + sampled levels ├─→ derived.*optional provider fields └─→ optional surface/level fieldsThe source hour is internal and may carry provider-facing intermediate names such as dew-point depression. The published contract is the stable boundary. Do not expose a builder intermediate only to skip an appropriate derivation.
Ensemble additions
Section titled “Ensemble additions”Ensemble builders derive every member independently, then aggregate matching
numeric positions. run.members is total membership.
EnsembleValue.members counts contributors at that position and can be lower
when null or censored member values are excluded. Wind direction uses circular
aggregation. Height censoring uses ceiledMembers only on positions where the
forecast engine records a column ceiling.
Focused builder tests should use committed fixtures or injected transport responses. They must not require live provider access. Verification establishes facts, and tests make behaviour repeatable.