fix(types): burn pyright type-debt below 568 baseline (#188)#193
Merged
Conversation
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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:
MemoryStoreis a factory whose__new__returns a fully-builtPgMemoryStore/SqliteMemoryStore, but it was unannotated, so pyright sawMemoryStore(...)andget_shared_store()as producing an empty factory shell. Everystore.get_memory(...)across 55+ handlers then read as "Cannot access attribute for classMemoryStore" (207 errors) or "for classobject" (29). Annotating the factory to return the real backend union collapses those clusters.The
MemoryStorename is bound toPgMemoryStore | SqliteMemoryStoreunderTYPE_CHECKINGonly; the runtime factory class is byte-identical (moved underif not TYPE_CHECKING:), so there is zero behaviour change.Before / after (pyright, pinned 1.1.410, full CI type-check env)
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. Thepg_store.pyargument/call cluster is pre-existing baseline debt, not introduced here.Real bug found (blocking tier — #187 precedent)
Truthful typing exposed a blocking
reportOptionalSubscriptinhandlers/wiki_emerge.py: the cold-startSELECT COUNT(*)read didrow["count"] if isinstance(row, dict) else row[0]oncur.fetchone(), which is typedOptional. ACOUNT(*)with noGROUP BYalways returns exactly one row, sorow is Noneis SQL-unreachable — but the code did not guard it. Added an explicitif 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:
MemoryStore.__new__runtime class (moved underif not TYPE_CHECKING)tests_py/integration/test_cold_start.py,tests_py/infrastructure/test_backend_marker.py,test_sqlite_backend.py47/47MemoryStoreTYPE_CHECKING union aliasMemoryStore-shell + 29objectattribute errors → gone (ratchet output)get_shared_store/_construct_store/_make_sqlite/_try_pg_verbosereturn annotationswiki_emergerow is Noneguard (True branch →total_claims = 0)wiki_emergerow is not Noneguard (False branch, reachable)tests_py/core/test_emergence_tracker.pygreen§13.1 checklist (applicable items):
_try_pg_verbose/_make_sqlite/factory return types now truthful; guard rationale at use siteMemoryStore(...)API unchanged (verified by import + construction)store: MemoryStorenow resolve the real interface; no annotation edits needed at call sitesformat --check .clean tree-wide; pyright ratchet PASS[Unreleased] > Changedentry added§14 Boy-Scout: the diff is annotation-only in
memory_store.pyplus one guard; no lint/format/dead-code defects seen in touched material remain. Pre-existing local-only test failures (17 intest_memory_store.py:cannot INSERT into generated column heat_base_set_at) are a local SQLite 3.53.3 generated-column artifact — identical onorigin/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.🤖 Generated with Claude Code