Files
DB-cleanup/merge-output/apply_detention_wording.py
T
StellarCrowandClaude Fable 5.1 439e83a4cb 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
2026-09-06 19:36:45 +02:00

132 lines
9.0 KiB
Python

"""
Rewrite the "Violence Used" line of every reviewed summary in summary-overrides.json so
that it names DETENTION explicitly where the victim was held, and says "No detention"
where the summary already states the attack was foiled, a snatch, or a hit-and-run.
Rule: README-START-HERE.md, "Definitions". Nothing is added that the summary does not
already say: a line gets the "Detention —" label only when that same summary (any
category) already describes the victim being held, tied, handcuffed, locked in, taken
away or abducted; the supporting phrase is checked before the edit. Idempotent.
Supersedes the Violence Used wording in build_overrides_batch1..7.py; summary-overrides.json
is the authoritative store. Run build_cases.py --all and verify_all.py afterwards.
"""
import json
import re
import sys
from pathlib import Path
BASE = Path(__file__).resolve().parent
STORE = BASE / "summary-overrides.json"
HELD = re.compile(r"(held (him|her|them|the|at|for|in|with|captive|hostage|against|under)|tied|bound|taped|handcuff|"
r"zip.?tie|cable tie|captiv|hostage|locked|restrain|confine|shackle|abduct|kidnapp|"
r"seized (him|her|them)|forced (him|her|them|the \w+) (into|inside|back)|drove (him|her|them)|"
r"took (him|her|them)|taken (away|to|from)|driven|put a sack|bag over|hooded)", re.I)
FOILED = re.compile(r"(none|no one was harmed|foiled|snatched|not a violent|no physical violence|"
r"unoccupied|no confrontation)", re.I)
# Per-case hand rewrites where the mechanical label would read badly. Each value is the
# full new Violence Used line (without the bold label). "support" must occur in the summary.
HAND = {
"218": ("Detention — she was held at gunpoint in her home and suffered a scalp laceration that needed stitches; she said she shot one of the intruders during the confrontation.", "held at gunpoint"),
"219": ("No detention and no assault reported — the bag was snatched from behind. (The database description's \"beaten with a stick\" is not supported by the linked article.)", "snatched from behind"),
"221": ("Detention — held for five days tied to a bed in a makeshift prison; he was kept drugged with sleeping pills, restrained and harassed throughout.", "tied to a bed"),
"222": ("No detention — a street attack from behind. He was slashed, suffering a 10-centimetre knife wound to the forehead (with no damage to his eyes or vital organs), and fell to the ground.", "from behind"),
"224": ("Detention — abducted and held by an armed group that accused him of illegal Hundi/Hawala dealing and threatened him.", "abducted"),
"229": ("Possible detention — by his own account he was drugged and became docile in the car; as he was made to get out in a residential area near Golders Green the car drove off and struck his leg, leaving him unable to walk.", "drugged"),
"230": ("No detention — the attempt to force her into the van was fought off. The child's father intervened and was struck by the attackers as the victim cried out and many passers-by looked on.", "force the mother into"),
"242": ("No detention — she was punched in the face roughly ten times in front of her family and taken to hospital; the attackers fled.", "punched"),
"253": ("Detention — forced into a van and driven off; inside the van he was physically assaulted and threatened with a firearm, and he was later taken to hospital with serious but non-life-threatening injuries.", "forced into"),
"256": ("No detention — the intruders were armed (one with a switchblade, another with a fake firearm) but the attempt was foiled at the fence and no one was harmed.", "foiled"),
"273": ("No detention — he was stabbed during the robbery, describing the scene afterwards as looking \"like a massacre\".", "stabbed"),
"292": ("Detention — threatened with a gun-like object and forced back inside his home.", "forced back"),
"298": ("He was assaulted during the robbery; the source on file does not say whether anyone was restrained.", "assaulted"),
"299": ("No detention beyond the moment of the mugging — physical force and coercion: forced phone handovers, being held against a wall and forced biometric unlocks.", "held against a wall"),
"322": ("Detention — abducted and held for two days; one of his fingers was severed.", "held for two days"),
"349": ("None — no detention and no confrontation: an unoccupied-premises break-in (the wider wave included violent, armed raids elsewhere, but not here).", "unoccupied"),
"364": ("No detention — a daylight snatch: he was hit across the head and face as he waited in his car.", "waited in his car"),
"301": ("Detention — the on-duty security personnel were tied up to carry out the theft; no injuries were reported.", "tied up"),
"372": ("Detention — taken and tied to a chair; she was stripped naked, struck in the legs with a hammer, and burned.", "tied to a chair"),
}
# Lead clause + original sentence (first letter lowercased). "support" must occur in the summary.
LEAD = {
"228": ("Detention — bound and held for about 17 days at the townhouse;", "held for about 17 days"),
"237": ("Detention — forced into a stolen vehicle and held until the next day;", "released the next day"),
"238": ("Detention — the man was held and later released in Créteil;", "released in Créteil"),
"240": ("Detention — the gang locked the victims in the shop before fleeing (they escaped through the shutters);", "locked them in the shop"),
"244": ("Detention — the pair were driven to a police station and held until about 8:30 p.m.;", "released around 8:30"),
"245": ("Detention — forced into a car and moved between locations through the night;", "moved between locations"),
"247": ("Detention — forcibly taken to the area of the Arc de Triomphe;", "forcibly took him"),
"272": ("Detention — a sack over his head, forced into a car and driven out of the city;", "sack over his head"),
"279": ("Detention — the family was held in the attackers' cars over a two-hour drive;", "two-hour drive"),
"317": ("Detention — seized outside his home and held until the ransom was paid;", "released after the ransom"),
"334": ("Detention — subdued at gunpoint inside the car;", "subdued him at gunpoint"),
"340": ("Detention — forced into a car, taken to a house in Jimbaran and released several hours later;", "released several hours later"),
"369": ("Detention — trapped in the bedsit;", "trap"),
"370": ("Detention — all three family members were bound with zip ties for about 13 hours;", "13 hours"),
"371": ("Detention — hooded, handcuffed and held blindfolded for six days;", "six days"),
"285": ("No detention, no violence —", "sleight-of-hand"),
}
def relabel(cid, summary):
m = re.search(r"(\*\*Violence Used:\*\*\s*)(.+)", summary)
if not m:
return summary, "no Violence Used line"
label, old = m.group(1), m.group(2)
if cid in HAND:
new, support = HAND[cid]
if support.lower() not in summary.lower():
raise SystemExit(f"ABORT case {cid}: support phrase {support!r} not in summary")
elif cid in LEAD:
lead, support = LEAD[cid]
if support.lower() not in summary.lower():
raise SystemExit(f"ABORT case {cid}: support phrase {support!r} not in summary")
body = old
while body.lower().startswith(lead.lower()): # idempotent: strip any lead already applied
body = body[len(lead):].lstrip()
body = body[0].lower() + body[1:]
if cid == "285":
body = body[len("none — "):] if body.startswith("none — ") else body
new = lead + " " + body
if new == old:
return summary, "already labelled"
elif re.match(r"(Detention|No detention|Possible detention)", old):
fixed = re.sub(r"^No detention — none — ", "No detention — ", old)
if fixed != old:
return summary.replace(label + old, label + fixed, 1), fixed[:90]
return summary, "already labelled"
elif HELD.search(old):
new = "Detention — " + old[0].lower() + old[1:]
elif FOILED.search(old):
body = re.sub(r"^None\s*[—-]\s*", "", old)
new = "No detention — " + body[0].lower() + body[1:]
elif HELD.search(summary):
new = "Detention (see Attack Method). " + old
else:
return summary, "UNLABELLED (no holding cue and not foiled)"
return summary.replace(label + old, label + new, 1), new[:90]
def main():
dry = "--dry-run" in sys.argv
store = json.loads(STORE.read_text(encoding="utf-8"))
changed = 0
for cid in sorted(store, key=int):
new, note = relabel(cid, store[cid])
print(f"{cid}: {note}")
if new != store[cid]:
store[cid] = new
changed += 1
if not dry:
STORE.write_text(json.dumps(store, ensure_ascii=False, indent=2), encoding="utf-8")
print(f"\n{'would change' if dry else 'changed'} {changed} of {len(store)} summaries")
if __name__ == "__main__":
main()