fix(trim): audit trail and review list rebuilt from the store; fetch list regenerated

- auto_trim.py: trim-decisions-auto.json and auto-trim-review.md are now derived
  from the whole store on every run (paragraphs_kept, cut_note, staged files),
  so a run limited to a few cases no longer overwrites the decisions for the
  rest. Restores the full trail: 378 decisions across 287 cases, 36 review
  entries (7 off-case suspects, 12 too small, 20 weak anchors).
- make_fetch_list.py: regenerates the manual fetch list on sofia-worklist.md
  from the store between markers; replaces the hand-written July list. 65
  sources remain unreadable by script, none on a case without other text.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GZZENdTLzNGsbNy4DyF1yt
This commit is contained in:
StellarCrow
2026-09-12 16:20:02 +02:00
co-authored by Claude Fable 5.1
parent 1387037367
commit 1309687723
5 changed files with 5180 additions and 119 deletions
+78 -10
View File
@@ -187,6 +187,77 @@ def reset_auto(store, only=()):
return n
def decisions_from_store(store):
"""Reconstruct every auto-trim decision from the block fields the trimmer wrote
(paragraphs_kept, cut_note, staged_file), plus anchors read from the staged file."""
out = {}
for cid_s, blocks in store.items():
cid = int(cid_s)
for b in blocks:
if b.get("verdict") != "KEEP" or not str(b.get("trimmed_by", "")).startswith("auto"):
continue
pk = b.get("paragraphs_kept", "")
m = re.search(r"(\d+)-(\d+)(?: excl \[([\d, ]*)\])? of (\d+)", pk)
if not m:
continue
start, end, excl, total = int(m.group(1)), int(m.group(2)), m.group(3), int(m.group(4))
exclude = [int(x) for x in excl.split(",")] if excl else []
title = 0 if pk.startswith("headline [0]") else None
h = re.search(r"hits on page: (\d+)", b.get("cut_note", ""))
path = STAGING / f"{cid:03d}" / b["staged_file"]
full = path.read_text(encoding="utf-8") if path.exists() else ""
paras = full.split("\n\n")
out.setdefault(cid_s, []).append({
"field": b["field"], "staged_file": b["staged_file"], "start": start, "end": end,
"title": title, "exclude": exclude, "hits": int(h.group(1)) if h else None,
"starts_with": paras[start][:45] if start < len(paras) else None,
"ends_with": paras[end][-55:] if end < len(paras) else None,
"chars_before": len(full), "chars_after": b.get("chars"),
"paywall": "PAYWALL" in b.get("cut_note", "")})
return out
def write_review(store, decisions):
"""Rebuild auto-trim-review.md from the whole store: every UNTRIMMED block with its reason,
every auto-trimmed block cut on a weak anchor. Regenerated on each run, never appended."""
records = {r["id"]: r for r in json.loads(JSONF.read_text(encoding="utf-8"))}
rows = []
for cid_s, blocks in store.items():
cid = int(cid_s)
terms = case_terms(records[cid])
for b in blocks:
if b.get("verdict") == "UNTRIMMED" and b.get("staged_file"):
path = STAGING / f"{cid:03d}" / b["staged_file"]
full = path.read_text(encoding="utf-8")
paras = full.split("\n\n")
hits = sum(score(p, terms) for p in paras)
start, end = find_range(paras, terms)
if start is None:
reason = "no body run found"
elif hits == 0:
reason = "OFF-CASE? no case term appears anywhere on the page"
else:
reason = (f"TOO SMALL: the only body run found is tiny on a {len(full):,}-char page; "
"page may be a script shell or list-form article")
rows.append((cid, b["field"], b.get("source"), reason, b["url"]))
for d in decisions.get(cid_s, []):
if d.get("hits") is not None and d["hits"] < MIN_HITS:
blk = next((b for b in blocks if b.get("field") == d["field"]), {})
rows.append((cid, d["field"], blk.get("source"),
f"WEAK anchor: trimmed on only {d['hits']} case-term hit(s); confirm the page is about this case",
blk.get("url")))
lines = ["# Auto-trim review list", "",
"Regenerated from the whole store on every auto_trim.py run. OFF-CASE? blocks were left UNTRIMMED",
"because no term from the record appears on the page; the linked URL may be the wrong source.",
"TOO SMALL blocks were left UNTRIMMED because almost no body text was found on a large page.",
"WEAK blocks were trimmed on one or two hits only, usually because the record is thin; confirm the",
"page is about this case. The full extract stays in staging/ either way.", ""]
for cid, field, src, reason, url in sorted(rows, key=lambda r: (r[0], r[1])):
lines.append(f"- **case {cid}** `{field}` {src}: {reason}\n <{url}>")
REVIEW_OUT.write_text("\n".join(lines) + "\n", encoding="utf-8")
return len(rows)
def main():
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8", errors="replace")
ap = argparse.ArgumentParser()
@@ -271,16 +342,13 @@ def main():
if not args.dry_run:
STORE.write_text(json.dumps(store, ensure_ascii=False, indent=2), encoding="utf-8")
DECISIONS_OUT.write_text(json.dumps(decisions, ensure_ascii=False, indent=2), encoding="utf-8")
lines = ["# Auto-trim review list", "",
"Three kinds of entry. TOO SMALL blocks were left UNTRIMMED because the trimmer found",
"almost no body text on a large page. OFF-CASE? blocks were left UNTRIMMED because no term from the",
"record appears on the page; the linked URL may be the wrong source. WEAK blocks were",
"trimmed but on one or two hits only, usually because the record is thin; confirm the",
"page is about this case. The full extract stays in staging/ either way.", ""]
for cid, field, src, reason, url in sorted(review):
lines.append(f"- **case {cid}** `{field}` {src}: {reason}\n <{url}>")
REVIEW_OUT.write_text("\n".join(lines) + "\n", encoding="utf-8")
# The audit trail is rebuilt from the store every run, so a run limited to a few
# cases never drops the decisions for the rest.
alld = decisions_from_store(store)
DECISIONS_OUT.write_text(json.dumps(alld, ensure_ascii=False, indent=2), encoding="utf-8")
n_rev = write_review(store, alld)
print(f"audit trail: {sum(len(v) for v in alld.values())} auto-trim decisions across {len(alld)} cases; "
f"{n_rev} entries on the review list")
before = sum(d["chars_before"] for ds in decisions.values() for d in ds)
after = sum(d["chars_after"] for ds in decisions.values() for d in ds)