codegraph-engine 2.2.0__py3-none-any.whl → 2.3.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.
- codegraph/__init__.py +1 -1
- codegraph/agent_capabilities.py +245 -4
- codegraph/branch_comparison.py +197 -0
- codegraph/change_impact.py +337 -0
- codegraph/cli.py +238 -0
- codegraph/context_freshness.py +349 -0
- codegraph/git_state.py +543 -0
- codegraph/indexing/parser.py +55 -21
- codegraph/mcp/server.py +69 -0
- codegraph/semantic_conflicts.py +270 -0
- codegraph/structural_diff.py +540 -0
- codegraph/symbol_history.py +275 -0
- codegraph_engine-2.3.0.dist-info/METADATA +733 -0
- {codegraph_engine-2.2.0.dist-info → codegraph_engine-2.3.0.dist-info}/RECORD +18 -11
- codegraph_engine-2.2.0.dist-info/METADATA +0 -642
- {codegraph_engine-2.2.0.dist-info → codegraph_engine-2.3.0.dist-info}/WHEEL +0 -0
- {codegraph_engine-2.2.0.dist-info → codegraph_engine-2.3.0.dist-info}/entry_points.txt +0 -0
- {codegraph_engine-2.2.0.dist-info → codegraph_engine-2.3.0.dist-info}/licenses/LICENSE +0 -0
- {codegraph_engine-2.2.0.dist-info → codegraph_engine-2.3.0.dist-info}/top_level.txt +0 -0
codegraph/__init__.py
CHANGED
codegraph/agent_capabilities.py
CHANGED
|
@@ -152,6 +152,12 @@ ROUTING_MANIFEST: dict[str, str] = {
|
|
|
152
152
|
"runtime_ingest": "ingest_runtime_traces",
|
|
153
153
|
"runtime_trace": "get_runtime_trace",
|
|
154
154
|
"runtime_reconcile": "reconcile_static_runtime",
|
|
155
|
+
"git_state": "get_git_state",
|
|
156
|
+
"git_diff": "compare_git",
|
|
157
|
+
"change_impact_deep": "get_change_impact",
|
|
158
|
+
"context_freshness": "check_context_freshness",
|
|
159
|
+
"symbol_history": "trace_symbol_history",
|
|
160
|
+
"semantic_conflicts": "detect_semantic_conflicts",
|
|
155
161
|
}
|
|
156
162
|
|
|
157
163
|
DEFAULT_AGENT_PROFILE_TOOLS: tuple[str, ...] = (
|
|
@@ -2390,6 +2396,235 @@ TOOL_CAPABILITY_REGISTRY: tuple[ToolCapabilitySpec, ...] = (
|
|
|
2390
2396
|
example_call='reconcile_static_runtime(route="/api/scan-bill")',
|
|
2391
2397
|
example_interpretation="Inspect confirmed_runtime_paths, static_runtime_conflicts, not_observed_at_runtime, and runtime_only_observed.",
|
|
2392
2398
|
),
|
|
2399
|
+
# 57. get_git_state
|
|
2400
|
+
ToolCapabilitySpec(
|
|
2401
|
+
tool_name="get_git_state",
|
|
2402
|
+
capability="git_state_tracking",
|
|
2403
|
+
description=(
|
|
2404
|
+
"Track current Git branch, HEAD commit, indexed commit, working tree modifications, staged files, and explicit freshness states (CLEAN, DIRTY, STALE, REINDEXING, ERROR). "
|
|
2405
|
+
"Use when asking if the index is up to date, which files changed, or what git branch is active. "
|
|
2406
|
+
"Does not perform a full repository scan when an incremental update is safe."
|
|
2407
|
+
),
|
|
2408
|
+
task_types=(
|
|
2409
|
+
AgentTaskCategory.DIAGNOSTIC.value,
|
|
2410
|
+
AgentTaskCategory.CHANGE_IMPACT.value,
|
|
2411
|
+
),
|
|
2412
|
+
required_inputs=(),
|
|
2413
|
+
optional_inputs=(),
|
|
2414
|
+
expected_output_type="GitStateReport",
|
|
2415
|
+
relationship_types_returned=(),
|
|
2416
|
+
evidence_guarantees="Deterministic Git rev-parse and status porcelain v1 inspection.",
|
|
2417
|
+
does_not_prove="Does not prove semantic impact; reports file-level and commit-level freshness.",
|
|
2418
|
+
useful_situations=(
|
|
2419
|
+
"Checking if repository has uncommitted changes or if re-indexing is required after pulling git commits",
|
|
2420
|
+
),
|
|
2421
|
+
avoid_when=(
|
|
2422
|
+
"You need symbol-level blast radius rather than repository freshness status",
|
|
2423
|
+
),
|
|
2424
|
+
profiles=("full",),
|
|
2425
|
+
minimal_invocation="get_git_state()",
|
|
2426
|
+
advanced_invocation="get_git_state()",
|
|
2427
|
+
result_fields=("is_git", "branch", "current_head", "indexed_head", "freshness", "working_tree", "reindex_required", "detail"),
|
|
2428
|
+
typical_followup="compare_git or get_change_impact if files were modified",
|
|
2429
|
+
common_mistakes=(
|
|
2430
|
+
"Assuming CLEAN means all tests pass; CLEAN only indicates index matches working tree and HEAD",
|
|
2431
|
+
),
|
|
2432
|
+
example_request="Is my CodeGraph index currently up to date with Git HEAD?",
|
|
2433
|
+
example_call="get_git_state()",
|
|
2434
|
+
example_interpretation="Inspect freshness (CLEAN, DIRTY, or STALE) and working_tree.modified_files.",
|
|
2435
|
+
),
|
|
2436
|
+
# 58. compare_git
|
|
2437
|
+
ToolCapabilitySpec(
|
|
2438
|
+
tool_name="compare_git",
|
|
2439
|
+
capability="structural_git_diff",
|
|
2440
|
+
description=(
|
|
2441
|
+
"Compare two Git revisions, commits, or branches structurally. "
|
|
2442
|
+
"Detects added/deleted/modified/renamed files, AST symbol changes, relationship diffs, routes, and affected tests. "
|
|
2443
|
+
"Use when comparing branches or commits structurally. "
|
|
2444
|
+
"Does not execute repository code."
|
|
2445
|
+
),
|
|
2446
|
+
task_types=(
|
|
2447
|
+
AgentTaskCategory.CHANGE_IMPACT.value,
|
|
2448
|
+
AgentTaskCategory.ARCHITECTURE.value,
|
|
2449
|
+
),
|
|
2450
|
+
required_inputs=(),
|
|
2451
|
+
optional_inputs=("base", "head", "branch_comparison"),
|
|
2452
|
+
expected_output_type="StructuralDiffResult",
|
|
2453
|
+
relationship_types_returned=("CALLS", "IMPORTS", "ROUTES_TO", "TESTS"),
|
|
2454
|
+
evidence_guarantees="AST parsing of before/after revisions with exact file:line citations and similarity scores.",
|
|
2455
|
+
does_not_prove="Does not execute code or run test assertions.",
|
|
2456
|
+
useful_situations=(
|
|
2457
|
+
"Understanding structural changes between two branches or commits without parsing raw diff text",
|
|
2458
|
+
),
|
|
2459
|
+
avoid_when=(
|
|
2460
|
+
"You only need high-level commit log summaries without AST symbol diffs",
|
|
2461
|
+
),
|
|
2462
|
+
profiles=("full",),
|
|
2463
|
+
minimal_invocation='compare_git(base="HEAD~1", head="HEAD")',
|
|
2464
|
+
advanced_invocation='compare_git(base="main", head="my-feature", branch_comparison=True)',
|
|
2465
|
+
result_fields=("base_ref", "head_ref", "added_files", "deleted_files", "modified_files", "renamed_files", "added_symbols", "removed_symbols", "changed_symbols", "changed_routes", "affected_tests"),
|
|
2466
|
+
typical_followup="get_change_impact to find downstream callers of changed symbols",
|
|
2467
|
+
common_mistakes=(
|
|
2468
|
+
"Expecting compare_git to format unified diff text; it outputs AST and structural symbol objects",
|
|
2469
|
+
),
|
|
2470
|
+
example_request="What symbols, routes, and tests changed structurally between main and this branch?",
|
|
2471
|
+
example_call='compare_git(base="main", head="HEAD", branch_comparison=True)',
|
|
2472
|
+
example_interpretation="Inspect packages, modules, changed_symbols, and changed_routes.",
|
|
2473
|
+
),
|
|
2474
|
+
# 59. get_change_impact
|
|
2475
|
+
ToolCapabilitySpec(
|
|
2476
|
+
tool_name="get_change_impact",
|
|
2477
|
+
capability="deep_change_impact_analysis",
|
|
2478
|
+
description=(
|
|
2479
|
+
"Compute deep downstream change impact across callers, callees, framework routes, covering tests, mutating database queries, and monorepo packages. "
|
|
2480
|
+
"Returns ranked entities with explicit confidence (FACT, POSSIBLE, UNKNOWN) and blast radius score. "
|
|
2481
|
+
"Use when evaluating ripple effects of git commits. "
|
|
2482
|
+
"Does not execute repository test suites."
|
|
2483
|
+
),
|
|
2484
|
+
task_types=(
|
|
2485
|
+
AgentTaskCategory.CHANGE_IMPACT.value,
|
|
2486
|
+
AgentTaskCategory.TEST_DISCOVERY.value,
|
|
2487
|
+
AgentTaskCategory.DEBUG.value,
|
|
2488
|
+
),
|
|
2489
|
+
required_inputs=(),
|
|
2490
|
+
optional_inputs=("base", "head", "max_depth", "max_results"),
|
|
2491
|
+
expected_output_type="DeepImpactResult",
|
|
2492
|
+
relationship_types_returned=("CALLS", "CALLED_BY", "ROUTES_TO", "TESTS", "WRITES_TABLE", "READS_TABLE", "DEPENDS_ON"),
|
|
2493
|
+
evidence_guarantees="Complete graph traversal connecting modified AST symbols to callers, routes, tests, and DB queries.",
|
|
2494
|
+
does_not_prove="Does not prove runtime execution frequencies; reports static graph reachability.",
|
|
2495
|
+
useful_situations=(
|
|
2496
|
+
"Evaluating what code, routes, database writes, and tests will break if git changes are merged",
|
|
2497
|
+
),
|
|
2498
|
+
avoid_when=(
|
|
2499
|
+
"No git changes exist and you are analyzing a single untouched function",
|
|
2500
|
+
),
|
|
2501
|
+
profiles=("full",),
|
|
2502
|
+
minimal_invocation="get_change_impact()",
|
|
2503
|
+
advanced_invocation='get_change_impact(base="HEAD~3", head="HEAD", max_depth=2, max_results=50)',
|
|
2504
|
+
result_fields=("base_ref", "head_ref", "changed_files", "changed_symbols", "direct_callers", "transitive_callers", "affected_routes", "affected_tests", "db_writers", "blast_radius_score"),
|
|
2505
|
+
typical_followup="find_tests or get_file on affected_routes / direct_callers",
|
|
2506
|
+
common_mistakes=(
|
|
2507
|
+
"Relying solely on textual search instead of graph reachability for blast radius analysis",
|
|
2508
|
+
),
|
|
2509
|
+
example_request="What is the blast radius and which routes, callers, and tests are affected by my branch?",
|
|
2510
|
+
example_call='get_change_impact(base="HEAD~1", head="HEAD")',
|
|
2511
|
+
example_interpretation="Inspect blast_radius_score, direct_callers, affected_routes, and affected_tests.",
|
|
2512
|
+
),
|
|
2513
|
+
# 60. check_context_freshness
|
|
2514
|
+
ToolCapabilitySpec(
|
|
2515
|
+
tool_name="check_context_freshness",
|
|
2516
|
+
capability="context_freshness_validation",
|
|
2517
|
+
description=(
|
|
2518
|
+
"Validate whether a previously compiled context packet or task context is still VALID, PARTIALLY_STALE, or STALE. "
|
|
2519
|
+
"Guarantees that changes to unrelated files keep context VALID. "
|
|
2520
|
+
"Use when checking if previously compiled context remains valid after edits. "
|
|
2521
|
+
"Does not invalidate context when only unrelated files change."
|
|
2522
|
+
),
|
|
2523
|
+
task_types=(
|
|
2524
|
+
AgentTaskCategory.DIAGNOSTIC.value,
|
|
2525
|
+
AgentTaskCategory.CHANGE_IMPACT.value,
|
|
2526
|
+
),
|
|
2527
|
+
required_inputs=(),
|
|
2528
|
+
optional_inputs=("task", "context_packet"),
|
|
2529
|
+
expected_output_type="ContextFreshnessResult",
|
|
2530
|
+
relationship_types_returned=(),
|
|
2531
|
+
evidence_guarantees="Intersection of Git diff line/file changes against exact symbol and file references in context packet.",
|
|
2532
|
+
does_not_prove="Does not recompile context; reports freshness validity and recommended action.",
|
|
2533
|
+
useful_situations=(
|
|
2534
|
+
"Checking if previously cached or generated AI context packet is still safe to use after editing files",
|
|
2535
|
+
),
|
|
2536
|
+
avoid_when=(
|
|
2537
|
+
"Compiling brand-new context from scratch for an initial prompt",
|
|
2538
|
+
),
|
|
2539
|
+
profiles=("full",),
|
|
2540
|
+
minimal_invocation='check_context_freshness(task="Fix auth timeout")',
|
|
2541
|
+
advanced_invocation="check_context_freshness(context_packet=my_packet)",
|
|
2542
|
+
result_fields=("status", "base_commit", "current_head", "repository_dirty", "context_files", "changed_relevant_files", "unrelated_changed_files", "invalidated_symbols", "reason", "recommended_action"),
|
|
2543
|
+
typical_followup="get_context if status is STALE or PARTIALLY_STALE",
|
|
2544
|
+
common_mistakes=(
|
|
2545
|
+
"Assuming any file change invalidates context; unrelated changes leave context VALID",
|
|
2546
|
+
),
|
|
2547
|
+
example_request="Is my current context packet still valid after my recent git modifications?",
|
|
2548
|
+
example_call='check_context_freshness(task="Fix auth timeout")',
|
|
2549
|
+
example_interpretation="Inspect status: VALID means safe to proceed, STALE means recompile required.",
|
|
2550
|
+
),
|
|
2551
|
+
# 61. trace_symbol_history
|
|
2552
|
+
ToolCapabilitySpec(
|
|
2553
|
+
tool_name="trace_symbol_history",
|
|
2554
|
+
capability="symbol_history_tracing",
|
|
2555
|
+
description=(
|
|
2556
|
+
"Trace deterministic symbol evolution across Git history: introduced, modified, moved across files, renamed, or deleted. "
|
|
2557
|
+
"Uses AST structure and body hashes to differentiate FACT from POSSIBLE/AMBIGUOUS renames. "
|
|
2558
|
+
"Use when discovering when a symbol was added, moved, or renamed. "
|
|
2559
|
+
"Does not guess ambiguous renames without evidence."
|
|
2560
|
+
),
|
|
2561
|
+
task_types=(
|
|
2562
|
+
AgentTaskCategory.SYMBOL_LOOKUP.value,
|
|
2563
|
+
AgentTaskCategory.CHANGE_IMPACT.value,
|
|
2564
|
+
),
|
|
2565
|
+
required_inputs=("symbol",),
|
|
2566
|
+
optional_inputs=("path", "max_commits"),
|
|
2567
|
+
expected_output_type="SymbolHistoryResult",
|
|
2568
|
+
relationship_types_returned=(),
|
|
2569
|
+
evidence_guarantees="Git log history correlated with AST body hashes and file rename metadata.",
|
|
2570
|
+
does_not_prove="Does not guarantee intent behind refactors; reports structural code evolution.",
|
|
2571
|
+
useful_situations=(
|
|
2572
|
+
"Tracing when a function was introduced, modified, moved to a different module, or renamed",
|
|
2573
|
+
),
|
|
2574
|
+
avoid_when=(
|
|
2575
|
+
"You only need current static callers and callees in the working tree",
|
|
2576
|
+
),
|
|
2577
|
+
profiles=("full",),
|
|
2578
|
+
minimal_invocation='trace_symbol_history(symbol="AuthService")',
|
|
2579
|
+
advanced_invocation='trace_symbol_history(symbol="AuthService", path="src/auth.py", max_commits=20)',
|
|
2580
|
+
result_fields=("symbol", "current_path", "events", "total_commits_evaluated", "status", "detail"),
|
|
2581
|
+
typical_followup="get_symbol or get_file on historical commit",
|
|
2582
|
+
common_mistakes=(
|
|
2583
|
+
"Assuming all renames are FACT; ambiguous renames are explicitly flagged as AMBIGUOUS or POSSIBLE",
|
|
2584
|
+
),
|
|
2585
|
+
example_request="Trace the history and renames of AuthService across git commits.",
|
|
2586
|
+
example_call='trace_symbol_history(symbol="AuthService")',
|
|
2587
|
+
example_interpretation="Inspect events list for INTRODUCED, MODIFIED, MOVED, or RENAMED occurrences.",
|
|
2588
|
+
),
|
|
2589
|
+
# 62. detect_semantic_conflicts
|
|
2590
|
+
ToolCapabilitySpec(
|
|
2591
|
+
tool_name="detect_semantic_conflicts",
|
|
2592
|
+
capability="semantic_merge_conflict_detection",
|
|
2593
|
+
description=(
|
|
2594
|
+
"Detect semantic and contract conflicts between Git branches (calling deleted symbols, broken call signatures, or missing route handlers). "
|
|
2595
|
+
"Use when verifying whether branches can be safely merged without semantic breakage. "
|
|
2596
|
+
"Does not report false conflicts when branches are structurally compatible."
|
|
2597
|
+
),
|
|
2598
|
+
task_types=(
|
|
2599
|
+
AgentTaskCategory.CHANGE_IMPACT.value,
|
|
2600
|
+
AgentTaskCategory.ARCHITECTURE.value,
|
|
2601
|
+
AgentTaskCategory.DEBUG.value,
|
|
2602
|
+
),
|
|
2603
|
+
required_inputs=(),
|
|
2604
|
+
optional_inputs=("base_branch", "head_branch"),
|
|
2605
|
+
expected_output_type="SemanticConflictResult",
|
|
2606
|
+
relationship_types_returned=("CALLS", "IMPORTS", "ROUTES_TO"),
|
|
2607
|
+
evidence_guarantees="Deterministic cross-branch AST call-site and definition verification.",
|
|
2608
|
+
does_not_prove="Does not execute code; reports contract incompatibility between revisions.",
|
|
2609
|
+
useful_situations=(
|
|
2610
|
+
"Checking if a feature branch introduces calls to symbols deleted or modified on main",
|
|
2611
|
+
"Pre-merge validation before merging PRs to prevent semantic regression",
|
|
2612
|
+
),
|
|
2613
|
+
avoid_when=(
|
|
2614
|
+
"Branches are known to be identical or when checking working tree cleanliness",
|
|
2615
|
+
),
|
|
2616
|
+
profiles=("full",),
|
|
2617
|
+
minimal_invocation='detect_semantic_conflicts(base_branch="main", head_branch="feature")',
|
|
2618
|
+
advanced_invocation='detect_semantic_conflicts(base_branch="main", head_branch="HEAD")',
|
|
2619
|
+
result_fields=("base_branch", "head_branch", "has_conflicts", "total_conflicts", "conflicts", "summary"),
|
|
2620
|
+
typical_followup="get_file or get_symbol on conflicted symbols",
|
|
2621
|
+
common_mistakes=(
|
|
2622
|
+
"Assuming Git text merge passes mean no semantic conflicts; semantic conflicts occur even with 0 text conflicts",
|
|
2623
|
+
),
|
|
2624
|
+
example_request="Are there any semantic merge conflicts between main and this branch?",
|
|
2625
|
+
example_call='detect_semantic_conflicts(base_branch="main", head_branch="HEAD")',
|
|
2626
|
+
example_interpretation="Inspect has_conflicts and conflicts list for DELETED_SYMBOL_REFERENCED or CALL_SIGNATURE_MISMATCH.",
|
|
2627
|
+
),
|
|
2393
2628
|
)
|
|
2394
2629
|
|
|
2395
2630
|
_TOOL_BY_NAME: dict[str, ToolCapabilitySpec] = {t.tool_name: t for t in TOOL_CAPABILITY_REGISTRY}
|
|
@@ -2449,10 +2684,10 @@ CAPABILITY_MATRIX: dict[AgentTaskCategory, TaskCapabilityRule] = {
|
|
|
2449
2684
|
category=AgentTaskCategory.CHANGE_IMPACT,
|
|
2450
2685
|
should_use_codegraph=True,
|
|
2451
2686
|
primary_tool="get_git_impact",
|
|
2452
|
-
acceptable_tools=("get_git_impact", "analyze_impact", "analyze_change_impact", "get_dependents", "get_callers", "find_related_tests", "find_tests", "get_context"),
|
|
2453
|
-
recommended_sequence=("
|
|
2687
|
+
acceptable_tools=("get_git_impact", "get_change_impact", "compare_git", "get_git_state", "check_context_freshness", "trace_symbol_history", "analyze_impact", "analyze_change_impact", "get_dependents", "get_callers", "find_related_tests", "find_tests", "get_context"),
|
|
2688
|
+
recommended_sequence=("get_git_state", "compare_git", "get_change_impact"),
|
|
2454
2689
|
direct_inspection_acceptable=False,
|
|
2455
|
-
rationale="Use
|
|
2690
|
+
rationale="Use get_git_state, compare_git, and get_change_impact to deterministically compute structural diffs, affected callers, routes, packages, and tests.",
|
|
2456
2691
|
),
|
|
2457
2692
|
AgentTaskCategory.TEST_DISCOVERY: TaskCapabilityRule(
|
|
2458
2693
|
category=AgentTaskCategory.TEST_DISCOVERY,
|
|
@@ -2696,12 +2931,15 @@ MCP_PROFILE_RECOMMENDATIONS: dict[str, dict[str, object]] = {
|
|
|
2696
2931
|
},
|
|
2697
2932
|
"full": {
|
|
2698
2933
|
"profile": "full",
|
|
2699
|
-
"tool_count":
|
|
2934
|
+
"tool_count": 62,
|
|
2700
2935
|
"recommended_for": "Complete diagnostic, database intelligence, runtime reconciliation, benchmark, and repository administration sessions",
|
|
2701
2936
|
"included_tools": [
|
|
2702
2937
|
"analyze_change_impact",
|
|
2703
2938
|
"analyze_impact",
|
|
2939
|
+
"check_context_freshness",
|
|
2940
|
+
"compare_git",
|
|
2704
2941
|
"compile_task",
|
|
2942
|
+
"detect_semantic_conflicts",
|
|
2705
2943
|
"find_callees",
|
|
2706
2944
|
"find_callers",
|
|
2707
2945
|
"find_db_callers",
|
|
@@ -2721,6 +2959,7 @@ MCP_PROFILE_RECOMMENDATIONS: dict[str, dict[str, object]] = {
|
|
|
2721
2959
|
"get_call_graph",
|
|
2722
2960
|
"get_callees",
|
|
2723
2961
|
"get_callers",
|
|
2962
|
+
"get_change_impact",
|
|
2724
2963
|
"get_context",
|
|
2725
2964
|
"get_db_impact",
|
|
2726
2965
|
"get_db_schema",
|
|
@@ -2733,6 +2972,7 @@ MCP_PROFILE_RECOMMENDATIONS: dict[str, dict[str, object]] = {
|
|
|
2733
2972
|
"get_file_history",
|
|
2734
2973
|
"get_file_symbols",
|
|
2735
2974
|
"get_git_impact",
|
|
2975
|
+
"get_git_state",
|
|
2736
2976
|
"get_graph",
|
|
2737
2977
|
"get_imports",
|
|
2738
2978
|
"get_project_structure",
|
|
@@ -2754,6 +2994,7 @@ MCP_PROFILE_RECOMMENDATIONS: dict[str, dict[str, object]] = {
|
|
|
2754
2994
|
"trace_call",
|
|
2755
2995
|
"trace_flow",
|
|
2756
2996
|
"trace_path",
|
|
2997
|
+
"trace_symbol_history",
|
|
2757
2998
|
"verify_evidence",
|
|
2758
2999
|
],
|
|
2759
3000
|
},
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
"""Branch Architecture Comparison Engine.
|
|
2
|
+
|
|
3
|
+
Provides hierarchical structural comparisons between Git branches:
|
|
4
|
+
package
|
|
5
|
+
↓
|
|
6
|
+
module
|
|
7
|
+
↓
|
|
8
|
+
symbol
|
|
9
|
+
↓
|
|
10
|
+
relationships (routes, tests, DB writes, calls)
|
|
11
|
+
|
|
12
|
+
Supports:
|
|
13
|
+
- deterministic ordering
|
|
14
|
+
- token-budget awareness (max_tokens)
|
|
15
|
+
- cursor-based pagination
|
|
16
|
+
- explicit summary of changed routes, services, database models, and affected tests
|
|
17
|
+
"""
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import sqlite3
|
|
21
|
+
from dataclasses import dataclass, field
|
|
22
|
+
from pathlib import Path
|
|
23
|
+
from typing import Any
|
|
24
|
+
|
|
25
|
+
from codegraph.monorepo import detect_workspace
|
|
26
|
+
from codegraph.structural_diff import compare_branches
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@dataclass
|
|
30
|
+
class SymbolChangeItem:
|
|
31
|
+
name: str
|
|
32
|
+
kind: str
|
|
33
|
+
change_type: str # "ADDED" | "MODIFIED" | "REMOVED"
|
|
34
|
+
line: int | None
|
|
35
|
+
relationships: list[str] = field(default_factory=list)
|
|
36
|
+
|
|
37
|
+
def as_dict(self) -> dict[str, Any]:
|
|
38
|
+
return {
|
|
39
|
+
"name": self.name,
|
|
40
|
+
"kind": self.kind,
|
|
41
|
+
"change_type": self.change_type,
|
|
42
|
+
"line": self.line,
|
|
43
|
+
"relationships": self.relationships,
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@dataclass
|
|
48
|
+
class ModuleChangeItem:
|
|
49
|
+
module: str
|
|
50
|
+
file_path: str
|
|
51
|
+
symbols: list[SymbolChangeItem] = field(default_factory=list)
|
|
52
|
+
|
|
53
|
+
def as_dict(self) -> dict[str, Any]:
|
|
54
|
+
return {
|
|
55
|
+
"module": self.module,
|
|
56
|
+
"file_path": self.file_path,
|
|
57
|
+
"symbols": [s.as_dict() for s in self.symbols],
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@dataclass
|
|
62
|
+
class PackageChangeItem:
|
|
63
|
+
package_id: str
|
|
64
|
+
modules: list[ModuleChangeItem] = field(default_factory=list)
|
|
65
|
+
|
|
66
|
+
def as_dict(self) -> dict[str, Any]:
|
|
67
|
+
return {
|
|
68
|
+
"package_id": self.package_id,
|
|
69
|
+
"modules": [m.as_dict() for m in self.modules],
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
@dataclass
|
|
74
|
+
class BranchArchitectureReport:
|
|
75
|
+
base_branch: str
|
|
76
|
+
head_branch: str
|
|
77
|
+
packages: list[PackageChangeItem] = field(default_factory=list)
|
|
78
|
+
changed_routes: list[dict[str, str]] = field(default_factory=list)
|
|
79
|
+
changed_services: list[str] = field(default_factory=list)
|
|
80
|
+
changed_db: list[dict[str, str]] = field(default_factory=list)
|
|
81
|
+
affected_tests: list[str] = field(default_factory=list)
|
|
82
|
+
next_cursor: str | None = None
|
|
83
|
+
total_packages: int = 0
|
|
84
|
+
total_symbols: int = 0
|
|
85
|
+
estimated_tokens: int = 0
|
|
86
|
+
|
|
87
|
+
def as_dict(self) -> dict[str, Any]:
|
|
88
|
+
return {
|
|
89
|
+
"base_branch": self.base_branch,
|
|
90
|
+
"head_branch": self.head_branch,
|
|
91
|
+
"packages": [p.as_dict() for p in self.packages],
|
|
92
|
+
"changed_routes": self.changed_routes,
|
|
93
|
+
"changed_services": self.changed_services,
|
|
94
|
+
"changed_db": self.changed_db,
|
|
95
|
+
"affected_tests": self.affected_tests,
|
|
96
|
+
"next_cursor": self.next_cursor,
|
|
97
|
+
"total_packages": self.total_packages,
|
|
98
|
+
"total_symbols": self.total_symbols,
|
|
99
|
+
"estimated_tokens": self.estimated_tokens,
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def compare_branch_architecture(
|
|
104
|
+
repository: Path,
|
|
105
|
+
base_branch: str = "main",
|
|
106
|
+
head_branch: str = "HEAD",
|
|
107
|
+
con: sqlite3.Connection | None = None,
|
|
108
|
+
limit: int = 50,
|
|
109
|
+
cursor: str | None = None,
|
|
110
|
+
max_tokens: int = 4000,
|
|
111
|
+
) -> BranchArchitectureReport:
|
|
112
|
+
"""Compare two branches and return hierarchical architecture diff."""
|
|
113
|
+
if base_branch == head_branch:
|
|
114
|
+
return BranchArchitectureReport(base_branch=base_branch, head_branch=head_branch)
|
|
115
|
+
|
|
116
|
+
# 1. Run structural branch diff
|
|
117
|
+
diff_report = compare_branches(repository, base_branch=base_branch, head_branch=head_branch, con=con)
|
|
118
|
+
|
|
119
|
+
# 2. Workspace package mapper
|
|
120
|
+
ws = detect_workspace(repository, con=con)
|
|
121
|
+
|
|
122
|
+
# 3. Build hierarchical package -> module -> symbol map
|
|
123
|
+
package_map: dict[str, dict[str, list[SymbolChangeItem]]] = {}
|
|
124
|
+
services: set[str] = set()
|
|
125
|
+
|
|
126
|
+
all_symbols = diff_report.added_symbols + diff_report.changed_symbols + diff_report.removed_symbols
|
|
127
|
+
# Sort deterministically by path and name
|
|
128
|
+
all_symbols.sort(key=lambda s: (s.path, s.name))
|
|
129
|
+
|
|
130
|
+
# Apply cursor pagination if cursor provided
|
|
131
|
+
start_idx = 0
|
|
132
|
+
if cursor and cursor.isdigit():
|
|
133
|
+
start_idx = int(cursor)
|
|
134
|
+
|
|
135
|
+
paged_symbols = all_symbols[start_idx : start_idx + limit]
|
|
136
|
+
next_cursor = str(start_idx + limit) if (start_idx + limit) < len(all_symbols) else None
|
|
137
|
+
|
|
138
|
+
for s in paged_symbols:
|
|
139
|
+
pkg = ws.get_package_for_file(s.path)
|
|
140
|
+
pkg_id = pkg.package_id if pkg else "root"
|
|
141
|
+
|
|
142
|
+
if pkg_id not in package_map:
|
|
143
|
+
package_map[pkg_id] = {}
|
|
144
|
+
if s.path not in package_map[pkg_id]:
|
|
145
|
+
package_map[pkg_id][s.path] = []
|
|
146
|
+
|
|
147
|
+
# Find associated relationships
|
|
148
|
+
rels: list[str] = []
|
|
149
|
+
for r in diff_report.changed_routes:
|
|
150
|
+
if r.handler == s.name:
|
|
151
|
+
rels.append(f"{r.change_type} ROUTE {r.method} {r.path}")
|
|
152
|
+
for db in diff_report.db_impact:
|
|
153
|
+
if db.get("symbol") == s.name:
|
|
154
|
+
rels.append(f"{db.get('change_type')} DB {db.get('operation')} {db.get('table')}")
|
|
155
|
+
|
|
156
|
+
if "service" in s.name.lower() or "service" in s.path.lower():
|
|
157
|
+
services.add(s.name)
|
|
158
|
+
|
|
159
|
+
package_map[pkg_id][s.path].append(
|
|
160
|
+
SymbolChangeItem(
|
|
161
|
+
name=s.name,
|
|
162
|
+
kind=s.kind,
|
|
163
|
+
change_type=s.change_type,
|
|
164
|
+
line=s.new_line or s.old_line,
|
|
165
|
+
relationships=rels,
|
|
166
|
+
)
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
# Build ordered PackageChangeItem objects
|
|
170
|
+
package_items: list[PackageChangeItem] = []
|
|
171
|
+
total_sym_count = 0
|
|
172
|
+
|
|
173
|
+
for pkg_id in sorted(package_map.keys()):
|
|
174
|
+
module_items: list[ModuleChangeItem] = []
|
|
175
|
+
for f_path in sorted(package_map[pkg_id].keys()):
|
|
176
|
+
mod_name = Path(f_path).stem
|
|
177
|
+
sym_items = package_map[pkg_id][f_path]
|
|
178
|
+
total_sym_count += len(sym_items)
|
|
179
|
+
module_items.append(ModuleChangeItem(module=mod_name, file_path=f_path, symbols=sym_items))
|
|
180
|
+
package_items.append(PackageChangeItem(package_id=pkg_id, modules=module_items))
|
|
181
|
+
|
|
182
|
+
# Rough token estimation: ~25 tokens per symbol + overhead
|
|
183
|
+
token_est = total_sym_count * 25 + len(diff_report.changed_routes) * 15 + 150
|
|
184
|
+
|
|
185
|
+
return BranchArchitectureReport(
|
|
186
|
+
base_branch=base_branch,
|
|
187
|
+
head_branch=head_branch,
|
|
188
|
+
packages=package_items,
|
|
189
|
+
changed_routes=[r.as_dict() for r in diff_report.changed_routes],
|
|
190
|
+
changed_services=sorted(services),
|
|
191
|
+
changed_db=diff_report.db_impact,
|
|
192
|
+
affected_tests=diff_report.affected_tests,
|
|
193
|
+
next_cursor=next_cursor,
|
|
194
|
+
total_packages=len(package_items),
|
|
195
|
+
total_symbols=total_sym_count,
|
|
196
|
+
estimated_tokens=token_est,
|
|
197
|
+
)
|