Skip to content

tech-debt: 308 non-test class names are defined in more than one module — one pair had drifted into two different security policies #16177

Open

Description

@mrveiss

308 non-test class names are defined in more than one module

Measured 2026-09-10 on origin/Dev_new_gui, over 3,227 non-test Python files
(tests, /tests/, test_* and node_modules excluded):

Colliding class names 308
Worst offenders ServiceStatus ×10 · DeploymentMode ×9 · SearchResult ×9 · ServiceRegistry ×8 · ValidationResult ×7 · MeshDB ×6 · Claim ×5 · ServiceHealth ×5 · SourceType ×5

Found while closing #15757, which was one instance of exactly this: two classes
named PasswordChangeRateLimiter in different modules, with different rate-limit
policies
. A reader at either call site could not tell which one they had, and an
edit to "the" limiter could land on the wrong security surface.

Why this is a maintainability defect, not a style preference

A shared name across modules makes three ordinary operations unsafe:

  • Reading a call site. PasswordChangeRateLimiter() said nothing about which
    policy applied — 5 attempts per 300s, or 3 per 1800s.
  • Grepping. git grep ServiceStatus returns ten unrelated concepts; there is no
    query that finds "the one I mean".
  • Editing. A change reasoned about one definition and applied to the other is
    invisible to review, because the diff reads correctly against the wrong class.

In #15757 the two policies had already drifted apart, and nobody had noticed
because both spellings were correct.

What this is NOT

Test doubles are excluded and should stay excluded: _FakeRedis ×13, _FakeSession
×12, _FakeAsyncRedis ×12 are local fixtures whose whole purpose is to be local. A
guard on all duplicate names would report ~384 and drown the 308 that matter.

Not every one of the 308 is a defect either. Two ValidationResult dataclasses in
genuinely unrelated domains are less dangerous than two ServiceRegistry classes
that both look like the registry. Triage is the work, and this issue is the
enumeration that makes triage possible.

Acceptance criteria

  • The 308 are triaged into: same concept (consolidate), related concept (rename to disambiguate), genuinely unrelated (leave, record why)
  • The consolidate set is fixed, highest-collision-count first — ServiceStatus, ServiceRegistry and ServiceHealth look like one concept spread across ten files and are the place to start
  • Any pair guarding a security or policy surface is fixed regardless of count — tech-debt(security): two PasswordChangeRateLimiter classes with different policies and one name #15757 is the worked example of why
  • A ratchet freezes the post-triage count so a new collision fails; test doubles excluded by the same rule used here
  • The ratchet is proven to fire by adding a duplicate and watching it fail, not by reasoning that it would

Reproduce

python3 - <<'PY'
import ast, collections, subprocess, pathlib
files = [f for f in subprocess.run(["git","ls-files","*.py"],capture_output=True,text=True).stdout.split()
         if "node_modules" not in f and not f.endswith("_test.py")
         and "/tests/" not in f and "/test_" not in f]
seen = collections.defaultdict(set)
for rel in files:
    try: tree = ast.parse(pathlib.Path(rel).read_text(encoding="utf-8"))
    except Exception: continue
    for node in tree.body:
        if isinstance(node, ast.ClassDef): seen[node.name].add(rel)
dup = {k: sorted(v) for k, v in seen.items() if len(v) > 1}
print(len(files), "files;", len(dup), "colliding names")
PY
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

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions