Expand description
§Hydra Common — Foundation Contract
Status: v1.4 — ratified 2026-07-31 (v1.1 added opaque per-block options
to the production contract, §3.4; v1.2 added the chart fragment item,
§3.3; v1.3 added engine availability and import formats, §2.1–2.3; v1.4
added the recognition contract and its routing rules, §2.5).
This file is the module documentation
of the hydra-common crate and follows the same spec-first workflow as the
engine specs: implementation changes flow from changes here, never the
reverse.
§1. Purpose and Scope
The common layer is the foundation every engine and every application may depend on. It depends on nothing else in the workspace. It exists so that applications can host any Hydra engine — present or future — through one uniform surface instead of per-engine hardcoded knowledge.
v1 is deliberately slim. It defines exactly two contracts:
- Engine identity — what an engine is, including how it is recognised from a model’s bytes (§2).
- Reportable output — what an engine can contribute to a report (§3).
Explicit non-goals for v1 (ratified 2026-07-28): a shared element schema (node / link / subcatchment / …), a unified unit system, and any cross-engine simulation contract. These are deferred until at least one additional engine implementation exists to exercise them; abstracting them from a single implementation risks baking water-distribution assumptions into the foundation. Nothing in v1 may presuppose the shape those future contracts will take.
§2. Engine Identity
§2.1 Engine descriptor
Every engine publishes one immutable descriptor:
| Field | Meaning | Constraints |
|---|---|---|
key | Stable machine identifier | Lowercase ASCII, short domain-umbrella abbreviation (wds, uds, och). Never changes once released — it is persisted in project metadata and report templates. |
label | Human-facing product name | Practitioner-familiar term (e.g. “Water Distribution”). May be revised between releases. |
pill | Two-character badge | Uppercase, exactly 2 characters (e.g. “WD”). |
accent | Brand color for this engine | #rrggbb hex string. |
summary | One-sentence description of the engine’s domain | Plain text, no markup. |
status | Whether this distribution can actually run the engine | available or planned (§2.3). |
import | Source-model formats the engine imports | Ordered list of import-format descriptors (§2.2); may be empty for an engine with no import path. |
The key and the label/pill pair are two deliberately separate naming
systems: the key carries the accurate domain umbrella; the label carries
the familiar practitioner term. They are allowed to diverge and must not
be derived from one another.
§2.2 Import formats
An engine’s models originate in some external tool’s file format. The descriptor names those formats so applications can offer a correctly filtered file picker for any engine without hardcoding per-engine file knowledge:
| Field | Meaning | Constraints |
|---|---|---|
label | Human-facing format name | Plain text, e.g. “EPANET input file”. |
extensions | Filename extensions the format uses | One or more, lowercase ASCII, no leading dot. |
This is deliberately the only file knowledge in the foundation layer.
It names formats; it says nothing about their contents, and nothing here
may be used to decide whether a given file is valid. Validating that a
file really is a model of the named format is the owning engine’s job —
extensions are a picker filter and a first-pass hint, never a check. Two
engines legitimately share the inp extension (EPANET and SWMM both use
it) with entirely incompatible contents, so an application that trusted
the extension would hand a stormwater model to a water-distribution
solver.
§2.3 Availability
A registered engine is either:
available— implemented in this distribution and usable;planned— registered so applications can present it (and so its key is reserved), but carrying no implementation.
Planned engines are registered rather than hidden because a user choosing a modelling domain deserves to see what Hydra covers and what is coming, and because the key must be reserved before anything persists it.
Applications must present planned engines as explicitly unavailable and must refuse to create projects, run simulations, or import models for them. Refusing is a hard requirement, not a UI nicety: a persisted project naming a planned engine would be indistinguishable from one whose engine was removed.
Resolving a planned engine’s key is not an error and must not be conflated with the unknown-key case (§2.4) — the descriptor exists and its identity fields are valid; only its implementation is absent.
§2.4 Registry
The registry is the ordered collection of descriptors for every engine compiled into a distribution. It supports:
- Enumeration in a stable, deliberate order (the order engines are presented to users), available and planned engines alike.
- Lookup by key, which either yields the descriptor or a typed “unknown engine” error.
Applications must treat an unknown key (e.g. a project created by a newer Hydra carrying an engine this build lacks) as an explicit unsupported state, never as a fallback to a default engine.
v1 ships three registered engines — wds (available), uds (planned),
and och (planned) — in that order.
§2.5 Recognition
§2.2 establishes that an extension cannot decide which engine owns a file. Recognition is how that question is answered: given the bytes of a candidate model, each engine reports whether the model is one of its own.
The foundation layer defines only the neutral verdict. It contains no section names, no format grammar, and no engine vocabulary of any kind — the judgement is authored entirely by the engine, and this layer merely gives every engine the same three words to say it in:
| Verdict | Meaning |
|---|---|
definite | The bytes carry a marker that belongs to this engine’s format and to no other. |
plausible | The bytes are shaped like this engine’s format but carry nothing that distinguishes them from another engine claiming the same shape. |
no | The bytes are not this engine’s, either because the format is unrecognised or because they carry another format’s marker. May carry engine-authored text saying what the engine believes the file is instead. |
The optional text on no is the same device the reportable-output contract
uses for an unavailable block (§3.4): the foundation layer holds no words of
its own, and an engine that can say “this is a SWMM model, it declares a
[SUBCATCHMENTS] section” gives an application something far more useful to
report than a bare refusal. It is advisory — an application must behave
identically whether or not it is present.
Recognition is not validation. It answers “whose is this?”, not “can
this run?”. It must be cheap enough to run against every registered
engine before any model is parsed, so it may inspect only as much of the
input as identification requires. A definite verdict is not a promise
that the model is well-formed or simulable — that remains the owning
engine’s parse and validation step, which may still reject it.
Recognition may be stricter than parsing. An engine may decline to claim a file it would nonetheless parse successfully when told to. This is deliberate: automatic routing must not guess, whereas an explicit instruction from the user carries information routing does not have.
§2.5.1 Routing
An application holding a model of unknown provenance resolves it by asking every available engine (§2.3) and applying, in order:
- Exactly one
definite— that engine owns the model. - More than one
definite— ambiguous. This indicates two engines claiming the same marker and is a defect in one of them; report it as ambiguity rather than choosing. - No
definite, one or moreplausible— ambiguous, however few engines answered that way. - Nothing but
no— unrecognised.
Rule 3 holds even when exactly one engine answered plausible, and
even when only one engine is available at all. A plausible verdict means
precisely “I cannot distinguish this from another engine’s model”, so
acting on it is the guess this contract exists to prevent — the model may
belong to an engine that is registered but planned (§2.3), or to one a
later release adds. An engine that can genuinely identify its own models
returns definite; if it cannot, the shortfall is in its recognition, not
something routing should paper over.
The two failures are therefore distinguishable and should be reported differently: ambiguity means “narrow it down for me” and is answered by naming the engine explicitly, whereas unrecognised means no engine here reads this format at all.
Routing must never fall back to a default engine. Ambiguous and unrecognised are terminal outcomes that the application reports, offering the user the means to name the engine explicitly. Choosing arbitrarily would hand a model to a solver that models different physics and return a confident, wrong answer — the failure §2.2 exists to prevent.
Planned engines (§2.3) are not consulted, having no implementation to
consult. An application that can otherwise identify the model as a
planned engine’s — for example because the owning engine returned no
and named the foreign format — should say so rather than reporting a
generic failure: “this is a SWMM model, and that engine is not yet
implemented” is actionable where “unrecognised” is not.
§2.5.2 Layering
The registry (§2.4) is inert data and cannot invoke engines: this layer depends on nothing, and an engine’s recognition lives in the engine. The dispatch that consults each engine and applies §2.5.1 therefore belongs to a layer that sees both this contract and every engine — never to an individual application, which would duplicate the routing policy in every interface and let them drift apart.
§3. Reportable-Output Contract
The contract by which an engine describes — and produces — the content blocks a report can include. Presentation (layout, styling, output formats, templates) is not part of this contract; it belongs to the report layer, which consumes this contract and knows nothing engine-specific.
§3.1 Concepts
| Term | Meaning |
|---|---|
| Block | One self-contained unit of reportable content an engine can produce (e.g. a pressure summary, a pump energy table). |
| Catalog | The engine’s complete list of block descriptors. Queryable statically — without any simulation having run. |
| Fragment | The materialized content of one block for one completed simulation. |
§3.2 Block descriptor
| Field | Meaning | Constraints |
|---|---|---|
id | Stable block identifier | Namespaced by engine key: <engine>.<name> (e.g. wds.pressure-summary). Never changes once released — report templates reference it. |
title | Default human-facing heading | Plain text. |
summary | What this block contains, for the template-builder UI | One or two sentences, plain text. |
The descriptor deliberately carries no result-class or prerequisite vocabulary — what a block needs from a simulation is the producing engine’s internal concern, expressed only through the production error contract (§3.4). Encoding result taxonomies (hydraulic vs. quality vs. anything else) here would bake one engine family’s domain into the foundation layer.
Removing a block id, or changing the meaning of an existing id, is a breaking change to every saved template that references it and must be treated with the same gravity as a file-format break.
§3.2.1 Option descriptors
A block may accept options (§3.4). So that a template-builder UI can offer them without knowing any engine, an engine can describe the options one of its blocks accepts. A description is a list of option descriptors:
| Field | Meaning | Constraints |
|---|---|---|
key | Field name in the options object | Stable per block; renaming one is a break, like a block id. |
label | Human-facing control label | Plain text, engine-authored. |
help | One or two sentences explaining the option | Plain text, engine-authored. |
kind | What shape the value takes, and its bounds | Below. |
unit | Display unit text, or absent | Display text only — never a unit system (§3.3). |
kind is one of: number (optional default, optional inclusive
minimum and maximum), integer (same), boolean (optional default),
text (optional default), number list (optional default, optional
minimum length, and a flag requiring strict ascent — threshold edges),
choice (one of a supplied list of items), or multi-choice (any
subset of one). A choice item is an opaque value plus a label for
display.
Descriptions are resolved against a model, not fixed by the catalog. The catalog (§3.2) is static and model-free, because listing blocks must not require a loaded model. Options are the opposite: their permissible values and their correct defaults are frequently properties of the model in hand — which constituents exist, which land uses, and what unit system the file declares. An engine therefore describes a block’s options given that block’s id and the model, exactly as it produces a fragment given the model (§3.4). Only the description vocabulary lives in this layer; the model type is the engine’s own and is never named here.
This is why descriptors carry values rather than pre-rendered text: an
engine resolving minPressure for a US-customary model returns a default
of 20 with unit psi, and for an SI model 14 with unit m. A consumer
displays what it is given and computes nothing.
A description is advisory. It tells a UI what to offer; it is not the validation authority. Production (§3.4) validates independently and remains the sole judge of a malformed options value, so an engine is free to accept values no description advertised, and a consumer that skips the description entirely — as a template authored by hand does — is unaffected. Describing no options for a block means a UI offers none, not that none are accepted.
§3.3 Fragment model
Fragments are neutral data — no colors, fonts, page geometry, or format hints. A fragment is a titled sequence of items; each item is one of:
| Item | Shape | Notes |
|---|---|---|
| Key-value list | Ordered pairs of (label, value) | For scalar summaries (“Total demand”, “Simulation duration”). |
| Table | Column descriptors + row-major values | Column descriptor: name, optional unit text, value kind. |
| Note | Plain text paragraph | For caveats and methodological remarks (e.g. “Convergence relaxed at 3 timesteps”). |
| Chart | Axis labels/units + chart data (below) | Declarative data only — engines describe what is charted, never colors, geometry, or layout. |
Chart data is one of:
- Bar — parallel category labels and values (distributions, rankings). Single-series in this revision.
- Line — one or more named series of (x, y) points over a continuous x axis (time series).
Every chart must be table-derivable: renderers without graphics support present the chart as a data table derived mechanically from its data (bar → category/value rows; line → x column plus one column per series, absent where a series lacks that x). A chart therefore never gates information behind a graphics-capable format.
Values are typed: number (with optional unit text), integer, boolean,
text, timestamp, or absent. Unit strings are display text in v1; a
structured unit system in common is an explicit non-goal (§1).
Nested sections and images are deferred to a later revision.
§3.4 Production
An engine produces a fragment given:
- a block
idfrom its catalog, - the artifacts of one completed simulation (results and derived analytics — the engine defines internally what it needs), and
- an optional options value: JSON-shaped structured data whose
meaning is defined entirely by the producing engine (thresholds,
top-N counts, tolerances). The foundation layer and the report layer
treat it as fully opaque — carrying it, never interpreting it. An
absent options value means the engine’s documented defaults; a
malformed options value fails production with the
failederror naming the problem. No option vocabulary may be defined in this layer.
Production is read-only and deterministic: the same simulation artifacts, block id, and options always yield the same fragment. Production fails with one of three neutral, typed errors:
- unknown block — the id is not in this engine’s catalog;
- unavailable — the block does not apply to this run, with a human-readable reason supplied by the engine, written as a complete sentence because a consumer may show it standing alone rather than after a label (e.g. “The run has no water-quality results.”); an expected condition, not a fault;
- failed — reading or deriving from the simulation artifacts failed.
The report layer decides how an unavailable or failed block renders (placeholder, omission) — the engine never does, and the contract carries no engine vocabulary for why beyond the engine-authored reason text.
Block options arrived in v1.1 as production inputs only. Since v1.3 an engine can additionally describe the options a block accepts (§3.2.1), so a template-builder UI can offer them generically. Production is unchanged by this: it validates the options value it is given regardless of what was described, and a hand-authored template that never consults a description behaves exactly as before.
§3.5 Consumers and dependency rules
| Layer | May depend on | Must not depend on |
|---|---|---|
common | nothing in the workspace | — |
engine-* | common | the report layer, applications |
| report layer | common | any engine-*, applications |
| applications (CLI, GUI) | everything above, via the umbrella | — |
Applications are the composition root: they obtain catalogs and fragments from engines and hand fragments to the report layer for rendering. The report layer never invokes an engine; engines never render.
§4. Evolution
- All v1 contracts evolve additively; fields are added, never repurposed.
- The deferred contracts (element schema, units, simulation surface) will arrive as new modules of this layer with their own spec sections, gated on a second engine implementation existing to validate them. Their arrival must not require changes to the v1 identity or report contracts.
- If a future revision must break a v1 contract, the break follows the library release track’s semver discipline.
Structs§
- Block
Descriptor - Descriptor of one block in an engine’s catalog (spec §3.2).
- Chart
- A declarative chart (spec §3.3): data plus axis labels only — engines describe what is charted, never colors, geometry, or layout. Every chart is table-derivable so it never gates information behind a graphics-capable format.
- Choice
Item - One selectable item of a
OptionKind::ChoiceorOptionKind::MultiChoice(spec §3.2.1). - Column
- Column descriptor of a table (spec §3.3).
- Engine
Descriptor - Immutable identity of one Hydra engine (spec §2.1).
- Fragment
- The materialized content of one block for one completed simulation (spec §3.1): a titled sequence of items.
- Import
Format - One source-model file format an engine imports (spec §2.2).
- KeyValue
- One (label, value) pair in a key-value list (spec §3.3).
- Line
Series - One named series of (x, y) points in x order (spec §3.3).
- Option
Descriptor - Description of one option a block accepts (spec §3.2.1).
- Table
- Column descriptors plus row-major values (spec §3.3).
- Unknown
Engine Error - Lookup failure for
engine_by_key.
Enums§
- Block
Error - Failure producing a block (spec §3.4). The report layer decides how an unavailable or failed block renders (placeholder, omission) — the engine never does, and the contract carries no engine vocabulary for why beyond the engine-authored reason text.
- Chart
Data - Chart data (spec §3.3).
- Engine
Status - Whether a registered engine is implemented in this distribution (spec §2.3).
- Fragment
Item - One item of a fragment (spec §3.3).
- Option
Kind - Shape and bounds of one describable block option (spec §3.2.1).
- Recognition
- How strongly an engine claims a candidate model as its own (spec §2.5).
- Value
- One typed value inside a fragment (spec §3.3). Unit strings are display text — a structured unit system in this layer is an explicit non-goal (spec §1).
- Value
Kind - Kind of a
Value, used in column descriptors (spec §3.3).
Constants§
- ENGINES
- Every engine compiled into this distribution, in presentation order (spec §2.4) — planned engines included, so applications can present the full modelling scope rather than only what ships today.
- HYDRA_
COMMON_ VERSION - The crate version, taken from
Cargo.tomlat compile time.
Functions§
- engine_
by_ key - Resolve an engine key to its descriptor (spec §2.2).