[object Object]

← back to Unclaimed Property Platform

Cycle 7: state suppression / reinstatement path (near-real-time takedown)

3ca22d7316b65f8c4b2c55b5b9742b17b13c513c · 2026-08-01 20:13:56 -0700 · Steve Abrams

- SqliteRepository.suppress_property/reinstate_property: flip search_document_state.is_suppressed
  so a record leaves/returns to anonymous search IMMEDIATELY (masked_search already filters it),
  while property + owner + version history are fully retained. Every action writes an immutable
  audit_event; audit_trail() reads it back. Unknown record -> False (no silent success).
- Closes the brief's near-real-time suppression requirement (privacy/fraud takedown) — the gap
  where nothing could pull a record from public search. State-authenticated in production.
- tests/test_cycle8_suppression.py: suppress->gone, data retained, audit written, reinstate->back.
Tests: 8/8 suites green. All local/synthetic/$0.

TK-10097

Files touched

Diff

commit 3ca22d7316b65f8c4b2c55b5b9742b17b13c513c
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Sat Aug 1 20:13:56 2026 -0700

    Cycle 7: state suppression / reinstatement path (near-real-time takedown)
    
    - SqliteRepository.suppress_property/reinstate_property: flip search_document_state.is_suppressed
      so a record leaves/returns to anonymous search IMMEDIATELY (masked_search already filters it),
      while property + owner + version history are fully retained. Every action writes an immutable
      audit_event; audit_trail() reads it back. Unknown record -> False (no silent success).
    - Closes the brief's near-real-time suppression requirement (privacy/fraud takedown) — the gap
      where nothing could pull a record from public search. State-authenticated in production.
    - tests/test_cycle8_suppression.py: suppress->gone, data retained, audit written, reinstate->back.
    Tests: 8/8 suites green. All local/synthetic/$0.
    
    TK-10097
---
 services/common/sqlite_repo.py   | 54 ++++++++++++++++++++++++++
 tests/test_cycle8_suppression.py | 84 ++++++++++++++++++++++++++++++++++++++++
 2 files changed, 138 insertions(+)

diff --git a/services/common/sqlite_repo.py b/services/common/sqlite_repo.py
index f2f854f..36adba3 100644
--- a/services/common/sqlite_repo.py
+++ b/services/common/sqlite_repo.py
@@ -238,3 +238,57 @@ class SqliteRepository:
                 "amount_band": r["amount_band"],
             }))
         return out
