Suppressions and debt accumulate faster than they are drained β the track ratchets the hook-suppression count to shrink-only, then audits and drains it.
CLOSED as stale; a merged PR resolves to MERGED, so a # noqa citing one passes forever. Measured on main: six cited trackers, two of them merged PRs, reported by --audit-trackers as “none closed” β invisible debt, and citing a merged PR is strictly better for passing the gate than citing an open issue. Fix shapes: treat MERGED as stale (the minimum), or reject PR references at write time. Fixing it will likely turn the nightly red on the two existing citations β resolve or re-point those two in the same change.PersonDataRepository, PersonResolutionRepository) while production calls only the PersonIdentityRepository twin, so eight real-DB tests guard code paths production has routed around. Split out of the V1-XFAIL drain batch (#2986, merged), which deleted three sibling methods on the same basis but left these eight because deleting eight methods from two live classes is a bigger change than a list drain and deserves its own review.Ratchet before drain, then close the tracker's blind spot before trusting its counts, then drain what production has routed around. Applied top to bottom; each rung only breaks ties left by the one above it.
What this deliberately ignores: the backlog size (603 is a moving denominator β the ratchet moves first), issue age or filing order, how many suppressions one change removes, the apparent smallness of a deletion (the eight methods are, per their own issue, “a bigger change than a list drain”), and priority labels alone (the two p2s are ordered by the ratchet rule, not by label).
Evidence below is generated from the read-only audit of 2026-08-29 and reconciled against the open issues at build time. Every claim cites the issue it came from. Nothing here is a decision β rulings stay in the prose above, which is written by hand.
PR-A partially landed: `ca6d3de93` ("retire 12 legacy v1 graph-building phases + dead types (Epic 4 PR-A, #1323)", PR #1550) is on origin/main, and `backend/app/slices/` has no `inference/` directory (`git ls-tree -d origin/main backend/app/slices/` lists 20 dirs, inference absent). PR-B, PR-C, PR-D are NOT done: production schema census shows `public.nodes_v2` (111,542 exact rows) and `public.edges_v2` (233,603) still carrying the `_v2` suffix β the rename never happened. `git grep -n substrate_version origin/main -- backend/app` still returns `app/db/substrate_version.py` live and imported by `app/db/substrate_routing.py:28` and `app/slices/message/repo/search.py:34`. The flags survive: `backend/app/models/tenant.py:162 conversation_graph_writer_enabled: bool = True`, plus read/write endpoints at `backend/app/api/endpoints/platform/feature_flags.py:27,35,40,87`, and `legacy_extraction_disabled` documented live at `backend/app/core/config.py:638`. `org_inference_phase.py` still exists at `backend/app/slices/graph_building/phases/org_inference_phase.py`.
Of the six named callsites, two were deleted with their phases and four are still unconverted, with the noqa the issue says to remove still in place. Still violating: `backend/app/connectors/whatsapp/webhook_connection.py:244` and `:624` β both `await self._instance_repo.update(instance.id, upd) # noqa: materializer-writes` (the file moved from `webhook.py`, the pattern did not change); `backend/app/orchestration/phases/sensitivity_classification_phase.py:56` still carries the verbatim justification `# noqa: materializer-writes β direct SensitivityEvidenceRepository write (propagate_conversation_tiers); pre-dates ctx.repos write surface (see #1341)`, and line 458 still calls `self._sensitivity_repo.propagate_conversation_tiers()`. Gone, not fixed: `entities_phase.py` and `topic_segmentation_phase.py` no longer exist under `backend/app/orchestration/phases/` (retired by ca6d3de93); `git grep -n "get_or_create(" origin/main -- backend/app/orchestration/phases/person_resolution_phase.py` returns ZERO hits, so that third site is also gone by deletion rather than by an intent path. Scale check: `git grep -h "noqa: materializer-writes" origin/main -- backend/` counts 611 suppressions, versus the ~26 the issue body describes.
origin/main:backend/app/services/ask/reasoning/retrieval.py:40 still carries `from app.slices.message.repo import ( # noqa: materializer-writes β read-only (hybrid_search) from a service that has no ctx in scope; ... see #1342)`, and the direct call survives at retrieval.py:287 (`message_repository.hybrid_search(...)`, as_user=True). `git show origin/main:backend/app/orchestration/ctx_repos/messages.py | grep 'def '` lists 24 methods (get_messages_without_embeddings β¦ get_latest_timestamp_for_person) and NONE is hybrid_search β so the requested ctx.repos surface does not exist. Sub-premise check: `git grep -n "no ctx.repos path exists for agent tools" origin/main -- backend/app` β 0 hits, so the '21 ask-layer read' noqa wording in the body no longer exists in the tree; the materializer-writes noqa population is now 148 files (`git grep -c 'noqa: materializer-writes' origin/main -- backend/app | wc -l`).
`git ls-tree -r --name-only origin/main backend/app/services/ask/ | wc -l` β 93 files (grown from the body's ~43); `git ls-tree -r --name-only origin/main backend/app/slices/ask/ | wc -l` β 0. The slice does not exist. HOWEVER the body's headline cost is STALE: .pre-commit-config.yaml:352-358 now reads 'Collect ALL of `app/` recursively so the tests run against the same tree --cov=app measures β enumerated globs silently dropped colocated tests outside `app/slices/` (app/core, app/services, ...)', and colocated tests now live inside the tree (derived_test.py, graph_traversal_test.py, tools/_candidate_disclosure_test.py, reasoning/surface_relevant_to_now_test.py, plus app/services/ask/tests/integration/). So the coverage-hook rationale is closed; only the slice-gate/public-API rationale remains.
Step 1 (the ratchet, which the issue's own checklist puts FIRST) has SHIPPED and is reachable, not merely present: `backend/scripts/hooks/suppression_ratchet.py` + `backend/scripts/hooks/suppression_baseline.json` (32,241 bytes, per-fileβruleβkind counts) exist on origin/main, and the hook is WIRED in `.pre-commit-config.yaml:235-242` (`id: suppression-ratchet`, `files: ^backend/(app|scripts|tests)/.*\.py$`, `pass_filenames: false`). The design constraints the issue demanded were honoured: the baseline distinguishes `tracked` (cites #NNNN) / `permanent` / `unjustified`, gates unjustified growth specifically, and records `malformed` tokens matching no rule β closing the PERMANENT-vs-DEFERRED conflation and the malformed `# noqa: function-level import` family the body called out. The openness half the issue said a count-only ratchet cannot see also shipped: `.github/workflows/suppression-tracker-nightly.yml` (`cron: 0 6 * * 1`, calls `suppression_ratchet.py --audit-trackers`), whose header states exactly this issue's premise. A companion `backend/scripts/hooks/noop_suppression_check.py` is wired at .pre-commit-config.yaml:267-271. Step 2 (audit) was executed and reported in the issue's own comment. What remains open is steps 3-4 (document per-category rulings, drain), which are a different, smaller task than the issue title. Prod cross-check: the weekly tracker job ran 2026-08-24 and ended `failure` (2 of last 3) β consistent with its documented design of exiting non-zero when stale trackers are found, but I did not read the run log.
6 of the 8 rows are still exactly as described on origin/main; 2 are stale. Still present as dead twins: `find_person_by_email` (person_resolution_repository.py:113), `find_person_by_phone` (:89), `find_persons_by_display_name` (:67), `link_person_to_user` (person_data_repository.py:117), `update_person_user_id` (:499), `merge_person_properties` (:535). Every non-test production call site resolves on the PersonIdentityRepository receiver, not these: identity_service.py:334,590,605,389; jid_auto_matcher.py:190,215,323; webhook_connection.py:472,501; profile_fetch_queue.py:573; platform_service.py:394 β all `person_identity_repo.`/`_person_identity_repo.`. Search that establishes the absence of callers on the old receivers: `git grep -n "\.<method>(" origin/main -- backend | grep -v test`, population = every .py under backend/ on origin/main, run for all 8 names; the only non-test hits are the identity-repo receivers listed plus one intra-class self-call at person_identity_reads_repository.py:316. Stale rows: `find_person_node_ids_by_user_ids` no longer exists anywhere on origin/main (`git grep -n "def find_person_node_ids_by_user_ids" origin/main -- backend` β no output) β both the PersonDataRepository copy (deleted by PR #3169, per the issue comment) and the identity twin are gone; and `find_unlinked_persons` now exists only on person_identity_admin_repository.py:280, so its PersonDataRepository copy is already removed.
backend/scripts/hooks/suppression_ratchet.py:466 on origin/main still reads `if probe.stdout.strip() == "CLOSED":` and nothing else; `git grep -n 'MERGED' origin/main -- backend/scripts/hooks/suppression_ratchet.py` returns no hits (all MERGED handling in the repo lives under scripts/auto_fix/, a different tool). Line 476 still prints `tracker audit: OK ({len(cited)} distinct tracker(s) cited, none closed)`, so fix shape 3 is unimplemented too. The three commits touching this file since the ratchet was built β f05473b63, 65fd52ac7, 9843e6dcf (PR #3606, most recent) β none touch audit_trackers. Compounding, from the prod cron census: suppression-tracker-nightly.yml (`0 6 * * 1`) FAILED 2 of its last 3 weekly runs, so the audit itself is not green.
track-order.json records no decided rulings for this track. The rulings in force are the two the page already cites: ratchet FIRST β baseline today's count shrink-only, then audit and drain (owner ruling 2026-08-02, stated in the suppression ratchet, #2976); and paused for WIRE-TRUTH, resume at the ratchet (the index row).
The ratchet's measured instance. A # noqa justification (“deferred to post-Landing-11 read-API migration”) was false for 2.5 months β Landing 11 closed 2026-05-16 β and nothing noticed. The per-file .ai/materializer-write-allowlist.txt that once counted suppressions was drained 145 β 0 and deleted 2026-07-12 (the allowlist retirement, PR #2411). Over that window per-line noqa went 211 β 425, then 425 β 516.
Blind-spot provenance. The merged-reference hole was found by the round-3 adversarial lens on the no-op suppression drain (PR #3302, merged β retired 22 no-op suppressions). The two merged PRs cited as trackers are the WhatsApp reply/mention context PR #1695 and the live Exchange staged-drain PR #1834.
Superseded-methods provenance. The 8-method issue split out of the V1-XFAIL drain batch (#2986, merged β drained test_person_repositories_e2e.py 66 β 51), which deleted three sibling methods on the same zero-caller basis.