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.
- code_recycle-0.1.0/.github/workflows/ci.yml +30 -0
- code_recycle-0.1.0/.github/workflows/publish.yml +54 -0
- code_recycle-0.1.0/.gitignore +11 -0
- code_recycle-0.1.0/CHANGELOG.md +22 -0
- code_recycle-0.1.0/LICENSE +21 -0
- code_recycle-0.1.0/METHOD.md +47 -0
- code_recycle-0.1.0/PKG-INFO +176 -0
- code_recycle-0.1.0/README.md +157 -0
- code_recycle-0.1.0/bench/README.md +21 -0
- code_recycle-0.1.0/bench/codesearchnet.py +127 -0
- code_recycle-0.1.0/bench/own_entries.py +102 -0
- code_recycle-0.1.0/code_recycle/__init__.py +3 -0
- code_recycle-0.1.0/code_recycle/chunks.py +168 -0
- code_recycle-0.1.0/code_recycle/cli.py +69 -0
- code_recycle-0.1.0/code_recycle/embed.py +68 -0
- code_recycle-0.1.0/code_recycle/files.py +155 -0
- code_recycle-0.1.0/code_recycle/index.py +101 -0
- code_recycle-0.1.0/code_recycle/search.py +176 -0
- code_recycle-0.1.0/code_recycle/secret_check.py +99 -0
- code_recycle-0.1.0/code_recycle/server.py +42 -0
- code_recycle-0.1.0/code_recycle/settings.py +151 -0
- code_recycle-0.1.0/examples/demo/code_recycle.toml +11 -0
- code_recycle-0.1.0/examples/demo/notes/research_plan.md +25 -0
- code_recycle-0.1.0/examples/demo/tools/dates.py +12 -0
- code_recycle-0.1.0/examples/demo/tools/http_utils.py +24 -0
- code_recycle-0.1.0/pyproject.toml +23 -0
- code_recycle-0.1.0/results/accuracy_2026-11-05.md +77 -0
- code_recycle-0.1.0/results/csn_test_meaning_2026-11-05.txt +6 -0
- code_recycle-0.1.0/results/csn_validation_2026-11-05.txt +31 -0
- code_recycle-0.1.0/results/own_entries_meaning_2026-11-05.txt +4 -0
- code_recycle-0.1.0/results/prereg_embeddings.md +94 -0
- code_recycle-0.1.0/tests/conftest.py +31 -0
- code_recycle-0.1.0/tests/test_bench.py +22 -0
- code_recycle-0.1.0/tests/test_chunks.py +114 -0
- code_recycle-0.1.0/tests/test_cli.py +38 -0
- code_recycle-0.1.0/tests/test_embed.py +95 -0
- code_recycle-0.1.0/tests/test_files.py +117 -0
- code_recycle-0.1.0/tests/test_search.py +129 -0
- code_recycle-0.1.0/tests/test_secret_check.py +42 -0
- code_recycle-0.1.0/tests/test_server.py +70 -0
- code_recycle-0.1.0/tests/test_settings.py +49 -0
- 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,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']}")
|