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.
- agent_code_guard/__init__.py +1 -0
- agent_code_guard/analysis/__init__.py +13 -0
- agent_code_guard/analysis/adapters.py +608 -0
- agent_code_guard/analysis/errors.py +13 -0
- agent_code_guard/analysis/facts.py +101 -0
- agent_code_guard/analysis/language_specs.py +82 -0
- agent_code_guard/analysis/pipeline.py +37 -0
- agent_code_guard/analysis/provider.py +45 -0
- agent_code_guard/analysis/regions.py +108 -0
- agent_code_guard/code_guard.py +236 -0
- agent_code_guard/config_validation.py +90 -0
- agent_code_guard/file_selection.py +228 -0
- agent_code_guard/guards/__init__.py +1 -0
- agent_code_guard/guards/callable_size.py +79 -0
- agent_code_guard/guards/complexity.py +94 -0
- agent_code_guard/guards/loc.py +235 -0
- agent_code_guard/guards/markdown_document_size.py +66 -0
- agent_code_guard/guards/markdown_section_size.py +66 -0
- agent_code_guard/guards/nesting.py +109 -0
- agent_code_guard/markdown/__init__.py +6 -0
- agent_code_guard/markdown/facts.py +27 -0
- agent_code_guard/markdown/scanner.py +109 -0
- agent_code_guard/path_matching.py +25 -0
- agent_code_guard/reporting.py +11 -0
- agent_code_guard/result_model.py +128 -0
- agent_code_guard/skill_distribution.py +96 -0
- agent_code_guard-0.1.0.data/data/share/agent-code-guard/skill/LICENSE.txt +21 -0
- agent_code_guard-0.1.0.data/data/share/agent-code-guard/skill/SKILL.md +138 -0
- agent_code_guard-0.1.0.data/data/share/agent-code-guard/skill/agents/openai.yaml +8 -0
- agent_code_guard-0.1.0.data/data/share/agent-code-guard/skill/references/callable-size-policy.md +39 -0
- agent_code_guard-0.1.0.data/data/share/agent-code-guard/skill/references/complexity-policy.md +39 -0
- agent_code_guard-0.1.0.data/data/share/agent-code-guard/skill/references/loc-policy.md +40 -0
- agent_code_guard-0.1.0.data/data/share/agent-code-guard/skill/references/markdown-size-policy.md +16 -0
- agent_code_guard-0.1.0.data/data/share/agent-code-guard/skill/references/nesting-policy.md +37 -0
- agent_code_guard-0.1.0.dist-info/METADATA +206 -0
- agent_code_guard-0.1.0.dist-info/RECORD +40 -0
- agent_code_guard-0.1.0.dist-info/WHEEL +5 -0
- agent_code_guard-0.1.0.dist-info/entry_points.txt +2 -0
- agent_code_guard-0.1.0.dist-info/licenses/LICENSE +21 -0
- 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())
|