mirror of
https://github.com/PR0M3TH3AN/unredact.git
synced 2026-10-27 08:40:29 +00:00
feat: add separate V (victim) badge independent of associate tiers
Victim names are stored as a flat set of lowercased name variants with no person details — the V badge appears but is not clickable, protecting victim identities. A result can show both T1/T2/T3 and V badges simultaneously. - 15 known victims from public court records → 43 name variants - Enabler/perpetrator categories promoted to T2 - Purple V badge with pointer-events disabled - Victim matching uses prefix/suffix combination like associate matching Co-Authored-By: Claude Opus 4.6 <[email protected]>
This commit is contained in:
1 parent
d0e6a7c47a
commit
2268e8549e
5 files changed
+109
-6
No files matched your search
@@ -62,6 +62,23 @@ NICKNAMES = {
|
||||
# Tier 2 categories (staff/financial get auto-promoted)
|
||||
TIER2_CATEGORIES = {"staff", "financial"}
|
||||
|
||||
# Known victims from public court records and reporting (Giuffre v. Maxwell, etc.)
|
||||
# Stored as a separate name set — no person details exposed in the UI.
|
||||
KNOWN_VICTIMS = {
|
||||
"Virginia Giuffre", "VIRGINIA ROBERTS", "Virginia Roberts",
|
||||
"Courtney Wild",
|
||||
"Sarah Ransome",
|
||||
"Maria Farmer", "Annie Farmer",
|
||||
"Chauntae Davies",
|
||||
"Johanna Sjoberg",
|
||||
"Michelle Licata",
|
||||
"Carolyn Andriano",
|
||||
"Priscilla Doe",
|
||||
"Teresa Helm",
|
||||
"Anouska De Georgiou",
|
||||
"Haley Robson",
|
||||
}
|
||||
|
||||
|
||||
def _compute_tiers(registry: list[dict], relationships: list[dict]) -> dict[str, int]:
|
||||
"""Compute tier for each person name.
|
||||
@@ -91,7 +108,7 @@ def _compute_tiers(registry: list[dict], relationships: list[dict]) -> dict[str,
|
||||
|
||||
if name in traveled:
|
||||
tiers[name] = 1
|
||||
elif category in TIER2_CATEGORIES or rel_counts.get(name, 0) >= 3:
|
||||
elif category in TIER2_CATEGORIES or category in {"enabler", "perpetrator"} or rel_counts.get(name, 0) >= 3:
|
||||
tiers[name] = 2
|
||||
else:
|
||||
tiers[name] = 3
|
||||
@@ -154,13 +171,42 @@ def _generate_lookups_for_name(
|
||||
_add_name_entries(names, f"{nick} {last}", person_id, tier, "nickname_full")
|
||||
|
||||
|
||||
def _build_victim_names() -> set[str]:
|
||||
"""Build a flat set of lowercased victim name variants for matching.
|
||||
|
||||
No person details — just strings to check against. This keeps victim
|
||||
identities private in the UI (the V badge shows but is not clickable).
|
||||
"""
|
||||
victim_names: set[str] = set()
|
||||
|
||||
for full_name in KNOWN_VICTIMS:
|
||||
low = full_name.lower().strip()
|
||||
if len(low) < 2:
|
||||
continue
|
||||
victim_names.add(low)
|
||||
|
||||
parts = low.split()
|
||||
if len(parts) >= 2:
|
||||
first, last = parts[0], parts[-1]
|
||||
victim_names.add(first)
|
||||
victim_names.add(last)
|
||||
|
||||
# Nicknames
|
||||
if first in NICKNAMES:
|
||||
for nick in NICKNAMES[first]:
|
||||
victim_names.add(nick)
|
||||
victim_names.add(f"{nick} {last}")
|
||||
|
||||
return victim_names
|
||||
|
||||
|
||||
def process_associates(
|
||||
registry: list[dict],
|
||||
relationships: list[dict],
|
||||
) -> dict:
|
||||
"""Process raw registry + relationships into the associates lookup format.
|
||||
|
||||
Returns: {"names": {str: [...]}, "persons": {str: {...}}}
|
||||
Returns: {"names": {str: [...]}, "persons": {str: {...}}, "victim_names": [str, ...]}
|
||||
"""
|
||||
tiers = _compute_tiers(registry, relationships)
|
||||
|
||||
@@ -193,7 +239,9 @@ def process_associates(
|
||||
for alias in person.get("aliases", []):
|
||||
_generate_lookups_for_name(names, alias, person_id, tier)
|
||||
|
||||
return {"names": names, "persons": persons}
|
||||
victim_names = sorted(_build_victim_names())
|
||||
|
||||
return {"names": names, "persons": persons, "victim_names": victim_names}
|
||||
|
||||
|
||||
def download_data(data_dir: Path) -> tuple[list, list]:
|
||||
|
||||
@@ -42,6 +42,8 @@ def test_process_returns_names_and_persons():
|
||||
result = process_associates(_sample_registry(), _sample_relationships())
|
||||
assert "names" in result
|
||||
assert "persons" in result
|
||||
assert "victim_names" in result
|
||||
assert isinstance(result["victim_names"], list)
|
||||
|
||||
|
||||
def test_tier1_traveled_with():
|
||||
@@ -127,3 +129,21 @@ def test_nickname_generation():
|
||||
names = result["names"]
|
||||
assert "jeff" in names
|
||||
assert any(m["match_type"] == "nickname" for m in names["jeff"])
|
||||
|
||||
|
||||
def test_victim_names_generated():
|
||||
"""Victim names should be a flat list of lowercased name variants."""
|
||||
result = process_associates(_sample_registry(), _sample_relationships())
|
||||
vn = result["victim_names"]
|
||||
# Known victims from the KNOWN_VICTIMS set should produce entries
|
||||
assert "virginia giuffre" in vn
|
||||
assert "giuffre" in vn
|
||||
assert "virginia" in vn
|
||||
|
||||
|
||||
def test_victim_names_independent_of_associates():
|
||||
"""Victim names don't require the person to be in the registry."""
|
||||
result = process_associates([], [])
|
||||
vn = result["victim_names"]
|
||||
# Victims come from the hardcoded set, not the registry
|
||||
assert "virginia giuffre" in vn
|
||||
@@ -1 +1 @@
|
||||
{"names":{"*d - cbp offcr-c":[{"person_id":"p0000","match_type":"full","tier":3}],"*d":[{"person_id":"p0000","match_type":"first","tier":3},{"person_id":"p0001","match_type":"first","tier":3},{"person_id":"p0002","match_type":"first","tier":3}],"offcr-c":[{"person_id":"p0000","match_type":"last","tier":3},{"person_id":"p0003","match_type":"last","tier":3}],"*. offcr-c":[{"person_id":"p0000","match_type":"initial_last","tier":3},{"person_id":"p0003","match_type":"initial_last","tier":3}],"* offcr-c":[{"person_id":"p0000","match_type":"initial_last","tier":3},{"person_id":"p0003","match_type":"initial_last","tier":3}],"*d - cbp officer":[{"person_id":"p0001","match_type":"full","tier":3}],"officer":[{"person_id":"p0001","match_type":"last","tier":3},{"person_id":"p0004","match_type":"last","tier":3}],"*. officer":[{"person_id":"p0001","match_type":"initial_last","tier":3},{"person_id":"p0004","match_type":"initial_last","tier":3}],"* officer":[{"person_id":"p0001","match_type":"initial_last","tier":3},{"person_id":"p0004","match_type":"initial_last","tier":3}],"*d - cbp officer-c":[{"person_id":"p0002","match_type":"full","tier":3}],"officer-c":[{"person_id":"p0002","match_type":"last","tier":3},{"person_id":"p0005","match_type":"last","tier":3}],"*. officer-c":[{"person_id":"p0002","match_type":"initial_last","tier":3},{"person_id":"p0005","match_type":"initial_last","tier":3}],"* officer-c":[{"person_id":"p0002","match_type":"initial_last","tier":3},{"person_id":"p0005","match_type":"initial_last","tier":3}],"*f - cbp offcr-c":[{"person_id":"p0003","match_type":"full","tier":3}],"*f":[{"person_id":"p0003","match_type":"first","tier":3},{"person_id":"p0004","match_type":"first","tier":3},{"person_id":"p0005","match_type":"first","tier":3}],"*f - cbp officer":[{"person_id":"p0004","match_type":"full","tier":3}],"*f - cbp officer-c":[{"person_id":"p0005","match_type":"full","tier":3}],"36653-g-1159b-n908je-pbi-tist-foia-1336-no records ?":[{"person_id":"p0006","match_type":"full","tier":3}],"36653-g-1159b-n908je-pbi-tist-foia-1336-no":[{"person_id":"p0006","match_type":"first","tier":3}],"3. ?":[{"person_id":"p0006","match_type":"initial_last","tier":3},{"person_id":"p0007","match_type":"initial_last","tier":3},{"person_id":"p0008","match_type":"initial_last","tier":3},{"person_id":"p0009","match_type":"initial_last","tier":3},{"person_id":"p0010","match_type":"initial_last","tier":3},{"person_id":"p0011","match_type":"initial_last","tier":3},{"person_id":"p0012","match_type":"initial_last","tier":3},{"person_id":"p0013","match_type":"initial_last","tier":3},{"person_id":"p0014","match_type":"initial_last","tier":3},{"person_id":"p0015","match_type":"initial_last","tier":3},{"person_id":"p0016","match_type":"initial_last","tier":3},{"person_id":"p0017","match_type":"initial_last","tier":3},{"person_id":"p0018","match_type":"initial_last","tier":3},{"person_id":"p0019","match_type":"initial_last","tier":3},{"person_id":"p0020","match_type":"initial_last","tier":3},{"person_id":"p0021","match_type":"initial_last","tier":3},{"person_id":"p0022","match_type":"initial_last","tier":3},{"person_id":"p0023","match_type":"initial_last","tier":3},{"person_id":"p0024","match_type":"initial_last","tier":3},{"person_id":"p0025","match_type":"initial_last","tier":3}],"3 ?":[{"person_id":"p0006","match_type":"initial_last","tier":3},{"person_id":"p0007","match_type":"initial_last","tier":3},{"person_id":"p0008","match_type":"initial_last","tier":3},{"person_id":"p0009","match_type":"initial_last","tier":3},{"person_id":"p0010","match_type":"initial_last","tier":3},{"person_id":"p0011","match_type":"initial_last","tier":3},{"person_id":"p0012","match_type":"initial_last","tier":3},{"person_id":"p0013","match_type":"initial_last","tier":3},{"person_id":"p0014","match_type":"initial_last","tier":3},{"person_id":"p0015","match_type":"initial_last","tier":3},{"person_id":"p0016","match_type":"initial_last","tier":3},{"person_id":"p0017","match_type":"initial_last","tier":3},{"person_id":"p0018","match_type":"initial_last","tier":3},{"person_id":"p0019","match_type":"initial_last","tier":3},{"person_id":"p0020","match_type":"initial_last","tier":3},{"person_id":"p0021","match_type":"initial_last","tier":3},{"person_id":"p0022","match_type":"initial_last","tier":3},{"person_id":"p0023","match_type":"initial_last","tier":3},{"person_id":"p0024","match_type":"initial_last","tier":3},{"person_id":"p0025","match_type":"initial_last","tier":3}],"36738-g-1159b-n908je-saf-teb-foia-1371-no records ?":[{"person_id":"p0007","match_type":"full","tier":3}],"36738-g-1159b-n908je-saf-teb-foia-1371-no":[{"person_id":"p0007","match_type":"first","tier":3}],"36742-g-1159b-n908je-teb-pbi-foia-1372-no records ?":[{"person_id":"p0008","match_type":"full","tier":3}],"36742-g-1159b-n908je-teb-pbi-foia-1372-no":[{"person_id":"p0008","match_type":"first","tier":3}],"36746-g-1159b-n908je-pbi-tvc-foia-1373-no records ?":[{"person_id":"p0009","match_type":"Line truncated
|
||||
{"names":{"*d - cbp offcr-c":[{"person_id":"p0000","match_type":"full","tier":3}],"*d":[{"person_id":"p0000","match_type":"first","tier":3},{"person_id":"p0001","match_type":"first","tier":3},{"person_id":"p0002","match_type":"first","tier":3}],"offcr-c":[{"person_id":"p0000","match_type":"last","tier":3},{"person_id":"p0003","match_type":"last","tier":3}],"*. offcr-c":[{"person_id":"p0000","match_type":"initial_last","tier":3},{"person_id":"p0003","match_type":"initial_last","tier":3}],"* offcr-c":[{"person_id":"p0000","match_type":"initial_last","tier":3},{"person_id":"p0003","match_type":"initial_last","tier":3}],"*d - cbp officer":[{"person_id":"p0001","match_type":"full","tier":3}],"officer":[{"person_id":"p0001","match_type":"last","tier":3},{"person_id":"p0004","match_type":"last","tier":3}],"*. officer":[{"person_id":"p0001","match_type":"initial_last","tier":3},{"person_id":"p0004","match_type":"initial_last","tier":3}],"* officer":[{"person_id":"p0001","match_type":"initial_last","tier":3},{"person_id":"p0004","match_type":"initial_last","tier":3}],"*d - cbp officer-c":[{"person_id":"p0002","match_type":"full","tier":3}],"officer-c":[{"person_id":"p0002","match_type":"last","tier":3},{"person_id":"p0005","match_type":"last","tier":3}],"*. officer-c":[{"person_id":"p0002","match_type":"initial_last","tier":3},{"person_id":"p0005","match_type":"initial_last","tier":3}],"* officer-c":[{"person_id":"p0002","match_type":"initial_last","tier":3},{"person_id":"p0005","match_type":"initial_last","tier":3}],"*f - cbp offcr-c":[{"person_id":"p0003","match_type":"full","tier":3}],"*f":[{"person_id":"p0003","match_type":"first","tier":3},{"person_id":"p0004","match_type":"first","tier":3},{"person_id":"p0005","match_type":"first","tier":3}],"*f - cbp officer":[{"person_id":"p0004","match_type":"full","tier":3}],"*f - cbp officer-c":[{"person_id":"p0005","match_type":"full","tier":3}],"36653-g-1159b-n908je-pbi-tist-foia-1336-no records ?":[{"person_id":"p0006","match_type":"full","tier":3}],"36653-g-1159b-n908je-pbi-tist-foia-1336-no":[{"person_id":"p0006","match_type":"first","tier":3}],"3. ?":[{"person_id":"p0006","match_type":"initial_last","tier":3},{"person_id":"p0007","match_type":"initial_last","tier":3},{"person_id":"p0008","match_type":"initial_last","tier":3},{"person_id":"p0009","match_type":"initial_last","tier":3},{"person_id":"p0010","match_type":"initial_last","tier":3},{"person_id":"p0011","match_type":"initial_last","tier":3},{"person_id":"p0012","match_type":"initial_last","tier":3},{"person_id":"p0013","match_type":"initial_last","tier":3},{"person_id":"p0014","match_type":"initial_last","tier":3},{"person_id":"p0015","match_type":"initial_last","tier":3},{"person_id":"p0016","match_type":"initial_last","tier":3},{"person_id":"p0017","match_type":"initial_last","tier":3},{"person_id":"p0018","match_type":"initial_last","tier":3},{"person_id":"p0019","match_type":"initial_last","tier":3},{"person_id":"p0020","match_type":"initial_last","tier":3},{"person_id":"p0021","match_type":"initial_last","tier":3},{"person_id":"p0022","match_type":"initial_last","tier":3},{"person_id":"p0023","match_type":"initial_last","tier":3},{"person_id":"p0024","match_type":"initial_last","tier":3},{"person_id":"p0025","match_type":"initial_last","tier":3}],"3 ?":[{"person_id":"p0006","match_type":"initial_last","tier":3},{"person_id":"p0007","match_type":"initial_last","tier":3},{"person_id":"p0008","match_type":"initial_last","tier":3},{"person_id":"p0009","match_type":"initial_last","tier":3},{"person_id":"p0010","match_type":"initial_last","tier":3},{"person_id":"p0011","match_type":"initial_last","tier":3},{"person_id":"p0012","match_type":"initial_last","tier":3},{"person_id":"p0013","match_type":"initial_last","tier":3},{"person_id":"p0014","match_type":"initial_last","tier":3},{"person_id":"p0015","match_type":"initial_last","tier":3},{"person_id":"p0016","match_type":"initial_last","tier":3},{"person_id":"p0017","match_type":"initial_last","tier":3},{"person_id":"p0018","match_type":"initial_last","tier":3},{"person_id":"p0019","match_type":"initial_last","tier":3},{"person_id":"p0020","match_type":"initial_last","tier":3},{"person_id":"p0021","match_type":"initial_last","tier":3},{"person_id":"p0022","match_type":"initial_last","tier":3},{"person_id":"p0023","match_type":"initial_last","tier":3},{"person_id":"p0024","match_type":"initial_last","tier":3},{"person_id":"p0025","match_type":"initial_last","tier":3}],"36738-g-1159b-n908je-saf-teb-foia-1371-no records ?":[{"person_id":"p0007","match_type":"full","tier":3}],"36738-g-1159b-n908je-saf-teb-foia-1371-no":[{"person_id":"p0007","match_type":"first","tier":3}],"36742-g-1159b-n908je-teb-pbi-foia-1372-no records ?":[{"person_id":"p0008","match_type":"full","tier":3}],"36742-g-1159b-n908je-teb-pbi-foia-1372-no":[{"person_id":"p0008","match_type":"first","tier":3}],"36746-g-1159b-n908je-pbi-tvc-foia-1373-no records ?":[{"person_id":"p0009","match_type":"Line truncated
|
||||
+30
-2
@@ -102,10 +102,12 @@ async function loadAssociates() {
|
||||
try {
|
||||
const resp = await fetch("/api/associates");
|
||||
state.associates = await resp.json();
|
||||
console.log(`Loaded ${Object.keys(state.associates.names).length} associate lookups`);
|
||||
// Convert victim_names array to Set for fast lookup
|
||||
state.associates.victim_set = new Set(state.associates.victim_names || []);
|
||||
console.log(`Loaded ${Object.keys(state.associates.names).length} associate lookups, ${state.associates.victim_set.size} victim names`);
|
||||
} catch (e) {
|
||||
console.warn("Failed to load associates data:", e);
|
||||
state.associates = { names: {}, persons: {} };
|
||||
state.associates = { names: {}, persons: {}, victim_set: new Set() };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -930,6 +932,22 @@ function tierDescription(tier) {
|
||||
return "Named in Epstein case files";
|
||||
}
|
||||
|
||||
function isVictimMatch(text) {
|
||||
const vs = state.associates?.victim_set;
|
||||
if (!vs || vs.size === 0) return false;
|
||||
const key = text.toLowerCase().trim();
|
||||
if (vs.has(key)) return true;
|
||||
// Also check with prefix/suffix combined
|
||||
const prefix = solveFilterPrefix.value.toLowerCase().trim();
|
||||
const suffix = solveFilterSuffix.value.toLowerCase().trim();
|
||||
if (prefix || suffix) {
|
||||
if (vs.has(prefix + key + suffix)) return true;
|
||||
if (prefix && vs.has(prefix + key)) return true;
|
||||
if (suffix && vs.has(key + suffix)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function showAssocDetail(assocMatches, anchorEl) {
|
||||
// Remove any existing popup
|
||||
const old = document.getElementById("assoc-detail");
|
||||
@@ -1090,6 +1108,16 @@ function handleSolveEvent(data, gapIdx) {
|
||||
<span class="result-error">${data.error_px.toFixed(1)}px ${data.source || ""}</span>
|
||||
`;
|
||||
|
||||
const victim = isVictimMatch(data.text);
|
||||
|
||||
if (victim) {
|
||||
const vBadge = document.createElement("span");
|
||||
vBadge.className = "assoc-badge victim";
|
||||
vBadge.textContent = "V";
|
||||
vBadge.title = "Matches a known victim name";
|
||||
div.prepend(vBadge);
|
||||
}
|
||||
|
||||
if (assocMatches.length > 0) {
|
||||
const badge = document.createElement("button");
|
||||
badge.className = `assoc-badge ${tierBadgeClass(topMatch.tier)}`;
|
||||
|
||||
@@ -521,6 +521,13 @@ main {
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.assoc-badge.victim {
|
||||
background: #7b1fa2;
|
||||
color: #fff;
|
||||
cursor: default;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* Associate detail popup */
|
||||
#assoc-detail {
|
||||
position: absolute;
|
||||
|
||||
Reference in new issue
Block a user