diff --git a/CHANGELOG.md b/CHANGELOG.md index cef0acda6..e56f422a6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,8 @@ this file. The format follows Keep a Changelog, and versioned releases follow Semantic Versioning where the repository publishes a release. ## [Unreleased] +- Review summaries now report test and documentation coverage separately and + consistently. Open the summary to confirm both values before merging. - Route Strix cross-provider fallbacks to explicit direct-OpenAI models (`openai-direct/...`) through the OpenAI inference endpoint instead of inheriting a provider-specific primary base: the workflow now provisions diff --git a/scripts/ci/opencode_review_normalize_output.py b/scripts/ci/opencode_review_normalize_output.py index 6e07c29d0..615239a3e 100755 --- a/scripts/ci/opencode_review_normalize_output.py +++ b/scripts/ci/opencode_review_normalize_output.py @@ -133,9 +133,14 @@ "security/privacy:", ) -APPROVAL_VERIFICATION_PATTERNS = { - label: re.compile(re.escape(label)) for label in APPROVAL_VERIFICATION_LABELS -} +# Match the longest label first so a future label cannot shadow a more specific +# label that starts at the same position (for example, docstring coverage:). +ANY_LABEL_PATTERN = re.compile( + "|".join( + re.escape(label) + for label in sorted(APPROVAL_VERIFICATION_LABELS, key=len, reverse=True) + ) +) SOURCE_LIKE_CHANGED_FILE_EXTENSIONS = frozenset( { @@ -959,32 +964,22 @@ def mentions_verification_posture(reason: str, summary: str) -> bool: def label_section(text: str, label: str) -> str: """Return text after a verification label until the next known label.""" - def label_starts(candidate: str) -> list[int]: - """Return exact verification-label starts without suffix collisions.""" - starts = [] - pattern = APPROVAL_VERIFICATION_PATTERNS.get(candidate) - if pattern is None: - pattern = re.compile(re.escape(candidate)) - for match in pattern.finditer(text): - index = match.start() - if ( - candidate == "coverage:" - and text[max(0, index - 10) : index] == "docstring " - ): - continue - starts.append(index) - return starts - - starts = label_starts(label) + actual_matches = [ + (match.start(), match.group(0)) for match in ANY_LABEL_PATTERN.finditer(text) + ] + if label in APPROVAL_VERIFICATION_LABELS: + starts = [index for index, candidate in actual_matches if candidate == label] + else: + # Preserve repository-specific labels without rescanning catalogue labels. + starts = [match.start() for match in re.finditer(re.escape(label), text)] if not starts: return "" + start = starts[-1] + len(label) next_starts = [ - candidate_start - for candidate in APPROVAL_VERIFICATION_LABELS - if candidate != label - for candidate_start in label_starts(candidate) - if candidate_start >= start + index + for index, candidate in actual_matches + if candidate != label and index >= start ] end = min(next_starts) if next_starts else len(text) return text[start:end] diff --git a/tests/test_opencode_review_normalize_output.py b/tests/test_opencode_review_normalize_output.py index a24c54174..6b528198e 100644 --- a/tests/test_opencode_review_normalize_output.py +++ b/tests/test_opencode_review_normalize_output.py @@ -1386,6 +1386,7 @@ def test_label_and_full_coverage_detection(tmp_path, monkeypatch): assert not norm.mentions_full_coverage("", no_source_summary) assert norm.contradicts_changed_file_kinds("", no_source_summary) + changed_files = tmp_path / "opencode-changed-files.txt" changed_files.write_text("README.md\n", encoding="utf-8") monkeypatch.setenv("OPENCODE_CHANGED_FILES_FILE", str(changed_files)) @@ -1434,6 +1435,26 @@ def test_label_and_full_coverage_detection(tmp_path, monkeypatch): ) +def test_label_section_scans_labels_once_and_prefers_longest_label(): + """Parse adjacent labels without rescanning the input for every label.""" + text = "docstring coverage: 100% coverage: 98% performance: measured" + + assert norm.label_section(text, "docstring coverage:") == " 100% " + assert norm.label_section(text, "coverage:") == " 98% " + assert norm.label_section(text, "performance:") == " measured" + alternatives = norm.ANY_LABEL_PATTERN.pattern.split("|") + assert alternatives.index(re.escape("docstring coverage:")) < alternatives.index( + re.escape("coverage:") + ) + + +def test_label_section_preserves_custom_label_lookup(): + """Repository-specific labels retain the helper's backwards-compatible path.""" + text = "custom evidence: measured coverage: 98%" + + assert norm.label_section(text, "custom evidence:") == " measured " + + def test_check_structural_approval_rejects_invalid_or_unsafe_approvals( tmp_path, monkeypatch ):