From 102a7beeeb275ec842f747461618e8ced2277b07 Mon Sep 17 00:00:00 2001 From: Sinity Date: Fri, 7 Aug 2026 11:40:17 +0200 Subject: [PATCH 1/8] fix(maintenance): harden cursor reconciliation proof gates Problem: the scoped reconciliation route rejected a disappeared selected cursor row when the known incomparable population remained, and backup validation trusted receipt-level blob claims without re-reading the current backup files. What changed: emit a typed not_applicable plan for that zero-ahead case, re-hash every content-addressed backup blob, compare the live inventory with blob-inventory.json and the authenticated verification receipt, and include the verified inventory summary in reconciliation evidence. Compatibility/migration: the live cursor-authority receipt remains a separate production obligation. No production archive was mutated. Ref polylogue-s8gcr Co-Authored-By: Claude --- .../maintenance/cursor_authority_reconcile.py | 124 +++++++++++++++++- .../test_cursor_authority_reconcile.py | 66 +++++++++- 2 files changed, 186 insertions(+), 4 deletions(-) diff --git a/polylogue/maintenance/cursor_authority_reconcile.py b/polylogue/maintenance/cursor_authority_reconcile.py index 01ee7a0fdd..fc63a3e60d 100644 --- a/polylogue/maintenance/cursor_authority_reconcile.py +++ b/polylogue/maintenance/cursor_authority_reconcile.py @@ -357,7 +357,23 @@ def _build_plan(root: Path, source_path: Path, *, require_candidate: bool = True _require_healthy_projection_siblings(projection) path_digest = cursor_authority_path_digest(source_path) if projection.cursor_ahead_count == 0: - if require_candidate and projection.cursor_authority_gap_count == 0 and projection.overall_status == "healthy": + current_cursor_paths = {Path(path).resolve() for path, _offset in _cursor_rows(root)} + selected_path_disappeared = source_path.resolve() not in current_cursor_paths + incomparable_population_preserved = ( + projection.available + and projection.cursor_ahead_status == "unknown" + and projection.cursor_authority_gap_count > 0 + and projection.broken_head_status == "healthy" + and projection.missing_source_raw_status == "healthy" + ) + if require_candidate and ( + projection.overall_status == "healthy" or (selected_path_disappeared and incomparable_population_preserved) + ): + reason = ( + "selected cursor row disappeared while the pre-existing incomparable authority population remained" + if selected_path_disappeared and incomparable_population_preserved + else "selected cursor-ahead violation is no longer present" + ) not_applicable_plan: dict[str, object] = { "format": PLAN_FORMAT, "archive_identity": _path_identity(root), @@ -369,6 +385,7 @@ def _build_plan(root: Path, source_path: Path, *, require_candidate: bool = True "selected_path_digest": path_digest, "observed_at_ms": int(time.time() * 1000), "status": "not_applicable", + "not_applicable_reason": reason, "cursor_byte_offset": None, "accepted_frontier": None, "accepted_raw_id_digest": None, @@ -436,6 +453,104 @@ def _backup_root(manifest_path: Path) -> Path: return root +def _validated_blob_inventory( + root: Path, + manifest: Mapping[str, object], + receipt: Mapping[str, object], +) -> dict[str, object]: + """Re-hash the current backup blob files and compare them with the receipt.""" + + inventory_path = root / str(manifest.get("blob_inventory_file", "blob-inventory.json")) + try: + declared = json.loads(inventory_path.read_text(encoding="utf-8")) + except (OSError, ValueError) as exc: + raise CursorAuthorityReconciliationError("backup blob inventory is unreadable") from exc + expected = receipt.get("blobs") + if not isinstance(declared, list) or not isinstance(expected, list): + raise CursorAuthorityReconciliationError("backup blob inventory is not fully attested") + + declared_by_hash: dict[str, dict[str, object]] = {} + for item in declared: + if not isinstance(item, dict) or not isinstance(item.get("blob_hash"), str): + raise CursorAuthorityReconciliationError("backup blob inventory contains an invalid row") + blob_hash = str(item["blob_hash"]).lower() + if len(blob_hash) != 64 or any(character not in "0123456789abcdef" for character in blob_hash): + raise CursorAuthorityReconciliationError("backup blob inventory contains an invalid blob hash") + if blob_hash in declared_by_hash: + raise CursorAuthorityReconciliationError("backup blob inventory contains duplicate blob hashes") + declared_by_hash[blob_hash] = item + + actual_rows: list[dict[str, object]] = [] + blob_root = root / "blob" + for path in sorted(blob_root.rglob("*")): + if path.is_symlink(): + raise CursorAuthorityReconciliationError("backup blob inventory contains a symlink") + if not path.is_file(): + continue + relative = path.relative_to(root).as_posix() + if len(path.parent.name) != 2 or len(path.name) != 62: + raise CursorAuthorityReconciliationError(f"backup blob path is not content-addressed: {relative}") + blob_hash = f"{path.parent.name}{path.name}".lower() + if blob_hash not in declared_by_hash: + raise CursorAuthorityReconciliationError("backup contains a blob absent from blob-inventory.json") + size_bytes, sha256 = _file_fingerprint(path) + if sha256 != blob_hash: + raise CursorAuthorityReconciliationError(f"backup blob digest does not match its path: {relative}") + declared_item = declared_by_hash[blob_hash] + protection = declared_item.get("protection") + if not isinstance(protection, list) or not all(isinstance(value, str) for value in protection): + raise CursorAuthorityReconciliationError("backup blob inventory has invalid protection metadata") + if declared_item.get("size_bytes") != size_bytes: + raise CursorAuthorityReconciliationError("backup blob size disagrees with blob-inventory.json") + actual_rows.append( + { + "blob_hash": blob_hash, + "path": relative, + "size_bytes": size_bytes, + "sha256": sha256, + "protection": sorted(str(value) for value in protection), + } + ) + + actual_rows.sort(key=lambda item: str(item["blob_hash"])) + expected_rows = sorted( + [ + { + "blob_hash": item.get("blob_hash"), + "path": item.get("path"), + "size_bytes": item.get("size_bytes"), + "sha256": item.get("sha256"), + "protection": sorted(str(value) for value in item.get("protection", [])) + if isinstance(item.get("protection"), list) + else item.get("protection"), + } + for item in expected + if isinstance(item, dict) + ], + key=lambda item: str(item["blob_hash"]), + ) + if expected_rows != actual_rows: + raise CursorAuthorityReconciliationError( + "current backup blob inventory does not match its verification receipt" + ) + if len(declared_by_hash) != len(actual_rows): + raise CursorAuthorityReconciliationError("blob-inventory.json contains a missing backup blob") + manifest_count = manifest.get("blob_count") + if isinstance(manifest_count, int) and manifest_count != len(actual_rows): + raise CursorAuthorityReconciliationError("backup manifest blob count does not match current blob inventory") + total_size_bytes = 0 + for item in actual_rows: + row_size_bytes = item["size_bytes"] + if not isinstance(row_size_bytes, int): + raise CursorAuthorityReconciliationError("current backup blob inventory has an invalid size") + total_size_bytes += row_size_bytes + return { + "count": len(actual_rows), + "size_bytes": total_size_bytes, + "inventory_digest": _canonical_digest(actual_rows), + } + + def _validate_backup(manifest_path: Path, plan: Mapping[str, object]) -> dict[str, object]: root = _backup_root(manifest_path) try: @@ -456,6 +571,7 @@ def _validate_backup(manifest_path: Path, plan: Mapping[str, object]) -> dict[st raise CursorAuthorityReconciliationError("backup lacks complete blob rollback evidence") if not (root / "blob").is_dir() or not (root / "blob-inventory.json").is_file(): raise CursorAuthorityReconciliationError("backup lacks blob rollback evidence") + blob_inventory = _validated_blob_inventory(root, manifest, receipt) archive_root = _archive_root() location = ArchiveLocation.resolve(archive_root) expected_active_index = plan.get("active_index") @@ -503,7 +619,11 @@ def _validate_backup(manifest_path: Path, plan: Mapping[str, object]) -> dict[st raise CursorAuthorityReconciliationError( "backup index fingerprint does not bind the active index generation" ) - return {"root": _path_identity(root), "manifest_sha256": _sha256_file(root / "manifest.json")} + return { + "root": _path_identity(root), + "manifest_sha256": _sha256_file(root / "manifest.json"), + "blob_inventory": blob_inventory, + } def _quick_checks(root: Path) -> dict[str, list[str]]: diff --git a/tests/unit/maintenance/test_cursor_authority_reconcile.py b/tests/unit/maintenance/test_cursor_authority_reconcile.py index 4e5c16c843..b0dcae1803 100644 --- a/tests/unit/maintenance/test_cursor_authority_reconcile.py +++ b/tests/unit/maintenance/test_cursor_authority_reconcile.py @@ -2,6 +2,7 @@ from __future__ import annotations +import hashlib import json import sqlite3 from dataclasses import replace @@ -168,6 +169,35 @@ def test_planner_preserves_incomparable_population(monkeypatch: pytest.MonkeyPat watcher.stop() +def test_disappeared_selected_row_is_typed_not_applicable_with_incomparable_population( + monkeypatch: pytest.MonkeyPatch, tmp_path: Path +) -> None: + from tests.unit.sources.test_live_watcher import _seed_live_cursor_authority_case + + _processor, watcher, _cursor, source_path = _seed_live_cursor_authority_case(tmp_path) + projection = replace( + reconcile._projection_for(tmp_path), + overall_status="unknown", + cursor_ahead_status="unknown", + cursor_ahead_count=0, + cursor_ahead_samples=(), + cursor_authority_gap_count=2, + ) + monkeypatch.setattr(reconcile, "_projection_for", lambda root: projection) + monkeypatch.setattr(reconcile, "_cursor_rows", lambda root: []) + + plan = reconcile._build_plan(tmp_path, source_path) + + assert plan["status"] == "not_applicable" + reason = plan["not_applicable_reason"] + assert isinstance(reason, str) + assert "disappeared" in reason + before_projection = plan["before_projection"] + assert isinstance(before_projection, dict) + assert before_projection["cursor_authority_gap_count"] == 2 + watcher.stop() + + def test_planner_refuses_multiple_true_ahead_rows(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None: from tests.unit.sources.test_live_watcher import _seed_live_cursor_authority_case @@ -357,8 +387,15 @@ def test_backup_validation_rehashes_and_rejects_mismatched_tier( ) -> None: backup = tmp_path / "backup" backup.mkdir() - (backup / "blob").mkdir() - (backup / "blob-inventory.json").write_text("{}", encoding="utf-8") + blob_payload = b"blob" + blob_hash = hashlib.sha256(blob_payload).hexdigest() + blob_path = backup / "blob" / blob_hash[:2] / blob_hash[2:] + blob_path.parent.mkdir(parents=True) + blob_path.write_bytes(blob_payload) + (backup / "blob-inventory.json").write_text( + json.dumps([{"blob_hash": blob_hash, "size_bytes": len(blob_payload), "protection": ["referenced"]}]), + encoding="utf-8", + ) tiers: dict[str, dict[str, object]] = {} for tier in ("source", "index", "ops", "audit"): path = backup / f"{tier}.db" @@ -381,6 +418,15 @@ def test_backup_validation_rehashes_and_rejects_mismatched_tier( "index_attachment_blobs_resolved": True, "blob_inventory_exact": True, }, + "blobs": [ + { + "blob_hash": blob_hash, + "path": f"blob/{blob_hash[:2]}/{blob_hash[2:]}", + "size_bytes": len(blob_payload), + "sha256": blob_hash, + "protection": ["referenced"], + } + ], } ), encoding="utf-8", @@ -392,6 +438,22 @@ def test_backup_validation_rehashes_and_rejects_mismatched_tier( validated = reconcile._validate_backup(backup, plan) assert isinstance(validated["root"], dict) assert validated["root"]["basename"] == backup.name + blob_path.write_bytes(b"changed") + with pytest.raises(reconcile.CursorAuthorityReconciliationError, match="blob digest"): + reconcile._validate_backup(backup, plan) + blob_path.write_bytes(blob_payload) + blob_path.unlink() + with pytest.raises(reconcile.CursorAuthorityReconciliationError, match="blob inventory"): + reconcile._validate_backup(backup, plan) + blob_path.write_bytes(blob_payload) + extra_payload = b"extra" + extra_hash = hashlib.sha256(extra_payload).hexdigest() + extra_path = backup / "blob" / extra_hash[:2] / extra_hash[2:] + extra_path.parent.mkdir(parents=True) + extra_path.write_bytes(extra_payload) + with pytest.raises(reconcile.CursorAuthorityReconciliationError, match="absent from blob-inventory"): + reconcile._validate_backup(backup, plan) + extra_path.unlink() (backup / "audit.db").unlink() with pytest.raises(reconcile.CursorAuthorityReconciliationError, match="tier is missing"): reconcile._validate_backup(backup, plan) From 27f8fe45c677be203210616310d5df3f873c1637 Mon Sep 17 00:00:00 2001 From: Sinity Date: Fri, 7 Aug 2026 12:10:36 +0200 Subject: [PATCH 2/8] fix(maintenance): enforce canonical cursor backup proof Problem: The reconciliation backup gate could validate noncanonical inventory/blob locations, and a canonical unavailable projection could not produce the documented no-op plan. What changed: Bind inventory validation to blob-inventory.json, reject symlinked and noncanonical blob paths, and accept only the exact disappeared-cursor unknown projection. Extend real-route tests. Compatibility/migration: No archive data or cursor row is changed. Ref polylogue-s8gcr Co-Authored-By: Claude --- .../maintenance/cursor_authority_reconcile.py | 91 ++++++++++++------- .../test_cursor_authority_reconcile.py | 69 +++++++++++++- 2 files changed, 122 insertions(+), 38 deletions(-) diff --git a/polylogue/maintenance/cursor_authority_reconcile.py b/polylogue/maintenance/cursor_authority_reconcile.py index fc63a3e60d..e92818e67b 100644 --- a/polylogue/maintenance/cursor_authority_reconcile.py +++ b/polylogue/maintenance/cursor_authority_reconcile.py @@ -351,50 +351,65 @@ def _require_healthy_projection_siblings(projection: RawFrontierIntegrityProject raise CursorAuthorityReconciliationError("raw-frontier sibling projections are not healthy") +def _is_disappeared_cursor_incomparable_projection(projection: RawFrontierIntegrityProjection) -> bool: + """Recognize the sole unavailable projection that proves a scoped no-op.""" + + return ( + not projection.available + and projection.overall_status == "unknown" + and projection.broken_head_status == "healthy" + and projection.missing_source_raw_status == "healthy" + and projection.cursor_ahead_status == "unknown" + and projection.cursor_ahead_count == 0 + and projection.cursor_authority_gap_count > 0 + ) + + def _build_plan(root: Path, source_path: Path, *, require_candidate: bool = True) -> dict[str, object]: tiers = _tier_snapshots(root) projection = _projection_for(root) - _require_healthy_projection_siblings(projection) path_digest = cursor_authority_path_digest(source_path) + not_applicable_reason: str | None = None if projection.cursor_ahead_count == 0: current_cursor_paths = {Path(path).resolve() for path, _offset in _cursor_rows(root)} selected_path_disappeared = source_path.resolve() not in current_cursor_paths - incomparable_population_preserved = ( - projection.available - and projection.cursor_ahead_status == "unknown" - and projection.cursor_authority_gap_count > 0 - and projection.broken_head_status == "healthy" - and projection.missing_source_raw_status == "healthy" - ) - if require_candidate and ( - projection.overall_status == "healthy" or (selected_path_disappeared and incomparable_population_preserved) + if ( + require_candidate + and selected_path_disappeared + and _is_disappeared_cursor_incomparable_projection(projection) ): - reason = ( + not_applicable_reason = ( "selected cursor row disappeared while the pre-existing incomparable authority population remained" - if selected_path_disappeared and incomparable_population_preserved - else "selected cursor-ahead violation is no longer present" ) - not_applicable_plan: dict[str, object] = { - "format": PLAN_FORMAT, - "archive_identity": _path_identity(root), - "active_index": _active_index_binding(root), - "code_sha": _code_sha(), - "deployed_package_sha": _deployed_package_sha(), - "tier_fingerprints": tiers, - "source_schema_versions": {tier: tiers[tier]["user_version"] for tier in _REQUIRED_TIERS}, - "selected_path_digest": path_digest, - "observed_at_ms": int(time.time() * 1000), - "status": "not_applicable", - "not_applicable_reason": reason, - "cursor_byte_offset": None, - "accepted_frontier": None, - "accepted_raw_id_digest": None, - "source_prefix_digest": None, - "before_projection": _private_projection(projection), - } - not_applicable_plan["plan_digest"] = _canonical_digest(not_applicable_plan) - return not_applicable_plan - raise CursorAuthorityReconciliationError("cursor authority is incomparable or has no selected violation") + + if not_applicable_reason is None: + _require_healthy_projection_siblings(projection) + if projection.cursor_ahead_count == 0 and not_applicable_reason is None: + if require_candidate and projection.overall_status == "healthy": + not_applicable_reason = "selected cursor-ahead violation is no longer present" + else: + raise CursorAuthorityReconciliationError("cursor authority is incomparable or has no selected violation") + if not_applicable_reason is not None: + not_applicable_plan: dict[str, object] = { + "format": PLAN_FORMAT, + "archive_identity": _path_identity(root), + "active_index": _active_index_binding(root), + "code_sha": _code_sha(), + "deployed_package_sha": _deployed_package_sha(), + "tier_fingerprints": tiers, + "source_schema_versions": {tier: tiers[tier]["user_version"] for tier in _REQUIRED_TIERS}, + "selected_path_digest": path_digest, + "observed_at_ms": int(time.time() * 1000), + "status": "not_applicable", + "not_applicable_reason": not_applicable_reason, + "cursor_byte_offset": None, + "accepted_frontier": None, + "accepted_raw_id_digest": None, + "source_prefix_digest": None, + "before_projection": _private_projection(projection), + } + not_applicable_plan["plan_digest"] = _canonical_digest(not_applicable_plan) + return not_applicable_plan if projection.cursor_ahead_count != 1: raise CursorAuthorityReconciliationError("refusing to guess among multiple cursor-ahead rows") if projection.broken_head_count or projection.missing_source_raw_count: @@ -460,7 +475,9 @@ def _validated_blob_inventory( ) -> dict[str, object]: """Re-hash the current backup blob files and compare them with the receipt.""" - inventory_path = root / str(manifest.get("blob_inventory_file", "blob-inventory.json")) + if manifest.get("blob_inventory_file") != "blob-inventory.json": + raise CursorAuthorityReconciliationError("backup uses a noncanonical blob inventory path") + inventory_path = root / "blob-inventory.json" try: declared = json.loads(inventory_path.read_text(encoding="utf-8")) except (OSError, ValueError) as exc: @@ -482,6 +499,8 @@ def _validated_blob_inventory( actual_rows: list[dict[str, object]] = [] blob_root = root / "blob" + if blob_root.is_symlink(): + raise CursorAuthorityReconciliationError("backup blob root is a symlink") for path in sorted(blob_root.rglob("*")): if path.is_symlink(): raise CursorAuthorityReconciliationError("backup blob inventory contains a symlink") @@ -491,6 +510,8 @@ def _validated_blob_inventory( if len(path.parent.name) != 2 or len(path.name) != 62: raise CursorAuthorityReconciliationError(f"backup blob path is not content-addressed: {relative}") blob_hash = f"{path.parent.name}{path.name}".lower() + if relative != f"blob/{blob_hash[:2]}/{blob_hash[2:]}": + raise CursorAuthorityReconciliationError(f"backup blob path is not canonical: {relative}") if blob_hash not in declared_by_hash: raise CursorAuthorityReconciliationError("backup contains a blob absent from blob-inventory.json") size_bytes, sha256 = _file_fingerprint(path) diff --git a/tests/unit/maintenance/test_cursor_authority_reconcile.py b/tests/unit/maintenance/test_cursor_authority_reconcile.py index b0dcae1803..7d2527c033 100644 --- a/tests/unit/maintenance/test_cursor_authority_reconcile.py +++ b/tests/unit/maintenance/test_cursor_authority_reconcile.py @@ -177,6 +177,7 @@ def test_disappeared_selected_row_is_typed_not_applicable_with_incomparable_popu _processor, watcher, _cursor, source_path = _seed_live_cursor_authority_case(tmp_path) projection = replace( reconcile._projection_for(tmp_path), + available=False, overall_status="unknown", cursor_ahead_status="unknown", cursor_ahead_count=0, @@ -198,6 +199,28 @@ def test_disappeared_selected_row_is_typed_not_applicable_with_incomparable_popu watcher.stop() +def test_planner_refuses_unknown_projection_when_selected_cursor_remains( + monkeypatch: pytest.MonkeyPatch, tmp_path: Path +) -> None: + from tests.unit.sources.test_live_watcher import _seed_live_cursor_authority_case + + _processor, watcher, _cursor, source_path = _seed_live_cursor_authority_case(tmp_path) + projection = replace( + reconcile._projection_for(tmp_path), + available=False, + overall_status="unknown", + cursor_ahead_status="unknown", + cursor_ahead_count=0, + cursor_ahead_samples=(), + cursor_authority_gap_count=2, + ) + monkeypatch.setattr(reconcile, "_projection_for", lambda root: projection) + + with pytest.raises(reconcile.CursorAuthorityReconciliationError, match="unavailable"): + reconcile._build_plan(tmp_path, source_path) + watcher.stop() + + def test_planner_refuses_multiple_true_ahead_rows(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None: from tests.unit.sources.test_live_watcher import _seed_live_cursor_authority_case @@ -210,16 +233,21 @@ def test_planner_refuses_multiple_true_ahead_rows(monkeypatch: pytest.MonkeyPatc watcher.stop() -def test_planner_refuses_unavailable_healthy_sibling_projection( +def test_planner_refuses_unavailable_projection_with_unknown_sibling( monkeypatch: pytest.MonkeyPatch, tmp_path: Path ) -> None: from tests.unit.sources.test_live_watcher import _seed_live_cursor_authority_case _processor, watcher, _cursor, source_path = _seed_live_cursor_authority_case(tmp_path) - projection = replace(reconcile._projection_for(tmp_path), missing_source_raw_status="unknown") + projection = replace( + reconcile._projection_for(tmp_path), + available=False, + overall_status="unknown", + missing_source_raw_status="unknown", + ) monkeypatch.setattr(reconcile, "_projection_for", lambda root: projection) - with pytest.raises(reconcile.CursorAuthorityReconciliationError, match="sibling"): + with pytest.raises(reconcile.CursorAuthorityReconciliationError, match="unavailable"): reconcile._build_plan(tmp_path, source_path) watcher.stop() @@ -406,6 +434,7 @@ def test_backup_validation_rehashes_and_rejects_mismatched_tier( manifest = { "profile": "full_evidence", "included_tiers": [f"{tier}.db" for tier in tiers], + "blob_inventory_file": "blob-inventory.json", "tier_source_fingerprints": {f"{tier}.db": value for tier, value in tiers.items()}, } (backup / "manifest.json").write_text(json.dumps(manifest), encoding="utf-8") @@ -438,6 +467,40 @@ def test_backup_validation_rehashes_and_rejects_mismatched_tier( validated = reconcile._validate_backup(backup, plan) assert isinstance(validated["root"], dict) assert validated["root"]["basename"] == backup.name + assert isinstance(validated["blob_inventory"], dict) + assert validated["blob_inventory"]["count"] == 1 + assert validated["blob_inventory"]["size_bytes"] == len(blob_payload) + assert validated["blob_inventory"]["inventory_digest"] == reconcile._canonical_digest( + [ + { + "blob_hash": blob_hash, + "path": f"blob/{blob_hash[:2]}/{blob_hash[2:]}", + "size_bytes": len(blob_payload), + "sha256": blob_hash, + "protection": ["referenced"], + } + ] + ) + manifest["blob_inventory_file"] = "alternate-inventory.json" + (backup / "manifest.json").write_text(json.dumps(manifest), encoding="utf-8") + with pytest.raises(reconcile.CursorAuthorityReconciliationError, match="noncanonical blob inventory path"): + reconcile._validate_backup(backup, plan) + manifest["blob_inventory_file"] = "blob-inventory.json" + (backup / "manifest.json").write_text(json.dumps(manifest), encoding="utf-8") + blob_root = backup / "blob" + relocated_blob_root = backup / "attested-blob" + blob_root.rename(relocated_blob_root) + blob_root.symlink_to(relocated_blob_root, target_is_directory=True) + with pytest.raises(reconcile.CursorAuthorityReconciliationError, match="blob root is a symlink"): + reconcile._validate_backup(backup, plan) + blob_root.unlink() + relocated_blob_root.rename(blob_root) + noncanonical_blob_path = blob_root / "nested" / blob_hash[:2] / blob_hash[2:] + noncanonical_blob_path.parent.mkdir(parents=True) + blob_path.rename(noncanonical_blob_path) + with pytest.raises(reconcile.CursorAuthorityReconciliationError, match="blob path is not canonical"): + reconcile._validate_backup(backup, plan) + noncanonical_blob_path.rename(blob_path) blob_path.write_bytes(b"changed") with pytest.raises(reconcile.CursorAuthorityReconciliationError, match="blob digest"): reconcile._validate_backup(backup, plan) From 8cd7476031c3cfe40f4001281834b3f1325db3bd Mon Sep 17 00:00:00 2001 From: Sinity Date: Fri, 7 Aug 2026 17:43:58 +0200 Subject: [PATCH 3/8] chore: refresh PR scope authority Reissue the exact carrier head after rebasing onto current master. From 2f456e2a1a2b0cd642d09ad4a9a66de9a9e05f6d Mon Sep 17 00:00:00 2001 From: Sinity Date: Fri, 7 Aug 2026 18:09:45 +0200 Subject: [PATCH 4/8] fix(maintenance): bind cursor proof no-ops to live candidates Problem: the cursor planner could classify an arbitrary path as a no-op when the projection was unavailable and backup validation trusted malformed or weakly bound receipt evidence.\n\nWhat changed: require healthy projection siblings before cursor-row access, require a current selected cursor row for healthy no-op classification, authenticate manifest and inventory bytes before blob rehashing, reject malformed receipt rows, symlinks, and hard-linked backup artifacts, and extend mutation tests.\n\nCompatibility/migration: no archive schema or production data path changed. The reconciliation route now rejects ambiguous unavailable-projection inputs. --- .../maintenance/cursor_authority_reconcile.py | 61 +++++++---- .../test_cursor_authority_reconcile.py | 103 +++++++++++++----- 2 files changed, 113 insertions(+), 51 deletions(-) diff --git a/polylogue/maintenance/cursor_authority_reconcile.py b/polylogue/maintenance/cursor_authority_reconcile.py index e92818e67b..29f7e9b81e 100644 --- a/polylogue/maintenance/cursor_authority_reconcile.py +++ b/polylogue/maintenance/cursor_authority_reconcile.py @@ -370,22 +370,14 @@ def _build_plan(root: Path, source_path: Path, *, require_candidate: bool = True projection = _projection_for(root) path_digest = cursor_authority_path_digest(source_path) not_applicable_reason: str | None = None - if projection.cursor_ahead_count == 0: + _require_healthy_projection_siblings(projection) + if projection.cursor_ahead_count == 0 and not_applicable_reason is None: current_cursor_paths = {Path(path).resolve() for path, _offset in _cursor_rows(root)} - selected_path_disappeared = source_path.resolve() not in current_cursor_paths if ( require_candidate - and selected_path_disappeared - and _is_disappeared_cursor_incomparable_projection(projection) + and projection.overall_status == "healthy" + and source_path.resolve() in current_cursor_paths ): - not_applicable_reason = ( - "selected cursor row disappeared while the pre-existing incomparable authority population remained" - ) - - if not_applicable_reason is None: - _require_healthy_projection_siblings(projection) - if projection.cursor_ahead_count == 0 and not_applicable_reason is None: - if require_candidate and projection.overall_status == "healthy": not_applicable_reason = "selected cursor-ahead violation is no longer present" else: raise CursorAuthorityReconciliationError("cursor authority is incomparable or has no selected violation") @@ -478,6 +470,24 @@ def _validated_blob_inventory( if manifest.get("blob_inventory_file") != "blob-inventory.json": raise CursorAuthorityReconciliationError("backup uses a noncanonical blob inventory path") inventory_path = root / "blob-inventory.json" + try: + inventory_metadata = inventory_path.lstat() + except OSError as exc: + raise CursorAuthorityReconciliationError("backup blob inventory is unreadable") from exc + if stat.S_ISLNK(inventory_metadata.st_mode) or not stat.S_ISREG(inventory_metadata.st_mode): + raise CursorAuthorityReconciliationError("backup blob inventory is not a regular file") + if inventory_metadata.st_nlink != 1: + raise CursorAuthorityReconciliationError("backup blob inventory must not be hard-linked") + inventory_evidence = receipt.get("blob_inventory_file") + if not isinstance(inventory_evidence, dict): + raise CursorAuthorityReconciliationError("backup blob inventory lacks authenticated file evidence") + if ( + inventory_evidence.get("path") != "blob-inventory.json" + or inventory_evidence.get("present") is not True + or inventory_evidence.get("size_bytes") != inventory_metadata.st_size + or inventory_evidence.get("sha256") != _sha256_file(inventory_path) + ): + raise CursorAuthorityReconciliationError("backup blob inventory does not match its verification receipt") try: declared = json.loads(inventory_path.read_text(encoding="utf-8")) except (OSError, ValueError) as exc: @@ -506,6 +516,12 @@ def _validated_blob_inventory( raise CursorAuthorityReconciliationError("backup blob inventory contains a symlink") if not path.is_file(): continue + try: + metadata = path.lstat() + except OSError as exc: + raise CursorAuthorityReconciliationError(f"backup blob is unreadable: {path}") from exc + if not stat.S_ISREG(metadata.st_mode) or metadata.st_nlink != 1: + raise CursorAuthorityReconciliationError(f"backup blob must be a single-linked regular file: {path}") relative = path.relative_to(root).as_posix() if len(path.parent.name) != 2 or len(path.name) != 62: raise CursorAuthorityReconciliationError(f"backup blob path is not content-addressed: {relative}") @@ -534,8 +550,11 @@ def _validated_blob_inventory( ) actual_rows.sort(key=lambda item: str(item["blob_hash"])) - expected_rows = sorted( - [ + expected_rows: list[dict[str, object]] = [] + for item in expected: + if not isinstance(item, dict): + raise CursorAuthorityReconciliationError("backup verification receipt contains an invalid blob row") + expected_rows.append( { "blob_hash": item.get("blob_hash"), "path": item.get("path"), @@ -545,11 +564,8 @@ def _validated_blob_inventory( if isinstance(item.get("protection"), list) else item.get("protection"), } - for item in expected - if isinstance(item, dict) - ], - key=lambda item: str(item["blob_hash"]), - ) + ) + expected_rows.sort(key=lambda item: str(item["blob_hash"])) if expected_rows != actual_rows: raise CursorAuthorityReconciliationError( "current backup blob inventory does not match its verification receipt" @@ -592,7 +608,6 @@ def _validate_backup(manifest_path: Path, plan: Mapping[str, object]) -> dict[st raise CursorAuthorityReconciliationError("backup lacks complete blob rollback evidence") if not (root / "blob").is_dir() or not (root / "blob-inventory.json").is_file(): raise CursorAuthorityReconciliationError("backup lacks blob rollback evidence") - blob_inventory = _validated_blob_inventory(root, manifest, receipt) archive_root = _archive_root() location = ArchiveLocation.resolve(archive_root) expected_active_index = plan.get("active_index") @@ -611,6 +626,10 @@ def _validate_backup(manifest_path: Path, plan: Mapping[str, object]) -> dict[st ) except BackupAttestationError as exc: raise CursorAuthorityReconciliationError("backup verification receipt attestation is invalid") from exc + manifest_sha256 = _sha256_file(root / "manifest.json") + if receipt.get("manifest_sha256") != manifest_sha256: + raise CursorAuthorityReconciliationError("backup manifest does not match its verification receipt") + blob_inventory = _validated_blob_inventory(root, manifest, receipt) declared = manifest.get("tier_source_fingerprints") expected = plan.get("tier_fingerprints") if not isinstance(declared, dict) or not isinstance(expected, dict): @@ -642,7 +661,7 @@ def _validate_backup(manifest_path: Path, plan: Mapping[str, object]) -> dict[st ) return { "root": _path_identity(root), - "manifest_sha256": _sha256_file(root / "manifest.json"), + "manifest_sha256": manifest_sha256, "blob_inventory": blob_inventory, } diff --git a/tests/unit/maintenance/test_cursor_authority_reconcile.py b/tests/unit/maintenance/test_cursor_authority_reconcile.py index 7d2527c033..c10204ff06 100644 --- a/tests/unit/maintenance/test_cursor_authority_reconcile.py +++ b/tests/unit/maintenance/test_cursor_authority_reconcile.py @@ -187,15 +187,8 @@ def test_disappeared_selected_row_is_typed_not_applicable_with_incomparable_popu monkeypatch.setattr(reconcile, "_projection_for", lambda root: projection) monkeypatch.setattr(reconcile, "_cursor_rows", lambda root: []) - plan = reconcile._build_plan(tmp_path, source_path) - - assert plan["status"] == "not_applicable" - reason = plan["not_applicable_reason"] - assert isinstance(reason, str) - assert "disappeared" in reason - before_projection = plan["before_projection"] - assert isinstance(before_projection, dict) - assert before_projection["cursor_authority_gap_count"] == 2 + with pytest.raises(reconcile.CursorAuthorityReconciliationError, match="unavailable"): + reconcile._build_plan(tmp_path, source_path) watcher.stop() @@ -252,6 +245,28 @@ def test_planner_refuses_unavailable_projection_with_unknown_sibling( watcher.stop() +def test_planner_classifies_bound_current_cursor_as_not_applicable( + monkeypatch: pytest.MonkeyPatch, tmp_path: Path +) -> None: + from tests.unit.sources.test_live_watcher import _seed_live_cursor_authority_case + + _processor, watcher, _cursor, source_path = _seed_live_cursor_authority_case(tmp_path) + projection = replace( + reconcile._projection_for(tmp_path), + overall_status="healthy", + cursor_ahead_status="healthy", + cursor_ahead_count=0, + cursor_ahead_samples=(), + ) + monkeypatch.setattr(reconcile, "_projection_for", lambda root: projection) + + plan = reconcile._build_plan(tmp_path, source_path) + + assert plan["status"] == "not_applicable" + assert plan["not_applicable_reason"] == "selected cursor-ahead violation is no longer present" + watcher.stop() + + def test_wal_effective_snapshot_matches_sqlite_backup(tmp_path: Path) -> None: live = tmp_path / "live.db" backup = tmp_path / "backup.db" @@ -435,31 +450,36 @@ def test_backup_validation_rehashes_and_rejects_mismatched_tier( "profile": "full_evidence", "included_tiers": [f"{tier}.db" for tier in tiers], "blob_inventory_file": "blob-inventory.json", + "blob_count": 1, "tier_source_fingerprints": {f"{tier}.db": value for tier, value in tiers.items()}, } (backup / "manifest.json").write_text(json.dumps(manifest), encoding="utf-8") - (backup / "verification-receipt.json").write_text( - json.dumps( + inventory_bytes = (backup / "blob-inventory.json").read_bytes() + receipt_payload: dict[str, object] = { + "verdict": "success", + "verification": { + "source_blobs_resolved": True, + "index_attachment_blobs_resolved": True, + "blob_inventory_exact": True, + }, + "manifest_sha256": hashlib.sha256((backup / "manifest.json").read_bytes()).hexdigest(), + "blob_inventory_file": { + "path": "blob-inventory.json", + "present": True, + "size_bytes": len(inventory_bytes), + "sha256": hashlib.sha256(inventory_bytes).hexdigest(), + }, + "blobs": [ { - "verdict": "success", - "verification": { - "source_blobs_resolved": True, - "index_attachment_blobs_resolved": True, - "blob_inventory_exact": True, - }, - "blobs": [ - { - "blob_hash": blob_hash, - "path": f"blob/{blob_hash[:2]}/{blob_hash[2:]}", - "size_bytes": len(blob_payload), - "sha256": blob_hash, - "protection": ["referenced"], - } - ], + "blob_hash": blob_hash, + "path": f"blob/{blob_hash[:2]}/{blob_hash[2:]}", + "size_bytes": len(blob_payload), + "sha256": blob_hash, + "protection": ["referenced"], } - ), - encoding="utf-8", - ) + ], + } + (backup / "verification-receipt.json").write_text(json.dumps(receipt_payload), encoding="utf-8") monkeypatch.setattr(reconcile, "ARCHIVE_ROOT", tmp_path) with pytest.raises(reconcile.CursorAuthorityReconciliationError, match="attestation"): reconcile._validate_backup(backup, plan) @@ -481,12 +501,23 @@ def test_backup_validation_rehashes_and_rejects_mismatched_tier( } ] ) + manifest["blob_count"] = 2 + (backup / "manifest.json").write_text(json.dumps(manifest), encoding="utf-8") + receipt_payload["manifest_sha256"] = hashlib.sha256((backup / "manifest.json").read_bytes()).hexdigest() + (backup / "verification-receipt.json").write_text(json.dumps(receipt_payload), encoding="utf-8") + with pytest.raises(reconcile.CursorAuthorityReconciliationError, match="blob count"): + reconcile._validate_backup(backup, plan) + manifest["blob_count"] = 1 manifest["blob_inventory_file"] = "alternate-inventory.json" (backup / "manifest.json").write_text(json.dumps(manifest), encoding="utf-8") + receipt_payload["manifest_sha256"] = hashlib.sha256((backup / "manifest.json").read_bytes()).hexdigest() + (backup / "verification-receipt.json").write_text(json.dumps(receipt_payload), encoding="utf-8") with pytest.raises(reconcile.CursorAuthorityReconciliationError, match="noncanonical blob inventory path"): reconcile._validate_backup(backup, plan) manifest["blob_inventory_file"] = "blob-inventory.json" (backup / "manifest.json").write_text(json.dumps(manifest), encoding="utf-8") + receipt_payload["manifest_sha256"] = hashlib.sha256((backup / "manifest.json").read_bytes()).hexdigest() + (backup / "verification-receipt.json").write_text(json.dumps(receipt_payload), encoding="utf-8") blob_root = backup / "blob" relocated_blob_root = backup / "attested-blob" blob_root.rename(relocated_blob_root) @@ -506,8 +537,20 @@ def test_backup_validation_rehashes_and_rejects_mismatched_tier( reconcile._validate_backup(backup, plan) blob_path.write_bytes(blob_payload) blob_path.unlink() - with pytest.raises(reconcile.CursorAuthorityReconciliationError, match="blob inventory"): + with pytest.raises( + reconcile.CursorAuthorityReconciliationError, + match="current backup blob inventory does not match its verification receipt", + ): + reconcile._validate_backup(backup, plan) + blob_path.write_bytes(blob_payload) + hardlink_path = backup / "hardlink-target" + hardlink_path.write_bytes(blob_payload) + blob_path.unlink() + blob_path.hardlink_to(hardlink_path) + with pytest.raises(reconcile.CursorAuthorityReconciliationError, match="single-linked"): reconcile._validate_backup(backup, plan) + blob_path.unlink() + hardlink_path.unlink() blob_path.write_bytes(blob_payload) extra_payload = b"extra" extra_hash = hashlib.sha256(extra_payload).hexdigest() From 744dd0a1fdaabddbe6750e9377832aa414c7b048 Mon Sep 17 00:00:00 2001 From: Sinity Date: Fri, 7 Aug 2026 19:05:41 +0200 Subject: [PATCH 5/8] fix(maintenance): fail closed on cursor proof I/O gaps Problem: the cursor proof retained a dead unavailable-projection helper, its sibling guard test did not reach the unknown-sibling branch, and inventory hash read failures could escape as raw filesystem errors. What changed: remove the unreachable no-op helper, make the sibling test cover the intended guard, and translate inventory hashing I/O failures into the reconciliation error type with a focused regression. Compatibility/migration: fail-closed validation behavior only; no successful backup or reconciliation path changes. Ref polylogue-s8gcr Co-Authored-By: Claude --- .../maintenance/cursor_authority_reconcile.py | 23 +++++-------------- .../test_cursor_authority_reconcile.py | 17 +++++++++++--- 2 files changed, 20 insertions(+), 20 deletions(-) diff --git a/polylogue/maintenance/cursor_authority_reconcile.py b/polylogue/maintenance/cursor_authority_reconcile.py index 29f7e9b81e..6081c29ec9 100644 --- a/polylogue/maintenance/cursor_authority_reconcile.py +++ b/polylogue/maintenance/cursor_authority_reconcile.py @@ -56,9 +56,12 @@ def _canonical_digest(payload: object) -> str: def _sha256_file(path: Path) -> str: digest = hashlib.sha256() - with path.open("rb") as handle: - for chunk in iter(lambda: handle.read(1024 * 1024), b""): - digest.update(chunk) + try: + with path.open("rb") as handle: + for chunk in iter(lambda: handle.read(1024 * 1024), b""): + digest.update(chunk) + except OSError as exc: + raise CursorAuthorityReconciliationError(f"backup blob inventory is unreadable: {path}") from exc return digest.hexdigest() @@ -351,20 +354,6 @@ def _require_healthy_projection_siblings(projection: RawFrontierIntegrityProject raise CursorAuthorityReconciliationError("raw-frontier sibling projections are not healthy") -def _is_disappeared_cursor_incomparable_projection(projection: RawFrontierIntegrityProjection) -> bool: - """Recognize the sole unavailable projection that proves a scoped no-op.""" - - return ( - not projection.available - and projection.overall_status == "unknown" - and projection.broken_head_status == "healthy" - and projection.missing_source_raw_status == "healthy" - and projection.cursor_ahead_status == "unknown" - and projection.cursor_ahead_count == 0 - and projection.cursor_authority_gap_count > 0 - ) - - def _build_plan(root: Path, source_path: Path, *, require_candidate: bool = True) -> dict[str, object]: tiers = _tier_snapshots(root) projection = _projection_for(root) diff --git a/tests/unit/maintenance/test_cursor_authority_reconcile.py b/tests/unit/maintenance/test_cursor_authority_reconcile.py index c10204ff06..46f9d2ce1e 100644 --- a/tests/unit/maintenance/test_cursor_authority_reconcile.py +++ b/tests/unit/maintenance/test_cursor_authority_reconcile.py @@ -169,7 +169,7 @@ def test_planner_preserves_incomparable_population(monkeypatch: pytest.MonkeyPat watcher.stop() -def test_disappeared_selected_row_is_typed_not_applicable_with_incomparable_population( +def test_disappeared_selected_row_refuses_unavailable_projection( monkeypatch: pytest.MonkeyPatch, tmp_path: Path ) -> None: from tests.unit.sources.test_live_watcher import _seed_live_cursor_authority_case @@ -234,13 +234,13 @@ def test_planner_refuses_unavailable_projection_with_unknown_sibling( _processor, watcher, _cursor, source_path = _seed_live_cursor_authority_case(tmp_path) projection = replace( reconcile._projection_for(tmp_path), - available=False, + available=True, overall_status="unknown", missing_source_raw_status="unknown", ) monkeypatch.setattr(reconcile, "_projection_for", lambda root: projection) - with pytest.raises(reconcile.CursorAuthorityReconciliationError, match="unavailable"): + with pytest.raises(reconcile.CursorAuthorityReconciliationError, match="not healthy"): reconcile._build_plan(tmp_path, source_path) watcher.stop() @@ -560,6 +560,17 @@ def test_backup_validation_rehashes_and_rejects_mismatched_tier( with pytest.raises(reconcile.CursorAuthorityReconciliationError, match="absent from blob-inventory"): reconcile._validate_backup(backup, plan) extra_path.unlink() + real_sha256_file = reconcile._sha256_file + + def fail_inventory_hash(path: Path) -> str: + if path.name == "blob-inventory.json": + return real_sha256_file(path.with_name("missing-inventory.json")) + return real_sha256_file(path) + + monkeypatch.setattr(reconcile, "_sha256_file", fail_inventory_hash) + with pytest.raises(reconcile.CursorAuthorityReconciliationError, match="blob inventory is unreadable"): + reconcile._validate_backup(backup, plan) + monkeypatch.setattr(reconcile, "_sha256_file", real_sha256_file) (backup / "audit.db").unlink() with pytest.raises(reconcile.CursorAuthorityReconciliationError, match="tier is missing"): reconcile._validate_backup(backup, plan) From 5af79c380fda21ccd117f0b1709a9f780025f87b Mon Sep 17 00:00:00 2001 From: Sinity Date: Fri, 7 Aug 2026 19:20:56 +0200 Subject: [PATCH 6/8] fix(maintenance): bind cursor no-op to prior violation Problem: A caller-supplied current cursor path could be classified as the disappeared selected violation without evidence that the original plan selected that path.\n\nWhat changed: Require the plan's redacted cursor-ahead sample to contain the selected path digest before apply can enter recovery or no-op handling. Add a focused regression for a mismatched sample.\n\nCompatibility/migration: Existing plans remain valid when their before-projection sample contains the selected path; unrelated paths now fail closed.\n\nCo-Authored-By: Claude --- .../maintenance/cursor_authority_reconcile.py | 13 +++++++++++++ .../test_cursor_authority_reconcile.py | 18 ++++++++++++++++++ 2 files changed, 31 insertions(+) diff --git a/polylogue/maintenance/cursor_authority_reconcile.py b/polylogue/maintenance/cursor_authority_reconcile.py index 6081c29ec9..d8d74a803b 100644 --- a/polylogue/maintenance/cursor_authority_reconcile.py +++ b/polylogue/maintenance/cursor_authority_reconcile.py @@ -841,6 +841,18 @@ def _before_projection(plan: Mapping[str, object]) -> dict[str, object]: return value +def _require_selected_path_in_before_projection(plan: Mapping[str, object], source_path: Path) -> None: + before_projection = _before_projection(plan) + samples = before_projection.get("cursor_ahead_samples") + selected_path_digest = cursor_authority_path_digest(source_path) + if not isinstance(samples, list) or not any( + isinstance(sample, dict) and sample.get("source_path") == selected_path_digest for sample in samples + ): + raise CursorAuthorityReconciliationError( + "plan does not bind the selected path to a previously observed cursor-ahead violation" + ) + + def _same_plan_bindings(left: Mapping[str, object], right: Mapping[str, object]) -> bool: def comparable(plan: Mapping[str, object]) -> dict[str, object]: value = dict(plan) @@ -942,6 +954,7 @@ def apply_reconciliation(*, plan_path: Path, backup_manifest: Path, receipt: Pat with owner: backup_evidence = _validate_backup(backup_manifest, plan) current_path = _find_path_by_digest(root, str(plan["selected_path_digest"])) + _require_selected_path_in_before_projection(plan, current_path) try: current_plan = _build_plan(root, current_path) except CursorAuthorityReconciliationError: diff --git a/tests/unit/maintenance/test_cursor_authority_reconcile.py b/tests/unit/maintenance/test_cursor_authority_reconcile.py index 46f9d2ce1e..9bd39bda09 100644 --- a/tests/unit/maintenance/test_cursor_authority_reconcile.py +++ b/tests/unit/maintenance/test_cursor_authority_reconcile.py @@ -267,6 +267,24 @@ def test_planner_classifies_bound_current_cursor_as_not_applicable( watcher.stop() +def test_apply_plan_requires_selected_path_in_original_cursor_ahead_sample(tmp_path: Path) -> None: + from tests.unit.sources.test_live_watcher import _seed_live_cursor_authority_case + + _processor, watcher, _cursor, source_path = _seed_live_cursor_authority_case(tmp_path) + plan = reconcile._build_plan(tmp_path, source_path) + + reconcile._require_selected_path_in_before_projection(plan, source_path) + + before_projection = plan["before_projection"] + assert isinstance(before_projection, dict) + samples = before_projection["cursor_ahead_samples"] + assert isinstance(samples, list) and samples + samples[0] = {**samples[0], "source_path": reconcile.cursor_authority_path_digest(tmp_path / "other.jsonl")} + with pytest.raises(reconcile.CursorAuthorityReconciliationError, match="previously observed"): + reconcile._require_selected_path_in_before_projection(plan, source_path) + watcher.stop() + + def test_wal_effective_snapshot_matches_sqlite_backup(tmp_path: Path) -> None: live = tmp_path / "live.db" backup = tmp_path / "backup.db" From bfab730ae5b014b2f4bfddb4715db80371167e6a Mon Sep 17 00:00:00 2001 From: Sinity Date: Fri, 7 Aug 2026 20:01:04 +0200 Subject: [PATCH 7/8] fix(maintenance): canonicalize cursor proof paths Problem: A symlinked cursor path was resolved for apply but hashed in its original spelling in the frozen projection, making a valid plan fail closed during apply. What changed: Canonicalize source paths before redacting cursor-authority projection evidence and cover the symlink spelling case in the focused proof suite. Compatibility/migration: No archive or cursor mutation is performed. --- .../maintenance/cursor_authority_reconcile.py | 4 +++- .../test_cursor_authority_reconcile.py | 21 +++++++++++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/polylogue/maintenance/cursor_authority_reconcile.py b/polylogue/maintenance/cursor_authority_reconcile.py index d8d74a803b..91a7cc4193 100644 --- a/polylogue/maintenance/cursor_authority_reconcile.py +++ b/polylogue/maintenance/cursor_authority_reconcile.py @@ -241,7 +241,9 @@ def redact(value: object) -> object: if isinstance(value, dict): return { key: ( - _identity_digest(item) + cursor_authority_path_digest(Path(item)) + if key == "source_path" and isinstance(item, str) + else _identity_digest(item) if key in {"source_path", "logical_source_key", "accepted_raw_id", "raw_id", "session_id"} and isinstance(item, str) else None diff --git a/tests/unit/maintenance/test_cursor_authority_reconcile.py b/tests/unit/maintenance/test_cursor_authority_reconcile.py index 9bd39bda09..b4054c3a76 100644 --- a/tests/unit/maintenance/test_cursor_authority_reconcile.py +++ b/tests/unit/maintenance/test_cursor_authority_reconcile.py @@ -390,6 +390,27 @@ def test_private_projection_redacts_paths_and_preserves_missing_sample_branches( watcher.stop() +def test_private_projection_canonicalizes_symlinked_source_paths(tmp_path: Path) -> None: + from tests.unit.sources.test_live_watcher import _seed_live_cursor_authority_case + + _processor, watcher, _cursor, source_path = _seed_live_cursor_authority_case(tmp_path) + alias = tmp_path / "source-alias.jsonl" + alias.symlink_to(source_path) + projection = replace( + reconcile._projection_for(tmp_path), + cursor_ahead_samples=( + replace(reconcile._projection_for(tmp_path).cursor_ahead_samples[0], source_path=str(alias)), + ), + ) + + private = reconcile._private_projection(projection) + + samples = private["cursor_ahead_samples"] + assert isinstance(samples, list) and samples + assert samples[0]["source_path"] == reconcile.cursor_authority_path_digest(source_path) + watcher.stop() + + def test_recovery_attempt_requires_a_later_completed_observation(tmp_path: Path) -> None: from tests.unit.sources.test_live_watcher import _seed_live_cursor_authority_case From dd265be86e998d15efd03be913c4b820734e9e45 Mon Sep 17 00:00:00 2001 From: Sinity Date: Fri, 7 Aug 2026 20:29:21 +0200 Subject: [PATCH 8/8] fix(maintenance): reject unbound healthy cursor plans Problem: A healthy archive accepted any current cursor path as a not-applicable proof, even when no earlier cursor-ahead candidate had selected that path. What changed: Healthy zero-gap inputs now fail closed unless they are handled through the existing apply-time prior-plan recovery path. The focused test records the rejection. Compatibility/migration: A previously planned reconciliation still uses its bound recovery checks; no cursor mutation is added. --- .../maintenance/cursor_authority_reconcile.py | 36 ++----------------- .../test_cursor_authority_reconcile.py | 8 ++--- 2 files changed, 6 insertions(+), 38 deletions(-) diff --git a/polylogue/maintenance/cursor_authority_reconcile.py b/polylogue/maintenance/cursor_authority_reconcile.py index 91a7cc4193..1e8b25663b 100644 --- a/polylogue/maintenance/cursor_authority_reconcile.py +++ b/polylogue/maintenance/cursor_authority_reconcile.py @@ -356,43 +356,13 @@ def _require_healthy_projection_siblings(projection: RawFrontierIntegrityProject raise CursorAuthorityReconciliationError("raw-frontier sibling projections are not healthy") -def _build_plan(root: Path, source_path: Path, *, require_candidate: bool = True) -> dict[str, object]: +def _build_plan(root: Path, source_path: Path) -> dict[str, object]: tiers = _tier_snapshots(root) projection = _projection_for(root) path_digest = cursor_authority_path_digest(source_path) - not_applicable_reason: str | None = None _require_healthy_projection_siblings(projection) - if projection.cursor_ahead_count == 0 and not_applicable_reason is None: - current_cursor_paths = {Path(path).resolve() for path, _offset in _cursor_rows(root)} - if ( - require_candidate - and projection.overall_status == "healthy" - and source_path.resolve() in current_cursor_paths - ): - not_applicable_reason = "selected cursor-ahead violation is no longer present" - else: - raise CursorAuthorityReconciliationError("cursor authority is incomparable or has no selected violation") - if not_applicable_reason is not None: - not_applicable_plan: dict[str, object] = { - "format": PLAN_FORMAT, - "archive_identity": _path_identity(root), - "active_index": _active_index_binding(root), - "code_sha": _code_sha(), - "deployed_package_sha": _deployed_package_sha(), - "tier_fingerprints": tiers, - "source_schema_versions": {tier: tiers[tier]["user_version"] for tier in _REQUIRED_TIERS}, - "selected_path_digest": path_digest, - "observed_at_ms": int(time.time() * 1000), - "status": "not_applicable", - "not_applicable_reason": not_applicable_reason, - "cursor_byte_offset": None, - "accepted_frontier": None, - "accepted_raw_id_digest": None, - "source_prefix_digest": None, - "before_projection": _private_projection(projection), - } - not_applicable_plan["plan_digest"] = _canonical_digest(not_applicable_plan) - return not_applicable_plan + if projection.cursor_ahead_count == 0: + raise CursorAuthorityReconciliationError("cursor authority is incomparable or has no selected violation") if projection.cursor_ahead_count != 1: raise CursorAuthorityReconciliationError("refusing to guess among multiple cursor-ahead rows") if projection.broken_head_count or projection.missing_source_raw_count: diff --git a/tests/unit/maintenance/test_cursor_authority_reconcile.py b/tests/unit/maintenance/test_cursor_authority_reconcile.py index b4054c3a76..b4cb667a97 100644 --- a/tests/unit/maintenance/test_cursor_authority_reconcile.py +++ b/tests/unit/maintenance/test_cursor_authority_reconcile.py @@ -245,7 +245,7 @@ def test_planner_refuses_unavailable_projection_with_unknown_sibling( watcher.stop() -def test_planner_classifies_bound_current_cursor_as_not_applicable( +def test_planner_rejects_healthy_input_without_a_prior_candidate( monkeypatch: pytest.MonkeyPatch, tmp_path: Path ) -> None: from tests.unit.sources.test_live_watcher import _seed_live_cursor_authority_case @@ -260,10 +260,8 @@ def test_planner_classifies_bound_current_cursor_as_not_applicable( ) monkeypatch.setattr(reconcile, "_projection_for", lambda root: projection) - plan = reconcile._build_plan(tmp_path, source_path) - - assert plan["status"] == "not_applicable" - assert plan["not_applicable_reason"] == "selected cursor-ahead violation is no longer present" + with pytest.raises(reconcile.CursorAuthorityReconciliationError, match="no selected violation"): + reconcile._build_plan(tmp_path, source_path) watcher.stop()