Releases: Stoica-Mihai/recast
v0.1.15
[0.1.15] — 2026-05-31
Added
--searchflag: find match locations (file:line:col:snippet) without rewriting. Outputs grep-like human text or structured JSON (kind: "search"). Supports all filter flags,--at-least/--at-mostguard,--json,--quiet,--verbose, and structural mode (--lang+--ast/--query).recast_searchMCP tool: structured code navigation returning per-match line/column/snippet/capture. Supports regex and tree-sitter AST-aware queries; capture names distinguish definition from usage captures.
v0.1.14
[0.1.14] — 2026-05-29
Added
- Structural attr-aware delete (
--include-leading-attrs). In
structural mode, deleting or replacing an item now optionally extends
each match backward over the contiguous run of preceding
attribute_item/ doc-comment siblings — so deleting a function also
removes its#[test]/#[cfg(...)]////lines instead of
orphaning them. This is the real fix for the orphaned-attribute class
that the (syntactic-only) syntax-regression guard cannot catch: an
orphaned#[test]parses clean.- A blank line ends the run (an attribute separated by an empty line
is treated as detached and left in place). Plain////* */
comments are never swallowed — only doc comments (///,//!,
/**,/*!). - Node kinds are tree-sitter-rust's (
attribute_item); languages
without those kinds never extend (no-op), so the flag is safe to set
in any language. - CLI
--include-leading-attrs(structural mode only); MCP
include_leading_attrs: trueonrecast_structural. Default off.
- A blank line ends the run (an attribute separated by an empty line
v0.1.13
[0.1.13] — 2026-05-29
Added
- Syntax-regression guard. A third planner guard (alongside the
match-count and convergence checks): for every changed file whose
extension maps to a compiled tree-sitter grammar (rust, ts, tsx, js,
py, sh, go, json, md), recast re-parses the post-image and rejects the
rewrite if it introduces new parse errors relative to the pre-image.
Catches greedy regex that strands a brace or truncates an expression
before anything is written to disk. The check is a count delta — a
file that was already unparsable stays acceptable as long as the
rewrite doesn't make it worse. Runs at plan time, sorecast_preview
/--diffsurface the regression too.- Limitation (by design): the guard is syntactic, not semantic.
An orphaned#[test]left on the wrong item parses clean and is NOT
caught — that is a rustc-level error one layer above tree-sitter. Use
structural mode for shape-sensitive deletes. - Opt out per run with
--allow-syntax-errors(CLI) /
allow_syntax_errors: true(MCPrecast_preview/recast_apply/
recast_structural). On by default. - New
Error::SyntaxRegressionvariant +ErrorKind::syntax_regression
for JSON output. - New
Language::from_pathmaps a file extension to its grammar; the
guard is skipped (rewrite passes through unchecked) for extensions
with no compiled grammar and for--no-default-featuresbuilds.
- Limitation (by design): the guard is syntactic, not semantic.
v0.1.12
[0.1.12] — 2026-05-22
Real-session feedback from a Claude Code user surfaced two
documentation gaps in v0.1.11's tool descriptions. Doc-only patch
release.
Changed
- MCP tool descriptions document the
\\nfootgun.
recast_apply/recast_previewreplacementis a regex
template — backreferences ($1,${name}) are interpolated but
C-style escape sequences (\\n,\\t) are NOT decoded. An agent
passing"foo\\nbar"ends up writing literal backslash-n on disk,
not a newline. Descriptions now carry aFOOTGUN —block telling
callers to use real LFs in the JSON string value, not the\\n
escape. - Refined "when to use" threshold. The flat "3+ files" rule
under-served simple 4-site edits whereEditis genuinely faster.
Descriptions now distinguish: 5+ sites for simple text changes;
any count for shape-sensitive changes (use
recast_structural); any count when atomicity is required.
Added an explicitWHEN NOT TO USEblock so the agent can opt
out without abandoning recast entirely for the harder cases.
v0.1.11
[0.1.11] — 2026-05-22
Agent-adoption pass for recast-mcp: real-world Claude Code survey
showed the agent defaulting to Edit / write_file loops even with
the MCP server installed. Three documentation-shaped fixes encode the
decision rule at every surface the agent reads.
Changed
- MCP tool descriptions rewritten with worked examples + decision
rule.recast_preview,recast_apply,recast_structural, and
recast_recovernow embed 2-3 concrete JSON invocations each plus
the "if 3+ files, call recast first" trigger. The LLM tool-ranker
reads these on every selection step; example-rich descriptions
reduce uncertainty enough to flip the default away fromEdit. ServerInfo.instructionsexpanded from one sentence into a
decision-rule + two-step-workflow block. The MCP client injects
this into its system prompt during handshake, so the heuristic
reaches the agent before any tool call is considered.
Added
docs/src/agent-rules.md— copy-pasteable rules snippet for
every common agent runtime (Claude Code, Cursor, Continue, Cline,
Aider). Users drop the block into their project'sAGENTS.md/
.cursorrules/ equivalent so the in-project system prompt
doubles up with the MCP server's instructions field. Linked from
both the root README and therecast-mcpcrates.io README.
v0.1.10
[0.1.10] — 2026-05-22
Hotfix: replace the placeholder root README on the recast-mcp
crates.io page with a dedicated MCP-focused README. Otherwise
identical to 0.1.9.
Fixed
recast-mcpcrates.io listing showed CLI install instructions.
All three crates pinnedreadme = "../../README.md", so the
crates.io page for the MCP server told you tocargo install recast-cli. Addedcrates/recast-mcp/README.mdwith the MCP
install + client-config + tool-list content and pointed the crate
manifest at it.recast-cliandrecast-corestill share the
root README (close enough to their actual landing-page audience).
v0.1.9
[0.1.9] — 2026-05-22
Post-0.1.8 follow-through: a deep audit pass over the previously
untouched modules (walker, lockfile, json, rewrite, script, parallel),
the recast-mcp server crate for MCP-aware AI agents, a nightly
fuzz workflow, a criterion regression gate on PRs, and the symlink +
concurrent-apply regression tests that pin down behavior the audit
exposed.
Added
recast-mcpcrate. Model Context Protocol server that exposes
the recast engine to MCP-aware AI agents (Claude Desktop, Cursor,
Continue, Cline, custom MCP clients). Library-linked against
recast-core— no subprocess, no CLI string assembly, no JSON
parse round-trip. Speaks JSON-RPC over stdio per MCP convention.
Four tools, 1:1 with the planner API:recast_preview,
recast_apply,recast_structural,recast_recover. Typed
argument schemas (viarmcp1.7 +schemars1.x) so agents can't
malform calls; engine errors propagate asMcpErrorwith the
typedkinddiscriminator preserved in the payload.- Walker symlink regression tests. Cover cycle detection,
dangling links, escape-root behavior with/without
follow_symlinks, and the gitignore interaction when following
links. Symlink semantics now documented inwalker.rsmodule
doc-comment. - Concurrent-apply integration tests in
crates/recast/tests/concurrency.rs. External fs2 lock on
.recast.lockforcesrecast --applyto exit non-zero with the
Lockederror;--forcebypasses the guard. - Nightly fuzz workflow (
.github/workflows/fuzz.yml) runs three
cargo-fuzz targets (compile_friendly_query,
structural_rewrite_friendly, pattern_compile_convergence) for 60
minutes each per day. Corpus cached across runs so coverage
compounds. - Criterion regression gate (
.github/workflows/bench.yml) runs
benches on every PR, diffs against themainbaseline stored on
gh-pages, comments + fails the check at >50% regression.
Changed
- Walker uses
WalkParallelinstead of the single-threaded
iterator. Honors the surrounding rayon pool's thread count so
--threads Nis now respected for the walk phase as well. label_for_pathfast path for absolute / plain-relative paths
— skips the PathBuf rebuild when no leading./needs stripping.
Saves one allocation per labeled file in the planner's hot loop.from_applyroutes throughheader(plan)to dedupe the
JsonHeader construction; the two header builders no longer drift.
Fixed
- Lockfile error misclassification.
acquire_workspace_lock
used to fold everyio::Errorfromtry_lock_exclusiveinto
Error::Locked, hiding permission-denied / ENOSPC / EIO behind
"another recast is already applying". Now matches on
ErrorKind::WouldBlockand only that variant maps toLocked;
every other variant propagates asError::Iowith the underlying
source preserved. - Workspace lock derivation canonicalizes input paths and locks
at the deepest common ancestor. Two--applyinvocations against
the same tree from different CWDs (or one againstsrc/, one
againstsrc/sub/) now share one.recast.lockinstead of
proceeding in parallel. - EXDEV fallback in
commit_one,rollback_committed, and
recover_sweep. A newrename_with_exdev_fallbackhelper catches
ErrorKind::CrossesDevicesfromfs::renameand degrades to
copy + sync_all + remove_file. Same-directory renames inside a
normal filesystem never hit this path; overlayfs, unionfs, FUSE
backends, and certain container layouts can return EXDEV even for
lexically-sibling renames, so the apply now degrades cleanly
rather than aborting.
v0.1.8
[0.1.8] — 2026-05-22
Post-0.1.7 follow-through: residual cleanup-pass items + a CI
deprecation fix flagged by the 0.1.7 release run, plus a deep
correctness/durability audit pass over commit, structural, and
the CLI dispatch layer.
Changed
commit::apply_innerhidden behind#[cfg(test)]. Was
pub(crate)so the rollback test could inject a mid-commit
failure; that leaked the test-only seam into the production API.
Productionapply_changesandcommit_allcarry no hook
references;apply_inner+commit_all_withexist only under
cfg(test). The two paths share afinalize_applyhelper, and
commit_allnow delegates to a singlecommit_all_withimpl with
a no-op hook closure instead of duplicating the loop body.- Per-apply
NonceGenreplaces the staticAtomicU64counter
insidefn nonce(). The struct is constructed once per
apply_changescall and threaded by reference through stage and
commit phases; sibling-filename uniqueness across rayon workers
is unchanged, but the state is scoped to the invocation instead
of living in static mutable memory. NonceGen::newsamplesSystemTime::now()once and stores
it as a precomputed mix seed. The previous shape sampled the
clock inside everynext()call, so a 1k-file apply did 1k extra
syscalls just to disambiguate sibling filenames.emit_diff/emit_applynarrowed from&Clito
&OutputOptions, &Plan. Dropped the transitive dependency on
GuardOptions and StructuralCli substructs that those helpers
never touched.--threads Nhonored across the whole pipeline. Previously
only the regex planner ran inside the user-scoped rayon pool;
scripted-plan, structural-plan, and the commit/stage phase all
fell back to the global pool. The CLI now installs the pool once
and wraps every parallel phase, regardless of mode.- Primary capture fallback in
structural::applyis deterministic.
When a query lacks an explicit@root, the apply phase now picks
the outermost-by-byte-range capture (smallest start, then largest
end, then lowest capture index) instead of "the capture with the
largest index", which was declaration-order dependent.
Fixed
recover_sweepskips parentless sibling entries. Previously
fell back toPathBuf::new()as the HashMap target key when
path.parent()returned None, silently bucketing unrelated
parentless siblings together. Skip with atrace!line instead
so recovery never makes cross-target decisions.recover_sweepaggregates per-group errors instead of
bailing on the first failure. The user invokes--recover
precisely when the tree is in a partial state; aborting at the
first bad group left the rest unreconciled. All groups are
attempted; the first error is propagated after the sweep so the
exit code still reflects failure.structural::compile_friendly_querypasses child index, not
node id, tofield_name_for_named_child. Was calling
field_name_for_child(child.id() as u32), conflating the
pointer-derived opaque identifier with the positional child
index. Generated--astqueries could silently drop / mis-name
field annotations as a result.- UTF-8 corruption in three byte walkers.
structural::parse_template,structural::substitute_metavars,
andpattern::CompiledPattern::replacement_probeadvanced one
byte at a time and pushed each raw byte aschar, mojibaking
every multibyte codepoint. All three now advance by a full UTF-8
scalar via a sharedtemplate_scan::utf8_char_lenhelper. rollback_committedno longer unlinks the target before
restoring the backup. The explicitremove_filewas redundant
(renamereplaces atomically on Unix) and opened a window where
neither the old nor the new content existed on disk.finalize_applyfsyncs parent dirs before deleting backups.
The previous order unlinked the safety net before the rename
batch's directory entries were durable; a crash in the window
could leave the target absent with the backup already gone.structural::emit_nodeis iterative. The recursive
implementation grew the stack proportionally to the depth of the
user's--astpattern AST, giving pathological patterns a
stack-overflow vector. Replaced with an explicitOpen/Close
frame stack — identical output, bounded by heap.
CI
actions/checkout/actions/upload-artifact/
actions/download-artifactbumped from@v4to@v5across
audit, ci, docs, and release workflows. Closes the Node 20
deprecation annotation surfaced by the v0.1.7 release run before
the 2026-06-02 forced-Node-24 cutover.
Removed
- Windows as a supported target. No longer running
cargo testonwindows-latest; no longer cross-compiling
x86_64-pc-windows-msvcartifacts; no longer carrying the
#[cfg(windows)]/#[cfg(unix)]carve-outs in source. The
release matrix shrinks from seven targets to six (Linux gnu/musl
× x86_64/aarch64 plus macOS x86_64/aarch64). The parent-directory
fsync, the symlink walker tests, and the permission-preservation
test all assume unix unconditionally now.label_for_pathdrops
the backslash-to-forward-slash translation it carried for Windows
diff headers.
v0.1.7
[0.1.7] — 2026-05-21
Post-0.1.6 cleanup pass: targeted perf wins across the structural,
scripted, and commit pipelines plus a round of error-schema and DRY
hardening surfaced by an internal review.
Performance
- Structural mode is no longer per-file recompiled. The tree-sitter
Query, capture-index table, and rewrite template are built once
per invocation as a sharedCompiledStructural; only the
Parser+QueryCursorare per-thread.plan_structural_rewrite
now drives per-file work viarayon par_iter().map_init(...). The
rewrite template is pre-parsed intoVec<TemplatePart::{Literal, Capture { index, name }}>so capture-name lookup at match time is
an index hit, not an O(N) byte scan. commit::stage_allparallel. Per-file write +sync_allruns
on rayon workers so kernel fsync overlaps; commit phase stays serial
for deterministic rollback. Local measurement: 500-file apply ~9ms,
1000-file ~15ms (NVMe, ext4).plan_rewrite_scriptedparallel. Each rayon worker gets a fresh
sandboxed RhaiEngineviaScriptRewriter::fresh(); the compiled
AST is shared. Enablessyncfeature on the rhai dep (Rc → Arc
internally).- Single-pass match count in
rewrite_text. Drops the redundant
find_iter().count()scan that ran beforereplace_alland the
before.to_owned()clone on every changed file. - Cheaper regex convergence probe. The per-file idempotency check
usesRegex::find_iter().count()on the post-image instead of a
full secondreplace_allround. - HashSet parent-dir dedup in fsync.
best_effort_fsync_parents
was O(N²) viaVec<PathBuf>::iter().any(...); nowHashSet<&Path>. - Planner peak memory ~halved.
FileChange::before(full
pre-image per changed file) is dropped — it was set during diff
rendering and never read afterward. Worst-case (max_bytes 10 MiB ×
max_files 1000) drops ~10 GiB from peak resident memory. - Hot-path micro-allocations trimmed.
label_for_pathskips the
\→/scan on Unix where it can't matter; structural splice
pre-reservessource.len() + (replacement - range) deltaso the
outputStringdoesn't realloc when matches grow text;
emit_nodewrites literal-terminal predicates withwrite!
instead of three intermediateformat!Strings per terminal.
Changed
- Wire JSON field order shifted. Plan / Apply / Check reports now
emitoutcome,files_scanned,total_matchesas the shared
prefix (extracted intoJsonHeaderwith#[serde(flatten)])
followed by the mode-specific count. JSON members are unordered
per spec so this is semantically identical, but the wire bytes do
reorder. Snapshot fixtures updated. Error::kind()returnsErrorKinddirectly. The
Error → ErrorKindmapping lives onimpl Errorinstead of in
json::error_kind(); the JSON module re-exportsErrorKindfrom
errorfor back-compat.Language::from_namereturnsResult<Self, Error>instead of
Option<Self>. Unknown names surface asError::UnknownLanguage
from the library boundary.RewriteOutcomeshape. Dropped thebeforefield — the caller
already owns the pre-image.changed()method removed; compare
outcome.after != beforeat the call site if needed.FileChange.beforefield removed. Was#[serde(skip)]and
only used to render the diff during planning;apply_changeshas
always read fromafter. Pre-1.0 break for anyone reading the
field directly;FileChange.diffcarries the rendered diff.handle_plan_errorreturnsResult<u8>instead ofu8so a
failed JSON serialize during error reporting is now observable
rather than silently dropped.- CLI options grouped into substructs. Output (
--diff/
--json/--quiet/--verbose), guards (--at-least/
--at-most/--allow-non-convergent/--max-bytes/
--max-files), and structural (--lang/--query/--ast)
are now#[command(flatten)]-ed substructs. CLI surface
byte-identical; only the in-code access path changes
(cli.output.json,cli.guard.max_files, etc.). SiblingKindenum drives recast-sibling filenames. The
.{target}.recast.{token}.{nonce}token (bak/tmp) is now
emitted and parsed through oneSiblingKind::as_str/
SiblingKind::from_tokenpair so the two sides cannot drift.
Added
Error::InvalidThreads+Error::ThreadPoolvariants replace
the syntheticError::Io { path: empty }wrappers that
parallel::build_poolused to emit for non-IO failures. JSON
surfaces them asinvalid_threads/thread_poolerrorkinds.ScriptRewriter::fresh()— sibling rewriter, new sandboxed
Engine, shared AST. Designed for per-rayon-worker construction.template_scanmodule — shared$NAME/${NAME}/$$$NAME
placeholder scanners. The regex convergence probe and the
structural pattern preprocess + template parser now share one
identifier grammar so the three byte walkers can't drift.plan::read_text_or_skip_binary— single helper that wraps the
metadata + max-bytes check +read_to_string+ UTF-8 skip. Used by
bothplan_rewriteandplan_structural_rewrite.- Multi-file structural criterion bench
(bench_plan_structural_rewrite) covering 10 / 100 / 500 file
fixtures.
Internal
commit::recover_sweepreportsignore::Errorvia the existing
Error::Walkvariant instead of building a synthetic
Error::Io { path: empty }.commitextractedparent_dir/remove_noncedhelpers; rollback
loops collapsed.- Binary's
Language::from_name/dispatch(plan)helpers consolidate
the apply / check / diff trailer duplicated betweenrunand
run_structural. - Binary
Cliaddsmin_matches(),paths_as_pathbufs(),
recover_paths(), andacquire_workspace_lock_for(&cli)helpers
so the recurringSome(at_least.unwrap_or(1)), path conversions,
the--recoverclap-workaround fold, and the lock-root probe
each live in one place. CompiledStructural::applycollects matches into a named
struct Hit { start, end, replacement }instead of the positional
(usize, usize, String)tuple it had before — readability only.
Documentation
CHANGELOG.md+PLAN.md§7.1 +docs/src/json-schema.md
brought back in sync with the wire format (shared-header field
order; fullErrorKindvocabulary including
invalid_threads/thread_pool).AGENTS.md§11 records the harness-classifier workaround for
git commit: splitgit addandgit commitinto separate
shell calls; do not chain them with&&+ heredoc.
v0.1.6
[0.1.6] — 2026-05-21
First successful crates.io release. v0.1.5's publish step failed
because the crates.io account hadn't verified its email yet; this
is the same content with the verified email now in place.