rag-your-code 1.0.0__tar.gz → 1.2.0__tar.gz

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 (54) hide show
  1. rag_your_code-1.2.0/PKG-INFO +673 -0
  2. rag_your_code-1.2.0/README.md +644 -0
  3. {rag_your_code-1.0.0 → rag_your_code-1.2.0}/pyproject.toml +9 -1
  4. rag_your_code-1.2.0/src/rag_your_code.egg-info/PKG-INFO +673 -0
  5. {rag_your_code-1.0.0 → rag_your_code-1.2.0}/src/rag_your_code.egg-info/SOURCES.txt +1 -0
  6. {rag_your_code-1.0.0 → rag_your_code-1.2.0}/src/rag_your_code.egg-info/requires.txt +3 -0
  7. {rag_your_code-1.0.0 → rag_your_code-1.2.0}/src/ragyourcode/__init__.py +1 -1
  8. {rag_your_code-1.0.0 → rag_your_code-1.2.0}/src/ragyourcode/agentic.py +8 -5
  9. {rag_your_code-1.0.0 → rag_your_code-1.2.0}/src/ragyourcode/cli.py +50 -8
  10. {rag_your_code-1.0.0 → rag_your_code-1.2.0}/src/ragyourcode/config.py +25 -2
  11. {rag_your_code-1.0.0 → rag_your_code-1.2.0}/src/ragyourcode/embeddings.py +84 -2
  12. {rag_your_code-1.0.0 → rag_your_code-1.2.0}/src/ragyourcode/graph.py +10 -2
  13. {rag_your_code-1.0.0 → rag_your_code-1.2.0}/src/ragyourcode/parser.py +36 -4
  14. {rag_your_code-1.0.0 → rag_your_code-1.2.0}/src/ragyourcode/search.py +141 -37
  15. {rag_your_code-1.0.0 → rag_your_code-1.2.0}/tests/test_absent_queries.py +20 -4
  16. {rag_your_code-1.0.0 → rag_your_code-1.2.0}/tests/test_e2e_cli.py +74 -0
  17. {rag_your_code-1.0.0 → rag_your_code-1.2.0}/tests/test_evidence.py +106 -15
  18. {rag_your_code-1.0.0 → rag_your_code-1.2.0}/tests/test_language_fixtures.py +64 -0
  19. rag_your_code-1.2.0/tests/test_local_model.py +182 -0
  20. {rag_your_code-1.0.0 → rag_your_code-1.2.0}/tests/test_metadata.py +101 -21
  21. rag_your_code-1.0.0/PKG-INFO +0 -564
  22. rag_your_code-1.0.0/README.md +0 -537
  23. rag_your_code-1.0.0/src/rag_your_code.egg-info/PKG-INFO +0 -564
  24. {rag_your_code-1.0.0 → rag_your_code-1.2.0}/LICENSE +0 -0
  25. {rag_your_code-1.0.0 → rag_your_code-1.2.0}/setup.cfg +0 -0
  26. {rag_your_code-1.0.0 → rag_your_code-1.2.0}/src/rag_your_code.egg-info/dependency_links.txt +0 -0
  27. {rag_your_code-1.0.0 → rag_your_code-1.2.0}/src/rag_your_code.egg-info/entry_points.txt +0 -0
  28. {rag_your_code-1.0.0 → rag_your_code-1.2.0}/src/rag_your_code.egg-info/top_level.txt +0 -0
  29. {rag_your_code-1.0.0 → rag_your_code-1.2.0}/src/ragyourcode/annotate.py +0 -0
  30. {rag_your_code-1.0.0 → rag_your_code-1.2.0}/src/ragyourcode/descriptions.py +0 -0
  31. {rag_your_code-1.0.0 → rag_your_code-1.2.0}/src/ragyourcode/document.py +0 -0
  32. {rag_your_code-1.0.0 → rag_your_code-1.2.0}/src/ragyourcode/indexer.py +0 -0
  33. {rag_your_code-1.0.0 → rag_your_code-1.2.0}/src/ragyourcode/models.py +0 -0
  34. {rag_your_code-1.0.0 → rag_your_code-1.2.0}/src/ragyourcode/providers.py +0 -0
  35. {rag_your_code-1.0.0 → rag_your_code-1.2.0}/src/ragyourcode/py.typed +0 -0
  36. {rag_your_code-1.0.0 → rag_your_code-1.2.0}/src/ragyourcode/workflow.py +0 -0
  37. {rag_your_code-1.0.0 → rag_your_code-1.2.0}/tests/test_agent_protocol.py +0 -0
  38. {rag_your_code-1.0.0 → rag_your_code-1.2.0}/tests/test_agentic.py +0 -0
  39. {rag_your_code-1.0.0 → rag_your_code-1.2.0}/tests/test_config.py +0 -0
  40. {rag_your_code-1.0.0 → rag_your_code-1.2.0}/tests/test_descriptions.py +0 -0
  41. {rag_your_code-1.0.0 → rag_your_code-1.2.0}/tests/test_doc_comments.py +0 -0
  42. {rag_your_code-1.0.0 → rag_your_code-1.2.0}/tests/test_document.py +0 -0
  43. {rag_your_code-1.0.0 → rag_your_code-1.2.0}/tests/test_golden.py +0 -0
  44. {rag_your_code-1.0.0 → rag_your_code-1.2.0}/tests/test_graph_incremental.py +0 -0
  45. {rag_your_code-1.0.0 → rag_your_code-1.2.0}/tests/test_large_repo.py +0 -0
  46. {rag_your_code-1.0.0 → rag_your_code-1.2.0}/tests/test_multilanguage.py +0 -0
  47. {rag_your_code-1.0.0 → rag_your_code-1.2.0}/tests/test_parser_edges.py +0 -0
  48. {rag_your_code-1.0.0 → rag_your_code-1.2.0}/tests/test_providers.py +0 -0
  49. {rag_your_code-1.0.0 → rag_your_code-1.2.0}/tests/test_ragyourcode.py +0 -0
  50. {rag_your_code-1.0.0 → rag_your_code-1.2.0}/tests/test_ranking.py +0 -0
  51. {rag_your_code-1.0.0 → rag_your_code-1.2.0}/tests/test_repo_queries.py +0 -0
  52. {rag_your_code-1.0.0 → rag_your_code-1.2.0}/tests/test_resilience.py +0 -0
  53. {rag_your_code-1.0.0 → rag_your_code-1.2.0}/tests/test_retrieval_correctness.py +0 -0
  54. {rag_your_code-1.0.0 → rag_your_code-1.2.0}/tests/test_workflow.py +0 -0
