Workers' Compensation case names get typed inconsistently. The same applicant appears in the system as O'Doe on the intake form, ODoe on a defense subpoena, O Brien on a hearing notice, and occasionally O'DOE in a court order. The applicant is the same person; the punctuation conventions of whoever typed the name vary.
This tool's search-by-name has to handle all four spellings as the same query. The binding-guard layer that compares an extracted applicant name against the case's stored applicant ALSO has to handle all four — otherwise the guard would refuse legitimate writes whenever the punctuation conventions on the two documents differ.
Both layers normalize the input before comparing. They were doing it differently.
The two normalizers
The search normalizer:
function normalize(str) {
return str
.normalize("NFD")
.replace(/[\u0300-\u036f]/g, "") // strip combining accents
.toLowerCase();
}
The binding-guard normalizer:
export function normalizeName(name) {
return String(name)
.normalize('NFD')
.toUpperCase()
.replace(/[^A-Z0-9]/g, '') // strip everything that isn't a letter or digit
.trim();
}
Both NFD-decompose Unicode. Both case-fold (one to lower, one to upper — doesn't matter as long as both directions are case-folded). The difference is what they do with non-alphanumeric characters.
The binding guard strips them all. O'Doe normalizes to ODOE; O Brien normalizes to ODOE; O-Brien normalizes to ODOE. All four spellings collapse to the same canonical form. The guard correctly recognizes them as the same name.
The search keeps them. O'Doe normalizes to o'doe; ODoe normalizes to odoe. Those are different strings. "o'doe".includes("odoe") is false because the apostrophe sits between the o and the b. The search misses the case.
The user-facing effect: an attorney types merus-search odoe looking for a client they know exists. The case is filed as O'Doe. Search returns no results. Attorney concludes the case isn't in the system. Attorney opens a new case file. Now there are two cases for the same applicant. The downstream work — billing, calendar, conflict checks — fragments across the two records.
How this slipped past testing
Both normalizers were correct in isolation. The search tests use queries that match the case names exactly, or with accents stripped. The guard tests use the binding's normalizeName end-to-end. Neither test set exercised the search with a query whose punctuation differed from the stored case name.
The bug surfaced through real use: attorneys typed names without apostrophes and the search missed. Even then, the failure mode was invisible to anyone other than the attorney — the search returned no results, which looks identical to "no such case exists." There's no error, no warning, no signal. The system simply silently fails to find the thing that's there.
For a probe round looking for this pattern, the right question is: are there two functions in this codebase that do similar normalization, and do they agree? If the same logical operation (canonicalize a name for comparison) has two implementations, the implementations should be identical or one should call the other. Drift between them produces silent inconsistency.
The fix
Bring the search normalizer in line with the binding-guard normalizer. Strip accents AND strip non-alphanumeric. The cost is that file-number search has to use the raw query (not the normalized one), since the file numbers are pure digits — but the code already separates the two paths (name.includes(queryNorm) || fileNum.includes(query)), so file-number search is unchanged.
function normalize(str) {
return String(str)
.normalize("NFD")
.toLowerCase()
.replace(/[\u0300-\u036f]/g, "")
.replace(/[^a-z0-9]/g, "");
}
After the fix, all four O'Doe spellings collapse to the same canonical form. Searching any of them finds all of them. Verified across seven test variants: O'Doe/ODoe interchangeability, García-López accent + hyphen handling, St. John period handling, Smith, John comma handling, file-number-as-query still works, and "xyz" against "Smith" still correctly does not match (no false positives).
The general pattern
Whenever a codebase has more than one place that does a similar transformation, the implementations should either be identical or one should call the other. If they drift apart, every difference between them is a bug surface — usually a silent one, because the two paths still produce SOMETHING, just not the same thing for the same input.
The fix in this case was small: about 10 characters of changed regex. The bug was in production for an unknown duration, surfacing as occasional duplicate case files that nobody attributed to a search bug because the search "worked" (returned results when the punctuation matched).
I think the meta-lesson is to grep the codebase periodically for similar transformations. Names, dates, paths, IDs, identifiers — any value that gets compared across boundaries has a canonicalization step somewhere, and any system that's been built incrementally has multiple canonicalization steps that drift. Aligning them is the kind of work that doesn't make any individual feature better but quietly closes a class of bugs at once.
The version on the install URL is 4.5.105.