algo-cli-runtime 0.14.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (237) hide show
  1. algo_cli/__init__.py +3 -0
  2. algo_cli/__main__.py +7 -0
  3. algo_cli/_internal/__init__.py +12 -0
  4. algo_cli/_internal/policy_chain.py +259 -0
  5. algo_cli/action_registry.py +1047 -0
  6. algo_cli/agent_blocks.py +550 -0
  7. algo_cli/agent_pipeline.py +1457 -0
  8. algo_cli/agent_threads.py +308 -0
  9. algo_cli/animations.py +316 -0
  10. algo_cli/cache_admission.py +209 -0
  11. algo_cli/capability_mask.py +66 -0
  12. algo_cli/chat_protocol.py +116 -0
  13. algo_cli/chatgpt_auth.py +510 -0
  14. algo_cli/chatgpt_client.py +657 -0
  15. algo_cli/code_rag.py +479 -0
  16. algo_cli/config.py +651 -0
  17. algo_cli/context_budget.py +679 -0
  18. algo_cli/credential_helpers.py +315 -0
  19. algo_cli/deliberation.py +29 -0
  20. algo_cli/display.py +1470 -0
  21. algo_cli/evals/__init__.py +21 -0
  22. algo_cli/evals/algorithm_effectiveness.py +560 -0
  23. algo_cli/evals/competitive_harness_rating.py +702 -0
  24. algo_cli/evals/cot_quality.py +220 -0
  25. algo_cli/evals/harness_retrieval_benchmark.py +401 -0
  26. algo_cli/evals/performance_regression.py +136 -0
  27. algo_cli/evals/scorecard_grading.py +308 -0
  28. algo_cli/evals/session_distribution.py +84 -0
  29. algo_cli/execution_guardrails.py +806 -0
  30. algo_cli/extensions_manifest.py +84 -0
  31. algo_cli/git_evidence.py +227 -0
  32. algo_cli/google_workspace.py +407 -0
  33. algo_cli/google_workspace_auth.py +523 -0
  34. algo_cli/harness.py +2587 -0
  35. algo_cli/identity.py +557 -0
  36. algo_cli/index_compute_lab.py +228 -0
  37. algo_cli/inference_harness.py +70 -0
  38. algo_cli/intelligence/__init__.py +1103 -0
  39. algo_cli/intelligence/acrobat_config.py +307 -0
  40. algo_cli/intelligence/acrobat_manifests.py +338 -0
  41. algo_cli/intelligence/acrobat_models.py +195 -0
  42. algo_cli/intelligence/acrobat_pipeline.py +295 -0
  43. algo_cli/intelligence/acrobat_runtime.py +302 -0
  44. algo_cli/intelligence/acrobat_security.py +261 -0
  45. algo_cli/intelligence/acrobat_workflows.py +226 -0
  46. algo_cli/intelligence/actionability.py +165 -0
  47. algo_cli/intelligence/adversarial_audit.py +136 -0
  48. algo_cli/intelligence/agent_arena.py +92 -0
  49. algo_cli/intelligence/agent_benchmark.py +236 -0
  50. algo_cli/intelligence/agent_runtime.py +171 -0
  51. algo_cli/intelligence/agents_as_tools.py +70 -0
  52. algo_cli/intelligence/artifact_binding.py +80 -0
  53. algo_cli/intelligence/autonomous_engineer.py +1976 -0
  54. algo_cli/intelligence/backpressure.py +99 -0
  55. algo_cli/intelligence/bloom_filter.py +186 -0
  56. algo_cli/intelligence/bonferroni.py +66 -0
  57. algo_cli/intelligence/boundary_compaction.py +98 -0
  58. algo_cli/intelligence/catalog_verifier.py +172 -0
  59. algo_cli/intelligence/cavecrew.py +118 -0
  60. algo_cli/intelligence/changelog.py +176 -0
  61. algo_cli/intelligence/checkpoint_resume.py +92 -0
  62. algo_cli/intelligence/circuit_breaker.py +88 -0
  63. algo_cli/intelligence/clarification_gate.py +101 -0
  64. algo_cli/intelligence/code_graph.py +180 -0
  65. algo_cli/intelligence/coderank.py +97 -0
  66. algo_cli/intelligence/consistent_hash.py +150 -0
  67. algo_cli/intelligence/consortium_synthesis.py +139 -0
  68. algo_cli/intelligence/construction/__init__.py +241 -0
  69. algo_cli/intelligence/construction/common.py +273 -0
  70. algo_cli/intelligence/construction/documents.py +496 -0
  71. algo_cli/intelligence/construction/labor_units.py +1395 -0
  72. algo_cli/intelligence/construction/payments.py +470 -0
  73. algo_cli/intelligence/construction/risk.py +784 -0
  74. algo_cli/intelligence/content_extractor.py +132 -0
  75. algo_cli/intelligence/context_adaptive.py +102 -0
  76. algo_cli/intelligence/context_ops.py +95 -0
  77. algo_cli/intelligence/count_min.py +145 -0
  78. algo_cli/intelligence/cow_state.py +103 -0
  79. algo_cli/intelligence/critic_loop.py +119 -0
  80. algo_cli/intelligence/cross_source.py +113 -0
  81. algo_cli/intelligence/daemon_mode.py +99 -0
  82. algo_cli/intelligence/dag_orchestration.py +151 -0
  83. algo_cli/intelligence/deep_research.py +155 -0
  84. algo_cli/intelligence/degenerate_detector.py +78 -0
  85. algo_cli/intelligence/delta_report.py +92 -0
  86. algo_cli/intelligence/discovery_event_log.py +92 -0
  87. algo_cli/intelligence/document_ingest.py +298 -0
  88. algo_cli/intelligence/dual_layer_validate.py +151 -0
  89. algo_cli/intelligence/echo_fidelity.py +73 -0
  90. algo_cli/intelligence/ema_tuning.py +104 -0
  91. algo_cli/intelligence/event_log.py +92 -0
  92. algo_cli/intelligence/evidence_graph.py +114 -0
  93. algo_cli/intelligence/extension_host.py +162 -0
  94. algo_cli/intelligence/extension_manifest.py +115 -0
  95. algo_cli/intelligence/falsification_suite.py +178 -0
  96. algo_cli/intelligence/finance/__init__.py +169 -0
  97. algo_cli/intelligence/finance/anomalies.py +135 -0
  98. algo_cli/intelligence/finance/ap_ar.py +351 -0
  99. algo_cli/intelligence/finance/cash.py +162 -0
  100. algo_cli/intelligence/finance/close.py +332 -0
  101. algo_cli/intelligence/finance/common.py +244 -0
  102. algo_cli/intelligence/finance/construction.py +135 -0
  103. algo_cli/intelligence/finance/controls.py +172 -0
  104. algo_cli/intelligence/finance/evidence.py +119 -0
  105. algo_cli/intelligence/finance/exceptions.py +157 -0
  106. algo_cli/intelligence/finance/reconciliations.py +254 -0
  107. algo_cli/intelligence/finance/revenue.py +109 -0
  108. algo_cli/intelligence/finance/tax.py +74 -0
  109. algo_cli/intelligence/finance/workpapers.py +111 -0
  110. algo_cli/intelligence/finding_record.py +120 -0
  111. algo_cli/intelligence/flow_dag.py +267 -0
  112. algo_cli/intelligence/gatherer.py +223 -0
  113. algo_cli/intelligence/golden_master.py +98 -0
  114. algo_cli/intelligence/graph_rag.py +195 -0
  115. algo_cli/intelligence/group_chat.py +143 -0
  116. algo_cli/intelligence/hash_dedup.py +145 -0
  117. algo_cli/intelligence/hyperloglog.py +128 -0
  118. algo_cli/intelligence/incremental_index.py +316 -0
  119. algo_cli/intelligence/index_store.py +16 -0
  120. algo_cli/intelligence/iteration_plan.py +133 -0
  121. algo_cli/intelligence/kernel_plugins.py +167 -0
  122. algo_cli/intelligence/lesson_catalog.py +135 -0
  123. algo_cli/intelligence/llm_fallback.py +169 -0
  124. algo_cli/intelligence/log2_histogram.py +267 -0
  125. algo_cli/intelligence/lsp_integration.py +147 -0
  126. algo_cli/intelligence/memory_evolution.py +117 -0
  127. algo_cli/intelligence/minhash_lsh.py +182 -0
  128. algo_cli/intelligence/multi_model_score.py +174 -0
  129. algo_cli/intelligence/multi_tier_grade.py +211 -0
  130. algo_cli/intelligence/negative_controls.py +113 -0
  131. algo_cli/intelligence/numeric_clamp.py +63 -0
  132. algo_cli/intelligence/occ_editor.py +66 -0
  133. algo_cli/intelligence/output_normalize.py +112 -0
  134. algo_cli/intelligence/parallel_delegation.py +98 -0
  135. algo_cli/intelligence/parallel_fanout.py +104 -0
  136. algo_cli/intelligence/permission_modes.py +105 -0
  137. algo_cli/intelligence/pre_push_gate.py +68 -0
  138. algo_cli/intelligence/prefetch.py +171 -0
  139. algo_cli/intelligence/process_framework.py +217 -0
  140. algo_cli/intelligence/project_graph.py +387 -0
  141. algo_cli/intelligence/query_expansion.py +146 -0
  142. algo_cli/intelligence/ralph_loop.py +117 -0
  143. algo_cli/intelligence/rate_limiter.py +153 -0
  144. algo_cli/intelligence/refactor_transaction.py +94 -0
  145. algo_cli/intelligence/research_workspace.py +108 -0
  146. algo_cli/intelligence/retraction_ledger.py +72 -0
  147. algo_cli/intelligence/saga_pattern.py +88 -0
  148. algo_cli/intelligence/session_fork.py +100 -0
  149. algo_cli/intelligence/shadow_editor.py +67 -0
  150. algo_cli/intelligence/shell_session.py +213 -0
  151. algo_cli/intelligence/source_registry.py +143 -0
  152. algo_cli/intelligence/spawn_scales.py +99 -0
  153. algo_cli/intelligence/stat_stability.py +104 -0
  154. algo_cli/intelligence/structural_validator.py +148 -0
  155. algo_cli/intelligence/subagent_spawner.py +111 -0
  156. algo_cli/intelligence/symmetric_verify.py +70 -0
  157. algo_cli/intelligence/task_classifier.py +129 -0
  158. algo_cli/intelligence/team_execution.py +122 -0
  159. algo_cli/intelligence/tiered_access.py +121 -0
  160. algo_cli/intelligence/utility_registry.py +159 -0
  161. algo_cli/intuition_engine.py +560 -0
  162. algo_cli/intuition_injector.py +82 -0
  163. algo_cli/kernels/__init__.py +5 -0
  164. algo_cli/kernels/manifest.py +763 -0
  165. algo_cli/main.py +3903 -0
  166. algo_cli/memory_candidates.py +541 -0
  167. algo_cli/memory_echo_veil.py +394 -0
  168. algo_cli/memory_runtime.py +112 -0
  169. algo_cli/model_info.py +548 -0
  170. algo_cli/model_profile.py +160 -0
  171. algo_cli/model_routing.py +74 -0
  172. algo_cli/oneshot.py +331 -0
  173. algo_cli/perf_telemetry.py +389 -0
  174. algo_cli/plugins.py +245 -0
  175. algo_cli/private_event_store.py +654 -0
  176. algo_cli/quantization/__init__.py +24 -0
  177. algo_cli/quantization/lloyd_max.py +98 -0
  178. algo_cli/quantization/turbo_quant.py +308 -0
  179. algo_cli/reasoning/__init__.py +46 -0
  180. algo_cli/reasoning/combinatorial.py +356 -0
  181. algo_cli/reasoning/graph_of_thought.py +297 -0
  182. algo_cli/reasoning/mcts.py +220 -0
  183. algo_cli/reasoning/neuro_symbolic.py +250 -0
  184. algo_cli/reasoning/react.py +246 -0
  185. algo_cli/reasoning/reflexion.py +225 -0
  186. algo_cli/reasoning/tree_of_thought.py +241 -0
  187. algo_cli/reasoning_bridge.py +150 -0
  188. algo_cli/reconciliation.py +284 -0
  189. algo_cli/reflex.py +385 -0
  190. algo_cli/resources/docs/ALGO.md +13958 -0
  191. algo_cli/resources/docs/algo-cli-algorithm-evidence-contract.md +60 -0
  192. algo_cli/resources/docs/algo-cli-execution-verification-contract.md +59 -0
  193. algo_cli/resources/docs/algo-cli-memory-lifecycle-contract.md +72 -0
  194. algo_cli/resources/docs/harness-extension-cleanup-recommendation.md +41 -0
  195. algo_cli/resources/docs/index-compute-lab-integration.md +32 -0
  196. algo_cli/resources/docs/inference-harness-loop-blueprint-2026-06.md +55 -0
  197. algo_cli/resources/docs/main-split-map.md +35 -0
  198. algo_cli/resources/docs/privacy-and-context.md +48 -0
  199. algo_cli/resources/docs/reflex-loop-v0.2.md +354 -0
  200. algo_cli/resources/skills/README.md +26 -0
  201. algo_cli/resources/skills/algo-cli.md +59 -0
  202. algo_cli/resources/skills/edit-file-precision.md +49 -0
  203. algo_cli/resources/skills/harness-search-first.md +47 -0
  204. algo_cli/resources/skills/memory-recall-ritual.md +51 -0
  205. algo_cli/resources/skills/qol-algorithms.md +224 -0
  206. algo_cli/resources/skills/smart-error-recovery.md +56 -0
  207. algo_cli/resources/skills/tool-selection-cheatsheet.md +65 -0
  208. algo_cli/retrieval_algorithms.py +127 -0
  209. algo_cli/runtime_qos.py +236 -0
  210. algo_cli/runtime_services.py +320 -0
  211. algo_cli/session_commands.py +95 -0
  212. algo_cli/session_mode.py +113 -0
  213. algo_cli/skills.py +430 -0
  214. algo_cli/slash_dispatch.py +1265 -0
  215. algo_cli/small_context.py +206 -0
  216. algo_cli/spawn_budget.py +89 -0
  217. algo_cli/task_ledger.py +84 -0
  218. algo_cli/task_router.py +197 -0
  219. algo_cli/tool_context.py +94 -0
  220. algo_cli/tool_contract.py +99 -0
  221. algo_cli/tool_policy.py +357 -0
  222. algo_cli/tool_runtime.py +647 -0
  223. algo_cli/tools.py +3056 -0
  224. algo_cli/url_scheme.py +174 -0
  225. algo_cli/verify.py +154 -0
  226. algo_cli/version_manifest.py +178 -0
  227. algo_cli/vision_screenshot_verify.py +76 -0
  228. algo_cli/workspace_resolver.py +68 -0
  229. algo_cli/x_account.py +209 -0
  230. algo_cli/xai_auth.py +374 -0
  231. algo_cli/xai_client.py +600 -0
  232. algo_cli_runtime-0.14.0.dist-info/METADATA +369 -0
  233. algo_cli_runtime-0.14.0.dist-info/RECORD +237 -0
  234. algo_cli_runtime-0.14.0.dist-info/WHEEL +4 -0
  235. algo_cli_runtime-0.14.0.dist-info/entry_points.txt +3 -0
  236. algo_cli_runtime-0.14.0.dist-info/licenses/LICENSE +21 -0
  237. ollama_cli/__init__.py +67 -0
