@pmaddire/gcie 0.1.2
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.
- package/AGENT.md +256 -0
- package/AGENT_USAGE.md +231 -0
- package/ARCHITECTURE.md +151 -0
- package/CLAUDE.md +69 -0
- package/DEBUGGING_PLAYBOOK.md +160 -0
- package/KNOWLEDGE_INDEX.md +154 -0
- package/POTENTIAL_UPDATES +130 -0
- package/PROJECT.md +141 -0
- package/README.md +371 -0
- package/REPO_DIGITAL_TWIN.md +98 -0
- package/ROADMAP.md +301 -0
- package/SETUP_ANY_REPO.md +85 -0
- package/bin/gcie-init.js +20 -0
- package/bin/gcie.js +45 -0
- package/cli/__init__.py +1 -0
- package/cli/app.py +163 -0
- package/cli/commands/__init__.py +1 -0
- package/cli/commands/cache.py +35 -0
- package/cli/commands/context.py +2426 -0
- package/cli/commands/context_slices.py +617 -0
- package/cli/commands/debug.py +24 -0
- package/cli/commands/index.py +17 -0
- package/cli/commands/query.py +20 -0
- package/cli/commands/setup.py +73 -0
- package/config/__init__.py +1 -0
- package/config/scanner_config.py +82 -0
- package/context/__init__.py +1 -0
- package/context/architecture_bootstrap.py +170 -0
- package/context/architecture_index.py +185 -0
- package/context/architecture_parser.py +170 -0
- package/context/architecture_slicer.py +308 -0
- package/context/context_router.py +70 -0
- package/context/fallback_evaluator.py +21 -0
- package/coverage_integration/__init__.py +1 -0
- package/coverage_integration/coverage_loader.py +55 -0
- package/debugging/__init__.py +12 -0
- package/debugging/bug_localizer.py +81 -0
- package/debugging/execution_path_analyzer.py +42 -0
- package/embeddings/__init__.py +6 -0
- package/embeddings/encoder.py +45 -0
- package/embeddings/faiss_index.py +72 -0
- package/git_integration/__init__.py +1 -0
- package/git_integration/git_miner.py +78 -0
- package/graphs/__init__.py +17 -0
- package/graphs/call_graph.py +70 -0
- package/graphs/code_graph.py +81 -0
- package/graphs/execution_graph.py +35 -0
- package/graphs/git_graph.py +43 -0
- package/graphs/graph_store.py +25 -0
- package/graphs/node_factory.py +21 -0
- package/graphs/test_graph.py +65 -0
- package/graphs/validators.py +28 -0
- package/graphs/variable_graph.py +51 -0
- package/knowledge_index/__init__.py +1 -0
- package/knowledge_index/index_builder.py +60 -0
- package/knowledge_index/models.py +35 -0
- package/knowledge_index/query_api.py +38 -0
- package/knowledge_index/store.py +23 -0
- package/llm_context/__init__.py +6 -0
- package/llm_context/context_builder.py +67 -0
- package/llm_context/snippet_selector.py +57 -0
- package/package.json +14 -0
- package/parser/__init__.py +18 -0
- package/parser/ast_parser.py +216 -0
- package/parser/call_resolver.py +52 -0
- package/parser/models.py +75 -0
- package/parser/tree_sitter_adapter.py +56 -0
- package/parser/variable_extractor.py +31 -0
- package/retrieval/__init__.py +17 -0
- package/retrieval/cache.py +22 -0
- package/retrieval/hybrid_retriever.py +249 -0
- package/retrieval/query_parser.py +38 -0
- package/retrieval/ranking.py +43 -0
- package/retrieval/semantic_retriever.py +39 -0
- package/retrieval/symbolic_retriever.py +80 -0
- package/scanner/__init__.py +5 -0
- package/scanner/file_filters.py +37 -0
- package/scanner/models.py +44 -0
- package/scanner/repository_scanner.py +55 -0
- package/scripts/bootstrap_from_github.ps1 +41 -0
- package/tracing/__init__.py +1 -0
- package/tracing/runtime_tracer.py +60 -0
|
@@ -0,0 +1,617 @@
|
|
|
1
|
+
"""CLI command: context slices."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
import subprocess
|
|
7
|
+
import re
|
|
8
|
+
import json
|
|
9
|
+
|
|
10
|
+
from llm_context.snippet_selector import estimate_tokens
|
|
11
|
+
|
|
12
|
+
from context.context_router import route_context
|
|
13
|
+
|
|
14
|
+
from .context import run_context
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
_FRONTEND_KEYWORDS = {
|
|
18
|
+
"frontend",
|
|
19
|
+
"ui",
|
|
20
|
+
"ux",
|
|
21
|
+
"component",
|
|
22
|
+
"react",
|
|
23
|
+
"vue",
|
|
24
|
+
"svelte",
|
|
25
|
+
"angular",
|
|
26
|
+
"css",
|
|
27
|
+
"style",
|
|
28
|
+
"layout",
|
|
29
|
+
"toolbar",
|
|
30
|
+
"canvas",
|
|
31
|
+
"page",
|
|
32
|
+
"view",
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
_BACKEND_KEYWORDS = {
|
|
36
|
+
"backend",
|
|
37
|
+
"api",
|
|
38
|
+
"endpoint",
|
|
39
|
+
"server",
|
|
40
|
+
"service",
|
|
41
|
+
"pipeline",
|
|
42
|
+
"worker",
|
|
43
|
+
"job",
|
|
44
|
+
"queue",
|
|
45
|
+
"model",
|
|
46
|
+
"schema",
|
|
47
|
+
"db",
|
|
48
|
+
"database",
|
|
49
|
+
"sql",
|
|
50
|
+
"migration",
|
|
51
|
+
"redis",
|
|
52
|
+
"cache",
|
|
53
|
+
"auth",
|
|
54
|
+
"controller",
|
|
55
|
+
"router",
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
_WIRING_KEYWORDS = {
|
|
59
|
+
"wiring",
|
|
60
|
+
"route",
|
|
61
|
+
"routes",
|
|
62
|
+
"router",
|
|
63
|
+
"entry",
|
|
64
|
+
"bootstrap",
|
|
65
|
+
"app",
|
|
66
|
+
"main",
|
|
67
|
+
"index",
|
|
68
|
+
"init",
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
_TEST_KEYWORDS = {
|
|
72
|
+
"test",
|
|
73
|
+
"tests",
|
|
74
|
+
"spec",
|
|
75
|
+
"pytest",
|
|
76
|
+
"coverage",
|
|
77
|
+
"regression",
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
_PROFILE_SETTINGS = {
|
|
81
|
+
"recall": {
|
|
82
|
+
"stage_a_budget": 400,
|
|
83
|
+
"stage_b_budget": 800,
|
|
84
|
+
"max_total": 1200,
|
|
85
|
+
"pin_budget": 300,
|
|
86
|
+
"include_tests": False,
|
|
87
|
+
},
|
|
88
|
+
"low": {
|
|
89
|
+
"stage_a_budget": 300,
|
|
90
|
+
"stage_b_budget": 600,
|
|
91
|
+
"max_total": 800,
|
|
92
|
+
"pin_budget": 200,
|
|
93
|
+
"include_tests": False,
|
|
94
|
+
},
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _slice_path(repo: str, segment: str) -> str:
|
|
99
|
+
return str(Path(repo) / segment)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _frontend_bias(query: str) -> bool:
|
|
103
|
+
text = query.lower()
|
|
104
|
+
return any(keyword in text for keyword in _FRONTEND_KEYWORDS)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _backend_bias(query: str) -> bool:
|
|
108
|
+
text = query.lower()
|
|
109
|
+
return any(keyword in text for keyword in _BACKEND_KEYWORDS)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _wiring_needed(query: str) -> bool:
|
|
113
|
+
text = query.lower()
|
|
114
|
+
return any(keyword in text for keyword in _WIRING_KEYWORDS)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _needs_tests(query: str, include_tests: bool) -> bool:
|
|
118
|
+
if include_tests:
|
|
119
|
+
return True
|
|
120
|
+
text = query.lower()
|
|
121
|
+
return any(keyword in text for keyword in _TEST_KEYWORDS)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _node_file_path(node_id: str) -> str:
|
|
125
|
+
if node_id.startswith("file:"):
|
|
126
|
+
return node_id[len("file:") :]
|
|
127
|
+
if node_id.startswith("function:"):
|
|
128
|
+
return node_id[len("function:") :].split("::", 1)[0]
|
|
129
|
+
if node_id.startswith("class:"):
|
|
130
|
+
return node_id[len("class:") :].split("::", 1)[0]
|
|
131
|
+
return ""
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def _infer_slice_from_path(path: str) -> str:
|
|
135
|
+
lowered = path.replace("\\", "/").lower()
|
|
136
|
+
if "/frontend/" in lowered or lowered.startswith("frontend/"):
|
|
137
|
+
return "frontend"
|
|
138
|
+
if "/backend/" in lowered or lowered.startswith("backend/"):
|
|
139
|
+
return "backend"
|
|
140
|
+
if "/tests/" in lowered or lowered.startswith("tests/"):
|
|
141
|
+
return "tests"
|
|
142
|
+
return "pin"
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def _is_test_path(path: str) -> bool:
|
|
146
|
+
lowered = path.replace("\\", "/").lower()
|
|
147
|
+
if "/tests/" in lowered or lowered.startswith("tests/"):
|
|
148
|
+
return True
|
|
149
|
+
filename = Path(lowered).name
|
|
150
|
+
return (
|
|
151
|
+
filename.startswith("test_")
|
|
152
|
+
or filename.endswith("_test.py")
|
|
153
|
+
or ".test." in filename
|
|
154
|
+
or ".spec." in filename
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def _is_wiring_path(path: str) -> bool:
|
|
159
|
+
lowered = path.replace("\\", "/").lower()
|
|
160
|
+
filename = Path(lowered).name
|
|
161
|
+
wiring_names = {
|
|
162
|
+
"app.py",
|
|
163
|
+
"main.py",
|
|
164
|
+
"server.py",
|
|
165
|
+
"wsgi.py",
|
|
166
|
+
"asgi.py",
|
|
167
|
+
"app.js",
|
|
168
|
+
"app.jsx",
|
|
169
|
+
"app.ts",
|
|
170
|
+
"app.tsx",
|
|
171
|
+
"main.js",
|
|
172
|
+
"main.jsx",
|
|
173
|
+
"main.ts",
|
|
174
|
+
"main.tsx",
|
|
175
|
+
"index.js",
|
|
176
|
+
"index.jsx",
|
|
177
|
+
"index.ts",
|
|
178
|
+
"index.tsx",
|
|
179
|
+
}
|
|
180
|
+
if filename in wiring_names:
|
|
181
|
+
return True
|
|
182
|
+
return any(token in lowered for token in ("/routes/", "/router/"))
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def _classify_roles(path: str) -> set[str]:
|
|
186
|
+
roles: set[str] = set()
|
|
187
|
+
if _is_test_path(path):
|
|
188
|
+
roles.add("test")
|
|
189
|
+
return roles
|
|
190
|
+
if _is_wiring_path(path):
|
|
191
|
+
roles.add("wiring")
|
|
192
|
+
roles.add("implementation")
|
|
193
|
+
return roles
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def _dedupe_by_file(snippets: list[dict]) -> list[dict]:
|
|
197
|
+
best: dict[str, dict] = {}
|
|
198
|
+
for item in snippets:
|
|
199
|
+
path = _node_file_path(item.get("node_id", ""))
|
|
200
|
+
if not path:
|
|
201
|
+
continue
|
|
202
|
+
current = best.get(path)
|
|
203
|
+
if current is None or item.get("score", 0.0) > current.get("score", 0.0):
|
|
204
|
+
best[path] = item
|
|
205
|
+
return sorted(best.values(), key=lambda s: s.get("score", 0.0), reverse=True)
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
_EXCLUDE_GLOBS = ["!**/.gcie/**", "!**/.git/**", "!**/.venv/**", "!**/node_modules/**"]
|
|
209
|
+
_INCLUDE_GLOBS = ["**/*.py", "**/*.md", "**/*.js", "**/*.ts", "**/*.tsx"]
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def _rg_top_files(query: str, top_n: int = 5) -> list[str]:
|
|
213
|
+
terms = [t for t in re.split(r"[^A-Za-z0-9_]+", query.lower()) if len(t) >= 3]
|
|
214
|
+
if not terms:
|
|
215
|
+
return []
|
|
216
|
+
pattern = "|".join(re.escape(t) for t in sorted(set(terms)))
|
|
217
|
+
cmd = ["rg", "--count", "-i", pattern]
|
|
218
|
+
for g in _INCLUDE_GLOBS:
|
|
219
|
+
cmd.extend(["-g", g])
|
|
220
|
+
for g in _EXCLUDE_GLOBS:
|
|
221
|
+
cmd.extend(["-g", g])
|
|
222
|
+
cmd.append(".")
|
|
223
|
+
try:
|
|
224
|
+
proc = subprocess.run(cmd, capture_output=True, text=True, check=True)
|
|
225
|
+
except Exception:
|
|
226
|
+
return []
|
|
227
|
+
counts = {}
|
|
228
|
+
for line in proc.stdout.splitlines():
|
|
229
|
+
if ":" not in line:
|
|
230
|
+
continue
|
|
231
|
+
path, count = line.rsplit(":", 1)
|
|
232
|
+
try:
|
|
233
|
+
counts[path] = int(count.strip())
|
|
234
|
+
except ValueError:
|
|
235
|
+
continue
|
|
236
|
+
ranked = sorted(counts.items(), key=lambda item: item[1], reverse=True)
|
|
237
|
+
return [path for path, _ in ranked[:top_n]]
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def _index_files_for_query(query: str) -> list[str]:
|
|
242
|
+
index_path = Path(".gcie") / "architecture_index.json"
|
|
243
|
+
if not index_path.exists():
|
|
244
|
+
return []
|
|
245
|
+
try:
|
|
246
|
+
data = json.loads(index_path.read_text(encoding="utf-8"))
|
|
247
|
+
except Exception:
|
|
248
|
+
return []
|
|
249
|
+
tokens = [t for t in re.split(r"[^A-Za-z0-9_]+", query.lower()) if len(t) >= 3]
|
|
250
|
+
if not tokens:
|
|
251
|
+
return []
|
|
252
|
+
files: list[str] = []
|
|
253
|
+
for subsystem in data.get("subsystems", []):
|
|
254
|
+
name = (subsystem.get("name") or "").lower()
|
|
255
|
+
if not name:
|
|
256
|
+
continue
|
|
257
|
+
key_files = subsystem.get("key_files", []) or []
|
|
258
|
+
if any(token in name or name in token for token in tokens):
|
|
259
|
+
for path in key_files:
|
|
260
|
+
files.append(path)
|
|
261
|
+
continue
|
|
262
|
+
if any(token in (path.lower()) for path in key_files for token in tokens):
|
|
263
|
+
for path in key_files:
|
|
264
|
+
files.append(path)
|
|
265
|
+
|
|
266
|
+
file_map = data.get("file_map", {})
|
|
267
|
+
for path in file_map.keys():
|
|
268
|
+
lowered = path.lower()
|
|
269
|
+
if any(token in lowered for token in tokens):
|
|
270
|
+
files.append(path)
|
|
271
|
+
|
|
272
|
+
return files
|
|
273
|
+
def _file_snippet(path: Path, max_lines: int = 120) -> str:
|
|
274
|
+
try:
|
|
275
|
+
lines = path.read_text(encoding="utf-8").splitlines()
|
|
276
|
+
except Exception:
|
|
277
|
+
return ""
|
|
278
|
+
return "\n".join(lines[:max_lines]).strip()
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
def _merge_snippets(existing: list[dict], extra: list[dict], max_total: int) -> list[dict]:
|
|
282
|
+
by_id: dict[str, dict] = {item.get("node_id", ""): item for item in existing}
|
|
283
|
+
for item in extra:
|
|
284
|
+
node_id = item.get("node_id", "")
|
|
285
|
+
if node_id and node_id not in by_id:
|
|
286
|
+
by_id[node_id] = item
|
|
287
|
+
merged = list(by_id.values())
|
|
288
|
+
merged.sort(key=lambda s: s.get("score", 0.0), reverse=True)
|
|
289
|
+
out: list[dict] = []
|
|
290
|
+
used = 0
|
|
291
|
+
for item in merged:
|
|
292
|
+
tokens = estimate_tokens(item.get("content", ""))
|
|
293
|
+
if used + tokens > max_total:
|
|
294
|
+
continue
|
|
295
|
+
out.append(item)
|
|
296
|
+
used += tokens
|
|
297
|
+
return out
|
|
298
|
+
|
|
299
|
+
def _total_tokens(snippets: list[dict]) -> int:
|
|
300
|
+
return sum(estimate_tokens(item.get("content", "")) for item in snippets)
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
def _found_roles_by_slice(snippets: list[dict]) -> dict[str, set[str]]:
|
|
304
|
+
found: dict[str, set[str]] = {}
|
|
305
|
+
for item in snippets:
|
|
306
|
+
path = _node_file_path(item.get("node_id", ""))
|
|
307
|
+
if not path:
|
|
308
|
+
continue
|
|
309
|
+
slice_name = item.get("slice", "unknown")
|
|
310
|
+
roles = _classify_roles(path)
|
|
311
|
+
found.setdefault(slice_name, set()).update(roles)
|
|
312
|
+
return found
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
def _required_roles_for_slice(
|
|
316
|
+
slice_name: str,
|
|
317
|
+
query: str,
|
|
318
|
+
*,
|
|
319
|
+
include_tests: bool,
|
|
320
|
+
pin: str | None,
|
|
321
|
+
slice_names: set[str],
|
|
322
|
+
) -> set[str]:
|
|
323
|
+
roles: set[str] = set()
|
|
324
|
+
wiring_required = _frontend_bias(query) or _wiring_needed(query) or bool(pin)
|
|
325
|
+
tests_required = _needs_tests(query, include_tests)
|
|
326
|
+
|
|
327
|
+
if slice_name in {"backend", "frontend"}:
|
|
328
|
+
roles.add("implementation")
|
|
329
|
+
if wiring_required:
|
|
330
|
+
if slice_name == "frontend" and "frontend" in slice_names:
|
|
331
|
+
roles.add("wiring")
|
|
332
|
+
elif slice_name == "backend" and "frontend" not in slice_names:
|
|
333
|
+
roles.add("wiring")
|
|
334
|
+
if tests_required and slice_name == "tests":
|
|
335
|
+
roles.add("test")
|
|
336
|
+
return roles
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
def _missing_required_slices(
|
|
340
|
+
snippets: list[dict],
|
|
341
|
+
slices: list[tuple[str, str]],
|
|
342
|
+
query: str,
|
|
343
|
+
*,
|
|
344
|
+
include_tests: bool,
|
|
345
|
+
pin: str | None,
|
|
346
|
+
) -> set[str]:
|
|
347
|
+
slice_names = {name for name, _ in slices}
|
|
348
|
+
found_roles = _found_roles_by_slice(snippets)
|
|
349
|
+
missing: set[str] = set()
|
|
350
|
+
for name, _ in slices:
|
|
351
|
+
required_roles = _required_roles_for_slice(
|
|
352
|
+
name,
|
|
353
|
+
query,
|
|
354
|
+
include_tests=include_tests,
|
|
355
|
+
pin=pin,
|
|
356
|
+
slice_names=slice_names,
|
|
357
|
+
)
|
|
358
|
+
if not required_roles:
|
|
359
|
+
continue
|
|
360
|
+
if not required_roles.issubset(found_roles.get(name, set())):
|
|
361
|
+
missing.add(name)
|
|
362
|
+
return missing
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
def _trim_to_budget(snippets: list[dict], max_total: int, required_slices: set[str]) -> list[dict]:
|
|
366
|
+
# Ensure at least one snippet per required slice, if available.
|
|
367
|
+
required: list[dict] = []
|
|
368
|
+
remaining: list[dict] = []
|
|
369
|
+
seen_required: set[str] = set()
|
|
370
|
+
|
|
371
|
+
for item in snippets:
|
|
372
|
+
slice_name = item.get("slice")
|
|
373
|
+
if slice_name in required_slices and slice_name not in seen_required:
|
|
374
|
+
required.append(item)
|
|
375
|
+
seen_required.add(slice_name)
|
|
376
|
+
else:
|
|
377
|
+
remaining.append(item)
|
|
378
|
+
|
|
379
|
+
ordered = required + remaining
|
|
380
|
+
out: list[dict] = []
|
|
381
|
+
used = 0
|
|
382
|
+
for item in ordered:
|
|
383
|
+
t = estimate_tokens(item.get("content", ""))
|
|
384
|
+
if used + t > max_total and item.get("slice") not in required_slices:
|
|
385
|
+
continue
|
|
386
|
+
out.append(item)
|
|
387
|
+
used += t
|
|
388
|
+
if used >= max_total and all(s in seen_required for s in required_slices):
|
|
389
|
+
break
|
|
390
|
+
|
|
391
|
+
return out
|
|
392
|
+
|
|
393
|
+
|
|
394
|
+
def _apply_profile(
|
|
395
|
+
profile: str | None,
|
|
396
|
+
*,
|
|
397
|
+
stage_a_budget: int,
|
|
398
|
+
stage_b_budget: int,
|
|
399
|
+
max_total: int,
|
|
400
|
+
pin_budget: int,
|
|
401
|
+
include_tests: bool,
|
|
402
|
+
) -> tuple[int, int, int, int, bool, str]:
|
|
403
|
+
if not profile:
|
|
404
|
+
return stage_a_budget, stage_b_budget, max_total, pin_budget, include_tests, "custom"
|
|
405
|
+
|
|
406
|
+
key = profile.lower()
|
|
407
|
+
settings = _PROFILE_SETTINGS.get(key)
|
|
408
|
+
if settings is None:
|
|
409
|
+
return stage_a_budget, stage_b_budget, max_total, pin_budget, include_tests, "custom"
|
|
410
|
+
|
|
411
|
+
return (
|
|
412
|
+
settings["stage_a_budget"],
|
|
413
|
+
settings["stage_b_budget"],
|
|
414
|
+
settings["max_total"],
|
|
415
|
+
settings["pin_budget"],
|
|
416
|
+
settings["include_tests"],
|
|
417
|
+
key,
|
|
418
|
+
)
|
|
419
|
+
|
|
420
|
+
|
|
421
|
+
def run_context_slices(
|
|
422
|
+
repo: str,
|
|
423
|
+
query: str,
|
|
424
|
+
*,
|
|
425
|
+
stage_a_budget: int,
|
|
426
|
+
stage_b_budget: int,
|
|
427
|
+
max_total: int,
|
|
428
|
+
intent: str | None,
|
|
429
|
+
pin: str | None,
|
|
430
|
+
pin_budget: int,
|
|
431
|
+
include_tests: bool,
|
|
432
|
+
profile: str | None = None,
|
|
433
|
+
) -> dict:
|
|
434
|
+
stage_a_budget, stage_b_budget, max_total, pin_budget, include_tests, profile_used = _apply_profile(
|
|
435
|
+
profile,
|
|
436
|
+
stage_a_budget=stage_a_budget,
|
|
437
|
+
stage_b_budget=stage_b_budget,
|
|
438
|
+
max_total=max_total,
|
|
439
|
+
pin_budget=pin_budget,
|
|
440
|
+
include_tests=include_tests,
|
|
441
|
+
)
|
|
442
|
+
|
|
443
|
+
payload = route_context(
|
|
444
|
+
repo,
|
|
445
|
+
query,
|
|
446
|
+
intent=intent,
|
|
447
|
+
max_total=max_total,
|
|
448
|
+
profile=profile_used,
|
|
449
|
+
normal_runner=lambda: run_context_slices_normal(
|
|
450
|
+
repo,
|
|
451
|
+
query,
|
|
452
|
+
stage_a_budget=stage_a_budget,
|
|
453
|
+
stage_b_budget=stage_b_budget,
|
|
454
|
+
max_total=max_total,
|
|
455
|
+
intent=intent,
|
|
456
|
+
pin=pin,
|
|
457
|
+
pin_budget=pin_budget,
|
|
458
|
+
include_tests=include_tests,
|
|
459
|
+
profile=profile_used,
|
|
460
|
+
),
|
|
461
|
+
)
|
|
462
|
+
|
|
463
|
+
if payload.get("mode") == "normal" and payload.get("fallback_reason"):
|
|
464
|
+
direct = run_context(repo, query, budget=max_total * 2, intent=intent, top_k=60)
|
|
465
|
+
snippets = direct.get("snippets", [])
|
|
466
|
+
extra = []
|
|
467
|
+
index_files = _index_files_for_query(query)
|
|
468
|
+
if index_files:
|
|
469
|
+
for rel in index_files:
|
|
470
|
+
path = Path(rel)
|
|
471
|
+
if not path.exists():
|
|
472
|
+
continue
|
|
473
|
+
content = _file_snippet(path)
|
|
474
|
+
if content:
|
|
475
|
+
extra.append({"node_id": f"file:{rel}", "score": 0.9, "content": content})
|
|
476
|
+
else:
|
|
477
|
+
for rel in _rg_top_files(query, top_n=12):
|
|
478
|
+
path = Path(rel)
|
|
479
|
+
if not path.exists():
|
|
480
|
+
continue
|
|
481
|
+
content = _file_snippet(path)
|
|
482
|
+
if content:
|
|
483
|
+
extra.append({"node_id": f"file:{rel}", "score": 0.2, "content": content})
|
|
484
|
+
snippets = _merge_snippets(snippets, extra, max_total=max_total * 2)
|
|
485
|
+
|
|
486
|
+
payload = {
|
|
487
|
+
"query": direct.get("query", query),
|
|
488
|
+
"profile": profile_used,
|
|
489
|
+
"mode": "direct",
|
|
490
|
+
"intent": intent,
|
|
491
|
+
"snippets": snippets,
|
|
492
|
+
"token_estimate": _total_tokens(snippets),
|
|
493
|
+
"fallback_reason": payload.get("fallback_reason"),
|
|
494
|
+
"secondary_fallback": "normal_empty",
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
return payload
|
|
498
|
+
|
|
499
|
+
|
|
500
|
+
def run_context_slices_normal(
|
|
501
|
+
repo: str,
|
|
502
|
+
query: str,
|
|
503
|
+
*,
|
|
504
|
+
stage_a_budget: int,
|
|
505
|
+
stage_b_budget: int,
|
|
506
|
+
max_total: int,
|
|
507
|
+
intent: str | None,
|
|
508
|
+
pin: str | None,
|
|
509
|
+
pin_budget: int,
|
|
510
|
+
include_tests: bool,
|
|
511
|
+
profile: str | None,
|
|
512
|
+
) -> dict:
|
|
513
|
+
repo_path = Path(repo)
|
|
514
|
+
|
|
515
|
+
slices = []
|
|
516
|
+
frontend_bias = _frontend_bias(query)
|
|
517
|
+
backend_bias = _backend_bias(query)
|
|
518
|
+
frontend_path = _slice_path(repo, "frontend")
|
|
519
|
+
backend_path = _slice_path(repo, "backend")
|
|
520
|
+
tests_path = _slice_path(repo, "tests")
|
|
521
|
+
|
|
522
|
+
if frontend_bias and Path(frontend_path).exists():
|
|
523
|
+
slices.append(("frontend", frontend_path))
|
|
524
|
+
if (backend_bias or not frontend_bias) and Path(backend_path).exists():
|
|
525
|
+
slices.append(("backend", backend_path))
|
|
526
|
+
if _needs_tests(query, include_tests) and Path(tests_path).exists():
|
|
527
|
+
slices.append(("tests", tests_path))
|
|
528
|
+
|
|
529
|
+
results: dict[str, dict] = {}
|
|
530
|
+
collected: list[dict] = []
|
|
531
|
+
|
|
532
|
+
# Pin first (cheap, high signal)
|
|
533
|
+
if pin:
|
|
534
|
+
pin_path = str(repo_path / pin)
|
|
535
|
+
if Path(pin_path).exists():
|
|
536
|
+
pin_result = run_context(pin_path, query, budget=pin_budget, intent=intent)
|
|
537
|
+
results["pin"] = pin_result
|
|
538
|
+
for item in pin_result.get("snippets", []):
|
|
539
|
+
node_path = _node_file_path(item.get("node_id", ""))
|
|
540
|
+
item["slice"] = _infer_slice_from_path(node_path)
|
|
541
|
+
collected.append(item)
|
|
542
|
+
|
|
543
|
+
# Stage A
|
|
544
|
+
for name, path in slices:
|
|
545
|
+
if Path(path).exists():
|
|
546
|
+
res = run_context(path, query, budget=stage_a_budget, intent=intent)
|
|
547
|
+
results[name] = res
|
|
548
|
+
for item in res.get("snippets", []):
|
|
549
|
+
item["slice"] = name
|
|
550
|
+
collected.append(item)
|
|
551
|
+
|
|
552
|
+
# Stage B only for missing required roles
|
|
553
|
+
missing = _missing_required_slices(
|
|
554
|
+
collected,
|
|
555
|
+
slices,
|
|
556
|
+
query,
|
|
557
|
+
include_tests=include_tests,
|
|
558
|
+
pin=pin,
|
|
559
|
+
)
|
|
560
|
+
|
|
561
|
+
for name, path in slices:
|
|
562
|
+
if name in missing and Path(path).exists():
|
|
563
|
+
res = run_context(path, query, budget=stage_b_budget, intent=intent)
|
|
564
|
+
results[f"{name}_retry"] = res
|
|
565
|
+
for item in res.get("snippets", []):
|
|
566
|
+
item["slice"] = name
|
|
567
|
+
collected.append(item)
|
|
568
|
+
|
|
569
|
+
deduped = _dedupe_by_file(collected)
|
|
570
|
+
required_slices = {
|
|
571
|
+
name
|
|
572
|
+
for name, _ in slices
|
|
573
|
+
if _required_roles_for_slice(
|
|
574
|
+
name,
|
|
575
|
+
query,
|
|
576
|
+
include_tests=include_tests,
|
|
577
|
+
pin=pin,
|
|
578
|
+
slice_names={n for n, _ in slices},
|
|
579
|
+
)
|
|
580
|
+
}
|
|
581
|
+
missing_after = _missing_required_slices(
|
|
582
|
+
deduped,
|
|
583
|
+
slices,
|
|
584
|
+
query,
|
|
585
|
+
include_tests=include_tests,
|
|
586
|
+
pin=pin,
|
|
587
|
+
)
|
|
588
|
+
effective_max = max_total if not missing_after else max_total * 4
|
|
589
|
+
trimmed = _trim_to_budget(deduped, max_total=effective_max, required_slices=required_slices)
|
|
590
|
+
|
|
591
|
+
return {
|
|
592
|
+
"query": query,
|
|
593
|
+
"profile": profile,
|
|
594
|
+
"mode": "normal",
|
|
595
|
+
"stage_a_budget": stage_a_budget,
|
|
596
|
+
"stage_b_budget": stage_b_budget,
|
|
597
|
+
"max_total_tokens": max_total,
|
|
598
|
+
"intent": intent,
|
|
599
|
+
"results": results,
|
|
600
|
+
"snippets": trimmed,
|
|
601
|
+
"token_estimate": _total_tokens(trimmed),
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
|
|
605
|
+
|
|
606
|
+
|
|
607
|
+
|
|
608
|
+
|
|
609
|
+
|
|
610
|
+
|
|
611
|
+
|
|
612
|
+
|
|
613
|
+
|
|
614
|
+
|
|
615
|
+
|
|
616
|
+
|
|
617
|
+
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""CLI command: debug."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
import networkx as nx
|
|
8
|
+
|
|
9
|
+
from debugging.bug_localizer import localize_bug
|
|
10
|
+
from graphs.call_graph import build_call_graph
|
|
11
|
+
from graphs.variable_graph import build_variable_graph
|
|
12
|
+
from parser.ast_parser import parse_python_file
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def run_debug(path: str, query: str) -> dict[str, list[str]]:
|
|
16
|
+
target = Path(path)
|
|
17
|
+
module = parse_python_file(target)
|
|
18
|
+
graph = nx.compose(build_call_graph((module,)), build_variable_graph((module,)))
|
|
19
|
+
report = localize_bug(graph, query)
|
|
20
|
+
return {
|
|
21
|
+
"relevant_functions": list(report.relevant_functions),
|
|
22
|
+
"call_chain": list(report.call_chain),
|
|
23
|
+
"variable_modifications": list(report.variable_modifications),
|
|
24
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""CLI command: index."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from scanner.repository_scanner import scan_repository
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def run_index(path: str) -> dict[str, int]:
|
|
11
|
+
manifest = scan_repository(Path(path))
|
|
12
|
+
return {
|
|
13
|
+
"total_files": manifest.total_files,
|
|
14
|
+
"source_files": len(manifest.source_files),
|
|
15
|
+
"test_files": len(manifest.test_files),
|
|
16
|
+
"config_files": len(manifest.config_files),
|
|
17
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"""CLI command: query."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
import networkx as nx
|
|
8
|
+
|
|
9
|
+
from graphs.call_graph import build_call_graph
|
|
10
|
+
from graphs.variable_graph import build_variable_graph
|
|
11
|
+
from parser.ast_parser import parse_python_file
|
|
12
|
+
from retrieval.symbolic_retriever import symbolic_retrieve
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def run_query(path: str, query: str, max_hops: int = 2) -> list[str]:
|
|
16
|
+
target = Path(path)
|
|
17
|
+
module = parse_python_file(target)
|
|
18
|
+
graph = nx.compose(build_call_graph((module,)), build_variable_graph((module,)))
|
|
19
|
+
candidates = symbolic_retrieve(graph, query, max_hops=max_hops)
|
|
20
|
+
return [c.node_id for c in candidates]
|