Files
DB-cleanup/merge-output/apply_detention_wording.py
StellarCrowandClaude Fable 5.1 de707f8751 docs(detention): forced-entry rule; close the ten open flag items
Owner's decisions of 2026-09-07 applied:
- Rule sharpened in README "Definitions": forced entry into the victim's
  space (home, office, hotel room, car) followed by beating or holding them
  there to take what is wanted is detention, however short.
- 354, 355 (null) and 124 set to 0. 242, 273, 413, 422, 423, 215, 298 stay 1;
  the set-to-0 proposals for them are withdrawn under the rule.
- corrections-approved.md, detention-flag-review.md and the corrections JSON
  carry the decisions; the worklist item says no open items remain.
- Summaries 242 and 273 relabelled "Detention" with the forced-entry wording.

verify_all.py PASSES.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GZZENdTLzNGsbNy4DyF1yt
2026-09-07 06:00:54 +02:00

132 lines
9.1 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": ("Detention — the two forced entry and held her in her own home while beating her: she was punched in the face roughly ten times in front of her family and taken to hospital; the attackers fled when the husband appeared.", "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": ("Detention — confronted inside his own home by the two men, who stabbed and robbed him; he described 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()