Skip to content

feat(casework): court-record binder for accused binds and case dates - #442

Open
gaurav-karki wants to merge 27 commits into
mainfrom
feat/casework-court-record-binder
Open

feat(casework): court-record binder for accused binds and case dates#442
gaurav-karki wants to merge 27 commits into
mainfrom
feat/casework-court-record-binder

Conversation

@gaurav-karki

@gaurav-karki gaurav-karki commented Aug 8, 2026

Copy link
Copy Markdown
Member

User description

Adds casework/enrich_court_record.py: a standalone control-plane enricher that reads a
case's court record, fills case_start_date / case_end_date, and binds the court's
defendants to the case as accused. No Django model imports — HTTP only, like every other
casework/ stage.

Why this is possible at all

Measured against production, anonymous GET only, across all 307 cases in the FY078/079
census.

Coverage
Court case readable 307 / 307
registration_date_ad (the start date) 307 / 307
An end date 306 / 307
— from both a deciding hearing row AND the case_status string 277, agreeing 277 / 277
≥1 named defendant 307 / 307 (1,343 rows, avg 4.4, max 19)
Jawafdehi case_start_date already set 1 / 307

The rule reproduces what caseworkers already do by hand: across the 62 published cases,
case_start_date equals the court registration date on 46 of 48, and case_end_date
equals the deciding hearing date on 29 of 29.

How it writes

PATCH /cases/<slug> with an If-Match ETag. entities is a whole-list-replace path, so
the binder reads the current list, merges on (nes_id, relationship_type), and sends the
whole list back — an existing bind is never dropped and never duplicated. A 412 is logged
and skipped, never retried or forced. Writes are gated to DRAFT (REQUIRED_WRITE_STATE);
--allow-remote-writes is required for any non-loopback PATCH.

The two wrong-person paths this closes

Binding the wrong person as accused on a corruption case is defamation of a named
individual, so both were measured rather than assumed.

1. Writ and appeal cases name government offices in the defendant column. Every one of
the 14 OA references is filed against नेपाल सरकार; the W* references name things like
मुख्यमन्त्री तथा मन्त्रिपरिषदको कार्यालय प्रदेश नं.१ विराटनगर. Without a filter the binder
creates a person entity for a ministry and marks it accused.

case_number_code classifies the court number and only prosecutions bind — CR (2,804
refs), CB (9), FJ (1), and the pre-FY073 no-code format like 93-068-0194 (139 refs on
135 cases, which a naive "must contain -CR-" rule would have silently dropped). Anything
unparseable skips rather than binds. The filter applies to binding only: the 16 cases whose
only reference is a writ still get both dates.

2. Name-only identity, with nothing to disambiguate it.

Defendant rows carrying an address 0 of 1,414
Defendant rows carrying an nes_id 0 of 1,414
Names resolving to one existing NES person 1 of 221
Names not in NES at all → create 146 of 221 (66%)
Names refused as ambiguous or truncated-window 74 of 221 (34%)
Names appearing on 2+ cases 80, covering 210 rows

run_entities is keyed on (name, address) and no row has an address, so every defendant
takes the address-less path. Refusing to reuse an address-less hit does not fix it: once
case A creates entity X for name N, case B's exact_person_match finds exactly one N and
binds X through the ordinary rung, no cache involved.

So a name appearing on more than one case in the run is held — reported with the other
cases it appears on, never auto-bound — and written to a <review>.held.json file for a
human to rule on. Names on exactly one case bind normally.

A 25-case production dry run confirmed the resolution rate holds at scale: 0 of 138
defendants matched an existing NES person.
Entity creation is not an edge case here, it
is the normal path, which is what makes the hold load-bearing.

Known limitations — please read before running --apply

The hold is scoped to one run. It only sees names repeated among the cases selected
in that run, so --limit, --slug, --court-case, --batch-csv and --fiscal-year
all narrow it. Running in small batches switches the protection off: two cases naming
the same person in different batches will both bind. The held_index event logs the scope
and warns at WARNING level whenever the selection is narrowed, but the warning cannot
prevent it. A follow-up adding a --held-names <file> flag — precompute the collision list
once over the whole corpus, load it in every batch — would make batching safe.

The index cannot see PUBLISHED cases. It is built from select_for_run, which filters
to ENRICHABLE_STATES = ("DRAFT", "IN_REVIEW"). Published cases carry confirmed accused
binds and are invisible to the collision check. This is deliberate — published cases are
not to be touched — and the held_index line states it explicitly.

A pass-1 read failure shrinks the index. A case whose court record fails to read
contributes no defendants, so another case sharing a name with it binds instead of holding.
Made visible (the warning fires on readable != selected), not repaired.

Full-corpus runs sit close to the rate limit. Measured 8.8 requests per case, ~4,470
requests/hour against a 5,000/hour ceiling. A ~2,900-case sweep is ~26,000 requests over
~6 hours at 89% of budget, and a throttled NES search degrades silently into "could not
resolve". Pace it or raise the limit before attempting one.

Testing

1,426 tests in tests/casework/, ruff check . and ty clean.

Every task went through an independent review and fix rounds, plus a whole-branch review.
The reviews found — and this branch fixes — a held file that crashed the run after the
PATCHes landed (destroying the review file), held defendants counted as "resolved" and
recorded in the ledger as a completed stage, review rows that read would-patch even under
Mode: APPLIED, and a merge against the API's read shape that would have duplicated binds
and 400'd on every case already carrying one.

A local end-to-end smoke test against a throwaway sqlite server covers all four fixture
shapes (shared defendant, writ case, pre-FY073 number, ordinary case) plus dry-run →
apply → idempotent-rerun. Verdict PASS.

No --apply has ever been run against production.

🤖 Generated with Claude Code


PR Type

Enhancement, Tests


Description

  • Add court_record enricher

  • Fill case dates from court records

  • Bind accused via cautious person resolution

  • Extend API, pipeline, ledger tests


