rag-your-code 1.4.0__tar.gz → 1.4.2__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 (51) hide show
  1. {rag_your_code-1.4.0/src/rag_your_code.egg-info → rag_your_code-1.4.2}/PKG-INFO +42 -31
  2. {rag_your_code-1.4.0 → rag_your_code-1.4.2}/README.md +41 -30
  3. {rag_your_code-1.4.0 → rag_your_code-1.4.2}/pyproject.toml +1 -1
  4. {rag_your_code-1.4.0 → rag_your_code-1.4.2/src/rag_your_code.egg-info}/PKG-INFO +42 -31
  5. {rag_your_code-1.4.0 → rag_your_code-1.4.2}/src/ragyourcode/__init__.py +1 -1
  6. {rag_your_code-1.4.0 → rag_your_code-1.4.2}/src/ragyourcode/annotate.py +9 -1
  7. {rag_your_code-1.4.0 → rag_your_code-1.4.2}/src/ragyourcode/document.py +1 -1
  8. {rag_your_code-1.4.0 → rag_your_code-1.4.2}/src/ragyourcode/parser.py +2 -2
  9. {rag_your_code-1.4.0 → rag_your_code-1.4.2}/src/ragyourcode/search.py +36 -1
  10. {rag_your_code-1.4.0 → rag_your_code-1.4.2}/tests/test_ranking.py +54 -0
  11. {rag_your_code-1.4.0 → rag_your_code-1.4.2}/LICENSE +0 -0
  12. {rag_your_code-1.4.0 → rag_your_code-1.4.2}/setup.cfg +0 -0
  13. {rag_your_code-1.4.0 → rag_your_code-1.4.2}/src/rag_your_code.egg-info/SOURCES.txt +0 -0
  14. {rag_your_code-1.4.0 → rag_your_code-1.4.2}/src/rag_your_code.egg-info/dependency_links.txt +0 -0
  15. {rag_your_code-1.4.0 → rag_your_code-1.4.2}/src/rag_your_code.egg-info/entry_points.txt +0 -0
  16. {rag_your_code-1.4.0 → rag_your_code-1.4.2}/src/rag_your_code.egg-info/requires.txt +0 -0
  17. {rag_your_code-1.4.0 → rag_your_code-1.4.2}/src/rag_your_code.egg-info/top_level.txt +0 -0
  18. {rag_your_code-1.4.0 → rag_your_code-1.4.2}/src/ragyourcode/agentic.py +0 -0
  19. {rag_your_code-1.4.0 → rag_your_code-1.4.2}/src/ragyourcode/cli.py +0 -0
  20. {rag_your_code-1.4.0 → rag_your_code-1.4.2}/src/ragyourcode/config.py +0 -0
  21. {rag_your_code-1.4.0 → rag_your_code-1.4.2}/src/ragyourcode/descriptions.py +0 -0
  22. {rag_your_code-1.4.0 → rag_your_code-1.4.2}/src/ragyourcode/embeddings.py +0 -0
  23. {rag_your_code-1.4.0 → rag_your_code-1.4.2}/src/ragyourcode/graph.py +0 -0
  24. {rag_your_code-1.4.0 → rag_your_code-1.4.2}/src/ragyourcode/indexer.py +0 -0
  25. {rag_your_code-1.4.0 → rag_your_code-1.4.2}/src/ragyourcode/models.py +0 -0
  26. {rag_your_code-1.4.0 → rag_your_code-1.4.2}/src/ragyourcode/providers.py +0 -0
  27. {rag_your_code-1.4.0 → rag_your_code-1.4.2}/src/ragyourcode/py.typed +0 -0
  28. {rag_your_code-1.4.0 → rag_your_code-1.4.2}/src/ragyourcode/workflow.py +0 -0
  29. {rag_your_code-1.4.0 → rag_your_code-1.4.2}/tests/test_absent_queries.py +0 -0
  30. {rag_your_code-1.4.0 → rag_your_code-1.4.2}/tests/test_agent_protocol.py +0 -0
  31. {rag_your_code-1.4.0 → rag_your_code-1.4.2}/tests/test_agentic.py +0 -0
  32. {rag_your_code-1.4.0 → rag_your_code-1.4.2}/tests/test_config.py +0 -0
  33. {rag_your_code-1.4.0 → rag_your_code-1.4.2}/tests/test_descriptions.py +0 -0
  34. {rag_your_code-1.4.0 → rag_your_code-1.4.2}/tests/test_doc_comments.py +0 -0
  35. {rag_your_code-1.4.0 → rag_your_code-1.4.2}/tests/test_document.py +0 -0
  36. {rag_your_code-1.4.0 → rag_your_code-1.4.2}/tests/test_e2e_cli.py +0 -0
  37. {rag_your_code-1.4.0 → rag_your_code-1.4.2}/tests/test_evidence.py +0 -0
  38. {rag_your_code-1.4.0 → rag_your_code-1.4.2}/tests/test_golden.py +0 -0
  39. {rag_your_code-1.4.0 → rag_your_code-1.4.2}/tests/test_graph_incremental.py +0 -0
  40. {rag_your_code-1.4.0 → rag_your_code-1.4.2}/tests/test_language_fixtures.py +0 -0
  41. {rag_your_code-1.4.0 → rag_your_code-1.4.2}/tests/test_large_repo.py +0 -0
  42. {rag_your_code-1.4.0 → rag_your_code-1.4.2}/tests/test_local_model.py +0 -0
  43. {rag_your_code-1.4.0 → rag_your_code-1.4.2}/tests/test_metadata.py +0 -0
  44. {rag_your_code-1.4.0 → rag_your_code-1.4.2}/tests/test_multilanguage.py +0 -0
  45. {rag_your_code-1.4.0 → rag_your_code-1.4.2}/tests/test_parser_edges.py +0 -0
  46. {rag_your_code-1.4.0 → rag_your_code-1.4.2}/tests/test_providers.py +0 -0
  47. {rag_your_code-1.4.0 → rag_your_code-1.4.2}/tests/test_ragyourcode.py +0 -0
  48. {rag_your_code-1.4.0 → rag_your_code-1.4.2}/tests/test_repo_queries.py +0 -0
  49. {rag_your_code-1.4.0 → rag_your_code-1.4.2}/tests/test_resilience.py +0 -0
  50. {rag_your_code-1.4.0 → rag_your_code-1.4.2}/tests/test_retrieval_correctness.py +0 -0
  51. {rag_your_code-1.4.0 → rag_your_code-1.4.2}/tests/test_workflow.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: rag-your-code
