fix(renumber): update Paperclip issue linkage on case renumber #334

Merged
chaim merged 1 commits from worktree-renumber-paperclip-linkage into main 2026-06-28 10:20:00 +00:00
2 changed files with 46 additions and 8 deletions

View File

@@ -200,7 +200,7 @@
| `drain_digests.py` | python | ריקון תור ההעשרה של יומונים (X12): מעבד כל digest בסטטוס `pending` דרך `digest_library.enrich_digest` (חילוץ-LLM Sonnet + embedding + autolink). מקבילי (CONCURRENCY=3, env-tunable), idempotent. מוסיף `~/.local/bin` ל-PATH כדי שה-claude CLI יימצא תחת cron. בודק דגל `drain_controls('legal-digest-drain')` ב-startup → no-op כשכבוי מ-/operations. | דרך `legal-digest-drain.config.cjs` (pm2 cron) + ידני אחרי backfill. חלופת-MCP: `digest_process_pending` |
| `legal-digest-drain.config.cjs` | pm2/js | **תזמון כל שעתיים של `drain_digests.py`** (cron `12 */2 * * *`, `DIGEST_DRAIN_CRON` לעקיפה; דקת-הצתה `:12` כדי לא לחלוק דקה עם metadata-drain `:00` — מונע deadlock של DDL-המיגרציה) — הועבר מ-crontab של המערכת ל-pm2 כדי שיופיע ויהיה שליט בדף `/operations` (הרץ-עכשיו/הפעל/כבה). `autorestart:false` (one-shot per tick). דורש claude CLI + `VOYAGE_API_KEY`. התקנה: `pm2 start scripts/legal-digest-drain.config.cjs && pm2 save`. | pm2 cron (host-side) |
| `renumber_cases.py` | python | **מיגרציה חד-פעמית (בוצעה 2026-06-12)** — תיקון 11 מספרי-תיקים לפורמט קנוני `NNNN-MM-YY` (הוספת ספרות-חודש; 1046-26→1024-02-26 תיקון-סידורי). רץ על ה-host (לא בקונטיינר): DB pool של האפליקציה + `mcli` (MinIO) + Gitea API + Paperclip DB. אטומי per-case עם גיבוי ל-`data/audit/` ואימות-אחרי. FK-ים על `cases.id` (UUID) לא נגעו; משכתב כל עמודה עם `cases/{old}/` (file_path **וגם** image_thumbnail_path שהוא storage-key בלי `/data`), מנרמל זהות חוצת-קורפוס (case_law/style_corpus/style_exemplars/citations — לא תוכן/full_text), מעביר מפתחי-MinIO ב-3 buckets (legal-immutable=WORM copy-only), משנה-שם repo ב-Gitea, ומעדכן שם-פרויקט ב-Paperclip. dry-run כברירת-מחדל; `--apply --tier clean\|archive`. **מיצוי — לא להריץ שוב** (ה-MAPPING היסטורי). | חד-פעמי — בוצע |
| `renumber_cases.py` | python | **מיגרציה חד-פעמית (בוצעה 2026-06-12)** — תיקון 11 מספרי-תיקים לפורמט קנוני `NNNN-MM-YY` (הוספת ספרות-חודש; 1046-26→1024-02-26 תיקון-סידורי). רץ על ה-host (לא בקונטיינר): DB pool של האפליקציה + `mcli` (MinIO) + Gitea API + Paperclip DB. אטומי per-case עם גיבוי ל-`data/audit/` ואימות-אחרי. FK-ים על `cases.id` (UUID) לא נגעו; משכתב כל עמודה עם `cases/{old}/` (file_path **וגם** image_thumbnail_path שהוא storage-key בלי `/data`), מנרמל זהות חוצת-קורפוס (case_law/style_corpus/style_exemplars/citations — לא תוכן/full_text), מעביר מפתחי-MinIO ב-3 buckets (legal-immutable=WORM copy-only), משנה-שם repo ב-Gitea, ומעדכן ב-Paperclip את **שם-הפרויקט + `plugin_state.legal-case-number` + כותרות-issues** (3 המשטחים ש-`get_case_issues` נשען עליהם — בלי (b)+(c) ה-issues נשארים על המספר הישן ו-run-learning/run-halacha מדלגים בשקט עם "no_issue"; תוקן 2026-06-28 אחרי שהפער התגלה ב-8137-11-24). dry-run כברירת-מחדל; `--apply --tier clean\|archive`. **מיצוי — לא להריץ שוב** (ה-MAPPING היסטורי); לתבנית-עתידית: ה-Paperclip-step המעודכן הוא הרפרנס. | חד-פעמי — בוצע |
## סקריפטים שנמחקו (git history בלבד)

