@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.
Files changed (82) hide show
  1. package/AGENT.md +256 -0
  2. package/AGENT_USAGE.md +231 -0
  3. package/ARCHITECTURE.md +151 -0
  4. package/CLAUDE.md +69 -0
  5. package/DEBUGGING_PLAYBOOK.md +160 -0
  6. package/KNOWLEDGE_INDEX.md +154 -0
  7. package/POTENTIAL_UPDATES +130 -0
  8. package/PROJECT.md +141 -0
  9. package/README.md +371 -0
  10. package/REPO_DIGITAL_TWIN.md +98 -0
  11. package/ROADMAP.md +301 -0
  12. package/SETUP_ANY_REPO.md +85 -0
  13. package/bin/gcie-init.js +20 -0
  14. package/bin/gcie.js +45 -0
  15. package/cli/__init__.py +1 -0
  16. package/cli/app.py +163 -0
  17. package/cli/commands/__init__.py +1 -0
  18. package/cli/commands/cache.py +35 -0
  19. package/cli/commands/context.py +2426 -0
  20. package/cli/commands/context_slices.py +617 -0
  21. package/cli/commands/debug.py +24 -0
  22. package/cli/commands/index.py +17 -0
  23. package/cli/commands/query.py +20 -0
  24. package/cli/commands/setup.py +73 -0
  25. package/config/__init__.py +1 -0
  26. package/config/scanner_config.py +82 -0
  27. package/context/__init__.py +1 -0
  28. package/context/architecture_bootstrap.py +170 -0
  29. package/context/architecture_index.py +185 -0
  30. package/context/architecture_parser.py +170 -0
  31. package/context/architecture_slicer.py +308 -0
  32. package/context/context_router.py +70 -0
  33. package/context/fallback_evaluator.py +21 -0
  34. package/coverage_integration/__init__.py +1 -0
  35. package/coverage_integration/coverage_loader.py +55 -0
  36. package/debugging/__init__.py +12 -0
  37. package/debugging/bug_localizer.py +81 -0
  38. package/debugging/execution_path_analyzer.py +42 -0
  39. package/embeddings/__init__.py +6 -0
  40. package/embeddings/encoder.py +45 -0
  41. package/embeddings/faiss_index.py +72 -0
  42. package/git_integration/__init__.py +1 -0
  43. package/git_integration/git_miner.py +78 -0
  44. package/graphs/__init__.py +17 -0
  45. package/graphs/call_graph.py +70 -0
  46. package/graphs/code_graph.py +81 -0
  47. package/graphs/execution_graph.py +35 -0
  48. package/graphs/git_graph.py +43 -0
  49. package/graphs/graph_store.py +25 -0
  50. package/graphs/node_factory.py +21 -0
  51. package/graphs/test_graph.py +65 -0
  52. package/graphs/validators.py +28 -0
  53. package/graphs/variable_graph.py +51 -0
  54. package/knowledge_index/__init__.py +1 -0
  55. package/knowledge_index/index_builder.py +60 -0
  56. package/knowledge_index/models.py +35 -0
  57. package/knowledge_index/query_api.py +38 -0
  58. package/knowledge_index/store.py +23 -0
  59. package/llm_context/__init__.py +6 -0
  60. package/llm_context/context_builder.py +67 -0
  61. package/llm_context/snippet_selector.py +57 -0
  62. package/package.json +14 -0
  63. package/parser/__init__.py +18 -0
  64. package/parser/ast_parser.py +216 -0
  65. package/parser/call_resolver.py +52 -0
  66. package/parser/models.py +75 -0
  67. package/parser/tree_sitter_adapter.py +56 -0
  68. package/parser/variable_extractor.py +31 -0
  69. package/retrieval/__init__.py +17 -0
  70. package/retrieval/cache.py +22 -0
  71. package/retrieval/hybrid_retriever.py +249 -0
  72. package/retrieval/query_parser.py +38 -0
  73. package/retrieval/ranking.py +43 -0
  74. package/retrieval/semantic_retriever.py +39 -0
  75. package/retrieval/symbolic_retriever.py +80 -0
  76. package/scanner/__init__.py +5 -0
  77. package/scanner/file_filters.py +37 -0
  78. package/scanner/models.py +44 -0
  79. package/scanner/repository_scanner.py +55 -0
  80. package/scripts/bootstrap_from_github.ps1 +41 -0
  81. package/tracing/__init__.py +1 -0
  82. 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]