Skip to content

Failures and schema artifacts

@azohra/meteo.core defines a closed vocabulary for upstream failure, the zod schema primitives that capability configs share, and the one renderer behind each capability’s published JSON Schema artifacts. They are defined in failures.ts, schema.ts, and schema-artifacts.ts.

When an upstream fails, the wire carries a reason code from a closed vocabulary. UPSTREAM_FAILURE_REASONS declares exactly four codes.

ReasonMeaning
upstream_errorThe upstream failed: an error response, or a network refusal.
timeoutThe request ran out of time or was aborted.
rate_limitedThe upstream refused for rate.
contract_breakThe upstream answered, but not in the shape the contract promises.

The vocabulary splits into two types. UpstreamFailureReason is all four codes, which is what the wire may carry. UpstreamErrorReason excludes contract_break. It is the set an UpstreamError may be thrown with, because contract_break is only ever the mapper’s verdict and is never thrown.

  • UpstreamError is the error transports throw when they know why an upstream failed. It carries a reason (default "upstream_error") alongside the human message.
  • unavailableReasonForError(error) maps any thrown value onto the wire’s reason codes. An UpstreamError keeps its own reason. An Error named TimeoutError or AbortError becomes timeout. A TypeError becomes upstream_error, because fetch rejects network refusals as TypeError. Anything else is contract_break.
import { UpstreamError, unavailableReasonForError } from "@azohra/meteo.core";
try {
throw new UpstreamError("provider returned 429", "rate_limited");
} catch (error) {
const reason = unavailableReasonForError(error); // "rate_limited"
}

The reason code is all that travels. Words, retries, and presentation are up to the consumer.

“Closed” applies to what a transport may report. A capability’s wire may extend the vocabulary with its own failures. Station’s UNAVAILABLE_REASONS is these four codes plus not_configured, a config verdict no upstream ever produced, as its wire contract documents.

These zod building blocks in schema.ts recur in capability configs.

  • ianaTimeZone is a string that Intl.DateTimeFormat accepts as a time zone. Anything else fails with not an IANA time zone.
  • httpUrl is a parseable URL whose protocol is http: or https:.
  • positionFields holds the position-claim fields that station configs spread in: elevationM (finite), latitude (−90 to 90), and longitude (−180 inclusive to 180 exclusive). Exactly 180 is rejected, so every position has one canonical longitude. The Tempest adapter normalizes a payload’s 180 to −180 before validating. All three fields are nullish, so a config that claims no position stays null.

Each capability that publishes wire documents also publishes JSON Schema for them, committed under its own schema/ directory (briefing, station), and this module is the one renderer behind those files. A consumer needs only that convention. The API below exists for the packages’ own schema emission, and a consumer of briefing or station never calls it.

  • SchemaArtifact declares one artifact: fileName, title, the zod schema it is generated from, and an optional description.
  • ExampleArtifact declares an example wire document committed beside the schemas. It is validated against its schema before writing, so a committed example can never drift from its contract.
  • schemaArtifactJson(artifact) produces the artifact’s JSON Schema document exactly as shipped. The zod schema is converted to JSON Schema draft 2020-12 and wrapped with $schema, the title, the description when present, and an $id of https://meteo.azohra.com/schema/<fileName>.
  • renderJsonArtifact(value) and renderSchemaArtifact(artifact) produce the exact shipped bytes, which are two-space-indented JSON with a trailing newline.

The conversion deliberately runs with zod’s io: "input", so a published schema never carries additionalProperties: false. Wire readers ignore unknown keys, which is how the contracts evolve. A schema that rejected unknown keys would contradict the wire’s own semantics.

Every committed schema is generated from its zod authority and ships with its package. The zod schemas and their parse guards remain the behavioural truth.