Expand description
The recover module: the stray files of the record stores, classified by evidence and
swept exactly once.
majordomus recover is a mutating development command — it writes .ai/repo/sessions/
and .ai/local/state/ — and until this module it was backed by nothing in the registry.
ADR 0040 says a development operation exists as a capability! declaration and that
every surface is a consumer of it; scripts/development-semantics-check measures exactly
that and reported backing:recover. This module is the first capability that backs it.
§What moved, and what deliberately did not
One capability, recover.orphans, and it owns the decision and the destruction: for
every stray file of the record stores it reads the clock and the content, returns a
verdict with the evidence that produced it, and — unless the call is a check — removes
the ones the verdict says are removable. lib/recover.sh no longer decides any of that;
it asks, renders the answer in the words a person reads, and counts.
What stayed in shell is the constructive half. A publish temp that holds the only
copy of an episode is published rather than deleted, and publishing a record is
mj_publish_record’s semantic — the same one session close uses, and the one
recover episodes composes a synthesised record with. Moving one caller of it without
moving the writer would put a second record writer in a second program, which is the
defect ADR 0040 exists to prevent rather than a step towards fixing it. So this
capability names an Incomplete stray and removes nothing; the shell publishes it. The
record writer is the next thing to converge, and when it does, recover.orphans gains
the publish and loses the Incomplete verdict’s “the caller acts” note.
The same argument keeps recover episodes and recover records in shell for now: both
write records, and neither can converge before the record writer does.
§Why this is not super::lifecycle’s job
lifecycle.recovery already lists the .tmp.* files of the sessions section, and a
second definition of the same thing would be this repository’s favourite defect. It is
not one. That module states its own constraint — “no thresholds and no clock … what this
module reports is what it can observe without an opinion” — so it reports a path and a
byte count and nothing else. This module is the opinion: it reads the clock, applies the
threshold, and acts. An observation and a decision over the same files are two different
things to depend on; what would be a duplicate is a second verdict, and there is one.
§The two guards, and why they are here rather than at the call site
The threshold has no default. older_than_seconds is a required field with no serde
default, and zero is refused. session.stranded_after is a policy value, and a reader
that carries its own default is a second place the number lives — majordomus doctor
refused exactly that when lib/recover.sh was first written. A sweep that ran against an
absent threshold would find every file stale, which is the one failure this command must
not have (project.destructive-sweeps-fail-closed).
Nothing is removed that is not a temp file this tool writes. removable is decided
from the verdict, and remove_stray refuses any path that is not a regular file whose
name matches the two patterns this tool creates — .tmp.* inside a record store, or
<target>.mj-tmp. A directory is never removed, whatever its verdict: SECURITY.md
states “no recursive deletion”, and a stale .mj-stage.* staging directory is therefore
reported and left in place. check is a true dry run: it is consulted before the single
call site of remove_stray, and nothing else in this file touches the filesystem for
writing.
§The one capability, as the registry states it
use majordomus_cli::capability::builtin::recover;
use majordomus_cli::capability::{CapabilityKind, Effect};
let m = recover::module();
assert_eq!(m.id.as_str(), "recover");
assert_eq!(m.capabilities.len(), 1);
let c = &m.capabilities[0].capability;
assert_eq!(c.id.as_str(), "recover.orphans");
// it owns the decision and the destruction: a command whose effect is on the repository
assert!(matches!(c.kind, CapabilityKind::Command));
assert_eq!(c.execution.effect, Effect::RepositoryMutation);
// and it is asked rather than typed: `majordomus recover` stays the shell command that
// asks it, so the capability has no command-line projection of its own
assert!(c.exposure.cli.is_none());
assert!(c.exposure.mcp.is_some() && c.exposure.http.is_some());Structs§
- Recover
Orphans Input - The input of
recover.orphans: how old a stray must be, and whether to act. - Recover
Orphans Result - What one sweep found and what it did about it.
- Stray
- One stray file, its verdict, and the evidence that produced it.
Enums§
- Stray
Kind - What kind of stray a path is, decided by where it is and how it is named rather than by what is in it. The content decides the verdict; this decides which question to ask.
- Stray
Verdict - What this stray is, decided by reading it.
Functions§
- front_
matter_ value - One scalar of a record’s front matter, read the way
lib/recover.shreads it: the first line that begins<key>:, anywhere in the file. - module
- The module descriptor: one capability, and it writes.