Diagram Walkthrough

flowchart LR
  A["NGM court record"] -- "read detail/hearings/parties" --> B["court_record enricher"]
  B -- "derive dates" --> C["case_start_date/case_end_date"]
  B -- "resolve defendants" --> D["accused entity binds"]
  C -- "conditional PATCH" --> E["Case API"]
  D -- "whole-list merge" --> E
Loading

File Walkthrough

Relevant files
Enhancement
4 files
enrich_court_record.py
Add court-record enrichment CLI                                                   
+1112/-0
court_record.py
Read full court record data                                                           
+124/-21
api.py
Add courtcase reads and patch helper                                         
+74/-3   
ledger.py
Treat court dry-runs as non-outcomes                                         
+14/-2   
Configuration changes
3 files
pipeline.py
Register court_record pipeline stage                                         
+11/-0   
llm.py
Register non-LLM stage tier                                                           
+4/-0     
0056_correct_case_date_help_text.py
Migrate corrected date help text                                                 
+23/-0   
Documentation
2 files
README.md
Document court-record enricher usage                                         
+1/-0     
models.py
Update case date help text                                                             
+7/-2     
Tests
5 files
test_enrich_court_record.py
Cover court-record enrichment planning                                     
+2070/-0
test_court_record.py
Cover court record reads and codes                                             
+121/-1 
test_api.py
Cover courtcase API helpers                                                           
+66/-0   
test_ledger.py
Cover court dry-run ledger behavior                                           
+44/-3   
test_build_api_guard_wiring.py
Pin guard wiring for all enrichers                                             
+15/-5   


🛠️ Relevant configurations:


These are the relevant configurations for this tool:

[config]

custom_model_max_tokens: 200000
git_provider: github
output_relevant_configurations: True
model: openai/cx/gpt-5.5
ENABLE_AUTO_APPROVAL: True
custom_reasoning_model: False
fallback_models: ['openai/cx/gpt-5.4-mini']
is_auto_command: True
publish_output: True
publish_output_progress: True
progress_gif_url: 
progress_gif_width: 48
verbosity_level: 0
use_extra_bad_extensions: False
log_level: DEBUG
use_wiki_settings_file: True
use_repo_settings_file: True
use_global_settings_file: True
extra_config_url: 
disable_auto_feedback: False
ai_timeout: 120
response_language: en-US
repo_context_files: ['AGENTS.md']
repo_context_from_default_branch: True
repo_context_max_lines: 500
max_description_tokens: 500
max_commits_tokens: 500
max_model_tokens: 32000
model_token_count_estimate_factor: 0.3
patch_extension_skip_types: ['.md', '.txt']
allow_dynamic_context: True
max_extra_lines_before_dynamic_context: 10
patch_extra_lines_before: 5
patch_extra_lines_after: 1
cli_mode: False
large_patch_policy: clip
duplicate_prompt_examples: False
seed: -1
temperature: 0.2
ignore_pr_title: ['^\\[Auto\\]', '^Auto', '^Bump ', '^chore\\(deps\\)']
ignore_pr_target_branches: []
ignore_pr_source_branches: []
ignore_pr_labels: []
ignore_pr_authors: []
ignore_repositories: []
ignore_language_framework: []
restricted_mode: False
enable_ai_metadata: False
reasoning_effort: medium
enable_claude_extended_thinking: False
extended_thinking_budget_tokens: 2048
extended_thinking_max_output_tokens: 4096
claude_extended_thinking_models_override: []
extract_issue_from_branch: True
branch_issue_regex: 
enable_custom_labels: False

[pr_description]

publish_labels: False
add_original_user_description: True
generate_ai_title: False
use_bullet_points: True
extra_instructions: 
enable_pr_type: True
final_update_message: True
enable_help_text: False
enable_help_comment: False
enable_pr_diagram: True
publish_description_as_comment: False
publish_description_as_comment_persistent: True
enable_semantic_files_types: True
collapsible_file_list: adaptive
collapsible_file_list_threshold: 6
inline_file_summary: False
use_description_markers: False
enable_large_pr_handling: True
include_generated_by_header: True
max_ai_calls: 4
async_ai_calls: True

Summary by CodeRabbit

  • New Features
    • Added court-record processing for case dates, hearings, parties, and accused entities.
    • Added support for retrieving court-case details and paginated hearings.
    • Added dry-run workflows, detailed review output, and held-name reporting.
    • Added case-number classification and defendant name normalization.
  • Improvements
    • Updates now combine field and list changes into a single conditional request.
    • Ambiguous or incomplete entity matches are left unbound for review.
    • Individual court-record retrieval failures no longer block other records.
  • Bug Fixes
    • Dry-run events are excluded from outcome reporting while applied changes remain recorded.

gaurav-karki and others added 24 commits August 7, 2026 10:59
… one PATCH

Adds get_courtcase and list_hearings (read the NGM composite-key detail
route and its hearings sub-resource, paging by page number the same way
get_court_case_entities does) plus patch_case, which sends scalar fields
and a whole-list path (e.g. entities) in ONE conditional PATCH -- the
fix for the two-request/one-ETag failure build_replace_ops documents
from enrich_card's 2026-08-04 smoke run.
…luck

The existing test's non-deciding row was filtered out before max() ever ran,
so the assertion passed identically under max(), decided[0], or decided[-1].
Add a case with two फैसला-passing rows at different dates, asserted in both
list orders, so only an actual max-by-date comparison can satisfy it.
…ate them

Adds the three-rung ladder for Task 4: a row's own nes_id is a pure copy,
then exact name equality within the person prefix (never the scored
resolver, which would pick a namesake by rank), then create-and-bind with
run-scoped de-duplication. Fixes two defects found in the plan's own test
snippet: the create payload was missing the server-required "slug" field,
and one collision-path assertion hardcoded an IRI that doesn't match
entity_slug's real (schwa-preserving) transliteration of the same name.
…esolve_defendant from ever raising

