Skip to content

Releases: Stoica-Mihai/recast

v0.1.15

30 May 22:58

Choose a tag to compare

[0.1.15] — 2026-05-31

Added

  • --search flag: 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-most guard, --json, --quiet, --verbose, and structural mode (--lang + --ast/--query).
  • recast_search MCP 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

29 May 19:30

Choose a tag to compare

[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: true on recast_structural. Default off.

v0.1.13

29 May 18:24

Choose a tag to compare

[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, so recast_preview
    / --diff surface 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 (MCP recast_preview / recast_apply /
      recast_structural). On by default.
    • New Error::SyntaxRegression variant + ErrorKind::syntax_regression
      for JSON output.
    • New Language::from_path maps a file extension to its grammar; the
      guard is skipped (rewrite passes through unchecked) for extensions
      with no compiled grammar and for --no-default-features builds.

v0.1.12

23 May 21:59

Choose a tag to compare

[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 \\n footgun.
    recast_apply / recast_preview replacement is 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 a FOOTGUN — 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 where Edit is 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 explicit WHEN NOT TO USE block so the agent can opt
    out without abandoning recast entirely for the harder cases.

v0.1.11

23 May 12:31

Choose a tag to compare

[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_recover now 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 from Edit.
  • ServerInfo.instructions expanded 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's AGENTS.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 the recast-mcp crates.io README.

v0.1.10

22 May 15:17

Choose a tag to compare

[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-mcp crates.io listing showed CLI install instructions.
    All three crates pinned readme = "../../README.md", so the
    crates.io page for the MCP server told you to cargo install recast-cli. Added crates/recast-mcp/README.md with the MCP
    install + client-config + tool-list content and pointed the crate
    manifest at it. recast-cli and recast-core still share the
    root README (close enough to their actual landing-page audience).

v0.1.9

22 May 15:06

Choose a tag to compare

[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-mcp crate. 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 (via rmcp 1.7 + schemars 1.x) so agents can't
    malform calls; engine errors propagate as McpError with the
    typed kind discriminator 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 in walker.rs module
    doc-comment.
  • Concurrent-apply integration tests in
    crates/recast/tests/concurrency.rs. External fs2 lock on
    .recast.lock forces recast --apply to exit non-zero with the
    Locked error; --force bypasses 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 the main baseline stored on
    gh-pages, comments + fails the check at >50% regression.

Changed

  • Walker uses WalkParallel instead of the single-threaded
    iterator. Honors the surrounding rayon pool's thread count so
    --threads N is now respected for the walk phase as well.
  • label_for_path fast 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_apply routes through header(plan) to dedupe the
    JsonHeader construction; the two header builders no longer drift.

Fixed

  • Lockfile error misclassification. acquire_workspace_lock
    used to fold every io::Error from try_lock_exclusive into
    Error::Locked, hiding permission-denied / ENOSPC / EIO behind
    "another recast is already applying". Now matches on
    ErrorKind::WouldBlock and only that variant maps to Locked;
    every other variant propagates as Error::Io with the underlying
    source preserved.
  • Workspace lock derivation canonicalizes input paths and locks
    at the deepest common ancestor. Two --apply invocations against
    the same tree from different CWDs (or one against src/, one
    against src/sub/) now share one .recast.lock instead of
    proceeding in parallel.
  • EXDEV fallback in commit_one, rollback_committed, and
    recover_sweep. A new rename_with_exdev_fallback helper catches
    ErrorKind::CrossesDevices from fs::rename and 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

21 May 21:04

Choose a tag to compare

[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_inner hidden 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.
    Production apply_changes and commit_all carry no hook
    references; apply_inner + commit_all_with exist only under
    cfg(test). The two paths share a finalize_apply helper, and
    commit_all now delegates to a single commit_all_with impl with
    a no-op hook closure instead of duplicating the loop body.
  • Per-apply NonceGen replaces the static AtomicU64 counter
    inside fn nonce(). The struct is constructed once per
    apply_changes call 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::new samples SystemTime::now() once and stores
    it as a precomputed mix seed. The previous shape sampled the
    clock inside every next() call, so a 1k-file apply did 1k extra
    syscalls just to disambiguate sibling filenames.
  • emit_diff / emit_apply narrowed from &Cli to
    &OutputOptions, &Plan. Dropped the transitive dependency on
    GuardOptions and StructuralCli substructs that those helpers
    never touched.
  • --threads N honored 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::apply is 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_sweep skips parentless sibling entries. Previously
    fell back to PathBuf::new() as the HashMap target key when
    path.parent() returned None, silently bucketing unrelated
    parentless siblings together. Skip with a trace! line instead
    so recovery never makes cross-target decisions.
  • recover_sweep aggregates 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_query passes child index, not
    node id, to field_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 --ast queries could silently drop / mis-name
    field annotations as a result.
  • UTF-8 corruption in three byte walkers.
    structural::parse_template, structural::substitute_metavars,
    and pattern::CompiledPattern::replacement_probe advanced one
    byte at a time and pushed each raw byte as char, mojibaking
    every multibyte codepoint. All three now advance by a full UTF-8
    scalar via a shared template_scan::utf8_char_len helper.
  • rollback_committed no longer unlinks the target before
    restoring the backup.
    The explicit remove_file was redundant
    (rename replaces atomically on Unix) and opened a window where
    neither the old nor the new content existed on disk.
  • finalize_apply fsyncs 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_node is iterative. The recursive
    implementation grew the stack proportionally to the depth of the
    user's --ast pattern AST, giving pathological patterns a
    stack-overflow vector. Replaced with an explicit Open / Close
    frame stack — identical output, bounded by heap.

CI

  • actions/checkout / actions/upload-artifact /
    actions/download-artifact bumped from @v4 to @v5
    across
    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 test on windows-latest; no longer cross-compiling
    x86_64-pc-windows-msvc artifacts; 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_path drops
    the backslash-to-forward-slash translation it carried for Windows
    diff headers.

v0.1.7

21 May 16:47

Choose a tag to compare

[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 shared CompiledStructural; only the
    Parser + QueryCursor are per-thread. plan_structural_rewrite
    now drives per-file work via rayon par_iter().map_init(...). The
    rewrite template is pre-parsed into Vec<TemplatePart::{Literal, Capture { index, name }}> so capture-name lookup at match time is
    an index hit, not an O(N) byte scan.
  • commit::stage_all parallel. Per-file write + sync_all runs
    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_scripted parallel. Each rayon worker gets a fresh
    sandboxed Rhai Engine via ScriptRewriter::fresh(); the compiled
    AST is shared. Enables sync feature on the rhai dep (Rc → Arc
    internally).
  • Single-pass match count in rewrite_text. Drops the redundant
    find_iter().count() scan that ran before replace_all and the
    before.to_owned() clone on every changed file.
  • Cheaper regex convergence probe. The per-file idempotency check
    uses Regex::find_iter().count() on the post-image instead of a
    full second replace_all round.
  • HashSet parent-dir dedup in fsync. best_effort_fsync_parents
    was O(N²) via Vec<PathBuf>::iter().any(...); now HashSet<&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_path skips the
    \/ scan on Unix where it can't matter; structural splice
    pre-reserves source.len() + (replacement - range) delta so the
    output String doesn't realloc when matches grow text;
    emit_node writes literal-terminal predicates with write!
    instead of three intermediate format! Strings per terminal.

Changed

  • Wire JSON field order shifted. Plan / Apply / Check reports now
    emit outcome, files_scanned, total_matches as the shared
    prefix (extracted into JsonHeader with #[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() returns ErrorKind directly. The
    Error → ErrorKind mapping lives on impl Error instead of in
    json::error_kind(); the JSON module re-exports ErrorKind from
    error for back-compat.
  • Language::from_name returns Result<Self, Error> instead of
    Option<Self>. Unknown names surface as Error::UnknownLanguage
    from the library boundary.
  • RewriteOutcome shape. Dropped the before field — the caller
    already owns the pre-image. changed() method removed; compare
    outcome.after != before at the call site if needed.
  • FileChange.before field removed. Was #[serde(skip)] and
    only used to render the diff during planning; apply_changes has
    always read from after. Pre-1.0 break for anyone reading the
    field directly; FileChange.diff carries the rendered diff.
  • handle_plan_error returns Result<u8> instead of u8 so 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.).
  • SiblingKind enum drives recast-sibling filenames. The
    .{target}.recast.{token}.{nonce} token (bak / tmp) is now
    emitted and parsed through one SiblingKind::as_str /
    SiblingKind::from_token pair so the two sides cannot drift.

Added

  • Error::InvalidThreads + Error::ThreadPool variants replace
    the synthetic Error::Io { path: empty } wrappers that
    parallel::build_pool used to emit for non-IO failures. JSON
    surfaces them as invalid_threads / thread_pool error kinds.
  • ScriptRewriter::fresh() — sibling rewriter, new sandboxed
    Engine, shared AST. Designed for per-rayon-worker construction.
  • template_scan module — 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
    both plan_rewrite and plan_structural_rewrite.
  • Multi-file structural criterion bench
    (bench_plan_structural_rewrite) covering 10 / 100 / 500 file
    fixtures.

Internal

  • commit::recover_sweep reports ignore::Error via the existing
    Error::Walk variant instead of building a synthetic
    Error::Io { path: empty }.
  • commit extracted parent_dir / remove_nonced helpers; rollback
    loops collapsed.
  • Binary's Language::from_name / dispatch(plan) helpers consolidate
    the apply / check / diff trailer duplicated between run and
    run_structural.
  • Binary Cli adds min_matches(), paths_as_pathbufs(),
    recover_paths(), and acquire_workspace_lock_for(&cli) helpers
    so the recurring Some(at_least.unwrap_or(1)), path conversions,
    the --recover clap-workaround fold, and the lock-root probe
    each live in one place.
  • CompiledStructural::apply collects 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; full ErrorKind vocabulary including
    invalid_threads / thread_pool).
  • AGENTS.md §11 records the harness-classifier workaround for
    git commit: split git add and git commit into separate
    shell calls; do not chain them with && + heredoc.

v0.1.6

21 May 14:57

Choose a tag to compare

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