rag-your-code 0.4.2__tar.gz → 0.5.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 (43) hide show
  1. {rag_your_code-0.4.2/src/rag_your_code.egg-info → rag_your_code-0.5.0}/PKG-INFO +43 -15
  2. {rag_your_code-0.4.2 → rag_your_code-0.5.0}/README.md +42 -14
  3. {rag_your_code-0.4.2 → rag_your_code-0.5.0}/pyproject.toml +1 -1
  4. {rag_your_code-0.4.2 → rag_your_code-0.5.0/src/rag_your_code.egg-info}/PKG-INFO +43 -15
  5. {rag_your_code-0.4.2 → rag_your_code-0.5.0}/src/rag_your_code.egg-info/SOURCES.txt +4 -0
  6. {rag_your_code-0.4.2 → rag_your_code-0.5.0}/src/ragyourcode/__init__.py +1 -1
  7. {rag_your_code-0.4.2 → rag_your_code-0.5.0}/src/ragyourcode/agentic.py +7 -0
  8. {rag_your_code-0.4.2 → rag_your_code-0.5.0}/src/ragyourcode/annotate.py +11 -0
  9. {rag_your_code-0.4.2 → rag_your_code-0.5.0}/src/ragyourcode/cli.py +100 -9
  10. {rag_your_code-0.4.2 → rag_your_code-0.5.0}/src/ragyourcode/config.py +51 -0
  11. {rag_your_code-0.4.2 → rag_your_code-0.5.0}/src/ragyourcode/descriptions.py +125 -11
  12. rag_your_code-0.5.0/src/ragyourcode/document.py +261 -0
  13. {rag_your_code-0.4.2 → rag_your_code-0.5.0}/src/ragyourcode/embeddings.py +22 -0
  14. {rag_your_code-0.4.2 → rag_your_code-0.5.0}/src/ragyourcode/graph.py +40 -0
  15. {rag_your_code-0.4.2 → rag_your_code-0.5.0}/src/ragyourcode/indexer.py +96 -17
  16. {rag_your_code-0.4.2 → rag_your_code-0.5.0}/src/ragyourcode/models.py +23 -0
  17. {rag_your_code-0.4.2 → rag_your_code-0.5.0}/src/ragyourcode/parser.py +148 -0
  18. {rag_your_code-0.4.2 → rag_your_code-0.5.0}/src/ragyourcode/search.py +20 -0
  19. {rag_your_code-0.4.2 → rag_your_code-0.5.0}/tests/test_config.py +1 -1
  20. {rag_your_code-0.4.2 → rag_your_code-0.5.0}/tests/test_descriptions.py +5 -1
  21. rag_your_code-0.5.0/tests/test_doc_comments.py +215 -0
  22. rag_your_code-0.5.0/tests/test_document.py +211 -0
  23. rag_your_code-0.5.0/tests/test_repo_queries.py +72 -0
  24. {rag_your_code-0.4.2 → rag_your_code-0.5.0}/LICENSE +0 -0
  25. {rag_your_code-0.4.2 → rag_your_code-0.5.0}/setup.cfg +0 -0
  26. {rag_your_code-0.4.2 → rag_your_code-0.5.0}/src/rag_your_code.egg-info/dependency_links.txt +0 -0
  27. {rag_your_code-0.4.2 → rag_your_code-0.5.0}/src/rag_your_code.egg-info/entry_points.txt +0 -0
  28. {rag_your_code-0.4.2 → rag_your_code-0.5.0}/src/rag_your_code.egg-info/requires.txt +0 -0
  29. {rag_your_code-0.4.2 → rag_your_code-0.5.0}/src/rag_your_code.egg-info/top_level.txt +0 -0
  30. {rag_your_code-0.4.2 → rag_your_code-0.5.0}/src/ragyourcode/py.typed +0 -0
  31. {rag_your_code-0.4.2 → rag_your_code-0.5.0}/tests/test_agent_protocol.py +0 -0
  32. {rag_your_code-0.4.2 → rag_your_code-0.5.0}/tests/test_agentic.py +0 -0
  33. {rag_your_code-0.4.2 → rag_your_code-0.5.0}/tests/test_e2e_cli.py +0 -0
  34. {rag_your_code-0.4.2 → rag_your_code-0.5.0}/tests/test_golden.py +0 -0
  35. {rag_your_code-0.4.2 → rag_your_code-0.5.0}/tests/test_graph_incremental.py +0 -0
  36. {rag_your_code-0.4.2 → rag_your_code-0.5.0}/tests/test_language_fixtures.py +0 -0
  37. {rag_your_code-0.4.2 → rag_your_code-0.5.0}/tests/test_large_repo.py +0 -0
  38. {rag_your_code-0.4.2 → rag_your_code-0.5.0}/tests/test_metadata.py +0 -0
  39. {rag_your_code-0.4.2 → rag_your_code-0.5.0}/tests/test_multilanguage.py +0 -0
  40. {rag_your_code-0.4.2 → rag_your_code-0.5.0}/tests/test_parser_edges.py +0 -0
  41. {rag_your_code-0.4.2 → rag_your_code-0.5.0}/tests/test_ragyourcode.py +0 -0
  42. {rag_your_code-0.4.2 → rag_your_code-0.5.0}/tests/test_resilience.py +0 -0
  43. {rag_your_code-0.4.2 → rag_your_code-0.5.0}/tests/test_retrieval_correctness.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: rag-your-code
