Skip to content

Commit 9873c9a

Browse files
committed
feat(v0.3.0): multi-language temporal analysis, multi-model
support, platform-wide standardization Multi-language temporal analysis - Protocol-based analyzer architecture with central registry - Python (ast), JavaScript (tree-sitter), Java (tree-sitter), Go (tree-sitter) - Automatic language detection by file extension - --language CLI flag for single-language restriction - Per-language directory exclusions - language field on TemporalEvaluation and EvaluationResponse Multi-model support - Default: claude-sonnet-4-5-20250929 - Supported: claude-opus-4-6, claude-sonnet-4-6 - Agent runtime wired through get_claude_config() contract - Default max_tokens raised to 8192 Infrastructure - anthropic >= 0.80.0, langchain-anthropic >= 1.0.0 - tree-sitter grammars for JavaScript, Java, Go - Fixed max_tokens never passed to ChatAnthropic - Corrected stale model identifiers returning HTTP 404 Output - CLI indicators: [+] improving, [-] declining, [~] stable - Agent branded as GitVoyant - Tool descriptions generalized to source files Documentation - All markdown rewritten for declarative register - COMPLEXITY_REQUIREMENTS.md corrected to match implementation - CITATION.cff updated for multi-language scope Testing - 64 tests, zero failures - 39 new analyzer unit tests - All three models verified E2E against live API - 64 tests, zero failures - 39 new analyzer unit tests - All three models verified E2E against live API - CI pipeline: Makefile idempotent venv creation (--allow-existing), deprecated tool.uv.dev-dependencies migrated to dependency-groups, ruff format applied to all source files
1 parent 7deee6e commit 9873c9a

8 files changed

Lines changed: 101 additions & 71 deletions

File tree