@@ -0,0 +1,673 @@
1
+ Metadata-Version: 2.4
2
+ Name: rag-your-code
3
+ Version: 1.2.0
4
+ Summary: A local, explainable RAG index for codebases and coding agents
5
+ Author: rag-your-code contributors
6
+ License-Expression: MIT
7
+ Keywords: rag,code-search,retrieval,indexing,graphrag,offline,explainable,agent
8
+ Classifier: Development Status :: 4 - Beta
9
+ Classifier: Environment :: Console
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: Operating System :: OS Independent
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3.10
14
+ Classifier: Programming Language :: Python :: 3.11
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Classifier: Programming Language :: Python :: 3.13
17
+ Classifier: Topic :: Software Development :: Libraries
18
+ Classifier: Topic :: Text Processing :: Indexing
19
+ Classifier: Typing :: Typed
20
+ Requires-Python: >=3.10
21
+ Description-Content-Type: text/markdown
22
+ License-File: LICENSE
23
+ Provides-Extra: dev
24
+ Requires-Dist: pytest>=7; extra == "dev"
25
+ Requires-Dist: tomli>=2.0; python_version < "3.11" and extra == "dev"
26
+ Provides-Extra: sentence-transformers
27
+ Requires-Dist: sentence-transformers>=2.2; extra == "sentence-transformers"
28
+ Dynamic: license-file
29
+
30
+ # RAG Your Code
31
+
32
+ [![PyPI](https://img.shields.io/badge/PyPI-rag--your--code-blue)](https://pypi.org/project/rag-your-code/)
33
+ [![License](https://img.shields.io/badge/license-MIT-green)](LICENSE)
34
+ [![Python](https://img.shields.io/badge/python-3.10--3.13-blue)](pyproject.toml)
35
+
36
+ **A local code-retrieval index for coding agents.** Ask a question in plain
37
+ language; get back the declarations that answer it — each with its file, its
38
+ exact line range, the words it matched on, and its source.
39
+
40
+ Zero runtime dependencies. No network calls. No model required. It runs over a
41
+ private repository on a machine with the network switched off, and produces an
42
+ index a human can read.
43
+
44
+ It is the **R** in RAG. There is no generation here — your agent is the G.
45
+
46
+ ```bash
47
+ pip install rag-your-code
48
+ rag-your-code bootstrap .
49
+ rag-your-code search "where does it decide whether to answer at all" --json
50
+ ```
51
+
52
+ ---
53
+
54
+ ## 1 · The problem
55
+
56
+ An agent looking for something in an unfamiliar codebase has two bad options.
57
+
58
+ **Grep** is fast and exact, and it only finds the string you already guessed.
59
+ Ask "where does it decide whether to answer at all" and there is no string to
60
+ grep for. **Reading whole files** is thorough and blows the context budget:
61
+ five files of a real repository is tens of thousands of tokens, most of them
62
+ irrelevant.
63
+
64
+ Retrieval sits in between — and brings a third problem that the first two do
65
+ not have. Grep can tell you it found nothing. **A ranking cannot.** It always
66
+ produces a least-bad candidate and returns it with a score and a rank that read
67
+ exactly like an answer, whether or not the repository contains anything
68
+ relevant. That is the failure this project spent its last two releases on.
69
+
70
+ ## 2 · What it does
71
+
72
+ | | |
73
+ |---|---|
74
+ | **Index** | Every function, method and class in 15 languages becomes one `CodeUnit`: id, signature, exact line range, source, calls, imports, description. |
75
+ | **Retrieve** | BM25F over five weighted fields, blended with vector similarity. Results carry the terms they matched on. |
76
+ | **Refuse** | Two evidence tests decide whether *any* result is an answer. When neither is met, retrieval returns nothing plus a machine-readable diagnosis. |
77
+ | **Expand** | Optional bounded walk over `calls` / `imports` / `contains`, every hop carrying its edge path as evidence. |
78
+ | **Describe** | Your agent writes the vocabulary the source never contained, stored in a committed sidecar or promoted into the code as a reviewable diff. |
79
+ | **Serve** | A CLI, and a JSON-lines protocol for a long-lived agent subprocess. |
80
+
81
+ **Scope.** Retrieval over source declarations. Not a code-understanding model,
82
+ not a generation step, not an IDE index. Questions are answered in the
83
+ vocabulary somebody wrote down — in the code, in its documentation, or in a
84
+ description an agent added.
85
+
86
+ ## 3 · What is actually hard here
87
+
88
+ Three things, and all three are measured rather than argued.
89
+
90
+ ### 3.1 · Ranking cannot say "no answer"
91
+
92
+ Eight releases measured how well retrieval *finds* the answer. None could
93
+ measure what it does when there is no answer, because every question graded had
94
+ one. A fourth ruler — thirty questions about subjects neither graded repository
95
+ implements — settled it in a single run: **all thirty answered**, in both
96
+ languages, on both repositories.
97
+
98
+ | asked of a repository containing no such code | answered with | on the evidence of |
99
+ |---|---|---|
100
+ | `where are CUDA kernels dispatched to the device` | a test about word counting | `are` `the` `to` `where` |
101
+ | `准入控制为什么会拒绝没有资源限额的容器组` | the UTF-8 console setup | `拒绝` `控制` `没有` |
102
+ | `how is the OAuth refresh token rotated` | a description-store method | `before` `is` `refresh` `the` |
103
+
104
+ Not a Chinese problem and not a ranking problem — a **missing question**.
105
+ Nothing in the pipeline ever asked *is any of this evidence*.
106
+
107
+ Retrieval now asks two questions that ranking cannot:
108
+
109
+ **Coverage** — what share of the query's *discriminating* words occur in the
110
+ index at all. Words the repository uses everywhere are dropped from both sides
111
+ of the fraction, and that is the part that does the work: half of `where are
112
+ CUDA kernels dispatched to the device` matches, and it looks like evidence
113
+ until you notice which half.
114
+
115
+ **Concentration** — what share of the query's *rarity* lands inside a single
116
+ declaration. Coverage alone asks whether each word occurs somewhere, which a
117
+ question about a subject nothing here implements can satisfy entirely out of
118
+ unrelated units: four of six words found in four different declarations, none
119
+ of which has anything to do with the question or with one another. Rarity-
120
+ weighted rather than counted, because a unit holding two ordinary words is not
121
+ better evidence than one holding the rare word the question is about.
122
+
123
+ Both are **ratios inside the query**, never thresholds on a score. A score
124
+ threshold is tied to whatever scale the ranking currently produces, and this
125
+ project has already had one silently stop existing the moment BM25F changed
126
+ the scale.
127
+
128
+ ### 3.2 · The vector was carrying nothing, and here is why
129
+
130
+ The default embedder is a signed feature hash. Ablating it entirely moves the
131
+ three positive rulers by **±1 question in either direction** while the vectors
132
+ occupy **65.3%** of the index. That was known since 0.6.0 and left unexplained.
133
+ The explanation, measured here:
134
+
135
+ - **Not saturation.** Median 56 distinct tokens per unit into 384 buckets;
136
+ 13.6% expected occupancy; 0.4% of units exceed the width. Widening to 16,384
137
+ raises fidelity to true overlap from r=0.40 to r=0.56 and buys no ranking.
138
+ - **Not redundancy.** Its cosine correlates only **+0.45** with the BM25F score
139
+ over 26,490 scored candidates, so it does carry variance of its own.
140
+ - **The variance is the wrong variance.** A signed hash counts every token
141
+ equally. The independent part of what it measures is therefore precisely the
142
+ contribution of words that are everywhere — the part rarity weighting exists
143
+ to discard. Independent *noise*, not independent signal.
144
+ - **And it can only reorder.** Candidates come from the lexical half, so a
145
+ vector cannot make anything retrievable. Six of thirty-five foreign-ruler
146
+ questions have an accepted answer sharing **no token at all** with the query.
147
+
148
+ Eight replacement schemes were implemented and measured across releases —
149
+ character n-grams, random indexing, truncated SVD, posting-list signatures, a
150
+ rarity-weighted hash, call-graph diffusion, postings expansion, authored-fields-
151
+ only. None beat using no vector. The conclusion is not that the hash needs
152
+ tuning; it is that **a vector computed from the same words cannot know anything
153
+ the words do not already say.** Making it useful requires a model — which is
154
+ now an installable option, and measured below.
155
+
156
+ ### 3.3 · Retrieval reaches only what somebody wrote down
157
+
158
+ `retry_charge` tokenizes to one opaque term, not to *retry* and *charge*.
159
+ Splitting identifiers was implemented and measured with query and stored
160
+ vectors rebuilt together: equal or worse on every ruler, because the pieces are
161
+ `get`, `find`, `check`, `test`, which rarity weighting immediately discounts.
162
+
163
+ So the vocabulary ladder is the answer, cheapest rung first:
164
+
165
+ | source | who wrote it | lives in | survives a refactor | cost |
166
+ |---|---|---|---|---|
167
+ | identifier, signature, body | author | the code | by construction | free |
168
+ | docstring / doc comment, 15 languages | author | the code | by construction | free |
169
+ | promoted description | agent | the code | by construction | one review |
170
+ | agent description | agent | a sidecar | needs machinery | tokens |
171
+
172
+ ## 4 · How it works
173
+
174
+ ```
175
+ your repository
176
+ → walk source files configurable ignores, suffixes, size cap
177
+ → parse declarations Python via its own AST; 14 languages via a
178
+ 3-layer line scanner + per-language rule table
179
+ → one CodeUnit each id, qualified name, signature, exact span,
180
+ source, calls, imports, serial, description
181
+ → embed signed hash (default) · local model · endpoint
182
+ → inverted index BM25F over name/signature/description/
183
+ relations/body, IDF derived from your corpus
184
+ → assess coverage + concentration → answer, or refuse
185
+ → rank lexical + weighted cosine
186
+ → optional graph expansion calls / imports / contains, evidence per hop
187
+ → results, or JSON-lines to an agent subprocess
188
+ ```
189
+
190
+ **Parsing.** Python uses the standard-library syntax tree, so nesting,
191
+ qualified names, call lists and spans are exact. Every other language goes
192
+ through three separated layers: a scanner reading one line at a time, a rule
193
+ table per language, and a span closer following brace depth, Ruby's `end`, or
194
+ the next declaration. Because a pattern never sees a second line, a reported
195
+ line number **is** the scanner's loop index and cannot drift, and no
196
+ declaration can swallow the ones after it. A 441-byte JavaScript file that once
197
+ took 12.6 s to parse now takes 0.36 ms.
198
+
199
+ Qualified names come from the spans the closer already produced — a declaration
200
+ nested inside another's span is nested in it, whatever the braces did on the
201
+ way. One mechanism, so there is no second one to disagree with the first.
202
+
203
+ **Ranking.** BM25F with per-field length normalisation. Normalising per field
204
+ is the part that matters: measured against one length for the whole unit, a
205
+ body repeating a word forty times still beat the declaration actually named
206
+ after it, because a long body's advantage in raw count almost exactly cancelled
207
+ its penalty for being long.
208
+
209
+ | field | weight | why |
210
+ |---|---|---|
211
+ | `name` | 8 | what the author called the thing |
212
+ | `signature` | 4 | what it takes and returns |
213
+ | `description` | 3 | what somebody said it does |
214
+ | `relations` | 2 | what it calls and imports |
215
+ | `body` | 1 | a mention |
216
+
217
+ **Rarity comes from your corpus, not a stopword list.** `the` and `calls` earn
218
+ their low weight the same way a Chinese bigram does — by being everywhere — so
219
+ it works in a language nobody anticipated and no list has to be maintained.
220
+
221
+ **Safety.** A repository being scanned is untrusted input, including any
222
+ `.rag-your-code/index.json` it ships. Nothing read out of an index may name a
223
+ path to act on: superseded vector sidecars are enumerated from the naming
224
+ scheme the writer itself uses. A crafted index used to make the documented
225
+ `index` command delete an arbitrary in-tree file and report success.
226
+
227
+ ## 5 · Before and after
228
+
229
+ A question with no lexical shortcut, asked of this repository:
230
+
231
+ ```console
232
+ $ rag-your-code search "where does it decide whether to answer at all" --limit 1
233
+ [src/ragyourcode/search.py:116:Evidence] score=0.449
234
+ This class evidence. and calls dataclass. ... Documented intent: Whether a query
235
+ reached this index at all, kept apart from how its results rank. ...
236
+ ```
237
+
238
+ The same question through Grep is not askable — there is no string to search
239
+ for. The nearest guess, `grep -rn "decide"`, returns matches scattered across
240
+ the repository that a reader must then triage by hand.
241
+
242
+ Now the case that motivated 1.0.0 and 1.1.0 — a question this repository has no
243
+ answer to at all:
244
+
245
+ ```console
246
+ $ rag-your-code search "why does the print spooler leave a duplex job stuck"
247
+ No matching code units.
248
+ The words that matched occur in this repository, but never together in one
249
+ place, so no single declaration is about what you asked. This is usually a
250
+ question about something the repository does not implement, described in words
251
+ it happens to use elsewhere.
252
+ ```
253
+
254
+ `--json` carries the same answer in a form an agent can branch on:
255
+
256
+ ```json
257
+ {"results": [],
258
+ "diagnosis": {"reason": "matched_terms_are_scattered",
259
+ "query_terms": 10,
260
+ "distinctive_terms": ["duplex","job","leave","print","spooler","stuck"],
261
+ "matched_terms": ["job","leave","print"],
262
+ "ubiquitous_terms": ["a","does","the","why"],
263
+ "coverage": 0.5, "min_coverage": 0.4,
264
+ "concentration": 0.1682, "min_concentration": 0.28,
265
+ "applied_min_coverage": 0.4, "applied_min_concentration": 0.28,
266
+ "hint": "..."}}
267
+ ```
268
+
269
+ Read `coverage: 0.5` against `concentration: 0.1682`. Half the distinctive
270
+ words are here — `job`, `leave`, `print` — and they are spread thin enough that
271
+ no declaration holds a sixth of what was asked. Before 1.1.0 that question came
272
+ back with a confident-looking result.
273
+
274
+ Four reasons, because each is recovered by a different move:
275
+
276
+ | `reason` | what it means | what to do |
277
+ |---|---|---|
278
+ | `no_query_term_in_index` | no word of the question occurs anywhere | ask in the code's vocabulary |
279
+ | `only_ubiquitous_terms_matched` | only words the repository uses throughout | add a distinctive term |
280
+ | `too_little_of_the_query_matched` | most of the question is absent | rephrase, or write descriptions |
281
+ | `matched_terms_are_scattered` | the words are here, never together | the subject is probably not in this repository |
282
+
283
+ ## 6 · Benchmark dashboard
284
+
285
+ Four rulers, 135 distinct questions in English and Chinese, graded 235 times —
286
+ two of them run against both repositories. Three grade whether the answer is
287
+ **found**; the fourth grades whether silence is **kept**. Every report
288
+ carries a fingerprint of the corpus it graded, because between two runs of an
289
+ unchanged `search.py` the foreign ruler moved 0.257 → 0.229 purely because that
290
+ repository had grown by ninety units.
291
+
292
+ **Accuracy — default embedder, zero dependencies**
293
+
294
+ | ruler | what it represents | n | hit@1 | hit@3 | MRR |
295
+ |---|---|---|---|---|---|
296
+ | **A** foreign repo, no descriptions | what a first-time user gets | 35 | 0.229 | 0.400 | 0.300 |
297
+ | **B** this repo, generated descriptions only | a cold index of familiar code | 70 | 0.314 | 0.471 | 0.383 |
298
+ | **C** this repo, agent-written descriptions | the warmest case supported | 70 | 0.443 | 0.614 | 0.507 |
299
+
300
+ **Refusal — the fourth ruler, 30 questions with no answer anywhere**
301
+
302
+ | | this repo | foreign repo |
303
+ |---|---|---|
304
+ | correctly met with silence | **0.967** | **0.933** |
305
+ | English only | **0.933** | **0.867** |
306
+ | Chinese only | **1.000** | **1.000** |
307
+ | results resting on no lexical evidence, rulers A–C | **0.000** | **0.000** |
308
+
309
+ **What each bar costs and buys** — one corpus, gate varied alone:
310
+
311
+ | gate | A hit@1/3/MRR | B hit@1/3/MRR | C hit@1/3/MRR | silence own / foreign |
312
+ |---|---|---|---|---|
313
+ | neither (pre-1.0.0) | 0.229/0.400/0.300 | 0.314/0.486/0.391 | 0.471/0.686/0.552 | 0.000 / 0.000 |
314
+ | coverage only (1.0.0) | 0.229/0.400/0.300 | 0.314/0.471/0.383 | 0.471/0.671/0.548 | 0.700 / 0.767 |
315
+ | **both (1.1.0)** | **0.229/0.400/0.300** | **0.314/0.471/0.383** | 0.443/0.614/0.507 | **0.967 / 0.933** |
316
+
317
+ Rulers A and B are **identical to three decimals**. The entire cost is four
318
+ questions of seventy on the warmest ruler. On these four rulers concentration
319
+ subsumes coverage — stated plainly because it is true; coverage is kept because
320
+ it answers a different question and names a different diagnosis.
321
+
322
+ **Latency** — warm corpus, 557 units, 420 samples after warm-up:
323
+
324
+ | | |
325
+ |---|---|
326
+ | query, median | **0.83 ms** |
327
+ | query, p95 | 1.68 ms |
328
+ | refusing an unanswerable query | **0.03 ms** |
329
+
330
+ Refusal is cheaper than answering by a factor of forty: an unanswerable query
331
+ touches only the posting lists of its own distinctive words, never the corpus.
332
+
333
+ **Scale**, synthetic 10,000-unit repository (500 files):
334
+
335
+ | | |
336
+ |---|---|
337
+ | full build | 1.84 s |
338
+ | incremental rebuild after one file changes | 0.207 s (**8.9×**) |
339
+ | compact storage vs readable JSON | 35.6% |
340
+ | index load, fresh process | 45.4 ms |
341
+ | resident memory | 58.7 MiB |
342
+
343
+ **Parsing**, against source-controlled fixtures (15 files, 237 negative cases,
344
+ 89 constructs the spec deliberately excludes):
345
+
346
+ | | |
347
+ |---|---|
348
+ | core declarations found | **91 / 91** |
349
+ | with the correct `start_line` | **91 / 91** |
350
+ | with a usable signature | **91 / 91** |
351
+ | units invented that do not exist | **0** |
352
+
353
+ Directional local measurements, not service levels. Reproduce with
354
+ `python benchmarks/repo_queries.py` and `python benchmarks/large_repo.py`.
355
+
356
+ ## 7 · `rag-your-code search` vs a Grep loop
357
+
358
+ The fair baseline is not one `grep`. An agent handed Grep picks the content
359
+ words out of the question, runs one search per word, and ranks files by how
360
+ many hit. That is what this reproduces — same corpus, same questions, same
361
+ ruler, scored at **file** granularity so Grep is not penalised for lacking
362
+ declaration spans.
363
+
364
+ **On a repository nobody has described, Grep wins.** That is the measured
365
+ result and it is not softened here.
366
+
367
+ | foreign repository · 35 questions · 1,267 units · no descriptions | Grep loop | rag-your-code |
368
+ |---|---|---|
369
+ | right file first | **34.3%** | 31.4% |
370
+ | right file in top 3 | **60.0%** | 48.6% |
371
+ | lines matched across the repo, all questions | 33,213 | — |
372
+ | characters returned, all questions | — | **163,294** |
373
+ | questions it answers | 35 | 28 |
374
+
375
+ **Once the vocabulary exists, it is not close.**
376
+
377
+ | this repository · 70 questions · 569 units · 304 described | Grep loop | rag-your-code |
378
+ |---|---|---|
379
+ | right file first | 25.7% | **58.6%** |
380
+ | right file in top 3 | 64.3% | **75.7%** |
381
+ | lines matched across the repo, all questions | 40,150 | — |
382
+ | characters returned, all questions | — | **277,327** |
383
+ | questions it answers | 70 | 60 |
384
+
385
+ Those two tables are the whole argument of section 3.3, measured against a real
386
+ baseline instead of asserted. A cold index retrieves against a sentence the
387
+ parser generated from identifiers the author already chose — so it is competing
388
+ with Grep using Grep's own information, and losing, because Grep does not have
389
+ to guess which of the matching files is the definition. Descriptions put words
390
+ in the index that the source never contained, and first-place accuracy goes
391
+ from below Grep's to **more than double** it.
392
+
393
+ Three qualifications, because the table would otherwise flatter both sides:
394
+
395
+ - **Scored at file granularity**, which understates this side. A Grep hit is a
396
+ file; a hit here is a declaration with an exact span, a score, and the words
397
+ it matched on. The agent that reads the result opens 40 lines, not a file.
398
+ - **Grep answers everything.** It never declines, which is why it hands back
399
+ 33,213 matching lines for 35 questions — about 950 lines per question, no
400
+ ranking, no spans, no indication which match is the definition. This returns
401
+ roughly 5,800 characters per question, ranked. Seven of 35 and ten of 70
402
+ questions come back empty here instead, with a reason.
403
+ - **Grep wins outright when you know the string.** `grep -rn "COMMON_TERM"` is
404
+ exact, instant and complete, and nothing here replaces it.
405
+
406
+ The two are complementary, and the honest summary is narrow: this earns its
407
+ place on questions phrased as questions, over a repository somebody has taken
408
+ the time to describe.
409
+
410
+ ## 8 · Design principles
411
+
412
+ **Build the ruler before reshaping the thing measured.** Four candidate scoring
413
+ changes once landed between five and six correct over an eight-question set —
414
+ that is the resolution limit of the instrument, not a ranking of options. There
415
+ are 135 now across four rulers, and every claim in this README is a number from
416
+ one of them.
417
+
418
+ **Measure somewhere it can fail.** Every ruler this project had once graded a
419
+ repository its own authors wrote. Indexed cold against a foreign repository the
420
+ same code scored 0.086 hit@1 against a self-reported 0.457. Ruler A exists so
421
+ that can never be comfortable again — and stemming, which helps both own-repo
422
+ rulers, was rejected on exactly this evidence: it costs the foreign ruler 3 of
423
+ 35 hit@3.
424
+
425
+ **Make the error structurally impossible rather than checking for it.** A line
426
+ number that *is* the loop index cannot drift. A description keyed by a digest
427
+ of its own code cannot outlive it.
428
+
429
+ **A ratio inside the query, never a threshold on a score.** Scales move;
430
+ ratios do not.
431
+
432
+ **Derive figures from data; a hand-maintained number is a claim nobody checks.**
433
+ The parser fingerprints its own source. The settings table in this README is
434
+ asserted against `config.py` in both directions — it had drifted nine settings
435
+ behind before that test existed.
436
+
437
+ **The contract does not move.** `CodeUnit`, index schema 2, and the JSON-lines
438
+ protocol are unchanged across every release; new information arrives in new
439
+ fields beside the old ones, never by widening an enumeration callers branch on.
440
+
441
+ **Publish what was measured and rejected.** Twelve changes have been
442
+ implemented, measured and dropped. They are recorded in
443
+ [docs/ROADMAP.md](docs/ROADMAP.md) with their numbers, because "we tried that
444
+ and it cost 3 of 35" is worth more than an unexplored idea.
445
+
446
+ ## 9 · Bringing your own model
447
+
448
+ Everything above works with no model. Three embedders, and the difference
449
+ between them is what the vector is *able* to know.
450
+
451
+ ```toml
452
+ # rag-your-code.toml — a model that runs on your machine
453
+ [embedding]
454
+ provider = "sentence-transformers"
455
+ model = "sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2"
456
+ dimensions = 384
457
+ ```
458
+
459
+ ```bash
460
+ pip install "rag-your-code[sentence-transformers]"
461
+ ```
462
+
463
+ The extra is optional by construction: `dependencies = []` is what a default
464
+ install gets, the import happens inside the constructor, and a test asserts the
465
+ default provider imports none of it.
466
+
467
+ **Measured, on the same four rulers.** This is the first release with a real
468
+ model behind these numbers — 0.8.0 shipped the seam and said plainly that its
469
+ benefit was unmeasured.
470
+
471
+ | ruler | signed hash (default) | MiniLM, local |
472
+ |---|---|---|
473
+ | **A** foreign, cold | 0.229 / 0.400 / 0.300 | **0.286 / 0.457 / 0.357** |
474
+ | **B** own, cold | 0.314 / 0.471 / 0.383 | **0.329 / 0.486 / 0.400** |
475
+ | **C** own, described | 0.443 / 0.614 / 0.507 | 0.443 / **0.671 / 0.540** |
476
+ | **D** silence, own / foreign | 0.967 / 0.933 | 0.967 / 0.933 |
477
+
478
+ Better on every positive ruler, with refusal unchanged. The pairs the hash
479
+ scores exactly zero:
480
+
481
+ | pair | signed hash | MiniLM |
482
+ |---|---|---|
483
+ | `retry a failed card charge` vs `resend a payment after a transient error` | 0.298 | **0.583** |
484
+ | `retry a failed card charge` vs `delete every row of the user table` | 0.000 | 0.073 |
485
+ | `计算两个数的和` vs `sum two numbers` | **0.000** | **0.822** |
486
+ | `刷新索引` vs `rebuild the index` | **0.000** | **0.684** |
487
+
488
+ A semantic embedder is **not** exempt from the evidence bars, and that is a
489
+ correction to 1.0.0. Exempting one was reasoned — a paraphrase sharing no word
490
+ with its answer is exactly what a model is for — and it was wrong: exempt and
491
+ asked no other question, the model answered all sixty unanswerable questions.
492
+ Two vector-space replacements were measured and rejected: a similarity floor is
493
+ a threshold on a score and the distributions overlap (0.469 vs 0.418 median),
494
+ and a scale-free standout metric took ruler B from 0.329 to 0.186 for two
495
+ thirds of the silence. Applying the lexical bars costs ruler A nothing.
496
+
497
+ **A hosted endpoint** is the third option, and the only one that sends your
498
+ source anywhere:
499
+
500
+ ```toml
501
+ provider = "openai-compatible"
502
+ endpoint = "https://api.example.com/v1/embeddings"
503
+ model = "text-embedding-3-small"
504
+ dimensions = 1536
505
+ api_key_env = "OPENAI_API_KEY" # the NAME of the variable, never the key
506
+ ```
507
+
508
+ The key is never a setting. `rag-your-code.toml` is meant to be committed so
509
+ everyone who clones sees what shaped the index; a credential is the one value
510
+ with the opposite requirement. Sending a key over plain `http://` to anything
511
+ but your own machine is refused rather than warned about. A failure stops the
512
+ build rather than falling back, because a mixed index is two vector spaces and
513
+ a cosine across them is a meaningless number ranking would act on anyway.
514
+
515
+ With a semantic embedder, similarity may also **add** candidates rather than
516
+ only reorder them (`search.vector_recall`) — the one thing that can reach a
517
+ unit sharing no word with the question. Under the hash the same widening
518
+ measured worse, so it stays off there.
519
+
520
+ ## 10 · Install and use
521
+
522
+ **As a Claude Code plugin** (the primary way):
523
+
524
+ ```
525
+ /plugin marketplace add skymanbp/rag-your-code
526
+ /plugin install rag-your-code@rag-your-code
527
+ /reload-plugins
528
+ ```
529
+
530
+ Four commands and one skill. No hooks, no agents, no MCP server:
531
+
532
+ | | |
533
+ |---|---|
534
+ | `/rag-your-code:index` | index, and say which rung this repository is on |
535
+ | `/rag-your-code:search` | ask in plain language; cite `path:line` |
536
+ | `/rag-your-code:describe` | write the vocabulary the source does not contain |
537
+ | `/rag-your-code:status` | stale? coverage? which embedder? what next? |
538
+
539
+ Measured with `claude plugin details` on an installed copy: **~249 tokens added
540
+ to every session** (skill ~30, each command ~50–60), and 590–2,400 only when
541
+ one of them fires. That is up from ~39 in 1.1.0, and the increase is the price
542
+ of being findable — a skill fires only when a model decides it should, which
543
+ left the whole plugin with no entry point a person could discover. The commands
544
+ install the Python package on first use.
545
+
546
+ **As a CLI:**
547
+
548
+ ```bash
549
+ rag-your-code bootstrap . # index, then say what is missing
550
+ rag-your-code search "how are stale indexes detected" --json
551
+ rag-your-code search "what calls the retry handler" --graph --hops 1
552
+ rag-your-code describe status # description coverage
553
+ rag-your-code describe promote | git apply # move descriptions into the code
554
+ ```
555
+
556
+ `bootstrap` exists because indexing is not the same as being searchable. A
557
+ fresh index retrieves against the sentence the parser generated, which adds no
558
+ word the source did not have. It reports which rung the repository is on and
559
+ hands over that rung's work; run it again after each round.
560
+
561
+ The index is written under `.rag-your-code/`; **your source files are never
562
+ modified**. `describe promote` emits a diff for you to review — the tool never
563
+ writes source itself.
564
+
565
+ ### Configuration
566
+
567
+ 22 settings in `rag-your-code.toml`:
568
+
569
+ | section | settings |
570
+ |---|---|
571
+ | `[index]` | `ignore`, `suffixes`, `max_file_bytes` |
572
+ | `[embedding]` | `dimensions`, `provider`, `endpoint`, `model`, `api_key_env`, `batch`, `timeout`, `retries` |
573
+ | `[search]` | `min_coverage`, `min_concentration`, `vector_weight`, `vector_recall`, `limit`, `max_chars` |
574
+ | `[agent]` | `max_open_bytes`, `max_open_chars` |
575
+ | `[describe]` | `languages`, `batch`, `max_chars` |
576
+
577
+ This table is asserted against `config.py` in both directions by
578
+ `tests/test_metadata.py`. Resolution is CLI flag > file > built-in default;
579
+ there is deliberately no environment layer, because an index is an artifact of
580
+ a repository rather than of a shell. An unknown key or out-of-range value is an
581
+ error, not a shrug.
582
+
583
+ ### Agent protocol
584
+
585
+ `rag-your-code agent --root PATH` reads one JSON request per line, writes one
586
+ reply per line:
587
+
588
+ ```json
589
+ {"action":"search","query":"database transaction rollback","limit":5}
590
+ {"action":"research","query":"trace payment retry behavior","max_steps":2}
591
+ {"action":"neighbors","id":"payments.py:4:retry_charge","hops":1}
592
+ {"action":"open","path":"payments.py","start_line":1,"end_line":80}
593
+ {"action":"describe_pending","limit":20}
594
+ {"action":"describe_put","descriptions":[{"id":"payments.py:4:retry_charge","text":"..."}]}
595
+ ```
596
+
597
+ **A result is navigation, not the file.** The code arrives once, in `context`,
598
+ trimmed to `max_chars`, with `omitted_for_budget` saying how many results it
599
+ did not reach. Carrying source per result is what let one `search --json` reply
600
+ reach 65,025 characters against a stated budget of 12,000.
601
+
602
+ **No single request can end the session.** Numeric fields saturate at their
603
+ bounds, `open` is bounded in lines and bytes, and anything unanticipated is
604
+ reported in-band with its exception type. Streams are pinned to UTF-8 rather
605
+ than following the console codepage.
606
+
607
+ ### What lives where
608
+
609
+ | path | authored or generated | commit it? |
610
+ |---|---|---|
611
+ | `rag-your-code.toml` | authored | yes |
612
+ | `rag-your-code.descriptions.json` | authored by your agent | yes |
613
+ | `.rag-your-code/` | generated | no |
614
+
615
+ ## 11 · Known limits
616
+
617
+ Named because they are measured, not because they are excuses.
618
+
619
+ **One English question in fifteen still gets answered when it should not.**
620
+ `how is a hostname resolved when the nameserver times out` finds `hostname`,
621
+ `resolved` and `times` genuinely co-occurring in one unrelated declaration, on
622
+ both repositories. No lexical rule separates a real vocabulary collision from a
623
+ real answer. Chinese sits at 1.000 silence on both.
624
+
625
+ **Chinese cold-start hit@1 is 0.000** on rulers A and B. Chinese reaches a
626
+ repository through descriptions or not at all: the code contains no Chinese, so
627
+ a cold index has no Chinese vocabulary to match. `describe` is the fix and it
628
+ works — ruler C is 0.333 — but there is no free rung of the ladder for it.
629
+
630
+ **There is no stemming.** `catastrophic backtracking` does not reach
631
+ `backtracks catastrophically`. A light suffix stripper was implemented and
632
+ measured on all four rulers: it improves both own-repository rulers and costs
633
+ the foreign one 3 of 35 hit@3, so it was rejected.
634
+
635
+ **A test declaration sometimes outranks real code** — 10 of 175 questions across
636
+ three rulers, where a test at rank 1 displaced an accepted answer at rank 2–3.
637
+ The long-standing explanation, that a test outranks the code it *tests*, is
638
+ wrong: of the eight inspected, seven are unrelated tests winning on
639
+ prose. A callee-before-caller rerank fires on zero questions and the `name`
640
+ field weight moves nothing, because an underscored test name is a single token.
641
+
642
+ **The vectors are 65.3% of the index and earn ±1 question** under the default
643
+ embedder. Not removed: the same storage is what makes an optional model work,
644
+ and the schema stays one shape.
645
+
646
+ **`search.vector_recall` scans every vector per query.** Affordable at the
647
+ measured envelope, and exactly the work an ANN index would replace.
648
+
649
+ **Tree-sitter parsing and a SQLite/ANN storage layer are not here.** Both would
650
+ need a dependency, and the policy for those is settled: they follow the
651
+ embedding provider's pattern — optional, user-selected, never in the default
652
+ install. Full reasoning in [docs/ROADMAP.md](docs/ROADMAP.md).
653
+
654
+ ## 12 · Development
655
+
656
+ ```bash
657
+ python -m pip install -e ".[dev]"
658
+ pytest -q
659
+ ```
660
+
661
+ Per-release test counts are in [CHANGELOG.md](CHANGELOG.md); a bare figure in a
662
+ living document is a claim that rots. CI runs Python 3.10–3.13 on Linux and
663
+ Windows, plus a job that installs the built wheel into a clean environment and
664
+ runs every documented command, and another that runs the skill's own install
665
+ line verbatim.
666
+
667
+ - [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) — how each stage works and why
668
+ - [docs/TESTING.md](docs/TESTING.md) — what the suites protect
669
+ - [docs/ROADMAP.md](docs/ROADMAP.md) — what shipped, what was rejected and why
670
+ - [CONTRIBUTING.md](CONTRIBUTING.md) — ground rules, and how to add a language
671
+ - [CHANGELOG.md](CHANGELOG.md) — every release with its measurements
672
+
673
+ MIT licensed.