fix(helpers): prevent citation metadata bleeding across neighboring citations - #350
Conversation
|
@claude review |
There was a problem hiding this comment.
Nothing blocking. The comments below are optional suggestions. There is no need to push a fix for them before merging.
Beyond the inline finding, I checked the new early break in _scan_for_case_boundaries (helpers.py:234) added in the latest commit: although it exits without recomputing start_index/candidate_case_name, those are already populated at the point v_token is first recorded, so no case-name text is lost by breaking early. I also confirmed the guard's v_token is not None scope is appropriate, since the year-bleed bug it targets only arises for citations whose case name contains "v."
Extended reasoning...
This run's inline comment reconfirms a pre-existing, unaddressed issue (regexes.py:93: adding "compare" to the shared STOP_WORDS also widens strip_stop_words's unanchored, case-insensitive substitution, so any real party name containing "compare" as a standalone word would be corrupted) — that concern predates the latest commit and is carried by the inline comment, not restated here. What is new since the last review is the second commit (fc9b4bc), which adds the v_token-aware break in _scan_for_case_boundaries for the year-boundary fix; I traced that logic against two candidate concerns (loss of already-extracted case-name state on early break, and the narrow v_token-only scope of the guard) and ruled both out based on the surrounding code's control flow, matching this run's ruled-out list.
| "granted", | ||
| "dismissed", | ||
| "Cf", | ||
| "compare", |
There was a problem hiding this comment.
🟡 (optional) Adding "compare" to STOP_WORDS also widens strip_stop_words (helpers.py), which reuses the same STOP_WORD_REGEX to clean already-extracted plaintiff/defendant text via a global, case-insensitive, non-anchored substitution. Any real party name containing "compare" as a standalone word (e.g. "Compare Networks, Inc.", "Compare.com") now gets that word silently stripped from metadata.plaintiff/defendant, which did not happen on base. Fix: scope the new stop-word entry to the tokenizer's boundary-detection use only (or anchor strip_stop_words's substitution to leading/signal-phrase position) so cleanup of already-parsed case names doesn't remove ordinary words that happen to match a stop word.
Extended reasoning...
STOP_WORD_REGEX (regexes.py:95-97) is built from STOP_WORDS and used both by AhocorasickTokenizer to detect signal words during backward scanning AND by strip_stop_words (helpers.py:805-831), which is called on already-extracted plaintiff/defendant strings at helpers.py:373,379,386,624,700,703,742,745,801. strip_stop_words's second re.sub (line 823-828, flags=re.IGNORECASE) matches the whole-word pattern space_boundaries_re anywhere in the string, not just at the start. Given defendant text "Compare Networks, Inc." extracted from HTML tags (helpers.py:734-745), the word "Compare" is stripped, producing metadata.defendant = "Networks, Inc." instead of the correct name -- a data-corruption regression not present before this diff.
Verification: nit. The mechanism is real and introduced by this diff. Adding "compare" to STOP_WORDS (regexes.py:93) feeds the shared STOP_WORD_REGEX (regexes.py:95-97 -> (?:^|\s)([^\sa-zA-Z0-9]*(?P<stop_word>...|compare)[^\sa-zA-Z0-9]*)(?:\s|$)), which strip_stop_words reuses on already-extracted party text. The second substitution (helpers.py:822-828) runs with flags=re.IGNORECASE and is not…
|
So this is good, but can I ask you to look at a few more examples I came up with. Try these.
Right now it gives College. There's more than one thing going on. Add tests for whatever you find. |
|
I can see that the two last examples are specific scenarios that I did not foresee before, but I have already addressed them. Let me break down each one: Multi-word Plaintiff Namecitation_text = "Smith v. Jones, 347 U.S. 999 (1952). Citizens United v. FEC, 558 U.S. 310 (2010)."
for c in get_citations(citation_text):
print(c.corrected_citation(), c.metadata.year, repr(c.metadata.plaintiff), repr(c.metadata.defendant))
347 U.S. 999 1952 'Smith' 'Jones'
558 U.S. 310 2010 'United' 'FEC' <-- missing 'Citizens'Not updating Plaintiff name including exceptional lowercase-only words ('of', 'and')citation_text = "Smith v. Jones, 347 U.S. 999 (1952). Trustees of Dartmouth College v. Woodward, 17 U.S. 518 (1819)."
for c in get_citations(citation_text):
print(c.corrected_citation(), c.metadata.year, repr(c.metadata.plaintiff), repr(c.metadata.defendant))
347 U.S. 999 1952 'Smith' 'Jones'
17 U.S. 518 1819 'Trustees Dartmouth College' 'Woodward' <-- missing 'of'Lowercase-only words are removed from I am still working on the first two examples provided before: It looks like those are not the same kind of issue as the previous ones, and both bleed the last citation year to the first one. |
|
They are not, but I am curious how you would or if you could tackle them. Plaintiff names have been particularly difficult but in parallel like this I would suspect we could manage. |
|
@flooie I can see that in the following text: The subtext: is being handled as post-citation metadata, and having its string Do you think that setting |
|
I'm not sure. I would think it could be, but it could cause down stream issues. Maybe a test example exists? |
|
@flooie Great point. I have just tested a parallel citation example composed with a unit test case and it cause conflicts: citation_text = 'bob Lissner v. Test 1 U.S. 12, 347-348. See also Baz v. Qux, 2 U.S. 2 (2013).'
for c in get_citations(citation_text):
print(c.corrected_citation(), c.metadata.year, repr(c.metadata.plaintiff), repr(c.metadata.defendant))
1 U.S. 12 2013 'Lissner' 'Test' <-- 2013 incorrectly assigned
2 U.S. 2 2013 'Baz' 'Qux'I believe that this matter would be better handled in a new Github issue of its own. Let me know if I can update the top description of this PR and we can move forward with it. |
Description
Fixes both repros in #321:
FullCaseCitation.metadatapicking up the previous citation's plaintiff/defendant, and separately its year, instead of its own.Fix 1 — missing stop word
When scanning backward from a citation to find its case name, eyecite stops the scan as soon as it hits a known "stop word" (
STOP_WORD_REGEX/STOP_WORDSineyecite/regexes.py) such assee,accord,cf, etc. Ifthe text immediately before a citation isn't a recognized stop word, the backward scan keeps walking past the sentence boundary and can pick up the previous citation's plaintiff/defendant as this citation's case name.
compare(as inCompare X v. Y ... with A v. B ...signal phrases) was missing fromSTOP_WORDS, so a citation introduced withComparehad nothing to stop the backward scan, and it inherited the prior citation's defendant instead of its own.Before:
After:
Fix: add
comparetoSTOP_WORDSineyecite/regexes.pyso the backward case-name scan stops there instead of crossing into the previous citation's text, the same mechanismsee/accord/cfalready rely on.Fix 2 — year leaking from the previous citation's parenthetical
The same backward scan in
_scan_for_case_boundaries(eyecite/helpers.py) also picks up a(YYYY)year token as it walks past the citation it's building a case name for. It didn't distinguish between a year parenthetical that belongs to this citation (appearing before thev.token) and one that belongs to an earlier citation in the text(appearing after the
v.token has already been seen, i.e. the year is on the far side of a case name the scan has already fully consumed). As a result, a citation could silently inherit the previous citation's year instead of using its own trailing(YYYY).Before:
After:
Fix: in
_scan_for_case_boundaries, once av_tokenhas been recorded for the current citation, hitting another(YYYY)token means the scan has crossed into an earlier citation's parenthetical — so the loop now breaksinstead of overwriting
pre_cite_yearwith that unrelated year.Both fixes are covered by new cases added to
tests/test_FindTest.py.AI Disclosure