agent-code-guard 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. agent_code_guard/__init__.py +1 -0
  2. agent_code_guard/analysis/__init__.py +13 -0
  3. agent_code_guard/analysis/adapters.py +608 -0
  4. agent_code_guard/analysis/errors.py +13 -0
  5. agent_code_guard/analysis/facts.py +101 -0
  6. agent_code_guard/analysis/language_specs.py +82 -0
  7. agent_code_guard/analysis/pipeline.py +37 -0
  8. agent_code_guard/analysis/provider.py +45 -0
  9. agent_code_guard/analysis/regions.py +108 -0
  10. agent_code_guard/code_guard.py +236 -0
  11. agent_code_guard/config_validation.py +90 -0
  12. agent_code_guard/file_selection.py +228 -0
  13. agent_code_guard/guards/__init__.py +1 -0
  14. agent_code_guard/guards/callable_size.py +79 -0
  15. agent_code_guard/guards/complexity.py +94 -0
  16. agent_code_guard/guards/loc.py +235 -0
  17. agent_code_guard/guards/markdown_document_size.py +66 -0
  18. agent_code_guard/guards/markdown_section_size.py +66 -0
  19. agent_code_guard/guards/nesting.py +109 -0
  20. agent_code_guard/markdown/__init__.py +6 -0
  21. agent_code_guard/markdown/facts.py +27 -0
  22. agent_code_guard/markdown/scanner.py +109 -0
  23. agent_code_guard/path_matching.py +25 -0
  24. agent_code_guard/reporting.py +11 -0
  25. agent_code_guard/result_model.py +128 -0
  26. agent_code_guard/skill_distribution.py +96 -0
  27. agent_code_guard-0.1.0.data/data/share/agent-code-guard/skill/LICENSE.txt +21 -0
  28. agent_code_guard-0.1.0.data/data/share/agent-code-guard/skill/SKILL.md +138 -0
  29. agent_code_guard-0.1.0.data/data/share/agent-code-guard/skill/agents/openai.yaml +8 -0
  30. agent_code_guard-0.1.0.data/data/share/agent-code-guard/skill/references/callable-size-policy.md +39 -0
  31. agent_code_guard-0.1.0.data/data/share/agent-code-guard/skill/references/complexity-policy.md +39 -0
  32. agent_code_guard-0.1.0.data/data/share/agent-code-guard/skill/references/loc-policy.md +40 -0
  33. agent_code_guard-0.1.0.data/data/share/agent-code-guard/skill/references/markdown-size-policy.md +16 -0
  34. agent_code_guard-0.1.0.data/data/share/agent-code-guard/skill/references/nesting-policy.md +37 -0
  35. agent_code_guard-0.1.0.dist-info/METADATA +206 -0
  36. agent_code_guard-0.1.0.dist-info/RECORD +40 -0
  37. agent_code_guard-0.1.0.dist-info/WHEEL +5 -0
  38. agent_code_guard-0.1.0.dist-info/entry_points.txt +2 -0
  39. agent_code_guard-0.1.0.dist-info/licenses/LICENSE +21 -0
  40. agent_code_guard-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,101 @@