Review round 1 findings:
- CRITICAL: exact_person_match ignored CandidateList.complete, so a single
  hit inside a search window that stopped on relevance (not exhaustion)
  read as a confirmed unique match. That is how a namesake gets bound as
  accused on a corruption case -- the exact failure this ladder exists to
  prevent. Now a lone hit from an incomplete window falls through to
  creation instead of binding.
- IMPORTANT: resolve_defendant called exact_person_match unguarded, so a
  transient search-API error would propagate and kill the whole run. Wrapped
  it the same way the create POST already was.
- IMPORTANT: _is_person's nested-category behavior (person/politician IS a
  person, personnel/organization are NOT) had no direct coverage.

Also corrects _is_person's docstring (it does not use startswith) and drops
a file-path citation from a comment.
…o entities key, and require every reference to plainly acquit

- plan_case now merges via current_entity_binds (PATCH shape), not the raw
  read list, which keyed every existing bind as (nes_id, "") and appended
  duplicate accused binds while re-sending outcome.
- Refuse a case payload missing the "entities" key outright, matching
  enrich_related_entities.plan_case_entities's own guard against the same
  destructive-replace hazard.
- bind_outcome now requires every court reference to have decided, using the
  same two-source truth end_date already relies on, and refuses to call a
  hearing cell a plain acquittal when it also carries आंशिक or ठहर.
…sspelled qualifiers before testing them

- Add the missing positive-branch test: every reference deciding a plain
  सफाई, one via its own hearing row and the other via the case_status
  fallback, asserts ACQUITTED. Nothing before this proved the rewrite's
  all(decided) and all(acquitted) branch survives; every prior
  multi-reference test only asserted CHARGED.
- Normalise the hearing decision_type cell through
  courts.case_status._order_key before testing for आंशिक/ठहर qualifiers, so
  a documented misspelling (आंशीक) can't slip a partial conviction past the
  guard and read as a plain acquittal. Reuses the same normalisation
  outcome_from_hearings already applies to this identical field, rather than
  hand-copying the spelling table.
… and log why every skip happened

- select-skip events now carry `plan.skips` as `detail`, so an operator
  replaying *.events.jsonl can tell "no court reference at all" from "every
  reference on it 404'd" instead of seeing an identical line for both.
- _log_plan routes per-reference read failures to court_read/unreadable
  instead of dates/no_source -- a broken read is not a date fact.
- the 412-vs-rejected split now checks isinstance(exc, HTTPError) and
  exc.code == 412 instead of a substring test on the exception's own
  message, which a slug like case-...-cr-0412 could spuriously match on the
  unrelated no-ETag ValueError.
- casework.ledger.NON_OUTCOME_STATUSES gains "dry_run" so a clean --dry-run
  patch event stops folding into the "what did we change, when" audit, the
  same way bind_materials' "planned" already does.

Covered by new tests for each: a skip-state detail check, a two-court-
reference partial-read case, an --apply 412 conflict plus its companion
success path, a slug-containing-412 regression proof, a dry-run test that
now falls through to the entity-creation rung instead of short-circuiting on
an existing nes_id, and a ledger test for the new non-outcome status.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…help text

STAGES/TIERS both gain court_record so test_stage_names_match_llm_tier_names
stays pinned. case_start_date/case_end_date help text now says what the
field actually holds (court registration date on 46 of 48 published cases
with a readable court record; deciding hearing date on 29 of 29), not the
"alleged incident" convention nobody uses.
…ing the ledger

