diff --git a/scripts/ci/opencode_review_normalize_output.py b/scripts/ci/opencode_review_normalize_output.py index a7f235e265..dacd9e4606 100755 --- a/scripts/ci/opencode_review_normalize_output.py +++ b/scripts/ci/opencode_review_normalize_output.py @@ -669,13 +669,25 @@ def valid_control( } +def extract_dicts(obj: Any) -> list[Any]: + """Recursively extract all dictionaries from a JSON-like object.""" + results = [] + if isinstance(obj, dict): + results.append(obj) + for v in obj.values(): + results.extend(extract_dicts(v)) + elif isinstance(obj, list): + for item in obj: + results.extend(extract_dicts(item)) + return results + 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] = [] try: - values.append(json.loads(text)) + return extract_dicts(json.loads(text)) except json.JSONDecodeError: # OpenCode exports may contain prose around the JSON control object. pass @@ -693,7 +705,7 @@ def iter_json_objects(text: str) -> list[Any]: continue try: value, new_index = decoder.raw_decode(text, index) - values.append(value) + values.extend(extract_dicts(value)) # ⚡ Bolt: Advance index to avoid O(N^2) redundant parsing of nested JSON blocks index = new_index continue diff --git a/scripts/ci/strix_quick_gate.sh b/scripts/ci/strix_quick_gate.sh index d9a5b30765..0dcbc62406 100755 --- a/scripts/ci/strix_quick_gate.sh +++ b/scripts/ci/strix_quick_gate.sh @@ -1881,6 +1881,10 @@ evaluate_pull_request_findings() { continue fi found_any_vuln_file=1 + if vulnerability_file_is_retryable_model_inconsistency "$vuln_file"; then + found_retryable_model_inconsistency=1 + continue + fi rank="$(extract_first_severity_rank "$vuln_file")" if [ "$rank" -lt 0 ]; then PR_FINDINGS_DECISION="block_unmapped" @@ -1890,10 +1894,6 @@ evaluate_pull_request_findings() { if [ "$rank" -lt "$threshold_rank" ]; then continue fi - if vulnerability_file_is_retryable_model_inconsistency "$vuln_file"; then - found_retryable_model_inconsistency=1 - continue - fi mapfile -t vulnerability_locations < <(extract_vulnerability_locations "$vuln_file") if [ "${#vulnerability_locations[@]}" -eq 0 ]; then PR_FINDINGS_DECISION="block_unmapped" diff --git a/scripts/ci/test_strix_quick_gate.sh b/scripts/ci/test_strix_quick_gate.sh index 87668acf00..dda9b76744 100755 --- a/scripts/ci/test_strix_quick_gate.sh +++ b/scripts/ci/test_strix_quick_gate.sh @@ -998,7 +998,7 @@ assert_pr_review_merge_scheduler_uses_github_actions_bot_token() { assert_file_contains "$scheduler_file" "same-head OpenCode dispatched" "scheduler records review dispatch after completed security evidence" assert_file_contains "$workflow_file" "--pr-number" "scheduler scopes required-workflow PR events to the current pull request" assert_file_contains "$workflow_file" "--review-workflow \"Required OpenCode Review\"" "scheduler dispatches the canonical required OpenCode Review workflow" - assert_file_contains "$readme_file" "github-actions[bot]" "README documents that mechanical branch updates and merges are attributed to GitHub Actions bot" + assert_file_contains "$readme_file" "PR_REVIEW_MERGE_TOKEN" "README documents that mechanical branch updates and merges use the central mutation credential" assert_file_contains "$fix_workflow_file" 'workflow_call:' "fix scheduler can run as the central reusable autofix-dispatch workflow" assert_file_contains "$fix_workflow_file" 'repository: ContextualWisdomLab/.github' "fix scheduler checks out the canonical implementation instead of relying on repo-local scheduler code" assert_file_contains "$fix_workflow_file" 'GH_TOKEN: ${{ github.token }}' "fix scheduler uses the caller repository workflow token for dispatch markers" diff --git a/tests/test_opencode_review_normalize_output.py b/tests/test_opencode_review_normalize_output.py index 3473299064..246e1f8a71 100644 --- a/tests/test_opencode_review_normalize_output.py +++ b/tests/test_opencode_review_normalize_output.py @@ -679,10 +679,11 @@ def raise_for_evidence(path, *args, **kwargs): def test_iter_json_objects_extracts_raw_and_embedded_json(): - assert norm.iter_json_objects('{"a": 1}') == [{"a": 1}, {"a": 1}] + assert norm.iter_json_objects('{"a": 1}') == [{"a": 1}] assert norm.iter_json_objects('prefix {"b": 2} suffix') == [{"b": 2}] - assert norm.iter_json_objects('prefix {"outer": {"inner": 1}} suffix') == [ - {"outer": {"inner": 1}} + assert norm.iter_json_objects('prefix {"wrapper": {"control": true}} suffix') == [ + {"wrapper": {"control": True}}, + {"control": True}, ] assert norm.iter_json_objects("prefix { } suffix") == [{}] assert norm.iter_json_objects("prefix {not json}") == []