From b5a5219a09851c137b1c4c3a7cf2be2db3be590a Mon Sep 17 00:00:00 2001 From: ajspig Date: Tue, 18 Aug 2026 13:36:44 -0400 Subject: [PATCH] fix: cascade scope removal through document_sources --- src/deriver/queue_manager.py | 3 +-- src/deriver/scope_backfill.py | 11 +++++++---- tests/deriver/test_scope_backfill.py | 2 +- 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/src/deriver/queue_manager.py b/src/deriver/queue_manager.py index e4c8124b..1c19c131 100644 --- a/src/deriver/queue_manager.py +++ b/src/deriver/queue_manager.py @@ -478,8 +478,7 @@ class QueueManager: @staticmethod def _is_tenant_work(work_unit_keys: Iterable[str]) -> bool: - """True if any claimed work unit is real tenant work, not housekeeping. - """ + """True if any claimed work unit is real tenant work, not housekeeping.""" for key in work_unit_keys: try: if parse_work_unit_key(key).task_type != "reconciler": diff --git a/src/deriver/scope_backfill.py b/src/deriver/scope_backfill.py index 81017e28..f602838b 100644 --- a/src/deriver/scope_backfill.py +++ b/src/deriver/scope_backfill.py @@ -28,8 +28,7 @@ import logging from dataclasses import dataclass from typing import Any -from sqlalchemy import select, update -from sqlalchemy.dialects.postgresql import array +from sqlalchemy import exists, select, update from sqlalchemy.sql.functions import func from src import crud, models @@ -441,7 +440,7 @@ async def process_scope_removal( all_removed = list(frontier) # Fail-closed cascade: soft-delete derived documents whose support - # (source_ids) intersects anything removed, transitively — a + # (document_sources) intersects anything removed, transitively — a # deduction resting on removed evidence must leave with it, and so # must an induction resting on that deduction. while frontier: @@ -453,7 +452,11 @@ async def process_scope_removal( models.Document.observed == observed, models.Document.level != "explicit", models.Document.deleted_at.is_(None), - models.Document.source_ids.has_any(array(frontier)), + exists().where( + models.DocumentSource.derived_id == models.Document.id, + models.DocumentSource.source_id.in_(frontier), + models.DocumentSource.workspace_name == workspace_name, + ), ) .values(deleted_at=func.now()) .returning(models.Document.id) diff --git a/tests/deriver/test_scope_backfill.py b/tests/deriver/test_scope_backfill.py index 06464c6f..9cca383a 100644 --- a/tests/deriver/test_scope_backfill.py +++ b/tests/deriver/test_scope_backfill.py @@ -522,7 +522,7 @@ async def test_removal_cascades_to_dependent_derived_docs_only( session_name=None, content="unrelated deduction", level="deductive", - source_ids=["some-other-doc-id-not-removed"], + source_ids=[generate_nanoid()], ) copy_id, dependent_id, unrelated_id = copy.id, dependent.id, unrelated.id