The court-record binder had two paths that bound a person the ladder had just
declined to identify. On a 409 the create kept the IRI it computed BEFORE the
POST and bound it -- but that branch fires precisely when rung 2 refused ("13
person entities carry this exact name", or the truncation veto), so the bind
went to whoever already owned the slug. And `run_entities`, shared across cases
by design, was keyed on the bare name, so two different people sharing a name on
two cases collapsed into one entity carrying both accusations.

Both now fail safe. The 409 path binds nothing and reports the taken IRI for a
human; the run key is name AND the court party row's address. This trades bind
coverage for safety and is an interim ruling -- see the fix report for the
alternatives (re-search and compare, or an explicit review queue).

Three more corrections:

- An unreadable prefix list (a transient 502 at run start) reported every
  defendant as needing a prefix that is not creatable -- false for a prefix as
  ordinary as `person`. It now says the list was never read, retry the case,
  matching `enrich_related_entities._cannot_create`.
- Every intermediate event now reports `ok` with its classification in the
  detail, the way every sibling enricher does. Adding `dry_run` to
  NON_OUTCOME_STATUSES achieved nothing while `bind_plan`/`merged` still landed
  in the "what did we change, when" audit for every dry-run case. A
  `nothing-to-do` case gains a terminal `idempotency`/`already` event so it does
  not vanish from the ledger instead.
- `court_record.py` no longer claims to be unwired, `defendant_names` and
  `_accused_binds` share one `is_defendant`/`party_name` pair rather than two
  copies of the filter, and the unread `CHARGED` is gone.

The guard-wiring pin covers `enrich_court_record` now, plus `enrich_card` and
`enrich_description` which had drifted out of it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ions

OA, RE and the W* writ codes name a government office in the court
record's defendant column, not a person. case_number_code() reads the
case-type letters out of the court case number, and _accused_binds now
skips a whole record whose code isn't on the CR/CB/FJ/"" allow-list
before it ever reads that record's parties -- start_date/end_date are
untouched, since the filter is on binding only.
… review debt

_log_plan had no branch for _accused_binds' new record-level skip, so it
fell into the dates/no_source bucket and the bind_plan summary claimed
0 defendants where the record had named one and been declined. Add
_NON_PROSECUTION_SKIP_PREFIX alongside _COURT_READ_FAILURE_PREFIX and
route on it; the bind_plan line now counts resolved defendants and
says how many references were skipped as non-prosecution.

Also: drop the self-referential BINDABLE_CODES test, collapse the
duplicate _court_record test builder into _record, and cut the
same rationale from being written three times across two docstrings
and a comment block.
Two same-name defendants on separate court cases collapsed into one NES
entity through the exact-match rung, putting one person's accusation on
another's record. defendant_name_index/held_names build a cross-case name
index from every selected case's court record; a name in the held set gets
a report row and no bind item, while the case's other defendants and dates
are unaffected. Also adds the automated _log_plan routing test the non-
prosecution skip was still missing.
A held row inflated len(plan.rows) into three consumers that all meant
"defendants this run resolved" -- the bind_plan summary, the review file's
accused+N, and the nothing-to-do idempotency detail all overcounted by the
held row and never mentioned the hold. A nothing-to-do case whose only
outstanding item is a held name also logged ledger status "already", which
casework.ledger.build_ledger records as a completed outcome; it now logs
the distinct held_for_review instead. Also makes held a required keyword
(no more held=None silently reopening the same-name collapse), fixes the
held-check to membership (is not None) rather than truthiness, keys the
per-case defendant dedup on the normalised name, and trims a docstring
paragraph and a dead fallback string flagged in review.
…w status

main() split into two passes: pass 1 reads every selected case's court record
and builds the cross-case held-name index once, pass 2 re-reads each case for
a fresh ETag and plans it against that same index -- closing the gap where
held={} left the Task 2 hold inert in a real run. Ships a held-names JSON
file beside the review file for a human to rule on. Also fixes review rows
always reading would-patch regardless of the case's actual outcome, and
renames a shadowed `detail` variable flagged in the Task 2 review
… index

write_held_file crashed after the PATCH landed whenever the review directory
didn't exist yet, since main() called it before review.write() -- the only
thing that ever created it -- losing the review file, the held file, and the
run footer. Also logs a run-level held_index event (cases selected vs
readable vs names indexed vs held) so a pass-1 read failure or a narrowed
selection (--limit/--slug/--court-case/--batch-csv) is visible instead of
silently weakening the hold, pins get_case_with_etag/get_courtcase call
counts, and rewires the three held-behavior tests onto the real two-pass
index instead of a monkeypatched one
…tion

The held_index WARNING only fired for --limit/--slug/--court-case/
--batch-csv, never when a pass-1 read failure shrank readable_cases below
the selection -- the exact scenario a transient 5xx on one case creates,
leaving no signal beyond error=1 in the footer. Now fires on either cause,
names which one applied, and logs at logging.WARNING so it's visible to
level-based filtering, not just to grepping detail text
case_number_code returned "" -- the pre-FY073 prosecution bucket -- for
anything it couldn't parse, not just the genuine legacy all-digit shape.
`W-081-0037`, `RE-081-1730`, `079_CR_0151`, and a Devanagari-transliterated
number all fell into BINDABLE_CODES by accident, inverting the allow-list
rule Task 1 was written around. Only `^[0-9]+-[0-9]+-[0-9]+$` (matching
`93-068-0194`) now maps to "", everything else this can't read returns
UNPARSEABLE, which is not in BINDABLE_CODES so it skips and logs like any
other unrecognised code.
…ld index can't see

narrowed checked --limit/--slug/--court-case/--batch-csv but not
--fiscal-year -- the selector an operator scoping a whole campaign is most
likely to use, so a fiscal-year-scoped run got no WARNING though the same
shrink under --limit did. Added to the predicate and the reason text.

The held_index detail also said nothing about select_cases's own
ENRICHABLE_STATES gate, so a clean, unrestricted, unshrunk run read as "the
index is complete" when a PUBLISHED case -- exactly where a confirmed
accused bind already lives -- is structurally invisible to it regardless of
any CLI flag. States the limit on every run now, and corrects the module
docstring's claim that an unrestricted sweep "is what actually makes the
hold protective"; it does not, on its own, see past the state gate.

Also:
- resolved_count = len(plan.rows) - held_count counted a how="failed" row
  (an unslugabble name, a search error, a slug collision) as resolved, so
  "accused+N" and the bind_plan summary overcounted by one phantom bind per
  failure. Factored into _rung_counts, shared by _log_plan and main.
- Pass 1 keyed court_records on `case.get("slug") or ""` with no guard, so
  two slug-less cases would both land on "" and pass 2 would plan both
  against whichever record set was written last. Refuse to index a
  slug-less case instead.
- _accused_binds read its new entity's citation off records[0] before the
  per-record prosecution filter ran, so a person created for a CR defendant
  could be cited to an OA/RE/writ material that never names them. Now taken
  from the first record that actually passes the filter.
- A dry run's "would create" resolution implied the printed IRI is the one
  --apply would bind, but dry_run never reaches the EntityAlreadyExists
  handler -- on the COMMON path for a common name (this slug already
  belongs to someone the ladder declined to identify), --apply refuses the
  bind instead. The reason now says that plainly.
…swallow it

`nes_id or reason` discarded the reason on every row carrying both, which is
exactly the two whose reason is a warning: a dry run's "would create" (--apply
refuses it when the slug is taken) and "reused from this run". Both read as a
settled bind in the events file a caseworker approves from.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…written

The selection gate (ENRICHABLE_STATES, DRAFT+IN_REVIEW) and the write gate
(REQUIRED_WRITE_STATE, DRAFT alone) are deliberately different widths, and
conflating them would start writing to cases already under human review.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Code review skipped — your organization's overage spend limit has been reached.

Code review is billed via overage credits. To resume reviews, an organization admin can raise the monthly limit at claude.ai/admin-settings/claude-code.

Once credits are available, push a new commit or reopen this pull request to trigger a review.

@coderabbitai

coderabbitai Bot commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

@gaurav-karki, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 30 minutes

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: b8e00645-b5a2-47e8-a5fc-8527f866ecd4

📥 Commits

Reviewing files that changed from the base of the PR and between d3a2477 and 45a24cd.

