Repository navigation
canonical(enums): CausalSeverity is a fourth severity fork, and 219 severity literals sit outside the ratchet's matcher #14988
Description
Found while doing the #14956 literal sweep. Two findings, both out of that PR's scope,
both measured on Dev_new_gui after the sweep landed.
1. CausalSeverity is a fourth severity fork
services/causal_inference_engine.py declares its own severity enum:
class CausalSeverity(str, Enum):
CRITICAL = "critical"
DEGRADED = "degraded"
WARNING = "warning"
After #14956 every one of its three values is a member of the canonical
autobot_shared.status_enums.Severity, so the member sets no longer diverge — it is a
straight alias away from being collapsed the way AlertSeverity and ErrorSeverity were
in #14957.
Why it was not collapsed there. CausalSeverity is (str, Enum); canonical Severity
is a plain Enum. Aliasing it changes the serialisation behaviour of every holder of a
member, and that is exactly the decision #13597 scope item 3 parked:
Severity,TaskStatus,Priority,LLMProvider,OperationOutcome,HealthStatus,
AgentStatusare plainEnumwhileAgentLifecycleStatusandConnectionStatusare
(str, Enum). Two serialisation behaviours in one module.
So this is blocked on that rule being stated, not on the member sets.
What is already known about the blast radius — every use is either an identity
comparison or a .value read; nothing observed depends on the str subclass:
_assess_severity/_generate_recommendationscompare members by identity.CausalAnalysisReport.to_dict()serialisesself.severity.value.- The report dataclass is reached through
to_dict(), not returned raw. CausalSeverity(EXAMPLE_1_POOL_EXHAUSTION["severity"])is a value lookup.
That needs confirming against the diagnostics endpoint before anyone acts on it — the point
here is that the audit is half-done, not that the change is safe.
2. 219 severity literals in two forms the ratchet cannot see
repo_tests/enum_union_guard_test.py ratchets one shape only — the "severity": "<literal>"
dict entry. Two other shapes carry the same defect and are unguarded. Measured under
autobot-backend/ and autobot_shared/:
| shape | matcher | hits | files |
|---|---|---|---|
| keyword / assignment | severity\s*=\s*["'][A-Za-z_]+["'] |
97 | |
| comparison | severity["']?\]?\s*==\s*["'][A-Za-z_]+["'] |
122 | |
| total | 219 | 55 |
20 of the 219 are outside the canonical vocabulary, and unlike the ones #14956 resolved
these are not case errors — they are words the enum has no rung for at all:
| value | count |
|---|---|
none |
10 |
moderate |
7 |
missing |
2 |
forbidden |
1 |
Each needs the same per-value decision #14956 applied: a genuine synonym gets mapped, a
genuinely distinct rung gets added to the canonical enum, and nothing gets folded if that
would change a wire format, an API response, or a stored value. moderate in particular
already exists as CommandRisk.MODERATE, and forbidden as CommandRisk.FORBIDDEN, so at
least some of these are a different enum's vocabulary appearing in a field named
severity — which is a mis-typed field, not a missing rung.
Work
- State the plain-
Enumvs(str, Enum)rule in thestatus_enumsmodule docstring
(canonical(enums): AlertSeverity and ErrorSeverity duplicate the shared Severity enum, and 119 severity literals bypass enums entirely #13597 scope item 3), then collapseCausalSeverityunder it - Verify the diagnostics endpoint serialises the causal report via
to_dict()before
the alias lands - Decide each of
none/moderate/missing/forbidden— synonym, new rung, or
wrong enum entirely - Convert the 219 and extend the ratchet in
enum_union_guard_test.pyto cover both
shapes, with the same floor-before-ceiling structure the dict-entry ratchet uses
Activity
mrveiss commentedon Sep 2, 2026
Measured this whole issue against
Dev_new_guiat2363ad3aa9before touching anything.Two of the four work items were already done on base; the counts in the third are wrong.
PR #15508 guards what had landed and ratchets what had not.
Item 1 —
CausalSeverityis a fourth fork: STALEIt is already collapsed.
services/causal_inference_engine.py:36:No
class CausalSeverityexists anywhere in the tree. Nothing asserted it, though, so are-declaration of the name would have been invisible — #15508 pins the alias by AST binding.
Item 1's blocker — the plain
Enumvs(str, Enum)rule: STALEThe rule is stated in the
status_enumsmodule docstring, and each(str, Enum)carries itsown reason (
AgentLifecycleStatusSQLAlchemy coercion,ConnectionStatuspre-existingcomparisons,
SecretTypepersisted value). #13597 scope item 3 is done.Item 2 — verify the diagnostics endpoint serialises via
to_dict(): CONFIRMEDapi/diagnostics.py:87—return FailureAnalysisResponse(data=report.to_dict()), andcausal_inference_engine.py:102emits"severity": self.severity.value. So dropping thestrbase was inert. Both halves of that path are now pinned by #15508, because the safetyargument only holds while the
.valueread is there.Item 4 — the 219 literals: MEASURED 194 across 53 files, not 219 across 55
Same matchers as this issue quotes, same two roots:
The two shapes are disjoint — 73 + 121 = 194 deduplicated by span — so each can be ratcheted
independently. #15508 pins keyword 68-73, comparison 116-121, file spread 48-53.
The out-of-vocabulary distribution in this issue is exactly right: 20 total,
none10 /moderate7 /missing2 /forbidden1.Item 3 — decide each of
none/moderate/missing/forbidden: ANSWEREDAll four are the same defect, and it is the one this issue suspected: a different enum's
vocabulary sitting in a field named
severity. None of them is a missing rung, and noneshould be added to
Severity.forbidden—security/command_patterns.py:78annotates the fieldstrwith the threewords in a trailing comment, and
:493branches on it. That ladder isCommandRisk, whichalready has
FORBIDDEN. A command permission is not an outcome grade.none/moderate—autobot_shared/delta_engine.py:120classifies a metric delta asnone/moderate/critical.nonemeans no finding at all; no severity ladder canexpress absence.
missing—autobot_shared/env_drift_detector.py:55records a drift kind, listedalongside
type_mismatchandunknown. It says what drifted, not how bad it is.This matters because the cheapest way to drive the out-of-vocabulary count to zero is to widen
Severity, which makes the guard read clean while making the canonical enum mean less — theopposite of #13597. #15508 fails if any of the four is added, and the
Severitydocstring nownames all three foreign vocabularies so the next reader does not have to re-derive this.
The remaining work is per-field typing in those three modules, which is a behaviour change in
each and belongs in its own PR.
What is left on this issue
CommandRiskincommand_patterns, a delta classification indelta_engine, a drift kind inenv_drift_detector)them. 40 of the 53 files are comparison-shaped, where the compared value may arrive from
outside the process — each needs the producer checked, which is why it was not bundled.
Items 1 and 2 can be ticked now.
test(enums): ratchet the two severity-literal shapes the #13597 guard…
mrveiss commentedon Sep 20, 2026
Re-verification against current
origin/main(2026-09-20). Tracked branchlane-enums-canonicalis already gone from origin.AC1 done; AC2/AC3/AC4 not:
Enumvs(str, Enum)rule is now stated inautobot_shared/status_enums.py's module docstring (the canonical(enums): AlertSeverity and ErrorSeverity duplicate the shared Severity enum, and 119 severity literals bypass enums entirely #13597 scope item), explaining when each applies (boundary serialization needs(str, Enum); identity-comparison contexts stay plainEnum).CausalSeverityno longer exists inservices/causal_inference_engine.pyat all — collapsed underSeverityas the issue proposed, not just documented.to_dict()before the alias landed) — no evidence found either way; not something I could confirm without tracing the live route, and the collapse already happened, so this is retroactive at this point regardless.none,moderate,missing,forbidden) — no decision found recorded anywhere (DELIBERATE_SEVERITY_LITERALSin the guard doesn't mention any of them, and I found no other tracking).repo_tests/enum_union_guard_test.py's_SEVERITY_LITERALregex still only matches the single{"severity": "value"}dict-entry shape (SEVERITY_LITERAL_CEILING=25, a much smaller, unrelated number from canonical(enums): 169 bare severity literals remain, 43 of them outside the canonical vocabulary #14956's original ratchet). Noseverity = "value"orseverity == "value"pattern exists anywhere in the file — the two new shapes the issue measured at 219 hits across 55 files remain completely unguarded and, as far as I can tell, unconverted.1 of 4 done (and done well — a real collapse, not just documentation). The 219-literal sweep is the substantial remaining work. Leaving open.