3
- Version: 1.4.0
3
+ Version: 1.4.2
4
4
  Summary: A local, explainable RAG index for codebases and coding agents
5
5
  Author: rag-your-code contributors
6
6
  License-Expression: MIT
@@ -295,11 +295,11 @@ repository had grown by ninety units.
295
295
  |---|---|---|---|---|---|
296
296
  | **A** Flask 3.1.3, no descriptions | what a first-time user gets | 35 | 0.200 | 0.286 | 0.238 |
297
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 |
298
+ | **C** this repo, agent-written descriptions | the warmest case supported | 70 | 0.443 | 0.614 | 0.509 |
299
299
 
300
300
  The corpora, without which none of the above is reproducible — **A** 1,572
301
- units, `5fd51169eacc`; **B** 581 units, `8e1e71942c1c`; **C** 581 units,
302
- `978a1d48a82a`. Ruler A grades **a copy of Flask 3.1.3 carried in this
301
+ units, `5fd51169eacc`; **B** 584 units, `fb1f841fa43a`; **C** 584 units,
302
+ `c9df00350cbd`. Ruler A grades **a copy of Flask 3.1.3 carried in this
303
303
  repository**, at [`benchmarks/corpus/flask`](benchmarks/corpus/flask), pinned
304
304
  to commit `22d9247`. Through 1.3.0 it graded a checkout on one machine, and
305
305
  that cost three things: two questions pointed at a declaration the subject had
@@ -330,10 +330,10 @@ Flask does not implement get through on exactly that.
330
330
 
331
331
  | gate | A hit@1/3/MRR | B hit@1/3/MRR | C hit@1/3/MRR | silence own / foreign |
332
332
  |---|---|---|---|---|
333
- | neither (pre-1.0.0) | 0.200/0.286/0.238 | 0.314/0.486/0.391 | 0.486/0.700/0.567 | 0.000 / 0.000 |
334
- | coverage only (1.0.0) | 0.200/0.286/0.238 | 0.314/0.471/0.383 | 0.486/0.686/0.562 | 0.600 / 0.733 |
335
- | concentration only | 0.200/0.286/0.238 | 0.314/0.471/0.383 | 0.443/0.614/0.507 | 0.967 / 0.800 |
336
- | **both (1.1.0)** | **0.200/0.286/0.238** | **0.314/0.471/0.383** | 0.443/0.614/0.507 | **0.967 / 0.833** |
333
+ | neither (pre-1.0.0) | 0.200/0.286/0.238 | 0.314/0.486/0.391 | 0.486/0.700/0.569 | 0.000 / 0.000 |
334
+ | coverage only (1.0.0) | 0.200/0.286/0.238 | 0.314/0.471/0.383 | 0.486/0.686/0.564 | 0.567 / 0.733 |
335
+ | concentration only | 0.200/0.286/0.238 | 0.314/0.471/0.383 | 0.443/0.614/0.509 | 0.967 / 0.800 |
336
+ | **both (1.1.0)** | **0.200/0.286/0.238** | **0.314/0.471/0.383** | 0.443/0.614/0.509 | **0.967 / 0.833** |
337
337
 
338
338
  Ruler A is **unmoved by either bar**, and B by concentration. The whole cost is
339
339
  three questions of seventy at hit@1 on the warmest ruler, and six at hit@3.
@@ -352,20 +352,20 @@ from 0.314 and C to 0.329 from 0.443. **0.28 was chosen before this corpus
352
352
  existed and survived meeting it**, which is the only kind of evidence a default
353
353
  can have.
354
354
 
355
- **Latency** — warm corpus, 581 units `978a1d48a82a`, five consecutive
355
+ **Latency** — warm corpus, 584 units `c9df00350cbd`, five consecutive
356
356
  invocations of `python -m benchmarks.query_latency --repeats 10` (420 samples
357
357
  each):
358
358
 
359
359
  | | | across the five |
360
360
  |---|---|---|
361
- | query, median | **1.0 ms** | 0.711.26 |
362
- | query, p95 | 1.7 ms | 1.23.7 |
363
- | refusing an unanswerable query | **0.029 ms** | 0.023 – 0.031 |
364
- | refusal cheaper than answering by | **~36×** | 3144 |
361
+ | query, median | **0.61 ms** | 0.600.64 |
362
+ | query, p95 | 1.09 ms | 1.061.14 |
363
+ | refusing an unanswerable query | **0.017 ms** | 0.015 – 0.019 |
364
+ | refusal cheaper than answering by | **~36×** | 3341 |
365
365
 
366
366
  Two significant figures and a spread, because that is the precision the
367
- measurement has. Across fifteen invocations over two releases on the same idle
368
- machine the median has landed anywhere from 0.62 to 1.44 ms and p95 from 1.09
367
+ measurement has. Across twenty invocations over three releases on the same idle
368
+ machine the median has landed anywhere from 0.51 to 1.44 ms and p95 from 0.85
369
369
  to 7.34 ms — a band wider than any change the code has ever made to this
370
370
  number. Releases before 1.3.0 published `0.83 ms / p95 1.68 ms` to three
371
371
  figures from a script that was never committed; both values sit inside that
@@ -420,17 +420,17 @@ framework reversed it. The honest claim is narrower than either table alone:
420
420
  | right file first | 22.9% | **37.1%** |
421
421
  | right file in top 3 | 45.7% | **57.1%** |
422
422
  | lines it hands back, all questions | 17,641 | — |
423
- | characters returned, all questions | 1,415,656 | **249,720** |
423
+ | characters returned, all questions | 1,415,656 | **258,236** |
424
424
  | questions it answers | 30 | 30 |
425
425
 
426
426
  **Once the vocabulary exists, it is not close.**
427
427
 
428
- | this repository · 70 questions · 581 units `978a1d48a82a` · 304 described | Grep loop | rag-your-code |
428
+ | this repository · 70 questions · 584 units `c9df00350cbd` · 304 described | Grep loop | rag-your-code |
429
429
  |---|---|---|
430
430
  | right file first | 22.9% | **58.6%** |
431
- | right file in top 3 | 54.3% | **75.7%** |
432
- | lines it hands back, all questions | 11,833 | — |
433
- | characters returned, all questions | 1,122,902 | **626,022** |
431
+ | right file in top 3 | 54.3% | **77.1%** |
432
+ | lines it hands back, all questions | 11,959 | — |
433
+ | characters returned, all questions | 1,135,411 | **615,673** |
434
434
  | questions it answers | **61** | 60 |
435
435
 
436
436
  Those two tables are the whole argument of section 3.3, measured against a real
@@ -467,13 +467,15 @@ Four qualifications, because the table would otherwise flatter both sides:
467
467
  `the` gets every file back in no order. It is also why Grep declines nine of
468
468
  the seventy questions here — those had no word left that this corpus does not
469
469
  use everywhere.
470
- - **Payload is counted in characters on both sides.** On this repository Grep
471
- hands back 18,400 characters per question it answers, unranked and without
472
- spans, against 10,400 here, ranked and capped by `search.max_chars` — a
473
- factor of 1.8. On Flask it is a factor of 5.7 — 47,200 characters against
474
- 8,300 because a framework repeats its own vocabulary across many files and
475
- Grep has no way to rank what it finds. Both sides decline the same five of
476
- those 35 and they are the five Chinese ones, all of them. A Chinese word is
470
+ - **Payload is counted in characters on both sides.** Grep hands back 18,600
471
+ characters per question it answers here, unranked and without spans, against
472
+ 10,300 ranked and capped by `search.max_chars` — a factor of 1.8, and 5.5 on
473
+ Flask, where a framework repeats its vocabulary across many files and Grep
474
+ cannot rank what it finds. 1.4.1 changed what fits in that cap: the block had
475
+ been reprinting the docstring the code below already showed, 2,381 of 3,382
476
+ characters of prose header on Flask, so the same budget now carries **119
477
+ declarations instead of 92** there and 323 instead of 305 here. Both sides
478
+ decline the same five of those 35 — the five Chinese ones, all of them. A Chinese word is
477
479
  not a substring of English source and it is not a token in an index built
478
480
  from English source, so on a repository written in one language the cold
479
481
  cross-language case is not this tool's failure but the corpus's.
@@ -554,10 +556,10 @@ against a pinned corpus:
554
556
  | **C** own, described | 581 `978a1d48a82a` | **0.443 / 0.614 / 0.507** | 0.429 / 0.600 / 0.500 |
555
557
  | **D** silence, own / foreign | as above | 0.967 / 0.833 | 0.967 / 0.833 |
556
558
 
557
- **Worse or identical on every ruler.** It is shipped anyway, as an extra nobody
558
- has to install, because it does one thing the hash cannot do at all and these
559
- rulers cannot see: reach a unit that shares no word with the question. The
560
- pairs the hash scores exactly zero:
559
+ **Worse or identical on every ruler.** The 581-unit stamps are the corpus both
560
+ arms shared, kept rather than refreshed that is what a stamp is for. It ships
561
+ anyway because it does one thing the hash cannot and these rulers cannot see:
562
+ reach a unit sharing no word with the question. The pairs it scores zero on:
561
563
 
562
564
  | pair | signed hash | MiniLM |
563
565
  |---|---|---|
@@ -612,6 +614,14 @@ measured worse, so it stays off there.
612
614
  /reload-plugins
613
615
  ```
614
616
 
617
+ Updating needs the full id and a marketplace refresh first; the bare name is
618
+ refused with `Plugin "rag-your-code" not found`, which reads like it is gone:
619
+
620
+ ```bash
621
+ claude plugin marketplace update rag-your-code
622
+ claude plugin update rag-your-code@rag-your-code # then restart
623
+ ```
624
+
615
625
  Four commands and one skill. No hooks, no agents, no MCP server:
616
626
 
617
627
  | | |
@@ -759,6 +769,7 @@ Windows, plus a job that installs the built wheel into a clean environment and
759
769
  runs every documented command, and another that runs the skill's own install
760
770
  line verbatim.
761
771
 
772
+ - [docs/FLOW.md](docs/FLOW.md) — the whole thing in four diagrams
762
773
  - [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) — how each stage works and why
763
774
  - [docs/TESTING.md](docs/TESTING.md) — what the suites protect
764
775
  - [docs/ROADMAP.md](docs/ROADMAP.md) — what shipped, what was rejected and why
@@ -266,11 +266,11 @@ repository had grown by ninety units.
266
266
  |---|---|---|---|---|---|
267
267
  | **A** Flask 3.1.3, no descriptions | what a first-time user gets | 35 | 0.200 | 0.286 | 0.238 |
268
268
  | **B** this repo, generated descriptions only | a cold index of familiar code | 70 | 0.314 | 0.471 | 0.383 |
269
- | **C** this repo, agent-written descriptions | the warmest case supported | 70 | 0.443 | 0.614 | 0.507 |
269
+ | **C** this repo, agent-written descriptions | the warmest case supported | 70 | 0.443 | 0.614 | 0.509 |
270
270
 
271
271
  The corpora, without which none of the above is reproducible — **A** 1,572
272
- units, `5fd51169eacc`; **B** 581 units, `8e1e71942c1c`; **C** 581 units,
273
- `978a1d48a82a`. Ruler A grades **a copy of Flask 3.1.3 carried in this
272
+ units, `5fd51169eacc`; **B** 584 units, `fb1f841fa43a`; **C** 584 units,
273
+ `c9df00350cbd`. Ruler A grades **a copy of Flask 3.1.3 carried in this
274
274
  repository**, at [`benchmarks/corpus/flask`](benchmarks/corpus/flask), pinned
275
275
  to commit `22d9247`. Through 1.3.0 it graded a checkout on one machine, and
276
276
  that cost three things: two questions pointed at a declaration the subject had
@@ -301,10 +301,10 @@ Flask does not implement get through on exactly that.
301
301
 
302
302
  | gate | A hit@1/3/MRR | B hit@1/3/MRR | C hit@1/3/MRR | silence own / foreign |
303
303
  |---|---|---|---|---|
304
- | neither (pre-1.0.0) | 0.200/0.286/0.238 | 0.314/0.486/0.391 | 0.486/0.700/0.567 | 0.000 / 0.000 |
305
- | coverage only (1.0.0) | 0.200/0.286/0.238 | 0.314/0.471/0.383 | 0.486/0.686/0.562 | 0.600 / 0.733 |
306
- | concentration only | 0.200/0.286/0.238 | 0.314/0.471/0.383 | 0.443/0.614/0.507 | 0.967 / 0.800 |
307
- | **both (1.1.0)** | **0.200/0.286/0.238** | **0.314/0.471/0.383** | 0.443/0.614/0.507 | **0.967 / 0.833** |
304
+ | neither (pre-1.0.0) | 0.200/0.286/0.238 | 0.314/0.486/0.391 | 0.486/0.700/0.569 | 0.000 / 0.000 |
305
+ | coverage only (1.0.0) | 0.200/0.286/0.238 | 0.314/0.471/0.383 | 0.486/0.686/0.564 | 0.567 / 0.733 |
306
+ | concentration only | 0.200/0.286/0.238 | 0.314/0.471/0.383 | 0.443/0.614/0.509 | 0.967 / 0.800 |
307
+ | **both (1.1.0)** | **0.200/0.286/0.238** | **0.314/0.471/0.383** | 0.443/0.614/0.509 | **0.967 / 0.833** |
308
308
 
309
309
  Ruler A is **unmoved by either bar**, and B by concentration. The whole cost is
310
310
  three questions of seventy at hit@1 on the warmest ruler, and six at hit@3.
@@ -323,20 +323,20 @@ from 0.314 and C to 0.329 from 0.443. **0.28 was chosen before this corpus
323
323
  existed and survived meeting it**, which is the only kind of evidence a default
324
324
  can have.
325
325
 
326
- **Latency** — warm corpus, 581 units `978a1d48a82a`, five consecutive
326
+ **Latency** — warm corpus, 584 units `c9df00350cbd`, five consecutive
327
327
  invocations of `python -m benchmarks.query_latency --repeats 10` (420 samples
328
328
  each):
329
329
 
330
330
  | | | across the five |
331
331
  |---|---|---|
332
- | query, median | **1.0 ms** | 0.711.26 |
333
- | query, p95 | 1.7 ms | 1.23.7 |
334
- | refusing an unanswerable query | **0.029 ms** | 0.023 – 0.031 |
335
- | refusal cheaper than answering by | **~36×** | 3144 |
332
+ | query, median | **0.61 ms** | 0.600.64 |
333
+ | query, p95 | 1.09 ms | 1.061.14 |
334
+ | refusing an unanswerable query | **0.017 ms** | 0.015 – 0.019 |
335
+ | refusal cheaper than answering by | **~36×** | 3341 |
336
336
 
337
337
  Two significant figures and a spread, because that is the precision the
338
- measurement has. Across fifteen invocations over two releases on the same idle
339
- machine the median has landed anywhere from 0.62 to 1.44 ms and p95 from 1.09
338
+ measurement has. Across twenty invocations over three releases on the same idle
339
+ machine the median has landed anywhere from 0.51 to 1.44 ms and p95 from 0.85
340
340
  to 7.34 ms — a band wider than any change the code has ever made to this
341
341
  number. Releases before 1.3.0 published `0.83 ms / p95 1.68 ms` to three
342
342
  figures from a script that was never committed; both values sit inside that
@@ -391,17 +391,17 @@ framework reversed it. The honest claim is narrower than either table alone:
391
391
  | right file first | 22.9% | **37.1%** |
392
392
  | right file in top 3 | 45.7% | **57.1%** |
393
393
  | lines it hands back, all questions | 17,641 | — |
394
- | characters returned, all questions | 1,415,656 | **249,720** |
394
+ | characters returned, all questions | 1,415,656 | **258,236** |
395
395
  | questions it answers | 30 | 30 |
396
396
 
397
397
  **Once the vocabulary exists, it is not close.**
398
398
 
399
- | this repository · 70 questions · 581 units `978a1d48a82a` · 304 described | Grep loop | rag-your-code |
399
+ | this repository · 70 questions · 584 units `c9df00350cbd` · 304 described | Grep loop | rag-your-code |
400
400
  |---|---|---|
401
401
  | right file first | 22.9% | **58.6%** |
402
- | right file in top 3 | 54.3% | **75.7%** |
403
- | lines it hands back, all questions | 11,833 | — |
404
- | characters returned, all questions | 1,122,902 | **626,022** |
402
+ | right file in top 3 | 54.3% | **77.1%** |
403
+ | lines it hands back, all questions | 11,959 | — |
404
+ | characters returned, all questions | 1,135,411 | **615,673** |
405
405
  | questions it answers | **61** | 60 |
406
406
 
407
407
  Those two tables are the whole argument of section 3.3, measured against a real
@@ -438,13 +438,15 @@ Four qualifications, because the table would otherwise flatter both sides:
438
438
  `the` gets every file back in no order. It is also why Grep declines nine of
439
439
  the seventy questions here — those had no word left that this corpus does not
440
440
  use everywhere.
441
- - **Payload is counted in characters on both sides.** On this repository Grep
442
- hands back 18,400 characters per question it answers, unranked and without
443
- spans, against 10,400 here, ranked and capped by `search.max_chars` — a
444
- factor of 1.8. On Flask it is a factor of 5.7 — 47,200 characters against
445
- 8,300 because a framework repeats its own vocabulary across many files and
446
- Grep has no way to rank what it finds. Both sides decline the same five of
447
- those 35 and they are the five Chinese ones, all of them. A Chinese word is
441
+ - **Payload is counted in characters on both sides.** Grep hands back 18,600
442
+ characters per question it answers here, unranked and without spans, against
443
+ 10,300 ranked and capped by `search.max_chars` — a factor of 1.8, and 5.5 on
444
+ Flask, where a framework repeats its vocabulary across many files and Grep
445
+ cannot rank what it finds. 1.4.1 changed what fits in that cap: the block had
446
+ been reprinting the docstring the code below already showed, 2,381 of 3,382
447
+ characters of prose header on Flask, so the same budget now carries **119
448
+ declarations instead of 92** there and 323 instead of 305 here. Both sides
449
+ decline the same five of those 35 — the five Chinese ones, all of them. A Chinese word is
448
450
  not a substring of English source and it is not a token in an index built
449
451
  from English source, so on a repository written in one language the cold
450
452
  cross-language case is not this tool's failure but the corpus's.
@@ -525,10 +527,10 @@ against a pinned corpus:
525
527
  | **C** own, described | 581 `978a1d48a82a` | **0.443 / 0.614 / 0.507** | 0.429 / 0.600 / 0.500 |
526
528
  | **D** silence, own / foreign | as above | 0.967 / 0.833 | 0.967 / 0.833 |
527
529
 
528
- **Worse or identical on every ruler.** It is shipped anyway, as an extra nobody
529
- has to install, because it does one thing the hash cannot do at all and these
530
- rulers cannot see: reach a unit that shares no word with the question. The
531
- pairs the hash scores exactly zero:
530
+ **Worse or identical on every ruler.** The 581-unit stamps are the corpus both
531
+ arms shared, kept rather than refreshed that is what a stamp is for. It ships
532
+ anyway because it does one thing the hash cannot and these rulers cannot see:
533
+ reach a unit sharing no word with the question. The pairs it scores zero on:
532
534
 
533
535
  | pair | signed hash | MiniLM |
534
536
  |---|---|---|
@@ -583,6 +585,14 @@ measured worse, so it stays off there.
583
585
  /reload-plugins
584
586
  ```
585
587
 
588
+ Updating needs the full id and a marketplace refresh first; the bare name is
589
+ refused with `Plugin "rag-your-code" not found`, which reads like it is gone:
590
+
591
+ ```bash
592
+ claude plugin marketplace update rag-your-code
593
+ claude plugin update rag-your-code@rag-your-code # then restart
594
+ ```
595
+
586
596
  Four commands and one skill. No hooks, no agents, no MCP server:
587
597
 
588
598
  | | |
@@ -730,6 +740,7 @@ Windows, plus a job that installs the built wheel into a clean environment and
730
740
  runs every documented command, and another that runs the skill's own install
731
741
  line verbatim.
732
742
 
743
+ - [docs/FLOW.md](docs/FLOW.md) — the whole thing in four diagrams
733
744
  - [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) — how each stage works and why
734
745
  - [docs/TESTING.md](docs/TESTING.md) — what the suites protect
735
746
  - [docs/ROADMAP.md](docs/ROADMAP.md) — what shipped, what was rejected and why
@@ -6,7 +6,7 @@ build-backend = "setuptools.build_meta"
6
6
 
7
7
  [project]
8
8
  name = "rag-your-code"
9
- version = "1.4.0"
9
+ version = "1.4.2"
10
10
  description = "A local, explainable RAG index for codebases and coding agents"
11
11
  readme = "README.md"
12
12
  requires-python = ">=3.10"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: rag-your-code
3
- Version: 1.4.0
3
+ Version: 1.4.2
4
4
  Summary: A local, explainable RAG index for codebases and coding agents
5
5
  Author: rag-your-code contributors
6
6
  License-Expression: MIT
@@ -295,11 +295,11 @@ repository had grown by ninety units.
295
295
  |---|---|---|---|---|---|
296
296
  | **A** Flask 3.1.3, no descriptions | what a first-time user gets | 35 | 0.200 | 0.286 | 0.238 |
297
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 |
298
+ | **C** this repo, agent-written descriptions | the warmest case supported | 70 | 0.443 | 0.614 | 0.509 |
299
299
 
300
300
  The corpora, without which none of the above is reproducible — **A** 1,572
301
- units, `5fd51169eacc`; **B** 581 units, `8e1e71942c1c`; **C** 581 units,
302
- `978a1d48a82a`. Ruler A grades **a copy of Flask 3.1.3 carried in this
301
+ units, `5fd51169eacc`; **B** 584 units, `fb1f841fa43a`; **C** 584 units,
302
+ `c9df00350cbd`. Ruler A grades **a copy of Flask 3.1.3 carried in this
303
303
  repository**, at [`benchmarks/corpus/flask`](benchmarks/corpus/flask), pinned
304
304
  to commit `22d9247`. Through 1.3.0 it graded a checkout on one machine, and
305
305
  that cost three things: two questions pointed at a declaration the subject had
@@ -330,10 +330,10 @@ Flask does not implement get through on exactly that.
330
330
 
331
331
  | gate | A hit@1/3/MRR | B hit@1/3/MRR | C hit@1/3/MRR | silence own / foreign |
332
332
  |---|---|---|---|---|
333
- | neither (pre-1.0.0) | 0.200/0.286/0.238 | 0.314/0.486/0.391 | 0.486/0.700/0.567 | 0.000 / 0.000 |
334
- | coverage only (1.0.0) | 0.200/0.286/0.238 | 0.314/0.471/0.383 | 0.486/0.686/0.562 | 0.600 / 0.733 |
335
- | concentration only | 0.200/0.286/0.238 | 0.314/0.471/0.383 | 0.443/0.614/0.507 | 0.967 / 0.800 |
336
- | **both (1.1.0)** | **0.200/0.286/0.238** | **0.314/0.471/0.383** | 0.443/0.614/0.507 | **0.967 / 0.833** |
333
+ | neither (pre-1.0.0) | 0.200/0.286/0.238 | 0.314/0.486/0.391 | 0.486/0.700/0.569 | 0.000 / 0.000 |
334
+ | coverage only (1.0.0) | 0.200/0.286/0.238 | 0.314/0.471/0.383 | 0.486/0.686/0.564 | 0.567 / 0.733 |
335
+ | concentration only | 0.200/0.286/0.238 | 0.314/0.471/0.383 | 0.443/0.614/0.509 | 0.967 / 0.800 |
336
+ | **both (1.1.0)** | **0.200/0.286/0.238** | **0.314/0.471/0.383** | 0.443/0.614/0.509 | **0.967 / 0.833** |
337
337
 
338
338
  Ruler A is **unmoved by either bar**, and B by concentration. The whole cost is
339
339
  three questions of seventy at hit@1 on the warmest ruler, and six at hit@3.
@@ -352,20 +352,20 @@ from 0.314 and C to 0.329 from 0.443. **0.28 was chosen before this corpus
352
352
  existed and survived meeting it**, which is the only kind of evidence a default
353
353
  can have.
354
354
 
355
- **Latency** — warm corpus, 581 units `978a1d48a82a`, five consecutive
355
+ **Latency** — warm corpus, 584 units `c9df00350cbd`, five consecutive
356
356
  invocations of `python -m benchmarks.query_latency --repeats 10` (420 samples
357
357
  each):
358
358
 
359
359
  | | | across the five |
360
360
  |---|---|---|
361
- | query, median | **1.0 ms** | 0.711.26 |
362
- | query, p95 | 1.7 ms | 1.23.7 |
363
- | refusing an unanswerable query | **0.029 ms** | 0.023 – 0.031 |
364
- | refusal cheaper than answering by | **~36×** | 3144 |
361
+ | query, median | **0.61 ms** | 0.600.64 |
362
+ | query, p95 | 1.09 ms | 1.061.14 |
363
+ | refusing an unanswerable query | **0.017 ms** | 0.015 – 0.019 |
364
+ | refusal cheaper than answering by | **~36×** | 3341 |
365
365
 
366
366
  Two significant figures and a spread, because that is the precision the
367
- measurement has. Across fifteen invocations over two releases on the same idle
368
- machine the median has landed anywhere from 0.62 to 1.44 ms and p95 from 1.09
367
+ measurement has. Across twenty invocations over three releases on the same idle
368
+ machine the median has landed anywhere from 0.51 to 1.44 ms and p95 from 0.85
369
369
  to 7.34 ms — a band wider than any change the code has ever made to this
370
370
  number. Releases before 1.3.0 published `0.83 ms / p95 1.68 ms` to three
371
371
  figures from a script that was never committed; both values sit inside that
@@ -420,17 +420,17 @@ framework reversed it. The honest claim is narrower than either table alone:
420
420
  | right file first | 22.9% | **37.1%** |
421
421
  | right file in top 3 | 45.7% | **57.1%** |
422
422
  | lines it hands back, all questions | 17,641 | — |
423
- | characters returned, all questions | 1,415,656 | **249,720** |
423
+ | characters returned, all questions | 1,415,656 | **258,236** |
424
424
  | questions it answers | 30 | 30 |
425
425
 
426
426
  **Once the vocabulary exists, it is not close.**
427
427
 
428
- | this repository · 70 questions · 581 units `978a1d48a82a` · 304 described | Grep loop | rag-your-code |
428
+ | this repository · 70 questions · 584 units `c9df00350cbd` · 304 described | Grep loop | rag-your-code |
429
429
  |---|---|---|
430
430
  | right file first | 22.9% | **58.6%** |
431
- | right file in top 3 | 54.3% | **75.7%** |
432
- | lines it hands back, all questions | 11,833 | — |
433
- | characters returned, all questions | 1,122,902 | **626,022** |
431
+ | right file in top 3 | 54.3% | **77.1%** |
432
+ | lines it hands back, all questions | 11,959 | — |
433
+ | characters returned, all questions | 1,135,411 | **615,673** |
434
434
  | questions it answers | **61** | 60 |
435
435
 
436
436
  Those two tables are the whole argument of section 3.3, measured against a real
@@ -467,13 +467,15 @@ Four qualifications, because the table would otherwise flatter both sides:
467
467
  `the` gets every file back in no order. It is also why Grep declines nine of
468
468
  the seventy questions here — those had no word left that this corpus does not
469
469
  use everywhere.
470
- - **Payload is counted in characters on both sides.** On this repository Grep
471
- hands back 18,400 characters per question it answers, unranked and without
472
- spans, against 10,400 here, ranked and capped by `search.max_chars` — a
473
- factor of 1.8. On Flask it is a factor of 5.7 — 47,200 characters against
474
- 8,300 because a framework repeats its own vocabulary across many files and
475
- Grep has no way to rank what it finds. Both sides decline the same five of
476
- those 35 and they are the five Chinese ones, all of them. A Chinese word is
470
+ - **Payload is counted in characters on both sides.** Grep hands back 18,600
471
+ characters per question it answers here, unranked and without spans, against
472
+ 10,300 ranked and capped by `search.max_chars` — a factor of 1.8, and 5.5 on
473
+ Flask, where a framework repeats its vocabulary across many files and Grep
474
+ cannot rank what it finds. 1.4.1 changed what fits in that cap: the block had
475
+ been reprinting the docstring the code below already showed, 2,381 of 3,382
476
+ characters of prose header on Flask, so the same budget now carries **119
477
+ declarations instead of 92** there and 323 instead of 305 here. Both sides
478
+ decline the same five of those 35 — the five Chinese ones, all of them. A Chinese word is
477
479
  not a substring of English source and it is not a token in an index built
