Repository navigation
canonical(enums): 10 *Severity(Enum) forks of the shared Severity survive #13597, and no guard blocks the next one #18098
Description
Problem
#13597 consolidated AlertSeverity and ErrorSeverity onto autobot_shared.status_enums.Severity.
That fix held — neither name is back. But it fixed two instances of a class rather than the
class, and 10 more were already there. Nothing blocks the eleventh.
Found while finishing GH#6757's AntiPatternType SSOT in #18097: the same file that redefined the
canonical enum also defines AntiPatternSeverity, which turned out to be the same defect in a
second vocabulary.
Why the existing tooling misses all of it:
tools/lint/check_symbol_forks.pymatches by name. These forks have different names
(PatternSeverity,CloneSeverity, …) for the same concept, so the detector cannot see one.- the
*Status(Enum)guard (refactor(types): consolidate 15+ *Status enums sharing pending/running/completed/failed/cancelled #6973,
autobot-infrastructure/shared/scripts/hooks/pre-commit-no-new-status-enum) is
Status-only — a grep of it forSeverityreturns nothing. The analogous guard was never
written, which is why canonical(enums): AlertSeverity and ErrorSeverity duplicate the shared Severity enum, and 119 severity literals bypass enums entirely #13597's fix did not generalise.
Measured
Canonical autobot_shared.status_enums.Severity has 10 members. Of the 16 other *Severity(Enum)
declarations across autobot-backend, autobot-slm-backend, autobot_shared (ansible mirror
excluded):
10 are exact subsets — every member name and value identical to canonical. These are forks:
| enum | members | location |
|---|---|---|
AntiPatternSeverity |
5 | autobot-backend/code_intelligence/anti_pattern_detection/types.py:27 |
CloneSeverity |
5 | autobot-backend/code_intelligence/fingerprinting/types.py:42 |
EventSeverity |
4 | autobot-slm-backend/models/database.py:195 |
OptimizationSeverity |
5 | autobot-backend/code_intelligence/redis_optimizer.py:42 |
PatternSeverity |
5 | autobot-backend/code_intelligence/cross_language_patterns/models.py:53 |
PatternSeverity |
5 | autobot-backend/code_intelligence/pattern_analysis/types.py:32 |
PerformanceSeverity |
5 | autobot-backend/code_intelligence/performance_analysis/types.py:17 |
SecurityEventSeverity |
4 | autobot-slm-backend/models/database.py:729 |
SecuritySeverity |
5 | autobot-backend/code_intelligence/security/constants.py:32 |
TestPatternSeverity |
5 | autobot-backend/code_intelligence/testing_pattern_analyzer.py:58 |
Because the values are identical, each is a drop-in migration: SomeSeverity = Severity as an
alias, or import the canonical name. No serialized value changes.
Note PatternSeverity is declared twice, so it is additionally a name fork and should already
be a check_symbol_forks.py cluster — worth confirming it is in the baseline.
6 are NOT forks — each carries at least one member canonical does not have, so they are genuinely
distinct vocabularies and should be left alone:
| enum | members | the non-canonical part |
|---|---|---|
CheckSeverity |
BLOCK, WARN, INFO | BLOCK/WARN — gate verdicts, not severities |
ConflictSeverity (merge) |
TRIVIAL…CRITICAL | a resolution-difficulty scale |
ConflictSeverity (manifest) |
HARD, SOFT | a conflict kind |
DiagnosticSeverity |
ERROR, WARNING, INFORMATION, HINT | the LSP spec's own four values |
ReviewSeverity |
+SUGGESTION | SUGGESTION is not a severity |
ValidationSeverity |
+SUCCESS | SUCCESS is not a severity |
Listing these explicitly so the next person does not "finish the job" by flattening six correct
vocabularies into one. The discriminator used was membership, not the name: does every member
name+value exist in canonical?
(Separately: the two ConflictSeverity declarations share a name and mean different things —
difficulty vs kind. That is a name collision to resolve on its own terms, not by merging them.)
Fix
One fix for the class, not ten patches:
- Extend the refactor(types): consolidate 15+ *Status enums sharing pending/running/completed/failed/cancelled #6973 guard (or add a sibling) to block new
*Severity(Enum)declarations outside
autobot_shared/status_enums.py, with the 6 legitimate vocabularies above as named exemptions
carrying the reason. Without this the class recurs, exactly as it did after canonical(enums): AlertSeverity and ErrorSeverity duplicate the shared Severity enum, and 119 severity literals bypass enums entirely #13597. - Migrate the 10 subset enums onto canonical
Severity.
Order matters: the guard first means the migration cannot be silently undone later.
Acceptance criteria
- A guard blocks a new
*Severity(Enum)outside the canonical module, with the 6 distinct vocabularies exempted by name and with a stated reason - The guard's exemption list is re-derived by an independent control, so an exemption added without reason fails
- All 10 subset enums resolve to
autobot_shared.status_enums.Severity; no serialized.valuechanges (verify: the member values are already identical) - The 6 distinct vocabularies are untouched, and a test records why each is exempt
-
PatternSeverity's double declaration is reconciled or confirmed present inrepo_tests/symbol_fork_baseline.json
Location
autobot_shared/status_enums.py(canonicalSeverity)autobot-infrastructure/shared/scripts/hooks/pre-commit-no-new-status-enum(Status-only today).pre-commit-config.yaml:838-847(the refactor(types): consolidate 15+ *Status enums sharing pending/running/completed/failed/cancelled #6973 hook entry)- the 10 + 6 paths tabled above
Discovered During
#18097 / #12771 — finishing GH#6757's AntiPatternType SSOT in the same file.
Refs #13597 (prior art, closed — this is the class it belonged to), #6973 (the guard pattern to copy)
Activity
mrveiss commentedon Oct 9, 2026
Four aliased, five held back — "drop-in migration" does not hold
Commit
69643c6944. Your counts reproduce exactly within the three roots you declared: canonical has 10 members, 16 other*Severity(Enum)declarations, 10 exact subsets, 6 not forks, and the #6973 hook isStatus-only (grep -ci severityon it returns 0). Two things need correcting before anyone finishes this.Correction 1: "No serialized value changes" is false for five of the ten
Five call sites build a response or report dict key set by iterating the member set:
Aliasing those to the ten-rung canonical grows those dicts from 5 keys to 10, adding
unknown,minimal,warning,degraded,erroras always-zero keys. And the client type is deliberately the narrow five —autobot-frontend/src/types/codeIntelligence.ts:21:with the comment that a value the generated
Severitydoes not carry "fails to compile here". SoOptimizationSeverity,SecuritySeverity,PerformanceSeverityandTestPatternSeverityare wire-format decisions, not refactors. The canonical's own docstring already names this trap andSeverity.score_ladder()as the mitigation — worth using if they migrate.Also not drop-in:
EventSeverityandSecurityEventSeverityare(str, enum.Enum)persisted to indexedString(16)columns and compared against those strings throughoutapi/errors.pyandapi/security.py. Aliasing them to the plain-Enumcanonical is a migration.So of the ten exact subsets, four are safe today and six are not, for two different reasons.
Correction 2: a sixth in-scope fork the name-based sweep could not see
OptimizationPriority—code_intelligence/llm_pattern_analysis/types.py:102— carries exactlycritical/high/medium/low/info. It is an exact subset of the canonical under a name that says "priority", so no*Severitysweep lists it. It also iterates its members (llm_pattern_analyzer.py:413), so it lands in the same held-back set, with urgency-vs-severity as a second question.This is the argument for probing by member set rather than by name, and it generalises: there may be more
Level/Grade/Impact-shaped enums carrying the same five values. I have not measured that wider population — stating it as unmeasured, not as zero.Landed
Four aliases:
All four build their distributions from observed values (
anti_pattern_detection/analyzer.py:456,fingerprinting/detector.py:842,pattern_analysis/types.py:309), so their response shape is unchanged. That also resolves thePatternSeverityname collision you flagged: both modules declared the same five rungs, so both collapse to the same alias.Plus
repo_tests/severity_enum_fork_guard_18098_test.py— AC 1, done the wayenum_union_guard_test.py::test_no_second_command_risk_enum_has_regrowndoes it forCommandRisk: match by member set, with a named-reason allowlist. Three properties worth noting, because a guard that merely passes is the failure mode this repo keeps hitting:autobot-slm-backend/is outside it on purpose, so green is never read as clean.Verified to discriminate: appending a bare
class DocsSeverity(Enum)with the five members fails it by file and symbol; removing it returns to 14 passed. Base matching is byast.unparserather than the literal word "Enum", sofrom enum import Enum as _Eis caught — pinned against synthetic source, since that was a fail-open the sibling guard shipped once.What is left
The five held-back subsets need the wire decision (narrow via
score_ladder(), or accept the ten-key shape and widen the frontend type). The two slm-backend ones need a column migration or a documented exemption. The six genuinely distinct vocabularies stay as they are — and your reading of them holds, with one nuance:ValidationSeverity(+SUCCESS) andReviewSeverity(+SUGGESTION) are 3-of-4 canonical, so they are overlapping with a non-canonical tail rather than a different scale. Still must not be flattened; canonical has noSUCCESSorSUGGESTION.review(18130): answer all eleven CodeRabbit threads — one KB store, d…
review(18130): round 3 — a cooldown withheld a healthy store, and aut…
refactor(forks): retire fifteen forks across eight issues, each onto …
refactor(forks): retire fifteen forks across eight issues, each onto …
mrveiss commentedon Oct 10, 2026
Status, and one decision for the owner. Checked against
main: the guard (AC1) shipped in #18130. The canonicalSeverityis instatus_enums.py. In progress, with no behaviour change: aliasing one more fork (a debug script) and widening the guard's probe to 3–4-rung subsets.Sequenced, not a decision:
PerformanceSeverity,OptimizationSeverity,SecuritySeverityandTestPatternSeverityare iterated to buildseverity_breakdownJSON. A plain alias would turn 5 wire keys into 10. They will iterate oneSeverity.finding_ladder()(the 5 rungs, info→critical) so the wire stays byte-identical. This lands after the branch that currently ownsstatus_enums.pymerges.Decision needed (owner):
OptimizationPriority(llm_pattern_analysis/types.py). It has the same 5 values in reversed order (CRITICAL→INFO), andllm_pattern_analyzeremits[p.value for p in OptimizationPriority], so the order is on the wire. Is it a severity at all, or an urgency concept?Being audited: the SLM
EventSeverityandSecurityEventSeverityarestr, Enumtypes stored in DB columns. They get aliased only if no consumer relies on str-ness; otherwise they get a documented exemption. Stored values never change either way.mrveiss commentedon Oct 10, 2026
More enums whose values are all canonical
SeverityvaluesThe guard extension on branch
issue-18098-severity-enum-forksadds a second probe. It flags any Enum with two or more members whose every value is a canonicalSeverityvalue, which catches 3- and 4-rung forks that the existing 5-rung probe misses. Onorigin/mainit also flags six enums nobody has ruled on. They are recorded in the guard rather than guessed at:autobot-backend/security/enterprise/threat_detection/types.py::ThreatLevelautobot-backend/api/knowledge_grounding_models.py::ReviewTicketPriorityOptimizationPriorityabove)autobot-backend/llc/models/enums.py::WorkItemPriorityautobot-backend/services/analytics_service.py::MaintenancePriorityautobot-backend/temporal_knowledge_manager.py::KnowledgePriorityautobot-backend/secure_sandbox_executor.py::SandboxSecurityLevelEach sits in a named allowlist with its reason (
UNCLASSIFIED_VALUE_MATCHES,DISTINCT_VOCABULARIES). The guard fails on any new match outside those lists, so the set can only shrink as rulings land.The SLM
EventSeverity/SecurityEventSeverityare documented exemptions (path-loadedmodels/database.py,String(16)persistence, and threat-summary iteration atapi/security.py:425); seedocs/developer/ARCHITECTURE_EXCEPTIONS.mdon the branch.mrveiss commentedon Oct 10, 2026
Addendum to the owner decision above: the widened value probe found four more
*Priorityenums with the same urgency-vs-severity question asOptimizationPriority:ReviewTicketPriority,WorkItemPriority,MaintenancePriorityandKnowledgePriority. One ruling covers all five. They are allowlisted with that reason, so the guard fails only on new matches.ThreatLevel(enterprise threat detection) gets a consumer audit before any alias.SandboxSecurityLevelgrades an isolation setting, not a finding, so it is recorded as a distinct vocabulary.mrveiss commentedon Oct 10, 2026
Owner decision (2026-10-10): create ONE canonical
Priorityenum (an urgency vocabulary, distinct fromSeverity) and alias all five*Priorityforks to it:OptimizationPriority,ReviewTicketPriority,WorkItemPriority,MaintenancePriority,KnowledgePriority. Where order is emitted on the wire, pin it explicitly so the output stays byte-identical.mrveiss commentedon Oct 11, 2026
Owner decisions (2026-10-11):
OptimizationPriorityis a severity vocabulary (critical/high/medium/low/info is the severity ladder reversed), so it aliases to the canonicalSeverity, notPriority, with its emitted order pinned toreversed(Severity.finding_ladder()). The output is byte-identical.WorkItemPriority(LLC, stored as a Postgres enum) becomes a derived subset. It stays a storedstrenum whose 4 members are derived from canonicalPriority, and a guard pins its names and values toPriority. No DB migration, no API change, the JSON and the generated types unchanged. Widening it later is a normal migration.ReviewTicketPriorityis deferred to the branch that owns its file (issue-18086).mrveiss commentedon Oct 11, 2026
Owner decisions (2026-10-11), via the coordinator. These close the questions left after the Severity part.
OptimizationPriorityaliasesSeverity, notPriority. Its values (critical, high, medium, low, info) are exactly theSeverityfinding ladder, reversed. CanonicalPriorityhas noINFO, so aliasing it to Priority would breakrecommendation_engine.py'sOptimizationPriority.INFOand change the emitted list. The emitted order is pinned toreversed(Severity.finding_ladder()), byte-identical, with literal-dict wire tests.WorkItemPriorityis a derived subset ofPriority. It stays a storedstrenum: the Postgresworkitemprioritytype is unchanged, and the API accepts the same 4 values. Its members are derived from canonicalPriorityas an explicit 4-member subset (names and values come fromPriority, not re-typed literals), and a guard pins every name and value toPriority, so there is one vocabulary.Required, with tests and a contrast:
strmixin kept, sojson.dumpsoutput is unchanged;api.tsunchanged.Widening it later is an ordinary migration.
ReviewTicketPriorityis safe to alias (its consumers use.valueor identity), but it is deferred, becauseautobot-backend/api/knowledge_grounding_models.pyis owned by the unmergedissue-18086-claim-modelsbranch. The fork guard records it with that reason. It is aliased once that branch lands.Status on
issue-18098-severity-enum-forks:Severity.finding_ladder()and the 4 Severity aliases (Performance, Optimization, Security, TestPattern; byte-identical wire tests), plusKnowledgePriority(order pinned) andMaintenancePriorityaliased toPriority.Refs #18098, because ReviewTicketPriority is deferred.refactor(infra): frontend_analysis_lib IssueSeverity is the canonical…
test(guard): severity fork guard probes by canonical value, scans inf…
docs(arch): SLM event severity enums are exempt from the shared Sever…
test(guard): severity fork guard declares its reach, floor pinned mid…
fix(infra): frontend_analysis_lib stays runnable without PYTHONPATH a…
refactor(security): threat_detection ThreatLevel is the canonical Sev…
refactor(code-intel): four Severity forks and two Priority forks alia…
refactor(llc,code-intel): OptimizationPriority aliases Severity, Work…
fix(train): migration gate covers approval_gate_service; the #18098 p…