code-recycle 0.1.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 (42) hide show
  1. code_recycle-0.1.0/.github/workflows/ci.yml +30 -0
  2. code_recycle-0.1.0/.github/workflows/publish.yml +54 -0
  3. code_recycle-0.1.0/.gitignore +11 -0
  4. code_recycle-0.1.0/CHANGELOG.md +22 -0
  5. code_recycle-0.1.0/LICENSE +21 -0
  6. code_recycle-0.1.0/METHOD.md +47 -0
  7. code_recycle-0.1.0/PKG-INFO +176 -0
  8. code_recycle-0.1.0/README.md +157 -0
  9. code_recycle-0.1.0/bench/README.md +21 -0
  10. code_recycle-0.1.0/bench/codesearchnet.py +127 -0
  11. code_recycle-0.1.0/bench/own_entries.py +102 -0
  12. code_recycle-0.1.0/code_recycle/__init__.py +3 -0
  13. code_recycle-0.1.0/code_recycle/chunks.py +168 -0
  14. code_recycle-0.1.0/code_recycle/cli.py +69 -0
  15. code_recycle-0.1.0/code_recycle/embed.py +68 -0
  16. code_recycle-0.1.0/code_recycle/files.py +155 -0
  17. code_recycle-0.1.0/code_recycle/index.py +101 -0
  18. code_recycle-0.1.0/code_recycle/search.py +176 -0
  19. code_recycle-0.1.0/code_recycle/secret_check.py +99 -0
  20. code_recycle-0.1.0/code_recycle/server.py +42 -0
  21. code_recycle-0.1.0/code_recycle/settings.py +151 -0
  22. code_recycle-0.1.0/examples/demo/code_recycle.toml +11 -0
  23. code_recycle-0.1.0/examples/demo/notes/research_plan.md +25 -0
  24. code_recycle-0.1.0/examples/demo/tools/dates.py +12 -0
  25. code_recycle-0.1.0/examples/demo/tools/http_utils.py +24 -0
  26. code_recycle-0.1.0/pyproject.toml +23 -0
  27. code_recycle-0.1.0/results/accuracy_2026-11-05.md +77 -0
  28. code_recycle-0.1.0/results/csn_test_meaning_2026-11-05.txt +6 -0
  29. code_recycle-0.1.0/results/csn_validation_2026-11-05.txt +31 -0
  30. code_recycle-0.1.0/results/own_entries_meaning_2026-11-05.txt +4 -0
  31. code_recycle-0.1.0/results/prereg_embeddings.md +94 -0
  32. code_recycle-0.1.0/tests/conftest.py +31 -0
  33. code_recycle-0.1.0/tests/test_bench.py +22 -0
  34. code_recycle-0.1.0/tests/test_chunks.py +114 -0
  35. code_recycle-0.1.0/tests/test_cli.py +38 -0
  36. code_recycle-0.1.0/tests/test_embed.py +95 -0
  37. code_recycle-0.1.0/tests/test_files.py +117 -0
  38. code_recycle-0.1.0/tests/test_search.py +129 -0
  39. code_recycle-0.1.0/tests/test_secret_check.py +42 -0
  40. code_recycle-0.1.0/tests/test_server.py +70 -0
  41. code_recycle-0.1.0/tests/test_settings.py +49 -0
  42. code_recycle-0.1.0/tests/test_smoke.py +34 -0
