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:
co-authored by
Claude Fable 5.1
parent
1387037367
commit
1309687723
+78
-10
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user