Skip to content

canonical(enums): 10 *Severity(Enum) forks of the shared Severity survive #13597, and no guard blocks the next one #18098

Open
1 of 1 issue completed

Description

@mrveiss

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:

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:

  1. 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.
  2. 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 .value changes (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 in repo_tests/symbol_fork_baseline.json

Location

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

  1. mrveiss commented on Oct 9, 2026

    @mrveiss
    OwnerAuthor

    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 is Status-only (grep -ci severity on 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:

    api/code_intelligence.py:1049   {sev.value: len([...]) for sev in OptimizationSeverity}
    api/code_intelligence.py:1176   {sev.value: len([...]) for sev in OptimizationSeverity}
    api/code_intelligence.py:1286   {sev.value: len([...]) for sev in SecuritySeverity}
    api/code_intelligence.py:1539   {sev.value: len([...]) for sev in PerformanceSeverity}
    testing_pattern_analyzer.py:1142  {s.value: ... for s in TestPatternSeverity}
    llm_pattern_analyzer.py:413       [p.value for p in OptimizationPriority]
    

    Aliasing those to the ten-rung canonical grows those dicts from 5 keys to 10, adding unknown, minimal, warning, degraded, error as always-zero keys. And the client type is deliberately the narrow five — autobot-frontend/src/types/codeIntelligence.ts:21:

    export const CODE_INTELLIGENCE_SEVERITIES = ['info','low','medium','high','critical'] as const satisfies readonly CanonicalSeverity[]

    with the comment that a value the generated Severity does not carry "fails to compile here". So OptimizationSeverity, SecuritySeverity, PerformanceSeverity and TestPatternSeverity are wire-format decisions, not refactors. The canonical's own docstring already names this trap and Severity.score_ladder() as the mitigation — worth using if they migrate.

    Also not drop-in: EventSeverity and SecurityEventSeverity are (str, enum.Enum) persisted to indexed String(16) columns and compared against those strings throughout api/errors.py and api/security.py. Aliasing them to the plain-Enum canonical 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 exactly critical/high/medium/low/info. It is an exact subset of the canonical under a name that says "priority", so no *Severity sweep 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:

    code_intelligence/anti_pattern_detection/types.py    AntiPatternSeverity = Severity
    code_intelligence/fingerprinting/types.py            CloneSeverity       = Severity
    code_intelligence/cross_language_patterns/models.py  PatternSeverity     = Severity
    code_intelligence/pattern_analysis/types.py          PatternSeverity     = Severity
    

    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 the PatternSeverity name 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 way enum_union_guard_test.py::test_no_second_command_risk_enum_has_regrown does it for CommandRisk: 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:

    • each exemption's stated reason is re-checked, not believed — a test asserts the named file still iterates that enum, so an exemption whose reason has expired fails loudly instead of exempting silently;
    • the scan is asserted non-vacuous (>3000 files, >200 enums, canonical present);
    • the scope it does not cover is asserted — 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 by ast.unparse rather than the literal word "Enum", so from enum import Enum as _E is 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) and ReviewSeverity (+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 no SUCCESS or SUGGESTION.

  2. mrveiss commented on Oct 10, 2026

    @mrveiss
    OwnerAuthor

    Status, and one decision for the owner. Checked against main: the guard (AC1) shipped in #18130. The canonical Severity is in status_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, SecuritySeverity and TestPatternSeverity are iterated to build severity_breakdown JSON. A plain alias would turn 5 wire keys into 10. They will iterate one Severity.finding_ladder() (the 5 rungs, info→critical) so the wire stays byte-identical. This lands after the branch that currently owns status_enums.py merges.

    Decision needed (owner): OptimizationPriority (llm_pattern_analysis/types.py). It has the same 5 values in reversed order (CRITICAL→INFO), and llm_pattern_analyzer emits [p.value for p in OptimizationPriority], so the order is on the wire. Is it a severity at all, or an urgency concept?

    • (a) It is an urgency concept: record it as a distinct vocabulary in the guard (recommended; the name and use say priority).
    • (b) It is a severity: alias it, and the emitted list's order changes to info→critical.

    Being audited: the SLM EventSeverity and SecurityEventSeverity are str, Enum types 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.

  3. added
    needs-decisionBlocked on an owner decision; options and a recommendation are on the issue
    on Oct 10, 2026
  4. mrveiss commented on Oct 10, 2026

    @mrveiss
    OwnerAuthor

    More enums whose values are all canonical Severity values

    The guard extension on branch issue-18098-severity-enum-forks adds a second probe. It flags any Enum with two or more members whose every value is a canonical Severity value, which catches 3- and 4-rung forks that the existing 5-rung probe misses. On origin/main it also flags six enums nobody has ruled on. They are recorded in the guard rather than guessed at:

    Enum Classification so far Needs
    autobot-backend/security/enterprise/threat_detection/types.py::ThreatLevel Its docstring says "Threat severity levels". Possibly a plain fork. A consumer audit (wire, storage, iteration) before alias vs exemption
    autobot-backend/api/knowledge_grounding_models.py::ReviewTicketPriority Priority A ruling: urgency vs severity (the same question as OptimizationPriority above)
    autobot-backend/llc/models/enums.py::WorkItemPriority Priority The same
    autobot-backend/services/analytics_service.py::MaintenancePriority Priority The same
    autobot-backend/temporal_knowledge_manager.py::KnowledgePriority Priority The same
    autobot-backend/secure_sandbox_executor.py::SandboxSecurityLevel Recorded as a distinct vocabulary: it grades an isolation setting the caller picks (low/medium/high), not a detected finding Confirmation of that classification

    Each 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/SecurityEventSeverity are documented exemptions (path-loaded models/database.py, String(16) persistence, and threat-summary iteration at api/security.py:425); see docs/developer/ARCHITECTURE_EXCEPTIONS.md on the branch.

  5. mrveiss commented on Oct 10, 2026

    @mrveiss
    OwnerAuthor

    Addendum to the owner decision above: the widened value probe found four more *Priority enums with the same urgency-vs-severity question as OptimizationPriority: ReviewTicketPriority, WorkItemPriority, MaintenancePriority and KnowledgePriority. 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. SandboxSecurityLevel grades an isolation setting, not a finding, so it is recorded as a distinct vocabulary.

  6. mrveiss commented on Oct 10, 2026

    @mrveiss
    OwnerAuthor

    Owner decision (2026-10-10): create ONE canonical Priority enum (an urgency vocabulary, distinct from Severity) and alias all five *Priority forks to it: OptimizationPriority, ReviewTicketPriority, WorkItemPriority, MaintenancePriority, KnowledgePriority. Where order is emitted on the wire, pin it explicitly so the output stays byte-identical.

  7. removed
    needs-decisionBlocked on an owner decision; options and a recommendation are on the issue
    on Oct 10, 2026
  8. mrveiss commented on Oct 11, 2026

    @mrveiss
    OwnerAuthor

    Owner decisions (2026-10-11):

    • OptimizationPriority is a severity vocabulary (critical/high/medium/low/info is the severity ladder reversed), so it aliases to the canonical Severity, not Priority, with its emitted order pinned to reversed(Severity.finding_ladder()). The output is byte-identical.
    • WorkItemPriority (LLC, stored as a Postgres enum) becomes a derived subset. It stays a stored str enum whose 4 members are derived from canonical Priority, and a guard pins its names and values to Priority. No DB migration, no API change, the JSON and the generated types unchanged. Widening it later is a normal migration.
    • ReviewTicketPriority is deferred to the branch that owns its file (issue-18086).
  9. mrveiss commented on Oct 11, 2026

    @mrveiss
    OwnerAuthor

    Owner decisions (2026-10-11), via the coordinator. These close the questions left after the Severity part.

    1. OptimizationPriority aliases Severity, not Priority. Its values (critical, high, medium, low, info) are exactly the Severity finding ladder, reversed. Canonical Priority has no INFO, so aliasing it to Priority would break recommendation_engine.py's OptimizationPriority.INFO and change the emitted list. The emitted order is pinned to reversed(Severity.finding_ladder()), byte-identical, with literal-dict wire tests.
    2. WorkItemPriority is a derived subset of Priority. It stays a stored str enum: the Postgres workitempriority type is unchanged, and the API accepts the same 4 values. Its members are derived from canonical Priority as an explicit 4-member subset (names and values come from Priority, not re-typed literals), and a guard pins every name and value to Priority, so there is one vocabulary.
      Required, with tests and a contrast:
      • no DB migration and no API change;
      • the str mixin kept, so json.dumps output is unchanged;
      • the generated api.ts unchanged.
        Widening it later is an ordinary migration.
    3. ReviewTicketPriority is safe to alias (its consumers use .value or identity), but it is deferred, because autobot-backend/api/knowledge_grounding_models.py is owned by the unmerged issue-18086-claim-models branch. The fork guard records it with that reason. It is aliased once that branch lands.

    Status on issue-18098-severity-enum-forks:

    • Done: Severity.finding_ladder() and the 4 Severity aliases (Performance, Optimization, Security, TestPattern; byte-identical wire tests), plus KnowledgePriority (order pinned) and MaintenancePriority aliased to Priority.
    • In progress: decisions 1 and 2.
    • The PR will carry Refs #18098, because ReviewTicketPriority is deferred.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions