Chapter 5

Chapter 5: Forecasting Protocol — Models, Ensembles and Alert Decisions

Chapters 2 to 4 described what flood data looks like and how it moves over the API. This chapter covers what happens in between: how a forecasting node turns observations into a flood prediction, how several models are combined into one ensemble, how forecast quality is measured, and how the decision to warn the public is made and carried out. It follows PHASE-3 (Protocol) §6–7 of WIA-ENE-033 and the helper functions of the reference TypeScript SDK.

5.1 Pipeline Roles and the Messages Between Them

PHASE-3 defines its scope in one sentence: it covers the protocols "between gauge networks, forecasting nodes, warning publishers, and downstream consumers." These four roles are the backbone of the chapter. They are logical roles, not products. One agency may run all four in a single system, or a national hydrological service may operate the gauge network and forecasting node while a municipal disaster-safety office acts as warning publisher.

PHASE-3 states that it "reflects the message types" defined in the WMO and OGC documents it references but "does not redefine them." It also does not prescribe a wire-level handshake for each hop. The sequence below is therefore this book's reading of how the ENE-033 objects line up with the roles; it is a practical interpretation, not a normative diagram from the spec.

Gauge network        Forecasting node          Warning publisher         Downstream consumer
     |                      |                          |                           |
     |-- RiverLevelData --->|                          |                           |
     |-- PrecipitationData->|                          |                           |
     |-- DamStatusData ---->|                          |                           |
     |-- DrainageData ----->|                          |                           |
     |                      |  run 1D / 2D / AI models |                           |
     |                      |  build EnsemblePrediction|                           |
     |                      |-- FloodPredictionResult->|                           |
     |                      |   (+ ensemble summary)   |                           |
     |                      |                          |  apply AlertCriteria      |
     |                      |                          |  manualReview? -> duty    |
     |                      |                          |  officer decides          |
     |                      |                          |-- FloodAlert / CAP 1.2 -->|
     |                      |                          |<-- delivery receipts -----|
     |                      |                          |   (AlertDistribution.status)
     |                      |                          |                           |
     |<================ new observations every cycle; the loop repeats =================|
HopMain ENE-033 object(s)Defined inWhat must travel with it
Gauge → forecasting nodeRiverLevelData, PrecipitationData, DamStatusData, DrainageDataPHASE-1 (Chapter 2)Timestamps in UTC, explicit units, station identifiers, the datum of each water level
Forecasting node → warning publisherFloodPredictionResult, EnsemblePredictionPHASE-1, PHASE-3 §6.3Probability, confidence, lead time, the model versions that produced the run
Warning publisher → consumerFloodAlert (emitted as CAP 1.2)PHASE-3 §7.2 (Chapter 3)Issuer identity, affected area geometry, onset and expiry, bilingual instructions
Consumer → warning publisherAlertDistribution.statusPHASE-3 §7.3Sent, delivered, failed and confirmed counts per channel

The last row matters more than it seems. A warning chain that never learns whether its messages arrived cannot tell a successful warning from a silent failure. Section 5.8 returns to this.

One practical consequence of splitting the roles is that each hop can fail independently, and each should degrade in a known way. If gauge data stops arriving, the forecasting node should mark its next run as based on stale inputs rather than silently reusing the last value. If the forecasting node is unavailable, the warning publisher still holds its own authority and can issue a warning from observed levels alone. If one distribution channel is down, the others carry on. Writing these fallback rules down, per hop, is part of implementing the protocol even though PHASE-3 does not list them.

5.2 The 1D River Model: Saint-Venant Equations

For flow along a river channel, PHASE-3 §6.2.1 specifies the one-dimensional Saint-Venant equations. They describe how a flood wave travels and changes shape between gauges, and they are the standard physical basis for river routing.

Continuity:  ∂A/∂t + ∂Q/∂x = q

Momentum:    ∂Q/∂t + ∂(Q²/A)/∂x + g·A·∂h/∂x + g·A·S_f = 0
SymbolMeaningUnitWhere it comes from in practice
AWetted cross-sectional aream²Surveyed cross-sections as a function of stage
QDischargem³/sModel state; compared with RiverLevelData.current.flowRate
hWater surface levelmModel state; compared with current.waterLevel
gGravitational acceleration9.81 m/s²Constant
SfFriction slopedimensionlessResistance law, commonly Manning's equation with a calibrated roughness n
qLateral inflow per unit lengthm³/s/mTributaries, local runoff, urban drainage outfalls
x, tDistance along the channel, timem, sModel grid

