Skip to main content

Module recover

Module recover 

Source
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§

RecoverOrphansInput
The input of recover.orphans: how old a stray must be, and whether to act.
RecoverOrphansResult
What one sweep found and what it did about it.
Stray
One stray file, its verdict, and the evidence that produced it.

Enums§

StrayKind
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.
StrayVerdict
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.sh reads it: the first line that begins <key>: , anywhere in the file.
module
The module descriptor: one capability, and it writes.