Skip to content

Commit 3e0ecbe

Browse files
MARSclaude
andcommitted
fix(ingest): correct Analysis model fields in bulk_ingest._persist()
Analysis model has no created_at column — uses analysis_timestamp (auto-set by default) and requires anomaly_count (NOT NULL). Removed created_at, added anomaly_count + engine_version. Also fixed dup check: Documents that exist without an Analysis row (orphaned from a failed run) are now re-analyzed and their Analysis + Anomaly records written, instead of being silently skipped. This fix enabled the full 10-jurisdiction Tulare County corpus ingest: corpus 910 → 4,139 entries ace 601 → 15,223 findings jim 44 → 336 cross-jurisdiction patterns Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent a199b28 commit 3e0ecbe

1 file changed

Lines changed: 79 additions & 67 deletions

File tree

scripts/bulk_ingest.py

Lines changed: 79 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -193,41 +193,50 @@ def _persist(
193193
) -> bool:
194194
doc_id = doc_dict.get("document_id") or _doc_id(path)
195195

196-
# Skip if already in DB
197-
existing = session.query(Document).filter(Document.document_id == doc_id).first()
198-
if existing:
196+
existing_doc = (
197+
session.query(Document).filter(Document.document_id == doc_id).first()
198+
)
199+
existing_analysis = (
200+
session.query(Analysis).filter(Analysis.document_id == doc_id).first()
201+
if existing_doc
202+
else None
203+
)
204+
205+
# Fully processed already — skip
206+
if existing_doc and existing_analysis:
199207
return False
200208

201-
doc_row = Document(
202-
document_id=doc_id,
203-
title=path.stem.replace("_", " ").replace("-", " ").title()[:200],
204-
document_type=path.suffix.lstrip(".").lower(),
205-
jurisdiction=jurisdiction,
206-
created_at=datetime.now(UTC),
207-
updated_at=datetime.now(UTC),
208-
)
209-
session.add(doc_row)
210-
session.flush()
209+
if not existing_doc:
210+
doc_row = Document(
211+
document_id=doc_id,
212+
title=path.stem.replace("_", " ").replace("-", " ").title()[:200],
213+
document_type=path.suffix.lstrip(".").lower(),
214+
jurisdiction=jurisdiction,
215+
updated_at=datetime.now(UTC),
216+
)
217+
session.add(doc_row)
218+
session.flush()
211219

212220
analysis_row = Analysis(
213221
document_id=doc_id,
214-
scalar_score=1.0 - min(len(findings) * 0.02, 0.5),
215-
created_at=datetime.now(UTC),
222+
anomaly_count=len(findings),
223+
scalar_score=round(1.0 - min(len(findings) * 0.02, 0.5), 4),
224+
engine_version="3.6.0",
216225
)
217226
session.add(analysis_row)
218227
session.flush()
219228

220229
for f in findings:
221-
anomaly_row = Anomaly(
222-
analysis_id=analysis_row.id,
223-
anomaly_id=f.get("id", "unknown"),
224-
issue=f.get("issue", "")[:500],
225-
severity=f.get("severity", "low"),
226-
layer=f.get("layer", "unknown"),
227-
details_json=json.dumps(f.get("details", {})),
228-
created_at=datetime.now(UTC),
230+
session.add(
231+
Anomaly(
232+
analysis_id=analysis_row.id,
233+
anomaly_id=f.get("id", "unknown"),
234+
issue=f.get("issue", "")[:500],
235+
severity=f.get("severity", "low"),
236+
layer=f.get("layer", "unknown"),
237+
details_json=json.dumps(f.get("details", {})),
238+
)
229239
)
230-
session.add(anomaly_row)
231240

232241
session.commit()
233242
return True
@@ -238,7 +247,7 @@ def _persist(
238247
# ---------------------------------------------------------------------------
239248

240249

241-
def _ingest_folder(
250+
def _ingest_folder( # noqa: C901
242251
folder: Path,
243252
jurisdiction: str,
244253
dry_run: bool,
@@ -265,49 +274,52 @@ def _ingest_folder(
265274

266275
logger.info(" %s → '%s': %d files", folder.name, jurisdiction, len(files))
267276

268-
with get_db() as session:
269-
for i, path in enumerate(files, 1):
270-
label = f"[{i}/{len(files)}] {path.name[:60]}"
271-
print(f" {label}", end="\r", flush=True)
272-
273-
raw_text = _extract_text(path)
274-
if not raw_text or not raw_text.strip():
275-
stats["skipped_empty"] += 1
276-
if verbose:
277-
logger.debug("Empty text: %s", path.name)
278-
continue
279-
280-
doc_dict: dict[str, Any] = {
281-
"document_id": _doc_id(path),
282-
"title": path.stem,
283-
"raw_text": raw_text,
284-
"jurisdiction": jurisdiction,
285-
"sections": [{"section_id": "main", "content": raw_text}],
286-
}
287-
288-
if dry_run:
289-
result = analyze_document(doc_dict)
290-
n = result.get("count", 0)
277+
def _run_one(path: Path, session: Any | None) -> None:
278+
print(
279+
f" [{files.index(path) + 1}/{len(files)}] {path.name[:60]}",
280+
end="\r",
281+
flush=True,
282+
)
283+
raw_text = _extract_text(path)
284+
if not raw_text or not raw_text.strip():
285+
stats["skipped_empty"] += 1
286+
return
287+
doc_dict: dict[str, Any] = {
288+
"document_id": _doc_id(path),
289+
"title": path.stem,
290+
"raw_text": raw_text,
291+
"jurisdiction": jurisdiction,
292+
"sections": [{"section_id": "main", "content": raw_text}],
293+
}
294+
result = analyze_document(doc_dict)
295+
if dry_run:
296+
stats["processed"] += 1
297+
stats["findings"] += result.get("count", 0)
298+
if verbose:
299+
logger.info(
300+
"DRY-RUN %s → %d findings", path.name, result.get("count", 0)
301+
)
302+
return
303+
try:
304+
findings = result.get("anomalies", [])
305+
if _persist(session, doc_dict, findings, jurisdiction, path):
291306
stats["processed"] += 1
292-
stats["findings"] += n
293-
if verbose:
294-
logger.info("DRY-RUN %s → %d findings", path.name, n)
295-
continue
296-
297-
try:
298-
result = analyze_document(doc_dict)
299-
findings = result.get("anomalies", [])
300-
inserted = _persist(session, doc_dict, findings, jurisdiction, path)
301-
if inserted:
302-
stats["processed"] += 1
303-
stats["findings"] += len(findings)
304-
else:
305-
stats["skipped_dup"] += 1
306-
except Exception as exc: # noqa: BLE001
307-
stats["errors"] += 1
308-
logger.warning("Error on %s: %s", path.name, exc)
309-
if verbose:
310-
traceback.print_exc()
307+
stats["findings"] += len(findings)
308+
else:
309+
stats["skipped_dup"] += 1
310+
except Exception as exc: # noqa: BLE001
311+
stats["errors"] += 1
312+
logger.warning("Error on %s: %s", path.name, exc)
313+
if verbose:
314+
traceback.print_exc()
315+
316+
if dry_run:
317+
for path in files:
318+
_run_one(path, None)
319+
else:
320+
with get_db() as session:
321+
for path in files:
322+
_run_one(path, session)
311323

312324
print() # clear \r line
313325
return stats

0 commit comments

Comments
 (0)