📒 Files selected for processing (5)
  • casework/common/api.py
  • casework/court_record.py
  • casework/enrich_court_record.py
  • tests/casework/test_api.py
  • tests/casework/test_enrich_court_record.py
📝 Walkthrough

Walkthrough

This change adds a zero-LLM court_record pipeline stage. It retrieves court records, derives case dates and defendant outcomes, resolves person entities, applies guarded case updates, supports dry runs, and records only applied outcomes in the ledger.

Changes

Court record enrichment

Layer / File(s) Summary
Court API access and conditional patching
casework/common/api.py, tests/casework/test_api.py
CaseworkApi retrieves court details and paginated hearings. patch_case validates scalar and list paths, combines operations, supports ETags, and skips empty updates.
Court record parsing and pipeline registration
casework/court_record.py, casework/common/pipeline.py, casework/common/llm.py, casework/README.md, tests/casework/test_court_record.py
The pipeline registers the zero-LLM stage. Court numbers and defendant parties are classified and normalized. Court details, hearings, and parties are aggregated with per-reference error handling.
Entity resolution and guarded enrichment
casework/enrich_court_record.py, casework/ledger.py, tests/casework/test_build_api_guard_wiring.py, tests/casework/test_ledger.py
The enricher plans dates and defendant outcomes, resolves or creates entities, merges binds, applies ETag-protected patches, supports dry runs, and isolates per-case failures. Dry-run events remain excluded from ledger outcomes.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant CLI
  participant court_record_for_case
  participant CaseworkApi
  participant plan_case
  participant resolve_defendant
  CLI->>court_record_for_case: Read court references
  court_record_for_case->>CaseworkApi: Fetch details, hearings, and parties
  CLI->>plan_case: Plan dates and defendant binds
  plan_case->>resolve_defendant: Resolve or create person entity
  plan_case->>CaseworkApi: Apply conditional case PATCH
Loading

Possibly related PRs

Suggested reviewers: jawafdehi-pr-agent

Poem

A rabbit reads records beneath the moon,
Plans each bind with care and tune.
Dates are patched when checks agree,
Dry runs leave the ledger free.
ETag guards each careful hop—
“Nibble, test, and safely stop!”

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 42.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main changes: adding a court-record enricher for accused bindings and case dates.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/casework-court-record-binder

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@jawafdehi-pr-agent

jawafdehi-pr-agent Bot commented Aug 8, 2026

Copy link
Copy Markdown

PR Reviewer Guide 🔍

(Review updated until commit 45a24cd)

Here are some key observations to aid the review process:

⏱️ Estimated effort to review: 5 🔵🔵🔵🔵🔵
🧪 PR contains tests
🔒 No security concerns identified
⚡ No major issues detected

🛠️ Relevant configurations:


These are the relevant configurations for this tool:

[config]

enable_ai_metadata: False
custom_model_max_tokens: 200000
git_provider: github
output_relevant_configurations: True
model: openai/cx/gpt-5.5
ENABLE_AUTO_APPROVAL: True
custom_reasoning_model: False
fallback_models: ['openai/cx/gpt-5.4-mini']
is_auto_command: True
publish_output: True
publish_output_progress: True
progress_gif_url: 
progress_gif_width: 48
verbosity_level: 0
use_extra_bad_extensions: False
log_level: DEBUG
use_wiki_settings_file: True
use_repo_settings_file: True
use_global_settings_file: True
extra_config_url: 
disable_auto_feedback: False
ai_timeout: 120
response_language: en-US
repo_context_files: ['AGENTS.md']
repo_context_from_default_branch: True
repo_context_max_lines: 500
max_description_tokens: 500
max_commits_tokens: 500
max_model_tokens: 32000
model_token_count_estimate_factor: 0.3
patch_extension_skip_types: ['.md', '.txt']
allow_dynamic_context: True
max_extra_lines_before_dynamic_context: 10
patch_extra_lines_before: 5
patch_extra_lines_after: 1
cli_mode: False
large_patch_policy: clip
duplicate_prompt_examples: False
seed: -1
temperature: 0.2
ignore_pr_title: ['^\\[Auto\\]', '^Auto', '^Bump ', '^chore\\(deps\\)']
ignore_pr_target_branches: []
ignore_pr_source_branches: []
ignore_pr_labels: []
ignore_pr_authors: []
ignore_repositories: []
ignore_language_framework: []
restricted_mode: False
reasoning_effort: medium
enable_claude_extended_thinking: False
extended_thinking_budget_tokens: 2048
extended_thinking_max_output_tokens: 4096
claude_extended_thinking_models_override: []
extract_issue_from_branch: True
branch_issue_regex: 
enable_custom_labels: False

[pr_reviewer]

require_ticket_analysis_review: False
require_score_review: False
require_tests_review: True
require_estimate_effort_to_review: True
require_can_be_split_review: False
require_security_review: True
require_estimate_contribution_time_cost: False
require_todo_scan: False
publish_output_no_suggestions: True
persistent_comment: True
extra_instructions: Focus on: logic errors and edge cases; security/authz regressions; missing error handling;
Django/DRF correctness (migrations, N+1 queries, transaction/atomicity, serializer & permission gaps).
Do NOT comment on formatting, import order, or naming — ruff handles those in CI.

num_max_findings: 3
final_update_message: True
enable_review_labels_security: True
enable_review_labels_effort: True
require_all_thresholds_for_incremental_review: False
minimal_commits_for_incremental_review: 0
minimal_minutes_for_incremental_review: 0
enable_intro_text: True
enable_help_text: False

@jawafdehi-pr-agent

Copy link
Copy Markdown

PR Code Suggestions ✨

Explore these optional code suggestions:

CategorySuggestion                                                                                                                                    Impact
General-offsetof
Avoid global ledger hiding

dry_run was added as a global non-outcome ledger status, which can hide real
terminal outcomes from other stages that use the same word. Reuse existing planned
for this dry-run patch preview, then avoid broadening NON_OUTCOME_STATUSES.

