SourceIndex 0.1.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.
- sourceindex/__init__.py +30 -0
- sourceindex/build/__init__.py +592 -0
- sourceindex/build/indexer.py +403 -0
- sourceindex/build/linerange/__init__.py +24 -0
- sourceindex/build/linerange/python_ast.py +155 -0
- sourceindex/build/linerange/treesitter.py +397 -0
- sourceindex/build/prompts.py +76 -0
- sourceindex/build/state.py +261 -0
- sourceindex/build/walker.py +94 -0
- sourceindex/claudecode/__init__.py +0 -0
- sourceindex/claudecode/savings.py +325 -0
- sourceindex/claudecode/savings_summary.py +88 -0
- sourceindex/claudecode/statusline.py +116 -0
- sourceindex/cli/__init__.py +304 -0
- sourceindex/cli/__main__.py +10 -0
- sourceindex/cli/api_key.py +171 -0
- sourceindex/cli/commands.py +678 -0
- sourceindex/cli/install.py +378 -0
- sourceindex/daemon/__init__.py +83 -0
- sourceindex/daemon/client.py +141 -0
- sourceindex/daemon/crypto.py +48 -0
- sourceindex/daemon/keyring_store.py +129 -0
- sourceindex/daemon/lifecycle.py +237 -0
- sourceindex/daemon/protocol.py +134 -0
- sourceindex/daemon/server.py +389 -0
- sourceindex/daemon/store.py +426 -0
- sourceindex/lib/__init__.py +0 -0
- sourceindex/lib/backend.py +240 -0
- sourceindex/lib/cost.py +96 -0
- sourceindex/lib/env.py +30 -0
- sourceindex/lib/git.py +33 -0
- sourceindex/lib/languages.py +406 -0
- sourceindex/lib/llm.py +298 -0
- sourceindex/lib/log.py +228 -0
- sourceindex/lib/registry.py +75 -0
- sourceindex/lib/timing.py +10 -0
- sourceindex/search/__init__.py +251 -0
- sourceindex/search/experiments.py +276 -0
- sourceindex/search/imports.py +155 -0
- sourceindex/search/passes.py +51 -0
- sourceindex/search/prompts.py +379 -0
- sourceindex/search/roadmap.py +265 -0
- sourceindex/server.py +127 -0
- sourceindex-0.1.0.dist-info/METADATA +73 -0
- sourceindex-0.1.0.dist-info/RECORD +47 -0
- sourceindex-0.1.0.dist-info/WHEEL +4 -0
- sourceindex-0.1.0.dist-info/entry_points.txt +2 -0
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"""Two-pass LLM file/function selection.
|
|
2
|
+
|
|
3
|
+
Pass 1a: index_search_model reads tier-1 index + query → picks relevant files.
|
|
4
|
+
Pass 1b: index_search_model reads tier-2 function indexes → picks specific
|
|
5
|
+
functions, tagging each EDIT or READ.
|
|
6
|
+
|
|
7
|
+
The two helpers here also clean up LLM prose around the selection lines so
|
|
8
|
+
roadmap synthesis sees clean data.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from ..lib.languages import all_extensions, default_languages
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
# All registered source-file extensions, used to detect path-like lines in
|
|
15
|
+
# LLM output (Pass 1a/1b post-processing) regardless of language. Cached at
|
|
16
|
+
# import time so the every-file roadmap loop doesn't rebuild it.
|
|
17
|
+
_KNOWN_EXTENSIONS: tuple[str, ...] = all_extensions(list(default_languages()))
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _looks_like_path(line: str) -> bool:
|
|
21
|
+
"""Heuristic: line looks like a file path (any registered language) or a
|
|
22
|
+
file::function entry. Used to filter LLM prose out of Pass 1a/1b output."""
|
|
23
|
+
s = line.strip()
|
|
24
|
+
if not s:
|
|
25
|
+
return False
|
|
26
|
+
if "::" in s or "/" in s:
|
|
27
|
+
return True
|
|
28
|
+
return s.lower().endswith(_KNOWN_EXTENSIONS)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _strip_llm_wrapper(text: str) -> str:
|
|
32
|
+
"""Strip markdown fences and prose from LLM output, keeping only data lines.
|
|
33
|
+
|
|
34
|
+
Smarter models (Sonnet/Opus) sometimes wrap output in ```...``` blocks
|
|
35
|
+
and add explanatory prose despite being told not to. This extracts only
|
|
36
|
+
the lines that look like file paths or file::function entries across all
|
|
37
|
+
registered languages.
|
|
38
|
+
"""
|
|
39
|
+
lines = text.strip().split("\n")
|
|
40
|
+
in_fence = False
|
|
41
|
+
fenced: list[str] = []
|
|
42
|
+
for line in lines:
|
|
43
|
+
stripped = line.strip()
|
|
44
|
+
if stripped.startswith("```"):
|
|
45
|
+
in_fence = not in_fence
|
|
46
|
+
continue
|
|
47
|
+
if in_fence:
|
|
48
|
+
fenced.append(stripped)
|
|
49
|
+
if fenced:
|
|
50
|
+
return "\n".join(fenced)
|
|
51
|
+
return "\n".join(ln.strip() for ln in lines if _looks_like_path(ln))
|
|
@@ -0,0 +1,379 @@
|
|
|
1
|
+
"""LLM prompts + roadmap template used by the search pipeline.
|
|
2
|
+
|
|
3
|
+
PASS_1A_PROMPT — file-level selection from the tier-1 index.
|
|
4
|
+
PASS_1B_PROMPT — function-level selection from tier-2 details, with
|
|
5
|
+
EDIT/READ tagging.
|
|
6
|
+
ROADMAP_TEMPLATE — wraps Pass 1b output into the authoritative pre-analysis
|
|
7
|
+
delivered to the agent.
|
|
8
|
+
|
|
9
|
+
QA variants (suffix _QA) are used when mode="qa": no EDIT tags, relaxed
|
|
10
|
+
constraints (runtime exploration encouraged, no recon-bash ban).
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
PASS_1A_PROMPT = """You are analyzing a source codebase to help another AI implement a feature.
|
|
15
|
+
|
|
16
|
+
=== CODEBASE INDEX ===
|
|
17
|
+
Each line: file_path — one-line description.
|
|
18
|
+
|
|
19
|
+
{index_content}
|
|
20
|
+
|
|
21
|
+
=== TASK ===
|
|
22
|
+
{instruction}
|
|
23
|
+
|
|
24
|
+
=== YOUR JOB ===
|
|
25
|
+
Based on the task and codebase index, list the file paths (one per line) that are most likely to need reading or modification to implement this feature.
|
|
26
|
+
|
|
27
|
+
Output ONLY file paths, one per line. No explanation, no markdown, no numbering.
|
|
28
|
+
Include files that:
|
|
29
|
+
- Would need to be modified to implement the feature
|
|
30
|
+
- Contain related classes/functions the implementation depends on
|
|
31
|
+
- Contain test patterns that reveal expected behavior
|
|
32
|
+
List every file that is genuinely relevant — do not cap the count artificially."""
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
PASS_1B_PROMPT = """You are analyzing a source codebase to help another AI implement a feature.
|
|
36
|
+
|
|
37
|
+
=== FUNCTION-LEVEL INDEXES OF SELECTED FILES ===
|
|
38
|
+
Each file lists its classes/functions with name, line range, and purpose.
|
|
39
|
+
|
|
40
|
+
{selected_details}
|
|
41
|
+
|
|
42
|
+
=== TASK ===
|
|
43
|
+
{instruction}
|
|
44
|
+
|
|
45
|
+
=== YOUR JOB ===
|
|
46
|
+
Produce two sections, in this order, separated by the literal marker lines
|
|
47
|
+
shown below. Output nothing outside these two sections.
|
|
48
|
+
|
|
49
|
+
------------------------------------------------------------------------
|
|
50
|
+
SECTION A — CODE LANDSCAPE (markdown, target 800–1500 tokens)
|
|
51
|
+
------------------------------------------------------------------------
|
|
52
|
+
Write a structured handoff describing the code landscape around the change
|
|
53
|
+
site, in the style of a thorough codebase-exploration report. Use markdown
|
|
54
|
+
sub-headers (### …). Cover:
|
|
55
|
+
|
|
56
|
+
• Where the change site sits in the architecture (which module, what
|
|
57
|
+
it's next to, what calls it).
|
|
58
|
+
• Conventions / patterns the surrounding code follows — naming, base
|
|
59
|
+
classes, how siblings are structured.
|
|
60
|
+
• Sibling files relevant as templates or examples — list their paths
|
|
61
|
+
with their one-line role, even if you do NOT recommend reading them.
|
|
62
|
+
The downstream agent should know these exist as patterns.
|
|
63
|
+
• Non-obvious gotchas the tier-2 indexes reveal — special cases,
|
|
64
|
+
version branches, edge cases tested.
|
|
65
|
+
• Cite specific paths and line ranges where load-bearing.
|
|
66
|
+
|
|
67
|
+
Do NOT inline code snippets — function bodies belong in the agent's later
|
|
68
|
+
Read calls, not in this landscape. Path + range citations only.
|
|
69
|
+
|
|
70
|
+
Do NOT recap the SECTION B selections in prose — landscape only.
|
|
71
|
+
|
|
72
|
+
------------------------------------------------------------------------
|
|
73
|
+
SECTION B — SELECTIONS (structured, one per line)
|
|
74
|
+
------------------------------------------------------------------------
|
|
75
|
+
List the specific functions and classes that are most relevant to
|
|
76
|
+
implementing this task. For each, output one line in this exact format:
|
|
77
|
+
<TAG> file_path::QualifiedName (Lstart-Lend)
|
|
78
|
+
|
|
79
|
+
Where <TAG> is:
|
|
80
|
+
- EDIT: this function/class will be modified (new code added, existing
|
|
81
|
+
code changed) to implement the task.
|
|
82
|
+
- READ: this function/class is reference material — called, extended,
|
|
83
|
+
followed as a pattern, or verified via tests.
|
|
84
|
+
|
|
85
|
+
STRICT TAGGING RULE: A file is EDIT only if new lines will be added or
|
|
86
|
+
existing lines changed there. Everything else — tests, sibling plugins,
|
|
87
|
+
base classes, utilities, reference patterns, callers, examples — is READ.
|
|
88
|
+
|
|
89
|
+
VETO RULE: A file in the input list may turn out to have no function
|
|
90
|
+
genuinely relevant to this task — Pass 1a casts a wide net by design.
|
|
91
|
+
If that is the case for a file, do NOT emit any SELECTIONS line for it.
|
|
92
|
+
Files with zero emitted entries are dropped from the structured roadmap.
|
|
93
|
+
You can still mention the file in SECTION A as a pattern/sibling if it
|
|
94
|
+
helps the agent — that's exactly the landscape signal.
|
|
95
|
+
|
|
96
|
+
The veto's purpose is to remove files that are clearly siblings/extras
|
|
97
|
+
Pass 1a picked up by proximity. It is NOT to second-guess Pass 1a's
|
|
98
|
+
central picks when their relevance is ambiguous.
|
|
99
|
+
|
|
100
|
+
FLOOR: The SELECTIONS list MUST contain at least 3 files. If relevance is
|
|
101
|
+
ambiguous for some of Pass 1a's picks, default to keeping rather than
|
|
102
|
+
dropping — the LANDSCAPE block can de-emphasize a file without removing
|
|
103
|
+
it from the structured roadmap. An empty or near-empty SELECTIONS list
|
|
104
|
+
is a Pass-1b failure mode, not a feature. Only emit fewer than 3 files
|
|
105
|
+
when the task genuinely touches a single file (e.g. a one-line config
|
|
106
|
+
fix); in that case still include the file's tests as READ entries.
|
|
107
|
+
|
|
108
|
+
Example output (format only — content depends on task):
|
|
109
|
+
|
|
110
|
+
=== CODE LANDSCAPE ===
|
|
111
|
+
### Change site
|
|
112
|
+
The feature touches `beets/autotag/match.py`, specifically `tag_album`
|
|
113
|
+
(L200-L250) which orchestrates album-level matching after track candidates
|
|
114
|
+
are scored.
|
|
115
|
+
|
|
116
|
+
### Surrounding patterns
|
|
117
|
+
Matching helpers follow a consistent shape: each scoring function returns
|
|
118
|
+
a `Distance` object built from comparator deltas. See `match.py::distance`
|
|
119
|
+
(L80-L110) for the canonical pattern.
|
|
120
|
+
|
|
121
|
+
### Sibling files (pattern reference, not necessarily to be read)
|
|
122
|
+
- `beets/autotag/mb.py` — MusicBrainz lookup hooks, mirrors `match.py`'s
|
|
123
|
+
callback shape.
|
|
124
|
+
- `beets/util.py` — utility helpers; offered by Pass 1a but no function
|
|
125
|
+
here is task-relevant.
|
|
126
|
+
|
|
127
|
+
### Gotchas
|
|
128
|
+
The candidate cache (`match.py` L150) silently dedupes by track-id; new
|
|
129
|
+
scoring logic must handle the deduped list, not the raw candidates.
|
|
130
|
+
|
|
131
|
+
=== SELECTIONS ===
|
|
132
|
+
EDIT beets/autotag/match.py::tag_album (L200-L250)
|
|
133
|
+
READ beets/plugins.py::BeetsPlugin.commands (L45-L60)
|
|
134
|
+
READ test/test_match.py::test_tag_album (L100-L140)
|
|
135
|
+
|
|
136
|
+
Output the literal markers `=== CODE LANDSCAPE ===` and `=== SELECTIONS ===`
|
|
137
|
+
exactly. No markdown fences around the whole output. No prose outside the
|
|
138
|
+
two sections."""
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
# ── QA variants ──────────────────────────────────────────────────────────
|
|
142
|
+
|
|
143
|
+
PASS_1A_QA_PROMPT = """You are analyzing a source codebase to help another AI answer a question.
|
|
144
|
+
|
|
145
|
+
=== CODEBASE INDEX ===
|
|
146
|
+
Each line: file_path — one-line description.
|
|
147
|
+
|
|
148
|
+
{index_content}
|
|
149
|
+
|
|
150
|
+
=== QUESTION ===
|
|
151
|
+
{instruction}
|
|
152
|
+
|
|
153
|
+
=== YOUR JOB ===
|
|
154
|
+
Based on the question and codebase index, list the file paths (one per line)
|
|
155
|
+
that are most relevant to answering this question.
|
|
156
|
+
|
|
157
|
+
Output ONLY file paths, one per line. No explanation, no markdown, no numbering.
|
|
158
|
+
Copy each path EXACTLY as it appears in the index above — same relative form,
|
|
159
|
+
no added prefix (such as /app/) and no leading slash.
|
|
160
|
+
Include files that:
|
|
161
|
+
- Contain code directly referenced or asked about in the question
|
|
162
|
+
- Implement the subsystem, feature, or behavior the question concerns
|
|
163
|
+
- Contain configuration, initialization, or entry points relevant to the topic
|
|
164
|
+
- Would help the answerer understand the architecture around the question
|
|
165
|
+
List every file that is genuinely relevant — do not cap the count artificially."""
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
PASS_1B_QA_PROMPT = """You are analyzing a source codebase to help another AI answer a question.
|
|
169
|
+
|
|
170
|
+
=== FUNCTION-LEVEL INDEXES OF SELECTED FILES ===
|
|
171
|
+
Each file lists its classes/functions with name, line range, and purpose.
|
|
172
|
+
|
|
173
|
+
{selected_details}
|
|
174
|
+
|
|
175
|
+
=== QUESTION ===
|
|
176
|
+
{instruction}
|
|
177
|
+
|
|
178
|
+
=== YOUR JOB ===
|
|
179
|
+
Produce two sections, in this order, separated by the literal marker lines
|
|
180
|
+
shown below. Output nothing outside these two sections.
|
|
181
|
+
|
|
182
|
+
------------------------------------------------------------------------
|
|
183
|
+
SECTION A — CODE LANDSCAPE (markdown, target 300–700 tokens)
|
|
184
|
+
------------------------------------------------------------------------
|
|
185
|
+
Write a structured handoff describing the code landscape relevant to the
|
|
186
|
+
question. Keep it narrow: include only context that helps the downstream
|
|
187
|
+
agent decide where to read or what to verify. Use markdown sub-headers
|
|
188
|
+
(### …). Cover:
|
|
189
|
+
|
|
190
|
+
• How the subsystem(s) the question asks about fit in the architecture
|
|
191
|
+
(which module, what depends on them, what calls them).
|
|
192
|
+
• Key abstractions, patterns, and conventions in the relevant code —
|
|
193
|
+
class hierarchies, naming, configuration loading, etc.
|
|
194
|
+
• Sibling files relevant as context — list their paths with their
|
|
195
|
+
one-line role, even if you do NOT recommend reading them.
|
|
196
|
+
• Non-obvious gotchas the tier-2 indexes reveal — edge cases, version
|
|
197
|
+
branches, runtime vs compile-time behavior, dynamic dispatch.
|
|
198
|
+
• Cite specific paths and line ranges where load-bearing.
|
|
199
|
+
|
|
200
|
+
Do NOT inline code snippets or answer the question — function bodies and
|
|
201
|
+
runtime facts belong in the agent's later Reads/Bash output, not in this
|
|
202
|
+
landscape. Path + range citations only.
|
|
203
|
+
|
|
204
|
+
Do NOT recap the SECTION B selections in prose — landscape only.
|
|
205
|
+
|
|
206
|
+
------------------------------------------------------------------------
|
|
207
|
+
SECTION B — SELECTIONS (structured, one per line)
|
|
208
|
+
------------------------------------------------------------------------
|
|
209
|
+
List the specific functions and classes that are most relevant to
|
|
210
|
+
answering this question. For each, output one line in this exact format:
|
|
211
|
+
READ file_path::QualifiedName (Lstart-Lend)
|
|
212
|
+
|
|
213
|
+
Every entry is READ — this is an investigation, not a modification task.
|
|
214
|
+
The agent will read these to understand the code, not edit it.
|
|
215
|
+
|
|
216
|
+
VETO RULE: A file in the input list may turn out to have no function
|
|
217
|
+
genuinely relevant to this question — Pass 1a casts a wide net by design.
|
|
218
|
+
If that is the case for a file, do NOT emit any SELECTIONS line for it.
|
|
219
|
+
Files with zero emitted entries are dropped from the structured roadmap.
|
|
220
|
+
You can still mention the file in SECTION A as context/sibling if it
|
|
221
|
+
helps the agent — that's exactly the landscape signal.
|
|
222
|
+
|
|
223
|
+
FLOOR: The SELECTIONS list MUST contain at least 3 files. If relevance is
|
|
224
|
+
ambiguous for some of Pass 1a's picks, default to keeping rather than
|
|
225
|
+
dropping. An empty or near-empty SELECTIONS list is a Pass-1b failure mode.
|
|
226
|
+
|
|
227
|
+
Example output (format only — content depends on question):
|
|
228
|
+
|
|
229
|
+
=== CODE LANDSCAPE ===
|
|
230
|
+
### Relevant subsystem
|
|
231
|
+
The question concerns `src/auth/middleware.py`, the WSGI middleware that
|
|
232
|
+
intercepts every request and resolves the session token (L40-L85).
|
|
233
|
+
|
|
234
|
+
### How auth is wired
|
|
235
|
+
The middleware is registered in `src/app.py::create_app` (L20-L35). It
|
|
236
|
+
delegates to `src/auth/backends.py` for token-to-user resolution.
|
|
237
|
+
|
|
238
|
+
### Sibling files (context, not necessarily to be read)
|
|
239
|
+
- `src/auth/backends.py` — token resolution strategies (DB, JWT, cookie).
|
|
240
|
+
- `src/auth/models.py` — the User and Session ORM models.
|
|
241
|
+
|
|
242
|
+
### Gotchas
|
|
243
|
+
Token refresh (L60-L72) silently extends expired sessions by 30 min;
|
|
244
|
+
the logging at L68 only fires in DEBUG mode — easy to miss in production.
|
|
245
|
+
|
|
246
|
+
=== SELECTIONS ===
|
|
247
|
+
READ src/auth/middleware.py::AuthMiddleware.__call__ (L40-L85)
|
|
248
|
+
READ src/auth/backends.py::resolve_token (L15-L45)
|
|
249
|
+
READ src/app.py::create_app (L20-L35)
|
|
250
|
+
|
|
251
|
+
Output the literal markers `=== CODE LANDSCAPE ===` and `=== SELECTIONS ===`
|
|
252
|
+
exactly. No markdown fences around the whole output. No prose outside the
|
|
253
|
+
two sections."""
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
ROADMAP_TEMPLATE = """=== CODEBASE ROADMAP (authoritative pre-analysis) ===
|
|
257
|
+
|
|
258
|
+
This roadmap is the output of a dedicated analysis pass over the full codebase
|
|
259
|
+
(file-level Pass 1a + function-level Pass 1b, both reading complete tier-1 and
|
|
260
|
+
tier-2 indexes). Line ranges are AST-extracted function boundaries, not
|
|
261
|
+
estimates. Treat it as authoritative — it IS the index. Do not rebuild it
|
|
262
|
+
with grep/find/ls; do not second-guess its ranges with whole-file reads.
|
|
263
|
+
|
|
264
|
+
{context_section}
|
|
265
|
+
|
|
266
|
+
OPENING TURN — read every roadmap file at once, in parallel:
|
|
267
|
+
|
|
268
|
+
Your first response should contain ONE Read tool call PER file listed
|
|
269
|
+
below, all in the same response (multiple tool_use blocks in one turn).
|
|
270
|
+
For a 4-file roadmap, that is 4 Read calls in turn 1, NOT 4 separate
|
|
271
|
+
turns. Each new turn rebuilds ~$0.05 of input cache and adds latency;
|
|
272
|
+
the roadmap was paid for once — read all of it at once. After that
|
|
273
|
+
single turn you have the entire context — proceed to Edit.
|
|
274
|
+
|
|
275
|
+
CONSTRAINTS (after the opening turn):
|
|
276
|
+
|
|
277
|
+
1. PARALLELIZE: Any time you need multiple independent Reads, Greps, or Bash
|
|
278
|
+
commands, issue them in a SINGLE response with multiple tool_use blocks —
|
|
279
|
+
not one tool call per turn. Serializing wastes turns and rebuilds cache.
|
|
280
|
+
|
|
281
|
+
2. USE THE LISTED RANGE: Each file below has an explicit (offset, limit).
|
|
282
|
+
Pass them to Read as-is. Do NOT call Read with no offset/limit on a
|
|
283
|
+
file that has a range listed — the range came from AST function
|
|
284
|
+
boundaries, not heuristics. Whole-file reads on roadmap files burn
|
|
285
|
+
tokens for no information gain.
|
|
286
|
+
|
|
287
|
+
3. READ ONCE: Read each listed (offset, limit) exactly once. Files marked
|
|
288
|
+
[EDIT TARGET] may carry a small imports-preamble hint alongside the
|
|
289
|
+
function range — read both so you can add new imports without duplicating.
|
|
290
|
+
Do not re-Read a span you have already Read — scroll back instead.
|
|
291
|
+
|
|
292
|
+
4. GAP-BEFORE-READ: Before reading or searching any file NOT listed below, you
|
|
293
|
+
MUST first state the specific gap in one sentence. Example:
|
|
294
|
+
"The roadmap covers the Discogs plugin but not the MusicBrainz sibling;
|
|
295
|
+
reading musicbrainz.py to find the extra_mb_field_by_tag pattern."
|
|
296
|
+
If you cannot name a concrete gap, the roadmap is sufficient — proceed to Edit.
|
|
297
|
+
|
|
298
|
+
5. NO RECON BASH: Do not run `git log`, `ls`, `find`, `pip list`, `which`,
|
|
299
|
+
`env`, or other reconnaissance commands. The roadmap + its imports IS the
|
|
300
|
+
recon. If you think you need to look at the filesystem or environment,
|
|
301
|
+
you don't — re-read the roadmap entry instead.
|
|
302
|
+
|
|
303
|
+
6. TEST RECOVERY: If a test fails with ImportError / ModuleNotFoundError /
|
|
304
|
+
command-not-found, the recovery is a single chained bash:
|
|
305
|
+
`pip install <pkg> -q && <retry command>`. Do not `find` the package on
|
|
306
|
+
disk, `ls` the venv, or inspect `env` first — the package name is in the
|
|
307
|
+
error and pip will resolve the path.
|
|
308
|
+
|
|
309
|
+
Entries are grouped by file.
|
|
310
|
+
|
|
311
|
+
{grouped}
|
|
312
|
+
=== IMPORT STATEMENTS (extracted from files above) ===
|
|
313
|
+
|
|
314
|
+
{import_section}
|
|
315
|
+
=== END CODEBASE ROADMAP ===
|
|
316
|
+
"""
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
ROADMAP_QA_TEMPLATE = """=== CODEBASE ROADMAP (navigation pre-context) ===
|
|
320
|
+
|
|
321
|
+
This roadmap is the output of a dedicated analysis pass over the full codebase
|
|
322
|
+
(file-level Pass 1a + function-level Pass 1b, both reading complete tier-1 and
|
|
323
|
+
tier-2 indexes). Line ranges are AST-extracted function boundaries, not
|
|
324
|
+
estimates. Treat it as a navigation index, not evidence or a substitute answer.
|
|
325
|
+
|
|
326
|
+
=== QUESTION TO ANSWER ===
|
|
327
|
+
|
|
328
|
+
{question}
|
|
329
|
+
|
|
330
|
+
=== ANSWER CONTRACT ===
|
|
331
|
+
|
|
332
|
+
- Answer every part of the question.
|
|
333
|
+
- If the question asks to run, verify, observe, measure, trace, or show output,
|
|
334
|
+
execute the relevant command or workflow and paste the literal runtime evidence.
|
|
335
|
+
- Use this roadmap to find where to investigate; do not satisfy evidence
|
|
336
|
+
requirements from the roadmap or static source summaries alone.
|
|
337
|
+
|
|
338
|
+
{context_section}
|
|
339
|
+
|
|
340
|
+
OPENING TURN — read every roadmap file at once, in parallel:
|
|
341
|
+
|
|
342
|
+
Your first response should contain ONE Read tool call PER file listed
|
|
343
|
+
below, all in the same response (multiple tool_use blocks in one turn).
|
|
344
|
+
For a 4-file roadmap, that is 4 Read calls in turn 1, NOT 4 separate
|
|
345
|
+
turns. Each new turn rebuilds ~$0.05 of input cache and adds latency;
|
|
346
|
+
the roadmap was paid for once — read all of it at once. After that
|
|
347
|
+
single turn you have the full structural context.
|
|
348
|
+
|
|
349
|
+
CONSTRAINTS (after the opening turn):
|
|
350
|
+
|
|
351
|
+
1. PARALLELIZE: Any time you need multiple independent Reads, Greps, or Bash
|
|
352
|
+
commands, issue them in a SINGLE response with multiple tool_use blocks —
|
|
353
|
+
not one tool call per turn. Serializing wastes turns and rebuilds cache.
|
|
354
|
+
|
|
355
|
+
2. USE THE LISTED RANGE: Each file below has an explicit (offset, limit).
|
|
356
|
+
Pass them to Read as-is. Do NOT call Read with no offset/limit on a
|
|
357
|
+
file that has a range listed — the range came from AST function
|
|
358
|
+
boundaries, not heuristics. Whole-file reads on roadmap files burn
|
|
359
|
+
tokens for no information gain.
|
|
360
|
+
|
|
361
|
+
3. READ ONCE: Read each listed (offset, limit) exactly once.
|
|
362
|
+
Do not re-Read a span you have already Read — scroll back instead.
|
|
363
|
+
|
|
364
|
+
4. EXPLORE BEYOND THE ROADMAP WHEN NEEDED: The roadmap covers the structural
|
|
365
|
+
layout — the files and functions most relevant to the question. If you need
|
|
366
|
+
to trace a call chain further, check runtime behavior, inspect configuration,
|
|
367
|
+
or follow an import the roadmap didn't cover, go ahead. State the gap briefly
|
|
368
|
+
("the roadmap covers X but I need to check Y for Z") and proceed.
|
|
369
|
+
|
|
370
|
+
5. RUNTIME EXPLORATION IS EXPECTED: Many questions require observing actual
|
|
371
|
+
behavior — running the application, executing test scripts, inspecting output.
|
|
372
|
+
Use Bash freely for runtime investigation. The roadmap tells you WHERE to
|
|
373
|
+
look; execution tells you WHAT HAPPENS.
|
|
374
|
+
|
|
375
|
+
Entries are grouped by file.
|
|
376
|
+
|
|
377
|
+
{grouped}
|
|
378
|
+
=== END CODEBASE ROADMAP ===
|
|
379
|
+
"""
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
"""Roadmap synthesis: turn Pass-1b output into the authoritative pre-analysis.
|
|
2
|
+
|
|
3
|
+
Annotates each function entry with its tier-2 description, groups entries
|
|
4
|
+
per file, computes merged read windows from per-function ranges, marks
|
|
5
|
+
EDIT targets, prepends an imports-preamble Read window for Python EDIT
|
|
6
|
+
targets, and wraps everything in ROADMAP_TEMPLATE.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import re
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
from ..lib.log import get_logger, log_file_error
|
|
13
|
+
from .imports import _imports_preamble_end, extract_imports
|
|
14
|
+
from .prompts import ROADMAP_QA_TEMPLATE, ROADMAP_TEMPLATE
|
|
15
|
+
|
|
16
|
+
_log = get_logger(__name__)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
_RANGE_RE = re.compile(r"\(L(\d+)-L(\d+)\)")
|
|
20
|
+
_TAG_RE = re.compile(r"^(EDIT|READ)\s+")
|
|
21
|
+
|
|
22
|
+
# Test files are never an imports-preamble target — tests don't gain new imports
|
|
23
|
+
# from the main code change. Haiku's Pass 1b was observed to mis-tag test files
|
|
24
|
+
# as EDIT ~half the time on beets benchmarks; this regex is the belt to the prompt's
|
|
25
|
+
# suspenders.
|
|
26
|
+
_TEST_PATH_RE = re.compile(r"(^|/)tests?/|(^|/)(test_\w+|\w+_test)\.\w+$")
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _strip_desc_prefix(line: str) -> str:
|
|
30
|
+
"""Detail files store each entry as `<name/range>: <description>`."""
|
|
31
|
+
idx = line.find(": ")
|
|
32
|
+
return (line[idx + 2:] if idx >= 0 else line).strip()
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _lookup_description(
|
|
36
|
+
detail_lines: list[str],
|
|
37
|
+
range_part: str,
|
|
38
|
+
func_name: str,
|
|
39
|
+
) -> str:
|
|
40
|
+
"""Find the tier-2 description. Range match wins (disambiguates duplicate
|
|
41
|
+
method names like `__init__`); qualified-name match is the fallback."""
|
|
42
|
+
if range_part:
|
|
43
|
+
for line in detail_lines:
|
|
44
|
+
if range_part in line:
|
|
45
|
+
return _strip_desc_prefix(line)
|
|
46
|
+
|
|
47
|
+
if func_name:
|
|
48
|
+
name_re = re.compile(rf"(^|\.){re.escape(func_name)} \(L")
|
|
49
|
+
for line in detail_lines:
|
|
50
|
+
if name_re.search(line):
|
|
51
|
+
return _strip_desc_prefix(line)
|
|
52
|
+
|
|
53
|
+
return ""
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _load_detail_lines(
|
|
57
|
+
fpath: str,
|
|
58
|
+
store,
|
|
59
|
+
detail_texts: dict[str, str] | None,
|
|
60
|
+
cache: dict[str, list[str]],
|
|
61
|
+
) -> list[str]:
|
|
62
|
+
"""Memoize splitlines() across entries that share a source file.
|
|
63
|
+
|
|
64
|
+
Prefers the pre-loaded ``detail_texts`` dict (populated by Pass 1a so
|
|
65
|
+
we don't double-decrypt encrypted blobs) and falls back to a fresh
|
|
66
|
+
store read for files Pass 1b raised that weren't in 1a's selection.
|
|
67
|
+
"""
|
|
68
|
+
lines = cache.get(fpath)
|
|
69
|
+
if lines is None:
|
|
70
|
+
text: str | None = None
|
|
71
|
+
if detail_texts is not None and fpath in detail_texts:
|
|
72
|
+
text = detail_texts[fpath]
|
|
73
|
+
else:
|
|
74
|
+
rel = f"details/{fpath}.md"
|
|
75
|
+
try:
|
|
76
|
+
if store is not None and store.exists(rel):
|
|
77
|
+
text = store.read_text(rel)
|
|
78
|
+
except Exception as e:
|
|
79
|
+
log_file_error(_log, rel, e, phase="roadmap.detail")
|
|
80
|
+
text = None
|
|
81
|
+
lines = text.splitlines() if text else []
|
|
82
|
+
cache[fpath] = lines
|
|
83
|
+
return lines
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _looks_like_test(fpath: str) -> bool:
|
|
87
|
+
return bool(_TEST_PATH_RE.search(fpath))
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _compute_read_windows(
|
|
91
|
+
ranges: list[tuple[int, int]],
|
|
92
|
+
merge_gap: int = 10_000,
|
|
93
|
+
) -> list[tuple[int, int]]:
|
|
94
|
+
"""Collapse per-function ranges into read windows.
|
|
95
|
+
|
|
96
|
+
1. Drop wrappers — an entry whose range fully contains another entry's
|
|
97
|
+
range is redundant (Haiku often lists the whole class alongside its
|
|
98
|
+
methods; the class entry is descriptive, the methods tell us what
|
|
99
|
+
to actually read). If every range wraps something, keep them all.
|
|
100
|
+
2. Merge close-or-overlapping — adjacent windows within ``merge_gap``
|
|
101
|
+
lines collapse into one. Default is effectively "always merge":
|
|
102
|
+
experiments with narrow multi-window output (``merge_gap=30``) showed
|
|
103
|
+
opus didn't reliably parallelize the resulting Reads, adding turns
|
|
104
|
+
and cost. A single merged window per file tracks the pre-wrapper-drop
|
|
105
|
+
behaviour while still avoiding the "whole-class-entry inflates limit"
|
|
106
|
+
pathology.
|
|
107
|
+
"""
|
|
108
|
+
if not ranges:
|
|
109
|
+
return []
|
|
110
|
+
uniq = sorted(set(ranges))
|
|
111
|
+
non_wrappers = [
|
|
112
|
+
(s, e) for s, e in uniq
|
|
113
|
+
if not any(os >= s and oe <= e and (os, oe) != (s, e) for os, oe in uniq)
|
|
114
|
+
]
|
|
115
|
+
effective = non_wrappers or uniq
|
|
116
|
+
effective.sort()
|
|
117
|
+
merged: list[tuple[int, int]] = [effective[0]]
|
|
118
|
+
for s, e in effective[1:]:
|
|
119
|
+
last_s, last_e = merged[-1]
|
|
120
|
+
if s <= last_e + merge_gap:
|
|
121
|
+
merged[-1] = (last_s, max(last_e, e))
|
|
122
|
+
else:
|
|
123
|
+
merged.append((s, e))
|
|
124
|
+
return merged
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def _build_roadmap(
|
|
128
|
+
function_list: str,
|
|
129
|
+
repo_root: Path,
|
|
130
|
+
store=None,
|
|
131
|
+
detail_texts: dict[str, str] | None = None,
|
|
132
|
+
*,
|
|
133
|
+
details_dir: Path | None = None,
|
|
134
|
+
landscape: str | None = None,
|
|
135
|
+
mode: str = "implement",
|
|
136
|
+
question: str | None = None,
|
|
137
|
+
) -> str:
|
|
138
|
+
"""Annotate, group, and wrap pass-1b output into the roadmap preamble.
|
|
139
|
+
|
|
140
|
+
``store`` is the daemon-aware path. ``details_dir`` is the legacy path
|
|
141
|
+
for callers that haven't migrated yet — when set, we wrap it in a
|
|
142
|
+
``PlaintextStore`` so the same lookup logic applies."""
|
|
143
|
+
if store is None and details_dir is not None:
|
|
144
|
+
# Reconstruct a store from the legacy details_dir argument by
|
|
145
|
+
# walking back to the index_dir (the parent of details/).
|
|
146
|
+
from ..daemon import PlaintextStore
|
|
147
|
+
store = PlaintextStore(details_dir.parent)
|
|
148
|
+
order: list[str] = []
|
|
149
|
+
per_file: dict[str, list[str]] = {}
|
|
150
|
+
file_ranges: dict[str, list[tuple[int, int]]] = {}
|
|
151
|
+
edit_files: set[str] = set()
|
|
152
|
+
detail_cache: dict[str, list[str]] = {}
|
|
153
|
+
|
|
154
|
+
for raw in function_list.split("\n"):
|
|
155
|
+
line = raw.strip()
|
|
156
|
+
if not line or "::" not in line:
|
|
157
|
+
continue
|
|
158
|
+
|
|
159
|
+
# Strip the optional EDIT/READ tag. Absence => treat as READ for
|
|
160
|
+
# backward compatibility with older Pass 1b outputs.
|
|
161
|
+
tag_match = _TAG_RE.match(line)
|
|
162
|
+
tag = tag_match.group(1) if tag_match else "READ"
|
|
163
|
+
line = _TAG_RE.sub("", line, count=1)
|
|
164
|
+
|
|
165
|
+
fpath_raw, rest = line.split("::", 1)
|
|
166
|
+
fpath = fpath_raw.strip().lstrip("/")
|
|
167
|
+
|
|
168
|
+
range_match = _RANGE_RE.search(rest)
|
|
169
|
+
range_part = range_match.group(0) if range_match else ""
|
|
170
|
+
func_name = rest.split("(", 1)[0].strip()
|
|
171
|
+
detail_lines = _load_detail_lines(fpath, store, detail_texts, detail_cache)
|
|
172
|
+
desc = _lookup_description(detail_lines, range_part, func_name)
|
|
173
|
+
entry = f" {rest} — {desc}" if desc else f" {rest}"
|
|
174
|
+
|
|
175
|
+
if fpath not in per_file:
|
|
176
|
+
per_file[fpath] = []
|
|
177
|
+
order.append(fpath)
|
|
178
|
+
per_file[fpath].append(entry)
|
|
179
|
+
|
|
180
|
+
is_qa = mode == "qa"
|
|
181
|
+
if not is_qa and tag == "EDIT" and not _looks_like_test(fpath):
|
|
182
|
+
edit_files.add(fpath)
|
|
183
|
+
|
|
184
|
+
if range_match:
|
|
185
|
+
s, e = int(range_match.group(1)), int(range_match.group(2))
|
|
186
|
+
file_ranges.setdefault(fpath, []).append((s, e))
|
|
187
|
+
|
|
188
|
+
if mode != "qa" and not edit_files:
|
|
189
|
+
promoted = next(
|
|
190
|
+
(fp for fp in order if fp in file_ranges and not _looks_like_test(fp)),
|
|
191
|
+
None,
|
|
192
|
+
)
|
|
193
|
+
if promoted:
|
|
194
|
+
edit_files.add(promoted)
|
|
195
|
+
|
|
196
|
+
# Module tops (imports, constants, class/module setup) are frequent targets
|
|
197
|
+
# of follow-up reads. Extend the first window's start back to line 1 when
|
|
198
|
+
# its first line sits within HEADER_PREFIX_MAX lines of the top; beyond
|
|
199
|
+
# that, keep the narrow range (and add a separate imports-preamble hint
|
|
200
|
+
# for EDIT targets, since they'll add new imports at the file top).
|
|
201
|
+
# Path-emission helper: header / import-block lines are absolute so the
|
|
202
|
+
# agent has an unambiguous Read target regardless of its cwd. Roadmap
|
|
203
|
+
# consumers should never need to guess between EXTRACT_DIR / WORKSPACE_DIR.
|
|
204
|
+
# Function-detail lines (per_file entries with `file::QualifiedName` syntax)
|
|
205
|
+
# stay relative — they're descriptive, not Read targets.
|
|
206
|
+
abs_root = str(repo_root)
|
|
207
|
+
def _abs(fp: str) -> str:
|
|
208
|
+
return f"{abs_root}/{fp}"
|
|
209
|
+
|
|
210
|
+
is_qa = mode == "qa"
|
|
211
|
+
HEADER_PREFIX_MAX = 80
|
|
212
|
+
grouped_lines: list[str] = []
|
|
213
|
+
for fpath in order:
|
|
214
|
+
marker = "" if is_qa else (" [EDIT TARGET]" if fpath in edit_files else "")
|
|
215
|
+
ranges = file_ranges.get(fpath, [])
|
|
216
|
+
if not ranges:
|
|
217
|
+
grouped_lines.append(f"{_abs(fpath)}{marker} — Read ONCE (full file)")
|
|
218
|
+
grouped_lines.extend(per_file[fpath])
|
|
219
|
+
grouped_lines.append("")
|
|
220
|
+
continue
|
|
221
|
+
|
|
222
|
+
windows = _compute_read_windows(ranges)
|
|
223
|
+
first_s, first_e = windows[0]
|
|
224
|
+
if first_s <= HEADER_PREFIX_MAX:
|
|
225
|
+
windows[0] = (1, first_e)
|
|
226
|
+
elif not is_qa and fpath.endswith(".py") and fpath in edit_files:
|
|
227
|
+
preamble_end = _imports_preamble_end(repo_root / fpath)
|
|
228
|
+
if preamble_end > 0:
|
|
229
|
+
windows.insert(0, (1, preamble_end))
|
|
230
|
+
|
|
231
|
+
for s, e in windows:
|
|
232
|
+
grouped_lines.append(
|
|
233
|
+
f"{_abs(fpath)}{marker} — Read ONCE: offset={s} limit={e - s + 1}"
|
|
234
|
+
)
|
|
235
|
+
marker = ""
|
|
236
|
+
grouped_lines.extend(per_file[fpath])
|
|
237
|
+
grouped_lines.append("")
|
|
238
|
+
grouped = "\n".join(grouped_lines)
|
|
239
|
+
|
|
240
|
+
landscape_text = (landscape or "").strip()
|
|
241
|
+
context_section = (
|
|
242
|
+
f"=== CODE LANDSCAPE (from Pass 1b) ===\n\n{landscape_text}\n"
|
|
243
|
+
if landscape_text
|
|
244
|
+
else ""
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
if is_qa:
|
|
248
|
+
return ROADMAP_QA_TEMPLATE.format(
|
|
249
|
+
grouped=grouped,
|
|
250
|
+
context_section=context_section,
|
|
251
|
+
question=(question or "").strip(),
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
import_blocks: list[str] = []
|
|
255
|
+
for fpath in order:
|
|
256
|
+
imports = extract_imports(repo_root / fpath, fpath)
|
|
257
|
+
if imports:
|
|
258
|
+
import_blocks.append(f"--- {_abs(fpath)} ---\n{imports}\n")
|
|
259
|
+
import_section = "\n".join(import_blocks)
|
|
260
|
+
|
|
261
|
+
return ROADMAP_TEMPLATE.format(
|
|
262
|
+
grouped=grouped,
|
|
263
|
+
import_section=import_section,
|
|
264
|
+
context_section=context_section,
|
|
265
|
+
)
|