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.