@@ -0,0 +1,387 @@
1
+ """Deterministic project graph construction for local coding work.
2
+
3
+ The first implementation is intentionally Python-AST based. It is syntax-aware,
4
+ offline, dependency-free, and leaves room for optional tree-sitter/LSP enrichers
5
+ without making those heavy dependencies mandatory.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import ast
11
+ import json
12
+ import os
13
+ import subprocess
14
+ import time
15
+ from dataclasses import asdict, dataclass, field
16
+ from pathlib import Path
17
+ from typing import Any
18
+
19
+
20
+ SOURCE_SUFFIXES = frozenset({".py", ".pyi"})
21
+ SKIP_DIRS = frozenset({
22
+ ".git",
23
+ ".algo",
24
+ ".mypy_cache",
25
+ ".pytest_cache",
26
+ ".ruff_cache",
27
+ ".venv",
28
+ "__pycache__",
29
+ "build",
30
+ "dist",
31
+ "node_modules",
32
+ "venv",
33
+ })
34
+
35
+
36
+ @dataclass(frozen=True)
37
+ class FileNode:
38
+ path: str
39
+ language: str
40
+ is_test: bool
41
+ size: int
42
+ mtime_ns: int
43
+ recent_commits: int = 0
44
+ recency_score: float = 0.0
45
+
46
+
47
+ @dataclass(frozen=True)
48
+ class SymbolNode:
49
+ symbol_id: str
50
+ name: str
51
+ qualname: str
52
+ kind: str
53
+ file: str
54
+ line: int
55
+ end_line: int
56
+
57
+
58
+ @dataclass(frozen=True)
59
+ class ImportEdge:
60
+ source_file: str
61
+ module: str
62
+ names: tuple[str, ...] = ()
63
+ target_file: str | None = None
64
+
65
+
66
+ @dataclass(frozen=True)
67
+ class TestMapping:
68
+ test_file: str
69
+ source_file: str
70
+ score: float
71
+ reasons: tuple[str, ...]
72
+
73
+
74
+ @dataclass
75
+ class ProjectGraph:
76
+ root: str
77
+ built_at: float
78
+ files: dict[str, FileNode] = field(default_factory=dict)
79
+ symbols: dict[str, SymbolNode] = field(default_factory=dict)
80
+ imports: list[ImportEdge] = field(default_factory=list)
81
+ test_mappings: list[TestMapping] = field(default_factory=list)
82
+ diagnostics: list[dict[str, Any]] = field(default_factory=list)
83
+
84
+ def to_dict(self) -> dict[str, Any]:
85
+ return {
86
+ "root": self.root,
87
+ "built_at": self.built_at,
88
+ "files": {path: asdict(node) for path, node in self.files.items()},
89
+ "symbols": {key: asdict(node) for key, node in self.symbols.items()},
90
+ "imports": [asdict(edge) for edge in self.imports],
91
+ "test_mappings": [asdict(mapping) for mapping in self.test_mappings],
92
+ "diagnostics": self.diagnostics,
93
+ }
94
+
95
+ @classmethod
96
+ def from_dict(cls, data: dict[str, Any]) -> "ProjectGraph":
97
+ graph = cls(root=str(data.get("root", "")), built_at=float(data.get("built_at", 0.0) or 0.0))
98
+ graph.files = {
99
+ str(path): FileNode(**node)
100
+ for path, node in (data.get("files") or {}).items()
101
+ if isinstance(node, dict)
102
+ }
103
+ graph.symbols = {
104
+ str(key): SymbolNode(**node)
105
+ for key, node in (data.get("symbols") or {}).items()
106
+ if isinstance(node, dict)
107
+ }
108
+ graph.imports = [
109
+ ImportEdge(
110
+ source_file=str(edge.get("source_file", "")),
111
+ module=str(edge.get("module", "")),
112
+ names=tuple(str(name) for name in edge.get("names", ()) or ()),
113
+ target_file=edge.get("target_file"),
114
+ )
115
+ for edge in data.get("imports", []) or []
116
+ if isinstance(edge, dict)
117
+ ]
118
+ graph.test_mappings = [
119
+ TestMapping(
120
+ test_file=str(mapping.get("test_file", "")),
121
+ source_file=str(mapping.get("source_file", "")),
122
+ score=float(mapping.get("score", 0.0) or 0.0),
123
+ reasons=tuple(str(reason) for reason in mapping.get("reasons", ()) or ()),
124
+ )
125
+ for mapping in data.get("test_mappings", []) or []
126
+ if isinstance(mapping, dict)
127
+ ]
128
+ graph.diagnostics = [
129
+ item for item in data.get("diagnostics", []) or [] if isinstance(item, dict)
130
+ ]
131
+ return graph
132
+
133
+
134
+ def index_path(root: Path) -> Path:
135
+ return root / ".algo" / "index" / "project_graph.json"
136
+
137
+
138
+ def load_project_graph(root: Path) -> ProjectGraph | None:
139
+ path = index_path(root)
140
+ if not path.exists():
141
+ return None
142
+ try:
143
+ data = json.loads(path.read_text(encoding="utf-8"))
144
+ except (OSError, json.JSONDecodeError):
145
+ return None
146
+ return ProjectGraph.from_dict(data) if isinstance(data, dict) else None
147
+
148
+
149
+ def save_project_graph(graph: ProjectGraph) -> Path:
150
+ path = index_path(Path(graph.root))
151
+ path.parent.mkdir(parents=True, exist_ok=True)
152
+ path.write_text(json.dumps(graph.to_dict(), indent=2, sort_keys=True), encoding="utf-8")
153
+ return path
154
+
155
+
156
+ def _iter_source_files(root: Path) -> list[Path]:
157
+ files: list[Path] = []
158
+ for current, dirs, names in os.walk(root):
159
+ dirs[:] = [name for name in dirs if name not in SKIP_DIRS]
160
+ for name in names:
161
+ path = Path(current) / name
162
+ if path.suffix.lower() in SOURCE_SUFFIXES:
163
+ files.append(path)
164
+ return sorted(files, key=lambda item: item.relative_to(root).as_posix().lower())
165
+
166
+
167
+ def _is_test_path(rel: str) -> bool:
168
+ path = rel.replace("\\", "/")
169
+ name = Path(path).name
170
+ return path.startswith("tests/") or name.startswith("test_") or name.endswith("_test.py")
171
+
172
+
173
+ def _module_to_rel(module: str, source_files: set[str]) -> str | None:
174
+ if not module:
175
+ return None
176
+ candidate = module.replace(".", "/") + ".py"
177
+ if candidate in source_files:
178
+ return candidate
179
+ package_candidate = module.replace(".", "/") + "/__init__.py"
180
+ return package_candidate if package_candidate in source_files else None
181
+
182
+
183
+ def _git_recent_counts(root: Path) -> dict[str, int]:
184
+ try:
185
+ proc = subprocess.run(
186
+ ["git", "-C", str(root), "log", "--since=30 days ago", "--name-only", "--pretty=format:"],
187
+ text=True,
188
+ encoding="utf-8",
189
+ errors="replace",
190
+ capture_output=True,
191
+ timeout=10,
192
+ )
193
+ except Exception:
194
+ return {}
195
+ if proc.returncode != 0:
196
+ return {}
197
+ counts: dict[str, int] = {}
198
+ for line in (proc.stdout or "").splitlines():
199
+ rel = line.strip().replace("\\", "/")
200
+ if rel:
201
+ counts[rel] = counts.get(rel, 0) + 1
202
+ return counts
203
+
204
+
205
+ class _SymbolVisitor(ast.NodeVisitor):
206
+ def __init__(self, rel: str) -> None:
207
+ self.rel = rel
208
+ self.scope: list[str] = []
209
+ self.symbols: list[SymbolNode] = []
210
+ self.imports: list[ImportEdge] = []
211
+
212
+ def _add_symbol(self, node: ast.AST, name: str, kind: str) -> None:
213
+ qualname = ".".join([*self.scope, name])
214
+ symbol_id = f"{self.rel}::{qualname}"
215
+ self.symbols.append(
216
+ SymbolNode(
217
+ symbol_id=symbol_id,
218
+ name=name,
219
+ qualname=qualname,
220
+ kind=kind,
221
+ file=self.rel,
222
+ line=int(getattr(node, "lineno", 1)),
223
+ end_line=int(getattr(node, "end_lineno", getattr(node, "lineno", 1))),
224
+ )
225
+ )
226
+
227
+ def visit_ClassDef(self, node: ast.ClassDef) -> None:
228
+ self._add_symbol(node, node.name, "class")
229
+ self.scope.append(node.name)
230
+ self.generic_visit(node)
231
+ self.scope.pop()
232
+
233
+ def visit_FunctionDef(self, node: ast.FunctionDef) -> None:
234
+ self._add_symbol(node, node.name, "function" if not self.scope else "method")
235
+ self.scope.append(node.name)
236
+ self.generic_visit(node)
237
+ self.scope.pop()
238
+
239
+ def visit_AsyncFunctionDef(self, node: ast.AsyncFunctionDef) -> None:
240
+ self.visit_FunctionDef(node) # type: ignore[arg-type]
241
+
242
+ def visit_Import(self, node: ast.Import) -> None:
243
+ for alias in node.names:
244
+ self.imports.append(ImportEdge(self.rel, alias.name, (alias.asname or alias.name,)))
245
+
246
+ def visit_ImportFrom(self, node: ast.ImportFrom) -> None:
247
+ module = "." * int(node.level or 0) + (node.module or "")
248
+ names = tuple(alias.asname or alias.name for alias in node.names)
249
+ self.imports.append(ImportEdge(self.rel, module, names))
250
+
251
+
252
+ def _parse_python(path: Path, root: Path) -> tuple[list[SymbolNode], list[ImportEdge]]:
253
+ rel = path.relative_to(root).as_posix()
254
+ try:
255
+ tree = ast.parse(path.read_text(encoding="utf-8", errors="replace"), filename=rel)
256
+ except (OSError, SyntaxError):
257
+ return [], []
258
+ visitor = _SymbolVisitor(rel)
259
+ visitor.visit(tree)
260
+ return visitor.symbols, visitor.imports
261
+
262
+
263
+ def _build_test_mappings(files: dict[str, FileNode], imports: list[ImportEdge]) -> list[TestMapping]:
264
+ source_files = [path for path, node in files.items() if not node.is_test]
265
+ test_files = [path for path, node in files.items() if node.is_test]
266
+ imports_by_test: dict[str, set[str]] = {}
267
+ for edge in imports:
268
+ if edge.source_file in test_files:
269
+ values = imports_by_test.setdefault(edge.source_file, set())
270
+ values.add(edge.module.lstrip("."))
271
+ values.update(edge.names)
272
+
273
+ mappings: list[TestMapping] = []
274
+ for test_file in test_files:
275
+ test_stem = Path(test_file).stem.replace("test_", "").replace("_test", "")
276
+ for source_file in source_files:
277
+ reasons: list[str] = []
278
+ source_stem = Path(source_file).stem
279
+ score = 0.0
280
+ if test_stem and test_stem == source_stem:
281
+ score += 0.7
282
+ reasons.append("filename maps test stem to source stem")
283
+ source_module = source_file.removesuffix(".py").replace("/", ".")
284
+ imported = imports_by_test.get(test_file, set())
285
+ if source_module in imported or source_stem in imported:
286
+ score += 0.4
287
+ reasons.append("test imports source module or symbol")
288
+ if source_stem in test_file:
289
+ score += 0.2
290
+ reasons.append("source name appears in test path")
291
+ if score > 0.0:
292
+ mappings.append(
293
+ TestMapping(
294
+ test_file=test_file,
295
+ source_file=source_file,
296
+ score=round(min(score, 1.0), 3),
297
+ reasons=tuple(reasons),
298
+ )
299
+ )
300
+ mappings.sort(key=lambda item: (-item.score, item.test_file, item.source_file))
301
+ return mappings
302
+
303
+
304
+ def build_project_graph(root: Path | str, *, persist: bool = True) -> ProjectGraph:
305
+ root_path = Path(root).resolve()
306
+ recent_counts = _git_recent_counts(root_path)
307
+ files: dict[str, FileNode] = {}
308
+ symbols: dict[str, SymbolNode] = {}
309
+ imports: list[ImportEdge] = []
310
+ source_paths = _iter_source_files(root_path)
311
+ source_rel = {path.relative_to(root_path).as_posix() for path in source_paths}
312
+
313
+ max_recent = max(recent_counts.values(), default=0)
314
+ for path in source_paths:
315
+ rel = path.relative_to(root_path).as_posix()
316
+ try:
317
+ stat = path.stat()
318
+ except OSError:
319
+ continue
320
+ recent = recent_counts.get(rel, 0)
321
+ files[rel] = FileNode(
322
+ path=rel,
323
+ language="python",
324
+ is_test=_is_test_path(rel),
325
+ size=int(stat.st_size),
326
+ mtime_ns=int(stat.st_mtime_ns),
327
+ recent_commits=recent,
328
+ recency_score=round(recent / max_recent, 3) if max_recent else 0.0,
329
+ )
330
+ parsed_symbols, parsed_imports = _parse_python(path, root_path)
331
+ for symbol in parsed_symbols:
332
+ symbols[symbol.symbol_id] = symbol
333
+ for edge in parsed_imports:
334
+ target_file = _module_to_rel(edge.module.lstrip("."), source_rel)
335
+ imports.append(
336
+ ImportEdge(
337
+ source_file=edge.source_file,
338
+ module=edge.module,
339
+ names=edge.names,
340
+ target_file=target_file,
341
+ )
342
+ )
343
+
344
+ graph = ProjectGraph(
345
+ root=str(root_path),
346
+ built_at=time.time(),
347
+ files=files,
348
+ symbols=symbols,
349
+ imports=imports,
350
+ )
351
+ graph.test_mappings = _build_test_mappings(files, imports)
352
+ if persist:
353
+ save_project_graph(graph)
354
+ return graph
355
+
356
+
357
+ def query_project_graph(graph: ProjectGraph, term: str, *, limit: int = 20) -> list[dict[str, Any]]:
358
+ needle = (term or "").strip().lower()
359
+ if not needle:
360
+ return []
361
+ rows: list[dict[str, Any]] = []
362
+ for file in graph.files.values():
363
+ if needle in file.path.lower():
364
+ rows.append({"kind": "file", "id": file.path, "score": 1.0, "path": file.path})
365
+ for symbol in graph.symbols.values():
366
+ haystack = f"{symbol.name} {symbol.qualname} {symbol.file}".lower()
367
+ if needle in haystack:
368
+ rows.append({
369
+ "kind": "symbol",
370
+ "id": symbol.symbol_id,
371
+ "score": 1.0 if needle in symbol.qualname.lower() else 0.75,
372
+ "path": symbol.file,
373
+ "line": symbol.line,
374
+ "qualname": symbol.qualname,
375
+ })
376
+ for edge in graph.imports:
377
+ if needle in edge.module.lower() or any(needle in name.lower() for name in edge.names):
378
+ rows.append({
379
+ "kind": "import",
380
+ "id": f"{edge.source_file}->{edge.module}",
381
+ "score": 0.65,
382
+ "path": edge.source_file,
383
+ "module": edge.module,
384
+ "target_file": edge.target_file,
385
+ })
386
+ rows.sort(key=lambda row: (-float(row["score"]), str(row["kind"]), str(row["id"])))
387
+ return rows[:limit]
@@ -0,0 +1,146 @@
1
+ """B49. Query Expansion + Cross-Encoder Reranking.
2
+
3
+ Generate query variants for better recall, then rerank with a cross-encoder
4
+ for precision. Source: PyRagix pattern.
5
+ """
6
+ from __future__ import annotations
7
+
8
+ import re
9
+ from dataclasses import dataclass
10
+ from typing import Callable, Sequence
11
+
12
+
13
+ @dataclass
14
+ class QueryVariant:
15
+ text: str
16
+ source: str = "synonym" # synonym, reformulation, keyword, original
17
+
18
+
19
+ @dataclass
20
+ class SearchResult:
21
+ title: str
22
+ url: str
23
+ content: str
24
+ score: float = 0.0
25
+
26
+
27
+ @dataclass
28
+ class RerankedResult:
29
+ title: str
30
+ url: str
31
+ content: str
32
+ original_score: float
33
+ rerank_score: float
34
+ rank: int = 0
35
+
36
+
37
+ class QueryExpander:
38
+ """Expand a single query into multiple variants for better recall."""
39
+
40
+ SYNONYM_MAP: dict[str, list[str]] = {
41
+ "error": ["error", "exception", "failure", "crash", "bug"],
42
+ "fast": ["fast", "quick", "efficient", "performant", "speed"],
43
+ "test": ["test", "unit test", "integration test", "spec"],
44
+ "config": ["config", "configuration", "settings", "options"],
45
+ "search": ["search", "find", "locate", "query", "lookup"],
46
+ "build": ["build", "compile", "make", "construct"],
47
+ "agent": ["agent", "assistant", "bot", "worker"],
48
+ "pattern": ["pattern", "approach", "strategy", "method", "technique"],
49
+ }
50
+
51
+ def expand(self, query: str, max_variants: int = 5) -> list[QueryVariant]:
52
+ variants: list[QueryVariant] = [QueryVariant(query, "original")]
53
+ words = re.findall(r"\w+", query.lower())
54
+
55
+ # Synonym substitution
56
+ for word in words:
57
+ if word in self.SYNONYM_MAP:
58
+ for syn in self.SYNONYM_MAP[word]:
59
+ if syn != word:
60
+ variant = re.sub(rf"\b{re.escape(word)}\b", syn, query, flags=re.IGNORECASE)
61
+ if variant.lower() != query.lower():
62
+ variants.append(QueryVariant(variant, "synonym"))
63
+
64
+ # Keyword extraction
65
+ keywords = " ".join(w for w in words if len(w) > 3)
66
+ if keywords and keywords.lower() != query.lower():
67
+ variants.append(QueryVariant(keywords, "keyword"))
68
+
69
+ # Reformulation
70
+ if len(words) > 2:
71
+ reversed_q = " ".join(reversed(words))
72
+ variants.append(QueryVariant(reversed_q, "reformulation"))
73
+
74
+ # Deduplicate
75
+ seen: set[str] = set()
76
+ unique: list[QueryVariant] = []
77
+ for v in variants:
78
+ key = v.text.lower().strip()
79
+ if key not in seen:
80
+ seen.add(key)
81
+ unique.append(v)
82
+
83
+ return unique[:max_variants]
84
+
85
+
86
+ class CrossEncoderReranker:
87
+ """Rerank search results using a scoring function.
88
+
89
+ In production, this would use a cross-encoder model. Here we use a
90
+ lightweight BM25-style overlap scorer as a fallback.
91
+ """
92
+
93
+ def __init__(self, score_fn: Callable[[str, str], float] | None = None):
94
+ self._score_fn = score_fn or self._overlap_score
95
+
96
+ @staticmethod
97
+ def _overlap_score(query: str, document: str) -> float:
98
+ q_terms = set(query.lower().split())
99
+ d_terms = document.lower().split()
100
+ if not q_terms or not d_terms:
101
+ return 0.0
102
+ hits = sum(1 for t in d_terms if t in q_terms)
103
+ return hits / (len(d_terms) ** 0.5 + 1e-9)
104
+
105
+ def rerank(
106
+ self,
107
+ query: str,
108
+ results: Sequence[SearchResult],
109
+ top_k: int = 10,
110
+ ) -> list[RerankedResult]:
111
+ scored: list[RerankedResult] = []
112
+ for r in results:
113
+ rerank_score = self._score_fn(query, f"{r.title} {r.content}")
114
+ scored.append(
115
+ RerankedResult(
116
+ title=r.title,
117
+ url=r.url,
118
+ content=r.content,
119
+ original_score=r.score,
120
+ rerank_score=rerank_score,
121
+ )
122
+ )
123
+ scored.sort(key=lambda x: x.rerank_score, reverse=True)
124
+ for i, r in enumerate(scored):
125
+ r.rank = i + 1
126
+ return scored[:top_k]
127
+
128
+
129
+ def expand_and_rerank(
130
+ query: str,
131
+ search_fn: Callable[[str], list[SearchResult]],
132
+ max_variants: int = 5,
133
+ top_k: int = 10,
134
+ ) -> list[RerankedResult]:
135
+ """Expand query, search with all variants, deduplicate, rerank."""
136
+ expander = QueryExpander()
137
+ reranker = CrossEncoderReranker()
138
+
139
+ variants = expander.expand(query, max_variants)
140
+ all_results: dict[str, SearchResult] = {}
141
+ for v in variants:
142
+ for r in search_fn(v.text):
143
+ if r.url not in all_results or r.score > all_results[r.url].score:
144
+ all_results[r.url] = r
145
+
146
+ return reranker.rerank(query, list(all_results.values()), top_k)
@@ -0,0 +1,117 @@
1
+ """B81. Ralph Loop: Continuous Test-Fix Cycles.
2
+
3
+ Run tests, fix failures, repeat until all pass (max N iterations).
4
+ Source: CCASP pattern.
5
+ """
6
+ from __future__ import annotations
7
+
8
+ import time
9
+ from dataclasses import dataclass, field
10
+ from enum import Enum, auto
11
+ from typing import Callable
12
+
13
+
14
+ class LoopStatus(Enum):
15
+ PASSING = auto()
16
+ FAILED = auto()
17
+ TIMEOUT = auto()
18
+ MAX_ITERATIONS = auto()
19
+
20
+
21
+ @dataclass
22
+ class TestRun:
23
+ iteration: int
24
+ passed: int = 0
25
+ failed: int = 0
26
+ errors: int = 0
27
+ duration_s: float = 0.0
28
+ failures: list[str] = field(default_factory=list)
29
+ output: str = ""
30
+
31
+
32
+ @dataclass
33
+ class RalphResult:
34
+ status: LoopStatus
35
+ iterations: int
36
+ runs: list[TestRun] = field(default_factory=list)
37
+ total_duration_s: float = 0.0
38
+ final_passed: int = 0
39
+ final_failed: int = 0
40
+
41
+
42
+ class RalphLoop:
43
+ """Run tests, fix failures, repeat until all pass."""
44
+
45
+ def __init__(self, max_iterations: int = 5, timeout_s: float = 120.0) -> None:
46
+ self._max_iterations = max_iterations
47
+ self._timeout_s = timeout_s
48
+
49
+ def run(self, test_fn: Callable[[], TestRun],
50
+ fix_fn: Callable[[TestRun], bool] | None = None) -> RalphResult:
51
+ """Execute the Ralph Loop.
52
+
53
+ Args:
54
+ test_fn: Run tests and return results
55
+ fix_fn: Attempt to fix failures. Return True if fixes were applied.
56
+ """
57
+ result = RalphResult(status=LoopStatus.FAILED, iterations=0)
58
+ start = time.time()
59
+
60
+ for i in range(self._max_iterations):
61
+ if time.time() - start > self._timeout_s:
62
+ result.status = LoopStatus.TIMEOUT
63
+ break
64
+
65
+ run = test_fn()
66
+ run.iteration = i + 1
67
+ result.runs.append(run)
68
+ result.iterations = i + 1
69
+
70
+ if run.failed == 0 and run.errors == 0:
71
+ result.status = LoopStatus.PASSING
72
+ result.final_passed = run.passed
73
+ result.final_failed = 0
74
+ break
75
+
76
+ result.final_passed = run.passed
77
+ result.final_failed = run.failed
78
+
79
+ if fix_fn:
80
+ fixed = fix_fn(run)
81
+ if not fixed:
82
+ result.status = LoopStatus.FAILED
83
+ break
84
+ else:
85
+ result.status = LoopStatus.FAILED
86
+ break
87
+
88
+ else:
89
+ result.status = LoopStatus.MAX_ITERATIONS
90
+
91
+ result.total_duration_s = time.time() - start
92
+ return result
93
+
94
+ @staticmethod
95
+ def parse_pytest_output(output: str) -> TestRun:
96
+ """Parse pytest output to extract pass/fail counts."""
97
+ run = TestRun(iteration=0)
98
+ run.output = output
99
+
100
+ for line in output.splitlines():
101
+ line = line.strip()
102
+ if "passed" in line and "failed" in line:
103
+ # "5 passed, 2 failed in 0.3s"
104
+ parts = line.split(",")
105
+ for part in parts:
106
+ part = part.strip()
107
+ if "passed" in part:
108
+ run.passed = int(part.split()[0])
109
+ elif "failed" in part:
110
+ run.failed = int(part.split()[0])
111
+ # Extract failure names
112
+ elif "passed" in line and "failed" not in line:
113
+ run.passed = int(line.split()[0])
114
+ elif "error" in line.lower():
115
+ run.failures.append(line)
116
+
117
+ return run