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])
|
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(
|
async def get_audit_log(
|
||||||
case_id: UUID | None = None,
|
case_id: UUID | None = None,
|
||||||
action: str | 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 def _run_schema_migrations(pool: asyncpg.Pool) -> None:
|
||||||
async with pool.acquire() as conn:
|
async with pool.acquire() as conn:
|
||||||
await conn.execute(SCHEMA_SQL)
|
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_V19_SQL)
|
||||||
await conn.execute(SCHEMA_V20_SQL)
|
await conn.execute(SCHEMA_V20_SQL)
|
||||||
await conn.execute(SCHEMA_V21_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:
|
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
|
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:
|
def _normalize_case_number(s: str) -> str:
|
||||||
"""Canonicalise a case number for tolerant lookup.
|
"""Canonicalise a case number for tolerant lookup.
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user