hydra-common 10.0.0

Hydra foundation contracts — engine identity and reportable-output interfaces shared by all engines and applications
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
# Hydra Common — Foundation Contract

Status: **v1.10 — 2026-08-09** (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; v1.5 — with a
second engine implemented and able to validate them — added the element
taxonomy contract (§4), the quantity contract (§5), the result-variable
contract (§6), and the run-dispatch layering rule (§2.6); v1.6 added the
optional compact symbol on variable descriptors, §6.1; v1.7 let fragment
numbers, table columns, and chart axes reference quantity keys, §3.3,
joining the fragment model to the quantity contract so consumers format
tagged values in a chosen display family, §5; v1.8 added the
engine-authored category on block descriptors, §3.2; v1.9 — with a second
engine's criteria implemented to validate it — added the criteria
contract, §7, moving Evolution to §8; v1.10 joined the two: a `banded`
variable now names the criterion its thresholds come from, §6.1, and a
criterion says what each of its regions means, §7.2 — without which a
threshold scale could only be offered to variables an application
recognised by name).
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.

The layer defines five contracts:

1. **Engine identity** — what an engine *is*, including how it is
   recognised from a model's bytes (§2).
2. **Reportable output** — what an engine can contribute to a report (§3).
3. **Element taxonomy** — how an engine describes its model's element
   vocabulary so an application can present any engine's model (§4).
4. **Quantities** — how an engine declares the physical quantities its
   values carry, so applications can format and convert them (§5).
5. **Result variables** — how an engine describes the per-element result
   series a completed simulation carries (§6).

Contracts 3–5 were **explicit non-goals for v1.0** (ratified 2026-07-28):
abstracting them from a single implementation risked baking
water-distribution assumptions into the foundation, so they were deferred
until a second engine implementation existed to exercise them. The urban
drainage engine is that second implementation, and its model — in
particular the subcatchment, which is neither a node nor a link — is the
stress test these contracts were shaped against.

**Still deferred:** a cross-engine simulation contract (a neutral session
type every engine implements). Two engines with genuinely different run
shapes are not yet evidence of the right trait; §2.6 assigns *where* the
uniform run surface lives without defining it here. Nothing in this layer
may presuppose the shape that future contract 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.

The registry holds three registered engines — `wds` (available), `uds`
(available), and `och` (planned) — in that order. `uds` shipped planned
through v1 and became available with its engine implementation.

### 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:

1. Exactly one `definite` — that engine owns the model.
2. 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.
3. No `definite`, one or more `plausible` — ambiguous, however few
   engines answered that way.
4. 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.

### 2.6 Run dispatch

Running a model is engine-owned, and each engine's run has its own shape:
one engine solves in phases and streams results as they become final;
another steps a single cascade and writes results when it completes.
Knowing those shapes — "how do I drive engine X from bytes to a results
file?" — is per-engine knowledge of exactly the kind §2.5.2 forbids
applications from holding, and for the same reason: an application that
encodes it duplicates it in every interface, and the copies drift.

The **uniform run surface** — open a model for its engine, advance it,
observe progress, persist its results, collect its warnings — therefore
belongs to the same both-seeing dispatch layer as routing (§2.5.2), and
every application drives every engine through that one implementation.

This section deliberately assigns *where* that surface lives and no more.
Its concrete shape is the dispatch layer's own, documented with its
implementation, because a neutral session contract in this layer remains
an explicit non-goal (§1): it would have to be abstracted from two run
shapes that genuinely differ, and this layer must not freeze a guess. When
a later engine proves the common shape, the surface graduates here as a
new contract, additively (§8).

---

## 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. |
| `category` | Engine-authored grouping heading | Plain text, short (one or two words). Blocks sharing the exact string belong together. |

`category` lets a consumer with many blocks on screen group them — as
tabs, section headings, or not at all; the choice is the consumer's.
Group order is catalog order: a category first appears where its first
block does. The string is display text with **no foundation-defined
vocabulary** — two engines using the same word ("Summary") are not
thereby related, exactly as with quantity keys (§5).

The descriptor otherwise 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; a category is not that — it is an
opaque engine-authored label carrying no semantics the foundation or the
report layer can act on beyond equality.

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; a structured
unit system in `common` remains an explicit non-goal (§1).
Nested sections and images are deferred to a later revision.

**Quantity-tagged numbers** (v1.7). A number value, a table column, and a
chart axis may each additionally reference a **quantity key** from the
producing engine's quantity catalog (§5). The tag changes what the number
*is*:

