Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,6 @@
/npm/rift-darwin-arm64/bin/
/npm/rift-windows-x64/bin/
/npm/rift-snapshot/prebuilds/

# Synthetic Python E2E bytecode
__pycache__/
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# Changelog

## Unreleased

- Add exact canonical-path reconciliation and explicit root/leaf trash with full review guards.
- Add durable lifecycle journals, crash recovery, verified finalization, and exact active/history rollback.
- Add shared candidate native ownership locking, journal fences, and synthetic CLI E2E receipts.
- Preserve release 0.0.12 schema, old trash history, row IDs, parents, timestamps, and rowids without migration.
73 changes: 73 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions PROGRESS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Progress

Native guarded lifecycle code and synthetic CLI E2E coverage are implemented. The existing APFS workspace checks pass outside the test sandbox. Final validation binds separate production and fixture binaries to the same clean local commit. No upstream publication or real cleanup is part of this change. See [the contract and validation procedure](docs/guarded-lifecycle.md).
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -216,3 +216,10 @@ Results include per-sample timings plus median, minimum, and maximum; `compare`
## License

MIT

### Guarded lifecycle

For exact reviewed canonical-path reconciliation, root/leaf trash, crash recovery,
finalization, and rollback, use the native `guarded` commands with an explicit
registry. See [the guarded lifecycle contract](docs/guarded-lifecycle.md), including
its ownership gate and synthetic verification procedure.
5 changes: 5 additions & 0 deletions ROADMAP.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Roadmap

- Complete native exact reconciliation, root/leaf trash, finalization, recovery, and rollback with synthetic CLI verification.
- Review the local patch and production build before any real dispatch.
- Validate the guarded runtime on Linux before Linux use.
3 changes: 3 additions & 0 deletions VISION.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Vision

Make exact reviewed Rift lifecycle operations native, recoverable, and safe to inspect. Preserve source files, markers, IDs, ancestry, and unrelated registry history. See [the lifecycle contract](docs/guarded-lifecycle.md).
4 changes: 4 additions & 0 deletions crates/cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,11 @@ license.workspace = true
name = "rift"
path = "src/main.rs"

[features]
fixture-faults = ["rift/fixture-faults"]

[dependencies]
serde_json.workspace = true
clap.workspace = true
rift = { path = "../core" }
thiserror.workspace = true
Expand Down
85 changes: 85 additions & 0 deletions crates/cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,11 @@ impl Shell {

#[derive(Subcommand)]
enum Command {
/// Exact reviewed native lifecycle; requires an explicit database.
Guarded {
#[command(subcommand)]
action: GuardedCommand,
},
#[command(hide = true)]
Rpc,
ShellInit {
Expand Down Expand Up @@ -124,6 +129,84 @@ enum Command {
Gc,
}

#[derive(Subcommand)]
enum GuardedCommand {
/// Report the exact build commit and compatible base release.
Provenance,
/// Read a request JSON file and emit an immutable dry review plan.
Preflight { request: PathBuf },
/// Apply the complete reviewed plan JSON file.
Apply { plan: PathBuf },
/// Resolve an interrupted journal without automatic retirement.
Recover,
/// Restore the exact journal; requires its reviewed SHA-256 hash.
Rollback {
#[arg(long)]
journal_hash: String,
#[arg(long)]
history: Option<PathBuf>,
},
/// Verify committed state and archive its complete rollback history.
Finalize {
#[arg(long)]
journal_hash: String,
},
}
fn guarded(database: Option<PathBuf>, action: GuardedCommand) -> Result<()> {
if matches!(action, GuardedCommand::Provenance) {
println!(
"{}",
serde_json::to_string_pretty(&rift::guarded::provenance())
.map_err(|e| rift::Error::Guarded(e.to_string()))?
);
return Ok(());
}
let database = database.ok_or_else(|| rift::Error::Guarded("--database is required".into()))?;
let json_error = |e: serde_json::Error| rift::Error::Guarded(e.to_string());
let mut hook_failed = false;
let output = match action {
GuardedCommand::Provenance => unreachable!(),
GuardedCommand::Preflight { request } => {
let request = serde_json::from_slice(&std::fs::read(request)?).map_err(json_error)?;
serde_json::to_string_pretty(&rift::guarded::preflight(database, request)?)
.map_err(json_error)?
}
GuardedCommand::Apply { plan } => {
let plan: rift::guarded::Plan =
serde_json::from_slice(&std::fs::read(plan)?).map_err(json_error)?;
if std::fs::canonicalize(database)? != plan.database {
return Err(rift::Error::Guarded("CLI database differs from plan".into()).into());
}
let outcome = rift::guarded::apply(&plan)?;
hook_failed = outcome.hook_error.is_some();
serde_json::to_string_pretty(&outcome).map_err(json_error)?
}
GuardedCommand::Recover => {
serde_json::to_string_pretty(&rift::guarded::recover(database)?).map_err(json_error)?
}
GuardedCommand::Finalize { journal_hash } => {
serde_json::to_string_pretty(&rift::guarded::finalize(database, &journal_hash)?)
.map_err(json_error)?
}
GuardedCommand::Rollback {
journal_hash,
history,
} => serde_json::to_string_pretty(&match history {
Some(history) => rift::guarded::rollback_history(database, history, &journal_hash)?,
None => rift::guarded::rollback(database, &journal_hash)?,
})
.map_err(json_error)?,
};
println!("{output}");
if hook_failed {
return Err(rift::Error::Guarded(
"retirement committed; postremove failed; journal retained".into(),
)
.into());
}
Ok(())
}

fn main() {
if let Err(error) = run() {
let message = match &error {
Expand Down Expand Up @@ -153,6 +236,7 @@ fn error_message(error: &rift::Error) -> String {
fn run() -> Result<()> {
let cli = Cli::parse();
let command = match cli.command {
Command::Guarded { action } => return guarded(cli.database, action),
Command::Rpc => {
const LIMIT: u64 = 1024 * 1024;
let mut input = Vec::new();
Expand All @@ -179,6 +263,7 @@ fn run() -> Result<()> {
None => Manager::open_default()?,
};
match command {
Command::Guarded { .. } => unreachable!(),
Command::Rpc => unreachable!(),
Command::ShellInit { shell } => {
print_shell_init(shell);
Expand Down
6 changes: 6 additions & 0 deletions crates/core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,13 @@ version.workspace = true
edition.workspace = true
license.workspace = true

[features]
# Explicit fixture-only crash/failure injection. Never enable in release packaging.
fixture-faults = []

[dependencies]
sha2 = "0.10"
tempfile.workspace = true
dirs.workspace = true
libc.workspace = true
rand.workspace = true
Expand Down
21 changes: 21 additions & 0 deletions crates/core/build.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
use std::process::Command;

fn main() {
println!("cargo:rerun-if-changed=../../.git/HEAD");
println!("cargo:rerun-if-changed=../../.git/refs");
println!("cargo:rerun-if-changed=../../.git/index");
let read = |args: &[&str]| {
Command::new("git")
.args(args)
.output()
.ok()
.filter(|output| output.status.success())
.and_then(|output| String::from_utf8(output.stdout).ok())
.map(|value| value.trim().to_owned())
};
let commit = read(&["rev-parse", "HEAD"]).unwrap_or_else(|| "unavailable".into());
let dirty = read(&["status", "--porcelain", "--untracked-files=normal"])
.is_none_or(|status| !status.is_empty());
println!("cargo:rustc-env=RIFT_SOURCE_COMMIT={commit}");
println!("cargo:rustc-env=RIFT_SOURCE_DIRTY={dirty}");
}
Loading