The continuity equation says that water is neither created nor lost: if more water enters a reach than leaves it, the cross-section fills and the level rises. The momentum equation balances the local and convective acceleration of the flow against the pressure gradient (the slope of the water surface) and friction. When the friction slope is written with Manning's law, it becomes Sf = n²Q|Q| / (A²R4/3), where R is the hydraulic radius. The roughness n is the main calibration parameter of most river models.

Many operational systems use simplified forms. Dropping the two acceleration terms gives the diffusive-wave approximation; also dropping the pressure term gives the kinematic wave, in which the water surface is assumed parallel to the bed. The simplifications are cheaper and stable, but they cannot represent backwater — for example a tributary held up by a high main river, or a tidal reach. ENE-033 does not force a particular solution method. The four-point implicit Preissmann scheme is a common choice for the full equations; explicit schemes are simpler but must respect the Courant stability condition, which ties the time step to the grid spacing and wave speed.

The data objects of Chapter 2 feed the 1D model in well-defined places:

5.3 The 2D Inundation Model: Shallow-Water Equations

Once water leaves the channel, or when rain ponds in a city faster than drains can remove it, flow is no longer confined to one direction. PHASE-3 §6.2.2 specifies the two-dimensional shallow-water equations for this case.

∂h/∂t + ∂(hu)/∂x + ∂(hv)/∂y = R

∂(hu)/∂t + ∂(hu²)/∂x + ∂(huv)/∂y = −g·h·∂z/∂x − τ_x

∂(hv)/∂t + ∂(huv)/∂x + ∂(hv²)/∂y = −g·h·∂z/∂y − τ_y

Here h is water depth (m), u and v are depth-averaged velocity components (m/s), z is ground elevation (m), R is the rainfall source term (m/s) and τx, τy are bed shear stresses. In the form given in the spec, the stress terms should be read as kinematic stresses (bed shear stress divided by water density), otherwise the units do not match. Readers who implement their own solver should also note that the equation as printed contains only the bed-slope term −gh∂z/∂x. Many solvers write the pressure term separately as ∂(½gh²)/∂x; the two forms are equivalent only when the equation is rearranged consistently, so check which convention a solver uses before comparing results.

The 2D model produces a depth grid for every output time step. PHASE-3 §6.1.1 and the FloodPredictionResult object define what must be derived from it:

Output fieldHow it is derived from the depth grid
inundationArea.estimatedArea (km²)Sum of cell areas where maximum depth exceeds a wet/dry threshold. The threshold is an implementation choice and must be declared (for example 0.05 m).
inundationArea.maxDepth (m)Largest depth over all cells and time steps
inundationArea.averageDepth (m)Mean of maximum depth over wet cells
inundationArea.geoJsonPolygon(s) traced around the wet cells, simplified to a tolerance the consumer can render
inundationArea.affectedPopulation, affectedBuildingsOverlay of the wet polygon with population and building layers

Two limits apply to v1.0. The release notes state that inundation models relying on lidar-derived DEMs with horizontal resolution finer than 1 m are out of scope, and that storm-surge coupling is a roadmap item for v1.1. In practice, this means an ENE-033 v1.0 system can model river and urban inundation on a DEM of 1 m or coarser, but a coastal flood driven by surge together with river discharge is not covered by the standard's model definitions. The coastal-flood event type still exists, so a publisher can warn for coastal flooding using an external surge model, but it should state in the alert metadata that the inundation extent did not come from an ENE-033 model.

Resolution interacts with run time. Halving the grid size in two dimensions gives four times as many cells, and for an explicit scheme the Courant condition also halves the time step, so run time grows roughly eightfold. This is why the resolution request field (coarse / medium / fine) matters operationally: a fine run may not finish inside the lead time it is meant to serve.

5.4 AI Models: LSTM, CNN and Attention

PHASE-3 §6.1.1 describes a data-driven model in three layers. The input layer takes past and forecast precipitation (the past 24 hours plus the forecast), river levels at key gauges, dam storage and release, terrain (elevation, slope, land use) and drainage facility status. The hidden layers combine an LSTM for temporal patterns, a CNN for spatial patterns, and an attention mechanism that weights the most influential factors. The output layer gives flood probability (0–100 %), the expected inundation extent as GeoJSON, maximum inundation depth and the peak time.

