Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions .github/workflows/opencode-review.yml
Original file line number Diff line number Diff line change
Expand Up @@ -487,6 +487,11 @@ jobs:
<opencode-evidence>
$(sed -n '1,900p' "$OPENCODE_EVIDENCE_FILE")
</opencode-evidence>
Use CodeGraph for blast-radius, call graph, and test-coverage questions before broad local reads.
Prefer deletion, stdlib/native platform features, and already-installed dependencies before proposing new code or packages, but do not simplify away trust-boundary validation, data-loss handling, security, accessibility, or required tests.
For Korean prose, preserve facts, identifiers, numbers, and quotes while removing only formulaic filler or translationese.
When Strix evidence supports it, name the concrete CWE/KISA-style class such as injection, auth/authz, secrets, crypto, path traversal/file upload, XSS/CSRF/SSRF, error disclosure, or debug/deployment config; do not invent a category without evidence.
Before APPROVE, the summary must include at least one exact changed file path inspected as changed-file evidence. Never approve with a reason or summary that says no changes, no files, or no actionable changes were found when bounded evidence lists changed files; that control block is invalid.
First line exactly:
<!-- opencode-review-gate head_sha=${HEAD_SHA} run_id=${RUN_ID} run_attempt=${RUN_ATTEMPT} -->
Then exactly one control block:
Expand Down Expand Up @@ -576,6 +581,11 @@ jobs:
<opencode-evidence>
$(sed -n '1,900p' "$OPENCODE_EVIDENCE_FILE")
</opencode-evidence>
Use CodeGraph for blast-radius, call graph, and test-coverage questions before broad local reads.
Prefer deletion, stdlib/native platform features, and already-installed dependencies before proposing new code or packages, but do not simplify away trust-boundary validation, data-loss handling, security, accessibility, or required tests.
For Korean prose, preserve facts, identifiers, numbers, and quotes while removing only formulaic filler or translationese.
When Strix evidence supports it, name the concrete CWE/KISA-style class such as injection, auth/authz, secrets, crypto, path traversal/file upload, XSS/CSRF/SSRF, error disclosure, or debug/deployment config; do not invent a category without evidence.
Before APPROVE, the summary must include at least one exact changed file path inspected as changed-file evidence. Never approve with a reason or summary that says no changes, no files, or no actionable changes were found when bounded evidence lists changed files; that control block is invalid.
First line exactly:
<!-- opencode-review-gate head_sha=${HEAD_SHA} run_id=${RUN_ID} run_attempt=${RUN_ATTEMPT} -->
Then exactly one control block:
Expand Down Expand Up @@ -665,6 +675,11 @@ jobs:
<opencode-evidence>
$(sed -n '1,900p' "$OPENCODE_EVIDENCE_FILE")
</opencode-evidence>
Use CodeGraph for blast-radius, call graph, and test-coverage questions before broad local reads.
Prefer deletion, stdlib/native platform features, and already-installed dependencies before proposing new code or packages, but do not simplify away trust-boundary validation, data-loss handling, security, accessibility, or required tests.
For Korean prose, preserve facts, identifiers, numbers, and quotes while removing only formulaic filler or translationese.
When Strix evidence supports it, name the concrete CWE/KISA-style class such as injection, auth/authz, secrets, crypto, path traversal/file upload, XSS/CSRF/SSRF, error disclosure, or debug/deployment config; do not invent a category without evidence.
Before APPROVE, the summary must include at least one exact changed file path inspected as changed-file evidence. Never approve with a reason or summary that says no changes, no files, or no actionable changes were found when bounded evidence lists changed files; that control block is invalid.
First line exactly:
<!-- opencode-review-gate head_sha=${HEAD_SHA} run_id=${RUN_ID} run_attempt=${RUN_ATTEMPT} -->
Then exactly one control block:
Expand Down
135 changes: 131 additions & 4 deletions scripts/ci/opencode_review_normalize_output.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,130 @@
from __future__ import annotations

import json
import re
import sys
from pathlib import Path
from typing import Any


STRUCTURAL_FAILURE_PHRASES = (
"structural exploration was not possible",
"structural exploration not possible",
"structural exploration is not required",
"structural exploration not required",
"structural analysis is not required",
"structural analysis not required",
"structural review is not required",
"structural review not required",
"no structural exploration required",
"no structural analysis required",
"no structural review required",
"structural exploration is unnecessary",
"structural analysis is unnecessary",
"structural review is unnecessary",
"changed files could not be inspected",
"source files could not be inspected",
"required files could not be inspected",
"could not access changed files",
"could not access the changed files",
"could not access source files",
"could not access the source files",
"could not access required files",
"could not access required evidence",
"evidence was truncated",
"truncated evidence",
"no changes detected",
"no changes were detected",
"no changes found",
"no changes were found",
"no files or changes were found",
"no files or changes found",
"no actionable changes to review",
"no changes to review",
"no changed files",
)

