From 2a76196f78040436037ef746ebceba4c2493275c Mon Sep 17 00:00:00 2001 From: Abdelrahman Mostafa Date: Tue, 6 Oct 2026 12:24:45 +0300 Subject: [PATCH] fix(coverage): count a joined/CamelCase class name as coverage A row recorded as "DirectoryTraversal" or "XSLeaks" normalizes to one token, which whole-word matching missed, so finish_scan/coverage.json could report tested work as a gap. _entry_is_about now also matches a phrasing's words joined into one token as a fallback - still a whole-token check, never a substring. The branch is additive: it can only turn a gap into a cover, never the reverse, so it introduces no false gap. --- strix/report/coverage.py | 13 +++++++++++-- tests/test_report_coverage.py | 11 +++++++++++ 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/strix/report/coverage.py b/strix/report/coverage.py index bcca1c33..c3444a95 100644 --- a/strix/report/coverage.py +++ b/strix/report/coverage.py @@ -333,10 +333,19 @@ def _entry_is_about(entry: dict[str, Any], phrasings: list[list[str]]) -> bool: A phrasing matches when every one of its words appears as a **word** in the row (``risk_area`` + ``surface``) — not as a substring, so "brute force" - never counts as ``rce`` coverage and "corridor" never as ``idor``. + never counts as ``rce`` coverage and "corridor" never as ``idor``. As a + fallback it also matches the phrasing's words joined into one token, so a + row that recorded a joined / CamelCase name the normalizer kept whole + ("DirectoryTraversal" -> "directorytraversal", "XSLeaks" -> "xsleaks") + still counts — still a whole-token check, never a substring. """ words = set(_normalized(f"{entry.get('risk_area', '')} {entry.get('surface', '')}").split()) - return any(all(_term_in_words(term, words) for term in terms) for terms in phrasings) + for terms in phrasings: + if all(_term_in_words(term, words) for term in terms): + return True + if len(terms) > 1 and _term_in_words("".join(terms), words): + return True + return False def _entry_authored_by(entry: dict[str, Any], owner_ids: set[str]) -> bool: diff --git a/tests/test_report_coverage.py b/tests/test_report_coverage.py index c0ede09d..50cf30fc 100644 --- a/tests/test_report_coverage.py +++ b/tests/test_report_coverage.py @@ -104,6 +104,17 @@ def test_substring_match_does_not_hide_a_gap() -> None: assert skill.replace("_", " ") in classes, (skill, surface, risk, classes) +def test_joined_camelcase_name_counts_as_coverage() -> None: + """A row recorded as a joined/CamelCase class name ('DirectoryTraversal') + still counts, via the joined-token fallback — no substring matching.""" + doc = _document( + entries=[_entry(risk_area="DirectoryTraversal reviewed", surface="/download")], + agent_graph=_graph(metadata={"agent-1": {"skills": ["path_traversal_lfi_rfi"]}}), + ) + + assert not [g for g in doc["gaps"] if g["kind"] == "unrecorded_risk_class"] + + def test_simple_plural_still_counts_as_coverage() -> None: """Whole-word matching tolerates a trivial plural, so 'passwords' / 'credentials' still cover weak_password_detection."""