Problem
The duplicate/replay comparison in calculate_sla (src/lib.rs/src/calculation.rs) checks MTTR and threshold but never the severity:
if prev.config_version_hash == config_version_hash {
if prev.mttr_minutes != mttr_minutes || prev.threshold_minutes != cfg.threshold_minutes {
// conflict -> DuplicateOutageInput
}
// otherwise: replay — return prev without touching state
return Ok(prev);
}
The caller's severity argument is not compared against prev's severity (the stored SLAResult doesn't even carry a severity field — it carries threshold_minutes and config_version_hash). Two severities with identical configs (e.g. high and medium both configured to threshold 30 / penalty 50 / reward 750 — allowed by validate_config) produce the same threshold_minutes and the same overall config hash, so resubmitting the same outage under the other severity with the same MTTR is treated as an idempotent replay.
Consequences:
- A severity change is silently dropped: an operator correcting a submission from
high to medium (with equal configs) gets back the high-generated result with no error, no new history entry, and no event — the correction is invisible.
- The duplicate policy's "conflicting inputs" definition omits severity: SC-W5-046's table keys on
(mttr_minutes, threshold_minutes); a different severity with matching threshold is "idempotent" even though the caller's intent changed — the table itself treats severity as irrelevant.
- The stored entry is indistinguishable:
SLAResult has no severity field, so even get_history_by_outage (companion issue) cannot tell which severity produced an entry; the severity lives only in the event's topic[2], which history queries never read.
Root cause
SLAResult predates the config-hash duplicate policy and carries thresholds but not severity; the comparison logic was built on the fields the struct happened to have.
Why this is architecturally hard
- Fixing it properly means adding
severity to SLAResult (an ABI change — RESULT_SCHEMA_FIELD_COUNT and RESULT_SCHEMA_VERSION must bump, the guardrail updates, history entries change layout — a migration) or comparing severity against the event/history context, which history entries don't store.
- The duplicate policy's documented matrix must be updated to include severity in the conflict comparison (or explicitly document severity-blind semantics), and the
dup_input event payload should carry the severity context.
- The interaction with
MAX_RECALCS_PER_OUTAGE and config generations means the fix must decide whether a severity-only change (same config hash) is a conflict, a replay, or a new generation — a policy decision, not just a comparison fix.
Acceptance criteria
Out of scope
The SLAResult schema migration itself (if severity is added) and the history-generation attribution (companion issue).
Getting started
Good first files to read: apexchainx_calculator/src/lib.rs (calculate_sla duplicate block, SLAResult), apexchainx_calculator/src/calculation.rs (same block).
Problem
The duplicate/replay comparison in
calculate_sla(src/lib.rs/src/calculation.rs) checks MTTR and threshold but never the severity:The caller's
severityargument is not compared againstprev's severity (the storedSLAResultdoesn't even carry a severity field — it carriesthreshold_minutesandconfig_version_hash). Two severities with identical configs (e.g.highandmediumboth configured to threshold 30 / penalty 50 / reward 750 — allowed byvalidate_config) produce the samethreshold_minutesand the same overall config hash, so resubmitting the same outage under the other severity with the same MTTR is treated as an idempotent replay.Consequences:
hightomedium(with equal configs) gets back thehigh-generated result with no error, no new history entry, and no event — the correction is invisible.(mttr_minutes, threshold_minutes); a different severity with matching threshold is "idempotent" even though the caller's intent changed — the table itself treats severity as irrelevant.SLAResulthas noseverityfield, so evenget_history_by_outage(companion issue) cannot tell which severity produced an entry; the severity lives only in the event's topic[2], which history queries never read.Root cause
SLAResultpredates the config-hash duplicate policy and carries thresholds but not severity; the comparison logic was built on the fields the struct happened to have.Why this is architecturally hard
severitytoSLAResult(an ABI change —RESULT_SCHEMA_FIELD_COUNTandRESULT_SCHEMA_VERSIONmust bump, the guardrail updates, history entries change layout — a migration) or comparing severity against the event/history context, which history entries don't store.dup_inputevent payload should carry the severity context.MAX_RECALCS_PER_OUTAGEand config generations means the fix must decide whether a severity-only change (same config hash) is a conflict, a replay, or a new generation — a policy decision, not just a comparison fix.Acceptance criteria
SLAError::DuplicateOutageInputdocs match the implemented behavior.Out of scope
The
SLAResultschema migration itself (if severity is added) and the history-generation attribution (companion issue).Getting started
just testGood first files to read:
apexchainx_calculator/src/lib.rs(calculate_sladuplicate block,SLAResult),apexchainx_calculator/src/calculation.rs(same block).