The specification names example values — "ten main gauge points" and "more than 30 years" of historical flood data. These should be read as examples of a reasonable configuration, not as requirements. A small catchment may have only three useful gauges; a newly urbanised basin may have only a decade of representative record.

The training data categories in §6.1.2 are: historical flood events, meteorological data (precipitation, wind, pressure), hydrological data (stage, discharge), terrain data (DEM) and infrastructure data (drainage, levees). Each category brings its own weakness, and three pitfalls deserve explicit treatment in any implementation.

5.4.1 Non-stationarity

A model learns the relationship between rain and river response that held in its training period. That relationship changes when a basin urbanises (more impervious surface, faster runoff), when a dam or levee is built, when a channel is dredged, or when the rainfall climate itself shifts. A model trained across such a change learns an average of two different rivers. Mitigations include training only on the period after the last major structural change, giving the model explicit inputs that describe the change (for example impervious fraction by year), and re-validating after every known intervention.

5.4.2 Rare-event imbalance

Floods above the danger level are rare by definition. In an hourly dataset, the "flood" class may be a tiny fraction of samples. A model optimised for overall error will learn to predict "no flood" almost always and still score well. Mitigations include weighting the loss toward high-flow samples, evaluating on event-based metrics (Section 5.6) rather than overall accuracy, and augmenting the training set with physically simulated extreme events from the 1D/2D models — with care, since the AI model then inherits the physical model's biases.

5.4.3 Leakage in hindcasts

A hindcast replays the past to test a model. Leakage happens when the hindcast gives the model information it would not have had in real time. The most common cases are: using observed rainfall as the "forecast" input instead of the rainfall forecast actually issued at the time; splitting time series randomly so that adjacent hours of the same event appear in both training and test sets; normalising inputs with statistics computed over the whole record; and using gauge data after quality control and revision, when the live system sees raw data. Each makes the hindcast look better than live operation will be. The safe practice is to split by whole events or by year, and to archive the forecast inputs exactly as they were available at issue time.

An AI model is a member of the ensemble, not a replacement for the physical models. Its strengths — speed, and the ability to learn local responses no physical model captures — are balanced by weak extrapolation beyond the range of its training data, which is exactly where the most dangerous floods lie.

Model familyStrengthTypical weaknessMain ENE-033 output
1D Saint-VenantPhysically based routing; handles dam releases and tributaries explicitlyNeeds surveyed cross-sections and calibration; no floodplain detailRiverLevelData.forecast
2D shallow-waterSpatial inundation extent and depthComputational cost; DEM quality limits accuracyinundationArea with GeoJSON
AI (LSTM + CNN + attention)Fast; learns local response from dataWeak extrapolation to unseen extremes; sensitive to regime changeProbability, peak time, extent
StatisticalSimple, transparent baselineCannot represent new conditionsProbability of threshold exceedance

5.5 Ensembles: Combining Models

PHASE-3 §6.3 defines EnsemblePrediction with three members — an AI model, a hydrological model and a statistical model — each returning a full FloodPredictionResult. It then asks for two summaries: an unweighted ensemble block (mean probability, median probability, confidence, uncertainty) and a weighted block (probability, inundation area, peak time) where the weights are based on model performance.

interface EnsemblePrediction {
  models: {
    ai: FloodPredictionResult;
    hydrological: FloodPredictionResult;
    statistical: FloodPredictionResult;
  };
  ensemble: {
    meanProbability: number;
    medianProbability: number;
    confidence: number;
    uncertainty: number;
  };
  weighted: {              // performance-weighted mean
    probability: number;
    inundationArea: number;
    peakTime: string;
  };
}

Note that EnsemblePrediction, like ModelPerformance, AlertCriteria and AlertDistribution later in this chapter, is defined in the PHASE-3 text but is not exported by the reference SDK's types.ts. Implementations that need these objects on the wire should define them from the spec and expect to reconcile names if a later SDK release adds them.

5.5.1 Skill-based weights

"Weighted by model performance" leaves the method open. A defensible choice is to derive each member's weight from its verified skill on past events in the same basin and season, using a score that matches what the weight will be applied to:

For example (illustrative numbers only): the three members give flood probabilities of 72 %, 58 % and 40 %, and past verification gives weights of 0.50 (AI), 0.35 (hydrological) and 0.15 (statistical). The unweighted mean is 56.7 % and the median is 58 %, while the weighted probability is 0.50 × 72 + 0.35 × 58 + 0.15 × 40 = 62.3 %. The difference is enough to cross a 60 % threshold, which is why the weighting method must be documented and stable, not tuned from run to run.

