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.
- algo_cli/__init__.py +3 -0
- algo_cli/__main__.py +7 -0
- algo_cli/_internal/__init__.py +12 -0
- algo_cli/_internal/policy_chain.py +259 -0
- algo_cli/action_registry.py +1047 -0
- algo_cli/agent_blocks.py +550 -0
- algo_cli/agent_pipeline.py +1457 -0
- algo_cli/agent_threads.py +308 -0
- algo_cli/animations.py +316 -0
- algo_cli/cache_admission.py +209 -0
- algo_cli/capability_mask.py +66 -0
- algo_cli/chat_protocol.py +116 -0
- algo_cli/chatgpt_auth.py +510 -0
- algo_cli/chatgpt_client.py +657 -0
- algo_cli/code_rag.py +479 -0
- algo_cli/config.py +651 -0
- algo_cli/context_budget.py +679 -0
- algo_cli/credential_helpers.py +315 -0
- algo_cli/deliberation.py +29 -0
- algo_cli/display.py +1470 -0
- algo_cli/evals/__init__.py +21 -0
- algo_cli/evals/algorithm_effectiveness.py +560 -0
- algo_cli/evals/competitive_harness_rating.py +702 -0
- algo_cli/evals/cot_quality.py +220 -0
- algo_cli/evals/harness_retrieval_benchmark.py +401 -0
- algo_cli/evals/performance_regression.py +136 -0
- algo_cli/evals/scorecard_grading.py +308 -0
- algo_cli/evals/session_distribution.py +84 -0
- algo_cli/execution_guardrails.py +806 -0
- algo_cli/extensions_manifest.py +84 -0
- algo_cli/git_evidence.py +227 -0
- algo_cli/google_workspace.py +407 -0
- algo_cli/google_workspace_auth.py +523 -0
- algo_cli/harness.py +2587 -0
- algo_cli/identity.py +557 -0
- algo_cli/index_compute_lab.py +228 -0
- algo_cli/inference_harness.py +70 -0
- algo_cli/intelligence/__init__.py +1103 -0
- algo_cli/intelligence/acrobat_config.py +307 -0
- algo_cli/intelligence/acrobat_manifests.py +338 -0
- algo_cli/intelligence/acrobat_models.py +195 -0
- algo_cli/intelligence/acrobat_pipeline.py +295 -0
- algo_cli/intelligence/acrobat_runtime.py +302 -0
- algo_cli/intelligence/acrobat_security.py +261 -0
- algo_cli/intelligence/acrobat_workflows.py +226 -0
- algo_cli/intelligence/actionability.py +165 -0
- algo_cli/intelligence/adversarial_audit.py +136 -0
- algo_cli/intelligence/agent_arena.py +92 -0
- algo_cli/intelligence/agent_benchmark.py +236 -0
- algo_cli/intelligence/agent_runtime.py +171 -0
- algo_cli/intelligence/agents_as_tools.py +70 -0
- algo_cli/intelligence/artifact_binding.py +80 -0
- algo_cli/intelligence/autonomous_engineer.py +1976 -0
- algo_cli/intelligence/backpressure.py +99 -0
- algo_cli/intelligence/bloom_filter.py +186 -0
- algo_cli/intelligence/bonferroni.py +66 -0
- algo_cli/intelligence/boundary_compaction.py +98 -0
- algo_cli/intelligence/catalog_verifier.py +172 -0
- algo_cli/intelligence/cavecrew.py +118 -0
- algo_cli/intelligence/changelog.py +176 -0
- algo_cli/intelligence/checkpoint_resume.py +92 -0
- algo_cli/intelligence/circuit_breaker.py +88 -0
- algo_cli/intelligence/clarification_gate.py +101 -0
- algo_cli/intelligence/code_graph.py +180 -0
- algo_cli/intelligence/coderank.py +97 -0
- algo_cli/intelligence/consistent_hash.py +150 -0
- algo_cli/intelligence/consortium_synthesis.py +139 -0
- algo_cli/intelligence/construction/__init__.py +241 -0
- algo_cli/intelligence/construction/common.py +273 -0
- algo_cli/intelligence/construction/documents.py +496 -0
- algo_cli/intelligence/construction/labor_units.py +1395 -0
- algo_cli/intelligence/construction/payments.py +470 -0
- algo_cli/intelligence/construction/risk.py +784 -0
- algo_cli/intelligence/content_extractor.py +132 -0
- algo_cli/intelligence/context_adaptive.py +102 -0
- algo_cli/intelligence/context_ops.py +95 -0
- algo_cli/intelligence/count_min.py +145 -0
- algo_cli/intelligence/cow_state.py +103 -0
- algo_cli/intelligence/critic_loop.py +119 -0
- algo_cli/intelligence/cross_source.py +113 -0
- algo_cli/intelligence/daemon_mode.py +99 -0
- algo_cli/intelligence/dag_orchestration.py +151 -0
- algo_cli/intelligence/deep_research.py +155 -0
- algo_cli/intelligence/degenerate_detector.py +78 -0
- algo_cli/intelligence/delta_report.py +92 -0
- algo_cli/intelligence/discovery_event_log.py +92 -0
- algo_cli/intelligence/document_ingest.py +298 -0
- algo_cli/intelligence/dual_layer_validate.py +151 -0
- algo_cli/intelligence/echo_fidelity.py +73 -0
- algo_cli/intelligence/ema_tuning.py +104 -0
- algo_cli/intelligence/event_log.py +92 -0
- algo_cli/intelligence/evidence_graph.py +114 -0
- algo_cli/intelligence/extension_host.py +162 -0
- algo_cli/intelligence/extension_manifest.py +115 -0
- algo_cli/intelligence/falsification_suite.py +178 -0
- algo_cli/intelligence/finance/__init__.py +169 -0
- algo_cli/intelligence/finance/anomalies.py +135 -0
- algo_cli/intelligence/finance/ap_ar.py +351 -0
- algo_cli/intelligence/finance/cash.py +162 -0
- algo_cli/intelligence/finance/close.py +332 -0
- algo_cli/intelligence/finance/common.py +244 -0
- algo_cli/intelligence/finance/construction.py +135 -0
- algo_cli/intelligence/finance/controls.py +172 -0
- algo_cli/intelligence/finance/evidence.py +119 -0
- algo_cli/intelligence/finance/exceptions.py +157 -0
- algo_cli/intelligence/finance/reconciliations.py +254 -0
- algo_cli/intelligence/finance/revenue.py +109 -0
- algo_cli/intelligence/finance/tax.py +74 -0
- algo_cli/intelligence/finance/workpapers.py +111 -0
- algo_cli/intelligence/finding_record.py +120 -0
- algo_cli/intelligence/flow_dag.py +267 -0
- algo_cli/intelligence/gatherer.py +223 -0
- algo_cli/intelligence/golden_master.py +98 -0
- algo_cli/intelligence/graph_rag.py +195 -0
- algo_cli/intelligence/group_chat.py +143 -0
- algo_cli/intelligence/hash_dedup.py +145 -0
- algo_cli/intelligence/hyperloglog.py +128 -0
- algo_cli/intelligence/incremental_index.py +316 -0
- algo_cli/intelligence/index_store.py +16 -0
- algo_cli/intelligence/iteration_plan.py +133 -0
- algo_cli/intelligence/kernel_plugins.py +167 -0
- algo_cli/intelligence/lesson_catalog.py +135 -0
- algo_cli/intelligence/llm_fallback.py +169 -0
- algo_cli/intelligence/log2_histogram.py +267 -0
- algo_cli/intelligence/lsp_integration.py +147 -0
- algo_cli/intelligence/memory_evolution.py +117 -0
- algo_cli/intelligence/minhash_lsh.py +182 -0
- algo_cli/intelligence/multi_model_score.py +174 -0
- algo_cli/intelligence/multi_tier_grade.py +211 -0
- algo_cli/intelligence/negative_controls.py +113 -0
- algo_cli/intelligence/numeric_clamp.py +63 -0
- algo_cli/intelligence/occ_editor.py +66 -0
- algo_cli/intelligence/output_normalize.py +112 -0
- algo_cli/intelligence/parallel_delegation.py +98 -0
- algo_cli/intelligence/parallel_fanout.py +104 -0
- algo_cli/intelligence/permission_modes.py +105 -0
- algo_cli/intelligence/pre_push_gate.py +68 -0
- algo_cli/intelligence/prefetch.py +171 -0
- algo_cli/intelligence/process_framework.py +217 -0
- algo_cli/intelligence/project_graph.py +387 -0
- algo_cli/intelligence/query_expansion.py +146 -0
- algo_cli/intelligence/ralph_loop.py +117 -0
- algo_cli/intelligence/rate_limiter.py +153 -0
- algo_cli/intelligence/refactor_transaction.py +94 -0
- algo_cli/intelligence/research_workspace.py +108 -0
- algo_cli/intelligence/retraction_ledger.py +72 -0
- algo_cli/intelligence/saga_pattern.py +88 -0
- algo_cli/intelligence/session_fork.py +100 -0
- algo_cli/intelligence/shadow_editor.py +67 -0
- algo_cli/intelligence/shell_session.py +213 -0
- algo_cli/intelligence/source_registry.py +143 -0
- algo_cli/intelligence/spawn_scales.py +99 -0
- algo_cli/intelligence/stat_stability.py +104 -0
- algo_cli/intelligence/structural_validator.py +148 -0
- algo_cli/intelligence/subagent_spawner.py +111 -0
- algo_cli/intelligence/symmetric_verify.py +70 -0
- algo_cli/intelligence/task_classifier.py +129 -0
- algo_cli/intelligence/team_execution.py +122 -0
- algo_cli/intelligence/tiered_access.py +121 -0
- algo_cli/intelligence/utility_registry.py +159 -0
- algo_cli/intuition_engine.py +560 -0
- algo_cli/intuition_injector.py +82 -0
- algo_cli/kernels/__init__.py +5 -0
- algo_cli/kernels/manifest.py +763 -0
- algo_cli/main.py +3903 -0
- algo_cli/memory_candidates.py +541 -0
- algo_cli/memory_echo_veil.py +394 -0
- algo_cli/memory_runtime.py +112 -0
- algo_cli/model_info.py +548 -0
- algo_cli/model_profile.py +160 -0
- algo_cli/model_routing.py +74 -0
- algo_cli/oneshot.py +331 -0
- algo_cli/perf_telemetry.py +389 -0
- algo_cli/plugins.py +245 -0
- algo_cli/private_event_store.py +654 -0
- algo_cli/quantization/__init__.py +24 -0
- algo_cli/quantization/lloyd_max.py +98 -0
- algo_cli/quantization/turbo_quant.py +308 -0
- algo_cli/reasoning/__init__.py +46 -0
- algo_cli/reasoning/combinatorial.py +356 -0
- algo_cli/reasoning/graph_of_thought.py +297 -0
- algo_cli/reasoning/mcts.py +220 -0
- algo_cli/reasoning/neuro_symbolic.py +250 -0
- algo_cli/reasoning/react.py +246 -0
- algo_cli/reasoning/reflexion.py +225 -0
- algo_cli/reasoning/tree_of_thought.py +241 -0
- algo_cli/reasoning_bridge.py +150 -0
- algo_cli/reconciliation.py +284 -0
- algo_cli/reflex.py +385 -0
- algo_cli/resources/docs/ALGO.md +13958 -0
- algo_cli/resources/docs/algo-cli-algorithm-evidence-contract.md +60 -0
- algo_cli/resources/docs/algo-cli-execution-verification-contract.md +59 -0
- algo_cli/resources/docs/algo-cli-memory-lifecycle-contract.md +72 -0
- algo_cli/resources/docs/harness-extension-cleanup-recommendation.md +41 -0
- algo_cli/resources/docs/index-compute-lab-integration.md +32 -0
- algo_cli/resources/docs/inference-harness-loop-blueprint-2026-06.md +55 -0
- algo_cli/resources/docs/main-split-map.md +35 -0
- algo_cli/resources/docs/privacy-and-context.md +48 -0
- algo_cli/resources/docs/reflex-loop-v0.2.md +354 -0
- algo_cli/resources/skills/README.md +26 -0
- algo_cli/resources/skills/algo-cli.md +59 -0
- algo_cli/resources/skills/edit-file-precision.md +49 -0
- algo_cli/resources/skills/harness-search-first.md +47 -0
- algo_cli/resources/skills/memory-recall-ritual.md +51 -0
- algo_cli/resources/skills/qol-algorithms.md +224 -0
- algo_cli/resources/skills/smart-error-recovery.md +56 -0
- algo_cli/resources/skills/tool-selection-cheatsheet.md +65 -0
- algo_cli/retrieval_algorithms.py +127 -0
- algo_cli/runtime_qos.py +236 -0
- algo_cli/runtime_services.py +320 -0
- algo_cli/session_commands.py +95 -0
- algo_cli/session_mode.py +113 -0
- algo_cli/skills.py +430 -0
- algo_cli/slash_dispatch.py +1265 -0
- algo_cli/small_context.py +206 -0
- algo_cli/spawn_budget.py +89 -0
- algo_cli/task_ledger.py +84 -0
- algo_cli/task_router.py +197 -0
- algo_cli/tool_context.py +94 -0
- algo_cli/tool_contract.py +99 -0
- algo_cli/tool_policy.py +357 -0
- algo_cli/tool_runtime.py +647 -0
- algo_cli/tools.py +3056 -0
- algo_cli/url_scheme.py +174 -0
- algo_cli/verify.py +154 -0
- algo_cli/version_manifest.py +178 -0
- algo_cli/vision_screenshot_verify.py +76 -0
- algo_cli/workspace_resolver.py +68 -0
- algo_cli/x_account.py +209 -0
- algo_cli/xai_auth.py +374 -0
- algo_cli/xai_client.py +600 -0
- algo_cli_runtime-0.14.0.dist-info/METADATA +369 -0
- algo_cli_runtime-0.14.0.dist-info/RECORD +237 -0
- algo_cli_runtime-0.14.0.dist-info/WHEEL +4 -0
- algo_cli_runtime-0.14.0.dist-info/entry_points.txt +3 -0
- algo_cli_runtime-0.14.0.dist-info/licenses/LICENSE +21 -0
- 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
|