diff --git a/src/agent_code_guard/code_guard.py b/src/agent_code_guard/code_guard.py index f8ea1dc..70e07d7 100644 --- a/src/agent_code_guard/code_guard.py +++ b/src/agent_code_guard/code_guard.py @@ -14,6 +14,7 @@ from .config_validation import validate_configuration from .file_selection import resolve_scope from .guards import callable_size, complexity, loc, markdown_document_size, markdown_section_size, nesting +from .human_output import format_completed_analysis from . import loc_baseline from .result_model import GuardResult, aggregate_state, required_policies from .skill_distribution import export_skill, skill_path as installed_skill_path @@ -357,92 +358,7 @@ def _empty_markdown_facts(): def print_text(data: dict[str, object]) -> None: - scope = data["scope"] - print( - f"{str(data['overall']).upper()}: {scope['selected']} selected; {scope['analyzed']} analyzed; " - f"{scope['inapplicable']} inapplicable; {scope['excluded']} excluded." - ) - loc_result = data["guards"]["loc"] - for finding in loc_result["findings"]: - if finding["nativeStatus"] == "ok" and finding.get("baselineLoc") is None: - continue - label = ( - "RATCHET" if finding["nativeStatus"] == "grandfathered" else - "EXEMPT" if finding["nativeStatus"] == "exempt" else finding["state"].upper() - ) - baseline_detail = "" - if finding.get("baselineLoc") is not None: - status = { - "within": "within", "exceeded": "exceeded", "notNeeded": "no longer needed", - }[finding["ratchetStatus"]] - baseline_detail = f"; baseline {finding['baselineLoc']}, {status}" - print( - f"{label}: {finding['path']} — {finding['countedLoc']} LOC " - f"(warn {finding['warnAt']}, fail {finding['failAt']}{baseline_detail})" - ) - if finding["overrideIndex"] is not None: - print(f" Threshold override: {finding['overrideIndex']}") - if finding["reason"]: - print(f" Reason: {finding['reason']}") - callable_result = data["guards"].get("callableSize") - if callable_result: - for finding in callable_result["findings"]: - if finding["state"] != "review": - continue - print( - f"REVIEW: {finding['path']}:{finding['range']['startLine']}-{finding['range']['endLine']} " - f"— {finding['callable']} is {finding['measured']} LOC " - f"(review {finding['thresholds']['reviewAt']})" - ) - nesting_result = data["guards"].get("nesting") - if nesting_result: - for finding in nesting_result["findings"]: - if finding["state"] != "review": - continue - deepest = finding.get("details", {}).get("deepestLine") - explanation = f"; deepest at line {deepest}" if deepest is not None else "" - print( - f"REVIEW: {finding['path']}:{finding['range']['startLine']}-{finding['range']['endLine']} " - f"— {finding['callable']} nesting depth {finding['measured']} " - f"(review {finding['thresholds']['reviewAt']}{explanation})" - ) - complexity_result = data["guards"].get("complexity") - if complexity_result: - for finding in complexity_result["findings"]: - if finding["state"] != "review": - continue - print( - f"REVIEW: {finding['path']}:{finding['range']['startLine']}-{finding['range']['endLine']} " - f"— {finding['callable']} complexity {finding['measured']} " - f"(review {finding['thresholds']['reviewAt']})" - ) - _print_markdown_findings(data) - policies = data["requiredPolicies"] - if policies: - print(f"Required policies: {', '.join(policies)}") - print("Required action: inspect each actionable finding using its policy guidance.") - - -def _print_markdown_findings(data: dict[str, object]) -> None: - markdown_document_result = data["guards"].get("markdownDocumentSize") - if markdown_document_result: - for finding in markdown_document_result["findings"]: - if finding["state"] != "review": - continue - print( - f"REVIEW: {finding['path']} — Markdown document is {finding['measured']} lines " - f"(review {finding['thresholds']['reviewAt']})" - ) - markdown_section_result = data["guards"].get("markdownSectionSize") - if markdown_section_result: - for finding in markdown_section_result["findings"]: - if finding["state"] != "review": - continue - print( - f"REVIEW: {finding['path']}:{finding['range']['startLine']}-{finding['range']['endLine']} " - f"— section {json.dumps(finding['heading'], ensure_ascii=False)} is {finding['measured']} lines " - f"(review {finding['thresholds']['reviewAt']})" - ) + print(format_completed_analysis(data)) def exit_code(overall: str, ci: bool) -> int: diff --git a/src/agent_code_guard/human_output.py b/src/agent_code_guard/human_output.py new file mode 100644 index 0000000..5843fcb --- /dev/null +++ b/src/agent_code_guard/human_output.py @@ -0,0 +1,122 @@ +"""Human presentation for completed normal-analysis payloads.""" + +from __future__ import annotations + +import json + + +def _loc_lines(data: dict[str, object]) -> list[str]: + lines = [] + loc_result = data["guards"]["loc"] + for finding in loc_result["findings"]: + if finding["nativeStatus"] == "ok" and finding.get("baselineLoc") is None: + continue + label = ( + "RATCHET" if finding["nativeStatus"] == "grandfathered" else + "EXEMPT" if finding["nativeStatus"] == "exempt" else finding["state"].upper() + ) + baseline_detail = "" + if finding.get("baselineLoc") is not None: + status = { + "within": "within", "exceeded": "exceeded", "notNeeded": "no longer needed", + }[finding["ratchetStatus"]] + baseline_detail = f"; baseline {finding['baselineLoc']}, {status}" + lines.append( + f"{label}: {finding['path']} — {finding['countedLoc']} LOC " + f"(warn {finding['warnAt']}, fail {finding['failAt']}{baseline_detail})" + ) + if finding["overrideIndex"] is not None: + lines.append(f" Threshold override: {finding['overrideIndex']}") + if finding["reason"]: + lines.append(f" Reason: {finding['reason']}") + return lines + + +def _callable_size_lines(data: dict[str, object]) -> list[str]: + lines = [] + result = data["guards"].get("callableSize") + if result: + for finding in result["findings"]: + if finding["state"] != "review": + continue + lines.append( + f"REVIEW: {finding['path']}:{finding['range']['startLine']}-{finding['range']['endLine']} " + f"— {finding['callable']} is {finding['measured']} LOC " + f"(review {finding['thresholds']['reviewAt']})" + ) + return lines + + +def _nesting_lines(data: dict[str, object]) -> list[str]: + lines = [] + result = data["guards"].get("nesting") + if result: + for finding in result["findings"]: + if finding["state"] != "review": + continue + deepest = finding.get("details", {}).get("deepestLine") + explanation = f"; deepest at line {deepest}" if deepest is not None else "" + lines.append( + f"REVIEW: {finding['path']}:{finding['range']['startLine']}-{finding['range']['endLine']} " + f"— {finding['callable']} nesting depth {finding['measured']} " + f"(review {finding['thresholds']['reviewAt']}{explanation})" + ) + return lines + + +def _complexity_lines(data: dict[str, object]) -> list[str]: + lines = [] + result = data["guards"].get("complexity") + if result: + for finding in result["findings"]: + if finding["state"] != "review": + continue + lines.append( + f"REVIEW: {finding['path']}:{finding['range']['startLine']}-{finding['range']['endLine']} " + f"— {finding['callable']} complexity {finding['measured']} " + f"(review {finding['thresholds']['reviewAt']})" + ) + return lines + + +def _markdown_lines(data: dict[str, object]) -> list[str]: + lines = [] + document_result = data["guards"].get("markdownDocumentSize") + if document_result: + for finding in document_result["findings"]: + if finding["state"] != "review": + continue + lines.append( + f"REVIEW: {finding['path']} — Markdown document is {finding['measured']} lines " + f"(review {finding['thresholds']['reviewAt']})" + ) + section_result = data["guards"].get("markdownSectionSize") + if section_result: + for finding in section_result["findings"]: + if finding["state"] != "review": + continue + lines.append( + f"REVIEW: {finding['path']}:{finding['range']['startLine']}-{finding['range']['endLine']} " + f"— section {json.dumps(finding['heading'], ensure_ascii=False)} is {finding['measured']} lines " + f"(review {finding['thresholds']['reviewAt']})" + ) + return lines + + +def format_completed_analysis(data: dict[str, object]) -> str: + """Return the complete human report for an existing completed payload.""" + scope = data["scope"] + lines = [ + f"{str(data['overall']).upper()}: {scope['selected']} selected; {scope['analyzed']} analyzed; " + f"{scope['inapplicable']} inapplicable; {scope['excluded']} excluded." + ] + lines.extend(_loc_lines(data)) + lines.extend(_callable_size_lines(data)) + lines.extend(_nesting_lines(data)) + lines.extend(_complexity_lines(data)) + lines.extend(_markdown_lines(data)) + policies = data["requiredPolicies"] + if policies: + lines.append(f"Required policies: {', '.join(policies)}") + lines.append("Required action: inspect each actionable finding using its policy guidance.") + return "\n".join(lines) diff --git a/tests/test_human_output.py b/tests/test_human_output.py new file mode 100644 index 0000000..bf27f56 --- /dev/null +++ b/tests/test_human_output.py @@ -0,0 +1,50 @@ +from __future__ import annotations + +import json +import tempfile +from pathlib import Path + +from helpers import CodeGuardTestCase + + +class CompletedAnalysisOutputTests(CodeGuardTestCase): + def test_formats_one_complete_mixed_report_exactly(self) -> None: + with tempfile.TemporaryDirectory() as temp: + root = Path(temp) + (root / "sample.py").write_text( + "def sample(value):\n if value:\n if value > 1:\n" + " return value\n return 0\n", + encoding="utf-8", + ) + (root / "guide.md").write_text('# Hé said `"hello`"\nbody\nbody\n', encoding="utf-8") + config = root / "code-guard.config.json" + config.write_text(json.dumps({"version": 1, "guards": { + "loc": {"warnAt": 1, "failAt": 99}, + "callableSize": {"reviewAt": 3}, + "nesting": {"reviewAt": 1}, + "cyclomaticComplexity": {"reviewAt": 1}, + "markdownDocumentSize": {"reviewAt": 2}, + "markdownSectionSize": {"reviewAt": 2}, + }}), encoding="utf-8") + + result = self.run_guard(root, ".", "--config", str(config)) + + self.assertEqual(result.returncode, 1) + self.assertEqual(result.stderr, "") + self.assertEqual(result.stdout, "\n".join([ + "REVIEW: 3 selected; 2 analyzed; 1 inapplicable; 0 excluded.", + "REVIEW: sample.py — 5 LOC (warn 1, fail 99)", + "REVIEW: sample.py:1-5 — sample.sample is 5 LOC (review 3)", + "REVIEW: sample.py:1-5 — sample.sample nesting depth 2 (review 1; deepest at line 3)", + "REVIEW: sample.py:1-5 — sample.sample complexity 3 (review 1)", + "REVIEW: guide.md — Markdown document is 3 lines (review 2)", + 'REVIEW: guide.md:1-3 — section "Hé said `\\"hello`\\"" is 3 lines (review 2)', + "Required policies: callableSize, complexity, loc, markdownDocumentSize, markdownSectionSize, nesting", + "Required action: inspect each actionable finding using its policy guidance.", + "", + ])) + + +if __name__ == "__main__": + import unittest + unittest.main()