Skip to content

Commit 804af0a

Browse files
committed
fix: g8r2h/yh044 review folds — freshness probe covers highlights, store lifecycle, message precision (critique [21092])
Critic Critical: highlights written locally in the migration-to-fix window were stranded AND guided-upgrade's freshness probe falsely reported the aspects slot clean (it anchored only on document_aspects.extracted_at while migrate_highlights ships document_highlights too). FRESHNESS_PROBES is now multi-probe per slot: freshness confirms only when EVERY probed table answers no-newer-writes; the stranded-window shape is regression-pinned. Recovery on an affected box is now just re-running guided-upgrade (the probe sees the newer ingested_at and re-ships the slot). Reviewer High: dt highlights stores are closed per use (try/finally, matching the _open_plan_library pattern actually cited) — the batch loop leaked one httpx pool per DEVONthink record. Reader distinguishes 'service unavailable' from 'no highlights ingested' (ClickException, never a raw traceback). Salience boost: first failure per process warns with the consequence named (a sustained outage of the now-networked read must not be silently dead); wrong no-close comment corrected (RefreshableHttpStoreMixin.close IS load-bearing). yh044 refusal message branches on frozen-source vs fresh-install shape; dry-run refusal separately pinned. Service-mode salience routing + close both test-pinned. 141 affected tests green.
1 parent 5b474c5 commit 804af0a

7 files changed

Lines changed: 215 additions & 44 deletions

File tree

src/nexus/commands/catalog_cmds/maintenance.py

Lines changed: 21 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -194,19 +194,29 @@ def chash_reconcile_cmd(apply: bool) -> None:
194194
from nexus.db.storage_mode import StorageBackend, storage_backend_for # noqa: PLC0415 — deferred import; rare/branch-local path
195195
from nexus.db.t2.chash_index import ChashIndex # noqa: PLC0415 — deferred import; rare/branch-local path or circular-dep / startup-cost avoidance
196196

197-
# nexus-yh044: refuse BEFORE any work on a migrated install — the local
198-
# SQLite chash_index there is the frozen migration source (RDR-176
199-
# immutability, rollback target until P4b), and the "No T2 db" guard
200-
# below does NOT cover migrated-in-place boxes where the file survives.
197+
# nexus-yh044: refuse BEFORE any work on a service-backed install — the
198+
# "No T2 db" guard below does NOT cover migrated-in-place boxes where
199+
# the file survives frozen. Message branches on which shape this box is
200+
# (critic Significant: SERVICE is the hard default for FRESH installs
201+
# too, where "frozen migration source" would be a false premise).
201202
if storage_backend_for("chash_index") == StorageBackend.SERVICE:
203+
db_path = default_db_path()
204+
if db_path.exists():
205+
raise click.ClickException(
206+
"chash-reconcile is a PRE-MIGRATION repair verb and this "
207+
"install is service-backed. The local SQLite chash_index "
208+
"here is the FROZEN MIGRATION SOURCE (RDR-176: immutable "
209+
"rollback target until RDR-155 P4b) — reconciling it "
210+
"against live T3 would manufacture ghosts and --apply would "
211+
"delete source rows. The PG side needs no reconciliation "
212+
"(RDR-187: the router is retired; chunk tables cannot go "
213+
"stale)."
214+
)
202215
raise click.ClickException(
203-
"chash-reconcile is a PRE-MIGRATION repair verb and this install "
204-
"is service-backed. The local SQLite chash_index here is the "
205-
"FROZEN MIGRATION SOURCE (RDR-176: immutable rollback target "
206-
"until RDR-155 P4b) — reconciling it against live T3 would "
207-
"manufacture ghosts and --apply would delete source rows. The "
208-
"PG side needs no reconciliation (RDR-187: the router is "
209-
"retired; chunk tables cannot go stale)."
216+
"chash-reconcile is a PRE-MIGRATION repair verb: this install "
217+
"is service-backed and has no local T2 database — there is "
218+
"nothing to reconcile. The PG side needs no reconciliation "
219+
"(RDR-187: the router is retired; chunk tables cannot go stale)."
210220
)
211221