1
+ """Immutable, parser-provider-neutral syntax facts consumed by future guards."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ from pathlib import Path
7
+
8
+
9
+ @dataclass(frozen=True, order=True)
10
+ class SourcePoint:
11
+ """A one-based line and one-based UTF-8 byte column in the original file."""
12
+
13
+ line: int
14
+ byte_column: int
15
+ byte_offset: int
16
+
17
+
18
+ @dataclass(frozen=True, order=True)
19
+ class SourceRange:
20
+ """A half-open original-source byte range with inclusive physical lines."""
21
+
22
+ start: SourcePoint
23
+ end: SourcePoint
24
+
25
+ @property
26
+ def start_line(self) -> int:
27
+ return self.start.line
28
+
29
+ @property
30
+ def end_line(self) -> int:
31
+ return self.end.line if self.end.byte_column > 1 else max(self.start.line, self.end.line - 1)
32
+
33
+ @property
34
+ def physical_loc(self) -> int:
35
+ return self.end_line - self.start_line + 1
36
+
37
+
38
+ @dataclass(frozen=True)
39
+ class CallableKey:
40
+ path: Path
41
+ embedded_language: str
42
+ identity: str
43
+ source_range: SourceRange
44
+
45
+
46
+ @dataclass(frozen=True)
47
+ class CallableFact:
48
+ path: Path
49
+ embedded_language: str
50
+ identity: str
51
+ source_range: SourceRange
52
+ parent_callable: str | None
53
+ boundary_kind: str
54
+ key: CallableKey
55
+ parent_key: CallableKey | None
56
+
57
+
58
+ @dataclass(frozen=True)
59
+ class ControlFlowFact:
60
+ callable_identity: str
61
+ callable_key: CallableKey
62
+ category: str
63
+ provider_kind: str
64
+ source_range: SourceRange
65
+ parent_control_range: SourceRange | None
66
+ increases_nesting: bool = True
67
+
68
+
69
+ @dataclass(frozen=True)
70
+ class DecisionFact:
71
+ callable_identity: str
72
+ callable_key: CallableKey
73
+ category: str
74
+ provider_kind: str
75
+ source_range: SourceRange
76
+
77
+
78
+ @dataclass(frozen=True)
79
+ class FileFacts:
80
+ path: Path
81
+ callables: tuple[CallableFact, ...]
82
+ controls: tuple[ControlFlowFact, ...]
83
+ decisions: tuple[DecisionFact, ...]
84
+ region_count: int
85
+
86
+
87
+ @dataclass(frozen=True)
88
+ class AnalysisFacts:
89
+ files: tuple[FileFacts, ...]
90
+
91
+ @property
92
+ def callables(self) -> tuple[CallableFact, ...]:
93
+ return tuple(fact for file in self.files for fact in file.callables)
94
+
95
+ @property
96
+ def controls(self) -> tuple[ControlFlowFact, ...]:
97
+ return tuple(fact for file in self.files for fact in file.controls)
98
+
99
+ @property
100
+ def decisions(self) -> tuple[DecisionFact, ...]:
101
+ return tuple(fact for file in self.files for fact in file.decisions)
@@ -0,0 +1,82 @@
1
+ """Declarative Tree-sitter node mappings for supported executable languages."""
2
+
3
+ CALLABLE_TYPES = {
4
+ "python": {"function_definition", "lambda"},
5
+ "go": {"function_declaration", "method_declaration", "func_literal"},
6
+ "kotlin": {"function_declaration", "secondary_constructor", "lambda_literal", "anonymous_function"},
7
+ "csharp": {"method_declaration", "constructor_declaration", "local_function_statement", "lambda_expression", "anonymous_method_expression"},
8
+ "java": {"method_declaration", "constructor_declaration", "lambda_expression"},
9
+ "javascript": {"function_declaration", "method_definition", "arrow_function", "function_expression"},
10
+ "typescript": {"function_declaration", "method_definition", "arrow_function", "function_expression"},
11
+ "tsx": {"function_declaration", "method_definition", "arrow_function", "function_expression"},
12
+ "cpp": {"function_definition", "lambda_expression"}, "rust": {"function_item", "closure_expression"},
13
+ "php": {"function_definition", "method_declaration", "anonymous_function", "arrow_function"},
14
+ "swift": {"function_declaration", "init_declaration", "lambda_literal", "protocol_function_declaration"},
15
+ "dart": {"function_signature", "method_signature", "function_expression", "lambda_expression"},
16
+ }
17
+
18
+ OPAQUE_LAMBDA_TYPES = {
19
+ "python": set(), "go": set(), "kotlin": set(), "csharp": set(), "java": set(),
20
+ "javascript": set(), "typescript": set(), "tsx": set(), "cpp": set(), "rust": set(),
21
+ "php": set(), "swift": set(), "dart": set(),
22
+ }
23
+
24
+ CONTROL_CATEGORIES = {
25
+ "if_statement": "condition", "if_expression": "condition", "elif_clause": "condition",
26
+ "else_if_clause": "condition", "guard_statement": "condition", "for_statement": "loop",
27
+ "foreach_statement": "loop", "enhanced_for_statement": "loop", "for_in_statement": "loop",
28
+ "while_statement": "loop", "do_statement": "loop", "do_while_statement": "loop",
29
+ "loop_expression": "loop", "while_expression": "loop", "for_expression": "loop",
30
+ "repeat_while_statement": "loop", "match_statement": "selection", "match_expression": "selection",
31
+ "when_expression": "selection", "switch_statement": "selection", "switch_expression": "selection",
32
+ "expression_switch_statement": "selection", "type_switch_statement": "selection",
33
+ "select_statement": "selection", "try_statement": "exception", "try_expression": "exception",
34
+ }
35
+
36
+ CONTROL_TYPES = {
37
+ "python": {"if_statement", "elif_clause", "for_statement", "while_statement", "match_statement", "try_statement"},
38
+ "go": {"if_statement", "for_statement", "expression_switch_statement", "type_switch_statement", "select_statement"},
39
+ "kotlin": {"if_expression", "for_statement", "while_statement", "do_while_statement", "when_expression", "try_expression"},
40
+ "csharp": {"if_statement", "for_statement", "foreach_statement", "while_statement", "do_statement", "switch_statement", "try_statement"},
41
+ "java": {"if_statement", "for_statement", "enhanced_for_statement", "while_statement", "do_statement", "switch_expression", "try_statement"},
42
+ "javascript": {"if_statement", "for_statement", "for_in_statement", "while_statement", "do_statement", "switch_statement", "try_statement"},
43
+ "typescript": {"if_statement", "for_statement", "for_in_statement", "while_statement", "do_statement", "switch_statement", "try_statement"},
44
+ "tsx": {"if_statement", "for_statement", "for_in_statement", "while_statement", "do_statement", "switch_statement", "try_statement"},
45
+ "cpp": {"if_statement", "for_statement", "range_based_for_statement", "while_statement", "do_statement", "switch_statement", "try_statement"},
46
+ "rust": {"if_expression", "loop_expression", "while_expression", "for_expression", "match_expression"},
47
+ "php": {"if_statement", "else_if_clause", "for_statement", "foreach_statement", "while_statement", "do_statement", "switch_statement", "match_expression", "try_statement"},
48
+ "swift": {"if_statement", "guard_statement", "for_statement", "while_statement", "repeat_while_statement", "switch_statement", "do_statement"},
49
+ "dart": {"if_statement", "for_statement", "while_statement", "do_statement", "switch_statement", "try_statement"},
50
+ }
51
+
52
+ DECISION_CATEGORIES = {
53
+ "if_statement": "condition", "if_expression": "condition", "elif_clause": "condition",
54
+ "else_if_clause": "condition", "guard_statement": "condition", "for_statement": "loop",
55
+ "foreach_statement": "loop", "enhanced_for_statement": "loop", "for_in_statement": "loop",
56
+ "while_statement": "loop", "do_statement": "loop", "do_while_statement": "loop",
57
+ "loop_expression": "loop", "while_expression": "loop", "for_expression": "loop",
58
+ "repeat_while_statement": "loop", "except_clause": "catch", "catch_clause": "catch",
59
+ "catch_block": "catch", "conditional_expression": "ternary", "ternary_expression": "ternary",
60
+ "list_comprehension": "comprehension", "set_comprehension": "comprehension",
61
+ "dictionary_comprehension": "comprehension", "generator_expression": "comprehension",
62
+ "case_clause": "switch_arm", "expression_case": "switch_arm", "type_case": "switch_arm",
63
+ "communication_case": "switch_arm", "when_entry": "switch_arm", "switch_expression_arm": "switch_arm",
64
+ "match_arm": "switch_arm", "match_conditional_expression": "switch_arm", "switch_entry": "switch_arm",
65
+ "switch_statement_case": "switch_arm", "case_statement": "switch_arm",
66
+ }
67
+
68
+ DECISION_TYPES = {
69
+ "python": {"if_statement", "elif_clause", "for_statement", "while_statement", "except_clause", "conditional_expression", "list_comprehension", "set_comprehension", "dictionary_comprehension", "generator_expression", "case_clause"},
70
+ "go": {"if_statement", "for_statement", "expression_case", "type_case", "communication_case"},
71
+ "kotlin": {"if_expression", "for_statement", "while_statement", "do_while_statement", "catch_block", "when_entry"},
72
+ "csharp": {"if_statement", "for_statement", "foreach_statement", "while_statement", "do_statement", "catch_clause", "conditional_expression", "switch_expression_arm"},
73
+ "java": {"if_statement", "for_statement", "enhanced_for_statement", "while_statement", "do_statement", "catch_clause", "ternary_expression"},
74
+ "javascript": {"if_statement", "for_statement", "for_in_statement", "while_statement", "do_statement", "catch_clause", "ternary_expression"},
75
+ "typescript": {"if_statement", "for_statement", "for_in_statement", "while_statement", "do_statement", "catch_clause", "ternary_expression"},
76
+ "tsx": {"if_statement", "for_statement", "for_in_statement", "while_statement", "do_statement", "catch_clause", "ternary_expression"},
77
+ "cpp": {"if_statement", "for_statement", "range_based_for_statement", "while_statement", "do_statement", "catch_clause", "conditional_expression"},
78
+ "rust": {"if_expression", "loop_expression", "while_expression", "for_expression", "match_arm"},
79
+ "php": {"if_statement", "else_if_clause", "for_statement", "foreach_statement", "while_statement", "do_statement", "catch_clause", "conditional_expression", "match_conditional_expression"},
80
+ "swift": {"if_statement", "guard_statement", "for_statement", "while_statement", "repeat_while_statement", "catch_block", "ternary_expression", "switch_entry"},
81
+ "dart": {"if_statement", "for_statement", "while_statement", "do_statement", "catch_clause", "conditional_expression", "switch_statement_case"},
82
+ }
@@ -0,0 +1,37 @@
1
+ """Parse applicable selected files once and return reusable immutable facts."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+
7
+ from .adapters import extract_facts
8
+ from .errors import SyntaxAnalysisError
9
+ from .facts import AnalysisFacts, FileFacts
10
+ from .provider import ParserProvider, TreeSitterProvider
11
+ from .regions import executable_regions, is_applicable
12
+
13
+
14
+ def analyze_files(files: tuple[Path, ...] | list[Path], provider: ParserProvider | None = None) -> AnalysisFacts:
15
+ """Analyze only applicable entries from the already-resolved caller scope."""
16
+ active_provider = provider or TreeSitterProvider()
17
+ results: list[FileFacts] = []
18
+ for path in files:
19
+ path = Path(path)
20
+ if not is_applicable(path):
21
+ continue
22
+ callables = []
23
+ controls = []
24
+ decisions = []
25
+ regions = executable_regions(path, active_provider)
26
+ for region in regions:
27
+ tree = active_provider.parse(region.language, region.source)
28
+ if tree.root_node.has_error:
29
+ raise SyntaxAnalysisError(
30
+ f"unable to parse {path}: embedded {region.language} syntax tree contains errors"
31
+ )
32
+ region_callables, region_controls, region_decisions = extract_facts(tree.root_node, region)
33
+ callables.extend(region_callables)
34
+ controls.extend(region_controls)
35
+ decisions.extend(region_decisions)
36
+ results.append(FileFacts(path, tuple(callables), tuple(controls), tuple(decisions), len(regions)))
37
+ return AnalysisFacts(tuple(results))
@@ -0,0 +1,45 @@
1
+ """Tree-sitter provider hidden behind a small cached parser boundary."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Protocol
6
+
7
+ from .errors import ProviderUnavailableError
8
+
9
+
10
+ class ParserProvider(Protocol):
11
+ def parse(self, language: str, source: bytes): ...
12
+
13
+
14
+ class TreeSitterProvider:
15
+ """Load each embedded-language parser once per analysis provider."""
16
+
17
+ def __init__(self, parser_factory=None) -> None:
18
+ self._parsers: dict[str, object] = {}
19
+ self._parser_factory = parser_factory
20
+
21
+ @property
22
+ def cached_languages(self) -> tuple[str, ...]:
23
+ return tuple(self._parsers)
24
+
25
+ def parse(self, language: str, source: bytes):
26
+ parser = self._parsers.get(language)
27
+ if parser is None:
28
+ try:
29
+ if self._parser_factory is None:
30
+ from tree_sitter_language_pack import get_parser
31
+ self._parser_factory = get_parser
32
+ parser = self._parser_factory(language)
33
+ except (ImportError, LookupError, OSError, RuntimeError) as exc:
34
+ raise ProviderUnavailableError(
35
+ f"syntax provider unavailable for supported language {language!r}: {exc}; "
36
+ "reinstall Agent Code Guard"
37
+ ) from exc
38
+ self._parsers[language] = parser
39
+ try:
40
+ return parser.parse(source)
41
+ except Exception as exc:
42
+ raise ProviderUnavailableError(
43
+ f"syntax provider failed for supported language {language!r}: {exc}; "
44
+ "verify the Agent Code Guard installation"
45
+ ) from exc
@@ -0,0 +1,108 @@
1
+ """Source/container adaptation into byte-mapped executable regions."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ from pathlib import Path
7
+
8
+ from .errors import SyntaxAnalysisError
9
+ from .facts import SourcePoint, SourceRange
10
+ from .provider import ParserProvider
11
+
12
+
13
+ LANGUAGE_BY_SUFFIX = {
14
+ ".py": "python", ".go": "go", ".kt": "kotlin", ".cs": "csharp",
15
+ ".java": "java", ".js": "javascript", ".jsx": "javascript",
16
+ ".ts": "typescript", ".tsx": "tsx",
17
+ ".cpp": "cpp", ".cc": "cpp", ".cxx": "cpp",
18
+ ".hpp": "cpp", ".hh": "cpp", ".hxx": "cpp",
19
+ ".rs": "rust", ".php": "php", ".swift": "swift", ".dart": "dart",
20
+ }
21
+ APPLICABLE_SUFFIXES = frozenset((*LANGUAGE_BY_SUFFIX, ".vue"))
22
+
23
+
24
+ @dataclass(frozen=True)
25
+ class ExecutableRegion:
26
+ original_path: Path
27
+ language: str
28
+ source: bytes
29
+ original_source: bytes
30
+ original_byte_offset: int = 0
31
+
32
+ def original_point(self, local_row: int, local_byte_column: int) -> SourcePoint:
33
+ absolute = self.original_byte_offset + _byte_at_point(self.source, local_row, local_byte_column)
34
+ prefix = self.original_source[:absolute]
35
+ line = prefix.count(b"\n") + 1
36
+ newline = prefix.rfind(b"\n")
37
+ byte_column = absolute + 1 if newline < 0 else absolute - newline
38
+ return SourcePoint(line, byte_column, absolute)
39
+
40
+ def original_range(self, node) -> SourceRange:
41
+ start_row, start_column = node.start_point
42
+ end_row, end_column = node.end_point
43
+ return SourceRange(
44
+ self.original_point(start_row, start_column),
45
+ self.original_point(end_row, end_column),
46
+ )
47
+
48
+
49
+ def is_applicable(path: Path) -> bool:
50
+ return path.suffix.lower() in APPLICABLE_SUFFIXES
51
+
52
+
53
+ def executable_regions(path: Path, provider: ParserProvider) -> tuple[ExecutableRegion, ...]:
54
+ source = path.read_bytes()
55
+ suffix = path.suffix.lower()
56
+ if suffix == ".vue":
57
+ return _vue_regions(path, source, provider)
58
+ language = LANGUAGE_BY_SUFFIX.get(suffix)
59
+ if language is None:
60
+ return ()
61
+ return (ExecutableRegion(path, language, source, source),)
62
+
63
+
64
+ def _vue_regions(path: Path, source: bytes, provider: ParserProvider) -> tuple[ExecutableRegion, ...]:
65
+ root = provider.parse("vue", source).root_node
66
+ if root.has_error:
67
+ raise SyntaxAnalysisError(f"unable to parse {path}: Vue container syntax tree contains errors")
68
+ regions: list[ExecutableRegion] = []
69
+ for element in root.named_children:
70
+ if element.type != "script_element":
71
+ continue
72
+ start_tag = next(child for child in element.named_children if child.type == "start_tag")
73
+ attributes = _attributes(start_tag, source)
74
+ if "src" in attributes:
75
+ raise SyntaxAnalysisError(f"unable to analyze {path}: external Vue script regions are unsupported")
76
+ language = _script_language(path, attributes.get("lang"))
77
+ raw_text = next((child for child in element.named_children if child.type == "raw_text"), None)
78
+ if raw_text is not None:
79
+ regions.append(ExecutableRegion(
80
+ path, language, source[raw_text.start_byte:raw_text.end_byte], source, raw_text.start_byte,
81
+ ))
82
+ return tuple(regions)
83
+
84
+
85
+ def _byte_at_point(source: bytes, row: int, column: int) -> int:
86
+ position = 0
87
+ for _ in range(row):
88
+ position = source.index(b"\n", position) + 1
89
+ return position + column
90
+
91
+
92
+ def _attributes(start_tag, source: bytes) -> dict[str, str | None]:
93
+ values: dict[str, str | None] = {}
94
+ for attribute in (child for child in start_tag.named_children if child.type == "attribute"):
95
+ name_node = next(child for child in attribute.named_children if child.type == "attribute_name")
96
+ value_node = next((child for child in attribute.named_children if child.type == "quoted_attribute_value"), None)
97
+ name = source[name_node.start_byte:name_node.end_byte].decode("utf-8").lower()
98
+ value = None if value_node is None else source[value_node.start_byte:value_node.end_byte].decode("utf-8")[1:-1].lower()
99
+ values[name] = value
100
+ return values
101
+
102
+
103
+ def _script_language(path: Path, value: str | None) -> str:
104
+ if value in {None, "js", "javascript"}:
105
+ return "javascript"
106
+ if value in {"ts", "typescript"}:
107
+ return "typescript"
108
+ raise SyntaxAnalysisError(f"unable to analyze {path}: unsupported Vue script language: {value}")
@@ -0,0 +1,236 @@
1
+ #!/usr/bin/env python3
2
+ """Single public runner for Agent Code Guard."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import argparse
7
+ from importlib import import_module
8
+ import json
9
+ import sys
10
+ from pathlib import Path
11
+
12
+ from .config_validation import validate_configuration
13
+ from .file_selection import resolve_scope
14
+ from .guards import callable_size, complexity, loc, markdown_document_size, markdown_section_size, nesting
15
+ from .result_model import GuardResult, aggregate_state, required_policies
16
+ from .skill_distribution import export_skill, skill_path as installed_skill_path
17
+
18
+
19
+ def parser() -> argparse.ArgumentParser:
20
+ value = argparse.ArgumentParser(description="Run deterministic Code Guard checks.")
21
+ value.add_argument(
22
+ "paths", nargs="*", default=[],
23
+ help="Files or directories to inspect; bounds files selected by a Git selection mode.",
24
+ )
25
+ value.add_argument("--config", help="Path to code-guard.config.json.")
26
+ value.add_argument("--warn", type=int, help="Override the global LOC warning threshold.")
27
+ value.add_argument("--fail", type=int, help="Override the global LOC failure threshold.")
28
+ value.add_argument("--json", action="store_true", help="Emit normalized JSON.")
29
+ value.add_argument("--ci", action="store_true", help="Do not fail solely on REVIEW.")
30
+ value.add_argument("--changed-only", action="store_true", help="Inspect staged, unstaged, and untracked files.")
31
+ value.add_argument("--staged", action="store_true", help="Inspect index-only changes.")
32
+ value.add_argument("--base-ref", help="Inspect committed ACMR changes from <ref>...HEAD.")
33
+ value.add_argument("--include", action="append", default=[], help="Extra LOC extension.")
34
+ value.add_argument("--exclude", action="append", default=[], help="Extra LOC exclusion glob.")
35
+ value.add_argument(
36
+ "--scope-exclude", action="append", default=[],
37
+ help="All-guards scope exclusion glob; repeat to add patterns (unlike LOC-only --exclude).",
38
+ )
39
+ value.add_argument("--count-blank-lines", action="store_true", help="Count blank lines for LOC.")
40
+ value.add_argument("--ignore-comment-lines", action="store_true", help="Ignore simple comment-only lines.")
41
+ value.add_argument(
42
+ "--skill-path", action="store_true",
43
+ help="Print the absolute path to this distribution's bundled Code Guard skill.",
44
+ )
45
+ value.add_argument(
46
+ "--export-skill", metavar="TARGET_DIRECTORY",
47
+ help="Copy this distribution's bundled Code Guard skill into an empty target directory.",
48
+ )
49
+ return value
50
+
51
+
52
+ def _management_mode(args: argparse.Namespace) -> int | None:
53
+ if not args.skill_path and args.export_skill is None:
54
+ args.paths = args.paths or ["."]
55
+ return None
56
+ incompatible = (
57
+ args.skill_path and args.export_skill is not None
58
+ or bool(args.paths)
59
+ or args.config is not None
60
+ or args.warn is not None
61
+ or args.fail is not None
62
+ or args.json
63
+ or args.ci
64
+ or args.changed_only
65
+ or args.staged
66
+ or args.base_ref is not None
67
+ or bool(args.include)
68
+ or bool(args.exclude)
69
+ or bool(args.scope_exclude)
70
+ or args.count_blank_lines
71
+ or args.ignore_comment_lines
72
+ )
73
+ if incompatible:
74
+ raise ValueError("skill management options cannot be combined with guard execution options or paths")
75
+ if args.skill_path:
76
+ print(installed_skill_path())
77
+ else:
78
+ print(export_skill(Path(args.export_skill)))
79
+ return 0
80
+
81
+
82
+ def payload(results: list[GuardResult]) -> dict[str, object]:
83
+ return {
84
+ "overall": aggregate_state(results),
85
+ "requiredPolicies": required_policies(results),
86
+ "guards": {result.guard_id: result.to_json() for result in results},
87
+ }
88
+
89
+
90
+ def run_guards(scope, args: argparse.Namespace) -> list[GuardResult]:
91
+ """Load guard configuration, then construct shared syntax facts at most once."""
92
+ loc_config = loc.load_config(args)
93
+ callable_size_config = callable_size.load_config(args)
94
+ nesting_config = nesting.load_config(args)
95
+ complexity_config = complexity.load_config(args)
96
+ markdown_document_config = markdown_document_size.load_config(args)
97
+ markdown_section_config = markdown_section_size.load_config(args)
98
+ results = [loc.run(scope.root, loc_config, scope.files)]
99
+ needs_analysis = callable_size_config.enabled or nesting_config.enabled or complexity_config.enabled
100
+ if needs_analysis:
101
+ analysis = import_module("agent_code_guard.analysis.pipeline")
102
+ facts = analysis.analyze_files(scope.files)
103
+ if callable_size_config.enabled:
104
+ results.append(callable_size.run(scope.root, callable_size_config, facts))
105
+ if nesting_config.enabled:
106
+ results.append(nesting.run(scope.root, nesting_config, facts))
107
+ if complexity_config.enabled:
108
+ results.append(complexity.run(scope.root, complexity_config, facts))
109
+ needs_markdown = markdown_document_config.enabled or markdown_section_config.enabled
110
+ markdown_files = tuple(path for path in scope.files if path.suffix.lower() == ".md") if needs_markdown else ()
111
+ if markdown_files:
112
+ markdown = import_module("agent_code_guard.markdown")
113
+ markdown_facts = markdown.analyze_files(markdown_files)
114
+ if markdown_document_config.enabled:
115
+ results.append(markdown_document_size.run(scope.root, markdown_document_config, markdown_facts))
116
+ if markdown_section_config.enabled:
117
+ results.append(markdown_section_size.run(scope.root, markdown_section_config, markdown_facts))
118
+ else:
119
+ if markdown_document_config.enabled:
120
+ results.append(markdown_document_size.run(scope.root, markdown_document_config, _empty_markdown_facts()))
121
+ if markdown_section_config.enabled:
122
+ results.append(markdown_section_size.run(scope.root, markdown_section_config, _empty_markdown_facts()))
123
+ return results
124
+
125
+
126
+ def _empty_markdown_facts():
127
+ """Avoid importing the scanner family for scopes with no applicable files."""
128
+ from types import SimpleNamespace
129
+ return SimpleNamespace(documents=())
130
+
131
+
132
+ def print_text(data: dict[str, object]) -> None:
133
+ print(str(data["overall"]).upper())
134
+ loc_result = data["guards"]["loc"]
135
+ for finding in loc_result["findings"]:
136
+ if finding["nativeStatus"] == "ok":
137
+ continue
138
+ label = "EXEMPT" if finding["nativeStatus"] == "exempt" else finding["state"].upper()
139
+ print(f"{label}: {finding['path']} — {finding['countedLoc']} LOC (warn {finding['warnAt']}, fail {finding['failAt']})")
140
+ if finding["overrideIndex"] is not None:
141
+ print(f" Threshold override: {finding['overrideIndex']}")
142
+ if finding["reason"]:
143
+ print(f" Reason: {finding['reason']}")
144
+ callable_result = data["guards"].get("callableSize")
145
+ if callable_result:
146
+ for finding in callable_result["findings"]:
147
+ if finding["state"] != "review":
148
+ continue
149
+ print(
150
+ f"REVIEW: {finding['path']}:{finding['range']['startLine']}-{finding['range']['endLine']} "
151
+ f"— {finding['callable']} is {finding['measured']} LOC "
152
+ f"(review {finding['thresholds']['reviewAt']})"
153
+ )
154
+ nesting_result = data["guards"].get("nesting")
155
+ if nesting_result:
156
+ for finding in nesting_result["findings"]:
157
+ if finding["state"] != "review":
158
+ continue
159
+ deepest = finding.get("details", {}).get("deepestLine")
160
+ explanation = f"; deepest at line {deepest}" if deepest is not None else ""
161
+ print(
162
+ f"REVIEW: {finding['path']}:{finding['range']['startLine']}-{finding['range']['endLine']} "
163
+ f"— {finding['callable']} nesting depth {finding['measured']} "
164
+ f"(review {finding['thresholds']['reviewAt']}{explanation})"
165
+ )
166
+ complexity_result = data["guards"].get("complexity")
167
+ if complexity_result:
168
+ for finding in complexity_result["findings"]:
169
+ if finding["state"] != "review":
170
+ continue
171
+ print(
172
+ f"REVIEW: {finding['path']}:{finding['range']['startLine']}-{finding['range']['endLine']} "
173
+ f"— {finding['callable']} complexity {finding['measured']} "
174
+ f"(review {finding['thresholds']['reviewAt']})"
175
+ )
176
+ _print_markdown_findings(data)
177
+ policies = data["requiredPolicies"]
178
+ if policies:
179
+ print(f"Required policies: {', '.join(policies)}")
180
+ print("Required action: inspect each actionable finding using its policy guidance.")
181
+
182
+
183
+ def _print_markdown_findings(data: dict[str, object]) -> None:
184
+ markdown_document_result = data["guards"].get("markdownDocumentSize")
185
+ if markdown_document_result:
186
+ for finding in markdown_document_result["findings"]:
187
+ if finding["state"] != "review":
188
+ continue
189
+ print(
190
+ f"REVIEW: {finding['path']} — Markdown document is {finding['measured']} lines "
191
+ f"(review {finding['thresholds']['reviewAt']})"
192
+ )
193
+ markdown_section_result = data["guards"].get("markdownSectionSize")
194
+ if markdown_section_result:
195
+ for finding in markdown_section_result["findings"]:
196
+ if finding["state"] != "review":
197
+ continue
198
+ print(
199
+ f"REVIEW: {finding['path']}:{finding['range']['startLine']}-{finding['range']['endLine']} "
200
+ f"— section {json.dumps(finding['heading'], ensure_ascii=False)} is {finding['measured']} lines "
201
+ f"(review {finding['thresholds']['reviewAt']})"
202
+ )
203
+
204
+
205
+ def exit_code(overall: str, ci: bool) -> int:
206
+ if overall == "fail":
207
+ return 2
208
+ if overall == "review" and not ci:
209
+ return 1
210
+ return 0
211
+
212
+
213
+ def main() -> int:
214
+ args = parser().parse_args()
215
+ try:
216
+ management_result = _management_mode(args)
217
+ if management_result is not None:
218
+ return management_result
219
+ validate_configuration(args.config, Path.cwd())
220
+ scope = resolve_scope(args, Path.cwd())
221
+ data = payload(run_guards(scope, args))
222
+ if args.json:
223
+ print(json.dumps(data, indent=2))
224
+ else:
225
+ print_text(data)
226
+ return exit_code(data["overall"], args.ci)
227
+ except Exception as exc:
228
+ if args.json:
229
+ print(json.dumps({"error": str(exc)}, indent=2))
230
+ else:
231
+ print(f"Code Guard error: {exc}", file=sys.stderr)
232
+ return 3
233
+
234
+
235
+ if __name__ == "__main__":
236
+ sys.exit(main())