3
- Version: 0.4.2
3
+ Version: 0.5.0
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
@@ -153,7 +153,15 @@ callees listed, the docstring appended. It introduces no vocabulary the source
153
153
  did not already have — which is exactly why retrieval cannot reach a concept
154
154
  nobody wrote down.
155
155
 
156
- The agent already reading the index can supply those words:
156
+ **First, the documentation you already wrote is indexed.** Fourteen of the
157
+ fifteen supported languages put documentation immediately above a declaration
158
+ — JSDoc, Javadoc, KDoc, rustdoc, Go doc comments, XML doc comments, PHPDoc —
159
+ and a unit's span begins at the declaration, so all of it used to sit outside
160
+ the index. The same sentence reached thirteen searchable words as a Python
161
+ docstring and two as a JavaScript comment. Now both reach thirteen. Commented-
162
+ out code, separator rules and licence headers are deliberately left out.
163
+
164
+ **Where there is none, the agent can write it:**
157
165
 
158
166
  ```bash
159
167
  rag-your-code describe status # coverage, and what is pending
@@ -165,23 +173,43 @@ rag-your-code index . # apply it
165
173
  or, in the protocol, `describe_pending` and `describe_put` — which take effect
166
174
  in the same session, with no refresh.
167
175
 
176
+ **And you can move it into the code**, where it needs no bookkeeping at all:
177
+
178
+ ```bash
179
+ rag-your-code describe promote | git apply # review it first
180
+ ```
181
+
182
+ That emits a unified diff adding a doc comment in each language's own
183
+ convention, for declarations that have none. The tool still never writes your
184
+ source. Only the half meant for a reader is promoted, so a bilingual
185
+ description leaves its second language in the store where retrieval still uses
186
+ it — measured, promoting all 68 on this repository discarded no description
187
+ and left Chinese retrieval unchanged.
188
+
168
189
  ### Measured on this repository
169
190
 
170
- This project describes its own implementation: all 120 units under `src/`
171
- carry an agent-written bilingual description, committed to the repo. Ten
172
- natural-language questions about the codebase, before and after:
191
+ This project describes its own implementation: every unit under `src/` carries
192
+ an agent-written bilingual description, committed to the repo, and 68 of them
193
+ have been promoted into the source as doc comments.
173
194
 
174
- | | before | after |
195
+ Seventy natural-language questions about this codebase, in English and
196
+ Chinese, each listing every unit that genuinely answers it
197
+ ([`benchmarks/repo_queries.json`](benchmarks/repo_queries.json)):
198
+
199
+ | | generated descriptions | agent-written |
175
200
  |---|---|---|
176
- | top result in the expected file | 2 / 8 | **6 / 8** |
177
- | Chinese-language queries | 0 / 4 | **3 / 4** |
178
- | queries with **no shared word at all** (pure fallback) | 4 | **0** |
201
+ | hit@1 | 0.171 | **0.500** |
202
+ | hit@3 | 0.314 | **0.729** |
203
+ | MRR | 0.240 | **0.605** |
204
+ | answered with no shared word at all | 15.7% | **0%** |
205
+
206
+ Roughly a threefold improvement in first-place accuracy. Nineteen questions
207
+ still fail, which is what makes the set usable for measuring the next change;
208
+ `tests/test_repo_queries.py` asserts that some question always does.
179
209
 
180
- Two still miss, and both are worth stating. One query says `catastrophic
181
- backtracking` where the description says `backtracks catastrophically`: there
182
- is no stemming, so those share no word — exactly the limit documented above.
183
- The other returned a unit that answers the question from a different file than
184
- predicted, so the expectation was wrong rather than the retrieval.
210
+ One failure is worth naming: a query saying `catastrophic backtracking` does
211
+ not reach a description saying `backtracks catastrophically`. There is no
212
+ stemming — exactly the limit documented above.
185
213
 
186
214
  **What this is:** it moves the semantic work from query time to index time.
187
215
  Matching stays lexical. It is LLM-authored keyword expansion, and its reach is
@@ -230,7 +258,7 @@ Directional local measurements, not service levels; the archived run is
230
258
  **Suite:** Python 3.10 – 3.13 on Linux and Windows, plus a job that installs
231
259
  the built wheel into a clean environment and runs every command the
232
260
  documentation prescribes, and another that runs the skill's own install line
233
- verbatim. 208 tests as of 0.4.2 — the count is version-stamped rather than
261
+ verbatim. 248 tests as of 0.5.0 — the count is version-stamped rather than
234
262
  maintained, because a bare figure in a living document is a claim that rots;
235
263
  per-release counts are in [CHANGELOG.md](CHANGELOG.md).
236
264
 
@@ -126,7 +126,15 @@ callees listed, the docstring appended. It introduces no vocabulary the source
126
126
  did not already have — which is exactly why retrieval cannot reach a concept
127
127
  nobody wrote down.
128
128
 
129
- The agent already reading the index can supply those words:
129
+ **First, the documentation you already wrote is indexed.** Fourteen of the
130
+ fifteen supported languages put documentation immediately above a declaration
131
+ — JSDoc, Javadoc, KDoc, rustdoc, Go doc comments, XML doc comments, PHPDoc —
132
+ and a unit's span begins at the declaration, so all of it used to sit outside
133
+ the index. The same sentence reached thirteen searchable words as a Python
134
+ docstring and two as a JavaScript comment. Now both reach thirteen. Commented-
135
+ out code, separator rules and licence headers are deliberately left out.
136
+
137
+ **Where there is none, the agent can write it:**
130
138
 
131
139
  ```bash
