Fix phantom "Important:" header stealing a taking-fire report's deadline
Turned out to be one bug, not two: "Answer by <time>" phrasing was
already covered by _TAKING_FIRE_TIME_RE ('before|by <time>'). The real
bug was the last-resort bare-"<Name>:" header fallback matching a
same-message "Important:" follow-up line as a brand new named entity
(nothing excluded common prose lead-ins), creating a bogus
Target#Important that stole the deadline into its own requested_time
instead of the real report's. Fixed with a blocklist on that fallback
rule (important/note/warning/attention/caution/alert/reminder/priority).
New regression test, 55 total passing.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
parent
6a61bffb22
commit
6e34f50b6a
29
TODO.md
29
TODO.md
@ -333,21 +333,22 @@ each confirmed live against the real VM, not just read off a diff:
|
||||
fixing: does the board/firing-panel data model even have a place
|
||||
to put a second shell for one strike request, or does this need a
|
||||
new field/shape entirely?
|
||||
- [ ] "Answer by 10:30:00" isn't recognized as a deadline at all --
|
||||
`_TAKING_FIRE_TIME_RE`/`_REQUESTED_BEFORE_RE` only know "before/by
|
||||
\<time\>" and "Requested before - \<time\> -" phrasing. A third
|
||||
deadline grammar to add.
|
||||
- [ ] A same-message "Important: ... Answer by \<time\>" follow-up line
|
||||
gets misread as a brand new named entity header (the last-resort
|
||||
bare-`<name>:` fallback rule matches "Important:"), creating a
|
||||
bogus `Target#Important` carrying that deadline as its own
|
||||
`requested_time`. Real false-positive in the header-detection
|
||||
fallback, needs either an exclusion list (blocklist "Important"
|
||||
and similar prose lead-ins) or a stricter bare-header shape.
|
||||
Confirmed live against the user's real pasted message (with an
|
||||
- [x] "Answer by 10:30:00" turned out to be one bug, not two. The
|
||||
phrasing itself was never the problem -- `_TAKING_FIRE_TIME_RE`
|
||||
already matches any `before|by <time>`, "Answer BY 10:30:00"
|
||||
included. The REAL bug: a same-message "Important: ..." follow-up
|
||||
line was misread as a brand new named entity header (the
|
||||
last-resort bare-`<name>:` fallback rule matched "Important:"
|
||||
itself with nothing excluding common prose lead-ins), creating a
|
||||
bogus `Target#Important` that stole "Answer by 10:30:00" into ITS
|
||||
own `requested_time` instead of the real report's. Fixed with a
|
||||
blocklist (`_BARE_NAME_HEADER_BLOCKLIST`: important/note/warning/
|
||||
attention/caution/alert/reminder/priority) on that fallback rule --
|
||||
once the phantom split stopped happening, the deadline resolved
|
||||
onto the right entry with no separate fix needed. New regression
|
||||
test, confirmed against the user's real pasted message (with an
|
||||
assumed `Infantry#N taking fire!` header line prepended, since
|
||||
their paste seems to have been cropped before it -- worth
|
||||
double-checking against the actual full in-game message).
|
||||
their paste seems to have been cropped before it).
|
||||
|
||||
- [x] "\<Type\>#\<id\> Destroyed" kill-feed parsing already exists and
|
||||
already marks the matching Target dead (`parse_destroyed`,
|
||||
|
||||
@ -375,6 +375,17 @@ _NAMED_HEADER_RE = re.compile(rf"^{_TYPE_ID_FRAGMENT}\s*:?\s*(.*)$")
|
||||
# one), nothing else anchors this match, so an optional colon would
|
||||
# false-positive on an ordinary clue-continuation line's leading word.
|
||||
_BARE_NAME_HEADER_RE = re.compile(r"^([A-Za-z][A-Za-z0-9]*)\s*:\s*(.*)$")
|
||||
# Even requiring the colon isn't quite enough: a prose lead-in word right
|
||||
# before a genuinely useful follow-up line ("Important: TEAR Shell first,
|
||||
# then HE Shell.") false-positives the same way -- confirmed live against
|
||||
# a real taking-fire message, where "Important:" got read as a brand new
|
||||
# named entity ("Target#Important"), stealing the actual report's own
|
||||
# "Answer by <time>" deadline into that bogus entry instead of the real
|
||||
# one. None of these read as an actual thing being spotted/named, only
|
||||
# ever as a prose interjection.
|
||||
_BARE_NAME_HEADER_BLOCKLIST = {
|
||||
"important", "note", "warning", "attention", "caution", "alert", "reminder", "priority",
|
||||
}
|
||||
|
||||
# Ad-hoc enemy installations are named in plain English rather than given a
|
||||
# Type#N id ("Enemy Signal Station:", "Bearing 034 from Enemy Signal
|
||||
@ -908,7 +919,11 @@ def parse_intel_blocks(text: str) -> list[dict]:
|
||||
# 'Target'), not a Reference Point, a named thing giving its own
|
||||
# clues is being spotted, not a fixed landmark spotters aim off
|
||||
# of, same reasoning as the "Target is at-" calibration line.
|
||||
bare_m = next((m for c in candidates if (m := _BARE_NAME_HEADER_RE.match(c))), None)
|
||||
bare_m = next(
|
||||
(m for c in candidates if (m := _BARE_NAME_HEADER_RE.match(c))
|
||||
and m.group(1).lower() not in _BARE_NAME_HEADER_BLOCKLIST),
|
||||
None,
|
||||
)
|
||||
if bare_m:
|
||||
flush()
|
||||
name = bare_m.group(1)
|
||||
|
||||
@ -383,3 +383,33 @@ def test_infantry_taking_fire_bearing_distance_short_range():
|
||||
assert coord == Coord("H", 7, 8, 4)
|
||||
assert shell is Shell.HE
|
||||
assert requested_time == "10:17:37"
|
||||
|
||||
|
||||
def test_taking_fire_important_followup_line_does_not_steal_the_deadline():
|
||||
"""A real user-pasted message: a same-report "Important: ... Answer by
|
||||
<time>" follow-up line was being misread as a brand new named entity
|
||||
header (the last-resort bare-"<Name>:" fallback matched "Important:"
|
||||
itself), creating a bogus Target#Important that stole the actual
|
||||
report's own deadline into that wrong entry instead of the real
|
||||
StrikeRequest. "Answer by <time>" is also a deadline phrasing
|
||||
_TAKING_FIRE_TIME_RE already covers (any 'before'/'by <time>') --
|
||||
once the phantom split stops happening, it resolves correctly with
|
||||
no extra fix needed."""
|
||||
text = ("Infantry#4 taking fire!\n"
|
||||
"Requesting <u><b>TEAR Shell</b></u> first, then <u><b>HE Shell</b></u>,\n"
|
||||
"at bearing <b>308°</b>, distance <b>1.86km</b> from our position, <b>N2 0:9</b>\n"
|
||||
"<u>Important:</u> <u><b>TEAR Shell</b></u> first, then <u><b>HE Shell</b></u>.\n"
|
||||
"Answer by <u>10:30:00</u>")
|
||||
info = ocr.parse_text(text)
|
||||
|
||||
assert (TargetType.UNKNOWN, "Important") not in info.targets
|
||||
|
||||
assert (TargetType.INFANTRY, "4") in info.allies
|
||||
assert (TargetType.INFANTRY, "4") not in info.targets
|
||||
|
||||
assert (TargetType.STRIKE_REQUEST, "Infantry4") in info.targets
|
||||
_, _, coord, shell, requested_time = info.targets[(TargetType.STRIKE_REQUEST, "Infantry4")]
|
||||
assert requested_time == "10:30:00"
|
||||
# Known gap, not asserted as fixed here: only the FIRST shell of a
|
||||
# "X first, then Y" sequence is captured -- see TODO.md.
|
||||
assert shell is Shell.TEAR
|
||||
|
||||
Loading…
Reference in New Issue
Block a user