Changelog¶
All notable changes to this project are documented here. The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
Unreleased¶
Fixed¶
- Selector alias drift (#10): documented aliases now resolve to their
documented targets instead of silently matching nothing or the wrong nodes.
.def/.let/.jumpwere missing from the alias table;.module/.ns/.namespace/.packageresolved toORGANIZATION_BLOCK(every block) instead ofDEFINITION_MODULE;.null/.noneresolved to string literals and.bool/.booleanto an unmapped class — both now map to the engine'sLITERAL_ATOMIC(new taxonomy class.literal-atom). Newly wired 1:1 equivalents verified against the installed sitting_duck build:.assert→ERROR_THROW,.union→DEFINITION_CLASS,.del/.delete→EXECUTION_MUTATION,.comp/.comprehension→TRANSFORM_QUERY,.include→EXTERNAL_IMPORT,.extern/.ffi→EXTERNAL_FOREIGN. - Unknown selector classes fail loudly (#10): a class that would
compile to a match-nothing predicate now raises
UnknownSelectorClassError(with the working alternative in the message) instead of silently returning an empty result. This covers typos (.fnn), removed aliases, and the taxonomy classes the engine cannot express — notably.self/.this/.super, which previously resolved toNAME_IDENTIFIERand matched every identifier. - Alias substitution no longer rewrites attribute values (#10):
.fn[name*=.str]used to become[name*=.literal_string].[attr]blocks, quoted strings, and non-selector pseudo-class arguments (:match('…'),:contains(…)) are now copied verbatim; aliases still resolve inside:has()/:not(). :line/:lines/:long/:complexarguments areint()-validated (#10, LOW hardening): a non-integer argument such as:line(5 OR 1=1)now raisesSelectorArgErrorinstead of being interpolated raw into the post-filter WHERE clause (boolean-context SQL-arg injection, reachable via squackit'spluckMCP tool). An argless:linealso raises instead of rendering malformed SQL. Same validation applied toSelection.filter(...)pseudo-classes (which now accept arguments, e.g..filter(":line(5)")),filter(field__gt=...)comparison values, andat_line/at_lines.:containsescapes LIKE wildcards:_/%in the argument now match literally (previously_matched any character).
Changed¶
docs/selectors.mdreconciled with the shipped taxonomy: documents the.literal-atombucket (engine classifies booleans and null together), notes that.member/.indexare equivalent today (singleCOMPUTATION_ACCESStype), documents the raising behavior for inexpressible aliases with alternatives, and corrects the false claim that_is auto-escaped in[attr^=...]values (the engine's LIKE has no ESCAPE clause; use.filter(name__startswith=...)for literal underscores).
[0.14.0] - 2026-06-08¶
Fixed¶
Plucker.docs()now lazy-loads theduckdb_markdowncommunity extension via_Context._ensure_markdown_extension(), so Pluckers configured withdocs=...work in ephemeral envs (e.g. fresh CI) that don't have the extension pre-installed. Previously hit a rawCatalogException: Table Function with name read_markdown_sections does not exist; now installs from community on first use and raises a cleanPluckerErrorif that fails. Pluckers that never call.docs()don't pay the install cost.TestSearchWithoutFledgling::test_error_message_without_ftsassertion was pinned to"FTS index"but the actual error message varies by env:"FTS search requires fledgling"(no fledgling),"FTS index not found"(no FTS schema),"FTS index is empty"(schema present, no rows). Regex broadened tor"FTS (index|search)".test_fn.py @requires_fledglingpredicate now verifies the fledgling extension actually loads (not just that the Python module is importable), so tests that assert on specific fledgling SQL macros (doc_outline,find_definitions) skip cleanly in CI envs where the bundled extension doesn't match the duckdb version pip resolves.- Internal: chain
except'd errors withraise ... from ein_assert_fts_indexso the underlying duckdb error is preserved in the traceback (B904). - Cleared 15 long-standing ruff errors (B905, B904, B018, F401×4, F841,
E741×5, I001×2) —
ruff check src testsnow clean onmain.
Changed¶
- Selector matching is now delegated to sitting_duck's
ast_select/ast_select_from. pluckit's partial Python selector→SQL compiler is removed; sitting_duck is now the single source of truth for the structural grammar (classes, types,#id,[attr], combinators,:has/:not, and its native pseudo-classes). This fixes silent over-matching::has()/:not()/ combinators that the old compiler dropped now work inPlucker.find()and chained.find()/.ancestor()(previously.fn:has(.call#x)collapsed to.fn).
Added¶
- Value-add pseudo-classes as post-filters.
:exported/:private(Python name convention),:contains(s)(peeksubstring), and:line(n)/:lines(a,b)/:long(n)/:complex(n)(line/size/complexity thresholds) are applied as a SQLWHEREover the delegated result — sitting_duck cannot express these natively. They have identical meaning infind()andfilter(). (:line/:lines/:long/:complexnow work in selector strings, not just deferred.)
Removed¶
- Dropped the never-implemented
:wideand:lastpseudo-classes (both had no SQL template and silently matched everything).
[0.13.0] — 2026-05-26¶
Added (public API — SemVer-stable from here)¶
Plucker.pluckins— public accessor for the loaded pluckin instances (in registration order). Lets downstream consumers (e.g. squackit) enumerate pluckins for tool discovery without reaching into the private._registry.Chain.MUTATION_OPS— public, stable set of mutation operation names (_MUTATION_OPSkept as a deprecated internal alias).- Documented
Plucker.connection's contract: it is afledgling.Connection(exposing.con/.tools/.ensure_fts()) whenfledgling-mcpis installed, else a bare DuckDB connection — so consumers needing.con/.toolsmust declare a directfledgling-mcpdependency. - (Includes the
pluckit.pluckins.search/.viewersurface.)
[0.12.0] — 2026-04-18¶
Added¶
--diffflag — preview mutations as unified diff output without writing files. Works like--dry-runbut outputs the actual diff to stdout, pipeable topatch,git apply, or pluckit's ownpatchop.--difftakes precedence when combined with--dry-run.patchmutation — apply a unified diff or raw replacement text to matched nodes. Auto-detects unified diffs (by---/diff --gitprefix); everything else is treated as raw replacement. Strict context matching in v1 — hunks must match exactly orPluckerErroris raised. Registered as a chain op (Selection.patch(content)).@fileargument syntax — any string argument in a chain step can reference a file with@path. The file's content replaces the argument at evaluation time.@@pathescapes to literal@path. In JSON,{"file": "path"}is an alternative to"@path". Resolution is relative to CWD and happens at eval time, keeping serialized chains portable.
Fixed¶
--dry-runnow works. The flag was parsed intoChain.dry_runbut never checked duringevaluate(). Mutations now run, roll back, and report{"applied": False, "dry_run": True}.--diffand--dry-runcan appear after steps in the CLI, not just before the source. Both positions are supported.
0.11.1 — 2026-04-14¶
Changed¶
- Pagination
totalis now lazy.Chain.evaluate()no longer runs a second count query to populatepage.totalby default. The field isNonein the result envelope; callChain.with_total(result)to fill it in (costs one extra query). This halves the query cost of paginated chains when the caller doesn't need the exact total. has_moreis now heuristic whentotalis unknown:data_length < limit→ definitivelyFalse;data_length >= limit→ conservativelyTrue.Chain.with_total(result)refineshas_moreto exact.
Added¶
Chain.with_total(result)classmethod — fills inpage.totaland refineshas_moreon a paginated result. Returns the mutated result for chaining. No-op on unpaginated results.
Documented¶
_attach_pagination_metadatadocstring now calls out two edge cases:page N SIZE+ subsequentlimit/offsetoverrides (well-defined but potentially confusing — use one or the other); andlimitbefore a mutation restricts the mutation to the first N matches (correct but surprising).
0.11.0 — 2026-04-14¶
Added¶
- Chain-level pagination. New ops limit / offset / page in the chain vocabulary, with matching Selection methods. Chain.evaluate() result envelope gains source_chain + page metadata (offset, limit, total, has_more) whenever any pagination op appears in the chain. Consumers can rebuild the "next page" chain by taking source_chain and appending a new offset/limit. No MCP-layer wrapping needed.
- Pagination navigation helpers:
Chain.next_page(result),Chain.prev_page(result),Chain.goto_page(result, n). Each takes an evaluated paginated result dict and returns a new Chain ready to evaluate for the requested page (orNonewhen navigation isn't possible — no more pages, already at offset 0, or the result wasn't paginated).
0.10.0 — 2026-04-14¶
Added¶
Isolatedtype — new terminal onSelectionvia.isolate()that extracts a block of code (e.g., a function body, a line range) as a standalone unit. It identifies free variables read by the block but defined outside it, classifies each as Python builtin / imported symbol / free parameter, and can render the result as a standalone function (.as_function()) or a Jupyter cell (.as_jupyter_cell()). Supportsto_dict/from_dict/to_json/from_jsonfor transport. Useful for extracting runnable snippets that an agent or user can paste into a notebook or test.
Notes¶
- The
Callspluckin's implementation (callers/callees/references) will simplify once sitting_duck ships a structuredscopestruct ({current, function, class, module, stack}) on everyread_astrow. The current provenance-walk +ast_selectround-trip is a workaround for that missing schema — when upstream lands, the per- file fan-out collapses substantially (filter directly onscope.functionfor callers). See docstring insrc/pluckit/pluckins/calls.py.
0.9.0 — 2026-04-14¶
Brand-consistency rename for the plugin system. Plugin authors should
migrate module paths (pluckit.plugins.* → pluckit.pluckins.*). The
class-level aliases (Plugin = Pluckin) keep existing source compiling
unchanged.
Changed¶
- Plugin base class renamed
Plugin→Pluckin(andPluginRegistry→PluckinRegistry) for brand consistency with the rest of the pluckit family ("pluckin" = pluckit + plugin) and to disambiguate from generic Python plugins in multi-plugin-system contexts (MCP, LSP, etc.). Old names are kept as aliases — existing code importingPluginandPluginRegistrycontinues to work without modification. - Breaking:
pluckit.pluginspackage renamed topluckit.pluckins. Imports of the formfrom pluckit.plugins.X import Ymust be updated tofrom pluckit.pluckins.X import Y. The class-level aliases (Plugin = Pluckin,PluginRegistry = PluckinRegistry) remain for source-level backward compat, but the package path itself is a clean break — there is no shim at the oldpluckit.pluginspath. Top-level imports (from pluckit import AstViewer, etc.) are unaffected.
Added¶
PluckinRegistry.pluckinsproperty — returns the list of unique registered pluckin instances. Designed for downstream consumers (e.g., squackit) to enumerate pluckins for tool/integration discovery without coupling pluckit to specific consumer APIs.
0.8.0 — 2026-04-14¶
Substantial release adding chain serialization, MCP transport, persistent AST caching, and three new pluckins (Calls, Scope, History). Breaking changes to the CLI surface — see "Changed" below.
Added¶
- Calls pluckin —
callers(),callees(),references()methods on Selection, wrapping sitting_duck's::callers/::callees/::referencespseudo-elements. Load withPlucker(plugins=[Calls]). - Scope pluckin —
scope(),defs(),refs()methods on Selection.scope()wraps sitting_duck's::scopepseudo-element (returns enclosing scope hierarchy).defs()/refs()filter byscope_idand the flags byte (IS_DEFINITION / IS_REFERENCE). - MCP-ready serialization protocol. A uniform
to/from_{dict,json,argv}interface across pluckit's core types so squackit (and other MCP consumers) can round-trip structured state: Selector— new class (subclassesstr) withvalidate(),is_valid, and the full serialization protocol. Backward-compatible everywhere a bare selector string is used today.Plucker— serializes its constructor args (code, plugins, repo), not the live DuckDB connection. Plugin names resolve viaresolve_plugins()on deserialization.View— gainsfrom_dict,from_json,to_json(already hadto_dict). Round-trips through JSON.Selection— gainsto_chain()which walks the_parent/_opprovenance to reconstruct the chain that produced it, plusto_dict()/to_json()as wrappers.Chain— gainsto_argv()(the inverse offrom_argv) so a chain round-trips CLI ↔ dict ↔ JSON ↔ argv.Commit— gainsto_dict,from_dict,to_json,from_json.- AST caching (
cache=True). APlucker(cache=True)opens a persistent DuckDB file (.pluckit.duckdbin the repo root by default) and materializesread_astoutput into per-pattern tables. Subsequent queries against the same pattern skip re-parsing and hit the cached table directly. File-stat mtime checks drive incremental invalidation — only modified files are re-parsed; the rest of the cache is preserved. cache=True— use.pluckit.duckdbunder the repocache="/custom/path.duckdb"— custom cache location[tool.pluckit] cache = trueandcache_path = "..."inpyproject.tomlASTCacheandPluckitConfigare both exported from the top-level package for programmatic use.-
.pluckit.duckdband.pluckit.duckdb.walare added to.gitignore. -
Viewreturn type forPlucker.view(). Previouslyview()returned a barestrof rendered markdown. It now returns a structuredViewobject that: - Stringifies to the markdown output (
str(v),print(v),f"{v}"all work as before) - Supports
len(v),bool(v), iteration (for block in v), indexing (v[0]), slicing (v[:3]), and containment ("def main" in v) - Exposes
.markdown,.blocks,.files, and.to_dict()for structured consumers (agents, JSON pipelines) - Wraps each rendered block in a frozen
ViewBlockdataclass withname,file_path,start_line,end_line,node_type,language,show, andrulefields - Treats multi-match signature tables as a single aggregate
ViewBlock(withfile_path/start_line/end_lineallNoneandshow == "signature-table") so consumers can detect auto-collapsed output Historypluckin — a v0.2 plugin wrappingduck_tailsfor git-history operations on AST selections. Four methods:history()— commits that touched each matched node's file (rename-aware viagit log --follow)authors()— distinct commit authors for those filesat(rev)— source text of each matched node as of revisionrev, AST-aware: re-parses the file at the old revision and looks the node up by(name, type)rather than naively slicing by current line rangediff(rev)— per-node unified diff between HEAD andrev, using the same AST-aware node resolutionCommitdataclass exported frompluckitandpluckit.pluckinsfor typed access to the fields returned byhistory().
Architecture notes¶
history()/authors()shell out togit log --followbecauseduck_tails's SQL surface has no line-range or file-history join point — a pure-SQL implementation would require iterated per-commitgit_diff_treecalls with no lateral-join support. Subprocess is faster, rename-aware for free, and simpler.at(rev)/diff(rev)useduck_tails.git_readto fetch file content at a revision, then re-parse via sitting_duck'sread_astagainst a tempfile and look up the matching node with pluckit's own selector compiler. When sitting_duck shipsast_selectas a community-extension release, that lookup becomes a one-line swap.-
blame()is deferred —duck_tailshas nogit_blametable function, and implementing line-level blame via iterated history reads is prohibitively expensive. The method raises aPluckerErrorpointing at the upstream tracker. -
Chain serializer/evaluator. Every pluckit interaction is now a serializable
Chain— Plucker args plus an ordered list ofChainStepoperations. Chains can be: - Constructed from CLI args:
pluckit src/**/*.py find ".fn" count - Parsed from JSON:
pluckit --json '{"source":[...],"steps":[...]}' - Emitted as JSON:
pluckit --to-json src/**/*.py find ".fn" count - Built in Python:
Chain(source=["src/**/*.py"], steps=[...]) - Evaluated:
chain.evaluate()→ JSON-serializable result dict - The chain that produced a result is always included in the output
under the
"chain"key for provenance/replay. - Selection stack —
reset(or bare--) clears the selection context and starts a newfind.popreturns to the previous selection (e.g., from a narrowed.fn#mainback to the enclosing.clsselection). - Project config —
[tool.pluckit]section inpyproject.tomlfor default plugins and named source shortcuts:CLI shortcuts:[tool.pluckit] plugins = ["AstViewer"] [tool.pluckit.sources] code = "src/**/*.py" tests = "tests/**/*.py"-c/--code,-d/--docs,-t/--tests. resolve_plugins()— string→class plugin lookup supporting both short names ("AstViewer") and fully-qualified module paths ("mypackage.plugins:MyPlugin").
Changed¶
- Breaking: CLI rewrite. The
view/find/editsubcommands are removed. Everything is now a chain:# Old: pluckit view ".fn#main" src/**/*.py # New: pluckit src/**/*.py find ".fn#main" view # Old: pluckit find ".fn:exported" --format names src/**/*.py # New: pluckit src/**/*.py find ".fn:exported" names # Old: pluckit edit ".fn#foo" --add-param "x: int" src/*.py # New: pluckit src/*.py find ".fn#foo" addParam "x: int"pluckit initis kept.--versionand--helpare kept. - Bumped the
duckdbdependency floor to>=1.3.2(required byduck_tails). - Breaking:
Plucker.view()and the module-levelpluckit.view()now return aViewobject instead of a barestr. Code that treated the return as a string directly (e.g.,pluck.view(q).split("\n")) must switch to the.markdownaccessor (pluck.view(q).markdown.split("\n")) or wrap withstr(...). Idiomatic uses —print(v),f"{v}","needle" in v,v == ""— continue to work unchanged.
Fixed¶
Selection.filter(name__startswith="_")was matching every identifier because_is a SQL LIKE wildcard; now routes through_esc_likeand emitsESCAPE '\\'.- Compound selectors like
.fn:exportedsilently dropped the pseudo-class in_selector_to_where; the compiler now parses:pseudotokens from the selector tail and looks them up in thePseudoClassRegistry.
Removed¶
- Removed the five history-related stubs (
history,at,diff,blame,authors) from coreSelection. They now live in theHistorypluckin; calling them without loading the pluckin raises aPluckerErrorwith a pointer via_KNOWN_PROVIDERS.
0.7.0 — 2026-04-12¶
Infrastructure sync with the fledgling-mcp ecosystem. No new pluckit-
facing features; the version bump was required to align with a
fledgling release. Functionally equivalent to the tip of the
feat/training-data-generator branch at that point (pre-chain,
pre-MCP-serialization, pre-cache).
Added¶
- Fledgling kwargs pass-through (
profile,modules,init) onPlucker.__init__and_Context.__init__. - Public
Plucker.connectionproperty exposing the underlying DuckDB connection (afledgling.Connectionproxy when fledgling is installed, otherwise a bare DuckDB connection).
0.1.0a1 — 2026-04-10¶
First public alpha. Query, view, and mutate all work end-to-end.
Added¶
-
Plucker— a fluent entry point that wraps a DuckDB connection, loads thesitting_duckcommunity extension, and exposesfind(),view(), and mutation methods on lazySelectionobjects. Selections are DuckDB relations that chain filters, navigation, and terminal operations without materializing until necessary. -
CSS-like selector language (
.fn,.cls,.call,.fn#name,.fn:exported,.fn[name^=test_],.cls#Foo .fn,.fn:has(.call#x)) compiled to SQL WHERE fragments over sitting_duck'sread_ast()table. Supports 27 languages via tree-sitter. -
AstViewerplugin — a CSS-stylesheet-style declaration language ({ show: signature; },{ show: outline; },{ show: 10; }, etc.) attached to selectors. Synthesized signatures from sitting_duck's native extraction columns. Multi-match signature queries collapse to a markdown table automatically. -
Mutation engine with transactional rollback. Line-granularity splicing with per-file snapshots, reverse-order application (later edits don't shift earlier line numbers), and re-parse validation. Any syntax error rolls back every affected file.
-
Mutation vocabulary:
ReplaceWith,ScopedReplace,Prepend,Append,Wrap,Unwrap,Remove,Rename,AddParam,RemoveParam,AddArg,RemoveArg,ClearBody,InsertBefore,InsertAfter.InsertBefore/InsertAftertake a CSS selector as the anchor and resolve it via a scoped AST sub-query — no heuristics. -
pluckitCLI with four subcommands: init— install and verify the required DuckDB community extensions.view— render matched code regions as markdown, reading queries from argv, a file, or stdin.find— list matches for scripting. Four output formats:locations(file:line:name, default),names,signature(markdown table),json.-
edit— apply structural mutations. Chainable within one invocation: multiple operations per group, multiple groups separated by--, with a real unified-diff preview in--dry-run. -
Plugin system — third-party plugins register new methods on
Selection, new pseudo-classes for the selector compiler, and optional upgrades to existing methods (e.g., theCallsplugin will upgradecallers()with import-resolved results). -
Cross-language indent detection for mutations in Python, C++, Go, Java, TypeScript, and Rust. Body-frame indent is computed from file context, not hard-coded to 4 spaces.
-
CI scaffolding — GitHub Actions workflow runs lint, pytest, and a wheel build on every push and PR.
Infrastructure¶
- MIT licensed.
- PyPI distribution name:
ast-pluckit. Import name, CLI name, and repo name are allpluckit. (The barepluckitPyPI name is held by an abandoned 2019 project.) - Python 3.10+ supported.
- Documentation published at pluckit.readthedocs.io.
Known limitations¶
- Call graph, git history, and scope plugins are stubs — landing in v0.2.
- pluckit's selector compiler supports only a subset of sitting_duck's full
selector language. Richer features like
:calls(),:matches(), and:scope()work when callingast_selectdirectly against the underlying DuckDB connection. - Mutations operate at line granularity because sitting_duck's
read_astdoes not yet expose byte offsets. Character-level insertions (--insert-chars) are reserved for v0.2.