Skip to content

Commit 7b0b9b8

Browse files
feat(investigator): add five modules for citizen & investigative journalism (#2)
Adds a toolkit of investigator-focused extraction modules aimed at citizen journalists and investigative reporters working with large document releases (Epstein filings, FinCEN Files, Panama/Paradise/Pandora Papers, etc.). Each module is a standalone Zig translation unit with stable C-ABI entry points, callable from Chapel, Rust, Julia, OCaml, or Python ctypes. Modules: - flight_log.zig: extracts aircraft tail numbers (N908JE, G-EJES), IATA/ICAO airport codes (whitelisted to reduce false positives), phone numbers, street addresses, and passenger-manifest markers. - entity_graph.zig: builds a cross-document co-occurrence graph of capitalised personal names with GraphML + CSV export for use in Gephi / Cytoscape / Maltego / spreadsheets. - redaction_recovery.zig: extends the base redaction-detection stage with per-page density maps and overlay-only text recovery for PDFs whose content stream was not scrubbed. Only extracts text already present in the stream — no encryption bypass. - evasion_detect.zig: categorises non-answer patterns in deposition transcripts ("I don't recall", "Fifth Amendment", "asked and answered", etc.) with per-1000-token rate metric to flag evasive witness segments. - investigator_summary.zig: emits investigator-friendly per-document JSON summaries with human-readable flags (has_redactions, has_recoverable_text, deposition, high_evasion, ...). Also adds docs/INVESTIGATOR-TOOLKIT.md documenting the recommended workflow and each module's C-ABI surface. All modules are pattern-based (no ML dependency), deterministic, and tested. Wired into docudactyl_ffi.zig so exports are included in the shared library. Co-authored-by: Claude <noreply@anthropic.com>
1 parent e533bc4 commit 7b0b9b8

7 files changed

Lines changed: 2224 additions & 0 deletions

File tree

docs/INVESTIGATOR-TOOLKIT.md

Lines changed: 281 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,281 @@
1+
# Docudactyl — Investigator Toolkit
2+
3+
<!--
4+
SPDX-License-Identifier: PMPL-1.0-or-later
5+
Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) <j.d.a.jewell@open.ac.uk>
6+
-->
7+
8+
This document describes the **investigator-focused extraction modules** added
9+
to Docudactyl for use by citizen journalists, independent researchers, and
10+
investigative reporters working with large document releases such as the
11+
Epstein filings, Panama/Paradise/Pandora Papers, FinCEN Files, etc.
12+
13+
These modules are **standalone Zig translation units** with stable C-ABI
14+
entry points. They can be called directly from Chapel, Rust, Julia, OCaml,
15+
Python `ctypes`, or any language that speaks the C FFI — without going
16+
through the full HPC pipeline.
17+
18+
---
19+
20+
## Why these modules exist
21+
22+
The base HPC pipeline answers "what is in this document?". Investigative
23+
journalism needs different questions:
24+
25+
1. **Who appears with whom, how often?**[`entity_graph`](#entity-graph)
26+
2. **What was hidden under black bars?**[`redaction_recovery`](#redaction-recovery)
27+
3. **Where did the jet actually go?**[`flight_log`](#flight-log)
28+
4. **Where did the witness stop answering?**[`evasion_detect`](#evasion-detect)
29+
5. **What does this document contain, at a glance?**[`investigator_summary`](#investigator-summary)
30+
31+
Each module is **pattern-based, no ML dependency**, fast, and deterministic.
32+
33+
---
34+
35+
## Entity Graph
36+
37+
**File:** `ffi/zig/src/entity_graph.zig`
38+
39+
Builds a cross-document co-occurrence graph of capitalised personal names.
40+
Exports to **GraphML** (Gephi, yEd, Cytoscape) and **CSV** (Excel,
41+
LibreOffice, Maltego, Neo4j).
42+
43+
### C ABI
44+
45+
```c
46+
typedef struct EntityGraph EntityGraph;
47+
48+
EntityGraph* ddac_entity_graph_new(void);
49+
void ddac_entity_graph_free(EntityGraph*);
50+
51+
int ddac_entity_graph_add_document(EntityGraph*, const char* text, size_t len);
52+
int ddac_entity_graph_export_graphml(EntityGraph*, const char* path);
53+
int ddac_entity_graph_export_csv(EntityGraph*, const char* path);
54+
55+
uint32_t ddac_entity_graph_node_count(EntityGraph*);
56+
uint32_t ddac_entity_graph_edge_count(EntityGraph*);
57+
```
58+
59+
### Usage sketch
60+
61+
```chapel
62+
// Chapel pseudocode
63+
var g = ddac_entity_graph_new();
64+
for doc in manifest {
65+
var text = readExtractedText(doc);
66+
ddac_entity_graph_add_document(g, text.c_str(), text.len);
67+
}
68+
ddac_entity_graph_export_graphml(g, "/out/entities.graphml");
69+
ddac_entity_graph_export_csv(g, "/out/entities.csv");
70+
ddac_entity_graph_free(g);
71+
```
72+
73+
Load `entities.graphml` in Gephi → run ForceAtlas2 → see the cluster
74+
structure. Load `entities.csv` in any spreadsheet to sort by weight.
75+
76+
### Notes
77+
- Extracts 2+ consecutive capitalised words, optionally preceded by a
78+
title (Mr./Mrs./Dr./Prince/Sir/…).
79+
- Filters a conservative stopword list (weekdays, months, common
80+
sentence-initial words).
81+
- Edge weight accumulates across documents — high weight indicates
82+
recurring co-occurrence worth examining.
83+
84+
---
85+
86+
## Redaction Recovery
87+
88+
**File:** `ffi/zig/src/redaction_recovery.zig`
89+
90+
Extends the base redaction-detection stage with **per-page density maps**
91+
and **overlay-only text recovery**. When a PDF carries `/Redact`
92+
annotations (black boxes) but the underlying content stream is intact,
93+
this module extracts the text that the overlay was meant to hide.
94+
95+
### When it works
96+
97+
✅ Overlay redactions where the text stream was NOT scrubbed (common
98+
FOIA failure mode — many Epstein-era productions exhibit this).
99+
❌ Redactions that rasterise the page or strip the content stream.
100+
❌ Redactions applied at scan time (pixel-level black bars).
101+
102+
### C ABI
103+
104+
```c
105+
typedef struct {
106+
int status;
107+
uint32_t total_pages;
108+
uint32_t total_redactions;
109+
uint32_t pages_with_redactions;
110+
uint32_t recoverable_pages;
111+
uint64_t recovered_bytes;
112+
PageStats pages[4096];
113+
char summary[512];
114+
} RedactionRecoveryResult;
115+
116+
int ddac_redaction_recovery_analyze(const char* pdf_path, RedactionRecoveryResult*);
117+
int ddac_redaction_recovery_dump_text(const char* pdf_path, const char* out_path);
118+
```
119+
120+
### Legal & ethical note
121+
122+
This module extracts text that is **already present** in the document's
123+
content stream — the same text that `cmd-A, cmd-C` in Preview would
124+
reveal. It does not break encryption, does not OCR under pixel-level
125+
redactions, and does not decode protected content. Use responsibly and
126+
check your local jurisdiction's rules on reporting improperly redacted
127+
material.
128+
129+
---
130+
131+
## Flight Log
132+
133+
**File:** `ffi/zig/src/flight_log.zig`
134+
135+
Extracts travel-document entities from text:
136+
137+
| Entity | Examples |
138+
|--------|----------|
139+
| Tail numbers | `N908JE`, `N212JE`, `G-EJES`, `D-IIKA` |
140+
| IATA codes | `TEB`, `PBI`, `JFK`, `STT`, `LHR`, `CDG`, `DXB` |
141+
| ICAO codes | `KTEB`, `KPBI`, `KJFK`, `EGLL`, `LFPB` |
142+
| Phones | `(212) 555-1234`, `+1 212 555 9999`, `+44 20 7946 0958` |
143+
| Addresses | Line-leading number + road-word heuristic |
144+
| Manifest markers | `PAX:`, `PASSENGERS:`, `MANIFEST:`, `GUESTS:` |
145+
146+
### C ABI
147+
148+
```c
149+
typedef struct { /* ... */ } FlightLogResult;
150+
int ddac_flight_log_process(const char* text, size_t len, FlightLogResult*);
151+
```
152+
153+
### Notes
154+
- IATA/ICAO codes use a **whitelist** of airports of interest
155+
(Teterboro, Palm Beach, St. Thomas, Le Bourget, Heathrow, Dubai,
156+
etc.) to avoid false positives on three-letter acronyms like `CEO`
157+
or `FBI`. Extend the whitelist in the module source as needed.
158+
- Tail-number pattern is liberal enough to tolerate OCR noise but
159+
tight enough to reject ordinary words.
160+
161+
---
162+
163+
## Evasion Detect
164+
165+
**File:** `ffi/zig/src/evasion_detect.zig`
166+
167+
Detects and categorises evasive / non-answer patterns in deposition and
168+
interview transcripts:
169+
170+
| Category | Example phrase |
171+
|----------|---------------|
172+
| `no_recall` | "I don't recall", "I have no recollection" |
173+
| `no_memory` | "I don't remember", "I can't remember" |
174+
| `not_sure` | "I'm not sure", "I couldn't say" |
175+
| `no_knowledge` | "Not to my knowledge", "I'm not aware" |
176+
| `would_check` | "I'd have to check", "I would need to check" |
177+
| `asked_answered` | "Asked and answered" (lawyer interjection) |
178+
| `fifth_amendment` | "Fifth Amendment", "on the advice of counsel" |
179+
| `decline_answer` | "I decline to answer", "refuse to answer" |
180+
181+
Reports category counts, total events, and an **evasion rate** (events
182+
per 1000 tokens, fixed-point ×1000). A rate above ~20 (i.e. `×1000 >
183+
20000`) typically indicates a heavily evasive witness segment.
184+
185+
### C ABI
186+
187+
```c
188+
typedef struct {
189+
int status;
190+
uint32_t category_counts[8];
191+
uint32_t total_events;
192+
uint32_t total_tokens;
193+
uint32_t events_per_1k_fixed; // rate × 1000
194+
char summary[512];
195+
} EvasionResult;
196+
197+
int ddac_evasion_detect(const char* text, size_t len, EvasionResult*);
198+
```
199+
200+
---
201+
202+
## Investigator Summary
203+
204+
**File:** `ffi/zig/src/investigator_summary.zig`
205+
206+
Takes a populated `InvestigatorSummary` struct and emits an
207+
investigator-friendly **JSON summary** per document. Designed to be
208+
readable in a text editor and ingestible by spreadsheets, dataset
209+
browsers, or static-site generators.
210+
211+
The JSON is flat and forgiving: any field may be zero/empty without
212+
breaking consumers. A `flags` array gives quick visual triage:
213+
214+
```json
215+
{
216+
"source_path": "/data/release_2024/doc_0042.pdf",
217+
"sha256": "...",
218+
"page_count": 184,
219+
"redactions": {"count": 12, "pages_affected": 4, "recoverable_pages": 2},
220+
"financial": {"amounts": 3, "accounts": 1},
221+
"legal": {"case_citations": 5, "dockets": 2, "statutes": 1},
222+
"speakers": {"count": 2, "is_deposition": true},
223+
"evasion": {"total": 17, "per_1k_tokens": 12.5},
224+
"entities": {
225+
"persons": ["Jeffrey Epstein", "Ghislaine Maxwell"],
226+
"tail_numbers": ["N908JE"],
227+
"airports": ["TEB", "PBI", "KTEB"],
228+
"phones": ["+1 212 555 1234"],
229+
"addresses": ["9 East 71st Street"]
230+
},
231+
"flags": ["has_redactions", "has_recoverable_text", "deposition", "high_evasion"]
232+
}
233+
```
234+
235+
### C ABI
236+
237+
```c
238+
int ddac_investigator_summary_write(const char* out_path, const InvestigatorSummary*);
239+
int ddac_investigator_summary_set_list_item(StringList*, uint32_t idx,
240+
const char* text, size_t len);
241+
```
242+
243+
---
244+
245+
## Recommended Investigator Workflow
246+
247+
1. **Run base pipeline** (`DocudactylHPC`) to extract text + SHA + PREMIS.
248+
2. **Per-document pass** — for each extracted text file, call:
249+
- `ddac_flight_log_process` → flight / travel entities
250+
- `ddac_evasion_detect` → deposition evasion stats
251+
- `ddac_redaction_recovery_analyze` + `_dump_text` (PDFs only)
252+
3. **Corpus-wide pass** — accumulate entities into a single graph:
253+
- `ddac_entity_graph_new`
254+
- `ddac_entity_graph_add_document` per document
255+
- `ddac_entity_graph_export_graphml` + `_export_csv`
256+
4. **Summary pass** — populate `InvestigatorSummary` from the results
257+
above and emit per-document JSON with
258+
`ddac_investigator_summary_write`.
259+
5. **Review** — open the GraphML in Gephi, the CSV in a spreadsheet, and
260+
the per-document JSONs in a text editor or a static-site browser.
261+
262+
---
263+
264+
## Building & Testing
265+
266+
From `ffi/zig/`:
267+
268+
```bash
269+
zig build # build shared + static libraries
270+
zig build test # run unit tests (all new modules included)
271+
```
272+
273+
All new modules have accompanying Zig unit tests. No new C dependencies
274+
beyond Poppler + GLib (which Docudactyl already links).
275+
276+
---
277+
278+
## License
279+
280+
All new modules are released under **PMPL-1.0-or-later** (with MPL-2.0
281+
fallback), matching the rest of Docudactyl.

ffi/zig/src/docudactyl_ffi.zig

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,12 @@ const financial_extract = @import("financial_extract.zig");
2828
const speaker_id = @import("speaker_id.zig");
2929
const reextract = @import("reextract.zig");
3030
const quality_stats = @import("quality_stats.zig");
31+
// ── Investigator-Focused Modules (citizen & investigative journalism) ──
32+
const flight_log = @import("flight_log.zig");
33+
const entity_graph = @import("entity_graph.zig");
34+
const redaction_recovery = @import("redaction_recovery.zig");
35+
const evasion_detect = @import("evasion_detect.zig");
36+
const investigator_summary = @import("investigator_summary.zig");
3137

3238
// Ensure submodule exports are included in the shared library
3339
comptime {
@@ -43,6 +49,11 @@ comptime {
4349
_ = speaker_id;
4450
_ = reextract;
4551
_ = quality_stats;
52+
_ = flight_log;
53+
_ = entity_graph;
54+
_ = redaction_recovery;
55+
_ = evasion_detect;
56+
_ = investigator_summary;
4657
}
4758

4859
const c = @cImport({

0 commit comments

Comments
 (0)