R0 — recovery rate at zero residue
Every computer-use project reports a success rate. Nobody reports what a failure leaves behind. R0 is that second number.
node scripts/recovery-bench.ts # table
node scripts/recovery-bench.ts --json # same numbers, machine-readable
Five scenarios (writes into new directories, explicit nested writes, overwrite + move, folder taxonomy, and moves into new directories) are run twice:
- happy path, then
undo— the directory must come back byte for byte; - failure injection at step k, for every k — the kernel must roll back and leave nothing.
What is measured
| Metric | Definition | | --- | --- | | R0-residue | after an injected failure, is the directory byte-identical to before? | | R0-rollback | were all steps executed before the failure actually undone? | | R0-traceable | does the adapter report at least one journal line for the failed run? |
The traceability score currently checks log presence, not each step's identifiers or fields. These synthetic scenarios inject a failing post-assertion; they do not establish recovery from every possible filesystem error, process crash, or failed undo.
Current result
| 场景 | 计划步数 | 失败注入点 | 残渣归零 | 已撤销/已执行 | 可追溯 |
| write-into-new-dir | 2 | 1..2 | ✅ | 0/0 1/1 | ✅ |
| nested-writes | 3 | 1..3 | ✅ | 0/0 1/1 2/2 | ✅ |
| move-and-overwrite | 3 | 1..3 | ✅ | 0/0 1/1 2/2 | ✅ |
| folder-taxonomy | 7 | 1..7 | ✅ | 0/0 … 6/6 | ✅ |
| move-into-new-dir | 2 | 1..2 | ✅ | 0/0 1/1 | ✅ |
R0-残渣 100.0%(17 次失败注入)
R0-回滚 100.0%
R0-可追溯 100.0%
正常路径撤销后逐字节还原:5/5
What this metric already caught
The write-into-new-dir scenario covers a regression where fs.restore deleted a newly written file but left its automatically created parent directories. The dedicated scripts/write-recovery-test.ts also checks that pre-existing directories and files added by others are preserved.
The first run reported R0-traceable = 76.9%. Cause: when a step fails, the kernel wrote a failed entry to the journal file, but RunReport.journal only collected successful entries and the report carried no taskId — so a failing run could not be cited afterwards. Both are fixed (RunReport.taskId, failure entries included), and the metric is now 100%.
A metric that never finds anything is decoration. This one found a hole in the audit trail on day one.
Running the same matrix on another tool
The point of publishing R0 is that someone else can run it. Any tool can be plugged in by implementing three methods:
// scripts/my-tool-adapter.ts
import type { R0Adapter } from "./recovery-bench.ts";
export function createAdapter(): R0Adapter {
return {
name: "my-tool",
async apply(root, steps, failAt) {
// run steps 1..failAt-1 for real, stop at failAt, return counts
return { executed: failAt - 1, rolledBack: /* how many you undid */ 0 };
},
async undo(root) { /* undo the last run */ },
journalLines(root) { return null; }, // null = no audit log
};
}
node scripts/recovery-bench.ts --adapter scripts/my-tool-adapter.ts
Proof the harness can fail something
A benchmark that passes everything is advertising, not measurement. scripts/r0-adapter-naive.ts is a tool that writes files and has no undo and no journal; running it through the same fixtures gives:
R0-残渣 0.0%
R0-回滚 0.0%
R0-可追溯 0.0%
正常路径撤销后逐字节还原:0/5
and the process exits non-zero. scripts/r0-negative-test.ts asserts exactly that (5/5). So when deskshell reports 100%, it is being scored by a harness that has demonstrably failed another implementation.
The UI side: recovery and refusal before execution
Files can be restored byte for byte. Windows and controls could not — until ui.value existed.
scripts/recovery-bench-ui.ts runs the same injection matrix against a mock window and measures state (geometry + control values + enabled flags):
| 场景 | 失败注入点 | 状态归零 | 已撤销/已执行 |
| move-window | 1 | ✅ | 0/0 |
| move-window | 2 | ✅ | 1/1 |
| type-into-field | 1 | ✅ | 0/0 |
| type-into-field | 2 | ✅ | 1/1 |
| invoke-button | 1 | ✅ | 0/0 |
R0-UI 状态归零 100.0%(5 次注入)
R0-UI 回滚 100.0%
正常路径撤销后一致:2/3
What changed: the protocol gained an undo kind ui.value — *write the previous value back* (null means "this control had no value"). ui.type now declares it, so typed text can be reverted. Drivers must implement the optional setValue primitive; Windows does (ValuePattern, falling back to focus + select-all + paste), Linux's window-level driver does not and says so.
The 100% injected-failure result includes rejecting the irreversible button action before it runs. That refusal leaves the initial state unchanged; it does not demonstrate undoing a pressed button. When explicitly allowed to run, the button action cannot be restored, which is why happy-path undo is 2/3.
Two traps this exposed:
nulland""are different states. Writingnullinto a control that started as""leaves it atnull— the first version of this bench did exactly that and scored itself down. Restore the value you read, not "empty".- Mock-only success is not success. The Windows driver's
setValuewas verified on a real window (write, read back, clear); it has no stable automated test because the WinForms fixture exposes a legacy UIA proxy whose Name is the text itself.
Limits
- Scenarios are synthetic and file-only. GUI driver failures are not covered yet.
- 17 injections cover these five scenarios only. Add scenarios in
scenarioListto expand the evidence. - R0 measures *our* kernel. Running other tools through the same harness is the point of publishing it — that has not been done yet.