+
+    # --- state suppression / reinstatement (state-integration API surface) ---------------
+    # A state instructs "remove from public search" (privacy / fraud takedown). We flip the
+    # search-publication flag — the record leaves anonymous search IMMEDIATELY (masked_search
+    # filters is_suppressed=0) while ALL underlying data + version history is retained for
+    # audit. Every action writes an immutable audit_event. In production this is authenticated
+    # to a state actor; here it's a repository method the state-integration layer would call.
+    def _property_id(self, jurisdiction: str, source_property_id: str) -> str | None:
+        row = self.conn.execute(
+            "SELECT property_id FROM property WHERE jurisdiction_id=? AND source_property_id=?",
+            (jurisdiction, source_property_id),
+        ).fetchone()
+        return row["property_id"] if row else None
+
+    def _set_suppression(self, jurisdiction: str, source_property_id: str,
+                         suppressed: bool, actor: str, reason: str = "") -> bool:
+        pid = self._property_id(jurisdiction, source_property_id)
+        if pid is None:
+            return False
+        before = self.conn.execute(
+            "SELECT is_suppressed FROM search_document_state WHERE property_id=?", (pid,)
+        ).fetchone()
+        before_val = None if before is None else before["is_suppressed"]
+        self.conn.execute(
+            "UPDATE search_document_state SET is_suppressed=? WHERE property_id=?",
+            (1 if suppressed else 0, pid),
+        )
+        self.conn.execute(
+            """INSERT INTO audit_event
+               (audit_event_id, actor, action, resource, before_hash, after_hash, created_at)
+               VALUES (?,?,?,?,?,?,?)""",
+            (str(uuid.uuid4()), actor, "suppress" if suppressed else "reinstate",
+             pid, str(before_val), "1" if suppressed else "0", _now()),
+        )
+        self.conn.commit()
+        return True
+
+    def suppress_property(self, jurisdiction: str, source_property_id: str,
+                          actor: str, reason: str = "") -> bool:
+        """Remove a record from public search (retains all data + history). Returns False
+        if the record isn't found."""
+        return self._set_suppression(jurisdiction, source_property_id, True, actor, reason)
+
+    def reinstate_property(self, jurisdiction: str, source_property_id: str,
+                           actor: str, reason: str = "") -> bool:
+        """Return a previously-suppressed record to public search."""
+        return self._set_suppression(jurisdiction, source_property_id, False, actor, reason)
+
+    def audit_trail(self, resource_property_id: str) -> list[dict]:
+        rows = self.conn.execute(
+            "SELECT actor, action, created_at FROM audit_event WHERE resource=? ORDER BY created_at",
+            (resource_property_id,),
+        ).fetchall()
+        return [dict(r) for r in rows]
diff --git a/tests/test_cycle8_suppression.py b/tests/test_cycle8_suppression.py
new file mode 100644
index 0000000..f0d6481
--- /dev/null
+++ b/tests/test_cycle8_suppression.py
@@ -0,0 +1,84 @@
+"""Cycle 7 test — state suppression / reinstatement of a record from public search.
+
+Run:  python -m tests.test_cycle8_suppression
+
+Proves the brief's near-real-time suppression requirement:
+  1. a state can remove a record from public search immediately (is_suppressed flip);
+  2. the underlying data + version history are RETAINED (nothing deleted);
+  3. every suppress/reinstate writes an immutable audit_event;
+  4. reinstate returns it to search;
+  5. suppressing an unknown record returns False (no silent no-op success).
+"""
+from __future__ import annotations
+
+import sys
+import tempfile
+from pathlib import Path
+
+sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
+
+from services.common.sqlite_repo import FileObjectStore, SqliteRepository
+from services.ingestion.ingest import FeedDefinition, ingest_authorized_feed
+
+SAMPLE = Path(__file__).resolve().parents[1] / "data" / "sample" / "sample_state_feed.csv"
+
+
+def ok(msg: str) -> None:
+    print(f"  ✓ {msg}")
+
+
+def _refs(repo, q):
+    return {h["public_reference"] for h in repo.masked_search(q)}
+
+
+def main() -> int:
+    tmp = Path(tempfile.mkdtemp(prefix="upp-cycle8-"))
+    store = FileObjectStore(tmp / "obj")
+    (tmp / "obj").mkdir(parents=True, exist_ok=True)
+    store.write_bytes("incoming/s.csv", SAMPLE.read_bytes())
+    repo = SqliteRepository(str(tmp / "p.db"))
+    ingest_authorized_feed(FeedDefinition("SAMPLE", "incoming/s.csv"), store, repo)
+
+    pid = repo._property_id("SAMPLE", "SP-0001")
+    assert pid, "SP-0001 should exist"
+
+    print("1) record is publicly searchable before suppression")
+    assert pid in _refs(repo, "Catherine"), "SP-0001 should be searchable"
+    ok("SP-0001 (Catherine O'Neil) appears in masked search")
+
+    print("2) suppress -> gone from search immediately")
+    assert repo.suppress_property("SAMPLE", "SP-0001", actor="state:CA", reason="privacy")
+    assert pid not in _refs(repo, "Catherine"), "suppressed record must not appear in search"
+    ok("after suppress, SP-0001 no longer in search results")
+
+    print("3) data + version history RETAINED (nothing deleted)")
+    assert repo._property_id("SAMPLE", "SP-0001") == pid, "property row must remain"
+    owner = repo.conn.execute(
+        "SELECT owner_name_raw FROM owner WHERE property_id=?", (pid,)).fetchone()
+    assert owner and owner["owner_name_raw"], "owner row must remain"
+    vers = repo.conn.execute(
+        "SELECT COUNT(*) FROM property_version WHERE property_id=?", (pid,)).fetchone()[0]
+    assert vers >= 1, "version history must remain"
+    ok(f"property + owner + {vers} version(s) all retained")
+
+    print("4) audit trail records the action")
+    trail = repo.audit_trail(pid)
+    assert any(e["action"] == "suppress" and e["actor"] == "state:CA" for e in trail), trail
+    ok(f"audit_event 'suppress' by state:CA recorded ({len(trail)} event(s))")
+
+    print("5) reinstate -> back in search + audit event")
+    assert repo.reinstate_property("SAMPLE", "SP-0001", actor="state:CA")
+    assert pid in _refs(repo, "Catherine"), "reinstated record should be searchable again"
+    assert any(e["action"] == "reinstate" for e in repo.audit_trail(pid))
+    ok("reinstate restores search visibility + writes audit event")
+
+    print("6) suppressing an unknown record returns False")
+    assert repo.suppress_property("SAMPLE", "NO-SUCH-ID", actor="state:CA") is False
+    ok("unknown record -> False (no silent success)")
+
+    print("\nALL CYCLE-7 SUPPRESSION ASSERTIONS PASSED ✅")
+    return 0
+
+
+if __name__ == "__main__":
+    raise SystemExit(main())

← 02a3ead docs: ledger Cycle-6 record (recall fix); TK-10097  ·  back to Unclaimed Property Platform  ·  docs: ledger Cycle-7 record (suppression path); TK-10097 afbc9ad →