- A tagged value is expressed in that quantity's **SI display unit**  the same convention §5 already fixes for every quantity-bearing value
  crossing an engine boundary. Its unit text, when present, is the
  quantity's SI label. The producer performs no display formatting
  beyond this; choosing a display family is the consumer's decision,
  which is the reason the tag exists.
- A consumer holding the producing engine's quantity catalog may
  re-express the value in either display family using only the
  descriptor: convert by the affine map, label from the family's unit
  text, round by the family's advisory decimals. The catalog reaches
  such a consumer from the application, which is the composition root
  (§3.5) — this layer still never resolves a key to a descriptor itself.
- A consumer holding no catalog renders the value and its unit text
  as written. A fragment therefore remains self-describing: the tag
  refines presentation, it never gates content.
- An untagged number is what every number was before v1.7:
  engine-authored display text, rendered as given. Tags are per-value
  facts, not a fragment-wide mode — one table may carry tagged and
  untagged columns side by side (a flow column beside an
  engine-spelling text column, a count beside a pressure).

A column's tag applies to every number in that column; a chart axis's tag
applies to that axis's coordinates in the chart data. Keys are opaque and
engine-scoped exactly as in §5; a tag naming a key the engine's catalog
does not declare is a producer defect, and consumers treat the value as
untagged rather than failing the fragment.

### 3.4 Production

An engine produces a fragment given:

- a block `id` from 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 `failed` error
  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
deliberately takes **no display-family input**: a producer emits
quantity-tagged values in SI display units (§3.3) and untagged values as
engine-authored text, and which family a reader sees is decided where the
fragment is presented, not where it is produced. 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. Element Taxonomy Contract

The contract by which an engine describes its model's element vocabulary,
so that an application can enumerate, render, and inspect *any* engine's
model without knowing what a junction or a subcatchment is. It follows the
same discipline as recognition (§2.5) and reportable output (§3):
**engine-specific meaning travels only through opaque ids and
engine-authored text**; this layer contributes structure, never domain
vocabulary.

### 4.1 Element classes

The single piece of structural vocabulary this layer owns is the **element
class** — the geometric and referential nature of an element, which an
application must know to render and organise it, and which is genuinely
engine-independent:

| Class | Nature | Application obligations |
|---|---|---|
| `point` | A located element: one coordinate. | Render as a marker; selectable; may anchor `polyline` ends and `region` outlets. |
| `polyline` | A connecting element: references a from-`point` and a to-`point`, with optional intermediate vertices. | Render as a line/path between its endpoints; selectable. |
| `region` | An areal element: a polygon boundary, with an optional reference to a `point` element it discharges to. | Render as a filled polygon; selectable; the discharge reference may be visualised as a connector. |
| `collection` | A non-spatial named object (a curve, a pattern, a time series, a control). | Enumerable and countable; presentation is application-defined and may be engine-specific. |

The class list is closed **in this revision**; extending it is an additive
spec change here, not an engine decision. A subcatchment is the proof case
for `region`: it is neither a node nor a link, and any taxonomy that
offered only those two classes would have baked one engine family's shape
into the foundation.

### 4.2 Element kinds

Within those classes, an engine describes its **kinds** — junction, tank,
conduit, subcatchment, rain gage — as an ordered catalog of descriptors:

| Field | Meaning | Constraints |
|---|---|---|
| `id` | Stable kind identifier | Opaque to this layer; stable per engine — persisted data and application preferences may reference it. |
| `label` | Human-facing singular name | Plain text, engine-authored. |
| `label_plural` | Human-facing plural name | Plain text, engine-authored. |
| `class` | The kind's element class (§4.1) | One of the four classes. |
| `role` | What the kind does in the network (§4.3) | One of the three roles, or absent. |
| `badge` | Short glyph for dense UI (markers, chips) | One or two characters, engine-authored. |

The catalog is static and model-free, like the block catalog (§3.2): an
application must be able to build its chrome — tables, filters, layer
toggles, legends — before any model is loaded. Kind ids follow the block-id
stability rule: removing one, or changing the *meaning* of one, is a break
on the order of a file-format break.

