fix(renumber): update Paperclip issue linkage on case renumber
All checks were successful
G12 Leak-Guard / leak-guard (pull_request) Successful in 4s
Lint — undefined names / undefined-names (pull_request) Successful in 11s

renumber_cases.py step 6 only renamed the Paperclip project, leaving the
case↔issue linkage on the OLD case number. get_case_issues() keys on two
text surfaces the script never touched:
  • plugin_state.legal-case-number (value_json) — the authoritative linkage
  • issues.title — the '[ערר {cn}] …' tag the title-path lookup matches

Result: after a renumber the issues kept the old number, get_case_issues
returned [], and post-final actions (run-learning / run-halacha) silently
skipped with reason "no_issue" — surfaced in the UI as "לא הופעלה למידה".
Discovered on case 8137-11-24 (renamed from 8137-24).

Now the Paperclip step rewrites all three surfaces (projects.name +
plugin_state + issue titles); inspect_paperclip + the dry-run report show
the linkage-row and issue-title counts that will be rewritten.

The 11-case migration already ran; the matching DB rows were fixed
manually (incl. this commit's logic) for all stale cases. This change is
for any future renumber.

Invariants: G1 (normalize at source — the linkage, not a read-time
workaround). G12 note: this is a one-time host-side migration script that
already connects directly to the Paperclip DB by design (below the platform
port, like the existing projects/MinIO/Gitea surgery) — it does not add a
parallel runtime path through agent_platform_port.py.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-28 10:19:24 +00:00
parent 4de555367d
commit 8932bd1f54
2 changed files with 46 additions and 8 deletions

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:")