132
140
  rag-your-code describe status # coverage, and what is pending
@@ -138,23 +146,43 @@ rag-your-code index . # apply it
138
146
  or, in the protocol, `describe_pending` and `describe_put` — which take effect
139
147
  in the same session, with no refresh.
140
148
 
149
+ **And you can move it into the code**, where it needs no bookkeeping at all:
150
+
151
+ ```bash
152
+ rag-your-code describe promote | git apply # review it first
153
+ ```
154
+
155
+ That emits a unified diff adding a doc comment in each language's own
156
+ convention, for declarations that have none. The tool still never writes your
157
+ source. Only the half meant for a reader is promoted, so a bilingual
158
+ description leaves its second language in the store where retrieval still uses
159
+ it — measured, promoting all 68 on this repository discarded no description
160
+ and left Chinese retrieval unchanged.
161
+
141
162
  ### Measured on this repository
142
163
 
143
- This project describes its own implementation: all 120 units under `src/`
144
- carry an agent-written bilingual description, committed to the repo. Ten
145
- natural-language questions about the codebase, before and after:
164
+ This project describes its own implementation: every unit under `src/` carries
165
+ an agent-written bilingual description, committed to the repo, and 68 of them
166
+ have been promoted into the source as doc comments.
146
167
 
147
- | | before | after |
168
+ Seventy natural-language questions about this codebase, in English and
169
+ Chinese, each listing every unit that genuinely answers it
170
+ ([`benchmarks/repo_queries.json`](benchmarks/repo_queries.json)):
171
+
172
+ | | generated descriptions | agent-written |
148
173
  |---|---|---|
149
- | top result in the expected file | 2 / 8 | **6 / 8** |
150
- | Chinese-language queries | 0 / 4 | **3 / 4** |
151
- | queries with **no shared word at all** (pure fallback) | 4 | **0** |
174
+ | hit@1 | 0.171 | **0.500** |
175
+ | hit@3 | 0.314 | **0.729** |
176
+ | MRR | 0.240 | **0.605** |
177
+ | answered with no shared word at all | 15.7% | **0%** |
178
+
179
+ Roughly a threefold improvement in first-place accuracy. Nineteen questions
180
+ still fail, which is what makes the set usable for measuring the next change;
181
+ `tests/test_repo_queries.py` asserts that some question always does.
152
182
 
153
- Two still miss, and both are worth stating. One query says `catastrophic
154
- backtracking` where the description says `backtracks catastrophically`: there
155
- is no stemming, so those share no word — exactly the limit documented above.
156
- The other returned a unit that answers the question from a different file than
157
- predicted, so the expectation was wrong rather than the retrieval.
183
+ One failure is worth naming: a query saying `catastrophic backtracking` does
184
+ not reach a description saying `backtracks catastrophically`. There is no
185
+ stemming — exactly the limit documented above.
158
186
 
159
187
  **What this is:** it moves the semantic work from query time to index time.
160
188
  Matching stays lexical. It is LLM-authored keyword expansion, and its reach is
@@ -203,7 +231,7 @@ Directional local measurements, not service levels; the archived run is
203
231
  **Suite:** Python 3.10 – 3.13 on Linux and Windows, plus a job that installs
204
232
  the built wheel into a clean environment and runs every command the
205
233
  documentation prescribes, and another that runs the skill's own install line
206
- verbatim. 208 tests as of 0.4.2 — the count is version-stamped rather than
234
+ verbatim. 248 tests as of 0.5.0 — the count is version-stamped rather than
207
235
  maintained, because a bare figure in a living document is a claim that rots;
208
236
  per-release counts are in [CHANGELOG.md](CHANGELOG.md).
209
237
 
@@ -6,7 +6,7 @@ build-backend = "setuptools.build_meta"
6
6
 
7
7
  [project]
8
8
  name = "rag-your-code"
9
- version = "0.4.2"
9
+ version = "0.5.0"
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: 0.4.2
3
+ Version: 0.5.0
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
@@ -153,7 +153,15 @@ callees listed, the docstring appended. It introduces no vocabulary the source
153
153
  did not already have — which is exactly why retrieval cannot reach a concept
154
154
  nobody wrote down.
155
155
 
156
- The agent already reading the index can supply those words:
156
+ **First, the documentation you already wrote is indexed.** Fourteen of the
157
+ fifteen supported languages put documentation immediately above a declaration
158
+ — JSDoc, Javadoc, KDoc, rustdoc, Go doc comments, XML doc comments, PHPDoc —
159
+ and a unit's span begins at the declaration, so all of it used to sit outside
160
+ the index. The same sentence reached thirteen searchable words as a Python
161
+ docstring and two as a JavaScript comment. Now both reach thirteen. Commented-
162
+ out code, separator rules and licence headers are deliberately left out.
163
+
164
+ **Where there is none, the agent can write it:**
157
165
 
158
166
  ```bash
159
167
  rag-your-code describe status # coverage, and what is pending
@@ -165,23 +173,43 @@ rag-your-code index . # apply it
165
173
  or, in the protocol, `describe_pending` and `describe_put` — which take effect
166
174
  in the same session, with no refresh.
167
175
 
176
+ **And you can move it into the code**, where it needs no bookkeeping at all:
177
+
178
+ ```bash
179
+ rag-your-code describe promote | git apply # review it first
180
+ ```
181
+
182
+ That emits a unified diff adding a doc comment in each language's own
183
+ convention, for declarations that have none. The tool still never writes your
184
+ source. Only the half meant for a reader is promoted, so a bilingual
185
+ description leaves its second language in the store where retrieval still uses
186
+ it — measured, promoting all 68 on this repository discarded no description
187
+ and left Chinese retrieval unchanged.
188
+
168
189
  ### Measured on this repository
169
190
 
170
- This project describes its own implementation: all 120 units under `src/`
171
- carry an agent-written bilingual description, committed to the repo. Ten
172
- natural-language questions about the codebase, before and after:
191
+ This project describes its own implementation: every unit under `src/` carries
192
+ an agent-written bilingual description, committed to the repo, and 68 of them
193
+ have been promoted into the source as doc comments.
173
194
 
174
- | | before | after |
195
+ Seventy natural-language questions about this codebase, in English and
196
+ Chinese, each listing every unit that genuinely answers it
197
+ ([`benchmarks/repo_queries.json`](benchmarks/repo_queries.json)):
198
+
199
+ | | generated descriptions | agent-written |
175
200
  |---|---|---|
176
- | top result in the expected file | 2 / 8 | **6 / 8** |
177
- | Chinese-language queries | 0 / 4 | **3 / 4** |
178
- | queries with **no shared word at all** (pure fallback) | 4 | **0** |
201
+ | hit@1 | 0.171 | **0.500** |
202
+ | hit@3 | 0.314 | **0.729** |
203
+ | MRR | 0.240 | **0.605** |
204
+ | answered with no shared word at all | 15.7% | **0%** |
205
+
206
+ Roughly a threefold improvement in first-place accuracy. Nineteen questions
207
+ still fail, which is what makes the set usable for measuring the next change;
208
+ `tests/test_repo_queries.py` asserts that some question always does.
179
209
 
180
- Two still miss, and both are worth stating. One query says `catastrophic
181
- backtracking` where the description says `backtracks catastrophically`: there
182
- is no stemming, so those share no word — exactly the limit documented above.
183
- The other returned a unit that answers the question from a different file than
184
- predicted, so the expectation was wrong rather than the retrieval.
210
+ One failure is worth naming: a query saying `catastrophic backtracking` does
211
+ not reach a description saying `backtracks catastrophically`. There is no
212
+ stemming — exactly the limit documented above.
185
213
 
186
214
  **What this is:** it moves the semantic work from query time to index time.
187
215
  Matching stays lexical. It is LLM-authored keyword expansion, and its reach is
@@ -230,7 +258,7 @@ Directional local measurements, not service levels; the archived run is
230
258
  **Suite:** Python 3.10 – 3.13 on Linux and Windows, plus a job that installs
231
259
  the built wheel into a clean environment and runs every command the
232
260
  documentation prescribes, and another that runs the skill's own install line
233
- verbatim. 208 tests as of 0.4.2 — the count is version-stamped rather than
261
+ verbatim. 248 tests as of 0.5.0 — the count is version-stamped rather than
234
262
  maintained, because a bare figure in a living document is a claim that rots;
235
263
  per-release counts are in [CHANGELOG.md](CHANGELOG.md).
236
264
 
@@ -13,6 +13,7 @@ src/ragyourcode/annotate.py
13
13
  src/ragyourcode/cli.py
14
14
  src/ragyourcode/config.py
15
15
  src/ragyourcode/descriptions.py
16
+ src/ragyourcode/document.py
16
17
  src/ragyourcode/embeddings.py
17
18
  src/ragyourcode/graph.py
18
19
  src/ragyourcode/indexer.py
@@ -24,6 +25,8 @@ tests/test_agent_protocol.py
24
25
  tests/test_agentic.py
25
26
  tests/test_config.py
26
27
  tests/test_descriptions.py
28
+ tests/test_doc_comments.py
29
+ tests/test_document.py
27
30
  tests/test_e2e_cli.py
28
31
  tests/test_golden.py
29
32
  tests/test_graph_incremental.py
@@ -33,5 +36,6 @@ tests/test_metadata.py
33
36
  tests/test_multilanguage.py
34
37
  tests/test_parser_edges.py
35
38
  tests/test_ragyourcode.py
39
+ tests/test_repo_queries.py
36
40
  tests/test_resilience.py
37
41
  tests/test_retrieval_correctness.py
@@ -3,4 +3,4 @@
3
3
  from .models import CodeUnit, SearchResult
4
4
 
5
5
  __all__ = ["CodeUnit", "SearchResult"]
6
- __version__ = "0.4.2"
6
+ __version__ = "0.5.0"
@@ -8,10 +8,17 @@ from .search import DEFAULT_VECTOR_WEIGHT, SearchIndex, search
8
8
 
9
9
 
10
10
  def _result_ids(results: list[SearchResult]) -> set[str]:
11
+ """Collects the unit identifiers out of a list of search results, so two
12
+ result sets can be compared for overlap or novelty. Used to decide
13
+ whether a second retrieval round actually surfaced anything new.
14
+ """
11
15
  return {result.unit.id for result in results}
12
16
 
13
17
 
14
18
  def _serialize(results: list[SearchResult]) -> list[dict]:
19
+ """Converts search results into plain JSON-ready dictionaries for the agent
20
+ protocol reply.
21
+ """
15
22
  return [result.to_dict() for result in results]
16
23
 
17
24
 
@@ -7,11 +7,22 @@ import re
7
7
 
8
8
 
9
9
  def _humanize(name: str) -> str:
10
+ """Turns a programmer identifier into ordinary words: splits camelCase
11
+ apart, replaces underscores with spaces, lowercases the result. Empty
12
+ input becomes a placeholder rather than an empty string.
13
+ """
10
14
  words = re.sub(r"([a-z0-9])([A-Z])", r"\1 \2", name).replace("_", " ").split()
11
15
  return " ".join(words).strip().lower() or "anonymous unit"
12
16
 
13
17
 
14
18
  def describe_python(node: ast.AST, source: str, calls: list[str], imports: list[str]) -> str:
19
+ """Builds a readable sentence about a Python function or class without
20
+ using a language model: the humanised name, the arguments it accepts,
21
+ the functions it calls, the modules it uses, and the docstring appended
22
+ verbatim as stated intent. Because it only rearranges words already in
23
+ the source, it adds no vocabulary the code did not have, which is why
24
+ retrieval cannot reach a concept nobody wrote down.
25
+ """
15
26
  name = getattr(node, "name", "anonymous")
16
27
  kind = "method" if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)) else "class"
17
28
  args = []
@@ -14,9 +14,10 @@ from .annotate import comment_for
14
14
  from .agentic import research
15
15
  from .config import BY_PATH, SETTINGS, Config, ConfigError
16
16
  from .descriptions import DescriptionStore, guidance, index_descriptions_fingerprint
17
+ from .document import plan as plan_documentation, render_patch, summarise as summarise_documentation
17
18
  from .embeddings import embed, embedding_metadata
18
19
  from .graph import build_graph, graph_from_dict, graph_search
19
- from .indexer import StaleMonitor, build_units, fingerprint, index_config_fingerprint, read_index, snapshot_repository, write_index
20
+ from .indexer import StaleMonitor, build_fingerprint, build_units, fingerprint, index_build_fingerprint, read_index, snapshot_repository, write_index
20
21
  from .search import build_search_index, context, search
21
22
 
22
23
  # Derived from the settings table so the default is written down once.
@@ -26,10 +27,24 @@ MAX_OPEN_CHARS = BY_PATH["agent.max_open_chars"].default
26
27
 
27
28
 
28
29
  def _default_index(root: Path) -> Path:
30
+ """Where a repository index file is kept when the caller names no other
31
+ location.
32
+ """
29
33
  return root / ".rag-your-code" / "index.json"
30
34
 
31
35
 
32
36
  def _refresh_index(root: Path, output: Path, full: bool = False, compact: bool | None = None, cfg: Config | None = None) -> dict:
37
+ """Builds or rebuilds a repository index and publishes it, reusing the
38
+ previous one where it can. It works out up front whether the rules that
39
+ decide what a unit is have changed — the indexing settings, the vector
40
+ width, or the parser itself — and when they have it discards the
41
+ previous work rather than reusing it, so the report can honestly say
42
+ whether reuse happened. That report previously claimed reuse on exactly
43
+ the runs that had rebuilt everything, because it was computed from
44
+ whether a previous index existed rather than from whether its units were
45
+ kept. It also applies the written descriptions and reports how many
46
+ units still have none.
47
+ """
33
48
  cfg = cfg if cfg is not None else config_module.load(root)
34
49
  previous_payload: dict = {}
35
50
  previous_units = []
@@ -48,9 +63,9 @@ def _refresh_index(root: Path, output: Path, full: bool = False, compact: bool |
48
63
  # rather than only inside build_units is what lets the reported
49
64
  # `incremental` describe what the run actually did: it claimed reuse on
50
65
  # exactly the runs where the configuration change had forbidden it.
51
- previous_config = index_config_fingerprint(previous_payload) if previous_payload else None
52
- config_changed = previous_config is not None and previous_config != cfg.build_fingerprint
53
- if config_changed:
66
+ previous_build = index_build_fingerprint(previous_payload) if previous_payload else None
67
+ inputs_changed = previous_build is not None and previous_build != build_fingerprint(cfg)
68
+ if inputs_changed:
54
69
  previous_payload, previous_units = {}, []
55
70
  diagnostics: list[dict] = []
56
71
  # One snapshot for both halves: parsing from one walk and publishing hashes
@@ -64,7 +79,7 @@ def _refresh_index(root: Path, output: Path, full: bool = False, compact: bool |
64
79
  diagnostics=diagnostics,
65
80
  snapshot=snapshot,
66
81
  cfg=cfg,
67
- previous_config=None if config_changed else previous_config,
82
+ previous_build=None if inputs_changed else previous_build,
68
83
  descriptions=store,
69
84
  )
70
85
  graph = build_graph(units)
@@ -75,7 +90,7 @@ def _refresh_index(root: Path, output: Path, full: bool = False, compact: bool |
75
90
  "graph_edges": len(graph.edges),
76
91
  "warnings": len(diagnostics),
77
92
  "incremental": bool(previous_units) and not full,
78
- "rebuilt_for_config": config_changed,
93
+ "rebuilt_for_inputs": inputs_changed,
79
94
  "compact": bool(compact),
80
95
  "described": len(groups["described"]),
81
96
  "pending_descriptions": len(groups["missing"]) + len(groups["superseded"]),
@@ -86,6 +101,10 @@ def _refresh_index(root: Path, output: Path, full: bool = False, compact: bool |
86
101
 
87
102
 
88
103
  def _cmd_index(args: argparse.Namespace) -> int:
104
+ """The index command: scans a repository and writes its index, reporting
105
+ how many units and relationships were found and how many descriptions
106
+ are still pending.
107
+ """
89
108
  root = Path(args.root).resolve()
90
109
  cfg = config_module.load(root)
91
110
  output = Path(args.output) if args.output else _default_index(root)
@@ -94,6 +113,13 @@ def _cmd_index(args: argparse.Namespace) -> int:
94
113
 
95
114
 
96
115
  def _load(args: argparse.Namespace):
116
+ """Opens a published index for a read-only command and settles whether it
117
+ is still current. Three things can make it out of date and only one of
118
+ them is a file edit: the repository content can have moved, the rules
119
+ that decide what a unit is can have changed, or the written descriptions
120
+ can have changed. Neither authored input nor the parser is an indexed
121
+ file, so each has to report itself.
122
+ """
97
123
  root = Path(args.root).resolve()
98
124
  cfg = config_module.load(root)
99
125
  store = descriptions_module.load(root)
@@ -105,7 +131,7 @@ def _load(args: argparse.Namespace):
105
131
  # descriptions mean it serves text nobody wrote any more. Neither moves
106
132
  # a tracked file, so each has to report itself.
107
133
  stale = payload.get("fingerprint") != fingerprint(root, cfg)
108
- stale = stale or index_config_fingerprint(payload) != cfg.build_fingerprint
134
+ stale = stale or index_build_fingerprint(payload) != build_fingerprint(cfg)
109
135
  payload["stale"] = stale or index_descriptions_fingerprint(payload) != store.fingerprint
110
136
  except OSError:
111
137
  payload["stale"] = True
@@ -113,6 +139,13 @@ def _load(args: argparse.Namespace):
113
139
 
114
140
 
115
141
  def _cmd_search(args: argparse.Namespace) -> int:
142
+ """The search command: retrieves the code units most relevant to a
143
+ question, optionally following relationships outward, and prints either
144
+ a readable context block or machine-readable output for an agent. Result
145
+ count, context budget and the balance between word overlap and vector
146
+ similarity all fall back to the repository settings when no flag
147
+ overrides them. Warns when the index no longer describes the repository.
148
+ """
116
149
  payload, units, graph, cfg, _ = _load(args)
117
150
  limit = args.limit if args.limit is not None else cfg["search.limit"]
118
151
  max_chars = args.max_chars if args.max_chars is not None else cfg["search.max_chars"]
@@ -133,6 +166,12 @@ def _cmd_search(args: argparse.Namespace) -> int:
133
166
 
134
167
 
135
168
  def _cmd_annotate(args: argparse.Namespace) -> int:
169
+ """The annotate command: writes a numbered inventory of every indexed unit
170
+ to a separate document, with its location, kind and description. Source
171
+ files are never touched. Refuses to run against an index that no longer
172
+ describes the repository, since a numbered inventory of stale code is
173
+ worse than none.
174
+ """
136
175
  payload, units, _, _, _ = _load(args)
137
176
  if payload.get("stale"):
138
177
  print("Index is stale; run `rag-your-code index` before annotating.", file=sys.stderr)
@@ -148,6 +187,14 @@ def _cmd_annotate(args: argparse.Namespace) -> int:
148
187
 
149
188
 
150
189
  def _cmd_config(args: argparse.Namespace) -> int:
190
+ """The config command: creates a commented settings file, lists every
191
+ setting with its effective value and whether it was customised, reads
192
+ one value, changes one value in place, or reports where the file lives.
193
+ Changing a value refuses anything out of range before writing, so a
194
+ rejected change leaves the file exactly as it was, and the reply says
195
+ whether the change forces a full rebuild. Creating refuses to overwrite
196
+ an existing file unless told to.
197
+ """
151
198
  root = Path(args.root).resolve()
152
199
  path = config_module.config_path(root)
153
200
  if args.action == "path":
@@ -293,7 +340,38 @@ def _apply_descriptions(units: list, store: DescriptionStore, cfg: Config) -> in
293
340
 
294
341
 
295
342
  def _cmd_describe(args: argparse.Namespace) -> int:
343
+ """The describe command: reports how many units have a usable description,
344
+ how many have one the code has since outgrown and how many have none;
345
+ exports a batch of pending work with source and brief; imports written
346
+ descriptions back; or emits a patch that moves a description into the
347
+ source as a doc comment. Importing says explicitly that a rebuild is
348
+ needed, because the published index still holds the previous wording and
349
+ no source file moved to signal it. The patch owns standard output so it
350
+ can be piped straight into git apply.
351
+ """
296
352
  payload, units, _, cfg, store = _load(args)
353
+ if args.action == "promote":
354
+ # A stored description exists so text about a unit can be written
355
+ # without touching the file, and that independence costs a digest, a
356
+ # relocation lookup, a fingerprint and a pruning rule -- all of them
357
+ # simulating a property a docstring has for free. This offers the
358
+ # promotion as a patch rather than performing it: the tool still never
359
+ # writes source, and a person stays between an agent's prose and the
360
+ # repository.
361
+ root = Path(args.root).resolve()
362
+ insertions = plan_documentation(units, store, root)
363
+ report = summarise_documentation(units, store, insertions, root)
364
+ patch = render_patch(root, insertions)
365
+ if args.output:
366
+ Path(args.output).write_text(patch, encoding="utf-8", newline="")
367
+ report["output"] = args.output
368
+ print(json.dumps(report, ensure_ascii=False, indent=2))
369
+ else:
370
+ # The patch owns stdout so it can be piped straight into `git
371
+ # apply`; the summary goes to stderr.
372
+ sys.stdout.write(patch)
373
+ print(json.dumps(report, ensure_ascii=False), file=sys.stderr)
374
+ return 0
297
375
  if args.action == "status":
298
376
  groups = store.classify(units)
299
377
  print(json.dumps({
@@ -484,6 +562,13 @@ def _cmd_agent(args: argparse.Namespace) -> int:
484
562
 
485
563
 
486
564
  def build_parser() -> argparse.ArgumentParser:
565
+ """Declares the whole command-line surface: indexing, retrieval,
566
+ annotation, the long-running agent, settings, and descriptions including
567
+ the promotion patch, with their options and help text. Options that have
568
+ a configurable counterpart default to nothing rather than to a literal,
569
+ so an unset flag means whatever the repository configured instead of a
570
+ number frozen into the program.
571
+ """
487
572
  parser = argparse.ArgumentParser(prog="rag-your-code", description="Index and retrieve explainable code units locally.")
488
573
  sub = parser.add_subparsers(dest="command", required=True)
489
574
  index = sub.add_parser("index", help="scan a repository and build its local index")
@@ -522,12 +607,12 @@ def build_parser() -> argparse.ArgumentParser:
522
607
  config_parser.add_argument("--force", action="store_true", help="with init, overwrite an existing file")
523
608
  config_parser.set_defaults(func=_cmd_config)
524
609
  describe = sub.add_parser("describe", help="inspect or supply agent-authored unit descriptions")
525
- describe.add_argument("action", choices=("status", "export", "import"))
610
+ describe.add_argument("action", choices=("status", "export", "import", "promote"))
526
611
  describe.add_argument("file", nargs="?", help="with import, a JSON file of {id, text} objects")
527
612
  describe.add_argument("--root", default=".")
528
613
  describe.add_argument("--index")
529
614
  describe.add_argument("--limit", type=int, default=None, help=f"with export, units per batch (config describe.batch, default {BY_PATH['describe.batch'].default})")
530
- describe.add_argument("--output", help="with export, write the batch here instead of stdout")
615
+ describe.add_argument("--output", help="with export or promote, write here instead of stdout")
531
616
  describe.set_defaults(func=_cmd_describe)
532
617
  return parser
533
618
 
@@ -557,6 +642,12 @@ def _use_utf8_streams() -> None:
557
642
 
558
643
 
559
644
  def main(argv: list[str] | None = None) -> int:
645
+ """The program entry point: pins the streams, parses the command line, runs
646
+ the chosen command, and turns an expected failure into a message and a
647
+ non-zero exit code instead of a stack trace. A settings problem is
648
+ reported separately and names the file, because that fix is always in
649
+ one known place.
650
+ """
560
651
  _use_utf8_streams()
561
652
  args = build_parser().parse_args(argv)
562
653
  try: