fix(task-6): bind the invalid-tag fixture to the reported letter and position

Every grammar error is prefixed with the tag's own letters, so the old
single-letter fragments (FCI/"I", PSVX/"X", ...) matched whatever the
grammar said: a mutation that dropped the letter and position from every
error left 9 of 11 invalid rows green. The fixture now quotes the error
text the spec demands ("unexpected letter I at position 3", "... after
valve V", "trailing O (open/closed) ...", "needs at least one function
letter after F"), and the test pins it to the single first error.

ABCDEF-106 was mispinned as a grammar rejection of "D": six letters
exceed the regex cap, so it never reached the grammar. It is now pinned
as a format rejection, and ABCDE-106 carries the "letter D at position 4"
case.

A fixture lint rejects any fragment that is a substring of its own tag
and requires grammar rows to name a letter, position or trailing rule,
so a future row cannot reintroduce the hole.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd
This commit is contained in:
Umut CAN
2026-09-15 13:27:59 +03:00
co-authored by Claude Opus 5
parent b8de3968f9
commit 2344b3b9d4
2 changed files with 29 additions and 12 deletions
+12 -11
View File
@@ -58,17 +58,18 @@
["CIC-903", "Indicating Controller", "903"]
],
"invalid": [
["FCI-101", "I"],
["FIH-102", "H"],
["F-103", "function"],
["FICC-104", "repeated"],
["PSVX-105", "X"],
["ABCDEF-106", "D"],
["FQQ-107", "Q"],
["1FC", "format"],
["FIC", "format"],
["fic-101-", "format"],
["LSO-108", "O"]
["FCI-101", "unexpected letter I at position 3"],
["FIH-102", "trailing H needs an alarm (A) or switch (S) before it"],
["F-103", "needs at least one function letter after F"],
["FICC-104", "repeated letter C at position 4"],
["PSVX-105", "unexpected letter X at position 4 after valve V"],
["ABCDE-106", "unexpected letter D at position 4"],
["ABCDEF-106", "not in [area-]LETTERS[-]loop[suffix] format"],
["FQQ-107", "unexpected letter Q at position 3"],
["1FC", "not in [area-]LETTERS[-]loop[suffix] format"],
["FIC", "not in [area-]LETTERS[-]loop[suffix] format"],
["fic-101-", "not in [area-]LETTERS[-]loop[suffix] format"],
["LSO-108", "trailing O (open/closed) is only valid after a switch on a position (Z) tag"]
],
"formats": [
["10-FIC-101A", "10", "FIC", "101", "A"],
+17 -1
View File
@@ -39,7 +39,23 @@ def test_invalid_tags_name_the_offending_letter(tag, fragment):
out = parse_tag(tag, kind="instrument")
assert out["valid"] is False
assert out["tag"] == tag
assert any(fragment in err for err in out["errors"]), out["errors"]
# The grammar stops at the first offending letter, so the fixture pins
# exactly one error and the fragment must sit in *that* one.
assert len(out["errors"]) == 1, out["errors"]
assert fragment in out["errors"][0], out["errors"]
@pytest.mark.parametrize("tag, fragment", FIXTURE["invalid"])
def test_invalid_fixture_fragments_bind_the_letter_and_position(tag, fragment):
# Every grammar error is prefixed with the tag's own letters, so a fragment
# that is a substring of the tag ("I" for FCI) matches whatever the grammar
# says and pins nothing. A row must quote the rule text, and a grammar row
# must quote the reported position as well as the letter.
assert fragment not in tag.upper(), f"{tag!r}: fragment {fragment!r} is part of the tag"
if "format" not in fragment:
assert "position" in fragment or "letter" in fragment or "trailing" in fragment, (
f"{tag!r}: fragment {fragment!r} names neither a letter nor a position"
)
@pytest.mark.parametrize("tag, area, letters, loop, suffix", FIXTURE["formats"])