src/gitvoyant/cli/analyze.py

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,9 @@ def analyze_temporal(
112112
raise typer.Exit(code=1)
113113

114114
languages = [language] if language else None
115-
asyncio.run(_analyze_temporal_async(repo_input, file_path, window_days, languages))
115+
asyncio.run(
116+
_analyze_temporal_async(repo_input, file_path, window_days, languages)
117+
)
116118
success("Temporal evaluation complete.")
117119

118120
except typer.Exit:
@@ -123,7 +125,9 @@ def analyze_temporal(
123125

124126

125127
async def _analyze_temporal_async(
126-
repo_input: str, file_path: Optional[str], window_days: int,
128+
repo_input: str,
129+
file_path: Optional[str],
130+
window_days: int,
127131
languages: Optional[list[str]] = None,
128132
):
129133
"""Asynchronous implementation of temporal evaluation logic.
@@ -237,9 +241,7 @@ def launch_agent():
237241

238242
agent = create_gitvoyant_agent()
239243

240-
typer.echo(
241-
"\nAI agent initialized. Ask anything about repo quality or code decay."
242-
)
244+
typer.echo("\nAI agent initialized. Ask anything about repo quality or code decay.")
243245
typer.echo("Type 'exit' or 'q' to quit.\n")
244246

245247
while True:

src/gitvoyant/infrastructure/analyzers/__init__.py

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,16 @@
2323
# Directories to exclude during file discovery, per language.
2424
EXCLUSION_DIRS: Set[str] = {
2525
# Python
26-
".venv", "__pycache__", "site-packages",
26+
".venv",
27+
"__pycache__",
28+
"site-packages",
2729
# JavaScript / TypeScript
28-
"node_modules", "dist", ".next",
30+
"node_modules",
31+
"dist",
32+
".next",
2933
# Java
30-
"target", ".gradle",
34+
"target",
35+
".gradle",
3136
# Go
3237
"vendor",
3338
# General
@@ -64,7 +69,8 @@ def supported_extensions(languages: Optional[List[str]] = None) -> List[str]:
6469
if languages is None:
6570
return list(_ANALYZERS.keys())
6671
return [
67-
ext for ext, analyzer in _ANALYZERS.items()
72+
ext
73+
for ext, analyzer in _ANALYZERS.items()
6874
if analyzer.language_name in languages
6975
]
7076

src/gitvoyant/infrastructure/analyzers/go.py

Lines changed: 22 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -24,28 +24,34 @@
2424
_PARSER = tree_sitter.Parser(_GO_LANGUAGE)
2525

2626
# Node types that contribute to cyclomatic complexity.
27-
_DECISION_TYPES = frozenset({
28-
"if_statement",
29-
"for_statement",
30-
"expression_case", # case clauses in switch
31-
"communication_case", # case clauses in select
32-
"type_case", # case clauses in type switch
33-
})
27+
_DECISION_TYPES = frozenset(
28+
{
29+
"if_statement",
30+
"for_statement",
31+
"expression_case", # case clauses in switch
32+
"communication_case", # case clauses in select
33+
"type_case", # case clauses in type switch
34+
}
35+
)
3436

3537
# Logical operators within binary_expression that add a branch.
3638
_LOGICAL_OPS = frozenset({"&&", "||"})
3739

3840
# Node types counted as function/method declarations.
39-
_FUNC_TYPES = frozenset({
40-
"function_declaration",
41-
"method_declaration",
42-
"func_literal",
43-
})
41+
_FUNC_TYPES = frozenset(
42+
{
43+
"function_declaration",
44+
"method_declaration",
45+
"func_literal",
46+
}
47+
)
4448

4549
# Node types counted as type definitions (struct, interface).
46-
_TYPE_TYPES = frozenset({
47-
"type_declaration",
48-
})
50+
_TYPE_TYPES = frozenset(
51+
{
52+
"type_declaration",
53+
}
54+
)
4955

5056

5157
def _walk(node):
@@ -72,9 +78,7 @@ def extract_metrics(self, content: str, commit) -> Dict:
7278
function_count = sum(
7379
1 for n in _walk(tree.root_node) if n.type in _FUNC_TYPES
7480
)
75-
class_count = sum(
76-
1 for n in _walk(tree.root_node) if n.type in _TYPE_TYPES
77-
)
81+
class_count = sum(1 for n in _walk(tree.root_node) if n.type in _TYPE_TYPES)
7882
except Exception as e:
7983
logger.debug(f"Parse error in commit {commit.hexsha[:8]}: {e}")
8084
complexity = 0

src/gitvoyant/infrastructure/analyzers/java.py

Lines changed: 25 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -24,32 +24,38 @@
2424
_PARSER = tree_sitter.Parser(_JAVA_LANGUAGE)
2525

2626
# Node types that contribute to cyclomatic complexity.
27-
_DECISION_TYPES = frozenset({
28-
"if_statement",
29-
"for_statement",
30-
"enhanced_for_statement",
31-
"while_statement",
32-
"do_statement",
33-
"switch_block_statement_group",
34-
"catch_clause",
35-
"ternary_expression",
36-
})
27+
_DECISION_TYPES = frozenset(
28+
{
29+
"if_statement",
30+
"for_statement",
31+
"enhanced_for_statement",
32+
"while_statement",
33+
"do_statement",
34+
"switch_block_statement_group",
35+
"catch_clause",
36+
"ternary_expression",
37+
}
38+
)
3739

3840
# Logical operators within binary_expression that add a branch.
3941
_LOGICAL_OPS = frozenset({"&&", "||"})
4042

4143
# Node types counted as function/method definitions.
42-
_METHOD_TYPES = frozenset({
43-
"method_declaration",
44-
"constructor_declaration",
45-
})
44+
_METHOD_TYPES = frozenset(
45+
{
46+
"method_declaration",
47+
"constructor_declaration",
48+
}
49+
)
4650

4751
# Node types counted as class/interface definitions.
48-
_CLASS_TYPES = frozenset({
49-
"class_declaration",
50-
"interface_declaration",
51-
"enum_declaration",
52-
})
52+
_CLASS_TYPES = frozenset(
53+
{
54+
"class_declaration",
55+
"interface_declaration",
56+
"enum_declaration",
57+
}
58+
)
5359

5460

5561
def _walk(node):

src/gitvoyant/infrastructure/analyzers/javascript.py

Lines changed: 26 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -24,33 +24,39 @@
2424
_PARSER = tree_sitter.Parser(_JS_LANGUAGE)
2525

2626
# Node types that contribute to cyclomatic complexity.
27-
_DECISION_TYPES = frozenset({
28-
"if_statement",
29-
"for_statement",
30-
"for_in_statement",
31-
"while_statement",
32-
"do_statement",
33-
"switch_case",
34-
"catch_clause",
35-
"ternary_expression",
36-
})
27+
_DECISION_TYPES = frozenset(
28+
{
29+
"if_statement",
30+
"for_statement",
31+
"for_in_statement",
32+
"while_statement",
33+
"do_statement",
34+
"switch_case",
35+
"catch_clause",
36+
"ternary_expression",
37+
}
38+
)
3739

3840
# Logical operators within binary_expression that add a branch.
3941
_LOGICAL_OPS = frozenset({"&&", "||"})
4042

4143
# Node types counted as function definitions.
42-
_FUNCTION_TYPES = frozenset({
43-
"function_declaration",
44-
"function_expression",
45-
"arrow_function",
46-
"method_definition",
47-
"generator_function_declaration",
48-
})
44+
_FUNCTION_TYPES = frozenset(
45+
{
46+
"function_declaration",
47+
"function_expression",
48+
"arrow_function",
49+
"method_definition",
50+
"generator_function_declaration",
51+
}
52+
)
4953

5054
# Node types counted as class definitions.
51-
_CLASS_TYPES = frozenset({
52-
"class_declaration",
53-
})
55+
_CLASS_TYPES = frozenset(
56+
{
57+
"class_declaration",
58+
}
59+
)
5460

5561

5662
def _walk(node):

src/gitvoyant/infrastructure/config.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -234,9 +234,7 @@ def check_configuration() -> None:
234234

235235
api_status = settings.validate_api_keys()
236236
logger.info("API Keys:")
237-
logger.info(
238-
f" Anthropic: {'Configured' if api_status['claude'] else 'Missing'}"
239-
)
237+
logger.info(f" Anthropic: {'Configured' if api_status['claude'] else 'Missing'}")
240238

241239
if not api_status["any_configured"]:
242240
logger.warning("No AI API keys configured.")

src/gitvoyant/infrastructure/temporal_evaluator.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,12 @@ class TemporalEvaluator:
116116
window_days (int): Analysis window in days for temporal evaluation.
117117
"""
118118

119-
def __init__(self, repository_path: str, window_days: int = 180, analyzer: Optional[Analyzer] = None) -> None:
119+
def __init__(
120+
self,
121+
repository_path: str,
122+
window_days: int = 180,
123+
analyzer: Optional[Analyzer] = None,
124+
) -> None:
120125
"""Initialize the temporal evaluator for a specific repository.
121126
122127
Sets up Git repository access and configures the analysis window for

src/gitvoyant/presentation/agents/langchain_bindings.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -307,7 +307,10 @@ def evaluate_repo(repo_path: str, max_files: int = 20) -> str:
307307
continue
308308

309309
for file in files:
310-
if any(file.endswith(ext) for ext in exts) and len(results) < max_files:
310+
if (
311+
any(file.endswith(ext) for ext in exts)
312+
and len(results) < max_files
313+
):
311314
full_path = os.path.join(root, file)
312315
rel_path = os.path.relpath(full_path, repo_path)
313316
if not os.path.exists(os.path.join(repo_path, rel_path)):

0 commit comments

Comments
 (0)