feat(sources): Wayback pass, automatic trimming, detention rule, ten cases re-sourced
Second and third rounds on the dossier pipeline, reviewed locally before commit. Retrieval - wayback_pass.py: retries every BLOCKED/DEAD/THIN/ERROR linked source through the Wayback Machine; blocks carry the snapshot date, archive URL and the live verdict. 143 slots retried: 93 fetched, 37 no snapshot, 12 script shells, 1 PDF. - fetch_sources.py: fetch_wayback() helper. - add_extra_sources.py: files staged extra_* finds (web search, not on the DB record) into the store with a provenance note. - Cases re-sourced by web search: 248 (Oslo: Document.no, Avisa Oslo, NRK; the linked Le Parisien piece is case 242 and is marked OFF-CASE), 258 Kharkiv, 260 Singapore, 358 Bangkok, 365 Las Vegas, 388 Verneuil-sur-Seine, 390 Zoersel, 430 Homestead. 341 of 364 cases now hold a fetched article; 11, 66 and 399 have no public text source (podcast, police video, direct victim report). Trimming - auto_trim.py: case-anchored furniture cut for UNTRIMMED blocks. Finds the body run that mentions the case, merges across subheadings and short furniture gaps, drops teasers, share bars, date/URL/caption lines and subscription pitches; refuses pages with no record term or almost no body. 302 blocks trimmed; 16 left on auto-trim-review.md (7 OFF-CASE suspects). Every cut is labelled AUTO-TRIMMED, UNREVIEWED in the dossier; the full extract stays in staging/. Decisions with anchors in trim-decisions-auto.json. - build_cases.py / verify_all.py: banners for auto-trimmed and archived blocks, staged-file check extended to Wayback blocks, flag legend under the record. Detention rule - README "Definitions": the DB field `kidnappings` is the DETENTION violence type (victim, guard, staff or relative held to force submission or execute the theft), distinct from the Kidnapping scenario (taken away and held). - detention-flag-review.md / detention-flag-corrections.json: 40 records reviewed with evidence; 30 set-to-1 proposals accepted by the owner on 2026-09-06 (listed in corrections-approved.md), 10 still open. - apply_detention_wording.py: "Violence Used" in the 70 reviewed summaries now names detention explicitly (62 of 70 labelled), supported by the summary's own text; idempotent; supersedes the batch scripts' wording. Worklist and docs - sofia-worklist.md: 248 decisions, detention rule item replacing the old "no abduction" item, fresh-search section for the textless cases. - README-START-HERE.md: progress notes, run commands, next steps. - Bug fixed in passing: Wayback blocks stored in-memory text with carriage returns; now stored as read back from disk. verify_all.py PASSES (2321 checks). 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
499fd61f79
commit
439e83a4cb
@@ -0,0 +1,69 @@
|
||||
"""
|
||||
File staged extra_* sources (found by web search, NOT on the DB record) into the article
|
||||
store as UNTRIMMED blocks, so build_cases.py renders them and auto_trim.py can cut them.
|
||||
|
||||
Reads staging/<case>/index.json; every extra_N entry that is not yet in the store becomes
|
||||
a block. FETCHED entries carry the staged text, hashed; anything else (THIN, BLOCKED, DEAD,
|
||||
NOT AN ARTICLE) is filed as a reference with its verdict. Each block says in its note that
|
||||
the URL is not on the record and when it was found, so provenance stays visible.
|
||||
|
||||
Usage: python add_extra_sources.py 258 260 ... then auto_trim.py <cases>, build, verify.
|
||||
"""
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
import sys
|
||||
from datetime import date
|
||||
from pathlib import Path
|
||||
|
||||
BASE = Path(__file__).resolve().parent
|
||||
STAGING = BASE / "staging"
|
||||
STORE = BASE / "sourced-articles.json"
|
||||
|
||||
NAMES = {"document.no": "Document.no", "alt.no": "Avisa Oslo (alt.no)", "nrk.no": "NRK", "ao.no": "Avisa Oslo",
|
||||
"mothership.sg": "Mothership", "khaosodenglish.com": "Khaosod English", "8newsnow.com": "8 News Now (KLAS)",
|
||||
"thedefiant.io": "The Defiant", "actu17.fr": "Actu17", "lagazette-yvelines.fr": "La Gazette en Yvelines",
|
||||
"mantes-actu.net": "Mantes Actu", "vrt.be": "VRT NWS", "nnieuws.be": "NNieuws", "indegazette.be": "In de Gazette",
|
||||
"justice.gov": "US Department of Justice", "decrypt.co": "Decrypt", "fortune.com": "Fortune",
|
||||
"ukrinform.ua": "Ukrinform", "hromadske.radio": "Hromadske Radio", "sud.ua": "Sud.ua", "atn.ua": "ATN"}
|
||||
|
||||
|
||||
def main():
|
||||
cases = [int(a) for a in sys.argv[1:]]
|
||||
store = json.loads(STORE.read_text(encoding="utf-8"))
|
||||
today = date.today().isoformat()
|
||||
added = 0
|
||||
for cid in cases:
|
||||
idx_path = STAGING / f"{cid:03d}" / "index.json"
|
||||
if not idx_path.exists():
|
||||
print(f"case {cid}: no staging index"); continue
|
||||
blocks = store.setdefault(str(cid), [])
|
||||
have = {b.get("field") for b in blocks}
|
||||
for e in json.loads(idx_path.read_text(encoding="utf-8")):
|
||||
if not e["field"].startswith("extra_") or e["field"] in have:
|
||||
continue
|
||||
src = NAMES.get(e["host"], e["host"])
|
||||
note = f"NOT on the DB record. Found {today} by web search because the linked source(s) gave no usable text."
|
||||
if e["verdict"] != "FETCHED":
|
||||
blocks.append({"field": e["field"], "verdict": e["verdict"], "url": e["url"], "source": src,
|
||||
"note": note + (" " + e["note"] if e.get("note") else "")})
|
||||
added += 1
|
||||
continue
|
||||
path = next(STAGING.joinpath(f"{cid:03d}").glob(f"{e['n']:02d}_*.txt"))
|
||||
text = path.read_text(encoding="utf-8")
|
||||
blocks.append({"field": e["field"], "verdict": "UNTRIMMED", "url": e["url"], "source": src,
|
||||
"language": "unknown", "text": text, "chars": len(text),
|
||||
"sha256": hashlib.sha256(text.encode("utf-8")).hexdigest(),
|
||||
"staged_file": path.name,
|
||||
"paragraphs_kept": f"ALL {len(text.split(chr(10) * 2))} paragraphs, untrimmed",
|
||||
"method": "raw HTTP retrieval, deterministic extraction",
|
||||
"fetched_at": e.get("fetched_at"), "http": e.get("http"), "note": note,
|
||||
"cut_note": "NOT TRIMMED. Full page extract; trim on read-through."})
|
||||
added += 1
|
||||
print(f"case {cid}: + {e['field']} {src} {len(text):,} chars")
|
||||
STORE.write_text(json.dumps(store, ensure_ascii=False, indent=2), encoding="utf-8")
|
||||
print(f"added {added} block(s)")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user