Project plan from the 2026-06-09 review. Two parts: (1) a snapshot index of what remains toward "fully validated" (authoritative detail lives in the per-area TODOs — do not let this index drift; update the source docs); (2) the audit-check catalog — systematic-error detectors runnable in Python, prioritized cheapest-first.
Authoritative sources: TODO-validation.md (opinions), TODO-primarylaw.md
(const/rules/statutes/admin), TODO-distribution.md (release/update system).
Opinions — substantial workstreams:
- §5 court-report diff residue (the court's own cTrack export as independent authority): (a) 1,808 case_name diffs to subcategorize; (b) ~101 >31d date holds; (c) ~25 author no-byline; (d) ~36 cite-mismatches; (e) 94 cite-collisions; (f) Nature-of-Action → case_type; (g) corpus-wide docket cross-check.
- §2 gap-era Westlaw worklist (~400+ pool, 1953–1996 Quick Check — bound-text quality/syllabi; era is already ~99% two-source).
- Priority #2/§3 cTrack PDF acquisition (court PDFs where we lack one; 1997 era especially). Dockets recovered → unblocked.
- §13 constitutional-cite coverage (wire
nd_const_phrasesintocite_extract; jetcite old-form recognizers; 1889 crosswalk; phrase-table buildout). - §14 forward-graph remainder: (d)
antecedent_namecoverage (re-measure post jetcite-2.3.0 rebuild), (e)parallel_groupfor out-of-corpus parallels, (g) recognizer omissions, (h) garbage-volume cites.
Opinions — closeout/small: §10 (28 NW2d-era shared-page orders; bracketed
synthetic display in lookup_opinion; publish freeze); §8 (11 pre-1953 OCR
residuals; LEXIS/L.R.A. removal decision incl. A.L.R. + scope questions; pre-1997
judges-panel rebuild); §7 (1997ND5/1998ND22 markdown recovery); §1 (Hassen v. Salem
50 N.D. 825-vs-813; syllabus cutoff year); §6 (130 dup-candidate bookkeeping);
detect_cite_swap .doc-row refinement; oid 12500 caption: Longtine v. Yeado →
State v. Herrick, 1997 ND 155 (confirmed live 2026-06-09 — text+cites are
Herrick; real Longtine is oid 12507). Pipeline: freshness heartbeat; scraper
letter-spacing cleanup; marker escalation; pilcrow detection at ingest.
Primary-law DBs:
- PL-1(b)/(c) footgun: regenerate
~/refs/statute/NDCCmarkdown with the fixed converter + commitscrape_nd_code.pyin code-mirror — until then a statutes re-ingest re-corrupts the DB. - PL-VALIDATE validators (folded into Part 2 below; none built before 2026-06-09).
- Constitution: PL-CONST-CROSSWALK (1889↔modern); PL-CONST-AMEND-RECONCILE; PL-CONST-STRUCTURAL (LXXVIII/XCVI/XC-2nd/LXXXI).
- PL-3 table-section converter weakness (low; DB hand-corrected).
Cross-cutting:
- §9's "% fully validated by decade" metric — specified, never wired into
get_database_stats. invariants.pycovers opinions.db only; the 4 primary-law DBs have no standing invariant dashboard (audit.py Part 2 is the seed).- TODO-distribution.md system (orthogonal to validation; gates public usefulness).
Sources fail in characteristic ways — CL OCR (char errors, co-party captions, date drift, double-ingest, cite contamination), pdfminer-from-PDF (broken ToUnicode, two-column interleave, misnamed files), Westlaw (editorial leak, shared-cite mispairing), pdfplumber/code-mirror (dropped link-anchor lines, phantom headers, table splits). The best cheap checks exploit internal redundancy — the corpus witnessing itself.
Implementation home: ndcourts_mcp/audit_corpus.py (read-only dashboard + TSV
triage outputs, python -m ndcourts_mcp.audit_corpus; audit.py is the older
missing-opinions audit).
Citation graph as self-witness:
- 1. Temporal sanity (
cite_temporal) — citing.date_filed ≥ cited.date_filed for everycited_byedge (same-day OK: companions). A wrong date on a much-cited case violates many edges at once. Built 2026-06-09. - 2. Antecedent-name witness — BUILT 2026-07-13 (
audit_corpus antecedent_witness): every cited_by edge'santecedent_namevs the resolved opinion'scase_nameby distinctive-token overlap (corporate suffixes non-distinctive). First run flagged 1,046/116,853 edges; the §4 reconciliation (cite_reconcile.py) auto-fixed 86 false edges + recovered 848 missing ones; 960 residual flags = abbreviation-style mismatches (edge fine) + the 463-row manual queue (triage/cite-reconcile-manual-2026-07-13.tsv) + bad antecedent spans (§14(d)). Coverage grows with §14(d). - 3. Pinpoint-range (
pinpoint_range) — a cite toYYYY ND n, ¶ Pwhere our copy of that opinion has max ¶ < P ⇒ truncation (or pincite garble). The cheapest silent-truncation detector. Built 2026-06-09. - 4. Cross-corpus resolver (
xref_resolve) — every NDCC/NDAC/rule/const cite intext_citationsresolves to a provision in the matching corpus DB (viacorpus.cite_key); bucketed by opinion era (old opinions legitimately cite repealed/renumbered law). Two corpora witness each other. Built 2026-06-09.
Text integrity:
- 5. ¶-marker continuity (
para_continuity) —[¶1]..[¶N]strictly ascending, no gaps/dups/restarts, per modern opinion. Gap = dropped paragraph. Built 2026-06-09. GAP classification WORKED 2026-06-09 (triage/para-gap-classified-2026-06-09.tsv): 341 CORRUPT_TOKEN → 287 markers fixed (para-marker-ocr-variants-2026-06-09). GAPs 1,206→1,056. Remaining queues (measured):- TEXT_MISSING — WORKED 2026-06-10 (
text-missing-measured-2026-06-09.tsv): 473 signature-only spliced (sig-splice-2026-06-09; 15 stale) + 219 substantive spliced (substantive-missing-2026-06-09; 7 held — 5 stale, Wodrich 12605 known-correct, Helbling 17357 page-rotated PDF missing ¶22–32, queued per-item). Digit-flip exposure for the cohort audited: 5,014 ¶s compared, 70 flips fixed PDF-print-verified (digit-flips-pdfverified-2026-06-09; crops intriage/flipverify/; 2 PDF text-layer ToUnicode rejections; 2 court typos preserved+baselined).para_continuity1,086→521. Remaining: 74 PDF-lacks-marker rows (per-item) + 14 opinions with no PDF. Helbling 17357 RESOLVED 2026-06-10 as a false positive — the "missing ¶22–32" are the divorce settlement agreement's own numbered paragraphs quoted verbatim (opinion is ¶1–21 complete); baselined with Wodrich/Carlsen/Simpson inaudit_corpus._KNOWN_QUOTED_DOC_MARKERS. - 106 NO_MARKER_TEXT: 5 fixed by PDF-opener matching
(
para-marker-restore-2026-06-09); the rest defeat exact matching via OCR drift/footnote structure — needs fuzzy alignment; ALSO includes an almost-clean-marker variant ([.¶ 23]) the classifier misjudged as clean — extend the variant fixer. - NONMONO-only flags include the legitimate embedded-quoted-order class (20124/20408 — the court's own PDFs print restarting sequences).
- TEXT_MISSING — WORKED 2026-06-10 (
- 6. Body-duplication detector — BUILT + RUN corpus-wide 2026-06-10
(
triage/shingle_selfsim_2026-06-10.py: fraction of word-8-shingles occurring 2+, flag ≥0.30). Pre-1997 is clean. 2 flags: Statoil 16959 legit (consolidated party/lease lists repeat); Ramstad 12824 was a real markerless stored-twice with a dotted[¶ N.]marker style invisible to every marker regex — deduped + style normalized; 6 more dotted carriers (1999 ND 24–35 ingest run) normalized, 12825 ¶10–11 spliced (dedup-ramstad-12824/marker-style-dotted-2026-06-10). Re-run the detector after any future merge concatenation. - 7. Truncation heuristics — FOLDED 2026-07-13 (
audit_corpus sig_truncation): the sigscan trailing-[¶N]-drop detector now runs in the dashboard (skips when ~refs absent); truncscan noted as superseded. Current 38 flags (31 CONTENT / 4 SIGNATURE? / 3 SIGNATURE) need a PDF-verified triage pass — the 2026-06-21 audit's 24 known non-defects have drifted (sources + corpus both changed); see TODO-reversion-recovery.md. - 8. Char-histogram regression scan, all 5 DBs — non-ASCII inventory per corpus; alarm on new entrants (statutes/rules/admin/const never had one).
- 9. Two-column interleave detector — Neset-style mis-extraction (another opinion's caption/docket mid-body; coherence breaks). Owed from §3 re-audit.
Metadata cross-field consistency:
- 10. Reporter-volume↔date window invariant — derive each N.W./N.W.2d/N.W.3d
volume's window from corpus medians; flag >60d outliers. Plus: neutral-cite year ==
year(date_filed); docket YYYYNNNN prefix vs filing year; synthetic ordering
monotonic vs (date, page). PARTIAL (audited 2026-06-22):
volume_date_checkexists AND the neutral-cite-year==date-year extension is already built (neutral_year()); only the docket-YYYYNNNN-prefix and synthetic-ordering-monotonic checks remain. - 11. Roster fuzzy-match — BUILT 2026-06-22.
audit_corpus.check_roster_fuzzy(registered checkroster_fuzzy): author/judges tokens within edit distance 1–2 of aKNOWN_LAST_NAMESsurname but not equal = probable OCR garble. Hunts near-misses, not tenure (names far from every roster surname are treated as legitimate surrogate/district judges, not flagged); dist-2 gated to len≥6; tie-break deterministic (alphabetical). First run: 975 occurrences / 139 distinct garbles (694 at d1 — almost all genuine, e.g. Burice→Bruce ×117, Bueke→Burke, Sature→Sathre, concentrated in the OCR'd judges field of 1890s–1930s opinions; 281 at d2 needing review — some are real surrogates (Anderson, Geiger) or field junk (Hearing, Having)). Detail TSVtriage/audit-roster-fuzzy-<date>.tsv. Follow-on cleanup DONE 2026-06-22 (scripts/fix_roster_garbles_2026-06-22.py, batchfix-roster-garbles-2026-06-22): normalized 622 garbles across 613 fields to canonical surnames, era-verified — a garble is fixed only when exactly ONE roster surname at min edit distance is tenure-consistent with the opinion's year (this auto-excludes the false positives: real surrogates near a roster name in the wrong era like Zane AndersonPederson, BensonBronson, BuhrBahr, GeigerTeigen, plus junk words in old junk-panels) AND it isn't a dup of a name already in the panel. Excluded for manual review: the equidistant short garbles Bure/Burk (d1 from both Burke AND Burr), plausible real alt-spellings (Pedersen, Spaulding, Gross), and d2 English/Latin words. Detector 975→353 (the 353 residue are exactly those exclusions). invariants 24/2/0. - 12. case_name lint — BUILT 2026-07-13 (
audit_corpus case_name_lint, with in-rem/district-number/ordinal digit exemptions). 460 flags: 342 double-space, 59 digits (leaked filing dates / term annotations), 38 ALL-CAPS, Westlawa1footnote-marker contamination ("Governora1") — a caption-cleanup batch queue, per the Westlaw-caption policy. - 13. Frontmatter-vs-citations sync — measure the known cosmetic lag.
Primary-law structural (ex-PL-VALIDATE):
- 14. provision_versions interval integrity (
version_intervals) — start<end; no overlaps per provision; ≤1 open version;current_version_idpoints at it. Built 2026-06-09. Queue WORKED 2026-06-10, 11→0 (rules-version-surgery-2026-06-10): zero-widths were foreign pages filed as rule versions (appendix-k/f/8.9-appendix/Table A/Rule 8.3.1 + Admin Rule 21 dups inside Admin Order 21), same-date republication twins, and renumber/repeal pages carrying the successor's date (4.1 was serving repealed text as current). 5 provisions created, 7 relocated, 9 deleted; verified vs live ndcourts.gov H1 titles + effective dates. - [~] 14b. Rules-ingest hardening (from the surgery's root causes) —
FOOTGUN CLOSED 2026-07-13 (
ingest_rulesguard + incremental mode; see TOOLS.md): a full build now REFUSES to overwrite a populated rules.db (listing the DB-only changelog batches it would wipe;--forcebacks the file up first), and routine currency runs through--update, which applies only mirror commits since the recordedmirror_headmarker and HOLDS any provision whose DB standing text diverges from the mirror's marker-time state — so the 2026-06-10 surgery and the manually-added appendices can no longer be silently clobbered. (a) H1-title-vs-provision guard — DONE in--update(foreign-page hold; full-build path still lacks it); (c) same-date republication twins —--updateholds them (dedup rule for the full-build path still open); (d) repealed rules —--updatesets provision.status='repealed' on a repeal-notice version (full-build already did). Still open: the mirror repo + scraper lack the c/d/e/l/m appendices (when the scraper eventually captures them,--updatewill hold them as "provision exists in DB but file is new to the mirror" — reconcile then), and the full-build path's (a)/(c) guards. Weekly automation: rules stage in~/code/scraper/tools/weekly_scrape.sh(scraper--update --all→ingest_rules --update --apply→ invariants; holds notify via macOS banner). - 15. Enumeration continuity — subsection 1,2,3…/subdivision a,b,c… gaps (dropped enumerated blocks read grammatically); syllabus-point numbering in pre-1953 opinions.
- 16. Doubled-token + seam detectors as standing scans (PL-1/PL-2 triage tools re-run after any re-ingest; baseline 41 source/FP).
- 17. Heading/body split sanity — headings ending mid-abbreviation, bodies starting lowercase.
- 18. Section-number sequence within chapters — gaps must correspond to repealed/reserved dispositions.
Process integrity:
- 19. Changelog reconciliation (
changelog_doc_sync) — every DB changelog batch has a CHANGELOG-data*.md mention (caughtaltenbrun-date-fixmissing by hand 2026-06-09); referenced snapshot files exist. Built 2026-06-09. - 20. FTS sync check, all 5 DBs — rowcount + random-sample equality between FTS index and base tables.
- 21. Section-inventory cross-check vs ndlegis.gov indexes / PDF bookmarks — the only check that catches whole missing sections.
- 22. Second-engine extraction diff (PyMuPDF vs pdfplumber per section) — certifies faithfulness to the PDF, not within-engine consistency.
- 23. Upstream-correction watch — re-hash sampled live ndcourts.gov PDFs vs stored; watch the index for corrected/substitute labels. Nothing currently detects an upstream correction post-ingest.
- 24. CL metadata re-diff by cluster_id (our drift + CL's corrections).
- 25. Per-year count time series vs the court's published annual statistics.
- 26. Statute currency heartbeat — flag when a new session's effective dates arrive with no re-ingest.
- Parallel-pair scan promoted to permanent check — DONE 2026-06-09
(
audit_corpus.check_parallel_pair; court typos baselined in_KNOWN_COURT_PAIRS). Reads 608 = exactly the enumerated held queues (singles/conflicts/window + unverified sibling clusters + OTHER); shrink it by working those, alarm on growth. - ~25 mixed/single-witness parallel clusters (
triage/class3-clusters-2026-06-09.tsv) — per-item verification vs CL/Westlaw before any move/add. Includes the622 N.W.2d 432three-claimant tangle (Lee 2000 ND 215 header / Matrix 2000 ND 213 witnesses ×6 / Syvertson 2000 ND 211 current holder) and Chinquist/Jaste sharing679 N.W.2d 257(Boedecker/McMahon-style shared page vs contamination). Mirror-image clusters where the holder dominates (Laib 6/1, Ceynar 5/1, Noack 43/1) are single court typos — no DB change. -
DB_NO_PARALLELbackfill — WORKED 2026-06-09 (parallel-backfill-witnessed-2026-06-09): 387 parallels added (246 window-gated + 141 interpolation-gated for absent volumes; garbage003 N.W.3d 122correctly rejected). Pair consistency 2,455→610 mismatches (98.9%). Residue queued intriage/backfill-parallel-candidates-2026-06-09.tsv: 125 single-witness (need CL/Westlaw — a lone witness may itself carry a digit flip), 24 WINDOW:OUT, 20 CONFLICT. +8 more AUTO adds 2026-06-10 (parallel-backfill-witnessed-2026-06-10, 2018–2025 CL-lag class, 2–11 witnesses each); parallel_pair 597→556. Remaining 556 = single-witness holds, the shared-table-page family (multiple summary affirmances on one N.W.2d page), and the ~25 clusters above. -
OTHERmismatch rows — now 271 (audited 2026-06-22; drifted up from 242) (triage/parallel-pair-classified-2026-06-09.tsv) — sample & classify (regex artifacts vs deeper tangles vs >1-digit corruption). - Cite-shaped
docket_numbervalues — DONE 2026-06-10 (docket-cite-shaped-2026-06-10): all 198 fixed + 2 side-catches (17035 had its neighbor's docket; 17046No.prefix) = 200 corrections, 0 remain. Sources: cTrack report join (36), 1997 frontmatter/captionCivil 960144→19960144(158+), caption file numbers for the stragglers. - Stored-twice opinions — WORKED 2026-06-09 (
dedup-storedtwice-2026-06-09): the 31 marker-flagged seeds led to a corpus sweep finding 563; 549 fixed (4-gate surgery incl. jaccard≥0.5 so appended rehearings can't be deleted). para_continuity 1,818→1,269. All 41 holds RESOLVED same day (see CHANGELOG held-queue closeout): 27 flip-explained drops; archive-arbitered marker fixes/splices (incl. Clayburgh's missing ¶37 signature, Lawrence archive rebuild); Fisk page-rotation re-extract; 2 false positives (court PDFs print the restart — embedded quoted orders). Self-audit caught 8 dropped## On Rehearingblocks → restored (rehearing-restitch-2026-06-09); the rehearing-below-max gate hole is why future dedups must scan dropped text for rehearing/amendment keywords. Final: para_continuity 1,240, xref 811, parallel_pair 595, invariants 23/2/0. Check #6 still owed for pre-1997 — the sweep keys on [¶N]/## Opinion; markerless old texts need shingle self-similarity. (Also note 20124/20408: embedded quoted orders with own ¶ numbering are a legitimate NONMONO class.) - Digit-flip exposure beyond citations — CLOSED corpus-wide 2026-06-10
(
digit-flips-corpus-2026-06-10; method proven on the 226-cohort first,digit-flips-pdfverified-2026-06-09). Full analyzer era: 6,965 opinions / 129,160 ¶s digit-compared vs PDFs → 621 candidates → all print-verified (21-agent crop fan-out + 10% personal spot-check 57/57 + personal read of all 38 unclears). 618 fixes in 499 opinions; 3 text-layer ToUnicode rejects (never apply from the text layer alone); 4 more court typos baselined. Residue:triage/digit-flip-followup-2026-06-10.tsv(5 adjacent multi-digit divergences the single-flip pairing can't propose — extend pass-2 to 2-digit/length-change diffs for a final small sweep); 130 opinions have no PDF (pre-scraper 1997–98 era — Westlaw cross-check is their only witness); 17 newly-visible cited-side parallel issues joined the parallel_pair queue.
Done 2026-06-09: 1, 3, 4, 5, 14, 19 (the top-tier set). Done 2026-06-10: 6 (shingle detector, corpus run clean). Next by yield-per-effort: 2 (antecedent witness — pairs with the §14(d) re-measure), 10 (volume-window invariant), 7 (truncation), 8 (char histogram, primary-law DBs), 20 (FTS sync). Tier 2 as sessions allow; 23 belongs in the weekly pipeline.
35 verified typos in the COURT'S print (text verbatim; cited_by overridden to the intended case on every rebuild; audit baselines must stay in sync).
- West-corrected-page follow-up sweep: for each registered anomaly,
check the West advance sheet / N.W.2d–3d bound volume (and any court-submitted
corrected page) — if West prints the corrected cite, the court may have
approved a correction our slip-PDF text misses. Work from
SELECT * FROM print_anomalies(each row carries the followup note). - Obtain correct official PDFs (misfiled primary source) — Priority A DONE 2026-06-22.
Log at
triage/missing-primary-source.md. Both filed (C-Track originals kept over BitMiracle re-renders): 20110136 / 2015 ND 15 (Nemec) and 20220121 / 2023 ND 32 (Queen; the 2022 ND 178 ⇄ 2023 ND 32 pair was transposed in the tree — both paths corrected). DB bodies were already correct; nochangelog. - Priority B/C source-tree backfill (optional): 7 1997 opinions with no PDF
(archive
.htmpresent — primary source already covered): 19960070–79 (1997 ND 1), 19960226 (1997 ND 40), 19960215 (1997 ND 59), 19960235 (1997 ND 50), 19960329 (1997 ND 72), 19960243 (1997 ND 102), 19970039 (1997 ND 156). C = 1998 ND 171 (19980094) PDF text-layer scrambled (optional re-OCR). Source-tree only — DB bodies correct.
cite_temporal / pinpoint_range / version_intervals / changelog_doc_sync 0;
parallel_pair 556; para_continuity 520; xref_resolve 759;
invariants 24/2/0; corpus 19,793. Digit-flip class CLOSED (1,025
print-verified corrections); TEXT_MISSING worked; stored-twice worked incl.
markerless; dockets clean. Synopsis-leakage CLOSED (720/720). Paragraph
markers normalized corpus-wide ([¶ N]→[¶N], 85,109 markers; 52 OCR-garbled
markers fixed, 26 deferred — triage/GARBLED-MARKERS-REMAINING.md). Proofing
fleet at offset 2230 (2019→2017 done; ~14 trios to the 1997 target).
(Prior snapshot, v0.12.1 2026-06-10: parallel_pair 556 / para_continuity 520 /
xref_resolve 759 / invariants 23/2/0.)
| Check | Flagged | Read |
|---|---|---|
| cite_temporal | 0 | graph temporally clean after today's rebuild |
| pinpoint_range | 0 (was 59) | WORKED 2026-06-09: root cause = OCR digit flips (3↔8, 5↔6) in analyzer-era texts — invisible to artifact-char cleanup. 310 cite/pincite corrections (PDF-verified) + 1 re-extract (2011 ND 242 markers) + 14 witness-attested parallel additions + 3 court typos preserved (one baselined in _KNOWN_COURT_PINCITES). See CHANGELOG citeflip-surgical-*-2026-06-09. |
| para_continuity | 1,876 (1,297 GAP) | spot-check Ertelt 1997 ND 15: ¶2 text present but marker OCR'd [IF 2] → pinpoint-navigation defect, not lost text; only ~25 carry the literal [IF/lf N] signature — classify the rest by source/gap-shape before bulk action |
| xref_resolve | 848 modern-citer unresolved (2,328 total distinct) | three classes: (a) repealed/recodified law (ch. 27-20 juvenile) — statutes.db is a current snapshot, suggests a repealed-section disposition layer; (b) genuine rules.db gap: N.D. Stds. Imposing Lawyer Sanctions (88 modern citers); (c) cross-tag oddities (N.D.C.C. ch. 75-02 = NDAC-shaped number) |
| version_intervals | 11 | zero-length rule versions (start==end) in rules.db — ingest artifact |
| changelog_doc_sync | 0 (was 110) | the 7 June batches got narratives same day (incl. the nd-consolidate-nestos→-state doc/DB name fix); the 103 pre-policy/narrated-elsewhere batches are grandfathered in _KNOWN_UNNARRATED(_RE) — shrink that list as narratives are backfilled, never grow it unchecked |