Architecture¶
Ownership rule
One main loop owns application state, terminal state, layout, rendering, and visual effects. Persistent scanner, deletion-planner, and serial deletion-executor workers perform blocking filesystem work in the background through typed queues with fixed limits. A terminal writer, a scan-store thread that writes the session's scan data, and a short-lived deletion-history exporter act only on the main loop's decisions and decide nothing themselves.
flowchart LR
Input[CLI, environment, and TOML] --> App[Main loop and terminal session]
App --> Coordinator[Session coordinator]
Coordinator --> Scanner[Persistent scanner]
Scanner --> Store[ScanStore on the store thread]
Store --> Views[Map and reports]
Coordinator --> Deletion[Planner and serial executor]
Deletion --> Filesystem[Verified filesystem operations]
Visual effects run only after the interface prepares its content. They never own product state or decide whether an operation is safe.
Components¶
Command Line and Configuration¶
Excise validates command-line options, environment variables, and the TOML configuration file before starting the terminal interface. Precedence is command line, environment, versioned file, then defaults. Unknown keys and invalid limits are rejected. Table and JSON reports work without a terminal.
Terminal Session¶
A terminal-session guard owns raw input mode, the separate screen, cursor visibility, colors, and optional mouse capture. It restores the terminal after normal return, a typed error, a panic, a boundary-safe cancellation, or an external signal or console control event. Restoration is ordered after every frame byte the terminal writer thread has been handed and waits for it, bounded, so a terminal that never reads anything cannot hang exit.
Main Loop¶
The main loop polls terminal input with a short maximum wait. It renders each folder opening before returning to queued scanning and applies at most one stored scan batch before checking input again. Admitting a batch, merging runs, and publishing a finished scan are not the loop's work: it hands each batch to the scan-store thread and swaps in each published generation when that thread reports one, and it takes no further scanner event while the thread's queue is nearly full. When the map is moving, a fixed-size channel slows scanner updates. The interface redraws when state changes, apart from deletion progress and visual effects. Effects run at most 30 times per second; overdue frames are dropped, and a new transition replaces an earlier transition with the same purpose. A dedicated writer thread transmits each rendered frame's bytes to the terminal; the loop decides what to render and renders a new frame only once that thread confirms the previous one drained, so a terminal slower than excise's output paces frame production instead of blocking scan ingestion or input handling. On Unix, a dedicated excise-signal-listener thread turns an external SIGTERM, SIGHUP, or SIGQUIT into the same confirmed-quit request that a Windows console control handler, running on a system thread rather than one Excise starts, delivers for a close, break, logoff, or shutdown event; see threat-model.md's "Terminal restoration" control.
Scanner and Session Coordinator¶
The scanner walks directories without recursion and uses a fixed worker count. On Unix a worker reaches each folder by opening it name by name from the scan root's handle, without following a link, and reads every entry's metadata through the handle of the folder that lists it. How deep a folder sits and how long its full path is therefore do not limit a scan, and a worker holds the same few descriptors at any depth. Depth still costs time, because every folder is reached from the scan root again: a very deep tree scans much more slowly than a flat tree of the same size. Windows still reads each entry by its path, so its own path length limit applies there. One session coordinator owns work keys, task ownership records, focus, scan versions, and final status for scanning, reduction, refresh, and deletion. Workers cannot change queue or task-ownership state.
One scanner service performs the first walk and later refreshes one at a time. A refresh has its own task-ownership and cancellation boundary. The published result remains available until a newer completed scan is ready. Focus requests change priority only when a worker accepts more work.
The scanner journal, ScanStore runs, and page-index build data use a separate scan-store quota. Directory deletion plans and results use separate temporary storage. The default worker count leaves one processor available for the main loop when possible and stays between one and eight; a configured value must be between one and 32. Exclusions and filesystem boundaries remain visible in the working model, and link targets are never traversed.
ScanStore¶
ScanStore is private session storage for scan data. Workers turn each fixed-size batch into sorted path and identity runs. The scan-store thread accepts a run only while the directory task that produced it remains valid, combines a fixed number of inputs at a time, and creates a compact block-indexed ChildQuery for navigation. Raw runs are released as publication proceeds, so a completed scan retains only the compact query. Each run's blocks carry SHA-256 checksums that every read verifies. The store belongs to one session and is never reopened, so no write waits on a durable sync. A sealed run holds no file descriptor until it is read, so neither a full admission channel nor a wide tree grows the store's descriptor use, and a published scan keeps a fixed number of runs open whatever its size. On Unix, startup also raises the process's soft file-descriptor limit toward its hard limit, never lowering an inherited one.
The scan-store thread, excise-scan-store, owns the store's write side. The main loop hands it each sealed batch, and the thread admits the batch, merges runs, and builds the published generation, so none of that blocking I/O delays input or a frame. The main loop decides every step and the thread only applies it: commands and results travel through two bounded queues, and a finished generation changes hands, so the main loop swaps it in and reads its pages synchronously, as it always has. Each sealed batch holds one in-flight credit from the scanner's seal until the batch is dropped, which caps how far the scanner can run ahead. background-tasks.md defines the thread's ownership, bounds, and shutdown.
A session's directory also holds a lock file that the session locks for as long as it lives, and the file records that Excise made the directory. The next start can therefore tell a directory that a killed session left behind from a running session's, and removes only the former, as the resource controls in threat-model.md describe.
Concrete pages, bounded retention
If a path cannot be represented, available pages remain concrete, the report records the omitted-path count, and affected space totals become lower bounds. A directory the scanner could not open or list is tracked by path, up to a fixed cap, so every ancestor up to the root can be reported uncertain with an open upper bound; past that cap the session stops tracking individual paths and reports every folder uncertain instead, rather than growing the tracked set without bound.
The interactive map reads one 512-entry page of direct children and its ancestor chain. It keeps only a small cache of recent pages. Page Down and Page Up move between concrete pages. No child is replaced with an undeletable summary. When an unfiltered page is not cached, the map reads only the requested slice from the published ChildQuery. Completed-folder navigation has no loading view or live-model fallback; filters read only the relevant stored page. While a scan is still running, the map shows a live view of each folder that the scan-store thread builds from the scan so far; a folder opened then appears when that page arrives, and the map keeps drawing what it had until then.
Working Model¶
Workers seal path and identity facts before the interface receives their events. During a scan, the interface retains only bounded progress and interaction state; it does not keep a second filesystem-wide model that could change the result. Once ScanStore publishes a scan, the map and reports read immutable pages and query records. Names, metrics, and child lists are retained only for the visible page rather than the complete filesystem. Walking and deletion use loops rather than the call stack.
The map retains a MapOverflow summary for entries that do not fit in the terminal. This changes display geometry only: direct-child pagination keeps filesystem entries concrete and selectable. The renderer draws the summary and its count or size labels only when space permits.
The default process-memory limit is 512 MiB. Working data may use 75 percent of that limit, leaving 25 percent for the rest of the process. ScanStore fan-in, batch size, page size, and sparse query indexes are fixed or block-bounded. No scan result can make an interface-owned collection grow with the scanned filesystem.
Space Accounting¶
When ScanStore publishes a scan, it combines identity observations, places allocations shared by multiple names at their lowest common ancestor as noninteractive shared totals, and keeps concrete directory paths. A metadata failure remains uncertain; interface memory pressure never fabricates a partial total.
Deletion¶
The main loop retains at most four non-overlapping interactive deletion requests. Every accepted planning or execution command has a coordinator-owned work record. An item returns to the ready queue only when its worker did not accept it, and the interface keeps it until success, failure, cancellation, or invalidation is recorded.
A separate planner can prepare the next identity plan while one executor performs the confirmed target’s final recheck and serial deletion. Large directory plans retain only a limited in-memory prefix and place later plan and result records in authenticated temporary storage outside the selected target before consent. Platform code works relative to the confirmed parent, does not follow links, and verifies every decoded plan path before mutation. On Unix the planner reads each entry, and asks whether each folder is the root of a mount, through the handle of the folder above it and the entry's own name, so a folder nested deeper than the system's path length limit is planned and deleted like any other. Every entry is reached from the scan root's handle again, so depth costs time: a very deep tree takes longer to plan and delete than a flat one of the same size. Excise moves each entry aside before it removes it, by exchanging the entry's name with a placeholder's, and names the placeholder, which is always a file, as the source of the exchange, because macOS 14 refuses a rename whose source is a folder whose path is longer than the system's path length limit. A system that still refuses the rename that moves an entry that deep aside stops the run at that entry. The background task system decision defines the session coordinator, fixed-size deletion panel, and review required before new task types.
Reports¶
Versioned scan-report and deletion-history documents use the same stable path encoding. Scan reports read immutable published ScanStore pages and state uncertainty from their recorded coverage. The interactive deletion-history export serializes on a thread of its own, so a long history never delays a frame; background-tasks.md's "Deletion-History Export" defines it.
Dependency Direction¶
Platform code supplies file information to the scanner and deletion code. The scanner seals facts for ScanStore, whose thread builds immutable pages that application state and reports then read. Domain code does not depend on terminal widgets, and interface code does not change the filesystem.
The storage map uses dense half-block cells in its pane. Each cell carries foreground and background shading without gaps between entries. Map movement belongs to application state: the board holds the current position, next position, and one transition clock. A new scan may redirect a transition from its current position without restarting it.
Opening an entry grows its contents from the selected rectangle. Moving back contracts departing contents into that rectangle while the parent view grows from it. Entries no longer in the parent view remain visible until they finish moving away. If the selected rectangle cannot be resolved, the board settles without a directed transition.
Architectural Constraints¶
| Constraint | Consequence |
|---|---|
| One writer | The owner loop is the only thread that decides application or terminal state and content; nothing else changes either. The threads that are not workers act only on its decisions. A dedicated writer thread (see background-tasks.md's "Terminal Output Writer") only transmits the bytes it is handed, in the order it is handed them, and never decides what they are. The scan-store thread (see "Scan Store Thread") applies the commands it is sent, in the order sent, to the store it owns and reports each outcome; a generation it publishes changes hands, and the owner loop decides what that changes. A transient thread writes one deletion-history file from the snapshot it is given ("Deletion-History Export"). |
| Fixed owners | No queue or model owner grows without a limit, including the scan store's open descriptors: a sealed run holds none until it is read, and a published generation retains a fixed count. The scan-store thread's two queues have capacities that follow the in-flight batch cap, not the tree, and every sealed batch returns its in-flight credit exactly once. |
| No shell mutation | No shell command performs scanning or deletion. |
| No network path | No network client or telemetry runs as part of the program. |
| Reviewed unsafe boundary | Unsafe code is confined to the Windows system interface; domain, model, runtime, and interface code remain safe Rust. |
| Public review | A new task type requires public design review, and so does a change to what another background thread owns, how its queues are bounded, the order it acts in, or how it stops (background-tasks.md, "Task Types"). |