Skip to content

fix(types): burn pyright type-debt below 568 baseline (#188)#193

Merged
cdeust merged 1 commit into
mainfrom
fix/type-debt-188
Jul 26, 2026
Merged

fix(types): burn pyright type-debt below 568 baseline (#188)#193
cdeust merged 1 commit into
mainfrom
fix/type-debt-188

Conversation

@cdeust

@cdeust cdeust commented Jul 26, 2026

Copy link
Copy Markdown
Owner

Summary

Burns the pyright type-debt back below the 568 ratchet baseline (issue #188, the +79 accrual from PRs #181/#183/#185/#186). No rebaseline — the errors are fixed by making types truthful.

Root cause: MemoryStore is a factory whose __new__ returns a fully-built PgMemoryStore / SqliteMemoryStore, but it was unannotated, so pyright saw MemoryStore(...) and get_shared_store() as producing an empty factory shell. Every store.get_memory(...) across 55+ handlers then read as "Cannot access attribute for class MemoryStore" (207 errors) or "for class object" (29). Annotating the factory to return the real backend union collapses those clusters.

The MemoryStore name is bound to PgMemoryStore | SqliteMemoryStore under TYPE_CHECKING only; the runtime factory class is byte-identical (moved under if not TYPE_CHECKING:), so there is zero behaviour change.

Before / after (pyright, pinned 1.1.410, full CI type-check env)

Rule Baseline floor Before (main) After Δ vs before
reportAttributeAccessIssue 351 387 192 −195
reportReturnType 52 54 18 −36
reportAssignmentType 26 27 1 −26
reportArgumentType 98 117 145 +28 (unmasked)
reportCallIssue 37 42 53 +11 (unmasked)
reportMissingImports 1 7 7 0 (pre-existing)
reportGeneralTypeIssues 2 3 5 +2 (unmasked)
reportReturnType/Operator 1 1 0
TOTAL 568 638 422 −216

Truthful typing unmasks latent argument/call errors that the shell type previously suppressed (the exact "Unknown suppresses downstream errors" phenomenon the CI env comment warns about). These were already latent on main; they now stay visible in the ratchet's tracked (non-blocking) counters for the iterative burn-down #188 describes, instead of hidden. The pg_store.py argument/call cluster is pre-existing baseline debt, not introduced here.

Real bug found (blocking tier — #187 precedent)

Truthful typing exposed a blocking reportOptionalSubscript in handlers/wiki_emerge.py: the cold-start SELECT COUNT(*) read did row["count"] if isinstance(row, dict) else row[0] on cur.fetchone(), which is typed Optional. A COUNT(*) with no GROUP BY always returns exactly one row, so row is None is SQL-unreachable — but the code did not guard it. Added an explicit if row is None: guard degrading to the cold-start path (0 claims). No observable behaviour change on the reachable path; the guarded branch is provably unreachable (documented at the use site), so it is a §12.1(b) documented-equivalent — no regression test is possible or required for an unreachable branch.

Completion Ledger (§13.2)

Code paths introduced by the diff:

Path Evidence
MemoryStore.__new__ runtime class (moved under if not TYPE_CHECKING) Runtime import + construction green: tests_py/integration/test_cold_start.py, tests_py/infrastructure/test_backend_marker.py, test_sqlite_backend.py 47/47
MemoryStore TYPE_CHECKING union alias pyright resolves 207 MemoryStore-shell + 29 object attribute errors → gone (ratchet output)
get_shared_store / _construct_store / _make_sqlite / _try_pg_verbose return annotations pyright ratchet PASS; factory-selection tests green
wiki_emerge row is None guard (True branch → total_claims = 0) SQL-unreachable (COUNT(*) always returns a row); documented-equivalent per §12.1(b)
wiki_emerge row is not None guard (False branch, reachable) Behaviour-identical to prior code; tests_py/core/test_emergence_tracker.py green

§13.1 checklist (applicable items):

Item State Evidence
A1 happy path HANDLED ratchet PASS 422 ≤ 568; sqlite-backend 47/47; factory/cold-start/emergence 69 passed / 2 skipped
A3 failure path (None guard) HANDLED unreachable branch, documented-equivalent §12.1(b)
A5 invariants stated HANDLED _try_pg_verbose/_make_sqlite/factory return types now truthful; guard rationale at use site
E1 additive, no break HANDLED runtime factory byte-identical; MemoryStore(...) API unchanged (verified by import + construction)
E2 downstream consumers HANDLED 55+ handlers annotating store: MemoryStore now resolve the real interface; no annotation edits needed at call sites
G3 deterministic tests HANDLED offline flags, sqlite backend, no new fixtures
G5 suite + gates HANDLED ruff check + format --check . clean tree-wide; pyright ratchet PASS
H1 rules pass HANDLED SOLID/layering intact (DIP: factory return now names the real abstraction); no new anti-patterns
H2/H3 readable, one language HANDLED annotations + one guard; conventions match neighbours
H4 CHANGELOG HANDLED [Unreleased] > Changed entry added
H7 boy-scout (§14) HANDLED see below

§14 Boy-Scout: the diff is annotation-only in memory_store.py plus one guard; no lint/format/dead-code defects seen in touched material remain. Pre-existing local-only test failures (17 in test_memory_store.py: cannot INSERT into generated column heat_base_set_at) are a local SQLite 3.53.3 generated-column artifact — identical on origin/main (verified by stash-run), absent in CI (which uses a SQLite that permits the INSERT). Outside this change's blast radius (unrelated to typing) and not a repo defect (CI green), so not filed.

Test evidence

  • pyright --outputjson mcp_server/ + ratchet, pinned 1.1.410, .[dev,postgresql,sqlite,codebase] + flashrank: PASS, 422 ≤ 568, no blocking rule regressed.
  • ruff==0.15.20 check . && ruff format --check .: clean tree-wide (1004 files).
  • test_sqlite_backend.py: 47 passed.
  • test_cold_start.py + test_backend_marker.py + test_emergence_tracker.py + test_write_class_db_closed_world.py + test_sqlite_hook_paths.py: 69 passed, 2 skipped.
  • Full PG-backed suite deferred to CI (no local PostgreSQL); the runtime factory is byte-identical so behaviour is unchanged.

🤖 Generated with Claude Code

Type the MemoryStore factory's __new__ / get_shared_store / _construct_store
results as the real PgMemoryStore | SqliteMemoryStore union it builds, instead
of the empty factory shell pyright saw. The shell suppressed attribute
resolution across 55+ handlers (every store.get_memory(...) read as an
attribute error on class MemoryStore); the truthful return type drops the
tree-wide pyright total 638 -> 422 (well under the 568 ratchet baseline):
reportAttributeAccessIssue 387->192, reportReturnType 52->18,
reportAssignmentType 26->1.

The MemoryStore name is a TYPE_CHECKING-only union alias; the runtime factory
class is byte-identical, so there is no behaviour change. Truthful typing
unmasked one blocking-tier reportOptionalSubscript in wiki_emerge's cold-start
COUNT(*) read (fetchone() typed Optional, None branch SQL-unreachable) — guarded
explicitly, degrading to the cold-start path rather than raising.

Gates: pyright ratchet PASS (422 <= 568, blocking rules at 0);
ruff check + format --check clean tree-wide; test_sqlite_backend 47/47 and
factory/cold-start/emergence suites green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@cdeust
cdeust merged commit 78a879c into main Jul 26, 2026
14 checks passed
@cdeust
cdeust deleted the fix/type-debt-188 branch July 26, 2026 07:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant