Splice: Span-Safe Refactoring That Knows Your Codebase
Splice edits code using byte-accurate span replacements backed by graph algorithms. It replaces function bodies, renames symbols across files, deletes definitions with all their references, and generates machine-checkable refactoring proofs – all with AST validation and automatic rollback on failure.
The repo is at github.com/oldnordic/splice. 49.6K lines of Rust, 781 commits, 482 tests. This is the development timeline.
The starting point: byte spans, not lines (December 2025)
Splice started with one idea: code editing should use byte offsets, not line numbers. Line-based editing breaks when the file changes between the edit decision and the edit application. Byte spans, anchored to AST nodes, stay correct as long as the AST doesn’t change.
v0.1.0 (December 23) did one thing: replace a function body in a Rust file, validate with tree-sitter and cargo check, and roll back if validation failed. 22 tests, single-file only, no cross-file references, no persistence.
The safety model was there from day one:
- UTF-8 boundary check – the replacement span must not split a multi-byte character
- Tree-sitter reparse – the modified file must still parse as valid AST
- Compiler gate –
cargo checkmust pass on the modified file - Atomic rollback – if any step fails, restore the original file from backup
Rapid expansion (December 28 – January 2)
Three releases in five days:
v0.2.0 (December 30) added the delete command with cross-file reference finding. It tracks imports, re-exports, and same-file unqualified calls across the workspace. Shadowing detection handles local variables that shadow imported symbols. Trait method references handle value.method(), Trait::method(), and Type::method() patterns. Test count jumped from 22 to 298.
v0.3.0 (December 30) went from Rust-only to 7 languages. Patch and delete now work on Python, C, C++, Java, JavaScript, and TypeScript. Each language gets its own compiler validation gate (python -m py_compile, gcc -fsyntax-only, javac, node --check, tsc --noEmit). Language auto-detection from file extensions. Test count: 339.
v0.4.1 (December 31) added batch operations, preview mode, backup/undo, multi-file pattern replacement, and structured error responses. The preview flow clones the workspace, applies changes, runs validation, and reports stats without touching the real files.
Magellan integration (January 2)
v0.5.0 was the architectural turning point. Splice stopped being a standalone refactoring tool and became a Magellan client. The new MagellanIntegration wrapper provided label-based symbol discovery, multi-language indexing via Magellan’s parsers, and code chunk storage with byte spans for fast retrieval.
This eliminated the need for Splice to maintain its own parsers. Magellan indexes; Splice queries and edits.
v2.0.0: the overhaul (January 18)
The first major release rewrote the internal architecture across 10 phases and 133 commits:
- All
unwrap()calls eliminated from production paths - Structured JSON output with
execution_id,match_id,span_idfor every operation - SHA-256 checksums for pre/post-operation verification
- Execution logging audit trail in
.splice/operations.db - SQLiteGraph upgraded from 0.2.11 to 1.0
- 215+ tests passing
This was the release where Splice went from “script that edits files” to “tool you can trust with automated refactoring.”
Unified CLI: query and edit in one tool (January 24)
v2.2.2 merged Magellan query commands into Splice’s CLI. Instead of switching between magellan find and splice patch, you could do both in one tool:
splice status– database statisticssplice query– list symbols in a filesplice find– locate symbols by name or IDsplice refs– show callers/calleessplice files– list indexed filessplice export– export graph data as JSON, JSONL, or CSV
The query commands delegate to Magellan in-process (library calls, not subprocess). The edit commands use Splice’s own span-safe pipeline. One tool, two capabilities.
Cross-file rename and graph algorithms (v2.3.0)
v2.3.0 (unreleased as a crate version, but the code shipped in later releases) added the heaviest features:
Cross-file rename. splice rename --symbol old_name --to new_name --path src/lib.rs replaces a symbol and every reference to it across all files. Uses Magellan’s ReferenceFact byte offsets for precise span targeting. References are sorted by (file_path, byte_start descending) so replacements don’t invalidate earlier byte offsets. UTF-8 boundary validation on every replacement.
Graph algorithms. Six commands that operate on the Magellan call graph:
reachable– BFS forward/backward from a symboldead-code– symbols unreachable from an entry pointcycles– Tarjan’s SCC algorithmcondense– collapse SCCs to DAG with topological levelsslice– forward/backward program slicingimpact– blast zone detection
Proof-based refactoring. splice rename --proof generates a machine-checkable proof file with before/after graph snapshots, invariant validation (reference counts, orphan detection, ID stability, entry points), and SHA-256 checksums for audit trail integrity. splice validate-proof --proof <path> verifies the proof independently.
Performance for 1K-symbol graphs: reachability 5-15ms, dead code 10-25ms, cycles 20-40ms, condensation 15-35ms, slicing 25-60ms.
The binary backend experiment (February 10)
v2.5.0 explored a binary B+Tree backend via sqlitegraph, promising 2-100x faster lookups for 100K+ LOC codebases and ~70% smaller databases. Like llmgrep and Mirage, the experiment didn’t deliver measurable benefit for the codebase sizes in my stack. The binary backend was retired. Current Splice uses SQLite databases.
Bug-fix sprint: seven fixes in one release (May 12)
v2.6.9 fixed seven independent bugs in a single release:
splice searchreturned 0 matches for all queries – Theglobcrate doesn’t support{rs,py,ts,...}brace expansion. Fixed to iterate per extension.splice apply-filesapplied immediately with no preview – Added--dry-runflag.splice snapshots cleanupsilently deleted hundreds of snapshots – Added--yesconfirmation for bulk deletions above 50.error_code.locationwas always"<unknown>"– Changed fromStringtoOption<String>, omitted from JSON when unavailable.splice migrate-dbused--db-pathinstead of--db– Aligned with project-wide convention.splice createreturned success on validation failure – Now returns proper error with compiler diagnostics.splice rename --proofhelp referenced nonexistent--previewflag – Corrected to--dry-run.
v2.6.8 (same day) fixed a deeper issue: the <unknown> placeholder in I/O errors. The root cause was a catch-all From<std::io::Error> for SpliceError impl that wrapped every bare ? on io::Error with PathBuf::from("<unknown>"). Removing the From impl made bare ? on io::Error a compile error – every call site must now explicitly provide the originating path. Six call sites migrated. Also fixed find_workspace_root walking up to /tmp when a stray Cargo.toml existed in an ancestor directory.
338 clippy warnings resolved in the same release.
The rename definition bug (May 8)
v2.6.4 fixed a bug where splice rename only renamed call sites, leaving fn old_name() unchanged while callers referenced fn new_name(). The fix injects the definition site (name-only span) into the reference list before grouping. The name-only byte offset is computed by searching for the symbol name within the declaration span, avoiding full-declaration replacement that would corrupt the function body.
The same release fixed splice undo failing on rename backups – restore_from_manifest() could only parse patch-format manifests, causing “expected a sequence” errors when restoring rename backups that used HashMap<String, String> instead of Vec<BackupEntry>.
Import-aware code completion (April 24)
v2.6.0 added grounded code completion using the Magellan database. splice complete --file src/lib.rs --line 27 --column 8 suggests symbols from imported modules, distinguishing local (Database) vs imported (Imported) sources. Every suggestion includes database IDs for verification – no hallucinated symbols.
Internal query time: 3-13ms on the Splice database. Single database query per completion request.
File split and forge API (May 29)
v2.8.0 was the biggest cleanup release. 11 files over 1K LOC were modularized into concern-separated submodules:
main.rs(7,067 → 589 lines) split into 18 command files undersrc/cmds/graph/magellan_integration.rs(3,712 → removed) split into 8 filesoutput.rs(1,646 → removed) split into 5 filespatch/pattern.rs(1,660 → removed) split into 4 files- Zero files remain over 1K LOC
The same release added the forge module: four high-level library functions (patch_symbol_in_file, rename_symbol_across_files, preview_patch_symbol, resolve_symbol_span) for programmatic use by agents and tools.
The architecture that survived
Source files → Magellan (index) → .db file → Splice (edit) → modified source files
↑
forge module → agents, tools
Splice’s editing pipeline for any operation:
- Resolve symbol via Magellan (name, ID, or semantic query via HNSW)
- Load byte spans from Magellan database
- Apply replacements in reverse byte order (so earlier offsets stay valid)
- Validate: UTF-8 boundary, tree-sitter reparse, compiler gate
- On failure: atomic rollback from backup
- On success: write modified file, optionally generate proof
No LSP, no language server, no background process. Byte spans from the graph database, validated by the compiler.
What I’d do differently
The From<std::io::Error> catch-all was a mistake. It let bare ? on I/O errors compile without providing the file path, which meant every I/O error said <unknown> instead of the actual path. Removing it and forcing explicit map_err at every call site was the right fix, but the catch-all should never have existed.
The binary backend was premature optimization. 100K+ LOC codebases would benefit from O(1) KV lookups, but I don’t have codebases that large in my stack. The SQLite backend handles everything I need.
The v2.3.0 features should have shipped as a numbered release. Cross-file rename, graph algorithms, and proof-based refactoring all landed in the codebase but never got their own crates.io version. They shipped incrementally in later releases. That makes the version history harder to follow.
By the numbers
| Metric | Value |
|---|---|
| Lines of Rust | 49,639 |
| Published versions | 38 |
| Languages supported | 7 |
| CLI commands | 17 |
| Tests | 482 |
| Commits | 781 |
| Crates.io downloads | 644 |
| Development span | December 2025 – June 2026 |
| Current version | 2.9.0 |
644 downloads across 38 versions over six months. Same niche as the rest of the stack: me, my agents, and the Magellan ecosystem.
The tool in context
Splice is the editing layer in the code intelligence stack:
- SQLiteGraph stores the graph data.
- Magellan indexes source code into that graph.
- llmgrep queries symbols and references.
- Mirage analyzes control flow.
- Splice edits code with graph-backed safety.
splice patch --file src/lib.rs --symbol my_fn --content "pub fn my_fn() -> i32 { 42 }"
splice rename --symbol old_name --to new_name --path src/lib.rs --dry-run
splice delete --symbol unused_fn --path src/lib.rs
splice reachable --symbol main --path src/main.rs --direction forward
splice dead-code --entry main --path src/main.rs
splice complete --file src/lib.rs --line 27 --column 8
splice rename --symbol old --to new --proof --dry-run
The code is at github.com/oldnordic/splice. The crate is on crates.io. GPL-3.0.