FU-7: audit-trail + provenance (GAP-17/18/19/20) #13

Merged
chaim merged 10 commits from fix/fu7-audit-provenance into main 2026-05-30 21:43:34 +00:00
2 changed files with 61 additions and 1 deletions
Showing only changes of commit a121f79d6a - Show all commits

View File

@@ -44,6 +44,26 @@ async def log_action(
json.dumps(details or {}, ensure_ascii=False)[:200])
async def log_action_safe(
action: str,
case_id: "UUID | None" = None,
document_id: "UUID | None" = None,
details: dict | None = None,
user: str = "system",
) -> None:
"""Non-fatal audit: never let an audit-log failure break the caller's action.
The authoritative integrity trail is git (X5 §2.1); audit_log is the
'who/what/when' observability layer, so a write failure is logged as a
warning and swallowed.
"""
try:
await log_action(action, case_id=case_id, document_id=document_id,
details=details, user=user)
except Exception as e: # noqa: BLE001 — observability must not break the op
logger.warning("audit log_action failed (non-fatal) for %s: %s", action, e)
async def get_audit_log(
case_id: UUID | None = None,
action: str | None = None,

View File

@@ -1106,6 +1106,16 @@ CREATE INDEX IF NOT EXISTS idx_case_law_searchable ON case_law (searchable);
"""
# ── V22: cases.blocks_stale — DOCX↔blocks drift flag (GAP-17 / INV-EX1) ──
# Set true when revise_draft/apply_user_edit make active_draft_path the live
# source-of-truth without re-syncing decision_blocks; cleared when blocks are
# re-exported or re-saved. Surfaced by health-check. Source-of-truth remains
# decision_blocks — this only flags known drift (no fragile DOCX→blocks reparse).
SCHEMA_V22_SQL = """
ALTER TABLE cases ADD COLUMN IF NOT EXISTS blocks_stale boolean NOT NULL DEFAULT false;
"""
async def _run_schema_migrations(pool: asyncpg.Pool) -> None:
async with pool.acquire() as conn:
await conn.execute(SCHEMA_SQL)
@@ -1130,7 +1140,8 @@ async def _run_schema_migrations(pool: asyncpg.Pool) -> None:
await conn.execute(SCHEMA_V19_SQL)
await conn.execute(SCHEMA_V20_SQL)
await conn.execute(SCHEMA_V21_SQL)
logger.info("Database schema initialized (v1-v21)")
await conn.execute(SCHEMA_V22_SQL)
logger.info("Database schema initialized (v1-v22)")
async def init_schema() -> None:
@@ -1206,6 +1217,35 @@ async def get_active_draft_path(case_id: UUID) -> str | None:
return row["active_draft_path"] if row else None
async def mark_blocks_stale(case_id: UUID, stale: bool) -> None:
"""Flag/clear DOCX↔blocks drift for a case (GAP-17)."""
pool = await get_pool()
async with pool.acquire() as conn:
await conn.execute(
"UPDATE cases SET blocks_stale = $1, updated_at = now() WHERE id = $2",
stale, case_id,
)
async def resolve_citation_case_law_ids(ids) -> dict:
"""Structural citation→corpus resolution (GAP-20 / INV-AUD3).
Given case_law_id values referenced by a decision's citations/provenance,
split into resolvable (exist in case_law) vs unresolvable.
"""
resolved, unresolved = [], []
pool = await get_pool()
async with pool.acquire() as conn:
for cid in ids:
try:
exists = await conn.fetchval(
"SELECT EXISTS(SELECT 1 FROM case_law WHERE id = $1)", cid)
except Exception:
exists = False
(resolved if exists else unresolved).append(cid)
return {"resolved": resolved, "unresolved": unresolved}
def _normalize_case_number(s: str) -> str:
"""Canonicalise a case number for tolerant lookup.