**Identity:** every element carries an engine-scoped string identifier.
This layer requires only that the pair (kind id, element id) is unique
within a model; whether identifiers are additionally unique across kinds
(as they are within one engine's node family) is the engine's own rule,
expressed through its validation, not through this contract.

### 4.3 Element roles

A class says what an element *is* geometrically. A **role** says what it
does in the network:

| Role | Meaning |
|---|---|
| `conveyance` | Carries flow without imposing a boundary or a control on it — a junction, a pipe, a conduit. The bulk of any model. |
| `boundary` | Where the model meets what it does not simulate: a fixed head or stage, a storage volume, an outfall. Flow enters or leaves the modelled system here. |
| `control` | Acts on the flow rather than merely passing it — a pump, a valve, a weir, an orifice, a flow divider. |

Role exists because it is the distinction an application must draw to
present an *unsimulated* model at all. Before any results exist there is
nothing to colour by, and a network drawn in one uniform tone tells a
reader nothing; what they need to see is where the system is fed and
drained, and where something acts on the flow. Class cannot answer that —
a pump and a pipe are both `polyline`, a reservoir and a junction both
`point` — and kind cannot either without the application naming kinds it
should not know.

**A kind may have no role at all.** A rain gage is located but conveys
nothing; a curve, a pattern and a control rule are not in the flow network
to begin with. Those declare no role, and an application draws them by
whatever means suits — the absence is information, not an omission to be
defaulted away.

The role list is closed in this revision, and extending it is an additive
spec change here rather than an engine decision, exactly as the class list
is. Roles carry no presentation: an application decides what a boundary
looks like, and this layer decides only which kinds are boundaries.

> **Assignment is the engine's judgement, not a lookup.** A storage unit is
> a boundary in drainage because it is where volume leaves the routed
> network, while a tank is a boundary in distribution for the same reason
> expressed differently. Where a kind is arguably two roles, the engine
> picks the one an application should draw it as.

### 4.4 Attribute schemas

For each kind, an engine describes the attributes an application may
display for elements of that kind — an ordered list of attribute
descriptors reusing the option-descriptor vocabulary of §3.2.1:

| Field | Meaning | Constraints |
|---|---|---|
| `key` | Field name in the element's attribute data | Stable per kind; renaming one is a break, like a block id. |
| `label` | Human-facing name | Plain text, engine-authored. |
| `kind` | Value shape and bounds | The §3.2.1 kinds, unchanged. |
| `quantity` | Key of the physical quantity the value carries (§5), or absent | Absent means dimensionless or textual. |

An attribute schema is advisory in exactly the §3.2.1 sense: it tells a
generic UI what to show; it is not the validation authority, and an engine
remains free to hold data no schema advertises. This revision defines
attribute schemas for **display**. Editability, defaults, and creation
flows are a later additive revision — describing them before a second
engine's editor exists would repeat the mistake §1 warns against.

---

## 5. Quantity Contract

Fragments carry unit strings as display text (§3.3) because a rendered
report needs no arithmetic. Live applications do: they let the user choose
a display unit system, format values in it, and accept input in it. The
quantity contract is how an engine declares the physical quantities its
values carry so that applications can do that generically.

An engine publishes a static catalog of **quantity descriptors**:

| Field | Meaning | Constraints |
|---|---|---|
| `key` | Stable quantity identifier | Opaque to this layer; referenced by attribute schemas (§4.3) and result variables (§6). |
| `si_label` | Unit text in the SI display system | Plain text, e.g. "m", "L/s", "mm/hr". |
| `us_label` | Unit text in the US-customary display system | Plain text, e.g. "ft", "gpm", "in/hr". |
| `si_to_us` | Affine conversion from SI display value to US display value | A scale factor and an offset (offset 0 for all but temperature-like quantities). |
| `si_decimals` / `us_decimals` | Suggested display precision per system | Advisory formatting hints. |

Values crossing an engine boundary for a quantity-bearing field are **in
that quantity's SI display unit**; the application converts for display
and converts back on input, using only the descriptor. Engines never
format, and applications never hardcode a conversion — the descriptor is
the single authority, so a quantity this layer has never heard of (a
rainfall intensity, an infiltration rate) costs an application nothing to
support.

Quantity keys are engine-scoped: two engines may both declare a `flow`
quantity, and nothing requires their descriptors to agree, because no
value ever crosses between engines. The catalog is static; which
attributes and variables *reference* which quantities is declared where
those are declared (§4.3, §6).

This contract deliberately does not model unit *systems* beyond the two
display families applications offer. Since v1.7 it reaches into the
fragment model: a fragment number, table column, or chart axis may
reference a quantity key (§3.3), in which case the value is in the
quantity's SI display unit and formatting for a display family belongs to
whichever consumer presents it — the report layer at render time, a live
application at display time. "Engines never format" thereby holds for
fragments too: a producer that tags a value stops choosing its display
family, and the engine-side conversion code that used to make that choice
is deleted, not parameterised.

---

## 6. Result-Variable Contract

The contract by which an engine describes the per-element time-series
variables a completed simulation carries — pressure, flow, depth,
runoff — so an application can offer result exploration (map colouring,
legends, per-element series, period scrubbing) for any engine.

### 6.1 Variable descriptors

For each element class it produces results for (§4.1), an engine publishes
an ordered catalog of variable descriptors:

| Field | Meaning | Constraints |
|---|---|---|
| `id` | Stable variable identifier | Opaque to this layer; application preferences and saved views may reference it. |
| `label` | Human-facing name | Plain text, engine-authored. |
| `symbol` | Compact notation for space-starved surfaces (column headers, chips), or absent | Engine-authored, at most three characters, ideally the domain's standard notation (Q for discharge, y for depth, Ø for diameter). Absent means the application derives its own fallback, e.g. the label's initial. |
| `quantity` | Key of the quantity the values carry (§5), or absent | Absent means dimensionless. |
| `ramp` | How values are meaningfully mapped to a colour scale | One of the ramp hints below. |

Ramp hints are the only presentation vocabulary this layer contributes,
and they are shape statements, never colours:

| Hint | Meaning |
|---|---|
| `sequential` | Magnitude on a continuous low→high scale. |
| `diverging` | Signed values around a meaningful zero (e.g. flow direction). |
| `banded` | Values classed against a criterion's threshold bands (§7); the hint carries the `criterion` key whose valuation supplies them. |
| `categorical` | A closed set of discrete states; the descriptor carries the engine-authored items described below, as a §3.2.1 choice does. |

An application chooses palettes and legend styling; the engine says only
which shape is truthful for the data — and, for `banded`, which criterion
the thresholds come from.

That last part is not decoration. A banded variable without it is
uninterpretable: an application holding a valuation of several criteria
cannot tell which of them bands *this* variable, and matching them by
quantity is a guess — two criteria may share a quantity, and two engines
may publish a variable of the same name meaning different things. The
consequence of guessing was observed: a drainage map was offered a
threshold scale annotated with water-distribution numbers. The criterion
named here must exist in the same engine's criteria catalog (§7.1) and
must carry severities (§7.2); an application that cannot resolve it
renders the variable as a plain magnitude rather than inventing bands.

#### Categorical items

Each item of a `categorical` variable carries:

| Field | Meaning | Constraints |
|---|---|---|
| `value` | The number the result series stores for this state | Engine-authored; unique within the variable. |
| `label` | Human-facing name for the state | Plain text, engine-authored. |
| `severity` | Whether the state is unremarkable, worth attention, or wrong, or absent when the states carry no such judgement | One of `nominal`, `caution`, `alarm`. |

Severity is a statement about the *domain*, not about presentation: a
closed pipe is an abnormal condition in a pressurised network whoever is
looking at it, and only the engine knows that. Without it an application
can order states but cannot rank them, so it must colour a closed pipe and
an open one as merely *different* — losing a distinction the engine
already held. It stays optional because it is a real claim: a state set
that is genuinely just a partition (a land-use class, a material) must not
be forced to invent a judgement, and absent means exactly that.

As with every hint here, this fixes no colours. An application decides
what caution and alarm look like, and remains free to ignore severity
entirely.

### 6.2 Presence

Not every catalog variable exists in every run — a quality variable is
absent from a run with quality disabled. An engine therefore reports,
**for a given completed simulation's results**, which of its catalog
variables are present, resolved the way block options are resolved against
a model (§3.2.1): the catalog stays static, presence is per-run. An
application offers only present variables and treats an absent one the way
the report layer treats an unavailable block — an expected state, not an
error.

### 6.3 Addressing

Consumers address results by (element class, variable id, reporting
period), and per-variable minimum/maximum envelopes are addressed by
(element class, variable id). Wire encodings, caching, and file formats
are the consumer's own concern and are not part of this contract — but
they must be derived from the catalog rather than fixing a variable list,
or they re-create the closed-set coupling this contract exists to remove.

---

## 7. Criteria Contract

The contract by which an engine describes — and consumes — the
**assessment criteria** a user asserts over a model's simulated
behaviour: minimum service pressure, a self-cleansing velocity, a
freeboard allowance. Criteria are engineering judgements about the
network, not display settings and not part of the model; they belong to
the person assessing, travel with a project, and outlive any single run.

### 7.1 Concepts

| Term | Meaning |
|---|---|
| **Criterion** | One field of the assessment standard, described by the engine. |
| **Criteria catalog** | The engine's complete, static, model-free list of criterion descriptors. |
| **Valuation** | A caller-held assignment of values to criterion keys. |

The foundation stays engine-blind, exactly as in §4–§6: criterion keys
are opaque, meaning travels only through engine-authored text, and no
criterion vocabulary (pressure, freeboard, anything else) enters this
layer.

### 7.2 Criterion descriptor

| Field | Meaning | Constraints |
|---|---|---|
| `key` | Stable criterion identifier | Unique within the engine; persisted by applications, so renaming one is a break. |
| `label` | Human-facing name | Plain text, engine-authored. |
| `help` | One or two sentences on what the criterion judges | Plain text, engine-authored. |
| `quantity` | §5 quantity key, or absent | Values of this criterion are expressed in the quantity's **SI display unit**; absent means dimensionless. |
| `kind` | Shape of the value | Below. |
| `severities` | What each region between the cut points means, ascending, or empty | Empty means the criterion is judged in reports but never drawn. When present, exactly one more entry than the criterion has cut points: a **value** criterion has one cut and so two regions, a **band** of *n* cuts has *n+1*. Each entry is `nominal`, `caution` or `alarm` — the §6.1 vocabulary, so a compliance verdict and a categorical state read alike. |

`kind` is one of:

- **value** — a single number, with a required `default`;
- **band** — an ordered list of named cut points, each `{key, label,
  default}`, defaults strictly ascending. A band's value is a same-length
  list of numbers, ascending in the same order.

Severities are a claim about the domain, and the reason they are the
engine's to make is that compliance is rarely monotonic. Service pressure
is worst when too low, acceptable in a middle, and worth attention again
when too high; conduit velocity is worth attention when too slow and wrong
when too fast. An application given only the numbers would have to decide
which end is bad, and it has no basis for that in either direction.

Defaults are the engine's judgement of a conventional standard; they are
advisory for editors and binding for consumption (§7.4).

### 7.3 Valuation

A valuation is a JSON object: criterion key → number (value kind) or
array of numbers (band kind), every number in the criterion's SI display
unit. A key absent from the valuation means the criterion's defaults; a
key the catalog does not declare is ignored, so a persisted valuation
survives catalog growth. A value of the wrong shape or holding a
non-finite number is **malformed**, and consumption refuses it with a
message naming the criterion. A band value out of ascending order is
well-formed but **degenerate** — an editor mid-edit produces one
transiently, so it must not poison the whole valuation; consumption
handles it per §7.4.

### 7.4 Consumption

An engine derives **per-block options** from a valuation: given a
valuation and a model, it answers with an options object (§3.2.1 shapes)
for each of its criteria-shaped blocks. This mapping is the engine's
own — which blocks a criterion drives, and in what units their options
are expressed, is engine knowledge that never leaks to the caller. A
criterion no block consumes may still be cataloged: applications judge
with criteria in more places than block production (a map colour scale),
and the catalog is the single description of the standard.

An engine omits a block from its answer when the valuation cannot shape
it (a degenerate band, §7.3); the block then runs on its documented
option defaults. Consumption of a well-formed valuation never fails.

### 7.5 Persistence and dependency rules

Persistence is the application's concern: where a valuation lives, and
per what scope (a project, a scenario), is not this contract's business.
The layering of §3.5 applies unchanged: engines depend on this crate,
applications compose catalogs, valuations, and production, and this
crate depends on nothing.

## 8. Evolution

- All contracts evolve **additively**; fields are added, never
  repurposed.
- The element, quantity, and result-variable contracts (§4–§6) arrived in
  v1.5 exactly this way: as new sections, gated on a second engine
  implementation existing to validate them, requiring no change to the
  identity or report contracts. The one remaining deferred contract — a
  neutral simulation session — follows the same path when a further engine
  proves its shape (§2.6); until then only its dispatch home is assigned.
- Known additive follow-ups already anticipated: editability, defaults,
  and creation flows on attribute schemas (§4.3), and additional element
  classes (§4.1) should an engine need one.
- If a future revision must break a contract, the break follows the
  library release track's semver discipline.