STRUCTURAL_FAILURE_PATTERNS = (
re.compile(
r"\b(?:could not|cannot|can't|unable to)\s+"
r"(?:inspect|access|review)\s+(?:the\s+)?"
r"(?:changed|source|required)\s+files?\b"
),
re.compile(
r"\b(?:changed|source|required)\s+files?\s+"
r"(?:could not|cannot|can't|were not|was not)\s+"
r"(?:be\s+)?(?:inspected|accessed|reviewed)\b"
),
re.compile(
r"\b(?:structural\s+(?:exploration|analysis|review))\s+"
r"(?:was\s+)?(?:unavailable|incomplete|blocked|not possible)\b"
),
re.compile(
r"\bno\s+(?:files?\s+or\s+)?changes?\s+"
r"(?:were\s+)?(?:detected|found|present)\b"
),
re.compile(r"\bno\s+(?:actionable\s+)?changes?\s+to\s+review\b"),
re.compile(r"\b(?:no|zero)\s+changed\s+files?\b"),
)

CHANGED_FILE_EVIDENCE_PATTERN = re.compile(
r"(?<![A-Za-z0-9_])(?:[A-Za-z0-9_.-]+/)+[A-Za-z0-9_.@+-]+"
r"|(?<![A-Za-z0-9_])[A-Za-z0-9_.-]+\."
r"(?:py|js|jsx|ts|tsx|mjs|cjs|sh|bash|yml|yaml|json|jsonc|toml|lock|md|txt|css|scss|html|sql|go|rs|java|kt|swift|rb|php|cs|xml|ini|cfg)"
r"(?![A-Za-z0-9_])"
r"|(?<![A-Za-z0-9_])(?:Dockerfile|Makefile|README|LICENSE|AGENTS\.md)(?![A-Za-z0-9_])"
)


def admits_missing_structural_review(reason: str, summary: str) -> bool:
"""Return whether an approval admits it did not inspect required structure."""
combined = f"{reason}\n{summary}".casefold()
return any(phrase in combined for phrase in STRUCTURAL_FAILURE_PHRASES) or any(
pattern.search(combined) for pattern in STRUCTURAL_FAILURE_PATTERNS
)


def mentions_changed_file_evidence(reason: str, summary: str) -> bool:
"""Return whether an approval names at least one concrete changed file/path."""
return bool(CHANGED_FILE_EVIDENCE_PATTERN.search(f"{reason}\n{summary}"))


def check_structural_approval(control_file: Path) -> int:
"""Validate an already-normalized control block before publishing approval."""
try:
value = json.loads(control_file.read_text(encoding="utf-8"))
except (OSError, json.JSONDecodeError) as exc:
print(f"cannot read OpenCode control JSON: {exc}", file=sys.stderr)
return 65

if not isinstance(value, dict):
print("NO_CONCLUSION", file=sys.stderr)
return 4

if value.get("result") == "APPROVE" and admits_missing_structural_review(
str(value.get("reason", "")),
str(value.get("summary", "")),
):
print("NO_CONCLUSION", file=sys.stderr)
return 4
if value.get("result") == "APPROVE" and not mentions_changed_file_evidence(
str(value.get("reason", "")),
str(value.get("summary", "")),
):
print("NO_CONCLUSION", file=sys.stderr)
return 4

return 0


def valid_control(
value: Any,
*,
expected_head_sha: str,
expected_run_id: str,
expected_run_attempt: str,
) -> dict[str, Any] | None:
"""Return a normalized control block when it matches the current run."""
if not isinstance(value, dict):
return None

Expand All @@ -34,14 +146,22 @@ def valid_control(
return None
if not isinstance(value.get("summary"), str) or not value["summary"].strip():
return None
reason = value["reason"].strip()
summary = value["summary"].strip()

findings = value.get("findings")
if findings is None and result == "APPROVE":
findings = []
if not isinstance(findings, list):
return None
if result == "APPROVE" and findings:
return None
if result == "REQUEST_CHANGES" and not findings:
return None
if result == "APPROVE" and admits_missing_structural_review(reason, summary):
return None
if result == "APPROVE" and not mentions_changed_file_evidence(reason, summary):
return None

required_finding_fields = (
"path",
Expand All @@ -56,7 +176,8 @@ def valid_control(
for finding in findings:
if not isinstance(finding, dict):
return None
if not isinstance(finding.get("line"), int) or finding["line"] <= 0:
line = finding.get("line")
if isinstance(line, bool) or not isinstance(line, int) or line <= 0:
return None
for field in required_finding_fields:
if not isinstance(finding.get(field), str) or not finding[field].strip():
Expand All @@ -67,13 +188,14 @@ def valid_control(
"run_id": value["run_id"],
"run_attempt": value["run_attempt"],
"result": result,
"reason": value["reason"],
"summary": value["summary"],
"reason": reason,
"summary": summary,
"findings": findings,
}


def iter_json_objects(text: str) -> list[Any]:
"""Extract JSON objects from raw OpenCode output that may include prose."""
decoder = json.JSONDecoder()
values: list[Any] = []

Expand All @@ -96,10 +218,15 @@ def iter_json_objects(text: str) -> list[Any]:


def main(argv: list[str]) -> int:
"""Run the normalizer CLI and write the publishable control block."""
if len(argv) == 3 and argv[1] == "--check-structural-approval":
return check_structural_approval(Path(argv[2]))

if len(argv) != 5:
print(
"usage: opencode_review_normalize_output.py "
"<expected_head_sha> <expected_run_id> <expected_run_attempt> <output_file>",
"<expected_head_sha> <expected_run_id> <expected_run_attempt> <output_file>\n"
" or: opencode_review_normalize_output.py --check-structural-approval <control_json_file>",
file=sys.stderr,
)
return 64
Expand Down
Loading