Skip to content
This repository was archived by the owner on Jul 20, 2026. It is now read-only.

fix: scope schema-drift check to litigation cases - #84

Merged
damo-da merged 2 commits into
mainfrom
fix/schema-drift-guard
Jun 23, 2026
Merged

fix: scope schema-drift check to litigation cases#84
damo-da merged 2 commits into
mainfrom
fix/schema-drift-guard

Conversation

@damo-da

@damo-da damo-da commented Jun 23, 2026

Copy link
Copy Markdown
Member

Summary

The ngm-supreme-court and ngm-special-court CronJobs have been failing on every run for days. Both scrape complete data (0 missing case_type), but they're killed by scripts/check_schema_drift.py, which sits in the && chain between scrape and enrichment.

The guard counts blank case_type across the entire 1.56M-row court_cases table with an absolute threshold of >10, then aborts. The ~20,368 blanks it trips on are all non-litigation administrative registrations (marriage-by-registration, power-of-attorney, guardianship, adoption, death-declaration) — filed under a numeric case-number namespace (e.g. 082-02-0001) that legitimately carries no case_type. Litigation cases use an alphabetic namespace (CP, FN, CR, WO, C1, …) and have zero blanks.

Verified split against prod:

case-number code total rows blank case_type
alphabetic (litigation) 1,503,830 0
numeric (administrative) 61,524 20,368

Changes

  • Count blanks only among litigation (alpha-coded) rows — administrative registrations no longer pollute the check.
  • Fail on a ratio (>2%) instead of an absolute >10, so the check scales with table size and catches genuine selector drift.
  • Support optional per-court scoping via argv (e.g. check_schema_drift.py supreme special).

Test plan

  • python -m py_compile + ruff check pass
  • Confirmed against prod DB that litigation rows have 0 blank case_type (guard now passes)
  • Next scheduled ngm-supreme-court / ngm-special-court runs complete green

🤖 Generated with Claude Code

check_schema_drift counted blank case_type across the entire court_cases
table with an absolute >10 threshold, which nightly false-failed the
supreme/special CronJobs (whose data is complete). The ~20k blanks are all
non-litigation registrations (marriage, power-of-attorney, guardianship,
etc.) filed under a numeric case-number namespace that legitimately has no
case_type.

Restrict the check to litigation rows (alphabetic case-number codes), fail
on a ratio instead of an absolute count so it scales with table size, and
allow optional per-court scoping via argv.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jun 23, 2026

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

@damo-da, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 28 minutes and 45 seconds. Learn how PR review limits work.

Your organization has used up its prepaid credits, and credit purchases are no longer available. Enable the review add-on in the billing tab to keep reviews running — you're only billed for reviews past your plan's rate limits ($0.25/file).

⌛ How to resolve this issue?

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 credits.

🚦 How do rate limits work?

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

For paid Pro and Pro+ PR reviews, CodeRabbit uses rolling per-developer review limits. Reviews become available again as older review attempts age out of the rolling limit window.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 6fb2a0b0-ceeb-4561-8cf2-e9d40b4b064b

📥 Commits

Reviewing files that changed from the base of the PR and between 6fecfd2 and f661c72.

📒 Files selected for processing (1)
  • scripts/check_schema_drift.py
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/schema-drift-guard

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.

@gemini-code-assist gemini-code-assist 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

This pull request updates the schema drift check script to calculate a ratio of missing case types specifically for litigation cases, rather than using an absolute count across all cases. It also adds support for scoping the check to specific court identifiers. The review feedback highlights two key issues: first, calculating the drift ratio over the entire table history can dilute recent failures and cause performance bottlenecks, so it is recommended to scope the check to recent cases (e.g., the last 3 days); second, providing invalid court identifiers currently results in a silent skip rather than an error, which should be resolved by validating the identifiers beforehand.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread scripts/check_schema_drift.py Outdated
Comment on lines +25 to +30
where = [LITIGATION_ONLY]
params = {}
if courts:
where.append("court_identifier = ANY(:courts)")
params["courts"] = list(courts)
scope = " AND ".join(where)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

high

⚠️ High: Drift Ratio Dilution & Performance Bottleneck

Calculating the schema drift ratio over the entire history of the court_cases table (1.5M+ rows) introduces two major issues:

  1. Dilution of Drift (False Pass): If the scraper starts failing today (e.g., 100% of today's 1,000 new cases are missing case_type), the ratio of missing cases over the entire 1.5M historical rows will be 1,000 / 1,501,000 ≈ 0.06%. This is far below the 2% threshold (DRIFT_RATIO_THRESHOLD), meaning the check will pass and the drift will go undetected.
  2. Performance: Evaluating the regex split_part(case_number, '-', 2) ~ '[A-Za-z]' via a full table scan on 1.5M+ rows on every cron job run is extremely slow and does not scale.

Recommendation

Scope the check to recently created cases (e.g., the last 3 days). This ensures that a sudden scraper failure on a new batch will immediately spike the ratio to ~100% and trigger the alert, while also keeping the query highly performant.

Suggested change
where = [LITIGATION_ONLY]
params = {}
if courts:
where.append("court_identifier = ANY(:courts)")
params["courts"] = list(courts)
scope = " AND ".join(where)
where = [LITIGATION_ONLY, "created_at >= NOW() - INTERVAL '3 days'"]
params = {}
if courts:
where.append("court_identifier = ANY(:courts)")
params["courts"] = list(courts)
scope = " AND ".join(where)

Comment on lines +19 to 23
def check(courts=None):
db_url = os.getenv("DATABASE_URL")
if not db_url:
print("DATABASE_URL not set")
sys.exit(1)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

⚠️ Medium: Silent Skip on Invalid Court Identifiers

If a user passes an invalid court identifier (e.g., due to a typo like supremee), the script currently queries court_identifier = ANY(['supremee']). Since no rows match, total will be 0, and the script will print:
Schema check skipped: no litigation rows in scope (courts=['supremee'])
and exit with code 0 (success).

This silently skips the schema check instead of alerting the user that they provided an invalid court name.

Recommendation

Validate the provided court identifiers using the existing is_valid_court_identifier utility before running the query.

Suggested change
def check(courts=None):
db_url = os.getenv("DATABASE_URL")
if not db_url:
print("DATABASE_URL not set")
sys.exit(1)
def check(courts=None):
db_url = os.getenv("DATABASE_URL")
if not db_url:
print("DATABASE_URL not set")
sys.exit(1)
if courts:
from ngm.utils.court_mapping import is_valid_court_identifier
invalid = [c for c in courts if not is_valid_court_identifier(c)]
if invalid:
print(f"ERROR: Invalid court identifier(s): {invalid}")
sys.exit(1)

…e courts

- Fix CI: black formatting.
- Only evaluate rows ingested in the last 3 days so a fresh selector break
  isn't diluted into insignificance by table history (and avoid scanning
  millions of rows each run); skip when the recent sample is below a floor.
- Validate court identifiers passed via argv and error on unknown ones,
  rather than silently skipping (which would mask a typo as a pass).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@damo-da

damo-da commented Jun 23, 2026

Copy link
Copy Markdown
Member Author

Addressed the review feedback and fixed CI:

  • CI (black): formatting fixed — format.sh --check runs black, which the first push missed.
  • Dilution / perf: the check now only evaluates rows ingested in the last 3 days (RECENT_WINDOW_DAYS), so a fresh selector break can't be diluted by table history, and it no longer regex-scans the whole table. Skips when the recent sample is below a floor (MIN_SAMPLE) to avoid noisy ratios.
  • Silent skip on bad court id: added _validate_courts — unknown identifiers passed via argv now error instead of passing silently.

@damo-da
damo-da merged commit a144959 into main Jun 23, 2026
4 checks passed
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant