fix(renumber): update Paperclip issue linkage on case renumber
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:
@@ -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:")
|
||||
|
||||
Reference in New Issue
Block a user