diff --git a/.gitignore b/.gitignore index 5292b38..0556aae 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,7 @@ __pycache__/ .env.* .ruff_cache/ .pytest_cache/ +.mypy_cache/ .coverage .coverage.* htmlcov/ diff --git a/aci.self-scan-operations.toml b/aci.self-scan-operations.toml new file mode 100644 index 0000000..d1695c0 --- /dev/null +++ b/aci.self-scan-operations.toml @@ -0,0 +1,16 @@ +# ACI baseline -- generated from a scan report by `aci emit-baseline`. +# Each entry accepts a finding as pre-existing; future scans report only +# NEW findings. Identity is the fingerprint (stable across line shifts), so +# no line numbers are stored here. Remove an entry once its finding is +# fixed -- ACI then reports it as resolved on the next scan. +[baseline] +entries = [ + # _iter_pep621_dependency_specs: depth-5 nesting mirrors the literal depth of + # the PEP 621 TOML schema (project → optional-dependencies → group → item). + # Splitting the traversal across functions would fragment a single-purpose parse. + { fingerprint = "edcbbddad0c4eb15", ci_id = "CI-02", target_file = "detectors/ci_14.py" }, + # _collect_tainted_names: fixpoint dataflow over an AST walk has an irreducible + # structure: while-convergence × for-walk × if-dispatch × for-targets. + # The nesting is the algorithm — not incidental tangling. + { fingerprint = "fea6bd94c9a930d5", ci_id = "CI-02", target_file = "detectors/ci_14_taint.py" }, +] diff --git a/shared/python/aci_github_summary.py b/shared/python/aci_github_summary.py index 3726f7d..2902f01 100644 --- a/shared/python/aci_github_summary.py +++ b/shared/python/aci_github_summary.py @@ -3,12 +3,30 @@ """GitHub-friendly markdown summary emitter for ACI reports.""" from __future__ import annotations +from typing import Callable + try: from .aci_report_helpers import report_map as _report_map except ImportError: # pragma: no cover - direct script/module import path from aci_report_helpers import report_map as _report_map # type: ignore[no-redef] +def _append_list_section( + lines: list[str], + items: object, + header: str, + row: Callable[[dict[str, object]], str], +) -> None: + if not isinstance(items, list) or not items: + return + lines.append(f"### {header}") + lines.append("") + for item in items: + if isinstance(item, dict): + lines.append(row(item)) + lines.append("") + + def build_github_summary_markdown(report: dict[str, object]) -> str: gate = _report_map(report.get("gate")) summary = _report_map(report.get("summary")) @@ -41,51 +59,26 @@ def build_github_summary_markdown(report: dict[str, object]) -> str: if advisory_headline: lines.append(str(advisory_headline)) lines.append("") - top_files = review_brief.get("top_files", []) - if isinstance(top_files, list) and top_files: - lines.append("### Hottest Files") - lines.append("") - for item in top_files: - if not isinstance(item, dict): - continue - lines.append(f"- `{item.get('name', '')}`: {item.get('count', 0)} finding(s)") - lines.append("") - top_signals = review_brief.get("top_signals", []) - if isinstance(top_signals, list) and top_signals: - lines.append("### Top Signals") - lines.append("") - for item in top_signals: - if not isinstance(item, dict): - continue - lines.append(f"- `{item.get('name', '')}`: {item.get('count', 0)}") - lines.append("") - advisory_scope_classes = review_brief.get("advisory_scope_classes", []) - if isinstance(advisory_scope_classes, list) and advisory_scope_classes: - lines.append("### Advisory Scope Classes") - lines.append("") - for item in advisory_scope_classes: - if not isinstance(item, dict): - continue - lines.append(f"- `{item.get('name', '')}`: {item.get('count', 0)}") - lines.append("") - analyzer_failures = review_brief.get("analyzer_failures", []) - if isinstance(analyzer_failures, list) and analyzer_failures: - lines.append("### Analyzer Failures") - lines.append("") - for item in analyzer_failures: - if not isinstance(item, dict): - continue - lines.append(f"- `{item.get('analyzer_id', '')}`: `{item.get('runtime_state', '')}`") - lines.append("") - analyzer_availability_notes = review_brief.get("analyzer_availability_notes", []) - if isinstance(analyzer_availability_notes, list) and analyzer_availability_notes: - lines.append("### Analyzer Availability Notes") - lines.append("") - for item in analyzer_availability_notes: - if not isinstance(item, dict): - continue - lines.append(f"- `{item.get('analyzer_id', '')}`: `{item.get('runtime_state', '')}`") - lines.append("") + _append_list_section( + lines, review_brief.get("top_files"), "Hottest Files", + lambda i: f"- `{i.get('name', '')}`: {i.get('count', 0)} finding(s)", + ) + _append_list_section( + lines, review_brief.get("top_signals"), "Top Signals", + lambda i: f"- `{i.get('name', '')}`: {i.get('count', 0)}", + ) + _append_list_section( + lines, review_brief.get("advisory_scope_classes"), "Advisory Scope Classes", + lambda i: f"- `{i.get('name', '')}`: {i.get('count', 0)}", + ) + _append_list_section( + lines, review_brief.get("analyzer_failures"), "Analyzer Failures", + lambda i: f"- `{i.get('analyzer_id', '')}`: `{i.get('runtime_state', '')}`", + ) + _append_list_section( + lines, review_brief.get("analyzer_availability_notes"), "Analyzer Availability Notes", + lambda i: f"- `{i.get('analyzer_id', '')}`: `{i.get('runtime_state', '')}`", + ) # The non-exhaustiveness disclosure must travel with the human-facing summary, # not just the JSON report: this PR summary is where a reviewer forms the # judgement "ACI passed, the code is clean." A pass with zero findings is diff --git a/shared/python/aci_report_view.py b/shared/python/aci_report_view.py index 6cd6578..c971400 100644 --- a/shared/python/aci_report_view.py +++ b/shared/python/aci_report_view.py @@ -278,6 +278,37 @@ def _build_residuals(findings: list[dict[str, object]]) -> list[dict[str, object return rows +def _gate_fail_reasons( + *, + blockers: list[dict[str, object]], + new_findings: list[dict[str, object]], + unreviewed: list[dict[str, object]], + analyzer_failures: list[dict[str, object]], + fail_on_new_findings: bool, + fail_on_unreviewed_review_required: bool, + fail_on_analyzer_errors: bool, +) -> tuple[list[str], list[dict[str, object]]]: + reasons: list[str] = [] + details: list[dict[str, object]] = [] + if blockers: + reasons.append("severity-threshold") + details.append({"reason": "severity-threshold", "count": len(blockers), + "finding_ids": [str(i.get("finding_id") or "") for i in blockers]}) + if fail_on_new_findings and new_findings: + reasons.append("new-findings-present") + details.append({"reason": "new-findings-present", "count": len(new_findings), + "finding_ids": [str(i.get("finding_id") or "") for i in new_findings]}) + if fail_on_unreviewed_review_required and unreviewed: + reasons.append("unreviewed-review-required") + details.append({"reason": "unreviewed-review-required", "count": len(unreviewed), + "finding_ids": [str(i.get("finding_id") or "") for i in unreviewed]}) + if fail_on_analyzer_errors and analyzer_failures: + reasons.append("analyzer-runtime-error") + details.append({"reason": "analyzer-runtime-error", "count": len(analyzer_failures), + "analyzers": [str(i.get("analyzer_id") or "") for i in analyzer_failures]}) + return reasons, details + + def _build_gate( findings: list[dict[str, object]], report: dict[str, object], @@ -288,24 +319,18 @@ def _build_gate( source_gate = _report_map(report.get("gate")) severity_threshold = str(source_gate.get("severity_threshold") or SEVERITY_HIGH) threshold_rank = _SEVERITY_RANK.get(severity_threshold, _SEVERITY_RANK[SEVERITY_HIGH]) - blocking_severities = [ - severity for severity, rank in _SEVERITY_RANK.items() if rank >= threshold_rank - ] - fail_on_new_findings = bool(source_gate.get("fail_on_new_findings", False)) - fail_on_unreviewed_review_required = bool( - source_gate.get("fail_on_unreviewed_review_required", False) - ) - fail_on_analyzer_errors = bool(source_gate.get("fail_on_analyzer_errors", False)) - gated_findings = [ - item for item in findings if _row_scope_class(item) in gate_scope_classes - ] + blocking_severities = [s for s, r in _SEVERITY_RANK.items() if r >= threshold_rank] + fail_on_new = bool(source_gate.get("fail_on_new_findings", False)) + fail_on_unreviewed = bool(source_gate.get("fail_on_unreviewed_review_required", False)) + fail_on_errors = bool(source_gate.get("fail_on_analyzer_errors", False)) + gated = [item for item in findings if _row_scope_class(item) in gate_scope_classes] new_findings = [ - item for item in gated_findings + item for item in gated if str(item.get("baseline_status") or "") == "new" and str(item.get("waiver_status") or "none") == "none" ] unreviewed = [ - item for item in gated_findings + item for item in gated if str(item.get("owner_lane") or "") == LANE_HUMAN_JUDGMENT and str(item.get("baseline_status") or "") == "new" and str(item.get("waiver_status") or "none") == "none" @@ -315,48 +340,12 @@ def _build_gate( item for item in analyzer_runs if str(item.get("runtime_state") or "") not in _NON_FAILING_ANALYZER_RUNTIME_STATES ] - reasons: list[str] = [] - if blockers: - reasons.append("severity-threshold") - if fail_on_new_findings and new_findings: - reasons.append("new-findings-present") - if fail_on_unreviewed_review_required and unreviewed: - reasons.append("unreviewed-review-required") - if fail_on_analyzer_errors and analyzer_failures: - reasons.append("analyzer-runtime-error") - reason_details: list[dict[str, object]] = [] - if blockers: - reason_details.append( - { - "reason": "severity-threshold", - "count": len(blockers), - "finding_ids": [str(item.get("finding_id") or "") for item in blockers], - } - ) - if fail_on_new_findings and new_findings: - reason_details.append( - { - "reason": "new-findings-present", - "count": len(new_findings), - "finding_ids": [str(item.get("finding_id") or "") for item in new_findings], - } - ) - if fail_on_unreviewed_review_required and unreviewed: - reason_details.append( - { - "reason": "unreviewed-review-required", - "count": len(unreviewed), - "finding_ids": [str(item.get("finding_id") or "") for item in unreviewed], - } - ) - if fail_on_analyzer_errors and analyzer_failures: - reason_details.append( - { - "reason": "analyzer-runtime-error", - "count": len(analyzer_failures), - "analyzers": [str(item.get("analyzer_id") or "") for item in analyzer_failures], - } - ) + reasons, reason_details = _gate_fail_reasons( + blockers=blockers, new_findings=new_findings, unreviewed=unreviewed, + analyzer_failures=analyzer_failures, fail_on_new_findings=fail_on_new, + fail_on_unreviewed_review_required=fail_on_unreviewed, + fail_on_analyzer_errors=fail_on_errors, + ) return { "decision": "fail" if reasons else "pass", "blocking_severities": blocking_severities, @@ -366,9 +355,9 @@ def _build_gate( "reasons": reasons, "reason_details": reason_details, "severity_threshold": severity_threshold, - "fail_on_new_findings": fail_on_new_findings, - "fail_on_unreviewed_review_required": fail_on_unreviewed_review_required, - "fail_on_analyzer_errors": fail_on_analyzer_errors, + "fail_on_new_findings": fail_on_new, + "fail_on_unreviewed_review_required": fail_on_unreviewed, + "fail_on_analyzer_errors": fail_on_errors, }