From 317ffbf109bedac429684c5c606036f27e103896 Mon Sep 17 00:00:00 2001 From: Cipher208 <269750686+Cipher208@users.noreply.github.com> Date: Wed, 7 Oct 2026 15:33:54 +0200 Subject: [PATCH] fix(graph): give the zero-results miner the same honest-failure contract MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `miner_zero_results` had the same shape as the bug just fixed in `miner_embedding`: one `except Exception` that returned `{"edges": 0}` — the value meaning "there was nothing to surface" — and reported the reason at `debug`, which is invisible at the level this service runs at. A failure in `find_or_add_entity` would therefore look exactly like a quiet night, and the record of what happened would not reach `memory.log`. After the previous commit, `0` means something specific: this miner looked and found no work. Leaving a second path that returns `0` on failure would make that contract a lie again, and the two cases would be indistinguishable by the new per-miner log line as well. The exception now escapes to the handler in `graph_enrich` that records `{"edges": -1}` plus a warning. The reachable `return {"edges": 0}` for "the journal had no repeated misses" stays — that one is genuine. This is the only other swallow in the file. The other `except Exception` left in `graph_miners` is `_get_ner`, which probes whether spaCy is installed and is a legitimate probe rather than a miner. Test: a backend failure escapes `miner_zero_results`, alongside the two existing zero-result tests that pin the real zero. Full suite: 2086 passed. --- lifecycle/graph_miners.py | 21 +++++++++++---------- tests/test_s17_stage1_tail.py | 29 +++++++++++++++++++++++++++++ 2 files changed, 40 insertions(+), 10 deletions(-) diff --git a/lifecycle/graph_miners.py b/lifecycle/graph_miners.py index 3ff81d6..c3ad819 100644 --- a/lifecycle/graph_miners.py +++ b/lifecycle/graph_miners.py @@ -1283,16 +1283,17 @@ async def miner_zero_results(cm: AsyncConnectionManager, layer: str) -> dict[str if not rows: return {"edges": 0} surfaced = 0 - try: - from graph.epistemic import EpistemicGraph - - g = EpistemicGraph(cm=cm, layer=layer) - for r in rows: - await g.find_or_add_entity(str(r["user_id"]), str(r["query"])[:300], "question") - surfaced += 1 - except Exception: - logger.debug("zero-result surfacing failed", exc_info=True) - return {"edges": 0} + # No try/except here, for the same reason `miner_embedding` has none: a + # failure that returns `{"edges": 0}` is indistinguishable from "there was + # nothing to surface", and this one logged at `debug`, which is invisible + # at the level this service actually runs at. Letting it escape reaches the + # handler in `graph_enrich` that records `edges: -1` with a warning. + from graph.epistemic import EpistemicGraph + + g = EpistemicGraph(cm=cm, layer=layer) + for r in rows: + await g.find_or_add_entity(str(r["user_id"]), str(r["query"])[:300], "question") + surfaced += 1 return {"edges": surfaced} diff --git a/tests/test_s17_stage1_tail.py b/tests/test_s17_stage1_tail.py index 75bc71a..4b74f6d 100644 --- a/tests/test_s17_stage1_tail.py +++ b/tests/test_s17_stage1_tail.py @@ -243,6 +243,35 @@ async def test_zero_result_single_hit_not_surfaced(migrated_cm) -> None: assert res["edges"] == 0, "один провал — не сигнал" +@pytest.mark.asyncio +async def test_zero_result_miner_lets_a_failure_escape(migrated_cm, monkeypatch) -> None: + """Сбой обязан выйти наружу, а не стать нулём с записью в `debug`. + + 07.10.2026: здесь был второй такой же глушитель, как в `miner_embedding` — + `except Exception` писал в `debug` (невидимо на рабочем уровне) и возвращал + `{"edges": 0}`, то есть «нечего выносить». Договор один на всех шахтёров: + ноль — это работа, которая не нашлась, а сбой пусть ловит `graph_enrich`. + """ + from graph.epistemic import EpistemicGraph + from lifecycle.graph_miners import log_zero_result, miner_zero_results + from shared.constants import DB_NAME + + cm = migrated_cm + conn = await cm.get(DB_NAME) + await conn.execute("DELETE FROM recall_zero_results") + await conn.commit() + for _ in range(2): + await log_zero_result(cm, "user", "s17zr3", "провал запроса, который надо вынести наверх") + + async def _boom(self, *args, **kwargs): + raise RuntimeError("find_or_add_entity is down") + + monkeypatch.setattr(EpistemicGraph, "find_or_add_entity", _boom) + + with pytest.raises(RuntimeError, match="find_or_add_entity is down"): + await miner_zero_results(cm, "user") + + # ── 7. counter-signal алиасы ─────────────────────────────────────────────────