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¶
[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.