diff --git a/polylogue/storage/raw_retention.py b/polylogue/storage/raw_retention.py index ddad3761a..23d0e0c70 100644 --- a/polylogue/storage/raw_retention.py +++ b/polylogue/storage/raw_retention.py @@ -1306,6 +1306,11 @@ def raw_frontier_blocked_source_paths( ) -> RawFrontierBlockedPaths: """Attribute the frontier proof's refusals to exact source paths.""" + if not (archive_root / "source.db").is_file(): + # No source tier means nothing has been acquired yet, so there is + # nothing to select and nothing to refuse. An unreadable tier still + # refuses below: absence and damage are different states. + return RawFrontierBlockedPaths(frozenset(), None) projection = raw_frontier_integrity_projection( archive_root, raw_materialization_readiness, diff --git a/tests/unit/maintenance/test_archive_verification.py b/tests/unit/maintenance/test_archive_verification.py index 2bf79deff..866832b0e 100644 --- a/tests/unit/maintenance/test_archive_verification.py +++ b/tests/unit/maintenance/test_archive_verification.py @@ -1419,12 +1419,20 @@ def test_blob_refs_liveness_passes_on_coherent_archive(tmp_path: Path) -> None: def test_blob_reference_closure_rejects_acquired_attachment_without_ref(tmp_path: Path) -> None: + """An acquired attachment whose refs vanished is archive debt. + + ``ref_count`` is the discriminator: a non-zero count with no surviving + ``attachment_refs`` row means the sweep lost edges it once had, while a + zero count is an attachment that never had one. + + Anti-vacuity: seeding ``ref_count`` 0 instead describes the explained + shape and the check stays clean.""" _seed_coherent_archive(tmp_path) conn = _connect(tmp_path / "index.db") try: conn.execute( "INSERT INTO attachments (attachment_id, byte_count, blob_hash, acquisition_status, ref_count) " - "VALUES ('orphan-acquired', 1, ?, 'acquired', 0)", + "VALUES ('orphan-acquired', 1, ?, 'acquired', 1)", (b"a" * 32,), ) conn.commit() diff --git a/tests/unit/storage/test_raw_retention.py b/tests/unit/storage/test_raw_retention.py index 1af69b475..a9d4a58c0 100644 --- a/tests/unit/storage/test_raw_retention.py +++ b/tests/unit/storage/test_raw_retention.py @@ -2577,6 +2577,23 @@ def test_deferred_cursor_never_blocks_source_selection(tmp_path: Path) -> None: assert raw_frontier_source_selection_block_reason(tmp_path) is None +def test_absent_source_tier_refuses_nothing(tmp_path: Path) -> None: + """An archive root with no source tier has nothing to select or refuse. + + Anti-vacuity: dropping the absence branch makes the projection report + the tier unavailable under both the broken-head and cursor checks, and + those reasons surface as an unattributed refusal that blocks the whole + materialization pass on a freshly created archive. + """ + archive_root = tmp_path / "archive" + archive_root.mkdir() + + blocked = raw_retention_mod.raw_frontier_blocked_source_paths(archive_root, {}) + + assert blocked.unattributed_reason is None + assert blocked.source_paths == frozenset() + + def test_blocked_source_paths_refuse_violations_and_admit_authority_gaps(tmp_path: Path) -> None: """A violation names the path it refuses; a gap names a path only processing can resolve.