SourceIndex 0.1.2__tar.gz → 0.1.4__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.
- {sourceindex-0.1.2 → sourceindex-0.1.4}/GETTING_STARTED.md +14 -12
- sourceindex-0.1.4/LICENSE +115 -0
- {sourceindex-0.1.2 → sourceindex-0.1.4}/PKG-INFO +26 -14
- {sourceindex-0.1.2 → sourceindex-0.1.4}/pyproject.toml +24 -1
- {sourceindex-0.1.2 → sourceindex-0.1.4}/sourceindex/__init__.py +1 -1
- sourceindex-0.1.4/sourceindex/build/linerange/anchor.py +220 -0
- {sourceindex-0.1.2 → sourceindex-0.1.4}/sourceindex/build/linerange/python_ast.py +21 -33
- {sourceindex-0.1.2 → sourceindex-0.1.4}/sourceindex/build/linerange/treesitter.py +24 -73
- {sourceindex-0.1.2 → sourceindex-0.1.4}/sourceindex/cli/__init__.py +5 -4
- {sourceindex-0.1.2 → sourceindex-0.1.4}/sourceindex/cli/api_key.py +87 -22
- {sourceindex-0.1.2 → sourceindex-0.1.4}/sourceindex/cli/commands.py +61 -1
- sourceindex-0.1.4/sourceindex/cli/consent.py +118 -0
- {sourceindex-0.1.2 → sourceindex-0.1.4}/sourceindex/cli/install.py +270 -50
- sourceindex-0.1.4/sourceindex/cli/upgrade.py +176 -0
- sourceindex-0.1.4/sourceindex/daemon/client.py +264 -0
- {sourceindex-0.1.2 → sourceindex-0.1.4}/sourceindex/daemon/lifecycle.py +7 -7
- {sourceindex-0.1.2 → sourceindex-0.1.4}/sourceindex/daemon/protocol.py +5 -0
- {sourceindex-0.1.2 → sourceindex-0.1.4}/sourceindex/daemon/server.py +79 -39
- {sourceindex-0.1.2 → sourceindex-0.1.4}/sourceindex/lib/env.py +15 -0
- sourceindex-0.1.2/sourceindex/daemon/client.py +0 -153
- {sourceindex-0.1.2 → sourceindex-0.1.4}/.gitignore +0 -0
- {sourceindex-0.1.2 → sourceindex-0.1.4}/sourceindex/build/__init__.py +0 -0
- {sourceindex-0.1.2 → sourceindex-0.1.4}/sourceindex/build/indexer.py +0 -0
- {sourceindex-0.1.2 → sourceindex-0.1.4}/sourceindex/build/linerange/__init__.py +0 -0
- {sourceindex-0.1.2 → sourceindex-0.1.4}/sourceindex/build/prompts.py +0 -0
- {sourceindex-0.1.2 → sourceindex-0.1.4}/sourceindex/build/state.py +0 -0
- {sourceindex-0.1.2 → sourceindex-0.1.4}/sourceindex/build/walker.py +0 -0
- {sourceindex-0.1.2 → sourceindex-0.1.4}/sourceindex/claudecode/__init__.py +0 -0
- {sourceindex-0.1.2 → sourceindex-0.1.4}/sourceindex/claudecode/savings.py +0 -0
- {sourceindex-0.1.2 → sourceindex-0.1.4}/sourceindex/claudecode/savings_summary.py +0 -0
- {sourceindex-0.1.2 → sourceindex-0.1.4}/sourceindex/claudecode/statusline.py +0 -0
- {sourceindex-0.1.2 → sourceindex-0.1.4}/sourceindex/cli/__main__.py +0 -0
- {sourceindex-0.1.2 → sourceindex-0.1.4}/sourceindex/daemon/__init__.py +0 -0
- {sourceindex-0.1.2 → sourceindex-0.1.4}/sourceindex/daemon/crypto.py +0 -0
- {sourceindex-0.1.2 → sourceindex-0.1.4}/sourceindex/daemon/keyring_store.py +0 -0
- {sourceindex-0.1.2 → sourceindex-0.1.4}/sourceindex/daemon/store.py +0 -0
- {sourceindex-0.1.2 → sourceindex-0.1.4}/sourceindex/lib/__init__.py +0 -0
- {sourceindex-0.1.2 → sourceindex-0.1.4}/sourceindex/lib/backend.py +0 -0
- {sourceindex-0.1.2 → sourceindex-0.1.4}/sourceindex/lib/cost.py +0 -0
- {sourceindex-0.1.2 → sourceindex-0.1.4}/sourceindex/lib/errors.py +0 -0
- {sourceindex-0.1.2 → sourceindex-0.1.4}/sourceindex/lib/git.py +0 -0
- {sourceindex-0.1.2 → sourceindex-0.1.4}/sourceindex/lib/languages.py +0 -0
- {sourceindex-0.1.2 → sourceindex-0.1.4}/sourceindex/lib/llm.py +0 -0
- {sourceindex-0.1.2 → sourceindex-0.1.4}/sourceindex/lib/log.py +0 -0
- {sourceindex-0.1.2 → sourceindex-0.1.4}/sourceindex/lib/registry.py +0 -0
- {sourceindex-0.1.2 → sourceindex-0.1.4}/sourceindex/lib/timing.py +0 -0
- {sourceindex-0.1.2 → sourceindex-0.1.4}/sourceindex/search/__init__.py +0 -0
- {sourceindex-0.1.2 → sourceindex-0.1.4}/sourceindex/search/experiments.py +0 -0
- {sourceindex-0.1.2 → sourceindex-0.1.4}/sourceindex/search/imports.py +0 -0
- {sourceindex-0.1.2 → sourceindex-0.1.4}/sourceindex/search/passes.py +0 -0
- {sourceindex-0.1.2 → sourceindex-0.1.4}/sourceindex/search/prompts.py +0 -0
- {sourceindex-0.1.2 → sourceindex-0.1.4}/sourceindex/search/roadmap.py +0 -0
|
@@ -10,8 +10,8 @@ weird" — would be hugely helpful to hear. No feedback is too small.
|
|
|
10
10
|
## What it is
|
|
11
11
|
|
|
12
12
|
When you ask a coding agent to fix or build something, it has to find the
|
|
13
|
-
relevant code first. By default Claude Code and
|
|
14
|
-
your session. SourceIndex does it ahead of time so the agent jumps straight
|
|
13
|
+
relevant code first. By default Claude Code, opencode, and Codex do that
|
|
14
|
+
live, in your session. SourceIndex does it ahead of time so the agent jumps straight
|
|
15
15
|
to the right files.
|
|
16
16
|
|
|
17
17
|
**Best on an existing repo you didn't fully write yourself.** Not very useful
|
|
@@ -22,8 +22,8 @@ on a brand-new empty project.
|
|
|
22
22
|
You'll need Python 3.10+ and a SourceIndex API key (starts with `sk-si-`).
|
|
23
23
|
Apply for access at https://sourceindex.dev/access.
|
|
24
24
|
|
|
25
|
-
**Easiest path:** open this file in Claude Code or
|
|
26
|
-
sourceindex up in this repo." It can run the steps for you.
|
|
25
|
+
**Easiest path:** open this file in Claude Code, opencode, or Codex and tell
|
|
26
|
+
it "set sourceindex up in this repo." It can run the steps for you.
|
|
27
27
|
|
|
28
28
|
If you'd rather do it by hand:
|
|
29
29
|
|
|
@@ -33,16 +33,18 @@ cd /path/to/your/repo
|
|
|
33
33
|
sourceindex init # auto-detects your agent(s)
|
|
34
34
|
sourceindex init claude # set up Claude Code only
|
|
35
35
|
sourceindex init opencode # set up opencode only
|
|
36
|
+
sourceindex init codex # set up Codex only
|
|
36
37
|
```
|
|
37
38
|
|
|
38
39
|
It'll prompt for the key, set up some git hooks plus a subagent for the
|
|
39
40
|
coding agent your repo already uses (a `CLAUDE.md` or `.claude/` means
|
|
40
|
-
Claude Code;
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
41
|
+
Claude Code; a `.opencode/` means opencode; a `.codex/` means Codex; an
|
|
42
|
+
`AGENTS.md` alone means both opencode and Codex; none of those means Claude
|
|
43
|
+
Code), and scan your repo. A few minutes, once per repo. You can also name
|
|
44
|
+
several at once (`sourceindex init claude opencode codex`), and it's safe
|
|
45
|
+
to re-run init later to add another agent.
|
|
46
|
+
If you don't have a `CLAUDE.md` (Claude Code) or `AGENTS.md` (opencode,
|
|
47
|
+
Codex) yet, run `/init` in your agent first.
|
|
46
48
|
|
|
47
49
|
To replace a rotated or expired key later:
|
|
48
50
|
|
|
@@ -68,8 +70,8 @@ global default.
|
|
|
68
70
|
|
|
69
71
|
## Day-to-day
|
|
70
72
|
|
|
71
|
-
Nothing. Open the repo in Claude Code or
|
|
72
|
-
SourceIndex on its own.
|
|
73
|
+
Nothing. Open the repo in Claude Code, opencode, or Codex as usual — it'll
|
|
74
|
+
use SourceIndex on its own.
|
|
73
75
|
|
|
74
76
|
## Feedback I'd love
|
|
75
77
|
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
SourceIndex — Software License Agreement
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 MANTEON PTE. LTD. (Singapore). All rights reserved.
|
|
4
|
+
|
|
5
|
+
SourceIndex is commercial software. It is licensed, not sold. By installing,
|
|
6
|
+
copying, or using it you agree to this Agreement. If you do not agree, do not
|
|
7
|
+
install or use it.
|
|
8
|
+
|
|
9
|
+
1. DEFINITIONS
|
|
10
|
+
|
|
11
|
+
"Licensor" means MANTEON PTE. LTD., a company incorporated in the Republic
|
|
12
|
+
of Singapore. "Software" means the SourceIndex program distributed by the
|
|
13
|
+
Licensor, in source or object form, together with its documentation.
|
|
14
|
+
"You" means the individual or entity exercising rights under this
|
|
15
|
+
Agreement.
|
|
16
|
+
|
|
17
|
+
2. GRANT OF LICENSE
|
|
18
|
+
|
|
19
|
+
Subject to Your compliance with this Agreement, the Licensor grants You a
|
|
20
|
+
limited, non-exclusive, non-transferable, non-sublicensable, revocable
|
|
21
|
+
licence to install and use unmodified copies of the Software for Your own
|
|
22
|
+
internal purposes.
|
|
23
|
+
|
|
24
|
+
No other rights are granted. All rights not expressly granted are reserved
|
|
25
|
+
by the Licensor.
|
|
26
|
+
|
|
27
|
+
3. RESTRICTIONS
|
|
28
|
+
|
|
29
|
+
Except to the extent that this restriction is prohibited by applicable law,
|
|
30
|
+
or expressly permitted by the Licensor in writing, You may not:
|
|
31
|
+
|
|
32
|
+
(a) distribute, publish, sublicense, sell, rent, lease, lend, or otherwise
|
|
33
|
+
make the Software available to any third party;
|
|
34
|
+
(b) modify the Software or create derivative works from it;
|
|
35
|
+
(c) reverse engineer, decompile, or disassemble the Software, or otherwise
|
|
36
|
+
attempt to derive its source code, methods, or prompts, save to the
|
|
37
|
+
extent applicable law confers a non-excludable right to do so for
|
|
38
|
+
interoperability purposes;
|
|
39
|
+
(d) use the Software, or any information derived from it, to develop,
|
|
40
|
+
train, or improve a product or service that competes with it;
|
|
41
|
+
(e) remove, obscure, or alter any copyright, trademark, or other
|
|
42
|
+
proprietary notice; or
|
|
43
|
+
(f) circumvent or disable any licensing, metering, or security mechanism.
|
|
44
|
+
|
|
45
|
+
The Software is distributed in a human-readable form because it is written
|
|
46
|
+
in an interpreted language. That fact does not grant any right beyond
|
|
47
|
+
Clause 2, and does not make the Software open source.
|
|
48
|
+
|
|
49
|
+
4. OWNERSHIP
|
|
50
|
+
|
|
51
|
+
The Software is protected by copyright and other intellectual property
|
|
52
|
+
laws. The Licensor and its licensors retain all right, title, and interest
|
|
53
|
+
in and to the Software, including all prompts, templates, indexes, models
|
|
54
|
+
of operation, and other content contained in it.
|
|
55
|
+
|
|
56
|
+
5. THIRD-PARTY COMPONENTS
|
|
57
|
+
|
|
58
|
+
The Software depends on third-party components distributed under their own
|
|
59
|
+
licences. Those components are licensed to You by their respective owners
|
|
60
|
+
under those licences, not under this Agreement, and nothing here limits
|
|
61
|
+
Your rights under them.
|
|
62
|
+
|
|
63
|
+
6. HOSTED SERVICES
|
|
64
|
+
|
|
65
|
+
The Software may communicate with services operated by the Licensor. Use of
|
|
66
|
+
those services is governed by separate terms and may require a valid
|
|
67
|
+
subscription or API credential. The Licensor may modify, suspend, or
|
|
68
|
+
discontinue those services at any time.
|
|
69
|
+
|
|
70
|
+
7. NO WARRANTY
|
|
71
|
+
|
|
72
|
+
THE SOFTWARE IS PROVIDED "AS IS" AND "AS AVAILABLE", WITHOUT WARRANTY OF
|
|
73
|
+
ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE IMPLIED
|
|
74
|
+
WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE,
|
|
75
|
+
SATISFACTORY QUALITY, AND NON-INFRINGEMENT. THE LICENSOR DOES NOT WARRANT
|
|
76
|
+
THAT THE SOFTWARE WILL BE UNINTERRUPTED, ERROR-FREE, OR THAT ITS OUTPUT
|
|
77
|
+
WILL BE ACCURATE OR COMPLETE.
|
|
78
|
+
|
|
79
|
+
8. LIMITATION OF LIABILITY
|
|
80
|
+
|
|
81
|
+
TO THE MAXIMUM EXTENT PERMITTED BY LAW, THE LICENSOR SHALL NOT BE LIABLE
|
|
82
|
+
FOR ANY INDIRECT, INCIDENTAL, SPECIAL, CONSEQUENTIAL, OR EXEMPLARY
|
|
83
|
+
DAMAGES, OR FOR ANY LOSS OF PROFITS, REVENUE, DATA, OR GOODWILL, ARISING
|
|
84
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE, EVEN IF ADVISED OF THE
|
|
85
|
+
POSSIBILITY OF SUCH DAMAGES.
|
|
86
|
+
|
|
87
|
+
THE LICENSOR'S TOTAL AGGREGATE LIABILITY ARISING OUT OF OR IN CONNECTION
|
|
88
|
+
WITH THIS AGREEMENT SHALL NOT EXCEED THE GREATER OF (a) THE AMOUNTS PAID BY
|
|
89
|
+
YOU TO THE LICENSOR FOR THE SOFTWARE IN THE TWELVE MONTHS PRECEDING THE
|
|
90
|
+
CLAIM, OR (b) ONE HUNDRED SINGAPORE DOLLARS (SGD 100).
|
|
91
|
+
|
|
92
|
+
Nothing in this Agreement excludes or limits liability that cannot lawfully
|
|
93
|
+
be excluded or limited, including liability for death or personal injury
|
|
94
|
+
caused by negligence, or for fraud or fraudulent misrepresentation.
|
|
95
|
+
|
|
96
|
+
9. TERMINATION
|
|
97
|
+
|
|
98
|
+
This licence terminates automatically if You breach any term of it. On
|
|
99
|
+
termination You must cease all use of the Software and destroy all copies
|
|
100
|
+
in Your possession. Clauses 4, 7, 8, and 10 survive termination.
|
|
101
|
+
|
|
102
|
+
10. GOVERNING LAW AND JURISDICTION
|
|
103
|
+
|
|
104
|
+
This Agreement is governed by the laws of the Republic of Singapore,
|
|
105
|
+
without regard to its conflict of laws rules. The courts of Singapore
|
|
106
|
+
have exclusive jurisdiction over any dispute arising out of or in
|
|
107
|
+
connection with it.
|
|
108
|
+
|
|
109
|
+
11. ENTIRE AGREEMENT
|
|
110
|
+
|
|
111
|
+
This Agreement is the entire agreement between You and the Licensor
|
|
112
|
+
regarding the Software, and supersedes any prior understanding. If any
|
|
113
|
+
provision is held unenforceable, the remainder stays in effect.
|
|
114
|
+
|
|
115
|
+
For licensing enquiries, contact MANTEON PTE. LTD.
|
|
@@ -1,7 +1,14 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
2
|
Name: SourceIndex
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.4
|
|
4
4
|
Summary: Codebase index for agentic coding
|
|
5
|
+
Author: MANTEON PTE. LTD.
|
|
6
|
+
License-Expression: LicenseRef-Proprietary
|
|
7
|
+
License-File: LICENSE
|
|
8
|
+
Classifier: Development Status :: 3 - Alpha
|
|
9
|
+
Classifier: Intended Audience :: Developers
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
5
12
|
Requires-Python: >=3.10
|
|
6
13
|
Requires-Dist: cryptography>=42
|
|
7
14
|
Requires-Dist: keyring>=24
|
|
@@ -16,6 +23,9 @@ Requires-Dist: tree-sitter-ruby>=0.23
|
|
|
16
23
|
Requires-Dist: tree-sitter-rust>=0.23
|
|
17
24
|
Requires-Dist: tree-sitter-typescript>=0.23
|
|
18
25
|
Requires-Dist: tree-sitter>=0.23
|
|
26
|
+
Provides-Extra: test
|
|
27
|
+
Requires-Dist: pytest; extra == 'test'
|
|
28
|
+
Requires-Dist: tomli; (python_version < '3.11') and extra == 'test'
|
|
19
29
|
Description-Content-Type: text/markdown
|
|
20
30
|
|
|
21
31
|
# Getting Started with SourceIndex
|
|
@@ -30,8 +40,8 @@ weird" — would be hugely helpful to hear. No feedback is too small.
|
|
|
30
40
|
## What it is
|
|
31
41
|
|
|
32
42
|
When you ask a coding agent to fix or build something, it has to find the
|
|
33
|
-
relevant code first. By default Claude Code and
|
|
34
|
-
your session. SourceIndex does it ahead of time so the agent jumps straight
|
|
43
|
+
relevant code first. By default Claude Code, opencode, and Codex do that
|
|
44
|
+
live, in your session. SourceIndex does it ahead of time so the agent jumps straight
|
|
35
45
|
to the right files.
|
|
36
46
|
|
|
37
47
|
**Best on an existing repo you didn't fully write yourself.** Not very useful
|
|
@@ -42,8 +52,8 @@ on a brand-new empty project.
|
|
|
42
52
|
You'll need Python 3.10+ and a SourceIndex API key (starts with `sk-si-`).
|
|
43
53
|
Apply for access at https://sourceindex.dev/access.
|
|
44
54
|
|
|
45
|
-
**Easiest path:** open this file in Claude Code or
|
|
46
|
-
sourceindex up in this repo." It can run the steps for you.
|
|
55
|
+
**Easiest path:** open this file in Claude Code, opencode, or Codex and tell
|
|
56
|
+
it "set sourceindex up in this repo." It can run the steps for you.
|
|
47
57
|
|
|
48
58
|
If you'd rather do it by hand:
|
|
49
59
|
|
|
@@ -53,16 +63,18 @@ cd /path/to/your/repo
|
|
|
53
63
|
sourceindex init # auto-detects your agent(s)
|
|
54
64
|
sourceindex init claude # set up Claude Code only
|
|
55
65
|
sourceindex init opencode # set up opencode only
|
|
66
|
+
sourceindex init codex # set up Codex only
|
|
56
67
|
```
|
|
57
68
|
|
|
58
69
|
It'll prompt for the key, set up some git hooks plus a subagent for the
|
|
59
70
|
coding agent your repo already uses (a `CLAUDE.md` or `.claude/` means
|
|
60
|
-
Claude Code;
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
71
|
+
Claude Code; a `.opencode/` means opencode; a `.codex/` means Codex; an
|
|
72
|
+
`AGENTS.md` alone means both opencode and Codex; none of those means Claude
|
|
73
|
+
Code), and scan your repo. A few minutes, once per repo. You can also name
|
|
74
|
+
several at once (`sourceindex init claude opencode codex`), and it's safe
|
|
75
|
+
to re-run init later to add another agent.
|
|
76
|
+
If you don't have a `CLAUDE.md` (Claude Code) or `AGENTS.md` (opencode,
|
|
77
|
+
Codex) yet, run `/init` in your agent first.
|
|
66
78
|
|
|
67
79
|
To replace a rotated or expired key later:
|
|
68
80
|
|
|
@@ -88,8 +100,8 @@ global default.
|
|
|
88
100
|
|
|
89
101
|
## Day-to-day
|
|
90
102
|
|
|
91
|
-
Nothing. Open the repo in Claude Code or
|
|
92
|
-
SourceIndex on its own.
|
|
103
|
+
Nothing. Open the repo in Claude Code, opencode, or Codex as usual — it'll
|
|
104
|
+
use SourceIndex on its own.
|
|
93
105
|
|
|
94
106
|
## Feedback I'd love
|
|
95
107
|
|
|
@@ -4,10 +4,25 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "SourceIndex"
|
|
7
|
-
version = "0.1.
|
|
7
|
+
version = "0.1.4"
|
|
8
8
|
description = "Codebase index for agentic coding"
|
|
9
9
|
readme = "GETTING_STARTED.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
11
|
+
authors = [{ name = "MANTEON PTE. LTD." }]
|
|
12
|
+
# Proprietary: the published wheel ships readable .py because Python is
|
|
13
|
+
# interpreted, not because the source is licensed for reuse. LicenseRef-* is
|
|
14
|
+
# how PEP 639 spells a licence with no SPDX identifier.
|
|
15
|
+
license = "LicenseRef-Proprietary"
|
|
16
|
+
license-files = ["LICENSE"]
|
|
17
|
+
# No "License ::" classifier: PEP 639 deprecates them, and pairing one with
|
|
18
|
+
# License-Expression is the case where tools are permitted to reject the
|
|
19
|
+
# upload. The expression above is the authoritative declaration.
|
|
20
|
+
classifiers = [
|
|
21
|
+
"Development Status :: 3 - Alpha",
|
|
22
|
+
"Intended Audience :: Developers",
|
|
23
|
+
"Programming Language :: Python :: 3",
|
|
24
|
+
"Topic :: Software Development :: Libraries",
|
|
25
|
+
]
|
|
11
26
|
dependencies = [
|
|
12
27
|
"litellm>=1.50",
|
|
13
28
|
"tree-sitter>=0.23",
|
|
@@ -32,6 +47,13 @@ dependencies = [
|
|
|
32
47
|
"tree-sitter-typescript>=0.23",
|
|
33
48
|
]
|
|
34
49
|
|
|
50
|
+
[project.optional-dependencies]
|
|
51
|
+
test = [
|
|
52
|
+
"pytest",
|
|
53
|
+
# tomllib backport for the TOML-validity tests on the 3.10 floor.
|
|
54
|
+
"tomli; python_version < '3.11'",
|
|
55
|
+
]
|
|
56
|
+
|
|
35
57
|
[project.scripts]
|
|
36
58
|
sourceindex = "sourceindex.cli:main"
|
|
37
59
|
|
|
@@ -46,4 +68,5 @@ include = [
|
|
|
46
68
|
"sourceindex/",
|
|
47
69
|
"pyproject.toml",
|
|
48
70
|
"GETTING_STARTED.md",
|
|
71
|
+
"LICENSE",
|
|
49
72
|
]
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
"""Anchor tier-2 entries to parser truth, name-first.
|
|
2
|
+
|
|
3
|
+
Shared by the python-ast and tree-sitter fixers. This is a PERMANENT
|
|
4
|
+
build-pipeline stage (every ``init`` runs it right after the LLM responds),
|
|
5
|
+
not a repair utility — LLM-emitted line numbers are unreliable for every
|
|
6
|
+
builder model, and this stage is the deterministic guarantee that cached
|
|
7
|
+
ranges are parser truth whenever the symbol exists. On perfect LLM output
|
|
8
|
+
it is a no-op.
|
|
9
|
+
|
|
10
|
+
The original fixers were format-first: a strict regex had to recognize the
|
|
11
|
+
whole entry line (canonical ``name (Lstart-Lend): desc``) before any range
|
|
12
|
+
correction happened. Models that drift off-format — Gemma emits ``(23:18)``
|
|
13
|
+
colon ranges ~6% of the time and Go receiver names like ``(b *Backend).foo``
|
|
14
|
+
— silently bypassed the fixer, which is how thousands of reversed/truncated
|
|
15
|
+
ranges survived into built caches.
|
|
16
|
+
|
|
17
|
+
This module inverts the order into two parts:
|
|
18
|
+
|
|
19
|
+
1. The language backend supplies parser truth: ``{qualified_name: [(s, e), …]}``
|
|
20
|
+
(lists, because the same name can legitimately define several ranges).
|
|
21
|
+
2. Each entry line is matched by NAME, and its range is converged to truth:
|
|
22
|
+
- any recognizable range token (dash, colon, en-dash, missing ``L``,
|
|
23
|
+
reversed, single-line) is REPLACED with the truth range;
|
|
24
|
+
- a matched name with no range token at all gets the truth range INSERTED;
|
|
25
|
+
- a name absent from truth whose range is clearly broken (reversed,
|
|
26
|
+
``L1-L1`` placeholder, or missing) is SNAPPED to its innermost existing
|
|
27
|
+
container's range (e.g. a @dataclass-generated ``__init__`` → the class);
|
|
28
|
+
- otherwise the line is left untouched — never dropped, so a well-formed
|
|
29
|
+
range for something the parser can't see (module constants, generated
|
|
30
|
+
members) keeps the LLM's answer.
|
|
31
|
+
|
|
32
|
+
Multiple definitions sharing a name: qualified keys are preferred over bare
|
|
33
|
+
ones by ``_lookup``; among remaining candidates we pick the one whose start is
|
|
34
|
+
closest to the LLM's reported start, falling back to the first definition in
|
|
35
|
+
file order when the entry carried no usable range.
|
|
36
|
+
"""
|
|
37
|
+
from __future__ import annotations
|
|
38
|
+
|
|
39
|
+
import re
|
|
40
|
+
from typing import Callable, Dict, List, Optional, Tuple
|
|
41
|
+
|
|
42
|
+
Range = Tuple[int, int]
|
|
43
|
+
Truth = Dict[str, List[Range]]
|
|
44
|
+
Resolver = Callable[[str, Optional[int], bool], Optional[Range]]
|
|
45
|
+
|
|
46
|
+
# Any parenthesized line token: (L12-L40) (12-40) (23:18) (L12:L40) (L12) (12)
|
|
47
|
+
# plus en/em-dash and tilde separators seen in off-format LLM output.
|
|
48
|
+
_RANGE_TOKEN = re.compile(
|
|
49
|
+
r"\(\s*L?(?P<s>\d+)\s*(?:(?P<sep>[-:–—~])\s*L?(?P<e>\d+))?\s*\)"
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
_BARE_ID = re.compile(r"^[A-Za-z_$~][\w$]*$")
|
|
53
|
+
|
|
54
|
+
# Leading tokens the LLM sometimes folds into the name ("func Foo", "pub fn bar").
|
|
55
|
+
_NAME_KEYWORDS = {
|
|
56
|
+
"func", "fn", "def", "function", "method", "class", "interface", "type",
|
|
57
|
+
"struct", "enum", "trait", "impl", "pub", "static", "async", "export",
|
|
58
|
+
"const", "var", "let", "val", "public", "private", "protected", "abstract",
|
|
59
|
+
"override", "suspend", "inline", "internal", "final", "new", "get", "set",
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
_TRAILING_SUFFIXES = (
|
|
63
|
+
".get", ".set", ".<init>", ".init", ".constructor", ".ctor",
|
|
64
|
+
".closure", ".lambda", ".function_",
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
_RECEIVER_RE = re.compile(r"^\([^)]*\)\s*\.") # Go receiver: (b *Backend).foo
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def normalize_name(qn: str) -> str:
|
|
71
|
+
"""Normalize separators to ``.``, drop receiver parens / keyword prefixes /
|
|
72
|
+
accessor suffixes, so lookup keys converge across LLM naming styles."""
|
|
73
|
+
qn = qn.strip()
|
|
74
|
+
qn = _RECEIVER_RE.sub("", qn) # "(b *Backend).foo" -> "foo"
|
|
75
|
+
qn = qn.replace("::", ".").replace("#", ".")
|
|
76
|
+
if " " in qn:
|
|
77
|
+
qn = qn.split()[-1]
|
|
78
|
+
while qn.endswith(_TRAILING_SUFFIXES):
|
|
79
|
+
for suf in _TRAILING_SUFFIXES:
|
|
80
|
+
if qn.endswith(suf):
|
|
81
|
+
qn = qn[: -len(suf)]
|
|
82
|
+
break
|
|
83
|
+
while ".." in qn:
|
|
84
|
+
qn = qn.replace("..", ".")
|
|
85
|
+
return qn.strip(".")
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _bare_tail(name: str) -> str:
|
|
89
|
+
tail = normalize_name(name).rsplit(".", 1)[-1]
|
|
90
|
+
return re.sub(r"<.*>$", "", tail) # strip generics: Foo<T> -> Foo
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _plausible_name(name: str) -> bool:
|
|
94
|
+
"""Loose gate for heads that sit directly in front of a range token."""
|
|
95
|
+
name = name.strip()
|
|
96
|
+
if not name or len(name) > 160 or "/" in name or "`" in name:
|
|
97
|
+
return False
|
|
98
|
+
if " " in name and not _RECEIVER_RE.match(name):
|
|
99
|
+
toks = name.split()
|
|
100
|
+
if not all(t.lower() in _NAME_KEYWORDS for t in toks[:-1]):
|
|
101
|
+
return False
|
|
102
|
+
return bool(_BARE_ID.match(_bare_tail(name)))
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _strict_name(name: str) -> bool:
|
|
106
|
+
"""Tight gate for the no-range insertion path (highest prose risk)."""
|
|
107
|
+
name = name.strip()
|
|
108
|
+
if not name or " " in name and not _RECEIVER_RE.match(name):
|
|
109
|
+
return False
|
|
110
|
+
if name[0] in "#-*>|":
|
|
111
|
+
return False
|
|
112
|
+
return _plausible_name(name)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _lookup(truth: Truth, qn: str) -> List[Range]:
|
|
116
|
+
"""Qualified match first; peel leading segments; finally bare name."""
|
|
117
|
+
norm = normalize_name(qn)
|
|
118
|
+
if not norm:
|
|
119
|
+
return []
|
|
120
|
+
if norm in truth:
|
|
121
|
+
return truth[norm]
|
|
122
|
+
parts = norm.split(".")
|
|
123
|
+
# Peel LLM-invented leading prefixes (mypkg.Foo.bar -> Foo.bar), but only
|
|
124
|
+
# accept still-qualified suffixes here — the bare tail is gated below.
|
|
125
|
+
for i in range(1, len(parts)):
|
|
126
|
+
suffix = ".".join(parts[i:])
|
|
127
|
+
if "." in suffix and suffix in truth:
|
|
128
|
+
return truth[suffix]
|
|
129
|
+
# Bare-name fallback — but NOT when the entry's own container exists in
|
|
130
|
+
# truth: then this member genuinely isn't defined there (generated or
|
|
131
|
+
# hallucinated), and borrowing a same-named member of another container
|
|
132
|
+
# would mis-anchor it (Dataclass.__init__ -> SomeOtherClass.__init__).
|
|
133
|
+
for i in range(len(parts) - 1):
|
|
134
|
+
container = ".".join(parts[i:-1])
|
|
135
|
+
if container and container in truth:
|
|
136
|
+
return []
|
|
137
|
+
return truth.get(_bare_tail(qn), [])
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def make_resolver(truth: Truth) -> Resolver:
|
|
141
|
+
"""Build a ``resolve(name, reported_start, allow_container)`` over truth.
|
|
142
|
+
|
|
143
|
+
``allow_container=True`` additionally tries the name's enclosing scopes
|
|
144
|
+
(``A.B.c`` → ``A.B`` → ``A``) — used only when the entry's own range is
|
|
145
|
+
already known to be broken, so a fabricated member at least points the
|
|
146
|
+
agent at the right class body.
|
|
147
|
+
"""
|
|
148
|
+
|
|
149
|
+
def resolve(name: str, reported_start: Optional[int] = None,
|
|
150
|
+
allow_container: bool = False) -> Optional[Range]:
|
|
151
|
+
cands = _lookup(truth, name)
|
|
152
|
+
if not cands and allow_container:
|
|
153
|
+
parent = normalize_name(name)
|
|
154
|
+
while "." in parent and not cands:
|
|
155
|
+
parent = parent.rsplit(".", 1)[0]
|
|
156
|
+
cands = truth.get(parent, [])
|
|
157
|
+
if not cands:
|
|
158
|
+
return None
|
|
159
|
+
if len(cands) > 1 and reported_start is not None:
|
|
160
|
+
return min(cands, key=lambda c: abs(c[0] - reported_start))
|
|
161
|
+
return cands[0]
|
|
162
|
+
|
|
163
|
+
return resolve
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def _is_broken(s: int, e: Optional[int]) -> bool:
|
|
167
|
+
if e is None: # single-line token: not broken, but improvable
|
|
168
|
+
return False
|
|
169
|
+
return e < s or (s == 1 and e == 1)
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def anchor_entries(functions_text: str, resolve: Resolver) -> str:
|
|
173
|
+
"""Apply the name-first convergence rules line by line (never drops a line)."""
|
|
174
|
+
out: List[str] = []
|
|
175
|
+
for line in functions_text.splitlines():
|
|
176
|
+
out.append(_anchor_line(line, resolve))
|
|
177
|
+
joined = "\n".join(out)
|
|
178
|
+
if functions_text.endswith("\n") and not joined.endswith("\n"):
|
|
179
|
+
joined += "\n"
|
|
180
|
+
return joined
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def _anchor_line(line: str, resolve: Resolver) -> str:
|
|
184
|
+
m = _RANGE_TOKEN.search(line)
|
|
185
|
+
if m:
|
|
186
|
+
head = line[: m.start()].rstrip()
|
|
187
|
+
after = line[m.end():]
|
|
188
|
+
# Real entries read "name (range): desc" — require the colon.
|
|
189
|
+
if head and re.match(r"\s*:", after) and _plausible_name(head.strip()):
|
|
190
|
+
s = int(m.group("s"))
|
|
191
|
+
e = int(m.group("e")) if m.group("e") else None
|
|
192
|
+
best = resolve(head.strip(), s, False)
|
|
193
|
+
if best is None:
|
|
194
|
+
# Name unknown to the parser: snap to the enclosing container
|
|
195
|
+
# when the range is clearly broken OR points outside it.
|
|
196
|
+
cont = resolve(head.strip(), s, True)
|
|
197
|
+
if cont is not None and (
|
|
198
|
+
_is_broken(s, e) or not (cont[0] <= s <= cont[1])
|
|
199
|
+
):
|
|
200
|
+
best = cont
|
|
201
|
+
if best is None:
|
|
202
|
+
# No truth to converge to — still canonicalize the token so
|
|
203
|
+
# downstream dash-only parsers (search pass, compliance) see it.
|
|
204
|
+
lo, hi = (s, e if e is not None else s)
|
|
205
|
+
if hi < lo:
|
|
206
|
+
lo, hi = hi, lo
|
|
207
|
+
best = (lo, hi)
|
|
208
|
+
return f"{head} (L{best[0]}-L{best[1]}){after}"
|
|
209
|
+
# fall through: token may live in the description of a range-less entry
|
|
210
|
+
|
|
211
|
+
head, sep, rest = line.partition(":")
|
|
212
|
+
if not sep:
|
|
213
|
+
return line
|
|
214
|
+
name = head.strip()
|
|
215
|
+
if not _strict_name(name):
|
|
216
|
+
return line
|
|
217
|
+
best = resolve(name, None, False) or resolve(name, None, True)
|
|
218
|
+
if best is None:
|
|
219
|
+
return line
|
|
220
|
+
return f"{head.rstrip()} (L{best[0]}-L{best[1]}):{rest}"
|
|
@@ -8,28 +8,38 @@ whenever the qualified name matches. Description quality is unchanged.
|
|
|
8
8
|
import ast
|
|
9
9
|
import re
|
|
10
10
|
|
|
11
|
+
from .anchor import Truth, make_resolver, anchor_entries
|
|
12
|
+
|
|
11
13
|
|
|
12
14
|
_LINE_RANGE_RE = re.compile(r"\(L?\d+\s*-\s*L?\d+\)")
|
|
13
15
|
_ENTRY_RE = re.compile(r"^([\w.:_<>]+)\s+\(L?\d+\s*-\s*L?\d+\)\s*:?")
|
|
14
16
|
|
|
15
17
|
|
|
16
|
-
def _python_ast_truth(source: str) ->
|
|
17
|
-
"""{qualified_name: (start_lineno, end_lineno)} for every def in source.
|
|
18
|
+
def _python_ast_truth(source: str) -> Truth:
|
|
19
|
+
"""{qualified_name: [(start_lineno, end_lineno), …]} for every def in source.
|
|
18
20
|
|
|
19
21
|
Methods qualified as Class.method, nested funcs as Outer.inner AND
|
|
20
|
-
Outer.<locals>.inner so either naming convention matches.
|
|
22
|
+
Outer.<locals>.inner so either naming convention matches. A bare-name
|
|
23
|
+
fallback key (last segment) is also populated so entries that drop the
|
|
24
|
+
class prefix still resolve; the resolver prefers qualified keys."""
|
|
21
25
|
try:
|
|
22
26
|
tree = ast.parse(source)
|
|
23
27
|
except (SyntaxError, ValueError):
|
|
24
28
|
return {}
|
|
25
29
|
|
|
26
|
-
out:
|
|
30
|
+
out: Truth = {}
|
|
31
|
+
|
|
32
|
+
def add(key: str, rng: tuple[int, int]) -> None:
|
|
33
|
+
out.setdefault(key, []).append(rng)
|
|
27
34
|
|
|
28
35
|
def walk(node, prefix: str = ""):
|
|
29
36
|
for child in ast.iter_child_nodes(node):
|
|
30
37
|
if isinstance(child, (ast.ClassDef, ast.FunctionDef, ast.AsyncFunctionDef)):
|
|
31
38
|
qname = f"{prefix}{child.name}" if prefix else child.name
|
|
32
|
-
|
|
39
|
+
rng = (child.lineno, child.end_lineno or child.lineno)
|
|
40
|
+
add(qname, rng)
|
|
41
|
+
if "." in qname:
|
|
42
|
+
add(child.name, rng)
|
|
33
43
|
walk(child, prefix=f"{qname}.")
|
|
34
44
|
if isinstance(child, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
|
35
45
|
walk(child, prefix=f"{qname}.<locals>.")
|
|
@@ -39,38 +49,16 @@ def _python_ast_truth(source: str) -> dict[str, tuple[int, int]]:
|
|
|
39
49
|
|
|
40
50
|
|
|
41
51
|
def fix_python_line_ranges(source: str, functions_text: str) -> str:
|
|
42
|
-
"""
|
|
52
|
+
"""Converge LLM-emitted line ranges to AST ground truth for Python files.
|
|
43
53
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
54
|
+
Name-first (see ``anchor``): entries are matched by qualified name and
|
|
55
|
+
their range replaced/inserted/snapped regardless of the LLM's range
|
|
56
|
+
formatting. Names AST can't see with a well-formed range are left
|
|
57
|
+
untouched — kept, never dropped."""
|
|
48
58
|
truth = _python_ast_truth(source)
|
|
49
59
|
if not truth:
|
|
50
60
|
return functions_text
|
|
51
|
-
|
|
52
|
-
out_lines: list[str] = []
|
|
53
|
-
for line in functions_text.splitlines():
|
|
54
|
-
m = _ENTRY_RE.match(line.strip())
|
|
55
|
-
if not m:
|
|
56
|
-
out_lines.append(line)
|
|
57
|
-
continue
|
|
58
|
-
qname = m.group(1).rstrip(".")
|
|
59
|
-
match_key = qname if qname in truth else None
|
|
60
|
-
if match_key is None:
|
|
61
|
-
for k in truth:
|
|
62
|
-
if k.endswith(qname) or qname.endswith(k):
|
|
63
|
-
match_key = k
|
|
64
|
-
break
|
|
65
|
-
if match_key is None:
|
|
66
|
-
out_lines.append(line)
|
|
67
|
-
continue
|
|
68
|
-
ls, le = truth[match_key]
|
|
69
|
-
new_line = _LINE_RANGE_RE.sub(f"(L{ls}-L{le})", line, count=1)
|
|
70
|
-
if match_key != qname:
|
|
71
|
-
new_line = new_line.replace(qname, match_key, 1)
|
|
72
|
-
out_lines.append(new_line)
|
|
73
|
-
return "\n".join(out_lines)
|
|
61
|
+
return anchor_entries(functions_text, make_resolver(truth))
|
|
74
62
|
|
|
75
63
|
|
|
76
64
|
def fill_missing_python_classes(source: str, functions_text: str) -> str:
|