@@ -0,0 +1,30 @@
1
+ # Tests on every push and pull request: three operating systems, three Python versions.
2
+ # Sources: GitHub Docs, "Building and testing Python" (version matrix, pytest step) and
3
+ # "Workflow syntax" (strategy.matrix with os, fail-fast).
4
+ name: CI
5
+
6
+ on: [push, pull_request]
7
+
8
+ jobs:
9
+ test:
10
+ runs-on: ${{ matrix.os }}
11
+ strategy:
12
+ fail-fast: false # a failure on one machine doesn't stop the others, so every problem shows
13
+ matrix:
14
+ os: [ubuntu-latest, macos-latest, windows-latest]
15
+ python-version: ["3.11", "3.12", "3.13"]
16
+
17
+ steps:
18
+ - uses: actions/checkout@v6
19
+ - name: Set up Python ${{ matrix.python-version }}
20
+ uses: actions/setup-python@v5
21
+ with:
22
+ python-version: ${{ matrix.python-version }}
23
+ - name: Check this Python's SQLite has full-text search (FTS5)
24
+ run: python -c "import sqlite3; sqlite3.connect(':memory:').execute('CREATE VIRTUAL TABLE t USING fts5(x)'); print('FTS5 ok', sqlite3.sqlite_version)"
25
+ - name: Install the package and test tools
26
+ run: |
27
+ python -m pip install --upgrade pip
28
+ pip install ".[dev]" # the embeddings tests use a fake model, so fastembed is not needed
29
+ - name: Test with pytest
30
+ run: pytest -q
@@ -0,0 +1,54 @@
1
+ # Publishes to PyPI when a version tag (for example v0.1.0) is pushed. Not used until the PyPI
2
+ # project and its trusted publisher are set up. Sources: Python Packaging User Guide,
3
+ # "Publishing package distribution releases using GitHub Actions CI/CD workflows".
4
+ name: Publish
5
+
6
+ on:
7
+ push:
8
+ tags: ["v*"]
9
+
10
+ jobs:
11
+ build:
12
+ name: Build distribution
13
+ runs-on: ubuntu-latest
14
+ steps:
15
+ - uses: actions/checkout@v6
16
+ with:
17
+ persist-credentials: false
18
+ - name: Set up Python
19
+ uses: actions/setup-python@v6
20
+ with:
21
+ python-version: "3.x"
22
+ - name: Install pypa/build
23
+ run: >-
24
+ python3 -m
25
+ pip install
26
+ build
27
+ --user
28
+ - name: Build a wheel and a source distribution
29
+ run: python3 -m build
30
+ - name: Store the distribution packages
31
+ uses: actions/upload-artifact@v5
32
+ with:
33
+ name: python-package-distributions
34
+ path: dist/
35
+
36
+ publish-to-pypi:
37
+ name: Publish to PyPI
38
+ if: startsWith(github.ref, 'refs/tags/') # only publish to PyPI on tag pushes
39
+ needs:
40
+ - build
41
+ runs-on: ubuntu-latest
42
+ environment:
43
+ name: pypi
44
+ url: https://pypi.org/p/code-recycle
45
+ permissions:
46
+ id-token: write # mandatory for trusted publishing; no stored token
47
+ steps:
48
+ - name: Download all the dists
49
+ uses: actions/download-artifact@v6
50
+ with:
51
+ name: python-package-distributions
52
+ path: dist/
53
+ - name: Publish distribution to PyPI
54
+ uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,11 @@
1
+ .venv/
2
+ dist/
3
+ __pycache__/
4
+ *.egg-info/
5
+ .code_recycle/
6
+ /code_recycle.toml
7
+ docs/research_plan.md
8
+ docs/reviews/
9
+ docs/gpt_brief.md
10
+ data/*.parquet
11
+ docs/project_plan.md
@@ -0,0 +1,22 @@
1
+ # Changelog
2
+
3
+ ## 0.1.0 (2026-11-06)
4
+
5
+ First release.
6
+
7
+ - Index folders: Python split by function, method and class with line numbers; Markdown research
8
+ notes read as structured entries (URL, where on the page, what it taught, copied code); other
9
+ Markdown by heading; other text in 60-line windows.
10
+ - Keyword search with SQLite FTS5: stemming, camelCase splitting, per-field weights, identical
11
+ copies shown once.
12
+ - MCP tool `search_own_work`, read-only, over stdio.
13
+ - Command `code-recycle`: `init`, `index`, `search`, `serve`.
14
+ - Settings file with typo checking, skip rules, `.gitignore` support and a file-type list.
15
+ - Secrets: secret-named files never opened, every file scanned before indexing, every result
16
+ scanned before returning, and a self-check that stops indexing if the scanner can't see a
17
+ planted fake key.
18
+ - Reuse rules: `no_suggest_from` and `no_suggest_into`. Rules apply before duplicate copies are
19
+ removed, and the MCP tool falls back to the folder it was started in when no `from_path` is given.
20
+ - Accuracy published: CodeSearchNet Python MRR 0.797; own research notes MRR 0.308.
21
+ - Experimental, off by default: meaning search with local embeddings (`pip install "code-recycle[embed]"`).
22
+ Tested and not adopted; see `results/prereg_embeddings.md`.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Zee Mehmood
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,47 @@
1
+ # How this was built
2
+
3
+ ## Sources first, then code
4
+
5
+ Each design decision has a source, found and checked before that part was built. For each
6
+ requirement there's a research note: the page's URL, where on the page, what it taught, and the
7
+ code copied from it word for word. During the research, a script checked each copied snippet
8
+ against the downloaded page, so a quote that didn't match the page was caught. Where no source
9
+ exists (the settings layout, the reuse rules, the secrets policy, the research-note parser), the
10
+ design is marked as my own and has its own tests.
11
+
12
+ The full research plan stays private, because it holds code copied from other people's pages. The
13
+ main sources:
14
+
15
+ | Part | Source |
16
+ |---|---|
17
+ | MCP server and testing | MCP Python SDK docs and examples (v2, pinned to 2.3.0) |
18
+ | Registering with clients | MCP SDK "Connect to a real host"; OpenAI Codex MCP docs |
19
+ | Splitting Python code | Python docs, `ast` |
20
+ | Keyword search | SQLite FTS5 docs (bm25 column weights, porter stemming, UNINDEXED columns) |
21
+ | Combining rankings | Cormack, Clarke and Büttcher (2009), Reciprocal Rank Fusion |
22
+ | Scoring | Mean reciprocal rank; CodeSearchNet (Husain et al. 2019) |
23
+ | Settings | Python docs, `tomllib` |
24
+ | Skip rules | `pathspec` (Git's own .gitignore behaviour) |
25
+ | Secrets | `detect-secrets` (Yelp) |
26
+ | Packaging and CI | Python Packaging User Guide; GitHub Actions docs |
27
+
28
+ ## A second opinion
29
+
30
+ Before building, the plan was given to a different AI model (GPT, through the Codex CLI, read-only)
31
+ and asked to attack it: what would make it look like a tutorial clone, what was missing, what to
32
+ cut. Its points were treated as opinions and checked before anything changed. It cut the first
33
+ release down, made the accuracy test fairer, and narrowed the secrets promise to what tests can
34
+ prove.
35
+
36
+ ## Measuring accuracy
37
+
38
+ - The test design was written down before the first run.
39
+ - The keyword baseline was frozen before anything else was tried.
40
+ - Embeddings were chosen on a validation split and then run once on the test sets, against a rule
41
+ set in advance. They didn't meet the rule, so they're not in v0.1. That result is published too.
42
+ - One change was made to that plan before any accuracy run: this laptop embeds about 6 pieces a
43
+ second, so only code and research notes are embedded, cut to 400 characters. It's recorded with
44
+ the reason.
45
+ - Leak checks: the CodeSearchNet test was re-run with the docstring deliberately left in the code
46
+ (MRR 0.988, against 0.797) to prove that removing it works, and with function names hidden (0.763)
47
+ to show the result doesn't rest on names alone.
@@ -0,0 +1,176 @@
1
+ Metadata-Version: 2.5
2
+ Name: code-recycle
3
+ Version: 0.1.0
4
+ Summary: Search your own code and verified sources before searching the web. Local, private, and usable from any MCP client.
5
+ License-Expression: MIT
6
+ License-File: LICENSE
7
+ Requires-Python: >=3.11
8
+ Requires-Dist: detect-secrets==1.5.0
9
+ Requires-Dist: mcp==2.3.0
10
+ Requires-Dist: pathspec==1.1.1
11
+ Provides-Extra: dev
12
+ Requires-Dist: anyio; extra == 'dev'
13
+ Requires-Dist: numpy; extra == 'dev'
14
+ Requires-Dist: pyarrow==22.0.0; extra == 'dev'
15
+ Requires-Dist: pytest==9.1.1; extra == 'dev'
16
+ Provides-Extra: embed
17
+ Requires-Dist: fastembed==0.9.1; extra == 'embed'
18
+ Description-Content-Type: text/markdown
19
+
20
+ # code_recycle
21
+
22
+ Search your own code and your own verified sources before you search the web.
23
+
24
+ Every new project I start, I end up looking for the same things again: how to retry a request,
25
+ how to read a settings file, which page had that bootstrap example. Most of the time I've already
26
+ solved it in another project, and I've already checked the source. I just can't find it.
27
+
28
+ Companies have the same problem, with a twist. Pasting internal code or questions into outside
29
+ tools is a risk, so the search needs to stay on the machine.
30
+
31
+ code_recycle indexes your folders and answers "have I already solved this?" with the file and line
32
+ numbers. If you keep research notes with a source URL and the code you copied from it, it finds
33
+ those too, with the link and what you learned. It runs locally, needs no account and no API key,
34
+ and plugs into Claude Code, Codex, Cursor or any MCP client.
35
+
36
+ ## Try it in 5 minutes
37
+
38
+ Needs Python 3.11 or newer.
39
+
40
+ ```bash
41
+ git clone https://github.com/Z33MScEssex/code_recycle
42
+ cd code_recycle
43
+ pip install .
44
+ cd examples/demo
45
+ code-recycle index
46
+ code-recycle search "read a settings file"
47
+ ```
48
+
49
+ The first result is a research note: the Python docs page for `tomllib`, where on the page, what it
50
+ taught, and the code copied from it. Try `code-recycle search "try a request again with a wait"`
51
+ and you get the `retry` function from `tools/http_utils.py`, lines 14 to 24.
52
+
53
+ ## Use it on your own folders
54
+
55
+ ```bash
56
+ code-recycle init # writes code_recycle.toml here; edit the folders list
57
+ code-recycle index # builds the index and prints what was skipped and why
58
+ code-recycle search "parse an iso date"
59
+ ```
60
+
61
+ Re-run `code-recycle index` after big changes. It reads everything again; on my laptop that's about
62
+ 40 seconds for 1,500 files.
63
+
64
+ ## Connect it to an AI coding assistant
65
+
66
+ The tool is called `search_own_work`. It's read-only. Hosts start it with a near-empty environment,
67
+ so give the settings file's full path.
68
+
69
+ Claude Code:
70
+
71
+ ```bash
72
+ claude mcp add code-recycle -e CODE_RECYCLE_SETTINGS=/full/path/to/code_recycle.toml -- code-recycle serve
73
+ ```
74
+
75
+ Codex:
76
+
77
+ ```bash
78
+ codex mcp add code-recycle --env CODE_RECYCLE_SETTINGS=/full/path/to/code_recycle.toml -- code-recycle serve
79
+ ```
80
+
81
+ If the host can't find `code-recycle`, use the full path from `which code-recycle` (or
82
+ `where code-recycle` on Windows).
83
+
84
+ ## Research notes it understands
85
+
86
+ A Markdown section becomes a research entry when it has a URL field. These labels are read, bold
87
+ or plain:
88
+
89
+ ```markdown
90
+ ## Topic 1: Read a TOML settings file
91
+
92
+ - **URL:** https://docs.python.org/3/library/tomllib.html
93
+ - **Where on the page:** "Examples", the first example.
94
+ - **What it taught:** the file must be opened in binary mode.
95
+ - **Code pattern from source:**
96
+ (a fenced code block)
97
+ ```
98
+
99
+ Everything else is still indexed: Python by function, method and class with exact line numbers,
100
+ other Markdown by heading, and other text files in overlapping windows of 60 lines.
101
+
102
+ ## How well it works
103
+
104
+ Measured, not assumed. The test design was written down before the first run, and nothing was
105
+ tuned on the test sets. Full write-up: [results/accuracy_2026-11-05.md](results/accuracy_2026-11-05.md).
106
+
107
+ | Test | What it asks | Result |
108
+ |---|---|---|
109
+ | CodeSearchNet, Python | Find a function from its docstring among 999 others (Husain et al. 2019) | MRR 0.797, right function in the top 5 87% of the time |
110
+ | My own research notes | Find another note that used the same source, from a topic heading | MRR 0.308, top 5 39% of the time, 51 cases |
111
+
112
+ The second test is the real job, and it's much harder. The same source often gets described in
113
+ different words in different projects, and keyword search can't bridge that.
114
+
115
+ I tried meaning-based search with a small local embedding model. It lifted CodeSearchNet to 0.907,
116
+ but it didn't help on my own notes (0.250 against 0.280 at the time), and the rule set beforehand
117
+ said it had to improve both. So v0.1 ships keyword search. The embeddings code is in the repo,
118
+ switched off and marked experimental: [results/prereg_embeddings.md](results/prereg_embeddings.md).
119
+
120
+ ## Privacy and secrets
121
+
122
+ - Nothing leaves your machine. There are no network calls, unless you switch on the experimental
123
+ embeddings, which download a model once.
124
+ - Files named like secrets (`.env`, `*.pem`, `id_rsa` and others) are never opened.
125
+ - Every other file is scanned with [detect-secrets](https://github.com/Yelp/detect-secrets) before
126
+ it's indexed. A flagged file is skipped and listed in the index report. If you've checked it's
127
+ safe, add it to `[secrets] allow`.
128
+ - Every result is scanned again before it's returned, for common live key formats (AWS, GitHub,
129
+ Slack, OpenAI-style, Telegram bot tokens, private key blocks). If one matches, the content is
130
+ withheld and only the location is shown.
131
+ - If the scanner fails its own self-check (it must spot a planted fake key), nothing is indexed.
132
+
133
+ Each of these behaviours has a test with fake keys. That isn't a guarantee: a pattern scanner can't
134
+ catch every possible secret, so don't point the tool at folders you know hold live credentials.
135
+
136
+ ## Reuse rules
137
+
138
+ Some folders shouldn't borrow from others. I have one project where every requirement must be
139
+ researched fresh, so code from other projects must never be suggested into it.
140
+
141
+ ```toml
142
+ [rules]
143
+ no_suggest_from = ["private/"] # never return results from here
144
+ no_suggest_into = ["clean-room/"] # searches made from here only see this folder's own code,
145
+ # plus a note that other matches exist
146
+ ```
147
+
148
+ The MCP tool takes a `from_path`, so the assistant can say where it's working. If it doesn't, the
149
+ folder the server was started in is used. The rules are a guard against accidents, not a security
150
+ boundary: they trust the path they're given.
151
+
152
+ ## Settings
153
+
154
+ `code-recycle init` writes a commented example. The settings file is found in this order:
155
+ `--settings`, then `$CODE_RECYCLE_SETTINGS`, then `./code_recycle.toml`, then
156
+ `~/.config/code_recycle/code_recycle.toml`. Typos are refused with a clear message, because a
157
+ misspelt rule silently ignored would be worse.
158
+
159
+ ## Limits
160
+
161
+ - Python is the only language split by function. Others are searched in windows of lines.
162
+ - The index is rebuilt in full each time. Fine for a few thousand files; not built for huge monorepos.
163
+ - Search is keyword-based (SQLite FTS5 with stemming), so it finds shared words, not shared ideas.
164
+ - So far only I have used it. It has had no outside users yet.
165
+
166
+ ## Development
167
+
168
+ ```bash
169
+ pip install -e ".[dev]"
170
+ pytest
171
+ ```
172
+
173
+ 87 tests. The CI workflow runs them on Linux, macOS and Windows with Python 3.11 to 3.13 on every push. How sources were
174
+ checked and how accuracy was measured: [METHOD.md](METHOD.md). Benchmarks: [bench/README.md](bench/README.md).
175
+
176
+ MIT licence.
@@ -0,0 +1,157 @@
1
+ # code_recycle
2
+
3
+ Search your own code and your own verified sources before you search the web.
4
+
5
+ Every new project I start, I end up looking for the same things again: how to retry a request,
6
+ how to read a settings file, which page had that bootstrap example. Most of the time I've already
7
+ solved it in another project, and I've already checked the source. I just can't find it.
8
+
9
+ Companies have the same problem, with a twist. Pasting internal code or questions into outside
10
+ tools is a risk, so the search needs to stay on the machine.
11
+
12
+ code_recycle indexes your folders and answers "have I already solved this?" with the file and line
13
+ numbers. If you keep research notes with a source URL and the code you copied from it, it finds
14
+ those too, with the link and what you learned. It runs locally, needs no account and no API key,
15
+ and plugs into Claude Code, Codex, Cursor or any MCP client.
16
+
17
+ ## Try it in 5 minutes
18
+
19
+ Needs Python 3.11 or newer.
20
+
21
+ ```bash
22
+ git clone https://github.com/Z33MScEssex/code_recycle
23
+ cd code_recycle
24
+ pip install .
25
+ cd examples/demo
26
+ code-recycle index
27
+ code-recycle search "read a settings file"
28
+ ```
29
+
30
+ The first result is a research note: the Python docs page for `tomllib`, where on the page, what it
31
+ taught, and the code copied from it. Try `code-recycle search "try a request again with a wait"`
32
+ and you get the `retry` function from `tools/http_utils.py`, lines 14 to 24.
33
+
34
+ ## Use it on your own folders
35
+
36
+ ```bash
37
+ code-recycle init # writes code_recycle.toml here; edit the folders list
38
+ code-recycle index # builds the index and prints what was skipped and why
39
+ code-recycle search "parse an iso date"
40
+ ```
41
+
42
+ Re-run `code-recycle index` after big changes. It reads everything again; on my laptop that's about
43
+ 40 seconds for 1,500 files.
44
+
45
+ ## Connect it to an AI coding assistant
46
+
47
+ The tool is called `search_own_work`. It's read-only. Hosts start it with a near-empty environment,
48
+ so give the settings file's full path.
49
+
50
+ Claude Code:
51
+
52
+ ```bash
53
+ claude mcp add code-recycle -e CODE_RECYCLE_SETTINGS=/full/path/to/code_recycle.toml -- code-recycle serve
54
+ ```
55
+
56
+ Codex:
57
+
58
+ ```bash
59
+ codex mcp add code-recycle --env CODE_RECYCLE_SETTINGS=/full/path/to/code_recycle.toml -- code-recycle serve
60
+ ```
61
+
62
+ If the host can't find `code-recycle`, use the full path from `which code-recycle` (or
63
+ `where code-recycle` on Windows).
64
+
65
+ ## Research notes it understands
66
+
67
+ A Markdown section becomes a research entry when it has a URL field. These labels are read, bold
68
+ or plain:
69
+
70
+ ```markdown
71
+ ## Topic 1: Read a TOML settings file
72
+
73
+ - **URL:** https://docs.python.org/3/library/tomllib.html
74
+ - **Where on the page:** "Examples", the first example.
75
+ - **What it taught:** the file must be opened in binary mode.
76
+ - **Code pattern from source:**
77
+ (a fenced code block)
78
+ ```
79
+
80
+ Everything else is still indexed: Python by function, method and class with exact line numbers,
81
+ other Markdown by heading, and other text files in overlapping windows of 60 lines.
82
+
83
+ ## How well it works
84
+
85
+ Measured, not assumed. The test design was written down before the first run, and nothing was
86
+ tuned on the test sets. Full write-up: [results/accuracy_2026-11-05.md](results/accuracy_2026-11-05.md).
87
+
88
+ | Test | What it asks | Result |
89
+ |---|---|---|
90
+ | CodeSearchNet, Python | Find a function from its docstring among 999 others (Husain et al. 2019) | MRR 0.797, right function in the top 5 87% of the time |
91
+ | My own research notes | Find another note that used the same source, from a topic heading | MRR 0.308, top 5 39% of the time, 51 cases |
92
+
93
+ The second test is the real job, and it's much harder. The same source often gets described in
94
+ different words in different projects, and keyword search can't bridge that.
95
+
96
+ I tried meaning-based search with a small local embedding model. It lifted CodeSearchNet to 0.907,
97
+ but it didn't help on my own notes (0.250 against 0.280 at the time), and the rule set beforehand
98
+ said it had to improve both. So v0.1 ships keyword search. The embeddings code is in the repo,
99
+ switched off and marked experimental: [results/prereg_embeddings.md](results/prereg_embeddings.md).
100
+
101
+ ## Privacy and secrets
102
+
103
+ - Nothing leaves your machine. There are no network calls, unless you switch on the experimental
104
+ embeddings, which download a model once.
105
+ - Files named like secrets (`.env`, `*.pem`, `id_rsa` and others) are never opened.
106
+ - Every other file is scanned with [detect-secrets](https://github.com/Yelp/detect-secrets) before
107
+ it's indexed. A flagged file is skipped and listed in the index report. If you've checked it's
108
+ safe, add it to `[secrets] allow`.
109
+ - Every result is scanned again before it's returned, for common live key formats (AWS, GitHub,
110
+ Slack, OpenAI-style, Telegram bot tokens, private key blocks). If one matches, the content is
111
+ withheld and only the location is shown.
112
+ - If the scanner fails its own self-check (it must spot a planted fake key), nothing is indexed.
113
+
114
+ Each of these behaviours has a test with fake keys. That isn't a guarantee: a pattern scanner can't
115
+ catch every possible secret, so don't point the tool at folders you know hold live credentials.
116
+
117
+ ## Reuse rules
118
+
119
+ Some folders shouldn't borrow from others. I have one project where every requirement must be
120
+ researched fresh, so code from other projects must never be suggested into it.
121
+
122
+ ```toml
123
+ [rules]
124
+ no_suggest_from = ["private/"] # never return results from here
125
+ no_suggest_into = ["clean-room/"] # searches made from here only see this folder's own code,
126
+ # plus a note that other matches exist
127
+ ```
128
+
129
+ The MCP tool takes a `from_path`, so the assistant can say where it's working. If it doesn't, the
130
+ folder the server was started in is used. The rules are a guard against accidents, not a security
131
+ boundary: they trust the path they're given.
132
+
133
+ ## Settings
134
+
135
+ `code-recycle init` writes a commented example. The settings file is found in this order:
136
+ `--settings`, then `$CODE_RECYCLE_SETTINGS`, then `./code_recycle.toml`, then
137
+ `~/.config/code_recycle/code_recycle.toml`. Typos are refused with a clear message, because a
138
+ misspelt rule silently ignored would be worse.
139
+
140
+ ## Limits
141
+
142
+ - Python is the only language split by function. Others are searched in windows of lines.
143
+ - The index is rebuilt in full each time. Fine for a few thousand files; not built for huge monorepos.
144
+ - Search is keyword-based (SQLite FTS5 with stemming), so it finds shared words, not shared ideas.
145
+ - So far only I have used it. It has had no outside users yet.
146
+
147
+ ## Development
148
+
149
+ ```bash
150
+ pip install -e ".[dev]"
151
+ pytest
152
+ ```
153
+
154
+ 87 tests. The CI workflow runs them on Linux, macOS and Windows with Python 3.11 to 3.13 on every push. How sources were
155
+ checked and how accuracy was measured: [METHOD.md](METHOD.md). Benchmarks: [bench/README.md](bench/README.md).
156
+
157
+ MIT licence.
@@ -0,0 +1,21 @@
1
+ # Benchmarks
2
+
3
+ ## CodeSearchNet (public)
4
+
5
+ Download the Python test split (28.7 MB) from the Hugging Face copy of CodeSearchNet. The original
6
+ S3 links in the CodeSearchNet README no longer work.
7
+
8
+ ```bash
9
+ curl -L -o data/csn_python_test.parquet \
10
+ https://huggingface.co/datasets/code-search-net/code_search_net/resolve/main/python/test-00000-of-00001.parquet
11
+ pip install pyarrow
12
+ python bench/codesearchnet.py
13
+ ```
14
+
15
+ Each function keeps its own repository's licence, so the data is never committed here.
16
+
17
+ ## Own research entries
18
+
19
+ `python bench/own_entries.py path/to/code_recycle.toml --detail somewhere_private.txt`
20
+
21
+ Uses your own index. The per-case detail names your files, so keep it out of public repos.
@@ -0,0 +1,127 @@
1
+ """CodeSearchNet benchmark: find a Python function from its docstring among 999 others.
2
+
3
+ Method: research plan Topic 7, "a public benchmark as well as my own entries". Follows the
4
+ paper's test (Husain et al. 2019, Table 3): the docstring is the query, the right function must
5
+ rank among 999 distractors, scored by mean reciprocal rank (MRR).
6
+
7
+ Uses the same index columns, weights and query building as code_recycle's real search, so the
8
+ number describes the shipped tool, not a tuned copy of it.
9
+
10
+ Usage: python bench/codesearchnet.py [PARQUET] [--mode keyword|meaning|both] [--model NAME]
11
+ Default PARQUET: data/csn_python_test.parquet. The model is chosen on the validation split only
12
+ (results/prereg_embeddings.md); the test split is for the final number.
13
+ Get the data: see bench/README.md.
14
+ """
15
+ import ast # removing the docstring from each function
16
+ import random # the fixed shuffle that builds the groups of 1,000
17
+ import sqlite3 # an in-memory FTS5 table per group
18
+ import argparse # command-line options
19
+ from pathlib import Path
20
+
21
+ import pyarrow.parquet as pq # reads the dataset file
22
+
23
+ from code_recycle import embed, index, search
24
+
25
+ SEED = 2026 # fixed before the first run, never changed
26
+ GROUP = 1000 # 1 right answer + 999 distractors, as in the paper
27
+ TOP = 5 # "hit rate at 5": is the right function in the first five?
28
+
29
+
30
+ def strip_docstring(code):
31
+ """The function's code without its docstring, so the query isn't copied inside the answer.
32
+
33
+ Returns (code, how) where how is "ast" or "unparsed" (the code doesn't parse; kept as is but counted).
34
+ """
35
+ try:
36
+ tree = ast.parse(code)
37
+ except SyntaxError:
38
+ return code, "unparsed"
39
+ fn = tree.body[0] if tree.body else None
40
+ if not isinstance(fn, (ast.FunctionDef, ast.AsyncFunctionDef)) or not fn.body:
41
+ return code, "unparsed"
42
+ first = fn.body[0]
43
+ if not (isinstance(first, ast.Expr) and isinstance(first.value, ast.Constant) and isinstance(first.value.value, str)):
44
+ return code, "ast" # no docstring to remove
45
+ lines = code.splitlines()
46
+ del lines[first.lineno - 1:first.end_lineno] # drop exactly the docstring's lines
47
+ return "\n".join(lines), "ast"
48
+
49
+
50
+ def run(path, mode="keyword", model_name=None, progress=lambda m: None):
51
+ rows = pq.read_table(path, columns=["func_name", "func_code_string", "func_documentation_string"]).to_pylist()
52
+ order = list(range(len(rows)))
53
+ random.Random(SEED).shuffle(order)
54
+ groups = [order[i:i + GROUP] for i in range(0, len(order) - GROUP + 1, GROUP)] # leftover dropped
55
+
56
+ stripped = [strip_docstring(r["func_code_string"]) for r in rows]
57
+ names = [index.split_name(r["func_name"].rsplit(".", 1)[-1]) for r in rows] # "YouTube.get_vid" -> "get_vid"
58
+ model = vectors = None
59
+ if mode != "keyword":
60
+ model = embed.load_model(model_name)
61
+ used = sorted({i for g in groups for i in g})
62
+ progress(f"Embedding {len(used)} functions with {model_name}...")
63
+ vecs = embed.embed_pieces(model, [embed.piece_text(names[i], stripped[i][0]) for i in used])
64
+ vectors = dict(zip(used, vecs))
65
+
66
+ modes = ["keyword", "meaning", "both"] if mode == "all" else [mode]
67
+ rr = {m: [] for m in modes}
68
+ hits = {m: 0 for m in modes}
69
+ no_words = 0
70
+ weights = ", ".join(map(str, search.WEIGHTS))
71
+ for gi, g in enumerate(groups, start=1):
72
+ con = sqlite3.connect(":memory:")
73
+ con.execute(index.SCHEMA)
74
+ for rowid, i in enumerate(g, start=1):
75
+ con.execute(
76
+ f"INSERT INTO chunks (rowid, {', '.join(index.COLUMNS + index.STORED)}) VALUES (?, {', '.join('?' * (len(index.COLUMNS) + len(index.STORED)))})",
77
+ (rowid, names[i], "", "", "", "", "", stripped[i][0], "function", "", 0, 0, rows[i]["func_name"]),
78
+ )
79
+ if vectors is not None:
80
+ import numpy as np
81
+ matrix = np.stack([vectors[i] for i in g]) # row r-1 is rowid r
82
+ ids = list(range(1, len(g) + 1))
83
+ for rowid, i in enumerate(g, start=1):
84
+ doc = rows[i]["func_documentation_string"]
85
+ q = search.to_query(doc)
86
+ keyword = [x for (x,) in con.execute(
87
+ f"SELECT rowid FROM chunks WHERE chunks MATCH ? ORDER BY bm25(chunks, {weights})", (q,))] if q else []
88
+ ranked = {"keyword": keyword}
89
+ if mode != "keyword":
90
+ meaning = embed.nearest(matrix, ids, embed.embed_query(model, doc), len(ids))
91
+ ranked["meaning"] = meaning
92
+ ranked["both"] = embed.fuse(keyword, meaning)
93
+ if not keyword:
94
+ no_words += 1
95
+ for m in modes:
96
+ if rowid in ranked[m]:
97
+ pos = ranked[m].index(rowid) + 1
98
+ rr[m].append(1 / pos)
99
+ hits[m] += pos <= TOP
100
+ else:
101
+ rr[m].append(0.0) # not returned at all counts as a miss
102
+ con.close()
103
+ progress(f"group {gi} of {len(groups)}")
104
+
105
+ n = len(rr[modes[0]])
106
+ return {
107
+ "queries": n,
108
+ "groups": len(groups),
109
+ "modes": {m: {"mrr": sum(rr[m]) / n, "hit_at_5": hits[m] / n} for m in modes},
110
+ "unparsed_code": sum(how == "unparsed" for _, how in (stripped[i] for g in groups for i in g)),
111
+ "queries_with_no_words": no_words,
112
+ }
113
+
114
+
115
+ if __name__ == "__main__":
116
+ p = argparse.ArgumentParser()
117
+ p.add_argument("parquet", nargs="?", default="data/csn_python_test.parquet")
118
+ p.add_argument("--mode", choices=["keyword", "meaning", "both", "all"], default="keyword")
119
+ p.add_argument("--model", default="BAAI/bge-small-en-v1.5")
120
+ a = p.parse_args()
121
+ r = run(Path(a.parquet), a.mode, a.model, progress=lambda m: print(m, flush=True))
122
+ print(f"CodeSearchNet Python, {Path(a.parquet).name}" + (f", model {a.model}" if a.mode != "keyword" else ""))
123
+ print(f"Queries: {r['queries']} in {r['groups']} groups of {GROUP} (1 right answer, 999 distractors), seed {SEED}")
124
+ for m, v in r["modes"].items():
125
+ print(f"{m:8} MRR {v['mrr']:.4f} hit rate at {TOP}: {v['hit_at_5']:.4f}")
126
+ print(f"Functions that didn't parse (docstring left in): {r['unparsed_code']}")
127
+ print(f"Queries with no keyword matches: {r['queries_with_no_words']}")