Follow-up to the status SSoT (#382): analyst_verified + research_complete are now first-class canonical statuses, so refresh the docs that still framed them as legacy/decorative. - HEARTBEAT.md §7: rewritten to point at the single source (case_status_model.py / GET /api/status-model), list the 12 canonical statuses by phase, and state the two intermediates are canonical (not legacy); genuinely old markers (proofread/analysis_enriched/…) are display-only fallbacks. - legal-qa.md / legal-ceo.md status maps: dropped the "legacy/optional" framing on research_complete; point to the canonical model. - scripts/backfill_case_status_trim.py: removed analyst_verified + research_complete from the downgrade map (they are valid statuses again — must NOT be downgraded) + updated the docstring. Docs/prompts only — no behaviour change. py_compile clean. NOTE: the host ~/legal-ai needs `git pull` for the Paperclip agents to pick up the refreshed prompts (they run on the host tree, not the container). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
116 lines
4.1 KiB
Python
116 lines
4.1 KiB
Python
"""Backfill case.status after the 17 → 10 status-menu trim.
|
|
|
|
Why this exists: the manual status menu was trimmed from 17 to 10 core
|
|
statuses (decorative mid-stage markers that no pipeline code ever set were
|
|
removed). Existing rows that currently hold a removed status would otherwise
|
|
be "stuck" on a value no longer in the dropdown / SSoT, rendering via the
|
|
Hebrew legacy fallback. This maps each removed status to the nearest
|
|
*preceding* kept status in the lifecycle order, so a case keeps the closest
|
|
truthful position.
|
|
|
|
Mapping (removed → kept):
|
|
|
|
uploading → processing
|
|
in_progress → outcome_set
|
|
brainstorming → outcome_set
|
|
analysis_enriched → direction_approved
|
|
ready_for_writing → direction_approved
|
|
drafting → direction_approved
|
|
qa_failed → qa_review
|
|
|
|
Idempotent: a second run is a no-op (no rows match the removed statuses).
|
|
Dry-run by default — prints the affected counts; pass --apply to write.
|
|
|
|
Usage (runs inside the legal-ai container — shared Postgres on :5433):
|
|
docker cp scripts/backfill_case_status_trim.py <c>:/tmp/
|
|
docker exec <c> python /tmp/backfill_case_status_trim.py # dry-run
|
|
docker exec <c> python /tmp/backfill_case_status_trim.py --apply # write
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import asyncio
|
|
import logging
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
|
|
def _setup_paths():
|
|
here = Path(__file__).resolve().parent
|
|
mcp_src = here.parent / "mcp-server" / "src"
|
|
if mcp_src.is_dir() and str(mcp_src) not in sys.path:
|
|
sys.path.insert(0, str(mcp_src))
|
|
|
|
|
|
_setup_paths()
|
|
from legal_mcp.services import db # noqa: E402
|
|
|
|
logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s")
|
|
log = logging.getLogger("status-trim")
|
|
|
|
# removed status → nearest preceding kept status.
|
|
# NOTE (2026-06-30): analyst_verified + research_complete were RE-CANONICALISED
|
|
# into the status model (legal_mcp/case_status_model.py, "thinking" phase) — they
|
|
# are valid statuses again, so they were removed from this map. Do NOT downgrade
|
|
# them; only genuinely-removed markers remain below.
|
|
STATUS_MAP = {
|
|
"uploading": "processing",
|
|
"in_progress": "outcome_set",
|
|
"brainstorming": "outcome_set",
|
|
"analysis_enriched": "direction_approved",
|
|
"ready_for_writing": "direction_approved",
|
|
"drafting": "direction_approved",
|
|
"qa_failed": "qa_review",
|
|
}
|
|
|
|
|
|
async def backfill(apply: bool) -> int:
|
|
pool = await db.get_pool()
|
|
|
|
# Show the full current distribution for context.
|
|
dist = await pool.fetch("SELECT status, count(*) AS n FROM cases GROUP BY status ORDER BY n DESC")
|
|
log.info("Current status distribution:")
|
|
for r in dist:
|
|
log.info(" %-22s %d", r["status"], r["n"])
|
|
|
|
affected = {r["status"]: r["n"] for r in dist if r["status"] in STATUS_MAP}
|
|
total = sum(affected.values())
|
|
if not total:
|
|
log.info("Nothing to migrate — no rows hold a removed status. ✓")
|
|
return 0
|
|
|
|
log.info("Rows to migrate (%d total):", total)
|
|
for old, n in affected.items():
|
|
log.info(" %-22s → %-20s (%d)", old, STATUS_MAP[old], n)
|
|
|
|
if not apply:
|
|
log.info("DRY-RUN — no changes written. Re-run with --apply to migrate.")
|
|
return total
|
|
|
|
migrated = 0
|
|
for old, new in STATUS_MAP.items():
|
|
if old not in affected:
|
|
continue
|
|
res = await pool.execute(
|
|
"UPDATE cases SET status = $1, updated_at = now() WHERE status = $2",
|
|
new, old,
|
|
)
|
|
# res like "UPDATE 3"
|
|
n = int(res.split()[-1]) if res and res.split()[-1].isdigit() else 0
|
|
migrated += n
|
|
log.info(" migrated %-22s → %-20s (%d)", old, new, n)
|
|
|
|
log.info("Done — migrated %d rows.", migrated)
|
|
return migrated
|
|
|
|
|
|
def main() -> int:
|
|
parser = argparse.ArgumentParser(description="Backfill case.status after the 17→10 status trim")
|
|
parser.add_argument("--apply", action="store_true", help="Write changes (default: dry-run)")
|
|
args = parser.parse_args()
|
|
return 0 if asyncio.run(backfill(args.apply)) >= 0 else 1
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|