View File

@@ -19,7 +19,13 @@ migrates atomically per case — is everything that embeds the number as *text*:
4. MinIO keys cases/{old}/… 3 buckets; cp→new then rm old.
legal-immutable (WORM/object-lock) → copy-only, old object stays locked.
5. Gitea repo cases/{old} API PATCH name + local .git remote rewrite
6. Paperclip project name replace(old→new) so case↔project lookup holds
6. Paperclip case↔issue linkage replace(old→new) in THREE places, because the
legal-ai → Paperclip lookup (get_case_issues) keys on the case number as text:
• projects.name so case↔project lookup holds
• plugin_state.legal-case-number the authoritative issue linkage value_json
• issues.title the '[ערר {cn}] …' tag the title-path lookup uses
Without (b)+(c) the issues keep the OLD number and get_case_issues returns [],
so post-final actions (run-learning / run-halacha) silently skip ("no_issue").
Bare occurrences of the old number that are NOT inside a '/cases/{old}/' path
(e.g. prose in notes, a citation) are *reported for review*, never auto-edited.
@@ -273,10 +279,25 @@ async def inspect_paperclip(old: str) -> dict:
try:
c = await asyncpg.connect(PAPERCLIP_DSN, timeout=10)
except Exception as e:
return {"reachable": False, "error": str(e)[:120], "projects": []}
return {"reachable": False, "error": str(e)[:120],
"projects": [], "linkage_rows": 0, "issue_titles": 0}
try:
rows = await c.fetch("SELECT id, name FROM projects WHERE name LIKE $1", f"%{old}%")
return {"reachable": True, "projects": [(str(r["id"]), r["name"]) for r in rows]}
# The two surfaces get_case_issues actually keys on: the legal-case-number
# plugin_state linkage and the '[ערר {cn}]' tag in issue titles.
linkage = await c.fetchval(
"SELECT count(*) FROM plugin_state "
"WHERE state_key = 'legal-case-number' AND value_json = to_jsonb($1::text)",
old,
)
titles = await c.fetchval(
"SELECT count(*) FROM issues WHERE title LIKE $1", f"%{old}%")
return {
"reachable": True,
"projects": [(str(r["id"]), r["name"]) for r in rows],
"linkage_rows": linkage or 0,
"issue_titles": titles or 0,
}
finally:
await c.close()
@@ -369,16 +390,31 @@ async def apply_case(conn, rec: dict, *, skip_minio: bool, skip_gitea: bool,
except urllib.error.HTTPError as e:
log(f" ✗ Gitea rename failed: HTTP {e.code} {e.read()[:160]!r}")
# 6. Paperclip project name
if not skip_paperclip and rec["paperclip"].get("reachable") and rec["paperclip"]["projects"]:
# 6. Paperclip case↔issue linkage — project name + legal-case-number value +
# issue titles. (b)+(c) are what get_case_issues keys on; without them the
# issues keep the old number and run-learning/run-halacha skip with "no_issue".
if not skip_paperclip and rec["paperclip"].get("reachable"):
import asyncpg
c = await asyncpg.connect(PAPERCLIP_DSN, timeout=10)
try:
if rec["paperclip"]["projects"]:
res = await c.execute(
"UPDATE projects SET name = replace(name, $1, $2), updated_at = now() "
"WHERE name LIKE $3",
old, new, f"%{old}%",
)
log(f" ✓ Paperclip projects: {res}")
res = await c.execute(
"UPDATE projects SET name = replace(name, $1, $2), updated_at = now() WHERE name LIKE $3",
"UPDATE plugin_state SET value_json = to_jsonb($2::text) "
"WHERE state_key = 'legal-case-number' AND value_json = to_jsonb($1::text)",
old, new,
)
log(f" ✓ Paperclip plugin_state (legal-case-number): {res}")
res = await c.execute(
"UPDATE issues SET title = replace(title, $1, $2) WHERE title LIKE $3",
old, new, f"%{old}%",
)
log(f" ✓ Paperclip projects: {res}")
log(f" ✓ Paperclip issue titles: {res}")
finally:
await c.close()
@@ -419,6 +455,8 @@ def print_inspection(rec: dict) -> None:
log(f" pclip: {name}")
if not pc["projects"]:
log(" pclip: (no matching project)")
log(f" pclip: linkage rows={pc.get('linkage_rows', 0)} "
f"issue titles={pc.get('issue_titles', 0)} (→ rewritten to {rec['new']})")
else:
log(f" pclip: unreachable ({pc.get('error','')})")
log(" DB path columns to rewrite:")