Merge pull request 'fix(renumber): update Paperclip issue linkage on case renumber' (#334) from worktree-renumber-paperclip-linkage into main
This commit was merged in pull request #334.
This commit is contained in:
@@ -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` |
|
| `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) |
|
| `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 בלבד)
|
## סקריפטים שנמחקו (git history בלבד)
|
||||||
|
|
||||||
|
|||||||
@@ -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.
|
4. MinIO keys cases/{old}/… 3 buckets; cp→new then rm old.
|
||||||
legal-immutable (WORM/object-lock) → copy-only, old object stays locked.
|
legal-immutable (WORM/object-lock) → copy-only, old object stays locked.
|
||||||
5. Gitea repo cases/{old} API PATCH name + local .git remote rewrite
|
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
|
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.
|
(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:
|
try:
|
||||||
c = await asyncpg.connect(PAPERCLIP_DSN, timeout=10)
|
c = await asyncpg.connect(PAPERCLIP_DSN, timeout=10)
|
||||||
except Exception as e:
|
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:
|
try:
|
||||||
rows = await c.fetch("SELECT id, name FROM projects WHERE name LIKE $1", f"%{old}%")
|
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:
|
finally:
|
||||||
await c.close()
|
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:
|
except urllib.error.HTTPError as e:
|
||||||
log(f" ✗ Gitea rename failed: HTTP {e.code} {e.read()[:160]!r}")
|
log(f" ✗ Gitea rename failed: HTTP {e.code} {e.read()[:160]!r}")
|
||||||
|
|
||||||
# 6. Paperclip project name
|
# 6. Paperclip case↔issue linkage — project name + legal-case-number value +
|
||||||
if not skip_paperclip and rec["paperclip"].get("reachable") and rec["paperclip"]["projects"]:
|
# 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
|
import asyncpg
|
||||||
c = await asyncpg.connect(PAPERCLIP_DSN, timeout=10)
|
c = await asyncpg.connect(PAPERCLIP_DSN, timeout=10)
|
||||||
try:
|
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(
|
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}%",
|
old, new, f"%{old}%",
|
||||||
)
|
)
|
||||||
log(f" ✓ Paperclip projects: {res}")
|
log(f" ✓ Paperclip issue titles: {res}")
|
||||||
finally:
|
finally:
|
||||||
await c.close()
|
await c.close()
|
||||||
|
|
||||||
@@ -419,6 +455,8 @@ def print_inspection(rec: dict) -> None:
|
|||||||
log(f" pclip: {name}")
|
log(f" pclip: {name}")
|
||||||
if not pc["projects"]:
|
if not pc["projects"]:
|
||||||
log(" pclip: (no matching project)")
|
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:
|
else:
|
||||||
log(f" pclip: unreachable ({pc.get('error','')})")
|
log(f" pclip: unreachable ({pc.get('error','')})")
|
||||||
log(" DB path columns to rewrite:")
|
log(" DB path columns to rewrite:")
|
||||||
|
|||||||
Reference in New Issue
Block a user