casework/enrich_court_record.py [1069-1070]

 log_event(logger, events, run_id=run_id, stage=STAGE, slug=slug,
-          step="patch", status="dry_run", detail=generated)
+          step="patch", status="planned", detail=generated)
Suggestion importance[1-10]: 6

__

Why: Valid concern: global dry_run non-outcome can suppress outcomes from any stage reusing that status. Local change fits, but full fix also needs ledger/tests updates.

Low

🛠️ Relevant configurations:


These are the relevant configurations for this tool:

[config]

enable_ai_metadata: False
custom_model_max_tokens: 200000
git_provider: github
output_relevant_configurations: True
model: openai/cx/gpt-5.5
ENABLE_AUTO_APPROVAL: True
custom_reasoning_model: False
fallback_models: ['openai/cx/gpt-5.4-mini']
is_auto_command: True
publish_output: True
publish_output_progress: True
progress_gif_url: 
progress_gif_width: 48
verbosity_level: 0
use_extra_bad_extensions: False
log_level: DEBUG
use_wiki_settings_file: True
use_repo_settings_file: True
use_global_settings_file: True
extra_config_url: 
disable_auto_feedback: False
ai_timeout: 120
response_language: en-US
repo_context_files: ['AGENTS.md']
repo_context_from_default_branch: True
repo_context_max_lines: 500
max_description_tokens: 500
max_commits_tokens: 500
max_model_tokens: 32000
model_token_count_estimate_factor: 0.3
patch_extension_skip_types: ['.md', '.txt']
allow_dynamic_context: True
max_extra_lines_before_dynamic_context: 10
patch_extra_lines_before: 5
patch_extra_lines_after: 1
cli_mode: False
large_patch_policy: clip
duplicate_prompt_examples: False
seed: -1
temperature: 0.2
ignore_pr_title: ['^\\[Auto\\]', '^Auto', '^Bump ', '^chore\\(deps\\)']
ignore_pr_target_branches: []
ignore_pr_source_branches: []
ignore_pr_labels: []
ignore_pr_authors: []
ignore_repositories: []
ignore_language_framework: []
restricted_mode: False
reasoning_effort: medium
enable_claude_extended_thinking: False
extended_thinking_budget_tokens: 2048
extended_thinking_max_output_tokens: 4096
claude_extended_thinking_models_override: []
extract_issue_from_branch: True
branch_issue_regex: 
enable_custom_labels: False

[pr_code_suggestions]

commitable_code_suggestions: False
dual_publishing_score_threshold: -1
focus_only_on_problems: True
extra_instructions: Prefer a few high-impact, project-specific suggestions over many generic ones.
Skip style/formatting (ruff-enforced) and changes under cases/migrations/.

enable_help_text: False
enable_chat_text: False
persistent_comment: True
max_history_len: 4
publish_output_no_suggestions: True
suggestions_score_threshold: 0
new_score_mechanism: True
new_score_mechanism_th_high: 9
new_score_mechanism_th_medium: 7
auto_extended_mode: True
num_code_suggestions_per_chunk: 3
max_number_of_calls: 3
parallel_calls: True
final_clip_factor: 0.8
decouple_hunks: False
demand_code_suggestions_self_review: False
code_suggestions_self_review_text: **Author self-review**: I have reviewed the PR code suggestions, and addressed the relevant ones.
approve_pr_on_self_review: False
fold_suggestions_on_self_review: True
num_code_suggestions: 4

@jawafdehi-pr-agent

Copy link
Copy Markdown

PR Agent Walkthrough 🤖

Welcome to the PR Agent, an AI-powered tool for automated pull request analysis, feedback, suggestions and more.

Here is a list of tools you can use to interact with the PR Agent:

ToolDescriptionTrigger Interactively 💎

DESCRIBE

Generates PR description - title, type, summary, code walkthrough and labels
  • Run

REVIEW

Adjustable feedback about the PR, possible issues, security concerns, review effort and more
  • Run

IMPROVE

Code suggestions for improving the PR
  • Run

UPDATE CHANGELOG

Automatically updates the changelog
  • Run

HELP DOCS

Answers a question regarding this repository, or a given one, based on given documentation path
  • Run

ADD DOCS

Generates documentation to methods/functions/classes that changed in the PR
  • Run

ASK

Answering free-text questions about the PR

[*]

GENERATE CUSTOM LABELS

Generates custom labels for the PR, based on specific guidelines defined by the user

[*]

(1) Note that each tool can be triggered automatically when a new PR is opened, or called manually by commenting on a PR.

(2) Tools marked with [*] require additional parameters to be passed. For example, to invoke the /ask tool, you need to comment on a PR: /ask "<question content>". See the relevant documentation for each tool for more details.

@jawafdehi-pr-agent

Copy link
Copy Markdown

Auto-approved PR

The help text was genuinely wrong -- `case_start_date` read "when the alleged
incident began" while 46 of 48 published cases use the court registration date,
which is also what the binder writes. But the fix is independent of the binder,
and it dragged the only cases/ change and a migration number into an otherwise
self-contained casework/ branch, where 0056 is a merge hazard for a cosmetic
edit. Goes back as its own PR.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@jawafdehi-pr-agent

Copy link
Copy Markdown

Persistent review updated to latest commit 7df92ab

…ecord-binder

# Conflicts:
#	casework/common/llm.py
#	casework/common/pipeline.py
@jawafdehi-pr-agent

Copy link
Copy Markdown

Persistent review updated to latest commit d3a2477

@gaurav-karki

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Aug 8, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (6)
tests/casework/test_api.py (1)

1069-1077: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add a short-first-page case to this pagination test.