212222
db_path = default_db_path()

src/nexus/commands/dt.py

Lines changed: 38 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -326,17 +326,28 @@ def _ingest_highlights_record(uuid: str) -> bool:
326326
# the previous direct DocumentHighlights construction wrote local
327327
# SQLite even on migrated boxes. Low contention either way: one
328328
# write per indexed record, not a long-lived worker (RDR-128 N/A).
329+
# Reviewer High fold: close per use (try/finally, like every
330+
# _open_plan_library caller) — this runs inside the dt-index batch
331+
# loop, and an unclosed HttpDocumentHighlightsStore leaks one httpx
332+
# connection pool PER RECORD. Construction itself is cheap (local
333+
# env/lease resolution, no network), so per-call open/close is the
334+
# minimal leak-free shape.
329335
store = _open_highlights_store()
330336
from datetime import datetime, timezone # noqa: PLC0415 — stdlib deferred to call site (datetime)
331337

332-
return store.upsert(HighlightRecord(
333-
doc_id=str(entry.tumbler),
334-
source_uri=dt_uri,
335-
collection=getattr(entry, "physical_collection", "") or "",
336-
highlights_md=highlights_md,
337-
mentions_md=mentions_md,
338-
ingested_at=datetime.now(timezone.utc).isoformat(),
339-
))
338+
try:
339+
return store.upsert(HighlightRecord(
340+
doc_id=str(entry.tumbler),
341+
source_uri=dt_uri,
342+
collection=getattr(entry, "physical_collection", "") or "",
343+
highlights_md=highlights_md,
344+
mentions_md=mentions_md,
345+
ingested_at=datetime.now(timezone.utc).isoformat(),
346+
))
347+
finally:
348+
_close = getattr(store, "close", None)
349+
if callable(_close):
350+
_close()
340351
except Exception as e: # noqa: BLE001 — DEVONthink boundary op is best-effort; failure logged via log.warning
341352
_log.warning("dt_highlights_failed", uuid=uuid, error=str(e))
342353
return False
@@ -1009,15 +1020,28 @@ def highlights_cmd(tumbler_or_uuid: str) -> None:
10091020
``document_highlights`` T2 table populated by ``nx dt index --highlights``.
10101021
This is a pure T2 read — DEVONthink need not be running.
10111022
"""
1012-
store = _open_highlights_store()
1013-
if _UUID_RE.match(tumbler_or_uuid):
1014-
rec = store.get_by_source_uri(f"x-devonthink-item://{tumbler_or_uuid}")
1015-
elif _TUMBLER_RE.match(tumbler_or_uuid):
1016-
rec = store.get(tumbler_or_uuid)
1017-
else:
1023+
if not (_UUID_RE.match(tumbler_or_uuid) or _TUMBLER_RE.match(tumbler_or_uuid)):
10181024
raise click.ClickException(
10191025
"argument is neither a tumbler (e.g. 1.2.3) nor a UUID.",
10201026
)
1027+
store = _open_highlights_store()
1028+
try:
1029+
if _UUID_RE.match(tumbler_or_uuid):
1030+
rec = store.get_by_source_uri(f"x-devonthink-item://{tumbler_or_uuid}")
1031+
else:
1032+
rec = store.get(tumbler_or_uuid)
1033+
except click.ClickException:
1034+
raise
1035+
except Exception as exc: # noqa: BLE001 — reviewer Medium (nexus-g8r2h): a connection-class failure must read as "service unavailable", never a raw traceback indistinguishable from "no highlights"
1036+
raise click.ClickException(
1037+
f"highlights store unavailable ({type(exc).__name__}: {exc}) — "
1038+
"check `nx doctor` / service status. This is NOT 'no highlights "
1039+
"ingested'."
1040+
) from exc
1041+
finally:
1042+
_close = getattr(store, "close", None)
1043+
if callable(_close):
1044+
_close()
10211045
if rec is None:
10221046
raise click.ClickException(
10231047
f"no ingested highlights for {tumbler_or_uuid} "

src/nexus/migration/guided_upgrade.py

Lines changed: 29 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -357,17 +357,27 @@ def detect_pending_migration_memoized() -> PreflightDetection:
357357
#: probe is used only to decide whether to SKIP re-shipping, never to
358358
#: confirm correctness, and a false skip is still safe (copy-not-move — the
359359
#: source is never at risk, only re-verified less eagerly).
360-
FRESHNESS_PROBES: dict[str, tuple[str, str]] = {
361-
"memory": ("memory", "timestamp"),
362-
"plans": ("plans", "created_at"),
363-
"telemetry": ("search_telemetry", "ts"),
364-
"taxonomy": ("topics", "created_at"),
365-
"aspects": ("document_aspects", "extracted_at"),
360+
FRESHNESS_PROBES: dict[str, tuple[tuple[str, str], ...]] = {
361+
"memory": (("memory", "timestamp"),),
362+
"plans": (("plans", "created_at"),),
363+
"telemetry": (("search_telemetry", "ts"),),
364+
"taxonomy": (("topics", "created_at"),),
365+
# nexus-g8r2h critic Critical (critique [21092]): document_highlights
366+
# rides the "aspects" ETL slot (aspects_etl.migrate_highlights) but the
367+
# probe never anchored on it — a highlights-only local write (the
368+
# pre-g8r2h service-mode writer bug's stranded window) read as "no newer
369+
# local writes": a CONFIDENTLY FALSE clean, worse than no signal. Every
370+
# table an ETL slot ships must be probed; multi-probe slots confirm
371+
# freshness only when EVERY probe answers "no newer writes".
372+
"aspects": (
373+
("document_aspects", "extracted_at"),
374+
("document_highlights", "ingested_at"),
375+
),
366376
# "chash" probe removed (RDR-187/nexus-piwya.9 sweep): unreachable since
367377
# .10 dropped the store from LADDER_ORDER; detect_already_migrated never
368378
# consults it.
369-
"catalog": ("documents", "indexed_at"),
370-
"aspects_queue": ("aspect_extraction_queue", "enqueued_at"),
379+
"catalog": (("documents", "indexed_at"),),
380+
"aspects_queue": (("aspect_extraction_queue", "enqueued_at"),),
371381
}
372382

373383

@@ -537,17 +547,23 @@ def _decide_store(
537547
f"{verification!r} — will migrate",
538548
)
539549

540-
probe = FRESHNESS_PROBES.get(store)
550+
probes = FRESHNESS_PROBES.get(store) or ()
541551
freshness_confirmed = False
542-
if probe is not None and completed_at:
543-
newer = _has_newer_local_writes(sqlite_path, probe, completed_at)
544-
if newer is True:
552+
if probes and completed_at:
553+
answers = [
554+
_has_newer_local_writes(sqlite_path, probe, completed_at)
555+
for probe in probes
556+
]
557+
if any(a is True for a in answers):
545558
return StoreMigrationStatus(
546559
store, False,
547560
f"{store}: local writes newer than the report ({completed_at}) "
548561
"— will migrate",
549562
)
550-
freshness_confirmed = newer is False
563+
# Confirmed only when EVERY probe positively answered "no newer
564+
# writes" — a missing/unreadable table (None) degrades to
565+
# trust-the-report, never to a confident clean (nexus-g8r2h fold).
566+
freshness_confirmed = bool(answers) and all(a is False for a in answers)
551567

552568
if freshness_confirmed:
553569
line = f"{store}: already migrated {completed_at}, no newer local writes"

src/nexus/search_engine.py

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -876,8 +876,22 @@ def _search_one(col: str) -> dict:
876876
query=query,
877877
weight=float(ag_cfg.get("weight", 0.025)),
878878
)
879-
except Exception: # noqa: BLE001 — best-effort salience boost; failure logged at debug, results returned unboosted
880-
_log.debug("salience_boost_failed", exc_info=True)
879+
except Exception: # noqa: BLE001 — best-effort salience boost; failure logged, results returned unboosted
880+
# nexus-g8r2h critique fold: post-routing this guards a real HTTP
881+
# round-trip on service boxes, not a rarely-failing local SQLite
882+
# read. Per-call stays quiet, but the FIRST failure per process
883+
# warns — a sustained outage must not be silently dead for weeks.
884+
global _salience_failure_warned
885+
if not _salience_failure_warned:
886+
_salience_failure_warned = True
887+
_log.warning(
888+
"salience_boost_failed_first",
889+
consequence="attention_guided_v1 boost inactive for this "
890+
"process (subsequent failures log at debug)",
891+
exc_info=True,
892+
)
893+
else:
894+
_log.debug("salience_boost_failed", exc_info=True)
881895

882896
# nexus-1qed: catalog-resolved display path attached as metadata
883897
# so formatters never need to import the catalog. Best-effort;
@@ -1057,6 +1071,11 @@ def _flag_contradictions(
10571071
return out
10581072

10591073

1074+
#: nexus-g8r2h critique fold: first-failure-per-process latch for the
1075+
#: salience boost's swallow (see the caller's except arm).
1076+
_salience_failure_warned: bool = False
1077+
1078+
10601079
def _apply_salience_boost(
10611080
results: list[SearchResult],
10621081
*,
@@ -1117,7 +1136,10 @@ def _apply_salience_boost(
11171136
if boost:
11181137
r.hybrid_score = float(r.hybrid_score) + boost
11191138
finally:
1120-
# HttpDocumentAspectsStore has no close(); the SQLite store does.
1139+
# Both stores close(): the SQLite store closes its connection and
1140+
# HttpDocumentAspectsStore inherits close() from
1141+
# RefreshableHttpStoreMixin (closes the httpx pool — load-bearing,
1142+
# not a no-op; reviewer Low corrected the earlier claim here).
11211143
close = getattr(aspects, "close", None)
11221144
if callable(close):
11231145
close()

tests/migration/test_guided_upgrade_already_migrated.py

Lines changed: 41 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -94,9 +94,12 @@ def _make_sqlite(tmp_path: Path, *, rows: dict[str, str], filename: str = "t2.db
9494
conn = sqlite3.connect(db)
9595
try:
9696
for store, ts in rows.items():
97-
table, column = FRESHNESS_PROBES[store]
98-
conn.execute(f"CREATE TABLE {table} ({column} TEXT)") # noqa: S608 — fixed internal test fixture, no external input
99-
conn.execute(f"INSERT INTO {table} ({column}) VALUES (?)", (ts,))
97+
# Multi-probe stores (nexus-g8r2h: "aspects" also probes
98+
# document_highlights) get every probe table stamped at *ts* so
99+
# all-probes-must-answer freshness semantics hold in fixtures.
100+
for table, column in FRESHNESS_PROBES[store]:
101+
conn.execute(f"CREATE TABLE {table} ({column} TEXT)") # noqa: S608 — fixed internal test fixture, no external input
102+
conn.execute(f"INSERT INTO {table} ({column}) VALUES (?)", (ts,))
100103
conn.commit()
101104
finally:
102105
conn.close()
@@ -379,3 +382,38 @@ def test_newer_clean_report_recovers_from_an_older_dirty_one(
379382
plan = detect_already_migrated(sqlite_path=db, reports_dir=reports_dir)
380383

381384
assert plan.all_skipped is True
385+
386+
387+
def test_highlights_only_local_write_defeats_false_clean(tmp_path: Path) -> None:
388+
"""nexus-g8r2h critic Critical (critique [21092]): a document_highlights
389+
row NEWER than the report — with document_aspects untouched (the exact
390+
stranded-window shape the pre-fix service-mode writer produced) — must
391+
read 'will migrate', never 'already migrated ... no newer local writes'.
392+
Pre-fold, the aspects slot probed only document_aspects.extracted_at and
393+
reported a confidently false clean."""
394+
import sqlite3 as _sqlite3
395+
396+
from nexus.migration.guided_upgrade import _decide_store
397+
398+
db = tmp_path / "t2.db"
399+
conn = _sqlite3.connect(db)
400+
try:
401+
conn.execute("CREATE TABLE document_aspects (extracted_at TEXT)")
402+
conn.execute(
403+
"INSERT INTO document_aspects (extracted_at) VALUES (?)",
404+
(BEFORE_T0,),
405+
)
406+
conn.execute("CREATE TABLE document_highlights (ingested_at TEXT)")
407+
conn.execute(
408+
"INSERT INTO document_highlights (ingested_at) VALUES (?)",
409+
(AFTER_T0,),
410+
)
411+
conn.commit()
412+
finally:
413+
conn.close()
414+
415+
status = _decide_store(
416+
"aspects", [_report(stores={"aspects": 0})], sqlite_path=db,
417+
)
418+
assert status.skip is False
419+
assert "newer than the report" in status.line

tests/test_chash_reconcile.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -332,3 +332,24 @@ def test_service_mode_refuses_loud(monkeypatch, tmp_path) -> None:
332332
assert result.exit_code != 0
333333
assert "pre-migration" in result.output.lower()
334334
assert "frozen migration source" in result.output.lower()
335+
336+
337+
def test_service_mode_refuses_dry_run_too(monkeypatch, tmp_path) -> None:
338+
"""The gate is unconditional on --apply: even the read-only dry-run is
339+
refused on a migrated install (diffing live T3 against the frozen index
340+
produces garbage ghosts — a misleading report is still harm)."""
341+
from click.testing import CliRunner
342+
343+
from nexus.commands.catalog_cmds.maintenance import chash_reconcile_cmd
344+
from nexus.db.storage_mode import StorageBackend
345+
346+
db = tmp_path / "memory.db"
347+
db.write_bytes(b"")
348+
monkeypatch.setattr("nexus.commands._helpers.default_db_path", lambda: db)
349+
monkeypatch.setattr(
350+
"nexus.db.storage_mode.storage_backend_for",
351+
lambda store: StorageBackend.SERVICE,
352+
)
353+
result = CliRunner().invoke(chash_reconcile_cmd, [])
354+
assert result.exit_code != 0
355+
assert "pre-migration" in result.output.lower()

tests/test_rdr_109_phase5_salience.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -292,3 +292,43 @@ def test_salience_boost_no_op_when_no_doc_id(tmp_path: Path, monkeypatch) -> Non
292292
)
293293
out = _apply_salience_boost([r], query="q", weight=0.5)
294294
assert out == [r]
295+
296+
297+
def test_salience_boost_routes_via_http_store_in_service_mode(monkeypatch) -> None:
298+
"""nexus-g8r2h fold (sweep [21089] item 8): on a service-backed box the
299+
boost must read salient_sentences through HttpDocumentAspectsStore —
300+
the old direct DocumentAspects(memory.db) read served STALE frozen
301+
pre-migration rows. Also pins that the store's close() is called (it
302+
closes the httpx pool — load-bearing, reviewer Low)."""
303+
from nexus.db.storage_mode import StorageBackend
304+
305+
calls: dict = {"salient": [], "closed": 0}
306+
307+
class _FakeHttpAspects:
308+
def get_salient_sentences(self, doc_id: str) -> list[str]:
309+
calls["salient"].append(doc_id)
310+
return ["hybrid retrieval cross-encoder reranking"] if doc_id == "B" else []
311+
312+
def close(self) -> None:
313+
calls["closed"] += 1
314+
315+
monkeypatch.setattr(
316+
"nexus.db.storage_mode.storage_backend_for",
317+
lambda store: StorageBackend.SERVICE,
318+
)
319+
monkeypatch.setattr(
320+
"nexus.db.t2.http_document_aspects_store.HttpDocumentAspectsStore",
321+
lambda: _FakeHttpAspects(),
322+
)
323+
324+
from nexus.search_engine import _apply_salience_boost
325+
results = [
326+
_make_result("a", "knowledge__rag", "A", score=0.50),
327+
_make_result("b", "knowledge__rag", "B", score=0.45),
328+
]
329+
out = _apply_salience_boost(
330+
results, query="hybrid retrieval cross-encoder", weight=0.5,
331+
)
332+
assert [r.id for r in out] == ["b", "a"]
333+
assert sorted(calls["salient"]) == ["A", "B"]
334+
assert calls["closed"] == 1

0 commit comments

Comments
 (0)