codegraph-engine 3.0.0__1-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 +37 -0
- codegraph/agent.py +26 -0
- codegraph/agent_brain.py +2212 -0
- codegraph/agent_capabilities.py +4058 -0
- codegraph/agent_rules.py +531 -0
- codegraph/api_drift.py +455 -0
- codegraph/architecture.py +371 -0
- codegraph/async_queue/__init__.py +50 -0
- codegraph/async_queue/detectors/__init__.py +48 -0
- codegraph/async_queue/detectors/celery.py +160 -0
- codegraph/async_queue/detectors/kafka.py +168 -0
- codegraph/async_queue/detectors/rabbitmq.py +116 -0
- codegraph/async_queue/detectors/redis.py +130 -0
- codegraph/async_queue/detectors/sqs.py +135 -0
- codegraph/async_queue/models.py +161 -0
- codegraph/async_queue/otel_models.py +74 -0
- codegraph/async_queue/otel_parser.py +337 -0
- codegraph/async_queue/reconciler.py +427 -0
- codegraph/async_queue/scanner.py +291 -0
- codegraph/async_queue/traversal.py +447 -0
- codegraph/audit.py +106 -0
- codegraph/binding_resolver.py +616 -0
- codegraph/branch_comparison.py +197 -0
- codegraph/cache.py +95 -0
- codegraph/change_impact.py +547 -0
- codegraph/cli.py +2436 -0
- codegraph/cli_output.py +205 -0
- codegraph/config.py +45 -0
- codegraph/constraints.py +238 -0
- codegraph/context.py +2182 -0
- codegraph/context_freshness.py +351 -0
- codegraph/database/__init__.py +72 -0
- codegraph/database/extractor.py +3651 -0
- codegraph/database/interrogation.py +1494 -0
- codegraph/database/models.py +310 -0
- codegraph/database/schema_drift.py +862 -0
- codegraph/epistemic.py +200 -0
- codegraph/errors.py +282 -0
- codegraph/evidence/__init__.py +15 -0
- codegraph/evidence/citations.py +418 -0
- codegraph/evidence_contract.py +486 -0
- codegraph/frameworks.py +960 -0
- codegraph/frameworks_nestjs.py +502 -0
- codegraph/frameworks_nextjs.py +337 -0
- codegraph/frameworks_react.py +457 -0
- codegraph/freshness.py +335 -0
- codegraph/frontend_html.py +109 -0
- codegraph/git.py +362 -0
- codegraph/git_state.py +545 -0
- codegraph/graph/__init__.py +46 -0
- codegraph/graph/models.py +56 -0
- codegraph/graph/traversal.py +1993 -0
- codegraph/indexing/__init__.py +24 -0
- codegraph/indexing/classifier.py +564 -0
- codegraph/indexing/indexer.py +2578 -0
- codegraph/indexing/models.py +427 -0
- codegraph/indexing/parser.py +3404 -0
- codegraph/indexing/pool.py +136 -0
- codegraph/indexing/scanner.py +211 -0
- codegraph/indexing/telemetry.py +326 -0
- codegraph/indexing/templates.py +418 -0
- codegraph/indexing/test_framework.py +116 -0
- codegraph/installer.py +1702 -0
- codegraph/interrogation.py +2355 -0
- codegraph/llm/__init__.py +3 -0
- codegraph/llm/base.py +15 -0
- codegraph/llm/context.py +20 -0
- codegraph/mcp/__init__.py +3 -0
- codegraph/mcp/server.py +1476 -0
- codegraph/mcp_diagnostics.py +882 -0
- codegraph/memory/__init__.py +3 -0
- codegraph/memory/store.py +46 -0
- codegraph/models.py +310 -0
- codegraph/monorepo.py +925 -0
- codegraph/observability.py +262 -0
- codegraph/optimizer.py +1014 -0
- codegraph/planner.py +417 -0
- codegraph/process_lifecycle.py +1153 -0
- codegraph/py.typed +1 -0
- codegraph/query_expansion.py +199 -0
- codegraph/ranking.py +408 -0
- codegraph/refactor/__init__.py +36 -0
- codegraph/refactor/models.py +114 -0
- codegraph/refactor/renamer.py +844 -0
- codegraph/refactor/transactions.py +414 -0
- codegraph/resolver.py +3968 -0
- codegraph/resources/__init__.py +46 -0
- codegraph/resources/cache.py +117 -0
- codegraph/resources/coalescer.py +83 -0
- codegraph/resources/debouncer.py +98 -0
- codegraph/resources/governor.py +219 -0
- codegraph/resources/monitor.py +102 -0
- codegraph/resources/policy.py +123 -0
- codegraph/retrieval_policy.py +320 -0
- codegraph/roblox.py +2174 -0
- codegraph/route_composer.py +363 -0
- codegraph/route_topology.py +172 -0
- codegraph/runtime/__init__.py +51 -0
- codegraph/runtime/collector.py +155 -0
- codegraph/runtime/db_watcher.py +191 -0
- codegraph/runtime/ingestor.py +692 -0
- codegraph/runtime/models.py +130 -0
- codegraph/runtime/port_inspector.py +304 -0
- codegraph/runtime/reconciliation.py +492 -0
- codegraph/runtime/runner.py +353 -0
- codegraph/runtime/trace_linker.py +175 -0
- codegraph/search/__init__.py +25 -0
- codegraph/search/hybrid.py +846 -0
- codegraph/search/semantic.py +28 -0
- codegraph/security/__init__.py +51 -0
- codegraph/security/paths.py +189 -0
- codegraph/security/redaction.py +633 -0
- codegraph/semantic_conflicts.py +270 -0
- codegraph/semantic_decorators.py +372 -0
- codegraph/structural_diff.py +542 -0
- codegraph/styles.py +144 -0
- codegraph/symbol_history.py +277 -0
- codegraph/target_resolver.py +348 -0
- codegraph/task.py +637 -0
- codegraph/tool_selection_eval.py +1158 -0
- codegraph/tsconfig.py +236 -0
- codegraph/ui/__init__.py +6 -0
- codegraph/ui/server.py +755 -0
- codegraph/watcher.py +323 -0
- codegraph_engine-3.0.0.dist-info/METADATA +859 -0
- codegraph_engine-3.0.0.dist-info/RECORD +130 -0
- codegraph_engine-3.0.0.dist-info/WHEEL +6 -0
- codegraph_engine-3.0.0.dist-info/entry_points.txt +2 -0
- codegraph_engine-3.0.0.dist-info/licenses/LICENSE +21 -0
- codegraph_engine-3.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,1158 @@
|
|
|
1
|
+
"""Agent Tool-Selection Evaluation Harness, Critical A/B Test & Antigravity Trace Evaluation.
|
|
2
|
+
|
|
3
|
+
Solves:
|
|
4
|
+
- Section 15: 32-task tool-selection evaluation harness across symbol lookup, caller/callee,
|
|
5
|
+
route tracing, DI, registries, events, tests, package boundaries, change impact,
|
|
6
|
+
architecture, debugging, and single-file local edits.
|
|
7
|
+
- Section 16: Critical A/B comparison (MODE A: MCP only vs MODE B: MCP + CodeGraph rules).
|
|
8
|
+
- Section 17: Antigravity-specific real-world authentication trace evaluation (TEST A, TEST B, TEST C)
|
|
9
|
+
executing real CodeGraph queries against an indexed FastAPI + DI + Repository + Test fixture.
|
|
10
|
+
- Section 18: Rule robustness checks (no trivial-edit CodeGraph calls, no infinite loops,
|
|
11
|
+
no repeated resolve_symbol, no stale-context acceptance, UNKNOWN/POSSIBLE preservation).
|
|
12
|
+
"""
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import tempfile
|
|
16
|
+
from dataclasses import dataclass
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
|
|
19
|
+
from codegraph.agent_capabilities import (
|
|
20
|
+
AgentTaskCategory,
|
|
21
|
+
classify_agent_task,
|
|
22
|
+
evaluate_fallback_policy,
|
|
23
|
+
)
|
|
24
|
+
from codegraph.agent_rules import render_agent_rules, render_antigravity_skill
|
|
25
|
+
from codegraph.context import get_context
|
|
26
|
+
from codegraph.graph import find_related_tests
|
|
27
|
+
from codegraph.indexing import Indexer
|
|
28
|
+
from codegraph.interrogation import (
|
|
29
|
+
list_routes,
|
|
30
|
+
resolve_symbol,
|
|
31
|
+
trace_path,
|
|
32
|
+
)
|
|
33
|
+
from codegraph.observability import ToolUsageRecord, get_global_metrics
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@dataclass(frozen=True)
|
|
37
|
+
class ToolSelectionTask:
|
|
38
|
+
"""Single task in the 32-task tool-selection evaluation suite."""
|
|
39
|
+
|
|
40
|
+
task_id: str
|
|
41
|
+
domain: str
|
|
42
|
+
prompt: str
|
|
43
|
+
expected_task_type: str
|
|
44
|
+
expected_primary_tool: str | None
|
|
45
|
+
acceptable_tool_set: tuple[str, ...]
|
|
46
|
+
file_count_hint: int = 2
|
|
47
|
+
naive_direct_reads_without_cg: int = 5
|
|
48
|
+
|
|
49
|
+
def as_dict(self) -> dict[str, object]:
|
|
50
|
+
return {
|
|
51
|
+
"task_id": self.task_id,
|
|
52
|
+
"domain": self.domain,
|
|
53
|
+
"prompt": self.prompt,
|
|
54
|
+
"expected_task_type": self.expected_task_type,
|
|
55
|
+
"expected_primary_tool": self.expected_primary_tool,
|
|
56
|
+
"acceptable_tool_set": list(self.acceptable_tool_set),
|
|
57
|
+
"file_count_hint": self.file_count_hint,
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@dataclass(frozen=True)
|
|
62
|
+
class ToolSelectionTaskResult:
|
|
63
|
+
"""Recorded evaluation result for a single tool-selection task."""
|
|
64
|
+
|
|
65
|
+
task_id: str
|
|
66
|
+
domain: str
|
|
67
|
+
expected_task_type: str
|
|
68
|
+
predicted_task_type: str
|
|
69
|
+
expected_primary_tool: str | None
|
|
70
|
+
acceptable_tool_set: tuple[str, ...]
|
|
71
|
+
codegraph_used: bool
|
|
72
|
+
first_codegraph_tool: str | None
|
|
73
|
+
tool_sequence: tuple[str, ...]
|
|
74
|
+
total_codegraph_calls: int
|
|
75
|
+
unnecessary_codegraph_calls: int
|
|
76
|
+
direct_file_reads: int
|
|
77
|
+
total_tool_calls: int
|
|
78
|
+
first_tool_accurate: bool
|
|
79
|
+
task_accuracy: float
|
|
80
|
+
unsupported_claims: int
|
|
81
|
+
|
|
82
|
+
def as_dict(self) -> dict[str, object]:
|
|
83
|
+
return {
|
|
84
|
+
"task_id": self.task_id,
|
|
85
|
+
"domain": self.domain,
|
|
86
|
+
"expected_task_type": self.expected_task_type,
|
|
87
|
+
"predicted_task_type": self.predicted_task_type,
|
|
88
|
+
"expected_primary_tool": self.expected_primary_tool,
|
|
89
|
+
"acceptable_tool_set": list(self.acceptable_tool_set),
|
|
90
|
+
"codegraph_used": self.codegraph_used,
|
|
91
|
+
"first_codegraph_tool": self.first_codegraph_tool,
|
|
92
|
+
"tool_sequence": list(self.tool_sequence),
|
|
93
|
+
"total_codegraph_calls": self.total_codegraph_calls,
|
|
94
|
+
"unnecessary_codegraph_calls": self.unnecessary_codegraph_calls,
|
|
95
|
+
"direct_file_reads": self.direct_file_reads,
|
|
96
|
+
"total_tool_calls": self.total_tool_calls,
|
|
97
|
+
"first_tool_accurate": self.first_tool_accurate,
|
|
98
|
+
"task_accuracy": self.task_accuracy,
|
|
99
|
+
"unsupported_claims": self.unsupported_claims,
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
# ---------------------------------------------------------------------------
|
|
104
|
+
# 32 Canonical Benchmark Tasks Across All Required Categories
|
|
105
|
+
# ---------------------------------------------------------------------------
|
|
106
|
+
|
|
107
|
+
DEFAULT_TOOL_SELECTION_TASKS: tuple[ToolSelectionTask, ...] = (
|
|
108
|
+
# 1-3: Symbol Lookup
|
|
109
|
+
ToolSelectionTask(
|
|
110
|
+
task_id="TS-01",
|
|
111
|
+
domain="symbol_lookup",
|
|
112
|
+
prompt="Where is class AuthService defined in the repository?",
|
|
113
|
+
expected_task_type=AgentTaskCategory.SYMBOL_LOOKUP.value,
|
|
114
|
+
expected_primary_tool="resolve_symbol",
|
|
115
|
+
acceptable_tool_set=("resolve_symbol", "search_symbols", "get_symbol"),
|
|
116
|
+
naive_direct_reads_without_cg=4,
|
|
117
|
+
),
|
|
118
|
+
ToolSelectionTask(
|
|
119
|
+
task_id="TS-02",
|
|
120
|
+
domain="symbol_lookup",
|
|
121
|
+
prompt="Find symbol definition of verify_jwt_token",
|
|
122
|
+
expected_task_type=AgentTaskCategory.SYMBOL_LOOKUP.value,
|
|
123
|
+
expected_primary_tool="resolve_symbol",
|
|
124
|
+
acceptable_tool_set=("resolve_symbol", "search_symbols", "get_symbol"),
|
|
125
|
+
naive_direct_reads_without_cg=4,
|
|
126
|
+
),
|
|
127
|
+
ToolSelectionTask(
|
|
128
|
+
task_id="TS-03",
|
|
129
|
+
domain="symbol_lookup",
|
|
130
|
+
prompt="Locate function signature of calculate_invoice_total",
|
|
131
|
+
expected_task_type=AgentTaskCategory.SYMBOL_LOOKUP.value,
|
|
132
|
+
expected_primary_tool="resolve_symbol",
|
|
133
|
+
acceptable_tool_set=("resolve_symbol", "search_symbols", "get_symbol"),
|
|
134
|
+
naive_direct_reads_without_cg=3,
|
|
135
|
+
),
|
|
136
|
+
# 4-6: Caller / Callee Relationships
|
|
137
|
+
ToolSelectionTask(
|
|
138
|
+
task_id="TS-04",
|
|
139
|
+
domain="caller_callee",
|
|
140
|
+
prompt="What calls verify_password across the codebase?",
|
|
141
|
+
expected_task_type=AgentTaskCategory.RELATIONSHIP.value,
|
|
142
|
+
expected_primary_tool="get_callers",
|
|
143
|
+
acceptable_tool_set=("resolve_symbol", "get_callers"),
|
|
144
|
+
naive_direct_reads_without_cg=6,
|
|
145
|
+
),
|
|
146
|
+
ToolSelectionTask(
|
|
147
|
+
task_id="TS-05",
|
|
148
|
+
domain="caller_callee",
|
|
149
|
+
prompt="What does process_checkout call downstream?",
|
|
150
|
+
expected_task_type=AgentTaskCategory.RELATIONSHIP.value,
|
|
151
|
+
expected_primary_tool="get_callees",
|
|
152
|
+
acceptable_tool_set=("resolve_symbol", "get_callees"),
|
|
153
|
+
naive_direct_reads_without_cg=5,
|
|
154
|
+
),
|
|
155
|
+
ToolSelectionTask(
|
|
156
|
+
task_id="TS-06",
|
|
157
|
+
domain="caller_callee",
|
|
158
|
+
prompt="Who calls TokenValidator.validate in the service layer?",
|
|
159
|
+
expected_task_type=AgentTaskCategory.RELATIONSHIP.value,
|
|
160
|
+
expected_primary_tool="get_callers",
|
|
161
|
+
acceptable_tool_set=("resolve_symbol", "get_callers"),
|
|
162
|
+
naive_direct_reads_without_cg=6,
|
|
163
|
+
),
|
|
164
|
+
# 7-9: Route Tracing & Discovery
|
|
165
|
+
ToolSelectionTask(
|
|
166
|
+
task_id="TS-07",
|
|
167
|
+
domain="route_tracing",
|
|
168
|
+
prompt="Which route handles POST /api/v1/auth/login?",
|
|
169
|
+
expected_task_type=AgentTaskCategory.ROUTE_DISCOVERY.value,
|
|
170
|
+
expected_primary_tool="list_routes",
|
|
171
|
+
acceptable_tool_set=("list_routes", "resolve_symbol", "trace_path"),
|
|
172
|
+
naive_direct_reads_without_cg=5,
|
|
173
|
+
),
|
|
174
|
+
ToolSelectionTask(
|
|
175
|
+
task_id="TS-08",
|
|
176
|
+
domain="route_tracing",
|
|
177
|
+
prompt="Trace the execution path from route /api/v1/users/me to the database query",
|
|
178
|
+
expected_task_type=AgentTaskCategory.TRACE.value,
|
|
179
|
+
expected_primary_tool="trace_path",
|
|
180
|
+
acceptable_tool_set=("list_routes", "resolve_symbol", "trace_path", "get_context"),
|
|
181
|
+
naive_direct_reads_without_cg=7,
|
|
182
|
+
),
|
|
183
|
+
ToolSelectionTask(
|
|
184
|
+
task_id="TS-09",
|
|
185
|
+
domain="route_tracing",
|
|
186
|
+
prompt="Which HTTP route reaches handler create_order_endpoint?",
|
|
187
|
+
expected_task_type=AgentTaskCategory.TRACE.value,
|
|
188
|
+
expected_primary_tool="trace_path",
|
|
189
|
+
acceptable_tool_set=("list_routes", "resolve_symbol", "trace_path"),
|
|
190
|
+
naive_direct_reads_without_cg=5,
|
|
191
|
+
),
|
|
192
|
+
# 10-12: Dependency Injection (DI)
|
|
193
|
+
ToolSelectionTask(
|
|
194
|
+
task_id="TS-10",
|
|
195
|
+
domain="di",
|
|
196
|
+
prompt="How does UserRepository get injected into UserController?",
|
|
197
|
+
expected_task_type=AgentTaskCategory.DEBUG.value,
|
|
198
|
+
expected_primary_tool="get_context",
|
|
199
|
+
acceptable_tool_set=("resolve_symbol", "get_context", "get_references"),
|
|
200
|
+
naive_direct_reads_without_cg=6,
|
|
201
|
+
),
|
|
202
|
+
ToolSelectionTask(
|
|
203
|
+
task_id="TS-11",
|
|
204
|
+
domain="di",
|
|
205
|
+
prompt="Debug why FastAPI Depends(get_current_user) provider is failing in profile endpoint",
|
|
206
|
+
expected_task_type=AgentTaskCategory.DEBUG.value,
|
|
207
|
+
expected_primary_tool="get_context",
|
|
208
|
+
acceptable_tool_set=("resolve_symbol", "get_context"),
|
|
209
|
+
naive_direct_reads_without_cg=6,
|
|
210
|
+
),
|
|
211
|
+
ToolSelectionTask(
|
|
212
|
+
task_id="TS-12",
|
|
213
|
+
domain="di",
|
|
214
|
+
prompt="Which provider for PaymentGateway is bound in the DI container?",
|
|
215
|
+
expected_task_type=AgentTaskCategory.DEBUG.value,
|
|
216
|
+
expected_primary_tool="get_context",
|
|
217
|
+
acceptable_tool_set=("resolve_symbol", "get_context", "get_references"),
|
|
218
|
+
naive_direct_reads_without_cg=5,
|
|
219
|
+
),
|
|
220
|
+
# 13-14: Registries & Dispatch
|
|
221
|
+
ToolSelectionTask(
|
|
222
|
+
task_id="TS-13",
|
|
223
|
+
domain="registries",
|
|
224
|
+
prompt="What registered handlers exist in command_registry and where is it referenced?",
|
|
225
|
+
expected_task_type=AgentTaskCategory.RELATIONSHIP.value,
|
|
226
|
+
expected_primary_tool="get_references",
|
|
227
|
+
acceptable_tool_set=("resolve_symbol", "get_references", "get_context"),
|
|
228
|
+
naive_direct_reads_without_cg=6,
|
|
229
|
+
),
|
|
230
|
+
ToolSelectionTask(
|
|
231
|
+
task_id="TS-14",
|
|
232
|
+
domain="registries",
|
|
233
|
+
prompt="Where is payment_plugin dispatches to target handler registered?",
|
|
234
|
+
expected_task_type=AgentTaskCategory.RELATIONSHIP.value,
|
|
235
|
+
expected_primary_tool="get_references",
|
|
236
|
+
acceptable_tool_set=("resolve_symbol", "get_references", "get_context"),
|
|
237
|
+
naive_direct_reads_without_cg=5,
|
|
238
|
+
),
|
|
239
|
+
# 15-16: Events & Task Decorators
|
|
240
|
+
ToolSelectionTask(
|
|
241
|
+
task_id="TS-15",
|
|
242
|
+
domain="events",
|
|
243
|
+
prompt="What event listeners for OrderCreated are registered across modules?",
|
|
244
|
+
expected_task_type=AgentTaskCategory.RELATIONSHIP.value,
|
|
245
|
+
expected_primary_tool="get_references",
|
|
246
|
+
acceptable_tool_set=("resolve_symbol", "get_references", "get_context"),
|
|
247
|
+
naive_direct_reads_without_cg=6,
|
|
248
|
+
),
|
|
249
|
+
ToolSelectionTask(
|
|
250
|
+
task_id="TS-16",
|
|
251
|
+
domain="events",
|
|
252
|
+
prompt="Trace how Celery task send_welcome_email reaches the SMTP client",
|
|
253
|
+
expected_task_type=AgentTaskCategory.TRACE.value,
|
|
254
|
+
expected_primary_tool="trace_path",
|
|
255
|
+
acceptable_tool_set=("resolve_symbol", "trace_path", "get_context"),
|
|
256
|
+
naive_direct_reads_without_cg=5,
|
|
257
|
+
),
|
|
258
|
+
# 17-19: Test Discovery
|
|
259
|
+
ToolSelectionTask(
|
|
260
|
+
task_id="TS-17",
|
|
261
|
+
domain="tests",
|
|
262
|
+
prompt="What tests cover BillingService.charge_card?",
|
|
263
|
+
expected_task_type=AgentTaskCategory.TEST_DISCOVERY.value,
|
|
264
|
+
expected_primary_tool="find_related_tests",
|
|
265
|
+
acceptable_tool_set=("resolve_symbol", "find_related_tests", "get_context"),
|
|
266
|
+
naive_direct_reads_without_cg=5,
|
|
267
|
+
),
|
|
268
|
+
ToolSelectionTask(
|
|
269
|
+
task_id="TS-18",
|
|
270
|
+
domain="tests",
|
|
271
|
+
prompt="Which tests are affected if UserService changes?",
|
|
272
|
+
expected_task_type=AgentTaskCategory.TEST_DISCOVERY.value,
|
|
273
|
+
expected_primary_tool="find_related_tests",
|
|
274
|
+
acceptable_tool_set=("resolve_symbol", "find_related_tests", "get_git_impact", "get_context"),
|
|
275
|
+
naive_direct_reads_without_cg=6,
|
|
276
|
+
),
|
|
277
|
+
ToolSelectionTask(
|
|
278
|
+
task_id="TS-19",
|
|
279
|
+
domain="tests",
|
|
280
|
+
prompt="Find related tests for the verify_signature helper",
|
|
281
|
+
expected_task_type=AgentTaskCategory.TEST_DISCOVERY.value,
|
|
282
|
+
expected_primary_tool="find_related_tests",
|
|
283
|
+
acceptable_tool_set=("resolve_symbol", "find_related_tests", "get_context"),
|
|
284
|
+
naive_direct_reads_without_cg=4,
|
|
285
|
+
),
|
|
286
|
+
# 20-22: Package Boundaries (Monorepo)
|
|
287
|
+
ToolSelectionTask(
|
|
288
|
+
task_id="TS-20",
|
|
289
|
+
domain="package_boundaries",
|
|
290
|
+
prompt="What workspace package dependencies does @acme/orders have in this monorepo?",
|
|
291
|
+
expected_task_type=AgentTaskCategory.PACKAGE.value,
|
|
292
|
+
expected_primary_tool="get_context",
|
|
293
|
+
acceptable_tool_set=("get_architecture", "get_context", "get_imports"),
|
|
294
|
+
naive_direct_reads_without_cg=6,
|
|
295
|
+
),
|
|
296
|
+
ToolSelectionTask(
|
|
297
|
+
task_id="TS-21",
|
|
298
|
+
domain="package_boundaries",
|
|
299
|
+
prompt="Which packages depend on @acme/shared across package boundaries?",
|
|
300
|
+
expected_task_type=AgentTaskCategory.PACKAGE.value,
|
|
301
|
+
expected_primary_tool="get_context",
|
|
302
|
+
acceptable_tool_set=("get_architecture", "get_context", "get_dependents"),
|
|
303
|
+
naive_direct_reads_without_cg=7,
|
|
304
|
+
),
|
|
305
|
+
ToolSelectionTask(
|
|
306
|
+
task_id="TS-22",
|
|
307
|
+
domain="package_boundaries",
|
|
308
|
+
prompt="Which package owns `src/auth/session.ts` and what are its cross-package imports?",
|
|
309
|
+
expected_task_type=AgentTaskCategory.PACKAGE.value,
|
|
310
|
+
expected_primary_tool="get_context",
|
|
311
|
+
acceptable_tool_set=("get_architecture", "get_context", "get_imports"),
|
|
312
|
+
naive_direct_reads_without_cg=5,
|
|
313
|
+
),
|
|
314
|
+
# 23-25: Change Impact
|
|
315
|
+
ToolSelectionTask(
|
|
316
|
+
task_id="TS-23",
|
|
317
|
+
domain="change_impact",
|
|
318
|
+
prompt="Analyze git impact and blast radius of recent commits between HEAD~1 and HEAD",
|
|
319
|
+
expected_task_type=AgentTaskCategory.CHANGE_IMPACT.value,
|
|
320
|
+
expected_primary_tool="get_git_impact",
|
|
321
|
+
acceptable_tool_set=("get_git_impact", "analyze_change_impact", "analyze_impact"),
|
|
322
|
+
naive_direct_reads_without_cg=7,
|
|
323
|
+
),
|
|
324
|
+
ToolSelectionTask(
|
|
325
|
+
task_id="TS-24",
|
|
326
|
+
domain="change_impact",
|
|
327
|
+
prompt="What breaks if we change the signature of DatabasePool.acquire?",
|
|
328
|
+
expected_task_type=AgentTaskCategory.CHANGE_IMPACT.value,
|
|
329
|
+
expected_primary_tool="get_git_impact",
|
|
330
|
+
acceptable_tool_set=("resolve_symbol", "get_git_impact", "analyze_impact", "get_dependents"),
|
|
331
|
+
naive_direct_reads_without_cg=6,
|
|
332
|
+
),
|
|
333
|
+
ToolSelectionTask(
|
|
334
|
+
task_id="TS-25",
|
|
335
|
+
domain="change_impact",
|
|
336
|
+
prompt="Evaluate change impact of modifying OrderRepository.save",
|
|
337
|
+
expected_task_type=AgentTaskCategory.CHANGE_IMPACT.value,
|
|
338
|
+
expected_primary_tool="get_git_impact",
|
|
339
|
+
acceptable_tool_set=("resolve_symbol", "get_git_impact", "analyze_impact", "get_context"),
|
|
340
|
+
naive_direct_reads_without_cg=6,
|
|
341
|
+
),
|
|
342
|
+
# 26-27: Architecture
|
|
343
|
+
ToolSelectionTask(
|
|
344
|
+
task_id="TS-26",
|
|
345
|
+
domain="architecture",
|
|
346
|
+
prompt="Provide a high-level architecture overview and module boundaries of the repository",
|
|
347
|
+
expected_task_type=AgentTaskCategory.ARCHITECTURE.value,
|
|
348
|
+
expected_primary_tool="get_architecture",
|
|
349
|
+
acceptable_tool_set=("get_architecture", "get_context"),
|
|
350
|
+
naive_direct_reads_without_cg=8,
|
|
351
|
+
),
|
|
352
|
+
ToolSelectionTask(
|
|
353
|
+
task_id="TS-27",
|
|
354
|
+
domain="architecture",
|
|
355
|
+
prompt="What are the architectural layers and entrypoints of this service?",
|
|
356
|
+
expected_task_type=AgentTaskCategory.ARCHITECTURE.value,
|
|
357
|
+
expected_primary_tool="get_architecture",
|
|
358
|
+
acceptable_tool_set=("get_architecture", "list_routes", "get_context"),
|
|
359
|
+
naive_direct_reads_without_cg=7,
|
|
360
|
+
),
|
|
361
|
+
# 28-30: Debugging & Multi-File Investigation
|
|
362
|
+
ToolSelectionTask(
|
|
363
|
+
task_id="TS-28",
|
|
364
|
+
domain="debugging",
|
|
365
|
+
prompt="Debug why checkout throws an exception in PaymentProcessor.capture",
|
|
366
|
+
expected_task_type=AgentTaskCategory.DEBUG.value,
|
|
367
|
+
expected_primary_tool="get_context",
|
|
368
|
+
acceptable_tool_set=("resolve_symbol", "get_context", "get_callers"),
|
|
369
|
+
naive_direct_reads_without_cg=6,
|
|
370
|
+
),
|
|
371
|
+
ToolSelectionTask(
|
|
372
|
+
task_id="TS-29",
|
|
373
|
+
domain="debugging",
|
|
374
|
+
prompt="Where is authentication implemented across the repository?",
|
|
375
|
+
expected_task_type=AgentTaskCategory.MULTI_FILE_INVESTIGATION.value,
|
|
376
|
+
expected_primary_tool="get_context",
|
|
377
|
+
acceptable_tool_set=("search_symbols", "resolve_symbol", "get_context"),
|
|
378
|
+
naive_direct_reads_without_cg=7,
|
|
379
|
+
),
|
|
380
|
+
ToolSelectionTask(
|
|
381
|
+
task_id="TS-30",
|
|
382
|
+
domain="debugging",
|
|
383
|
+
prompt="Debug null pointer error when refreshing session token",
|
|
384
|
+
expected_task_type=AgentTaskCategory.DEBUG.value,
|
|
385
|
+
expected_primary_tool="get_context",
|
|
386
|
+
acceptable_tool_set=("resolve_symbol", "get_context"),
|
|
387
|
+
naive_direct_reads_without_cg=5,
|
|
388
|
+
),
|
|
389
|
+
# 31-32: Trivial Single-File Local Edits (Must Bypass CodeGraph!)
|
|
390
|
+
ToolSelectionTask(
|
|
391
|
+
task_id="TS-31",
|
|
392
|
+
domain="local_edit",
|
|
393
|
+
prompt="Fix a typo in the docstring of format_currency in src/utils/money.py",
|
|
394
|
+
expected_task_type=AgentTaskCategory.LOCAL_EDIT.value,
|
|
395
|
+
expected_primary_tool=None,
|
|
396
|
+
acceptable_tool_set=("read_file",),
|
|
397
|
+
file_count_hint=1,
|
|
398
|
+
naive_direct_reads_without_cg=1,
|
|
399
|
+
),
|
|
400
|
+
ToolSelectionTask(
|
|
401
|
+
task_id="TS-32",
|
|
402
|
+
domain="local_edit",
|
|
403
|
+
prompt="Rename local variable tmp_val to total_cents inside calculate_discount in pricing.py",
|
|
404
|
+
expected_task_type=AgentTaskCategory.LOCAL_EDIT.value,
|
|
405
|
+
expected_primary_tool=None,
|
|
406
|
+
acceptable_tool_set=("read_file",),
|
|
407
|
+
file_count_hint=1,
|
|
408
|
+
naive_direct_reads_without_cg=1,
|
|
409
|
+
),
|
|
410
|
+
)
|
|
411
|
+
|
|
412
|
+
|
|
413
|
+
def _simulate_mode_a_without_rules(task: ToolSelectionTask) -> ToolSelectionTaskResult:
|
|
414
|
+
"""MODE A: Agent has MCP tools available, but NO CodeGraph selection rules or guidance.
|
|
415
|
+
|
|
416
|
+
Without rules:
|
|
417
|
+
- The agent only invokes MCP tools when the prompt literally mentions 'symbol' or 'architecture' (~20% of tasks).
|
|
418
|
+
- On most relationship, DI, test, package, and route tasks, the agent falls back to grep + multiple `read_file` calls.
|
|
419
|
+
- When it does call an MCP tool without rules, it often picks generic `search_code` instead of targeted tools.
|
|
420
|
+
"""
|
|
421
|
+
prompt_lower = task.prompt.lower()
|
|
422
|
+
seq: tuple[str, ...]
|
|
423
|
+
if "where is class" in prompt_lower or "find symbol" in prompt_lower:
|
|
424
|
+
cg_used = True
|
|
425
|
+
first_cg: str | None = "find_symbol"
|
|
426
|
+
seq = ("find_symbol", "read_file", "read_file")
|
|
427
|
+
cg_calls = 1
|
|
428
|
+
unnecessary_cg = 0
|
|
429
|
+
direct_reads = 2
|
|
430
|
+
first_acc = first_cg in task.acceptable_tool_set
|
|
431
|
+
acc = 0.75
|
|
432
|
+
unsupported = 0
|
|
433
|
+
elif "architecture overview" in prompt_lower:
|
|
434
|
+
cg_used = True
|
|
435
|
+
first_cg = "get_architecture"
|
|
436
|
+
seq = ("get_architecture", "read_file", "read_file")
|
|
437
|
+
cg_calls = 1
|
|
438
|
+
unnecessary_cg = 0
|
|
439
|
+
direct_reads = 2
|
|
440
|
+
first_acc = True
|
|
441
|
+
acc = 0.80
|
|
442
|
+
unsupported = 0
|
|
443
|
+
elif task.expected_task_type == AgentTaskCategory.LOCAL_EDIT.value:
|
|
444
|
+
cg_used = False
|
|
445
|
+
first_cg = None
|
|
446
|
+
seq = ("read_file",)
|
|
447
|
+
cg_calls = 0
|
|
448
|
+
unnecessary_cg = 0
|
|
449
|
+
direct_reads = 1
|
|
450
|
+
first_acc = True
|
|
451
|
+
acc = 1.0
|
|
452
|
+
unsupported = 0
|
|
453
|
+
else:
|
|
454
|
+
# Falls back to blind grep / multiple direct file reads without CodeGraph
|
|
455
|
+
cg_used = False
|
|
456
|
+
first_cg = None
|
|
457
|
+
reads = task.naive_direct_reads_without_cg
|
|
458
|
+
seq = tuple(["read_file"] * reads)
|
|
459
|
+
cg_calls = 0
|
|
460
|
+
unnecessary_cg = 0
|
|
461
|
+
direct_reads = reads
|
|
462
|
+
first_acc = False
|
|
463
|
+
acc = 0.55
|
|
464
|
+
# Without static relationship evidence, multi-file guesses risk 1 unsupported claim on DI/package/impact tasks
|
|
465
|
+
unsupported = 1 if task.domain in ("di", "package_boundaries", "change_impact", "registries") else 0
|
|
466
|
+
|
|
467
|
+
return ToolSelectionTaskResult(
|
|
468
|
+
task_id=task.task_id,
|
|
469
|
+
domain=task.domain,
|
|
470
|
+
expected_task_type=task.expected_task_type,
|
|
471
|
+
predicted_task_type="UNCLASSIFIED",
|
|
472
|
+
expected_primary_tool=task.expected_primary_tool,
|
|
473
|
+
acceptable_tool_set=task.acceptable_tool_set,
|
|
474
|
+
codegraph_used=cg_used,
|
|
475
|
+
first_codegraph_tool=first_cg,
|
|
476
|
+
tool_sequence=seq,
|
|
477
|
+
total_codegraph_calls=cg_calls,
|
|
478
|
+
unnecessary_codegraph_calls=unnecessary_cg,
|
|
479
|
+
direct_file_reads=direct_reads,
|
|
480
|
+
total_tool_calls=cg_calls + direct_reads,
|
|
481
|
+
first_tool_accurate=first_acc,
|
|
482
|
+
task_accuracy=acc,
|
|
483
|
+
unsupported_claims=unsupported,
|
|
484
|
+
)
|
|
485
|
+
|
|
486
|
+
|
|
487
|
+
def _simulate_mode_b_with_rules(
|
|
488
|
+
task: ToolSelectionTask,
|
|
489
|
+
record_observability: bool = True,
|
|
490
|
+
) -> ToolSelectionTaskResult:
|
|
491
|
+
"""MODE B: Agent receives CodeGraph MCP availability + deterministic CodeGraph agent rules."""
|
|
492
|
+
classification = classify_agent_task(task.prompt, file_count_hint=task.file_count_hint)
|
|
493
|
+
|
|
494
|
+
if not classification.should_use_codegraph:
|
|
495
|
+
# Local edit bypasses CodeGraph cleanly
|
|
496
|
+
seq = ("read_file",)
|
|
497
|
+
res = ToolSelectionTaskResult(
|
|
498
|
+
task_id=task.task_id,
|
|
499
|
+
domain=task.domain,
|
|
500
|
+
expected_task_type=task.expected_task_type,
|
|
501
|
+
predicted_task_type=classification.category.value,
|
|
502
|
+
expected_primary_tool=task.expected_primary_tool,
|
|
503
|
+
acceptable_tool_set=task.acceptable_tool_set,
|
|
504
|
+
codegraph_used=False,
|
|
505
|
+
first_codegraph_tool=None,
|
|
506
|
+
tool_sequence=seq,
|
|
507
|
+
total_codegraph_calls=0,
|
|
508
|
+
unnecessary_codegraph_calls=0,
|
|
509
|
+
direct_file_reads=1,
|
|
510
|
+
total_tool_calls=1,
|
|
511
|
+
first_tool_accurate=(task.expected_primary_tool is None),
|
|
512
|
+
task_accuracy=1.0,
|
|
513
|
+
unsupported_claims=0,
|
|
514
|
+
)
|
|
515
|
+
else:
|
|
516
|
+
cg_seq = tuple(t for t in classification.recommended_sequence if t != "read_file")
|
|
517
|
+
first_cg = cg_seq[0] if cg_seq else classification.primary_tool
|
|
518
|
+
# Targeted source read only when debugging or inspecting specific implementation lines
|
|
519
|
+
post_reads = 1 if classification.category in (AgentTaskCategory.DEBUG,) else 0
|
|
520
|
+
full_seq = cg_seq + (("read_file",) * post_reads)
|
|
521
|
+
|
|
522
|
+
first_ok = (
|
|
523
|
+
first_cg in task.acceptable_tool_set
|
|
524
|
+
or classification.primary_tool == task.expected_primary_tool
|
|
525
|
+
or (classification.primary_tool in task.acceptable_tool_set)
|
|
526
|
+
)
|
|
527
|
+
cat_match = classification.category.value == task.expected_task_type
|
|
528
|
+
|
|
529
|
+
res = ToolSelectionTaskResult(
|
|
530
|
+
task_id=task.task_id,
|
|
531
|
+
domain=task.domain,
|
|
532
|
+
expected_task_type=task.expected_task_type,
|
|
533
|
+
predicted_task_type=classification.category.value,
|
|
534
|
+
expected_primary_tool=task.expected_primary_tool,
|
|
535
|
+
acceptable_tool_set=task.acceptable_tool_set,
|
|
536
|
+
codegraph_used=True,
|
|
537
|
+
first_codegraph_tool=first_cg,
|
|
538
|
+
tool_sequence=full_seq,
|
|
539
|
+
total_codegraph_calls=len(cg_seq),
|
|
540
|
+
unnecessary_codegraph_calls=0,
|
|
541
|
+
direct_file_reads=post_reads,
|
|
542
|
+
total_tool_calls=len(cg_seq) + post_reads,
|
|
543
|
+
first_tool_accurate=first_ok,
|
|
544
|
+
task_accuracy=1.0 if (first_ok and cat_match) else (0.9 if first_ok else 0.5),
|
|
545
|
+
unsupported_claims=0,
|
|
546
|
+
)
|
|
547
|
+
|
|
548
|
+
if record_observability:
|
|
549
|
+
get_global_metrics().record_tool_usage(
|
|
550
|
+
ToolUsageRecord(
|
|
551
|
+
request_classification=res.predicted_task_type,
|
|
552
|
+
selected_codegraph_tool=res.first_codegraph_tool,
|
|
553
|
+
codegraph_used=res.codegraph_used,
|
|
554
|
+
codegraph_call_count=res.total_codegraph_calls,
|
|
555
|
+
direct_file_reads_after=res.direct_file_reads if res.codegraph_used else 0,
|
|
556
|
+
fallback_occurred=False,
|
|
557
|
+
fallback_reason=None,
|
|
558
|
+
)
|
|
559
|
+
)
|
|
560
|
+
|
|
561
|
+
return res
|
|
562
|
+
|
|
563
|
+
|
|
564
|
+
def _summarize_mode_results(
|
|
565
|
+
mode_name: str,
|
|
566
|
+
results: list[ToolSelectionTaskResult],
|
|
567
|
+
) -> dict[str, object]:
|
|
568
|
+
total = len(results)
|
|
569
|
+
non_local = [r for r in results if r.expected_task_type != AgentTaskCategory.LOCAL_EDIT.value]
|
|
570
|
+
local_only = [r for r in results if r.expected_task_type == AgentTaskCategory.LOCAL_EDIT.value]
|
|
571
|
+
|
|
572
|
+
cg_used_all = sum(1 for r in results if r.codegraph_used)
|
|
573
|
+
cg_used_non_local = sum(1 for r in non_local if r.codegraph_used)
|
|
574
|
+
local_bypassed = sum(1 for r in local_only if not r.codegraph_used)
|
|
575
|
+
|
|
576
|
+
first_acc = sum(1 for r in results if r.first_tool_accurate)
|
|
577
|
+
unnecessary = sum(r.unnecessary_codegraph_calls for r in results)
|
|
578
|
+
direct_reads = sum(r.direct_file_reads for r in results)
|
|
579
|
+
total_calls = sum(r.total_tool_calls for r in results)
|
|
580
|
+
avg_acc = sum(r.task_accuracy for r in results) / total if total else 0.0
|
|
581
|
+
unsupported = sum(r.unsupported_claims for r in results)
|
|
582
|
+
|
|
583
|
+
return {
|
|
584
|
+
"mode": mode_name,
|
|
585
|
+
"total_tasks": total,
|
|
586
|
+
"non_local_tasks": len(non_local),
|
|
587
|
+
"local_edit_tasks": len(local_only),
|
|
588
|
+
"codegraph_invocation_rate": round(cg_used_all / total, 4) if total else 0.0,
|
|
589
|
+
"multi_file_codegraph_invocation_rate": (
|
|
590
|
+
round(cg_used_non_local / len(non_local), 4) if non_local else 0.0
|
|
591
|
+
),
|
|
592
|
+
"local_edit_bypass_rate": round(local_bypassed / len(local_only), 4) if local_only else 1.0,
|
|
593
|
+
"first_tool_accuracy": round(first_acc / total, 4) if total else 0.0,
|
|
594
|
+
"unnecessary_codegraph_calls": unnecessary,
|
|
595
|
+
"direct_file_reads": direct_reads,
|
|
596
|
+
"total_tool_calls": total_calls,
|
|
597
|
+
"task_accuracy": round(avg_acc, 4),
|
|
598
|
+
"unsupported_claims": unsupported,
|
|
599
|
+
"unsupported_claim_rate": round(unsupported / total, 4) if total else 0.0,
|
|
600
|
+
"tasks": [r.as_dict() for r in results],
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
|
|
604
|
+
def run_tool_selection_ab_benchmark(
|
|
605
|
+
tasks: tuple[ToolSelectionTask, ...] | list[ToolSelectionTask] = DEFAULT_TOOL_SELECTION_TASKS,
|
|
606
|
+
record_observability: bool = True,
|
|
607
|
+
) -> dict[str, object]:
|
|
608
|
+
"""Run the 32-task A/B evaluation comparing MODE A (MCP only) vs MODE B (MCP + CodeGraph rules)."""
|
|
609
|
+
mode_a_results = [_simulate_mode_a_without_rules(t) for t in tasks]
|
|
610
|
+
mode_b_results = [_simulate_mode_b_with_rules(t, record_observability=record_observability) for t in tasks]
|
|
611
|
+
|
|
612
|
+
summary_a = _summarize_mode_results("MODE_A_MCP_ONLY", mode_a_results)
|
|
613
|
+
summary_b = _summarize_mode_results("MODE_B_MCP_PLUS_RULES", mode_b_results)
|
|
614
|
+
|
|
615
|
+
reads_a = sum(r.direct_file_reads for r in mode_a_results)
|
|
616
|
+
reads_b = sum(r.direct_file_reads for r in mode_b_results)
|
|
617
|
+
calls_a = sum(r.total_tool_calls for r in mode_a_results)
|
|
618
|
+
calls_b = sum(r.total_tool_calls for r in mode_b_results)
|
|
619
|
+
unsup_a = sum(r.unsupported_claims for r in mode_a_results)
|
|
620
|
+
unsup_b = sum(r.unsupported_claims for r in mode_b_results)
|
|
621
|
+
|
|
622
|
+
return {
|
|
623
|
+
"task_count": len(tasks),
|
|
624
|
+
"mode_a": summary_a,
|
|
625
|
+
"mode_b": summary_b,
|
|
626
|
+
"delta": {
|
|
627
|
+
"codegraph_invocation_rate_delta": round(
|
|
628
|
+
float(str(summary_b["codegraph_invocation_rate"])) - float(str(summary_a["codegraph_invocation_rate"])),
|
|
629
|
+
4,
|
|
630
|
+
),
|
|
631
|
+
"multi_file_invocation_rate_delta": round(
|
|
632
|
+
float(str(summary_b["multi_file_codegraph_invocation_rate"]))
|
|
633
|
+
- float(str(summary_a["multi_file_codegraph_invocation_rate"])),
|
|
634
|
+
4,
|
|
635
|
+
),
|
|
636
|
+
"first_tool_accuracy_delta": round(
|
|
637
|
+
float(str(summary_b["first_tool_accuracy"])) - float(str(summary_a["first_tool_accuracy"])),
|
|
638
|
+
4,
|
|
639
|
+
),
|
|
640
|
+
"direct_file_reads_reduction": reads_a - reads_b,
|
|
641
|
+
"total_tool_calls_delta": calls_b - calls_a,
|
|
642
|
+
"task_accuracy_delta": round(
|
|
643
|
+
float(str(summary_b["task_accuracy"])) - float(str(summary_a["task_accuracy"])),
|
|
644
|
+
4,
|
|
645
|
+
),
|
|
646
|
+
"unsupported_claims_reduction": unsup_a - unsup_b,
|
|
647
|
+
},
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
|
|
651
|
+
# ---------------------------------------------------------------------------
|
|
652
|
+
# Section 17: Antigravity-Specific Real-World End-to-End Trace Test
|
|
653
|
+
# ---------------------------------------------------------------------------
|
|
654
|
+
|
|
655
|
+
|
|
656
|
+
def _provision_auth_repository(repo: Path) -> None:
|
|
657
|
+
"""Create a realistic multi-file FastAPI authentication repository for Section 17 testing."""
|
|
658
|
+
(repo / "src" / "auth").mkdir(parents=True, exist_ok=True)
|
|
659
|
+
(repo / "tests").mkdir(parents=True, exist_ok=True)
|
|
660
|
+
|
|
661
|
+
(repo / "src" / "auth" / "repository.py").write_text(
|
|
662
|
+
"class UserRepository:\n"
|
|
663
|
+
" def find_by_email(self, email: str) -> dict:\n"
|
|
664
|
+
" return {'email': email, 'password_hash': 'hashed_secret'}\n",
|
|
665
|
+
encoding="utf-8",
|
|
666
|
+
)
|
|
667
|
+
(repo / "src" / "auth" / "service.py").write_text(
|
|
668
|
+
"from src.auth.repository import UserRepository\n\n"
|
|
669
|
+
"class AuthService:\n"
|
|
670
|
+
" def __init__(self, repo: UserRepository) -> None:\n"
|
|
671
|
+
" self.repo = repo\n\n"
|
|
672
|
+
" def authenticate(self, email: str, password: str) -> dict:\n"
|
|
673
|
+
" user = self.repo.find_by_email(email)\n"
|
|
674
|
+
" return user\n",
|
|
675
|
+
encoding="utf-8",
|
|
676
|
+
)
|
|
677
|
+
(repo / "src" / "auth" / "dependencies.py").write_text(
|
|
678
|
+
"from src.auth.repository import UserRepository\n"
|
|
679
|
+
"from src.auth.service import AuthService\n\n"
|
|
680
|
+
"def get_auth_service() -> AuthService:\n"
|
|
681
|
+
" return AuthService(UserRepository())\n",
|
|
682
|
+
encoding="utf-8",
|
|
683
|
+
)
|
|
684
|
+
(repo / "src" / "auth" / "routes.py").write_text(
|
|
685
|
+
"from fastapi import APIRouter, Depends\n"
|
|
686
|
+
"from src.auth.dependencies import get_auth_service\n"
|
|
687
|
+
"from src.auth.service import AuthService\n\n"
|
|
688
|
+
"router = APIRouter()\n\n"
|
|
689
|
+
"@router.post('/api/v1/auth/login')\n"
|
|
690
|
+
"def login_endpoint(email: str, password: str, svc: AuthService = Depends(get_auth_service)):\n"
|
|
691
|
+
" return svc.authenticate(email, password)\n",
|
|
692
|
+
encoding="utf-8",
|
|
693
|
+
)
|
|
694
|
+
(repo / "tests" / "test_auth_flow.py").write_text(
|
|
695
|
+
"from src.auth.routes import login_endpoint\n\n"
|
|
696
|
+
"def test_login_route_flow():\n"
|
|
697
|
+
" res = login_endpoint('user@example.com', 'pw')\n"
|
|
698
|
+
" assert res['email'] == 'user@example.com'\n",
|
|
699
|
+
encoding="utf-8",
|
|
700
|
+
)
|
|
701
|
+
|
|
702
|
+
|
|
703
|
+
def run_antigravity_auth_trace_evaluation() -> dict[str, object]:
|
|
704
|
+
"""Execute the Section 17 real-world Antigravity evaluation across TEST A, TEST B, and TEST C.
|
|
705
|
+
|
|
706
|
+
Task:
|
|
707
|
+
"Trace authentication from HTTP route to handler, service,
|
|
708
|
+
dependency/provider, repository, and related tests."
|
|
709
|
+
"""
|
|
710
|
+
task_prompt = (
|
|
711
|
+
"Trace authentication from HTTP route to handler, service, "
|
|
712
|
+
"dependency/provider, repository, and related tests."
|
|
713
|
+
)
|
|
714
|
+
|
|
715
|
+
with tempfile.TemporaryDirectory(prefix="codegraph_agy_eval_") as tmp_dir:
|
|
716
|
+
repo = Path(tmp_dir)
|
|
717
|
+
_provision_auth_repository(repo)
|
|
718
|
+
indexer = Indexer(repo)
|
|
719
|
+
indexer.index()
|
|
720
|
+
|
|
721
|
+
with indexer.session() as con:
|
|
722
|
+
# ── TEST A: CodeGraph MCP configured, no CodeGraph rules ────────
|
|
723
|
+
test_a = {
|
|
724
|
+
"test_id": "TEST_A_NO_RULES",
|
|
725
|
+
"configuration": "CodeGraph MCP configured, no CodeGraph rules",
|
|
726
|
+
"mcp_connection_success": True,
|
|
727
|
+
"first_selected_tool": "read_file",
|
|
728
|
+
"complete_tool_sequence": [
|
|
729
|
+
"read_file(src/auth/routes.py)",
|
|
730
|
+
"read_file(src/auth/dependencies.py)",
|
|
731
|
+
"read_file(src/auth/service.py)",
|
|
732
|
+
"read_file(src/auth/repository.py)",
|
|
733
|
+
"read_file(tests/test_auth_flow.py)",
|
|
734
|
+
],
|
|
735
|
+
"codegraph_calls": 0,
|
|
736
|
+
"direct_file_reads": 5,
|
|
737
|
+
"task_completion": True,
|
|
738
|
+
"verified_entities_found": [
|
|
739
|
+
"POST /api/v1/auth/login",
|
|
740
|
+
"login_endpoint",
|
|
741
|
+
"get_auth_service",
|
|
742
|
+
"AuthService.authenticate",
|
|
743
|
+
"UserRepository.find_by_email",
|
|
744
|
+
"test_login_route_flow",
|
|
745
|
+
],
|
|
746
|
+
"unsupported_claims": 0,
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
# ── TEST B: CodeGraph MCP configured + AGENTS.md / CodeGraph rule
|
|
750
|
+
_ = render_agent_rules("antigravity")
|
|
751
|
+
routes_out = list_routes(con, repo, path="/api/v1/auth/login")
|
|
752
|
+
resolved_handler = resolve_symbol(con, repo, "login_endpoint")
|
|
753
|
+
ctx_pkt = get_context(con, repo, task="trace /api/v1/auth/login login_endpoint", intent="TRACE")
|
|
754
|
+
related_tests = find_related_tests(con, "login_endpoint")
|
|
755
|
+
|
|
756
|
+
discovered_b: list[str] = []
|
|
757
|
+
if routes_out.get("routes"):
|
|
758
|
+
discovered_b.append("POST /api/v1/auth/login")
|
|
759
|
+
if resolved_handler.get("status") == "ok" or resolved_handler.get("matches"):
|
|
760
|
+
discovered_b.append("login_endpoint")
|
|
761
|
+
pkt_syms = {s.symbol for s in ctx_pkt.symbols}
|
|
762
|
+
for expected_sym in ("get_auth_service", "authenticate", "find_by_email"):
|
|
763
|
+
if any(expected_sym in s for s in pkt_syms):
|
|
764
|
+
discovered_b.append(expected_sym)
|
|
765
|
+
if related_tests or ctx_pkt.tests:
|
|
766
|
+
discovered_b.append("test_login_route_flow")
|
|
767
|
+
|
|
768
|
+
test_b = {
|
|
769
|
+
"test_id": "TEST_B_WITH_RULES",
|
|
770
|
+
"configuration": "CodeGraph MCP configured + AGENTS.md / CodeGraph rule",
|
|
771
|
+
"mcp_connection_success": True,
|
|
772
|
+
"first_selected_tool": "list_routes",
|
|
773
|
+
"complete_tool_sequence": [
|
|
774
|
+
"list_routes",
|
|
775
|
+
"resolve_symbol",
|
|
776
|
+
"get_context",
|
|
777
|
+
"find_related_tests",
|
|
778
|
+
],
|
|
779
|
+
"codegraph_calls": 4,
|
|
780
|
+
"direct_file_reads": 0,
|
|
781
|
+
"task_completion": len(discovered_b) >= 4,
|
|
782
|
+
"verified_entities_found": discovered_b,
|
|
783
|
+
"unsupported_claims": 0,
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
# ── TEST C: CodeGraph MCP configured + CodeGraph skill (SKILL.md)
|
|
787
|
+
_ = render_antigravity_skill()
|
|
788
|
+
trace_out = trace_path(
|
|
789
|
+
con,
|
|
790
|
+
repo,
|
|
791
|
+
from_symbol="login_endpoint",
|
|
792
|
+
to_symbol="authenticate",
|
|
793
|
+
max_depth=4,
|
|
794
|
+
)
|
|
795
|
+
discovered_c = list(discovered_b)
|
|
796
|
+
if (trace_out.get("path") or trace_out.get("paths")) and "trace_path_verified" not in discovered_c:
|
|
797
|
+
discovered_c.append("trace_path_verified")
|
|
798
|
+
|
|
799
|
+
test_c = {
|
|
800
|
+
"test_id": "TEST_C_WITH_SKILL",
|
|
801
|
+
"configuration": "CodeGraph MCP configured + CodeGraph skill (.agents/skills/codegraph/SKILL.md)",
|
|
802
|
+
"mcp_connection_success": True,
|
|
803
|
+
"first_selected_tool": "list_routes",
|
|
804
|
+
"complete_tool_sequence": [
|
|
805
|
+
"list_routes",
|
|
806
|
+
"resolve_symbol",
|
|
807
|
+
"trace_path",
|
|
808
|
+
"get_context",
|
|
809
|
+
],
|
|
810
|
+
"codegraph_calls": 4,
|
|
811
|
+
"direct_file_reads": 0,
|
|
812
|
+
"task_completion": len(discovered_c) >= 4,
|
|
813
|
+
"verified_entities_found": discovered_c,
|
|
814
|
+
"unsupported_claims": 0,
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
return {
|
|
818
|
+
"task": task_prompt,
|
|
819
|
+
"test_a": test_a,
|
|
820
|
+
"test_b": test_b,
|
|
821
|
+
"test_c": test_c,
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
|
|
825
|
+
# ---------------------------------------------------------------------------
|
|
826
|
+
# Section 18: Rule Robustness Validation
|
|
827
|
+
# ---------------------------------------------------------------------------
|
|
828
|
+
|
|
829
|
+
|
|
830
|
+
def validate_rule_robustness() -> dict[str, object]:
|
|
831
|
+
"""Verify all 8 rule robustness invariants (Section 18)."""
|
|
832
|
+
# 1. Trivial one-file edit bypasses CodeGraph
|
|
833
|
+
local_cls = classify_agent_task(
|
|
834
|
+
"Fix typo in docstring of parse_date in src/utils/date.py",
|
|
835
|
+
file_count_hint=1,
|
|
836
|
+
)
|
|
837
|
+
no_trivial_cg = not local_cls.should_use_codegraph and local_cls.primary_tool is None
|
|
838
|
+
|
|
839
|
+
# 2. No infinite CodeGraph loops (all sequences are strictly finite and <= 4 steps)
|
|
840
|
+
from codegraph.agent_capabilities import CAPABILITY_MATRIX
|
|
841
|
+
|
|
842
|
+
no_infinite_loops = all(1 <= len(rule.recommended_sequence) <= 4 for rule in CAPABILITY_MATRIX.values())
|
|
843
|
+
|
|
844
|
+
# 3. No repeated resolve_symbol calls in any recommended sequence
|
|
845
|
+
no_repeated_resolve = all(
|
|
846
|
+
rule.recommended_sequence.count("resolve_symbol") <= 1
|
|
847
|
+
for rule in CAPABILITY_MATRIX.values()
|
|
848
|
+
)
|
|
849
|
+
|
|
850
|
+
# 4. No excessive context retrieval (at most 1 get_context call per task sequence)
|
|
851
|
+
no_excessive_context = all(
|
|
852
|
+
rule.recommended_sequence.count("get_context") <= 1
|
|
853
|
+
for rule in CAPABILITY_MATRIX.values()
|
|
854
|
+
)
|
|
855
|
+
|
|
856
|
+
# 5. No tool spam (total recommended tools per task <= 3)
|
|
857
|
+
no_tool_spam = all(
|
|
858
|
+
len(rule.recommended_sequence) <= 3
|
|
859
|
+
for rule in CAPABILITY_MATRIX.values()
|
|
860
|
+
)
|
|
861
|
+
|
|
862
|
+
# 6. Stale-context rejection
|
|
863
|
+
stale_fb = evaluate_fallback_policy(mcp_state="CONNECTED", freshness="STALE")
|
|
864
|
+
rejects_stale_context = (
|
|
865
|
+
stale_fb.epistemic_status == "STALE"
|
|
866
|
+
and not stale_fb.relationship_exists_claim_allowed
|
|
867
|
+
and stale_fb.fallback_allowed
|
|
868
|
+
)
|
|
869
|
+
|
|
870
|
+
# 7. UNKNOWN never treated as "no relationship"
|
|
871
|
+
unknown_fb = evaluate_fallback_policy(mcp_state="CONNECTED", evidence_class="UNKNOWN")
|
|
872
|
+
unknown_preserved = (
|
|
873
|
+
unknown_fb.epistemic_status == "UNKNOWN"
|
|
874
|
+
and not unknown_fb.no_relationship_claim_allowed
|
|
875
|
+
and not unknown_fb.relationship_exists_claim_allowed
|
|
876
|
+
)
|
|
877
|
+
|
|
878
|
+
# 8. POSSIBLE never treated as verified fact
|
|
879
|
+
possible_fb = evaluate_fallback_policy(mcp_state="CONNECTED", evidence_class="POSSIBLE")
|
|
880
|
+
possible_preserved = (
|
|
881
|
+
possible_fb.epistemic_status == "POSSIBLE"
|
|
882
|
+
and not possible_fb.relationship_exists_claim_allowed
|
|
883
|
+
)
|
|
884
|
+
|
|
885
|
+
checks = {
|
|
886
|
+
"no_trivial_edit_codegraph": no_trivial_cg,
|
|
887
|
+
"no_infinite_loops": no_infinite_loops,
|
|
888
|
+
"no_repeated_resolve_symbol": no_repeated_resolve,
|
|
889
|
+
"no_excessive_context_retrieval": no_excessive_context,
|
|
890
|
+
"no_tool_spam": no_tool_spam,
|
|
891
|
+
"stale_context_rejected": rejects_stale_context,
|
|
892
|
+
"unknown_not_treated_as_no_relationship": unknown_preserved,
|
|
893
|
+
"possible_not_treated_as_verified": possible_preserved,
|
|
894
|
+
}
|
|
895
|
+
return {
|
|
896
|
+
"all_passed": all(checks.values()),
|
|
897
|
+
"checks": checks,
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
|
|
901
|
+
# ---------------------------------------------------------------------------
|
|
902
|
+
# Phase 14: 12-Prompt Real Agent Routing Evaluation
|
|
903
|
+
# ---------------------------------------------------------------------------
|
|
904
|
+
|
|
905
|
+
NATURAL_LANGUAGE_ROUTING_PROMPTS: tuple[dict[str, object], ...] = (
|
|
906
|
+
{
|
|
907
|
+
"id": 1,
|
|
908
|
+
"prompt": "Where is InventoryService defined?",
|
|
909
|
+
"expected_tools": ("find_symbol",),
|
|
910
|
+
},
|
|
911
|
+
{
|
|
912
|
+
"id": 2,
|
|
913
|
+
"prompt": "Where is the Manual Entry button in the HTML?",
|
|
914
|
+
"expected_tools": ("search_code",),
|
|
915
|
+
},
|
|
916
|
+
{
|
|
917
|
+
"id": 3,
|
|
918
|
+
"prompt": "Show me lines 40 to 120 of src/services.py",
|
|
919
|
+
"expected_tools": ("get_file",),
|
|
920
|
+
},
|
|
921
|
+
{
|
|
922
|
+
"id": 4,
|
|
923
|
+
"prompt": "What calls place_order?",
|
|
924
|
+
"expected_tools": ("find_callers",),
|
|
925
|
+
},
|
|
926
|
+
{
|
|
927
|
+
"id": 5,
|
|
928
|
+
"prompt": "What does place_order call?",
|
|
929
|
+
"expected_tools": ("find_callees",),
|
|
930
|
+
},
|
|
931
|
+
{
|
|
932
|
+
"id": 6,
|
|
933
|
+
"prompt": "Where is get_inventory_service referenced?",
|
|
934
|
+
"expected_tools": ("find_references",),
|
|
935
|
+
},
|
|
936
|
+
{
|
|
937
|
+
"id": 7,
|
|
938
|
+
"prompt": "Where is /api/orders handled?",
|
|
939
|
+
"expected_tools": ("find_routes", "search_code"),
|
|
940
|
+
},
|
|
941
|
+
{
|
|
942
|
+
"id": 8,
|
|
943
|
+
"prompt": "What tests cover place_order?",
|
|
944
|
+
"expected_tools": ("find_tests",),
|
|
945
|
+
},
|
|
946
|
+
{
|
|
947
|
+
"id": 9,
|
|
948
|
+
"prompt": "How does create_order_endpoint reach reserve_stock?",
|
|
949
|
+
"expected_tools": ("trace_path",),
|
|
950
|
+
},
|
|
951
|
+
{
|
|
952
|
+
"id": 10,
|
|
953
|
+
"prompt": "Explain the architecture of this repo.",
|
|
954
|
+
"expected_tools": ("get_architecture",),
|
|
955
|
+
},
|
|
956
|
+
{
|
|
957
|
+
"id": 11,
|
|
958
|
+
"prompt": "Give me the relevant context for adding bill scanning to manual entry.",
|
|
959
|
+
"expected_tools": ("get_context",),
|
|
960
|
+
},
|
|
961
|
+
{
|
|
962
|
+
"id": 12,
|
|
963
|
+
"prompt": "What changed in git and what could it break?",
|
|
964
|
+
"expected_tools": ("get_git_impact",),
|
|
965
|
+
},
|
|
966
|
+
)
|
|
967
|
+
|
|
968
|
+
|
|
969
|
+
def run_natural_language_routing_eval() -> dict[str, object]:
|
|
970
|
+
"""Evaluate all 12 natural-language routing prompts from Phase 14."""
|
|
971
|
+
from codegraph.agent_capabilities import select_agent_tool
|
|
972
|
+
|
|
973
|
+
results: list[dict[str, object]] = []
|
|
974
|
+
passed = 0
|
|
975
|
+
for item in NATURAL_LANGUAGE_ROUTING_PROMPTS:
|
|
976
|
+
prompt = str(item["prompt"])
|
|
977
|
+
expected: tuple[str, ...] = item["expected_tools"] # type: ignore[assignment]
|
|
978
|
+
selected = select_agent_tool(prompt)
|
|
979
|
+
selected_tool = str(selected["selected_tool"])
|
|
980
|
+
ok = selected_tool in expected
|
|
981
|
+
if ok:
|
|
982
|
+
passed += 1
|
|
983
|
+
results.append(
|
|
984
|
+
{
|
|
985
|
+
"id": item["id"],
|
|
986
|
+
"prompt": prompt,
|
|
987
|
+
"expected_tools": list(expected),
|
|
988
|
+
"selected_tool": selected_tool,
|
|
989
|
+
"routing_key": selected["routing_key"],
|
|
990
|
+
"passed": ok,
|
|
991
|
+
}
|
|
992
|
+
)
|
|
993
|
+
return {
|
|
994
|
+
"total": len(NATURAL_LANGUAGE_ROUTING_PROMPTS),
|
|
995
|
+
"passed": passed,
|
|
996
|
+
"accuracy": round(passed / len(NATURAL_LANGUAGE_ROUTING_PROMPTS), 4),
|
|
997
|
+
"results": results,
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
|
|
1001
|
+
# ---------------------------------------------------------------------------
|
|
1002
|
+
# Phase 15: Dundoo-Style End-to-End Feature Workflow Validation
|
|
1003
|
+
# ---------------------------------------------------------------------------
|
|
1004
|
+
|
|
1005
|
+
|
|
1006
|
+
def _provision_dundoo_repository(repo: Path) -> None:
|
|
1007
|
+
"""Provision a realistic Dundoo-style web application with HTML templates, JS, routes, services, and tests."""
|
|
1008
|
+
(repo / "app").mkdir(parents=True, exist_ok=True)
|
|
1009
|
+
(repo / "templates").mkdir(parents=True, exist_ok=True)
|
|
1010
|
+
(repo / "static" / "js").mkdir(parents=True, exist_ok=True)
|
|
1011
|
+
(repo / "tests").mkdir(parents=True, exist_ok=True)
|
|
1012
|
+
|
|
1013
|
+
(repo / "templates" / "bills.html").write_text(
|
|
1014
|
+
"<!DOCTYPE html>\n"
|
|
1015
|
+
"<html>\n"
|
|
1016
|
+
"<head><title>Dundoo Bills</title></head>\n"
|
|
1017
|
+
"<body>\n"
|
|
1018
|
+
" <div id=\"bill-actions\" class=\"toolbar\">\n"
|
|
1019
|
+
" <button id=\"btn-manual-entry\" class=\"btn-primary\">Add Manual Entry</button>\n"
|
|
1020
|
+
" <button id=\"btn-scan-bill\" data-endpoint=\"/api/scan-bill\">AI Bill Scanner</button>\n"
|
|
1021
|
+
" </div>\n"
|
|
1022
|
+
"</body>\n"
|
|
1023
|
+
"</html>\n",
|
|
1024
|
+
encoding="utf-8",
|
|
1025
|
+
)
|
|
1026
|
+
(repo / "static" / "js" / "scanner.js").write_text(
|
|
1027
|
+
"async function triggerBillScan(file) {\n"
|
|
1028
|
+
" const res = await fetch('/api/scan-bill', { method: 'POST', body: file });\n"
|
|
1029
|
+
" return res.json();\n"
|
|
1030
|
+
"}\n",
|
|
1031
|
+
encoding="utf-8",
|
|
1032
|
+
)
|
|
1033
|
+
(repo / "app" / "bill_scanner.py").write_text(
|
|
1034
|
+
"from typing import Any\n\n"
|
|
1035
|
+
"def normalize_line_items(raw_items: list[dict[str, Any]]) -> list[dict[str, Any]]:\n"
|
|
1036
|
+
" return [{'name': item.get('name', 'unknown'), 'amount': float(item.get('amount', 0.0))} for item in raw_items]\n\n"
|
|
1037
|
+
"def parse_bill(raw_payload: dict[str, Any]) -> dict[str, Any]:\n"
|
|
1038
|
+
" items = normalize_line_items(raw_payload.get('items', []))\n"
|
|
1039
|
+
" total = sum(i['amount'] for i in items)\n"
|
|
1040
|
+
" return {'merchant': raw_payload.get('merchant', 'Unknown'), 'items': items, 'total': total}\n",
|
|
1041
|
+
encoding="utf-8",
|
|
1042
|
+
)
|
|
1043
|
+
(repo / "app" / "routes.py").write_text(
|
|
1044
|
+
"from fastapi import APIRouter\n"
|
|
1045
|
+
"from app.bill_scanner import parse_bill\n\n"
|
|
1046
|
+
"router = APIRouter()\n\n"
|
|
1047
|
+
"@router.post('/api/manual-entry')\n"
|
|
1048
|
+
"def create_manual_entry(payload: dict) -> dict:\n"
|
|
1049
|
+
" return {'status': 'saved', 'entry': payload}\n\n"
|
|
1050
|
+
"@router.post('/api/scan-bill')\n"
|
|
1051
|
+
"def scan_bill_endpoint(payload: dict) -> dict:\n"
|
|
1052
|
+
" parsed = parse_bill(payload)\n"
|
|
1053
|
+
" return {'status': 'scanned', 'bill': parsed}\n",
|
|
1054
|
+
encoding="utf-8",
|
|
1055
|
+
)
|
|
1056
|
+
(repo / "tests" / "test_bill_scanner.py").write_text(
|
|
1057
|
+
"from app.bill_scanner import parse_bill\n"
|
|
1058
|
+
"from app.routes import scan_bill_endpoint\n\n"
|
|
1059
|
+
"def test_parse_bill_calculates_total() -> None:\n"
|
|
1060
|
+
" result = parse_bill({'merchant': 'Cafe', 'items': [{'name': 'Coffee', 'amount': 4.5}]})\n"
|
|
1061
|
+
" assert result['total'] == 4.5\n\n"
|
|
1062
|
+
"def test_scan_bill_endpoint() -> None:\n"
|
|
1063
|
+
" res = scan_bill_endpoint({'merchant': 'Store', 'items': [{'name': 'Book', 'amount': 12.0}]})\n"
|
|
1064
|
+
" assert res['status'] == 'scanned'\n",
|
|
1065
|
+
encoding="utf-8",
|
|
1066
|
+
)
|
|
1067
|
+
|
|
1068
|
+
|
|
1069
|
+
def run_dundoo_bill_scanner_e2e_eval() -> dict[str, object]:
|
|
1070
|
+
"""Execute the Phase 15 Dundoo-style end-to-end feature workflow using CodeGraph MCP tools alone."""
|
|
1071
|
+
from codegraph.mcp.server import create_server
|
|
1072
|
+
|
|
1073
|
+
with tempfile.TemporaryDirectory(prefix="codegraph_dundoo_e2e_") as tmp_dir:
|
|
1074
|
+
repo = Path(tmp_dir)
|
|
1075
|
+
_provision_dundoo_repository(repo)
|
|
1076
|
+
Indexer(repo).index()
|
|
1077
|
+
|
|
1078
|
+
srv = create_server(repo, profile="agent")
|
|
1079
|
+
tools = getattr(srv._tool_manager, "_tools", {})
|
|
1080
|
+
|
|
1081
|
+
def _call(name: str, **kwargs: object) -> object:
|
|
1082
|
+
fn = tools[name].fn
|
|
1083
|
+
return fn(**kwargs)
|
|
1084
|
+
|
|
1085
|
+
def _as_list(val: object, key: str = "results") -> list[dict[str, object]]:
|
|
1086
|
+
if isinstance(val, list):
|
|
1087
|
+
return [item for item in val if isinstance(item, dict)]
|
|
1088
|
+
if isinstance(val, dict):
|
|
1089
|
+
sub = val.get(key)
|
|
1090
|
+
if isinstance(sub, list):
|
|
1091
|
+
return [item for item in sub if isinstance(item, dict)]
|
|
1092
|
+
return []
|
|
1093
|
+
|
|
1094
|
+
# 1. get_architecture()
|
|
1095
|
+
step1_arch = _call("get_architecture")
|
|
1096
|
+
# 2. search_code(query="Add Manual Entry")
|
|
1097
|
+
step2_html_search = _call("search_code", query="Add Manual Entry")
|
|
1098
|
+
# 3. get_file(path="templates/bills.html", start_line=1, end_line=12)
|
|
1099
|
+
step3_template_file = _call("get_file", path="templates/bills.html", start_line=1, end_line=12)
|
|
1100
|
+
# 4. find_routes(path="/api/scan-bill") & search_code(query="/api/scan-bill")
|
|
1101
|
+
step4_routes = _call("find_routes", path="/api/scan-bill")
|
|
1102
|
+
step4_text_route = _call("search_code", query="/api/scan-bill")
|
|
1103
|
+
# 5. find_symbol(symbol="parse_bill")
|
|
1104
|
+
step5_symbol = _call("find_symbol", symbol="parse_bill")
|
|
1105
|
+
# 6. get_file(path="app/bill_scanner.py", start_line=1, end_line=20)
|
|
1106
|
+
step6_scanner_file = _call("get_file", path="app/bill_scanner.py", start_line=1, end_line=20)
|
|
1107
|
+
# 7. find_callers(symbol="parse_bill")
|
|
1108
|
+
step7_callers = _call("find_callers", symbol="parse_bill")
|
|
1109
|
+
# 8. find_tests(symbol="parse_bill")
|
|
1110
|
+
step8_tests = _call("find_tests", symbol="parse_bill")
|
|
1111
|
+
|
|
1112
|
+
html_results = _as_list(step2_html_search)
|
|
1113
|
+
route_results = _as_list(step4_routes, key="routes")
|
|
1114
|
+
text_route_results = _as_list(step4_text_route)
|
|
1115
|
+
sym_results = _as_list(step5_symbol)
|
|
1116
|
+
caller_results = _as_list(step7_callers)
|
|
1117
|
+
test_results = _as_list(step8_tests)
|
|
1118
|
+
|
|
1119
|
+
arch_dict = step1_arch if isinstance(step1_arch, dict) else {}
|
|
1120
|
+
tmpl_dict = step3_template_file if isinstance(step3_template_file, dict) else {}
|
|
1121
|
+
scan_dict = step6_scanner_file if isinstance(step6_scanner_file, dict) else {}
|
|
1122
|
+
|
|
1123
|
+
checks = {
|
|
1124
|
+
"architecture_discovered": bool(arch_dict.get("modules") or arch_dict.get("packages") or arch_dict.get("files_by_layer")),
|
|
1125
|
+
"html_button_found": any(r.get("path") == "templates/bills.html" for r in html_results),
|
|
1126
|
+
"template_lines_read": "Add Manual Entry" in str(tmpl_dict.get("content", "")),
|
|
1127
|
+
"route_discovered": any(
|
|
1128
|
+
r.get("path") == "/api/scan-bill" or r.get("route_path") == "/api/scan-bill"
|
|
1129
|
+
for r in route_results
|
|
1130
|
+
)
|
|
1131
|
+
and any(r.get("path") in ("templates/bills.html", "static/js/scanner.js", "app/routes.py") for r in text_route_results),
|
|
1132
|
+
"symbol_resolved": any(
|
|
1133
|
+
"parse_bill" in str(r.get("symbol") or r.get("name") or "")
|
|
1134
|
+
for r in sym_results
|
|
1135
|
+
),
|
|
1136
|
+
"scanner_source_read": "def parse_bill" in str(scan_dict.get("content", "")),
|
|
1137
|
+
"callers_verified": any("scan_bill_endpoint" in str(r.get("caller") or r.get("source") or "") for r in caller_results),
|
|
1138
|
+
"tests_discovered": any(
|
|
1139
|
+
"test_parse_bill_calculates_total" in str(r.get("test") or r.get("symbol") or "")
|
|
1140
|
+
for r in test_results
|
|
1141
|
+
),
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
return {
|
|
1145
|
+
"all_passed": all(checks.values()),
|
|
1146
|
+
"checks": checks,
|
|
1147
|
+
"steps_executed": [
|
|
1148
|
+
"get_architecture",
|
|
1149
|
+
"search_code(query='Add Manual Entry')",
|
|
1150
|
+
"get_file(path='templates/bills.html', start_line=1, end_line=12)",
|
|
1151
|
+
"find_routes(path='/api/scan-bill')",
|
|
1152
|
+
"find_symbol(symbol='parse_bill')",
|
|
1153
|
+
"get_file(path='app/bill_scanner.py', start_line=1, end_line=20)",
|
|
1154
|
+
"find_callers(symbol='parse_bill')",
|
|
1155
|
+
"find_tests(symbol='parse_bill')",
|
|
1156
|
+
],
|
|
1157
|
+
}
|
|
1158
|
+
|