The fixture returns exactly 100 rows on page 1, so the test only proves that a full page is followed. It cannot detect the truncation risk described on casework/common/api.py Line 414-421: a server that caps page_size below 100 makes page 1 short and the loop returns after one request. Add a case where page 1 is short but carries next, so the test pins the intended termination rule.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/casework/test_api.py` around lines 1069 - 1077, Extend
test_list_hearings_follows_pages_by_number with a short-first-page fixture where
page 1 has fewer than 100 results and includes a next-page indicator, while page
2 contains additional results. Assert both pages are followed and all rows are
returned, preserving the existing full-page pagination coverage.
tests/casework/test_court_record.py (1)

183-204: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider pinning a hearings-side and parties-side read failure.

Both failure tests inject the exception into detail, so they only exercise the first of the three reads. A regression that moved list_hearings or get_court_case_entities outside the try block would still pass. One extra case with the exception stored in hearings would close that.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/casework/test_court_record.py` around lines 183 - 204, Extend the court
record failure tests around court_record_for_case to cover read failures from
the hearings and parties/entity lookups, not only detail. Add cases injecting
the HTTPError into the _FullApi hearings and get_court_case_entities data, and
assert the affected reference is skipped without raising while other valid
records remain returned.
casework/enrich_court_record.py (4)

984-1002: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

On --dry-run, pass 2 re-reads every case for an ETag it never uses.

Pass 1 already fetched case_detail at Line 920 and discards it. Pass 2 fetches it again for a fresh ETag. If args.dry_run is set, no PATCH is sent, so the fresh ETag has no purpose. Keeping the pass-1 case_detail and reusing it on dry runs halves the case reads for the most common run mode. A full-corpus dry run currently pays about 3,000 avoidable round trips.

Keep the re-read for --apply, where a stale If-Match would 412.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@casework/enrich_court_record.py` around lines 984 - 1002, Update the
pass-1/pass-2 case processing around readable_cases and plan_case so --dry-run
retains and reuses the pass-1 case_detail instead of calling
api.get_case_with_etag again. Keep the existing pass-2 re-read and fresh ETag
acquisition for apply runs, while ensuring dry-run planning still receives the
retained case_detail and appropriate ETag value.

899-932: 🩺 Stability & Availability | 🔵 Trivial

Consider a request-rate control for the pass-1 sweep.

Pass 1 issues one case read plus up to three court reads per court reference, with no delay and no retry policy. Across the full corpus that is several thousand sequential requests against the NGM read plane. The client has no backoff for a 429 or a 5xx: court_record_for_case turns any failure into a skip, so a transient rate limit silently costs those cases their defendants and their dates for the whole run.

Two options, in order of value:

  1. Treat 429 and 5xx differently from 404 in court_record_for_case, so a throttled read is retried rather than converted into a permanent skip.
  2. Add an optional inter-request delay flag, validated with casework.common.cli.nonneg_float.

Based on learnings: "ensure numeric retry/interval/circuit-breaker flags validate non-negativity at argument parsing time by using casework.common.cli.nonneg_int / casework.common.cli.nonneg_float".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@casework/enrich_court_record.py` around lines 899 - 932, Update
court_record_for_case to distinguish transient 429 and 5xx responses from
permanent 404s, retrying transient failures with bounded backoff before skipping
the case. Preserve existing permanent-failure behavior, and ensure any retry or
interval CLI options use nonneg_int or nonneg_float validation so negative
values are rejected during argument parsing.

Source: Learnings


317-319: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

Keep rung 1 inside the person prefix policy.

Rung 1 currently accepts any canonical entity IRI, including organization/... or personnel/.... Since this module is meant to stay inside person and rung 2 filters with _is_person, add that guard at line 319 before binding the court row's nes_id as accused.

🛡️ Proposed fix
     row_nes_id = (row_nes_id or "").strip()