Three details need a decision that the spec does not make:

Three members is a small ensemble. It captures model structural uncertainty but not the uncertainty of the rainfall forecast itself, which is often the largest source of error beyond a few hours. Where a meteorological service provides ensemble rainfall forecasts, running the hydrological member once per rainfall member gives a much better picture of the range of outcomes. The ENE-033 structure can carry this by treating the hydrological member as the summary of such an inner ensemble.

5.6 Measuring Forecast Quality

PHASE-3 §6.1.3 defines ModelPerformance with eight fields:

FieldMeaningUnit
accuracyShare of all forecasts that were correct%
precisionShare of predicted floods that occurred%
recallShare of observed floods that were predicted%
f1ScoreHarmonic mean of precision and recallscore (the spec's target is written as a percentage)
rmseRoot mean squared error of a continuous variable, typically stageunit of the variable (m)
maeMean absolute error of the same variableunit of the variable (m)
leadTimeTime between forecast issue and eventhours
falseAlarmRateShare of false warnings (see the definition problem below)%

5.6.1 The contingency table

Categorical metrics come from a 2 × 2 contingency table that compares each forecast of "flood / no flood" (for a defined threshold, location and time window) with what happened.

Flood observedNo flood observed
Flood forecasta — hitsb — false alarms
No flood forecastc — missesd — correct negatives
Probability of detection   POD = a / (a + c)            (= recall)
False alarm ratio          FAR = b / (a + b)            (= 1 − precision)
Critical success index     CSI = a / (a + b + c)
Accuracy                   ACC = (a + d) / (a + b + c + d)
F1 score                   F1  = 2a / (2a + b + c)      (= 2·CSI / (1 + CSI))
Probability of false detection  POFD = b / (b + d)

There is a naming trap here. In forecast verification, the false alarm ratio (b / (a + b)) is the share of warnings that were false, while the probability of false detection, sometimes also called the false alarm rate, is b / (b + d) — the share of non-events that were wrongly flagged. The two can differ by orders of magnitude for rare events. ENE-033's falseAlarmRate does not say which it means. Implementations should publish the formula next to the number; this book recommends the false alarm ratio, because it is what the public experiences as "the warnings that turned out to be wrong."

5.6.2 Why accuracy misleads for floods

Consider an illustrative verification set of 1,000 hourly forecasts at one gauge with 10 flood hours. A model that always says "no flood" scores 99 % accuracy, with POD = 0. A useful model might produce a = 6, b = 14, c = 4, d = 976. Its accuracy is 98.2 % — lower than the useless model — while its POD is 60 %, FAR 70 % and CSI 25 %. Accuracy is dominated by the correct negatives, which are easy and numerous. For this reason event-oriented scores (POD, FAR, CSI) should lead any report, and accuracy should be quoted only with the base rate beside it.

The same example shows why a high POD target and a low false-alarm target pull against each other: warning earlier and more often raises POD and FAR together. Where the balance sits is a policy choice for the warning authority, not a model property.

The unit of verification also has to be fixed before any number is meaningful. Counting per hour, per gauge, per event or per administrative area gives very different values from the same forecasts. An event-based count — did a warning precede this flood by at least the required lead time, and did this warning have a matching flood — is closest to what the public experiences and is recommended for headline reporting, with hourly scores kept for model development.

5.6.3 Targets are design goals

PHASE-3 lists a targetPerformance of accuracy 85, precision 80, recall 90, F1 85, lead time 6 hours and false alarm rate 15. These are design targets stated by the spec, not measured results of any system. Two cautions apply. First, a precision of 80 % implies a false alarm ratio of 20 %, so a target of 15 % "false alarm rate" is only consistent if it means a different quantity (such as POFD) — another reason to fix the definition. Second, as the example above shows, an 85 % accuracy target says little for a rare-event problem. Any performance claim should state the verification period, the threshold, the number of events, and the formulas used.

RMSE and MAE complement the categorical scores for continuous quantities such as peak stage. RMSE penalises large errors more heavily and is therefore more sensitive to the one badly missed flood; MAE is more robust. Both should be computed on high-flow periods separately from all periods, for the same reason that accuracy misleads.

5.7 Alert Decision Rules

The warning publisher's first task is to decide whether a prediction becomes a warning. PHASE-3 §7.1 defines AlertCriteria with two parts:

interface AlertCriteria {
  automatic: {
    riskLevel: RiskLevel;          // LEVEL-2 or higher
    probability: number;           // 60 % or higher
    leadTime: number;              // minimum lead time (hours)
    affectedPopulation: number;    // minimum affected population
  };
  manualReview: {
    uncertaintyThreshold: number;
    criticalInfrastructure: boolean;
    historicalSignificance: boolean;
  };
}

The spec does not say how the two parts combine. A reading consistent with the field names, and the one this book recommends, is: issue automatically only when every automatic condition is met and no manualReview trigger fires; route to a duty officer when any trigger fires — the ensemble uncertainty exceeds its threshold, the affected area includes critical infrastructure (a hospital, a substation, a subway entrance), or the event would be historically significant (for example above the largest flood on record). Human review is a safeguard for the cases where automated logic is least reliable, and it keeps the legal authority to warn with the people who hold it.

The 60 % probability and LEVEL-2 thresholds are reference defaults written into the spec, not values validated for any particular basin. Each warning authority should set its own thresholds from local verification and the cost it attaches to misses versus false alarms. Note also that ENE-033's LEVEL scale is not the official Korean flood forecast (홍수주의보 and 홍수경보 issued by the flood control offices); Chapter 1 explains why the two must be crosswalked rather than treated as the same thing.

5.7.1 The SDK helper functions

The reference TypeScript SDK provides four helpers that implement a simple version of this logic.

FunctionRule in the reference implementation
calculateRiskScore(p, r, d, dr)0.3 × precipitation + 0.4 × river level + 0.2 × dam + 0.1 × drainage (each sub-score 0–100)
getRiskLevelFromScore(s)≥ 80 → LEVEL-4; ≥ 60 → LEVEL-3; ≥ 40 → LEVEL-2; ≥ 20 → LEVEL-1; otherwise LEVEL-0
isEvacuationRequired(level, prob)level ≥ LEVEL-3 and probability ≥ 60
estimateTimeToOverflow(current, danger, rise)(danger − current) / rise; null if rise ≤ 0 or current ≥ danger

The weights and cut-offs are reference-implementation defaults, not validated thresholds. The SDK also leaves open how each 0–100 sub-score is produced; a river-level sub-score, for instance, could be the current stage expressed as a fraction of the distance between the attention level and the danger level. Whatever mapping is used belongs in the deployment's documentation, because the final risk level depends on it as much as on the weights.

An example with illustrative sub-scores: precipitation 70, river level 85, dam 40, drainage 60 gives 21 + 34 + 8 + 6 = 69, which is LEVEL-3 (Warning). With an ensemble probability of 62.3 %, isEvacuationRequired returns true.

5.7.2 Time to overflow

A worked example, adapted from the README sample (which labels the 5.0 m threshold as a warning level; here it is treated as the danger level): the current level is 4.2 m, the danger level 5.0 m and the level is rising at 0.2 m/h. estimateTimeToOverflow returns (5.0 − 4.2) / 0.2 = 4 hours.

This is a linear extrapolation. It assumes the rate of rise stays constant, which is rarely true during the rising limb of a flood. RiverLevelData.trend also carries acceleration (m/h²). With constant acceleration a, the rise after time t is rt + ½at². Taking a = 0.02 m/h² as an example, 0.8 = 0.2t + 0.01t², which solves to about 3.4 hours — more than half an hour earlier than the linear estimate. Both are crude compared with a routed forecast; they are useful as a fallback when the forecasting node is unavailable, and as a sanity check against it. Two further points: the function's parameter is named dangerLevel, while physical overflow happens at bankHeight, so decide and document which threshold "time to overflow" refers to; and the helper returns null both when the river is falling and when it is already above the threshold — callers must check the current level to tell these apart.

5.7.3 A pitfall: comparing risk levels as strings

RiskLevel is a string enum whose values are 'LEVEL-0' to 'LEVEL-4'. isEvacuationRequired compares them with riskLevel >= RiskLevel.LEVEL_3. In JavaScript this is a lexical string comparison. It gives the right answer only because the five values share a prefix and differ in a single digit. It would silently break if a level such as 'LEVEL-10' were ever added (it sorts before 'LEVEL-2'), if values were localised, or if a value arrived with different case. A numeric mapping removes the dependency:

import { RiskLevel } from './types';

const LEVEL_RANK: Record<RiskLevel, number> = {
  [RiskLevel.LEVEL_0]: 0,
  [RiskLevel.LEVEL_1]: 1,
  [RiskLevel.LEVEL_2]: 2,
  [RiskLevel.LEVEL_3]: 3,
  [RiskLevel.LEVEL_4]: 4,
};

// Reference-implementation defaults; set per basin from local verification.
const WEIGHTS = { precipitation: 0.3, river: 0.4, dam: 0.2, drainage: 0.1 };
const CUTOFFS: [number, RiskLevel][] = [
  [80, RiskLevel.LEVEL_4],
  [60, RiskLevel.LEVEL_3],
  [40, RiskLevel.LEVEL_2],
  [20, RiskLevel.LEVEL_1],
];

export function riskScore(s: { precipitation: number; river: number;
                               dam: number; drainage: number }): number {
  return s.precipitation * WEIGHTS.precipitation + s.river * WEIGHTS.river
       + s.dam * WEIGHTS.dam + s.drainage * WEIGHTS.drainage;
}

export function levelFromScore(score: number): RiskLevel {
  for (const [min, level] of CUTOFFS) if (score >= min) return level;
  return RiskLevel.LEVEL_0;
}

export function atLeast(level: RiskLevel, min: RiskLevel): boolean {
  return LEVEL_RANK[level] >= LEVEL_RANK[min];
}

export function evacuationRequired(level: RiskLevel, probability: number): boolean {
  return atLeast(level, RiskLevel.LEVEL_3) && probability >= 60;
}

// Example: riskScore({precipitation:70, river:85, dam:40, drainage:60}) === 69
//          levelFromScore(69) === RiskLevel.LEVEL_3

The same atLeast helper should be used for the AlertCriteria.automatic.riskLevel check, so that the automatic-alert rule and the evacuation rule cannot drift apart.

Two thresholds, one number

The automatic-alert rule and the evacuation rule both use 60 % probability but at different levels (LEVEL-2 versus LEVEL-3). Keep both values in one configuration source. If an authority raises one and forgets the other, the system can end up recommending evacuation for events it would not even warn about.

5.8 Alert Distribution

Once a FloodAlert is issued, PHASE-3 §7.3 describes its delivery with AlertDistribution. The object has three parts: which channels are used, who the recipients are by priority group, and what happened to the messages.

Channel flagMediumPractical character
smsText message to registered numbersTargeted; needs subscriber lists; queues under load
pushApp push notificationRich content and links; reaches only app users
emailEmailSuitable for officials and partner agencies, too slow for the public
broadcastCell broadcast (the spec labels it 재난 문자, CBS)Reaches every compatible phone in the target cells without a subscriber list
sirenOutdoor sirensWorks without phones; carries little information on its own
radio, tvBroadcast radio, TV captionsWide reach; depends on broadcaster cooperation
socialSocial mediaAmplification and updates; not a primary channel

Recipients are grouped as highRisk (residents of high-risk areas), vulnerable (vulnerable groups), firstResponders, publicOfficials and general. The grouping implies an order: responders and officials need the warning early enough to act before the public does, and vulnerable residents — people with limited mobility, care facilities, people who do not read Korean — need both earlier notice and a channel suited to them, which is also why FloodAlert.instructions carries separate ko and en texts.

The status block counts sent, delivered, failed and confirmed. The spec does not state the relationships between them. Sensible invariants to enforce are delivered + failed ≤ sent (the remainder is still pending) and confirmed ≤ delivered. Counts should be kept per channel and per recipient group, not only as a total, since a warning that reached every official but failed on the cell-broadcast channel is a failed public warning. Cell broadcast and sirens usually give no per-recipient receipt, so their delivered counts can only mean "accepted by the network"; record that meaning rather than letting it look like confirmed reception.

Updates and cancellations need the same care as the first message. A warning that is upgraded, extended or withdrawn should reuse the original alertId as its reference so that consumers replace the earlier message instead of showing two conflicting ones; Chapter 3 shows how this maps to CAP 1.2 message types. The README's "within 5 minutes" delivery figure, like the other performance numbers in the spec, is a design target, not a guarantee; measure the actual time from issue to delivery per channel from the status records and report it alongside the verification scores of Section 5.6.

5.9 Summary

Chapter 6 turns from protocol to deployment: how these roles are placed on real infrastructure, secured, and connected to national and municipal systems.