ast-pluckit 0.1.0a1__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.
- ast_pluckit-0.1.0a1/.gitignore +17 -0
- ast_pluckit-0.1.0a1/CHANGELOG.md +86 -0
- ast_pluckit-0.1.0a1/LICENSE +21 -0
- ast_pluckit-0.1.0a1/PKG-INFO +376 -0
- ast_pluckit-0.1.0a1/README.md +335 -0
- ast_pluckit-0.1.0a1/pyproject.toml +98 -0
- ast_pluckit-0.1.0a1/src/pluckit/__init__.py +33 -0
- ast_pluckit-0.1.0a1/src/pluckit/_context.py +81 -0
- ast_pluckit-0.1.0a1/src/pluckit/_paths.py +57 -0
- ast_pluckit-0.1.0a1/src/pluckit/_sql.py +224 -0
- ast_pluckit-0.1.0a1/src/pluckit/cli.py +806 -0
- ast_pluckit-0.1.0a1/src/pluckit/mutation.py +222 -0
- ast_pluckit-0.1.0a1/src/pluckit/mutations.py +680 -0
- ast_pluckit-0.1.0a1/src/pluckit/plucker.py +121 -0
- ast_pluckit-0.1.0a1/src/pluckit/plugins/__init__.py +4 -0
- ast_pluckit-0.1.0a1/src/pluckit/plugins/base.py +62 -0
- ast_pluckit-0.1.0a1/src/pluckit/plugins/viewer.py +860 -0
- ast_pluckit-0.1.0a1/src/pluckit/selection.py +600 -0
- ast_pluckit-0.1.0a1/src/pluckit/selectors.py +366 -0
- ast_pluckit-0.1.0a1/src/pluckit/source.py +39 -0
- ast_pluckit-0.1.0a1/src/pluckit/types.py +50 -0
- ast_pluckit-0.1.0a1/tests/conftest.py +75 -0
- ast_pluckit-0.1.0a1/tests/plugins/__init__.py +0 -0
- ast_pluckit-0.1.0a1/tests/plugins/test_viewer.py +382 -0
- ast_pluckit-0.1.0a1/tests/test_cli.py +589 -0
- ast_pluckit-0.1.0a1/tests/test_mutations.py +412 -0
- ast_pluckit-0.1.0a1/tests/test_paths.py +91 -0
- ast_pluckit-0.1.0a1/tests/test_plucker.py +95 -0
- ast_pluckit-0.1.0a1/tests/test_plucker_context.py +42 -0
- ast_pluckit-0.1.0a1/tests/test_plugin_base.py +201 -0
- ast_pluckit-0.1.0a1/tests/test_selection.py +167 -0
- ast_pluckit-0.1.0a1/tests/test_selection_plugins.py +73 -0
- ast_pluckit-0.1.0a1/tests/test_selectors.py +85 -0
- ast_pluckit-0.1.0a1/tests/test_source.py +30 -0
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
__pycache__/
|
|
2
|
+
*.pyc
|
|
3
|
+
*.egg-info/
|
|
4
|
+
dist/
|
|
5
|
+
build/
|
|
6
|
+
.eggs/
|
|
7
|
+
|
|
8
|
+
# mkdocs build output
|
|
9
|
+
site/
|
|
10
|
+
|
|
11
|
+
# Training data is tracked via git lfs (see .gitattributes)
|
|
12
|
+
# Regenerate with: bash training/run_pipeline.sh generate
|
|
13
|
+
# These are intentionally committed so consumers can train without regenerating.
|
|
14
|
+
|
|
15
|
+
# Model artifacts
|
|
16
|
+
pluckit-lora/
|
|
17
|
+
*.gguf
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project are documented here. The format is based
|
|
4
|
+
on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project
|
|
5
|
+
adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
|
+
|
|
7
|
+
## [Unreleased]
|
|
8
|
+
|
|
9
|
+
## [0.1.0a1] — 2026-04-10
|
|
10
|
+
|
|
11
|
+
First public alpha. Query, view, and mutate all work end-to-end.
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
|
|
15
|
+
- **`Plucker` — a fluent entry point** that wraps a DuckDB connection, loads
|
|
16
|
+
the `sitting_duck` community extension, and exposes `find()`, `view()`, and
|
|
17
|
+
mutation methods on lazy `Selection` objects. Selections are DuckDB
|
|
18
|
+
relations that chain filters, navigation, and terminal operations without
|
|
19
|
+
materializing until necessary.
|
|
20
|
+
|
|
21
|
+
- **CSS-like selector language** (`.fn`, `.cls`, `.call`, `.fn#name`,
|
|
22
|
+
`.fn:exported`, `.fn[name^=test_]`, `.cls#Foo .fn`, `.fn:has(.call#x)`)
|
|
23
|
+
compiled to SQL WHERE fragments over sitting_duck's `read_ast()` table.
|
|
24
|
+
Supports 27 languages via tree-sitter.
|
|
25
|
+
|
|
26
|
+
- **`AstViewer` plugin** — a CSS-stylesheet-style declaration language
|
|
27
|
+
(`{ show: signature; }`, `{ show: outline; }`, `{ show: 10; }`, etc.)
|
|
28
|
+
attached to selectors. Synthesized signatures from sitting_duck's native
|
|
29
|
+
extraction columns. Multi-match signature queries collapse to a markdown
|
|
30
|
+
table automatically.
|
|
31
|
+
|
|
32
|
+
- **Mutation engine with transactional rollback.** Line-granularity splicing
|
|
33
|
+
with per-file snapshots, reverse-order application (later edits don't
|
|
34
|
+
shift earlier line numbers), and re-parse validation. Any syntax error
|
|
35
|
+
rolls back every affected file.
|
|
36
|
+
|
|
37
|
+
- **Mutation vocabulary:** `ReplaceWith`, `ScopedReplace`, `Prepend`,
|
|
38
|
+
`Append`, `Wrap`, `Unwrap`, `Remove`, `Rename`, `AddParam`, `RemoveParam`,
|
|
39
|
+
`AddArg`, `RemoveArg`, `ClearBody`, `InsertBefore`, `InsertAfter`.
|
|
40
|
+
`InsertBefore`/`InsertAfter` take a CSS selector as the anchor and resolve
|
|
41
|
+
it via a scoped AST sub-query — no heuristics.
|
|
42
|
+
|
|
43
|
+
- **`pluckit` CLI** with four subcommands:
|
|
44
|
+
- `init` — install and verify the required DuckDB community extensions.
|
|
45
|
+
- `view` — render matched code regions as markdown, reading queries from
|
|
46
|
+
argv, a file, or stdin.
|
|
47
|
+
- `find` — list matches for scripting. Four output formats: `locations`
|
|
48
|
+
(file:line:name, default), `names`, `signature` (markdown table), `json`.
|
|
49
|
+
- `edit` — apply structural mutations. Chainable within one invocation:
|
|
50
|
+
multiple operations per group, multiple groups separated by `--`,
|
|
51
|
+
with a real unified-diff preview in `--dry-run`.
|
|
52
|
+
|
|
53
|
+
- **Plugin system** — third-party plugins register new methods on
|
|
54
|
+
`Selection`, new pseudo-classes for the selector compiler, and optional
|
|
55
|
+
upgrades to existing methods (e.g., the `Calls` plugin will upgrade
|
|
56
|
+
`callers()` with import-resolved results).
|
|
57
|
+
|
|
58
|
+
- **Cross-language indent detection** for mutations in Python, C++, Go,
|
|
59
|
+
Java, TypeScript, and Rust. Body-frame indent is computed from file
|
|
60
|
+
context, not hard-coded to 4 spaces.
|
|
61
|
+
|
|
62
|
+
- **CI scaffolding** — GitHub Actions workflow runs lint, pytest, and a
|
|
63
|
+
wheel build on every push and PR.
|
|
64
|
+
|
|
65
|
+
### Infrastructure
|
|
66
|
+
|
|
67
|
+
- MIT licensed.
|
|
68
|
+
- PyPI distribution name: `ast-pluckit`. Import name, CLI name, and repo
|
|
69
|
+
name are all `pluckit`. (The bare `pluckit` PyPI name is held by an
|
|
70
|
+
abandoned 2019 project.)
|
|
71
|
+
- Python 3.10+ supported.
|
|
72
|
+
- Documentation published at [pluckit.readthedocs.io](https://pluckit.readthedocs.io).
|
|
73
|
+
|
|
74
|
+
### Known limitations
|
|
75
|
+
|
|
76
|
+
- Call graph, git history, and scope plugins are stubs — landing in v0.2.
|
|
77
|
+
- pluckit's selector compiler supports only a subset of sitting_duck's full
|
|
78
|
+
selector language. Richer features like `:calls()`, `:matches()`, and
|
|
79
|
+
`:scope()` work when calling `ast_select` directly against the underlying
|
|
80
|
+
DuckDB connection.
|
|
81
|
+
- Mutations operate at line granularity because sitting_duck's `read_ast`
|
|
82
|
+
does not yet expose byte offsets. Character-level insertions
|
|
83
|
+
(`--insert-chars`) are reserved for v0.2.
|
|
84
|
+
|
|
85
|
+
[Unreleased]: https://github.com/teaguesterling/pluckit/compare/v0.1.0a1...HEAD
|
|
86
|
+
[0.1.0a1]: https://github.com/teaguesterling/pluckit/releases/tag/v0.1.0a1
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Teague Sterling
|
|
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,376 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: ast-pluckit
|
|
3
|
+
Version: 0.1.0a1
|
|
4
|
+
Summary: A fluent API for querying, viewing, and mutating source code — CSS selectors over ASTs, backed by DuckDB.
|
|
5
|
+
Project-URL: Homepage, https://github.com/teaguesterling/pluckit
|
|
6
|
+
Project-URL: Documentation, https://pluckit.readthedocs.io
|
|
7
|
+
Project-URL: Repository, https://github.com/teaguesterling/pluckit
|
|
8
|
+
Project-URL: Issues, https://github.com/teaguesterling/pluckit/issues
|
|
9
|
+
Project-URL: Changelog, https://github.com/teaguesterling/pluckit/blob/main/CHANGELOG.md
|
|
10
|
+
Author-email: Teague Sterling <teaguesterling@gmail.com>
|
|
11
|
+
License-Expression: MIT
|
|
12
|
+
License-File: LICENSE
|
|
13
|
+
Keywords: ast,code-search,codemod,css-selectors,duckdb,refactoring,sitting-duck,static-analysis,tree-sitter
|
|
14
|
+
Classifier: Development Status :: 3 - Alpha
|
|
15
|
+
Classifier: Intended Audience :: Developers
|
|
16
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
17
|
+
Classifier: Operating System :: MacOS
|
|
18
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
19
|
+
Classifier: Programming Language :: Python :: 3
|
|
20
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
23
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
24
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
25
|
+
Classifier: Topic :: Software Development :: Code Generators
|
|
26
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
27
|
+
Classifier: Topic :: Software Development :: Quality Assurance
|
|
28
|
+
Classifier: Topic :: Utilities
|
|
29
|
+
Classifier: Typing :: Typed
|
|
30
|
+
Requires-Python: >=3.10
|
|
31
|
+
Requires-Dist: duckdb>=1.1.0
|
|
32
|
+
Provides-Extra: dev
|
|
33
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
34
|
+
Requires-Dist: ruff>=0.6; extra == 'dev'
|
|
35
|
+
Provides-Extra: docs
|
|
36
|
+
Requires-Dist: mkdocs-exclude>=1.0; extra == 'docs'
|
|
37
|
+
Requires-Dist: mkdocs-material>=9.5; extra == 'docs'
|
|
38
|
+
Requires-Dist: mkdocs>=1.6; extra == 'docs'
|
|
39
|
+
Requires-Dist: pymdown-extensions>=10.9; extra == 'docs'
|
|
40
|
+
Description-Content-Type: text/markdown
|
|
41
|
+
|
|
42
|
+
# pluckit
|
|
43
|
+
|
|
44
|
+
*A fluent API for querying, viewing, and mutating source code. CSS selectors over ASTs, backed by DuckDB.*
|
|
45
|
+
|
|
46
|
+
pluckit lets you select code with CSS-like selectors, render it as formatted
|
|
47
|
+
source regions, and apply structural mutations — all from a single fluent API
|
|
48
|
+
or a compact CLI. Under the hood it's a thin Python layer over DuckDB with
|
|
49
|
+
the [sitting_duck](https://github.com/teaguesterling/sitting_duck) AST
|
|
50
|
+
extension, so queries compile to SQL and run against tree-sitter ASTs for
|
|
51
|
+
27 languages.
|
|
52
|
+
|
|
53
|
+
- **Documentation:** [pluckit.readthedocs.io](https://pluckit.readthedocs.io)
|
|
54
|
+
- **PyPI:** [`ast-pluckit`](https://pypi.org/project/ast-pluckit/)
|
|
55
|
+
- **Changelog:** [CHANGELOG.md](CHANGELOG.md)
|
|
56
|
+
|
|
57
|
+
> **Status:** v0.1-alpha. Query, view, and mutate work end-to-end. Call graph,
|
|
58
|
+
> history, and scope plugins are v0.2.
|
|
59
|
+
|
|
60
|
+
## Install
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
pip install ast-pluckit
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
The distribution name is `ast-pluckit` (the bare `pluckit` name was taken on
|
|
67
|
+
PyPI by an abandoned 2019 project). The import name and CLI are still
|
|
68
|
+
`pluckit`:
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
pluckit view ".fn#main" src/**/*.py
|
|
72
|
+
```
|
|
73
|
+
```python
|
|
74
|
+
from pluckit import Plucker
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
For local development:
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
pip install -e .
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
pluckit needs DuckDB with the `sitting_duck` community extension. It will
|
|
84
|
+
auto-install on first use, but you can also run:
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
pluckit init
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
to install and verify the extensions eagerly and get clearer diagnostics
|
|
91
|
+
if anything fails.
|
|
92
|
+
|
|
93
|
+
## The CLI
|
|
94
|
+
|
|
95
|
+
Full reference lives in the [CLI docs](https://pluckit.readthedocs.io/en/latest/cli/).
|
|
96
|
+
The short version:
|
|
97
|
+
|
|
98
|
+
### View — render matched code regions as markdown
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
# Show the full body of a function
|
|
102
|
+
pluckit view ".fn#parse_chain" training/chain_parser.py
|
|
103
|
+
|
|
104
|
+
# Just the signature
|
|
105
|
+
pluckit view ".fn#parse_chain { show: signature; }" training/chain_parser.py
|
|
106
|
+
|
|
107
|
+
# First 10 lines
|
|
108
|
+
pluckit view ".fn#parse_chain { show: 10; }" training/chain_parser.py
|
|
109
|
+
|
|
110
|
+
# Class outline — header + method signatures
|
|
111
|
+
pluckit view ".cls#ChainSampler" training/chain_sampler.py
|
|
112
|
+
|
|
113
|
+
# Multi-rule query (CSS-stylesheet style)
|
|
114
|
+
pluckit view ".fn { show: signature; } #main { show: body; }" training/generate.py
|
|
115
|
+
|
|
116
|
+
# Read the query from stdin
|
|
117
|
+
echo ".fn[name^=test_] { show: signature; }" | pluckit view - tests/*.py
|
|
118
|
+
|
|
119
|
+
# Or from a file
|
|
120
|
+
pluckit view --query-file audit.q src/**/*.py
|
|
121
|
+
|
|
122
|
+
# Multiple paths and glob patterns
|
|
123
|
+
pluckit view ".fn#parse" src/*.py lib/*.py
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
When a signature-mode query matches more than one function, the output
|
|
127
|
+
collapses into a markdown table — dramatically smaller than a code fence per
|
|
128
|
+
match:
|
|
129
|
+
|
|
130
|
+
```
|
|
131
|
+
| File | Lines | Signature |
|
|
132
|
+
|---|---|---|
|
|
133
|
+
| src/validate.py | 35-54 | `def _is_garbled(intent: str) -> bool:` |
|
|
134
|
+
| src/validate.py | 73-88 | `def _flatten_ops(comp: Any) -> list[str]:`|
|
|
135
|
+
| src/validate.py | 102-196 | `def validate_chain(chain: str) -> Result:`|
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
### Find — list matches for scripting
|
|
139
|
+
|
|
140
|
+
`find` is the terse companion to `view`. It emits one line per match in
|
|
141
|
+
formats designed for shell pipelines and agent tool-use:
|
|
142
|
+
|
|
143
|
+
```bash
|
|
144
|
+
# Default: file:line:name, one per line — feed directly into $(...)
|
|
145
|
+
pluckit find ".fn:exported" src/**/*.py
|
|
146
|
+
|
|
147
|
+
# Just the names — good for set operations
|
|
148
|
+
pluckit find ".fn[name^=test_]" --format names tests/*.py | sort -u
|
|
149
|
+
|
|
150
|
+
# Signature table — a lightweight audit view
|
|
151
|
+
pluckit find ".fn:exported" --format signature src/**/*.py
|
|
152
|
+
|
|
153
|
+
# Machine-readable JSON — one object per match
|
|
154
|
+
pluckit find ".cls" --format json src/**/*.py
|
|
155
|
+
|
|
156
|
+
# Just the total count
|
|
157
|
+
pluckit find ".fn" --count src/**/*.py
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
### Edit — apply structural changes to matched nodes
|
|
161
|
+
|
|
162
|
+
All edits are **transactional**: if any affected file fails syntax re-validation
|
|
163
|
+
after splicing, every file is rolled back to its pre-edit state. Use `--dry-run`
|
|
164
|
+
to see how many matches each path would affect before writing.
|
|
165
|
+
|
|
166
|
+
```bash
|
|
167
|
+
# Replace a function's body entirely
|
|
168
|
+
pluckit edit ".fn#foo" --replace-with "def foo():\n return 1" src/*.py
|
|
169
|
+
|
|
170
|
+
# Scoped find-and-replace within matched nodes (2-arg replace)
|
|
171
|
+
pluckit edit ".fn#validate" --replace "return None" "raise ValueError()" src/*.py
|
|
172
|
+
|
|
173
|
+
# Add a parameter to every matched function
|
|
174
|
+
pluckit edit ".fn:exported" --add-param "timeout: int = 30" src/**/*.py
|
|
175
|
+
|
|
176
|
+
# Remove a parameter by name
|
|
177
|
+
pluckit edit ".fn#fetch_user" --remove-param "cache" src/*.py
|
|
178
|
+
|
|
179
|
+
# Add a keyword argument at every call site (paired with --add-param above)
|
|
180
|
+
pluckit edit ".call#fetch_user" --add-arg "timeout=timeout" src/**/*.py
|
|
181
|
+
|
|
182
|
+
# Remove a keyword argument from every call site
|
|
183
|
+
pluckit edit ".call#fetch_user" --remove-arg "cache" src/**/*.py
|
|
184
|
+
|
|
185
|
+
# Remove matched nodes entirely
|
|
186
|
+
pluckit edit ".fn#deprecated_helper" --remove src/*.py
|
|
187
|
+
|
|
188
|
+
# Clear a function/class body to `pass` (Python) or `{}` (C-family)
|
|
189
|
+
pluckit edit ".fn#todo_later" --clear-body src/*.py
|
|
190
|
+
|
|
191
|
+
# Rename a definition (first name occurrence)
|
|
192
|
+
pluckit edit ".fn#old_name" --rename "new_name" src/*.py
|
|
193
|
+
|
|
194
|
+
# Insert lines at the top/bottom of matched function bodies
|
|
195
|
+
pluckit edit ".fn:exported" --prepend-lines "logger.debug('entered')" src/*.py
|
|
196
|
+
pluckit edit ".fn:exported" --append-lines "logger.debug('exited')" src/*.py
|
|
197
|
+
|
|
198
|
+
# Insert at a specific sibling position — anchor is a CSS selector
|
|
199
|
+
# resolved against each matched node's subtree (exact, not heuristic)
|
|
200
|
+
pluckit edit ".cls#Foo" --insert-lines before ".fn#bar" "def pre_bar(self): pass" src/*.py
|
|
201
|
+
pluckit edit ".cls#Foo" --insert-lines after ".fn#bar" "def post_bar(self): pass" src/*.py
|
|
202
|
+
pluckit edit ".fn#main" --insert-lines before ".ret" "cleanup()" src/*.py
|
|
203
|
+
|
|
204
|
+
# Wrap matched nodes
|
|
205
|
+
pluckit edit ".call#query" --wrap "try:" "except DatabaseError:\n raise" src/*.py
|
|
206
|
+
|
|
207
|
+
# See what would change without writing — prints a real unified diff
|
|
208
|
+
pluckit edit ".fn#foo" --remove --dry-run src/*.py
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
**Chaining edits.** Multiple operations can share a single group, and
|
|
212
|
+
multiple `(selector, operations)` groups can run in one invocation,
|
|
213
|
+
separated by `--`. This is how you keep an API and its call sites in
|
|
214
|
+
sync atomically:
|
|
215
|
+
|
|
216
|
+
```bash
|
|
217
|
+
pluckit edit \
|
|
218
|
+
".cls#Foo .fn#__init__" --add-param "foo: int = 30" \
|
|
219
|
+
--append-lines "self.foo = foo" \
|
|
220
|
+
-- \
|
|
221
|
+
".call#Foo" --add-arg "foo=10" \
|
|
222
|
+
src/**/*.py
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
The whole command is one transaction: if any group produces invalid
|
|
226
|
+
syntax in any file, every affected file rolls back to its pre-edit state.
|
|
227
|
+
|
|
228
|
+
**Line-level vs character-level edits.** `--prepend-lines`, `--append-lines`,
|
|
229
|
+
and `--insert-lines` all insert whole new lines at indentation-matched
|
|
230
|
+
positions. The 2-arg `--replace OLD NEW` is the character-level equivalent —
|
|
231
|
+
it does a string-level replace within the matched node's text, preserving
|
|
232
|
+
the rest of each line verbatim. `--replace-with` replaces the entire node.
|
|
233
|
+
Character-level `--insert-chars` for inline positional insertions is
|
|
234
|
+
reserved for v0.2.
|
|
235
|
+
|
|
236
|
+
## The Python API
|
|
237
|
+
|
|
238
|
+
```python
|
|
239
|
+
from pluckit import Plucker, AstViewer
|
|
240
|
+
|
|
241
|
+
pluck = Plucker(code="src/**/*.py", plugins=[AstViewer])
|
|
242
|
+
|
|
243
|
+
# Query
|
|
244
|
+
fns = pluck.find(".fn:exported")
|
|
245
|
+
print(fns.count()) # 47
|
|
246
|
+
print(fns.names()[:5]) # ['authenticate', 'decode_jwt', ...]
|
|
247
|
+
|
|
248
|
+
# View
|
|
249
|
+
print(pluck.view(".fn#validate_token { show: signature; }"))
|
|
250
|
+
|
|
251
|
+
# Mutate (v0.1)
|
|
252
|
+
pluck.find(".fn#validate_token").replaceWith(
|
|
253
|
+
"return None",
|
|
254
|
+
"raise ValueError('token required')",
|
|
255
|
+
)
|
|
256
|
+
pluck.find(".fn:exported").addParam("timeout: int = 30")
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
### Module-level shortcuts
|
|
260
|
+
|
|
261
|
+
```python
|
|
262
|
+
from pluckit import view
|
|
263
|
+
|
|
264
|
+
# One-shot viewer query — creates an ephemeral Plucker
|
|
265
|
+
print(view(".fn#main { show: outline; }", code="src/**/*.py"))
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
## Selector syntax
|
|
269
|
+
|
|
270
|
+
Selectors mirror CSS but address AST nodes:
|
|
271
|
+
|
|
272
|
+
| Syntax | Meaning |
|
|
273
|
+
|---------------------------------|---------------------------------------------------|
|
|
274
|
+
| `.fn` | All function definitions (cross-language alias) |
|
|
275
|
+
| `.cls`, `.class` | All class definitions |
|
|
276
|
+
| `.call` | All call expressions |
|
|
277
|
+
| `.fn#name` | Function named `name` |
|
|
278
|
+
| `.fn:exported` | Public (non-underscore) functions |
|
|
279
|
+
| `.fn[name^=test_]` | Functions whose name starts with `test_` |
|
|
280
|
+
| `.fn[name*=auth]` | Functions whose name contains `auth` |
|
|
281
|
+
| `.cls#Foo .fn` | Functions inside `class Foo` |
|
|
282
|
+
| `.fn:has(.call#execute)` | Functions that call `execute()` |
|
|
283
|
+
| `.fn:not(:has(.try))` | Functions with no try block |
|
|
284
|
+
|
|
285
|
+
sitting_duck's full selector language is richer than what pluckit currently
|
|
286
|
+
compiles — see its docs for `:calls()`, `:matches()`, `:scope()`, and the
|
|
287
|
+
call graph pseudo-elements. These work when you call `ast_select` directly
|
|
288
|
+
against the underlying DuckDB connection; pluckit's fluent layer supports
|
|
289
|
+
a growing subset.
|
|
290
|
+
|
|
291
|
+
## Viewer `show` modes
|
|
292
|
+
|
|
293
|
+
The viewer supports a small declaration language — CSS declaration blocks
|
|
294
|
+
attached to selectors:
|
|
295
|
+
|
|
296
|
+
| Show value | Behavior |
|
|
297
|
+
|--------------|-------------------------------------------------------------------|
|
|
298
|
+
| `body` | Full matched node text (default for functions, calls, statements) |
|
|
299
|
+
| `signature` | Declaration line only (synthesized from native AST metadata) |
|
|
300
|
+
| `outline` | Class header + method signatures + dataclass fields (default for classes) |
|
|
301
|
+
| `enclosing` | Walk up to the nearest scope and render *that* as body |
|
|
302
|
+
| `N` (number) | First N lines of the body with `...` truncation marker |
|
|
303
|
+
|
|
304
|
+
Rules compose like a stylesheet:
|
|
305
|
+
|
|
306
|
+
```css
|
|
307
|
+
.fn { show: signature; } /* default most functions to signature */
|
|
308
|
+
.fn#main { show: body; } /* except main — show its full body */
|
|
309
|
+
.cls#Config { show: outline; } /* Config class with methods listed */
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
## Plugins
|
|
313
|
+
|
|
314
|
+
pluckit is composable. Core capabilities stay in `Selection`; anything that
|
|
315
|
+
depends on extra data or infrastructure moves to a plugin:
|
|
316
|
+
|
|
317
|
+
```python
|
|
318
|
+
from pluckit import Plucker, AstViewer
|
|
319
|
+
|
|
320
|
+
pluck = Plucker(
|
|
321
|
+
code="src/**/*.py",
|
|
322
|
+
plugins=[AstViewer], # viewer with `show:` declarations
|
|
323
|
+
# plugins=[Calls], # call graph (v0.2)
|
|
324
|
+
# plugins=[History], # git history (v0.2)
|
|
325
|
+
# plugins=[Scope], # scope analysis (v0.2)
|
|
326
|
+
)
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
Plugins register new methods on `Selection`, new pseudo-classes for the
|
|
330
|
+
selector compiler, and optional upgrades for existing methods (e.g.,
|
|
331
|
+
fledgling-python can upgrade `callers()` with import-resolved results).
|
|
332
|
+
|
|
333
|
+
## Training data
|
|
334
|
+
|
|
335
|
+
The `training/` directory contains a synthetic training data generator that
|
|
336
|
+
produces (intent, chain) pairs for fine-tuning a small code model. It works
|
|
337
|
+
entirely from the API spec in `reference/api.yaml` — no pluckit runtime
|
|
338
|
+
needed. A ~40k-pair corpus (19% error-driven, 19% context-bearing) is
|
|
339
|
+
committed via git LFS under `training/`.
|
|
340
|
+
|
|
341
|
+
See `training/README.md` for generation and formatting.
|
|
342
|
+
|
|
343
|
+
## Architecture
|
|
344
|
+
|
|
345
|
+
```
|
|
346
|
+
pluckit.Plucker entry point, plugin registry, DuckDB context
|
|
347
|
+
│
|
|
348
|
+
pluckit.Selection lazy DuckDB relation chain
|
|
349
|
+
│ query, filter, navigate, read, mutate
|
|
350
|
+
├── pluckit._sql selector → SQL WHERE fragments
|
|
351
|
+
│
|
|
352
|
+
pluckit.plugins optional capabilities
|
|
353
|
+
├── AstViewer CSS-style viewer with `show:` declarations
|
|
354
|
+
├── Calls (v0.2) call graph via name-join + plugin upgrades
|
|
355
|
+
├── History (v0.2) git history via duck_tails
|
|
356
|
+
└── Scope (v0.2) read/write interface via flags byte
|
|
357
|
+
│
|
|
358
|
+
pluckit.mutation byte-splice engine with transaction rollback
|
|
359
|
+
└── pluckit.mutations ReplaceWith, AddParam, Wrap, Rename, ...
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
All queries ultimately compile to SQL over sitting_duck's `read_ast()` table.
|
|
363
|
+
Mutations read files, apply string-level splices at line granularity, re-parse
|
|
364
|
+
to validate, and roll back on any syntax error.
|
|
365
|
+
|
|
366
|
+
## Contributing
|
|
367
|
+
|
|
368
|
+
Run tests:
|
|
369
|
+
|
|
370
|
+
```bash
|
|
371
|
+
pip install -e ".[dev]"
|
|
372
|
+
pytest
|
|
373
|
+
```
|
|
374
|
+
|
|
375
|
+
189 tests covering selectors, the Selection API, the plugin system, the
|
|
376
|
+
viewer, the CLI, and the mutation engine.
|