Status: implemented (M9 batch 3, plan v2.8 ruling ②; wire face
erase/1, introduced at proto 2.16.0, RG10 firewall at 6.1.0; predicate in CE.Erase.Cost). The implementation answers to this file; divergence is a defect in one of the two. Acceptance is pinned bycli/tests/it/erase_e2e.rsand the CI Dogfooderase .. --checkself-gate.
User ruling 2026-08-21 (plan v2.8): the GUI must be able to erase,
not only diagnose — via a deterministic two-phase ce erase that acts
ONLY on classes whose removal is provably safe, with plan/apply
separation, dry-run default, a clean-worktree precondition, and the
eraseability judgment in the Haskell core. Never an LLM rewrite:
this product's thesis is deterministic computation auditing
non-deterministic output, and an eraser that guesses would forfeit it.
| class | source verdict | the erase | why it is deterministic-safe |
|---|---|---|---|
| dead file | deadcode file-tier dead (no kept in-edge, no entry flag, ce deadcode --check's own bar) and private: verdict 1 unref_private or 3 unreach_private |
delete the file | the graph verdict IS the safety proof: nothing in-corpus references it; the unresolved-site count must be zero for its language, else the row is refused (a verdict that assumed no in-corpus lands is not a deletion licence). The PUBLIC half of the dead domain — 2 unref_public, 4 unreach_public — is refused by name as public_surface since 6.1.0: a library's exported API is unreferenced in-corpus by construction, so "nothing here calls it" is not evidence about its callers, and the four-way dead code exists precisely to keep the two apart (RG10) |
| verbatim doc duplicate | docdup pair with verbatim = full segment (byte-identical after the family's own masking) |
delete every occurrence after the first, in path-lexicographic order | prose has no call sites; byte-identity means zero information loss; the survivor is chosen deterministically, never judged "better" |
| whole-unit T1 twin | dedup T1 block spanning an ENTIRE unit whose twin is byte-identical AND whose copy is itself graph-dead and private — the row carries the copy's dead verdict code, and 2 / 4 are refused as public_surface like a dead file's (proto 7.0.0) |
delete the dead copy | the narrow intersection of the clone and liveness verdicts — a cross-function clone with live references has NO deterministic-safe erase, and this contract says so instead of pretending |
The planner runs only deadcode, docdup, and dedup. Candidates
from those families that fail the core predicate remain non-eraseable
plan rows; the sole named aggregate out_of_class advisory is
t1t2_block_no_whole_unit. T3 and structural findings are outside
the plan surface and never appear.
ce erase [root] (the plan — default, read-only with respect to
user files; it may create or refresh the .ce/ cache):
- runs the source families exactly as their own commands do (same caches, same cores, same knobs);
- sends the fact tables — and, since proto 7.2.0, the rows' targets
beside them — to the core's
erase/1, which answers the eraseable-row set and which row stands for each target — the PREDICATE and the closure are Haskell's (ADR-008: which rows are safe is judgment; the bytes are measurement; see The target closure below); - renders a unified diff (
--format json: machine rows with file/span/class/provenance), each hunk carrying its verdict provenance (family, member/segment id, evidencefile:line) and a content hash of the target file; - prints the advisory rows (what it will NOT touch, and why);
- exits 0 without changing user files; the
.ce/cache may have been created or refreshed.
ce erase --apply additionally requires, in order:
- a git repository (revert must be one
git checkoutaway); - a CLEAN worktree (
git status --porcelainempty — an erase must never be entangled with uncommitted work; the tool's OWN state under.ce/is exempt from the check, because running the planner is what creates it — ce's index is never "uncommitted work", batch-7 defect sweep: the carve-out existed in code, undocumented); - every target inside THIS repository's own subtree — a target below
a declared submodule (a gitlink in HEAD) refuses by name: the
worktree the command verified is the superproject's, and a file
under
cli/tests/belongs to another repository's index and history (plan v2.18 follow-up). Since a declared submodule is a reader of this tree and never a measured part (plan v2.18 step #12), the superproject's plan holds no row under it and this refusal is defence in depth against a hand-built plan; a dead file in the suite is reported and erased by the CI leg that runsce erase tests --checkinside the suite's own repository; - every target file's content hash equal to the plan's (a file that moved since planning refuses by name — plans are not portable across edits);
- after writing: the source family re-runs and the erased verdicts must be GONE — a survivor fails the command loudly (convergence is part of apply, not a suggestion);
- an append-only record in
.ce/erase-log.ndjson: ts, class, file/span, provenance, plan hash — an audit file for human review alongside git's recovery path, read back byce erase --log, the MCP toolerase_logand the GUI erase screen's audit-log section (one document,ce.erase-trail-report; a line the reader cannot parse is named by number and fails--log's exit code).
A target is one (path, span): the whole file, or one line span in it.
Several candidate rows may name one target — a dead file that is also
a byte-identical twin of a live unit names its path twice, once per
class — and the plan holds ONE row per target. Which row stands is
judgment and lives in the core (CE.Erase.Cost.keptRows) since plan
v2.30 step 7b; until then the planner closed the set for itself
(close_targets, 7.0.0 O51). The request carries
targets=[[pathId,start,end]] aligned with rows (dense path ids —
names never cross; 0/0 = the whole file; a verbatim_doc row names
a span, the other two classes a whole file; the table in key order),
and the reply carries kept, one bit per row:
- an eraseable whole-file row owns its path — every span row on that path is closed out (an apply that deleted the file and then spliced lines out of it would refuse on the hash it can no longer read);
- within one target the eraseable row with the richest
licencestands:t1_twin(names the live unit it duplicates) overdead_file(names only its death) oververbatim_doc; - with no eraseable row the
dead_filerow stands over thet1_twinrow (advisoryFirst): its reason is about the file — the categoricalpublic_surfacebar or the trust fact — where the twin's is about the copy; - ties inside one class break to the EARLIEST row, which is why the contract asks for key order: the planner sends its rows sorted by (path, span, class name), so two producers sending the same rows in the same order close the same way.
A reply without kept is refused by name as a pre-7.2.0 core: a plan
closed on this side would hold two rows per target.
.ceignoreis a human ruling and binds here exactly as in the guard: an ignored path is never planned, let alone applied.- The exclusion model (built-ins + ce.toml + .gitignore) bounds the plan the same way it bounds every walk.
--applytouches ONLY files the plan named; a plan is the complete and closed statement of intent.- No network, no LLM, no heuristics: every planned hunk must be reproducible byte-for-byte from the same tree.
- GUI (batch 4) renders the SAME plan JSON and applies through the SAME library entry — one implementation, two faces.
- plan-then-apply on a fixture tree erases one dead file, one
verbatim-doc span and one private dead twin through
t1_twinitself (spare.py, whose_spare_totalduplicates the live unit in__main__.py), while the tree's second dead file (copy.py, which declares an exporteddef) is refused by name aspublic_surface— RG10 held at the acceptance gate through apply, not only in the table above — and the re-run proves zero surviving source verdicts; - a dirty worktree, a drifted file hash, and a non-repo root each refuse BY NAME without touching anything;
- an advisory row (live T2 clone) is never planned;
ce erasetwice = byte-identical plans (determinism pinned by test);- the self repo:
ce eraseat HEAD plans ZERO rows (this repository keeps itself clean — a non-empty self-plan is a red gate).