478
480
  from English source, so on a repository written in one language the cold
479
481
  cross-language case is not this tool's failure but the corpus's.
@@ -554,10 +556,10 @@ against a pinned corpus:
554
556
  | **C** own, described | 581 `978a1d48a82a` | **0.443 / 0.614 / 0.507** | 0.429 / 0.600 / 0.500 |
555
557
  | **D** silence, own / foreign | as above | 0.967 / 0.833 | 0.967 / 0.833 |
556
558
 
557
- **Worse or identical on every ruler.** It is shipped anyway, as an extra nobody
558
- has to install, because it does one thing the hash cannot do at all and these
559
- rulers cannot see: reach a unit that shares no word with the question. The
560
- pairs the hash scores exactly zero:
559
+ **Worse or identical on every ruler.** The 581-unit stamps are the corpus both
560
+ arms shared, kept rather than refreshed that is what a stamp is for. It ships
561
+ anyway because it does one thing the hash cannot and these rulers cannot see:
562
+ reach a unit sharing no word with the question. The pairs it scores zero on:
561
563
 
562
564
  | pair | signed hash | MiniLM |
563
565
  |---|---|---|
@@ -612,6 +614,14 @@ measured worse, so it stays off there.
612
614
  /reload-plugins
613
615
  ```
614
616
 
617
+ Updating needs the full id and a marketplace refresh first; the bare name is
618
+ refused with `Plugin "rag-your-code" not found`, which reads like it is gone:
619
+
620
+ ```bash
621
+ claude plugin marketplace update rag-your-code
622
+ claude plugin update rag-your-code@rag-your-code # then restart
623
+ ```
624
+
615
625
  Four commands and one skill. No hooks, no agents, no MCP server:
616
626
 
617
627
  | | |
@@ -759,6 +769,7 @@ Windows, plus a job that installs the built wheel into a clean environment and
759
769
  runs every documented command, and another that runs the skill's own install
760
770
  line verbatim.
761
771
 
772
+ - [docs/FLOW.md](docs/FLOW.md) — the whole thing in four diagrams
762
773
  - [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) — how each stage works and why
763
774
  - [docs/TESTING.md](docs/TESTING.md) — what the suites protect
764
775
  - [docs/ROADMAP.md](docs/ROADMAP.md) — what shipped, what was rejected and why
@@ -3,4 +3,4 @@
3
3
  from .models import CodeUnit, SearchResult
4
4
 
5
5
  __all__ = ["CodeUnit", "SearchResult"]
6
- __version__ = "1.4.0"
6
+ __version__ = "1.4.2"
@@ -5,6 +5,14 @@ from __future__ import annotations
5
5
  import ast
6
6
  import re
7
7
 
8
+ # Where a generated description stops describing the signature and starts
9
+ # quoting what the author wrote. Three modules needed to recognise it and all
10
+ # three spelled it out: this one writes it, the parser writes it again for the
11
+ # other fourteen languages, and `document` looked for it. A string literal
12
+ # duplicated across modules that must agree is a rename away from a silent
13
+ # disagreement.
14
+ DOCUMENTED_MARKER = "Documented intent:"
15
+
8
16
 
9
17
  def _humanize(name: str) -> str:
10
18
  """Turns a programmer identifier into ordinary words: splits camelCase
@@ -37,7 +45,7 @@ def describe_python(node: ast.AST, source: str, calls: list[str], imports: list[
37
45
  pieces.append("using " + ", ".join(imports[:8]))
38
46
  doc = ast.get_docstring(node)
39
47
  if doc:
40
- pieces.append("Documented intent: " + " ".join(doc.split()))
48
+ pieces.append(f"{DOCUMENTED_MARKER} " + " ".join(doc.split()))
41
49
  return ". ".join(pieces) + "."
42
50
 
43
51
 
@@ -26,6 +26,7 @@ import re
26
26
  from dataclasses import dataclass
27
27
  from pathlib import Path
28
28
 
29
+ from .annotate import DOCUMENTED_MARKER
29
30
  from .descriptions import DescriptionStore
30
31
  from .models import CodeUnit
31
32
  from .parser import EXTENSIONS, parse_file
@@ -59,7 +60,6 @@ WRAP_WIDTH = 76
59
60
  # rewritten read as undocumented. That mistake proposed a hundred and seven
60
61
  # insertions against a repository with seventeen genuinely undocumented
61
62
  # declarations.
62
- DOCUMENTED_MARKER = "Documented intent:"
63
63
  _CJK = re.compile(r"[一-鿿]")
64
64
 
65
65
 
@@ -26,7 +26,7 @@ import re
26
26
  from dataclasses import dataclass
27
27
  from pathlib import Path
28
28
 
29
- from .annotate import describe_python
29
+ from .annotate import DOCUMENTED_MARKER, describe_python
30
30
  from .models import CodeUnit
31
31
 
32
32
  PARSER_VERSION = "3"
@@ -542,7 +542,7 @@ def _generic_units(path: Path, source: str, relative: str, language: str) -> lis
542
542
  if documented:
543
543
  # Phrased exactly as the Python path phrases a docstring, so the
544
544
  # two routes produce the same shape of text for the same thing.
545
- description += f" Documented intent: {documented}"
545
+ description += f" {DOCUMENTED_MARKER} {documented}"
546
546
  parent = dotted.rsplit(".", 1)[0] if "." in dotted else None
547
547
  units.append(
548
548
  CodeUnit(
@@ -8,6 +8,7 @@ from bisect import bisect_left
8
8
  from collections import Counter, defaultdict
9
9
  from dataclasses import dataclass
10
10
 
11
+ from .annotate import DOCUMENTED_MARKER
11
12
  from .config import BY_PATH
12
13
  from .embeddings import DEFAULT_DIMENSIONS, LocalEmbedder, tokenize
13
14
  from .models import CodeUnit, SearchResult
@@ -549,13 +550,47 @@ def search(
549
550
  ]
550
551
 
551
552
 
553
+ def _visible_description(unit: CodeUnit) -> str:
554
+ """The description minus whatever the code printed under it already says.
555
+
556
+ A generated description ends with the author's own docstring, because that
557
+ is how a docstring becomes searchable -- and the source below the block
558
+ then prints it a second time. Measured on a repository whose author wrote
559
+ them, 2,381 of 3,382 characters of prose header were a verbatim repeat of
560
+ the code beneath it: a fifth of everything a query returned, paid for
561
+ twice. Retrieval is the half of RAG that has to fit in a context window,
562
+ so this is not a cosmetic saving.
563
+
564
+ Only the rendering drops it. ``searchable_text`` keeps the whole thing, so
565
+ no ruler moves and the docstring stays exactly as findable as it was.
566
+
567
+ The quoted docstring is re-flowed onto one line where the source has it
568
+ indented across many, so the test is against whitespace-collapsed source
569
+ rather than against the source itself. An authored description the source
570
+ does not carry is kept: it is the one part a reader cannot get from the
571
+ code.
572
+ """
573
+ head, marker, documented = unit.description.partition(DOCUMENTED_MARKER)
574
+ if not marker:
575
+ return unit.description
576
+ quoted = " ".join(documented.split())
577
+ # `describe_python` joins its pieces with ". " and closes with ".", so a
578
+ # docstring that already ended in one arrives here with two. Matching on
579
+ # the raw text found nothing at all on the first repository tried.
580
+ trimmed = quoted[:-1] if quoted.endswith(".") else quoted
581
+ body = " ".join(unit.source.split())
582
+ if quoted in body or trimmed in body:
583
+ return head.rstrip()
584
+ return unit.description
585
+
586
+
552
587
  def _block(result: SearchResult) -> str:
553
588
  """One result as an agent reads it: identifier, score, why it matched,
554
589
  what it is, and the code itself.
555
590
  """
556
591
  unit = result.unit
557
592
  evidence = "\nEvidence: " + " | ".join(result.evidence) if result.evidence else ""
558
- return f"[{unit.id}] score={result.score:.3f}{evidence}\n{unit.description}\n```{unit.language}\n{unit.source}\n```"
593
+ return f"[{unit.id}] score={result.score:.3f}{evidence}\n{_visible_description(unit)}\n```{unit.language}\n{unit.source}\n```"
559
594
 
560
595
 
561
596
  def within_budget(results: list[SearchResult], max_chars: int) -> list[SearchResult]:
@@ -18,6 +18,7 @@ present anywhere in the unit -- ranks the *wrong* answer first.
18
18
 
19
19
  from __future__ import annotations
20
20
 
21
+ import dataclasses
21
22
  from pathlib import Path
22
23
 
23
24
  from ragyourcode.indexer import build_units
@@ -200,3 +201,56 @@ def test_a_field_every_unit_leaves_empty_does_not_divide_by_zero():
200
201
  )
201
202
  index = build_search_index([unit])
202
203
  assert search([unit], "nothing", limit=1, search_index=index)[0].unit.id == unit.id
204
+
205
+
206
+ def test_a_block_does_not_reprint_the_docstring_the_code_below_it_shows(tmp_path: Path):
207
+ """The author's own words reached the block twice: once quoted into the
208
+ generated description so they are searchable, once in the source.
209
+
210
+ Measured on a repository whose author wrote them, 2,381 of 3,382
211
+ characters of prose header were a verbatim repeat of the code beneath it.
212
+ At a fixed budget that is answers crowded out by their own duplicate: the
213
+ same twelve thousand characters carried 92 declarations before this and
214
+ 119 after.
215
+
216
+ Both halves are asserted. Dropping the quote must not drop what only the
217
+ header says, and it must not touch what the index can find -- the docstring
218
+ stays in `searchable_text`, which is why no ruler moves.
219
+ """
220
+ (tmp_path / "billing.py").write_text(
221
+ 'def charge(amount):\n'
222
+ ' """Charge the amount against the stored card and return a receipt."""\n'
223
+ ' return amount\n',
224
+ encoding="utf-8",
225
+ )
226
+ unit = build_units(tmp_path)[0]
227
+ index = build_search_index([unit])
228
+ rendered = _block(search([unit], "charge the amount", search_index=index, limit=1)[0])
229
+
230
+ quoted = "Charge the amount against the stored card and return a receipt."
231
+ assert rendered.count(quoted) == 1, "the docstring is printed once, by the code"
232
+ assert rendered.index(quoted) > rendered.index("```"), "the surviving copy is the one in the source"
233
+ assert "This method charge" in rendered, "the generated half is not in the source and must stay"
234
+ assert quoted in unit.searchable_text, "dropping it from the block must not drop it from the index"
235
+
236
+
237
+ def test_a_written_description_the_source_does_not_carry_survives(tmp_path: Path):
238
+ """The rule is "the code already shows it", not "it came after the marker".
239
+
240
+ An authored description is the one part of a block a reader cannot recover
241
+ by reading the code, so a rule that keyed on the marker alone would delete
242
+ exactly the text this project spends tokens to produce.
243
+ """
244
+ (tmp_path / "billing.py").write_text(
245
+ 'def charge(amount):\n'
246
+ ' """Charge the card."""\n'
247
+ ' return amount\n',
248
+ encoding="utf-8",
249
+ )
250
+ unit = build_units(tmp_path)[0]
251
+ unit = dataclasses.replace(
252
+ unit, description="This method charge. Documented intent: retries a declined authorisation once."
253
+ )
254
+ index = build_search_index([unit])
255
+ rendered = _block(search([unit], "charge the amount", search_index=index, limit=1)[0])
256
+ assert "retries a declined authorisation once." in rendered
File without changes
File without changes