feat(api): invalidate + supersede for active entities and memories (Issue E)

Public retraction path so mistakes can be corrected without SQL. Unblocks
the correction workflows that the live AKC p05 session exposed.

- engineering/service.py: invalidate_active_entity returns (ok, code)
  with codes invalidated/already_invalid/not_active/not_found for clean
  HTTP mapping. supersede_entity gains superseded_by + auto-creates the
  supersedes relationship (new SUPERSEDES old), rejects self-supersede
- memory/service.py: invalidate_memory/supersede_memory accept reason
  string that lands in audit note
- api/routes.py: POST /entities/{id}/invalidate, /supersede;
  POST /memory/{id}/invalidate, /supersede (all 4 behind /v1 aliases)
- tests/test_invalidate_supersede.py: 15 tests (idempotency, 404/409,
  supersede relationship auto-creation, self-supersede rejection,
  missing-replacement rejection, v1 alias presence)
- DEV-LEDGER.md: session log + test_count 494 -> 509

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-21 21:56:24 -04:00
parent 069d155585
commit 081c058f77
6 changed files with 427 additions and 9 deletions

View File

@@ -456,14 +456,26 @@ def update_memory(
return False
def invalidate_memory(memory_id: str, actor: str = "api") -> bool:
"""Mark a memory as invalid (error correction)."""
return update_memory(memory_id, status="invalid", actor=actor)
def invalidate_memory(
memory_id: str,
actor: str = "api",
reason: str = "",
) -> bool:
"""Mark a memory as invalid (error correction).
``reason`` lands in the audit row's ``note`` column so future
review has a record of why this memory was retracted.
"""
return update_memory(memory_id, status="invalid", actor=actor, note=reason)
def supersede_memory(memory_id: str, actor: str = "api") -> bool:
def supersede_memory(
memory_id: str,
actor: str = "api",
reason: str = "",
) -> bool:
"""Mark a memory as superseded (replaced by newer info)."""
return update_memory(memory_id, status="superseded", actor=actor)
return update_memory(memory_id, status="superseded", actor=actor, note=reason)
def promote_memory(memory_id: str, actor: str = "api", note: str = "") -> bool: