Repository navigation
Screening pre-check: registry lookups (#30) - #34
Conversation
The lookups half of #30: a shared result type and one entry point, checkRegistries(), that checks volunteers against the Georgia GBI registry (its published CSV) and returns a search-by-hand link for NSOPW, whose terms forbid automated searching. Hits are filtered by birth year (±1). Includes scripts/screening-check.ts, assertion checks that use made-up names only. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Disclaimer: This comment was AI-generated and is not necessarily completely accurate. Please take code comments with a grain of salt.
|
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GG4LpiuNziAYJVp9WJMbtK
- Reject a GBI download whose size doesn't match Content-Length, or whose rows don't match the header width, so a cut-off file can't say no match - parseCsv throws on an unclosed quote instead of swallowing the rest - An invalid date of birth reports missing_dob, not no match - A repeated userId gets one result per source - Only ScreeningSourceError messages are logged, so a source can't leak a name into the logs through an exception Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GG4LpiuNziAYJVp9WJMbtK
- ScreeningCheck collection keeps the latest result per source per volunteer, apart from users like Clearance - The hourly job checks volunteers with a sign-up pending on clearance who were never checked or hit a source error, plus queued reruns - Admins can queue a rerun from the person page; it's audited and runs with the next hourly job - Person page shows each source's result with links to verify hits or search by hand - Approvals adds a Waiting on clearance list next to Clearances to review - Clearance status is never touched Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GG4LpiuNziAYJVp9WJMbtK
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GG4LpiuNziAYJVp9WJMbtK
✅ Deploy Preview for pink-stem-staging ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
- Fold Latin letters that NFD leaves alone (Đ, Ł, Ø, Æ, Œ, Þ), and report a name with letters the matcher still can't read as "couldn't check" (unmatchable_name) instead of a false "no match" - Recheck a volunteer when a pending sign-up or their profile changes after the last check, so a lapsed clearance or a corrected name or date of birth gets a fresh look; the missing_dob case is covered by the same rule - Treat a quote inside an unquoted CSV value as text, so an inch mark cannot swallow the rows after it - Count a registry birth year that isn't a plain year as unknown, raise the entry floor to 20,000 so a cut-off compressed download can't pass, and log fetch failures by their error type - Run the pre-check last in the hourly job, and save volunteers in parallel - Drop rerunRequestedBy (the audit row records who asked), show a Verify link per hit instead of linking the name, and document the collection, route, script and job
Requested by Brody Lussier
Closes #30. This PR now covers both halves of the ticket: the registry lookups (below, by @spencer-banko) and the storage, hourly job, rerun, person page and Approvals queue (see "Storage, job and admin screens" further down).
Branch:
pre-check-volunteers-against-public-registries→mainProblem
Background clearance is fully manual. An admin runs a check somewhere else and types in the outcome. Fingerprinting rules out full automation, but searching public sex-offender registries for a volunteer's name can be automated. To do that we need:
Solution
checkRegistries(subjects)insrc/lib/screening/index.tschecks a batch of volunteers against every source. For each source it returns one of three outcomes per volunteer:no_matchpossible_matchunavailablemanual_only,missing_doborsource_error), with a link to search by handEvery result carries
source,sourceLabelandcheckedAt. The function only looks things up. It never throws, never touches clearance status and never logs names or results.Sources: what each one allows (checked 2026-09-30)
Disallow: /search/andDisallow: /node/*unavailable/manual_onlywith a link to https://www.nsopw.gov/search-0https://state.sor.gbi.ga.gov/SORT_PUBLIC/sor.csv), which avoids scraping entirely.Implementation details
src/types/screening.tsRegistryCheckResult,RegistryHit,RegistrySubject, and the source, outcome and reason enums. This is the contract with the storage and UI half.src/lib/screening/index.tscheckRegistries(subjects, sources?)→Map<userId, RegistryCheckResult[]>, one result per source in source order. A source that throws becomessource_errorfor every subject it was checking, and the other sources still run.src/lib/screening/source.tsScreeningSourceinterface (id,label,manualSearchUrl,check(subjects, checkedAt)). A vendor API is one new implementation of it.src/lib/screening/sources/gbi.tssrc/lib/screening/sources/nsopw.tssrc/lib/screening/match.tssrc/lib/csv.tsparseCsv(RFC 4180) next to the existingtoCsv.src/constants/limits.tsSCREENING_BIRTH_YEAR_TOLERANCE = 1andSCREENING_FETCH_TIMEOUT_MS = 8000.scripts/screening-check.ts+npm run screening:checkHow the GBI source works:
fetch, an 8-second timeout, a descriptive User-Agent and no retries. The whole batch shares that one download.no_matchfrom a bad download. If the expected columns are missing or the file has fewer than 1,000 entries, the result issource_error. The real file has about 25,600.missing_dob, and if nobody in the batch has one, the file isn't downloaded.Name matching:
Decisions made
missing_dob). We never flag on a name alone. Google sign-ups can have no DOB, and the admin gets the manual link instead.[screening] gbi_ga failed: download failed: 503. Never names, hits or outcomes.src/lib/screening/. The shared types are insrc/types/screening.ts, next to the other shared types.Tests
The repo has no test runner (see
docs/sprints/sprint-3.md), so this addsscripts/screening-check.ts, a script ofnode:assertchecks. Every name in it is invented, and no fixture comes from real data.The 23 offline checks cover:
toCsvround trip.no_matchno_matchmissing_dob, with the file never downloadedsource_errormanual_only, andfetchis never called (stubbed to throw).checkRegistries:The live check downloads the real file and confirms the made-up names come back
no_match. The download, parse and match took about 2.0 s, under the 8 s timeout. The[screening] … failed:lines in the output come from the failure-path checks and are expected.Also run locally:
npm run format:check,npm run lint,npm run typecheckandnpm run build. All pass.Not verified:
Storage, job and admin screens
Before: a registry result existed only in memory, and an admin waiting on a volunteer's clearance had nothing to look at. Approvals listed only clearances an admin had already marked
submitted.After: every volunteer with a sign-up pending on
clearancegets checked by the hourly job. The admin sees each registry's result on the person page and in a new Waiting on clearance list in Approvals. Clearance status is never changed.src/db/models/screeningCheck.ts,src/db/actions/screeningCheck.tsScreeningCheckper volunteer: the latestRegistryCheckResultper source, plusrerunRequestedAt/By. Separate collection, likeClearance. Results are kept after the volunteer stops being pending.src/services/screening.tsrunDue()(hourly) checks volunteers pending on clearance who were never checked, hitsource_error, or hadmissing_doband now have a DOB, plus queued reruns.requestRerun()queues a rerun and writes ascreening.rerun_requestedaudit entry.src/services/jobs.tsscreeningtorunAll.src/app/api/v1/admin/people/[id]/screening/route.tsPOST, admin only: queue a rerun. Lookups never run inside a request.src/services/admin.ts,src/types/api.tsPersonDetail.screeningandAdminApprovals.awaitingClearance.src/app/admin/people/[id]/page.tsx,src/components/admin/ScreeningResults.tsxsrc/app/admin/page.tsxWaiting on: clearance · GBI: No match · NSOPW: Search by hand.A rerun requested while the job is running stays queued for the next run, so it is never silently dropped.
Review fixes to the lookups
Content-Length, or whose rows don't match the header width, is nowsource_error, notno_match.parseCsvthrows on an unclosed quote.missing_dob.userIdin a batch gets one result per source.ScreeningSourceErrormessages are logged. Any other exception is logged asunexpected error, so a future source can't leak a name.console.loginscripts/screening-check.tsis left as is, matchingscripts/seed.tsandscripts/jobs.ts.Also merged
main, which resolves thepackage.jsonconflict with the staging scripts.Ran
npm run format:check,npm run lint,npm run typecheck,npm run buildandnpm run screening:check(29 checks, 6 new). All pass.Not verified: the job, rerun and admin screens against a real database. No MongoDB was reachable where this was built. To check: sign up a made-up volunteer (with and without a DOB) for an event that needs clearance, run
npm run jobs, then open Approvals and the person page, click Rerun, and run the jobs again.🤖 Generated with Claude Code
https://claude.ai/code/session_01GG4LpiuNziAYJVp9WJMbtK
Generated by Claude Code