From 722bb48381622168b974d4b906fc19a745a2a0d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20Gro=C3=9F?= Date: Tue, 17 Feb 2026 23:30:18 +0100 Subject: [PATCH 1/3] add (failing) unit test for line number bug --- .../tests/fixtures/repro/README.md | 7 +++++++ .../tests/fixtures/repro/en/per_fraction.yaml | 12 +++++++++++ .../tests/fixtures/repro/nb/per_fraction.yaml | 16 ++++++++++++++ .../audit_translations/tests/test_auditor.py | 21 +++++++++++++++++++ 4 files changed, 56 insertions(+) create mode 100644 PythonScripts/audit_translations/tests/fixtures/repro/README.md create mode 100644 PythonScripts/audit_translations/tests/fixtures/repro/en/per_fraction.yaml create mode 100644 PythonScripts/audit_translations/tests/fixtures/repro/nb/per_fraction.yaml diff --git a/PythonScripts/audit_translations/tests/fixtures/repro/README.md b/PythonScripts/audit_translations/tests/fixtures/repro/README.md new file mode 100644 index 00000000..05110a64 --- /dev/null +++ b/PythonScripts/audit_translations/tests/fixtures/repro/README.md @@ -0,0 +1,7 @@ +# Repro Fixtures + +This folder contains minimal fixtures from snapshots of the Rules at some point in time. + +The first use of this is comparing `per-fraction` between the English and Norwegian rules as of 2026-02-17, which had a bug of the wrong lines being shown. +- `en/per_fraction.yaml` +- `nb/per_fraction.yaml` diff --git a/PythonScripts/audit_translations/tests/fixtures/repro/en/per_fraction.yaml b/PythonScripts/audit_translations/tests/fixtures/repro/en/per_fraction.yaml new file mode 100644 index 00000000..02c012e2 --- /dev/null +++ b/PythonScripts/audit_translations/tests/fixtures/repro/en/per_fraction.yaml @@ -0,0 +1,12 @@ +- name: per-fraction + tag: fraction + match: + - "BaseNode(*[1])[contains(@data-intent-property, ':unit') or" + - " ( self::m:mrow and count(*)=3 and" # maybe a bit paranoid checking the structure... + - " *[1][self::m:mn] and *[2][.='\u2062'] and BaseNode(*[3])[contains(@data-intent-property, ':unit')] ) ] and" + - "BaseNode(*[2])[contains(@data-intent-property, ':unit')] " + replace: + - x: "*[1]" + - t: "per" # phrase('5 meters 'per' second) + - x: "*[2]" + diff --git a/PythonScripts/audit_translations/tests/fixtures/repro/nb/per_fraction.yaml b/PythonScripts/audit_translations/tests/fixtures/repro/nb/per_fraction.yaml new file mode 100644 index 00000000..6d89e1ad --- /dev/null +++ b/PythonScripts/audit_translations/tests/fixtures/repro/nb/per_fraction.yaml @@ -0,0 +1,16 @@ +- name: per-fraction + tag: fraction + match: + - "BaseNode(*[1])[contains(@data-intent-property, ':unit') or" + - " ( self::m:mrow and count(*)=3 and" # maybe a bit paranoid checking the structure... + - " *[1][self::m:mn] and *[2][.='\u2062'] and BaseNode(*[3])[contains(@data-intent-property, ':unit')] ) ] and" + - "BaseNode(*[2])[contains(@data-intent-property, ':unit') or (contains(@data-intent-property, ':unit') and .='t')] " + replace: + - x: "*[1]" + - T: "per" # phrase('5 meters 'per' second) + - test: + if: "*[2]='t'" + then: [T: "time"] + else: + - x: "*[2]" + diff --git a/PythonScripts/audit_translations/tests/test_auditor.py b/PythonScripts/audit_translations/tests/test_auditor.py index d757f5b4..f7543f2f 100644 --- a/PythonScripts/audit_translations/tests/test_auditor.py +++ b/PythonScripts/audit_translations/tests/test_auditor.py @@ -349,6 +349,27 @@ def test_missing_else_block_is_still_reported() -> None: assert issue["issue_line_tr"] == 1 # start of the rule +def test_structure_per_fraction_should_anchor_to_replace_lines_expected_behavior() -> None: + """ + Expected behavior: structure differences should point to the `replace:` line. + + This uses real `per-fraction` rules extracted from + `en/SimpleSpeak_Rules.yaml` and `nb/SimpleSpeak_Rules.yaml`. + In both fixtures, `replace:` is on line 8. + """ + base_dir = Path(__file__).parent + path = base_dir / "fixtures" / "repro" + result = compare_files(str(path / "en" / "per_fraction.yaml"), str(path / "nb" / "per_fraction.yaml")) + + issues = collect_issues(result, "per_fraction.yaml", "nb") + structure_issues = [i for i in issues if i["diff_type"] == "structure"] + + assert len(structure_issues) == 1 + issue = structure_issues[0] + assert issue["issue_line_en"] == 8 + assert issue["issue_line_tr"] == 8 + + def test_print_warnings_skips_misaligned_structures() -> None: """ Test that print_warnings doesn't display misaligned structure differences. From d3f26801bf93d95f4d8d8a1be716a7a626dca953 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20Gro=C3=9F?= Date: Wed, 18 Feb 2026 00:31:12 +0100 Subject: [PATCH 2/3] fix line reporting by showing latest shared token --- PythonScripts/audit_translations/auditor.py | 114 +++++++++++++----- .../tests/golden/jsonl/de.json | 8 +- .../golden/rich/cli_calculus_verbose.golden | 2 +- .../rich/structure_diff_nonverbose.golden | 2 +- .../golden/rich/structure_diff_verbose.golden | 2 +- .../audit_translations/tests/test_auditor.py | 53 +++++++- 6 files changed, 141 insertions(+), 40 deletions(-) diff --git a/PythonScripts/audit_translations/auditor.py b/PythonScripts/audit_translations/auditor.py index b26a71fa..603e5988 100644 --- a/PythonScripts/audit_translations/auditor.py +++ b/PythonScripts/audit_translations/auditor.py @@ -291,11 +291,90 @@ def resolve_issue_line(rule: RuleInfo, kind: str, token: Optional[str] = None) - return lines[0] if lines else rule.line_number +def structure_token_occurrence_index(tokens: List[str], position: int) -> Optional[int]: + """ + Return which occurrence of a token appears at a given absolute token position. + + Example: for ["test:", "if:", "test:"], position 2 returns 1. + """ + if position < 0 or position >= len(tokens): + return None + token = tokens[position] + return sum(1 for current in tokens[:position] if current == token) + + +def resolve_structure_issue_lines(diff: RuleDifference) -> Optional[Tuple[int, int]]: + """ + Resolve stable line anchors for a structural rule difference. + + Strategy: + - Skip truly misaligned token substitutions (both tokens present and different). + - Use position-aware token occurrence matching when possible. + - For insert/delete cases (one side missing token), anchor to the previous + shared structural token; if unavailable, anchor to `replace:`. + """ + en_tokens = extract_structure_elements(diff.english_rule.data) + tr_tokens = extract_structure_elements(diff.translated_rule.data) + en_token, tr_token, mismatch_pos = first_structure_mismatch(en_tokens, tr_tokens) + + if mismatch_pos < 0: + return None + + # Structural token streams diverged semantically; we currently suppress these + # to avoid reporting confusing line mappings. + if en_token is not None and tr_token is not None and en_token != tr_token: + return None + + # Insertion/deletion: anchor to the previous shared token if possible. + if en_token is None or tr_token is None: + anchor_pos = mismatch_pos - 1 + if ( + anchor_pos >= 0 + and anchor_pos < len(en_tokens) + and anchor_pos < len(tr_tokens) + and en_tokens[anchor_pos] == tr_tokens[anchor_pos] + ): + anchor_token = en_tokens[anchor_pos] + en_occ = structure_token_occurrence_index(en_tokens, anchor_pos) + tr_occ = structure_token_occurrence_index(tr_tokens, anchor_pos) + if en_occ is not None and tr_occ is not None: + line_en = resolve_issue_line_at_position(diff.english_rule, "structure", anchor_token, en_occ) + line_tr = resolve_issue_line_at_position(diff.translated_rule, "structure", anchor_token, tr_occ) + if line_en is not None and line_tr is not None: + return line_en, line_tr + + # Fallback: anchor both sides to replace, which is the rule body entrypoint. + line_en = resolve_issue_line(diff.english_rule, "structure", "replace:") or diff.english_rule.line_number + line_tr = resolve_issue_line(diff.translated_rule, "structure", "replace:") or diff.translated_rule.line_number + return line_en, line_tr + + # Exact token available on both sides: resolve by occurrence index at mismatch. + en_occ = structure_token_occurrence_index(en_tokens, mismatch_pos) + tr_occ = structure_token_occurrence_index(tr_tokens, mismatch_pos) + if en_occ is not None and tr_occ is not None: + line_en = resolve_issue_line_at_position(diff.english_rule, "structure", en_token, en_occ) + line_tr = resolve_issue_line_at_position(diff.translated_rule, "structure", tr_token, tr_occ) + if line_en is not None and line_tr is not None: + return line_en, line_tr + + line_en = resolve_issue_line(diff.english_rule, "structure", en_token) + line_tr = resolve_issue_line(diff.translated_rule, "structure", tr_token) + if line_en is None or line_tr is None: + return None + return line_en, line_tr + + def collect_issues( result: ComparisonResult, file_name: str, language: str, ) -> List[dict]: + """ + Flatten a ComparisonResult into one normalized dictionary per issue. + + This is the canonical bridge from parser/diff objects to serializable + records consumed by JSONL output, snapshot tests, and line-level assertions. + """ issues = [] for rule in result.missing_rules: @@ -345,23 +424,10 @@ def collect_issues( rule = diff.english_rule issue = issue_base(rule, file_name, language) if diff.diff_type == "structure": - en_tokens = extract_structure_elements(diff.english_rule.data) - tr_tokens = extract_structure_elements(diff.translated_rule.data) - en_token, tr_token, mismatch_pos = first_structure_mismatch(en_tokens, tr_tokens) - - # Skip reporting when tokens are misaligned (both exist but differ) - # This avoids misleading line numbers when entire blocks are missing/added - # We only report when one is None (clear case of missing element) - if en_token is not None and tr_token is not None and en_token != tr_token: - continue - - issue_line_en = resolve_issue_line(diff.english_rule, "structure", en_token) - issue_line_tr = resolve_issue_line(diff.translated_rule, "structure", tr_token) - - # Skip reporting structure differences where we can't find both tokens - # This avoids misleading line numbers when blocks are missing - if issue_line_en is None or issue_line_tr is None: + structure_lines = resolve_structure_issue_lines(diff) + if structure_lines is None: continue + issue_line_en, issue_line_tr = structure_lines else: issue_line_en = resolve_issue_line(diff.english_rule, diff.diff_type) issue_line_tr = resolve_issue_line(diff.translated_rule, diff.diff_type) @@ -438,20 +504,10 @@ def add_issue(rule: RuleInfo, issue_type: str, payload: Dict[str, Any]) -> None: for diff in result.rule_differences: if diff.diff_type == "structure": - en_tokens = extract_structure_elements(diff.english_rule.data) - tr_tokens = extract_structure_elements(diff.translated_rule.data) - en_token, tr_token, mismatch_pos = first_structure_mismatch(en_tokens, tr_tokens) - - # Skip reporting when tokens are misaligned (both exist but differ) - # This avoids misleading line numbers when entire blocks are missing/added - if en_token is not None and tr_token is not None and en_token != tr_token: - continue - - line_en = resolve_issue_line(diff.english_rule, "structure", en_token) - line_tr = resolve_issue_line(diff.translated_rule, "structure", tr_token) - # Skip structure diffs where we can't find both tokens - if line_en is None or line_tr is None: + structure_lines = resolve_structure_issue_lines(diff) + if structure_lines is None: continue + line_en, line_tr = structure_lines else: line_en = resolve_issue_line(diff.english_rule, diff.diff_type) line_tr = resolve_issue_line(diff.translated_rule, diff.diff_type) diff --git a/PythonScripts/audit_translations/tests/golden/jsonl/de.json b/PythonScripts/audit_translations/tests/golden/jsonl/de.json index 9ac62086..a2a1a43a 100644 --- a/PythonScripts/audit_translations/tests/golden/jsonl/de.json +++ b/PythonScripts/audit_translations/tests/golden/jsonl/de.json @@ -115,8 +115,8 @@ "rule_name": "struct-rule", "rule_tag": "mi", "rule_key": "struct-rule|mi", - "issue_line_en": 9, - "issue_line_tr": 1, + "issue_line_en": 7, + "issue_line_tr": 7, "rule_line_en": 1, "rule_line_tr": 1, "issue_type": "rule_difference", @@ -169,8 +169,8 @@ "rule_name": "missing-else-block", "rule_tag": "root", "rule_key": "missing-else-block|root", - "issue_line_en": 8, - "issue_line_tr": 1, + "issue_line_en": 7, + "issue_line_tr": 7, "rule_line_en": 1, "rule_line_tr": 1, "issue_type": "rule_difference", diff --git a/PythonScripts/audit_translations/tests/golden/rich/cli_calculus_verbose.golden b/PythonScripts/audit_translations/tests/golden/rich/cli_calculus_verbose.golden index 949f58c4..40f38185 100644 --- a/PythonScripts/audit_translations/tests/golden/rich/cli_calculus_verbose.golden +++ b/PythonScripts/audit_translations/tests/golden/rich/cli_calculus_verbose.golden @@ -43,7 +43,7 @@ en: $Verbosity!='Terse', not(IsNode(*[1], 'leaf')) tr: not(IsNode(*[1], 'leaf')) Structure Differences [1] - • (line 38 en, 18 tr) + • (line 40 en, 25 tr) Rule structure differs (test/if/then/else blocks) en: replace: test: if: then: test: if: then: tr: replace: test: if: then: diff --git a/PythonScripts/audit_translations/tests/golden/rich/structure_diff_nonverbose.golden b/PythonScripts/audit_translations/tests/golden/rich/structure_diff_nonverbose.golden index 314ad234..868bf007 100644 --- a/PythonScripts/audit_translations/tests/golden/rich/structure_diff_nonverbose.golden +++ b/PythonScripts/audit_translations/tests/golden/rich/structure_diff_nonverbose.golden @@ -7,5 +7,5 @@ ≠ Rule Issues [1] (grouped by rule and issue type) • struct-rule (mi) Structure Differences [1] - • (line 9 en, 1 tr) + • (line 7 en, 7 tr) Rule structure differs (test/if/then/else blocks) diff --git a/PythonScripts/audit_translations/tests/golden/rich/structure_diff_verbose.golden b/PythonScripts/audit_translations/tests/golden/rich/structure_diff_verbose.golden index ec624426..bde66a07 100644 --- a/PythonScripts/audit_translations/tests/golden/rich/structure_diff_verbose.golden +++ b/PythonScripts/audit_translations/tests/golden/rich/structure_diff_verbose.golden @@ -7,7 +7,7 @@ ≠ Rule Issues [1] (grouped by rule and issue type) • struct-rule (mi) Structure Differences [1] - • (line 9 en, 1 tr) + • (line 7 en, 7 tr) Rule structure differs (test/if/then/else blocks) en: replace: test: if: then: else: tr: replace: test: if: then: diff --git a/PythonScripts/audit_translations/tests/test_auditor.py b/PythonScripts/audit_translations/tests/test_auditor.py index f7543f2f..44d2c7e0 100644 --- a/PythonScripts/audit_translations/tests/test_auditor.py +++ b/PythonScripts/audit_translations/tests/test_auditor.py @@ -342,11 +342,56 @@ def test_missing_else_block_is_still_reported() -> None: f"but found {len(structure_issues)} structure issues" ) - # Verify the issue has proper line numbers + # Verify the issue anchors to the last shared structure token ('then:') issue = structure_issues[0] - assert issue["issue_line_en"] is not None - # When else: doesn't exist in translation, we fall back to the rule line number - assert issue["issue_line_tr"] == 1 # start of the rule + assert issue["issue_line_en"] == 7 + assert issue["issue_line_tr"] == 7 + + +def test_structure_diff_uses_position_aware_token_occurrence_for_missing_block(tmp_path) -> None: + """ + Structure diffs with repeated tokens should anchor at the divergence point. + + English contains an additional second `test` block. The first mismatch is + after the first `then:`, so both sides should anchor to that shared `then:` + line (not to the first `test:` line or the top of the rule). + """ + english_file = tmp_path / "en.yaml" + translated_file = tmp_path / "tr.yaml" + english_file.write_text( + """- name: repeated-structure + tag: root + match: "." + replace: + - test: + if: a + then: [T: "x"] + - test: + if: b + then: [T: "y"] +""", + encoding="utf-8", + ) + translated_file.write_text( + """- name: repeated-structure + tag: root + match: "." + replace: + - test: + if: a + then: [T: "x"] +""", + encoding="utf-8", + ) + + result = compare_files(str(english_file), str(translated_file)) + issues = collect_issues(result, "repeated-structure.yaml", "tr") + structure_issues = [i for i in issues if i["diff_type"] == "structure"] + + assert len(structure_issues) == 1 + issue = structure_issues[0] + assert issue["issue_line_en"] == 7 + assert issue["issue_line_tr"] == 7 def test_structure_per_fraction_should_anchor_to_replace_lines_expected_behavior() -> None: From 87cf5b34d2ca9231b5978676e4d7f10b4347303c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20Gro=C3=9F?= Date: Wed, 18 Feb 2026 00:57:50 +0100 Subject: [PATCH 3/3] report structure mismatches with different tokens --- PythonScripts/audit_translations/auditor.py | 6 -- .../tests/golden/jsonl/de.json | 20 ++++- .../audit_translations/tests/test_auditor.py | 79 ++++++++++++++----- 3 files changed, 77 insertions(+), 28 deletions(-) diff --git a/PythonScripts/audit_translations/auditor.py b/PythonScripts/audit_translations/auditor.py index 603e5988..9b431863 100644 --- a/PythonScripts/audit_translations/auditor.py +++ b/PythonScripts/audit_translations/auditor.py @@ -308,7 +308,6 @@ def resolve_structure_issue_lines(diff: RuleDifference) -> Optional[Tuple[int, i Resolve stable line anchors for a structural rule difference. Strategy: - - Skip truly misaligned token substitutions (both tokens present and different). - Use position-aware token occurrence matching when possible. - For insert/delete cases (one side missing token), anchor to the previous shared structural token; if unavailable, anchor to `replace:`. @@ -320,11 +319,6 @@ def resolve_structure_issue_lines(diff: RuleDifference) -> Optional[Tuple[int, i if mismatch_pos < 0: return None - # Structural token streams diverged semantically; we currently suppress these - # to avoid reporting confusing line mappings. - if en_token is not None and tr_token is not None and en_token != tr_token: - return None - # Insertion/deletion: anchor to the previous shared token if possible. if en_token is None or tr_token is None: anchor_pos = mismatch_pos - 1 diff --git a/PythonScripts/audit_translations/tests/golden/jsonl/de.json b/PythonScripts/audit_translations/tests/golden/jsonl/de.json index a2a1a43a..17cb6a0e 100644 --- a/PythonScripts/audit_translations/tests/golden/jsonl/de.json +++ b/PythonScripts/audit_translations/tests/golden/jsonl/de.json @@ -161,7 +161,25 @@ "english_snippet": "$Verbosity!='Terse', $Setting = 'Value', parent::m:minus, *[2][.='2']", "translated_snippet": "$Setting = 'Value', parent::m:minus, *[2][.='2']", "untranslated_texts": [], - "_explanation": "structure_misaligned.yaml: English has extra test block causing misalignment. Fix filters out misleading structure differences but reports condition difference." + "_explanation": "structure_misaligned.yaml: condition difference remains reported." + }, + { + "language": "de", + "file": "structure_misaligned.yaml", + "rule_name": "misaligned-structure", + "rule_tag": "root", + "rule_key": "misaligned-structure|root", + "issue_line_en": 11, + "issue_line_tr": 11, + "rule_line_en": 1, + "rule_line_tr": 1, + "issue_type": "rule_difference", + "diff_type": "structure", + "description": "Rule structure differs (test/if/then/else blocks)", + "english_snippet": "replace: test: if: then: test: if: then: test: if: then: else: test: if: then: else:", + "translated_snippet": "replace: test: if: then: test: if: then: else: test: if: then: else:", + "untranslated_texts": [], + "_explanation": "structure_misaligned.yaml: structure substitutions/realignments are now reported with position-aware anchors." }, { "language": "de", diff --git a/PythonScripts/audit_translations/tests/test_auditor.py b/PythonScripts/audit_translations/tests/test_auditor.py index 44d2c7e0..e416cd29 100644 --- a/PythonScripts/audit_translations/tests/test_auditor.py +++ b/PythonScripts/audit_translations/tests/test_auditor.py @@ -275,13 +275,13 @@ def test_print_warnings_includes_snippets_when_verbose(fixed_console_width) -> N assert output == golden_path.read_text(encoding="utf-8") -def test_misaligned_structure_differences_are_skipped() -> None: +def test_misaligned_structure_differences_are_reported() -> None: """ - Test that structure differences with misaligned tokens are skipped. + Test that structure differences with misaligned tokens are reported. When English has a "test" block that Norwegian doesn't have (and vice versa), - the structural tokens become misaligned. The fix skips reporting these - to avoid showing confusing line numbers. + the structural tokens become misaligned. We still report this as a structure + issue and anchor it to the divergence position. """ base_dir = Path(__file__).parent fixtures_dir = base_dir / "fixtures" @@ -295,16 +295,14 @@ def test_misaligned_structure_differences_are_skipped() -> None: assert len(result.rule_differences) > 0 assert any(diff.diff_type == "structure" for diff in result.rule_differences) - # But when collecting issues, misaligned structure diffs should be filtered out + # Collecting issues should include a structure issue. issues = collect_issues(result, "structure_misaligned.yaml", "de") structure_issues = [i for i in issues if i["diff_type"] == "structure"] - # CRITICAL: Before the fix, this would have structure issues with misleading line numbers - # After the fix, misaligned structures are skipped, so we should have 0 structure issues - assert len(structure_issues) == 0, ( - "Expected misaligned structure differences to be filtered out, " - f"but found {len(structure_issues)} structure issues" - ) + assert len(structure_issues) == 1 + issue = structure_issues[0] + assert issue["issue_line_en"] == 11 + assert issue["issue_line_tr"] == 11 # Other differences (like conditions) should still be reported condition_issues = [i for i in issues if i["diff_type"] == "condition"] @@ -394,6 +392,46 @@ def test_structure_diff_uses_position_aware_token_occurrence_for_missing_block(t assert issue["issue_line_tr"] == 7 +def test_structure_substitution_diff_is_reported(tmp_path) -> None: + """ + Structural token substitutions should be reported as structure issues. + """ + english_file = tmp_path / "en.yaml" + translated_file = tmp_path / "tr.yaml" + english_file.write_text( + """- name: substitution-structure + tag: root + match: "." + replace: + - test: + if: a + then: [T: "x"] +""", + encoding="utf-8", + ) + translated_file.write_text( + """- name: substitution-structure + tag: root + match: "." + replace: + - test: + if: a + else: [T: "x"] +""", + encoding="utf-8", + ) + + result = compare_files(str(english_file), str(translated_file)) + assert any(diff.diff_type == "structure" for diff in result.rule_differences) + + issues = collect_issues(result, "substitution-structure.yaml", "tr") + structure_issues = [i for i in issues if i["diff_type"] == "structure"] + assert len(structure_issues) == 1 + issue = structure_issues[0] + assert issue["issue_line_en"] == 7 + assert issue["issue_line_tr"] == 7 + + def test_structure_per_fraction_should_anchor_to_replace_lines_expected_behavior() -> None: """ Expected behavior: structure differences should point to the `replace:` line. @@ -415,9 +453,9 @@ def test_structure_per_fraction_should_anchor_to_replace_lines_expected_behavior assert issue["issue_line_tr"] == 8 -def test_print_warnings_skips_misaligned_structures() -> None: +def test_print_warnings_shows_misaligned_structures() -> None: """ - Test that print_warnings doesn't display misaligned structure differences. + Test that print_warnings displays misaligned structure differences. """ base_dir = Path(__file__).parent fixtures_dir = base_dir / "fixtures" @@ -435,17 +473,16 @@ def test_print_warnings_skips_misaligned_structures() -> None: issues_count = print_warnings(result, "structure_misaligned.yaml", verbose=False) output = capture.get() - # CRITICAL: The output should not contain "Rule structure differs" - # because misaligned structures are filtered during display - assert "Rule structure differs" not in output, ( - "Expected misaligned structure differences to be filtered from display" + # Misaligned structure differences should be rendered. + assert "Rule structure differs" in output, ( + "Expected misaligned structure differences to be shown in display" ) - # The issues count should not include filtered structure differences - # It should only count the condition differences + # The issues count should include both condition + structure differences. condition_diffs = [diff for diff in result.rule_differences if diff.diff_type == "condition"] - assert issues_count == len(condition_diffs), ( - f"Expected issues_count ({issues_count}) to equal condition_diffs ({len(condition_diffs)})" + structure_diffs = [diff for diff in result.rule_differences if diff.diff_type == "structure"] + assert issues_count == len(condition_diffs) + len(structure_diffs), ( + f"Expected issues_count ({issues_count}) to include condition+structure diffs" )