feat(audit): log_action_safe + V22 blocks_stale + citation resolver (FU-7)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user