-    if row_nes_id and is_valid_entity_iri(row_nes_id):
+    if row_nes_id and is_valid_entity_iri(row_nes_id) and _is_person(row_nes_id):
         return Resolution(row_nes_id, "nes_id")
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@casework/enrich_court_record.py` around lines 317 - 319, Restrict rung 1 in
the resolution logic to person entities by adding the existing _is_person guard
alongside is_valid_entity_iri before returning Resolution for row_nes_id. Keep
non-person IRIs from being bound as the accused, while preserving the current
valid-person resolution path.

107-107: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Move _order_key out of the private cross-app import.

_order_key is a private symbol that is only used by casework.enrich_court_record outside courts/, and its use is only justified by the shared spelling table it drives. Export a public helper from courts.case_status and import that instead, so this parser is not coupled to private court scraper internals.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@casework/enrich_court_record.py` at line 107, Replace the private _order_key
import in the casework parser with a public helper exported by
courts.case_status that provides the same shared spelling-table ordering
behavior, and update the courts-side definition/export accordingly. Keep
parse_case_status unchanged and remove the cross-app dependency on _order_key.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@casework/common/api.py`:
- Around line 414-421: The list_hearings paginator in
casework/common/api.py:414-421 should stop based on data.get("next") or an empty
batch, not on batch length, matching get_court_case_entities and iter_cases;
preserve extending all fetched results. Update
tests/casework/test_api.py:1069-1077 by adding next values to page fixtures and
covering a short first page that still has a next link, ensuring pagination
continues until next is absent.

In `@casework/enrich_court_record.py`:
- Around line 539-543: Update the party_name docstring in
casework/court_record.py to remove or correct the claim that both de-duplication
paths key on the exact returned string. Document the actual behavior: the
enricher uses normalise_name(name), while court_record.defendant_names uses the
raw stripped name.

---

Nitpick comments:
In `@casework/enrich_court_record.py`:
- Around line 984-1002: Update the pass-1/pass-2 case processing around
readable_cases and plan_case so --dry-run retains and reuses the pass-1
case_detail instead of calling api.get_case_with_etag again. Keep the existing
pass-2 re-read and fresh ETag acquisition for apply runs, while ensuring dry-run
planning still receives the retained case_detail and appropriate ETag value.
- Around line 899-932: Update court_record_for_case to distinguish transient 429
and 5xx responses from permanent 404s, retrying transient failures with bounded
backoff before skipping the case. Preserve existing permanent-failure behavior,
and ensure any retry or interval CLI options use nonneg_int or nonneg_float
validation so negative values are rejected during argument parsing.
- Around line 317-319: Restrict rung 1 in the resolution logic to person
entities by adding the existing _is_person guard alongside is_valid_entity_iri
before returning Resolution for row_nes_id. Keep non-person IRIs from being
bound as the accused, while preserving the current valid-person resolution path.
- Line 107: Replace the private _order_key import in the casework parser with a
public helper exported by courts.case_status that provides the same shared
spelling-table ordering behavior, and update the courts-side definition/export
accordingly. Keep parse_case_status unchanged and remove the cross-app
dependency on _order_key.

In `@tests/casework/test_api.py`:
- Around line 1069-1077: Extend test_list_hearings_follows_pages_by_number with
a short-first-page fixture where page 1 has fewer than 100 results and includes
a next-page indicator, while page 2 contains additional results. Assert both
pages are followed and all rows are returned, preserving the existing full-page
pagination coverage.

In `@tests/casework/test_court_record.py`:
- Around line 183-204: Extend the court record failure tests around
court_record_for_case to cover read failures from the hearings and
parties/entity lookups, not only detail. Add cases injecting the HTTPError into
the _FullApi hearings and get_court_case_entities data, and assert the affected
reference is skipped without raising while other valid records remain returned.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 072960fa-97a2-4f46-9777-14d60374ff50

📥 Commits

Reviewing files that changed from the base of the PR and between 0800f44 and d3a2477.

📒 Files selected for processing (12)
  • casework/README.md
  • casework/common/api.py
  • casework/common/llm.py
  • casework/common/pipeline.py
  • casework/court_record.py
  • casework/enrich_court_record.py
  • casework/ledger.py
  • tests/casework/test_api.py
  • tests/casework/test_build_api_guard_wiring.py
  • tests/casework/test_court_record.py
  • tests/casework/test_enrich_court_record.py
  • tests/casework/test_ledger.py

Comment thread casework/common/api.py
Comment thread casework/enrich_court_record.py
…erson

list_hearings exited on `len(batch) < 100`, but config.settings uses plain
PageNumberPagination with no page_size_query_param -- page_size is ignored and
every page is 20 rows, so the loop always stopped after page 1. Measured: 3 of
77 sampled FY078/079 cases carry more than 20 hearings (max 27), and a deciding
फैसला row in the dropped tail silently changes case_end_date and bind_outcome.

Also from review: rung 1 copied the court row's nes_id on IRI validity alone,
the one path by which an office could be bound as the accused individual; a dry
run re-read every case for an If-Match it can never send; and party_name's
docstring still claimed both de-dup paths key on the same string.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@gaurav-karki

Copy link
Copy Markdown
Member Author

Thanks — the pagination finding was real, and worse than described. Addressed in 45a24cd.

list_hearings pagination (Major) — confirmed against production, fixed.

The root cause is not "if the endpoint caps page_size". config/settings.py sets DEFAULT_PAGINATION_CLASS to plain rest_framework.pagination.PageNumberPagination, which defines no page_size_query_param — so the page_size=100 this method sends is ignored outright and every page comes back at PAGE_SIZE (20). The len(batch) < 100 exit therefore returned page 1 and stopped on every case, not just capped ones.

Measured on production, anonymous GET, over every 4th FY078/079 census case:

cases checked          : 77
max hearings on a case : 27
cases where page 1 was TRUNCATED: 3
   special/078-CR-0011: count=25 returned=20 next=True
   special/078-CR-0100: count=27 returned=20 next=True
   special/078-CR-0121: count=21 returned=20 next=True

~4% of cases silently lost hearings, and next was present on all three — so the correct signal was there and unread. Consequence is exactly as you described: deciding_hearing picks by max hearing_date_ad among फैसला rows, so a deciding row in the dropped tail changes case_end_date and bind_outcome.

Now exits on not batch or not data.get("next"), matching get_court_case_entities and iter_cases. Still pages by number — the next URL is absolute and get() builds base_url + path, so following it would double the prefix; only its presence is used.

Test rewritten as you asked, plus two more: the production shape (20 rows with next, then 7 with the फैसला rows) and a next that lies past the end so it cannot spin. Verified the short-page test goes red against the old rule — assert 20 == 27.

party_name docstring (Minor) — fixed. You are right that the invariant became false. It now states that defendant_names keys on the exact string while _accused_binds keys on normalise_name of it, why (punctuation variants of one name on one case must collapse and match the held-name index), and that it is benign only because defendant_names is off the enricher path.

Nitpicks:

  • Short-first-page pagination test — covered by the fix above.
  • Rung 1 outside the person prefix policy — fixed. Rungs 2 and 3 can only produce a person, so rung 1 was the one way a non-person IRI could reach an accused bind. Not dead code: this cohort carries no nes_id, but special/080-cr-0111 was backfilled with 185.
  • Dry run re-reads for an unused ETag — fixed. Worth more than it looks: measured 8.8 requests/case and ~4,470/hour against a 5,000/hour ceiling, so this drops ~2,900 requests off a full-corpus dry run. The --apply re-read stays unconditional, pinned by a call-count test on both paths.
  • Request-rate control for pass 1not done, deliberately. It is a behaviour change rather than a fix, and the measured numbers are in the PR description so an operator can size a run. Worth its own PR.
  • _order_key private cross-app importnot done. The clean fix is a public name in courts/case_status.py, and this PR is deliberately confined to casework/. The import is documented at its call site.
  • Hearings/parties read-failure tests in test_court_record.pynot done; court_record_for_case routes all three reads through one guard that is already pinned.

1618 tests pass, ruff check and ty clean.

@jawafdehi-pr-agent

Copy link
Copy Markdown

Persistent review updated to latest commit 45a24cd

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant