Skip to content
Merged
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
19 changes: 19 additions & 0 deletions crates/warp-cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,25 @@ echo-cli inspect state.wsc --raw
echo-cli --format json inspect state.wsc
```

### `echo-cli wal submission-posture ...`

Inspect the recovered posture of one submission without changing its WAL:

```sh
echo-cli --format json wal submission-posture /path/to/wal \
--submission-id <64-character-hex> \
--canonical-envelope-digest <64-character-hex>
```

For a matching decided submission, JSON includes `receipt_coordinate` with
`identity_digest`, `worldline_id`, `worldline_tick_after`, `commit_global_tick`,
`commit_hash`, `submission_id`, `ticket_digest`, and `receipt_content_digest`.
The existing `receipt_ref` remains the hex-encoded canonical receipt bytes.
The structured coordinate is `null` when there is no matching receipt,
including pending, unknown, and conflicting-envelope submissions. A rejected
or obstructed decision can still have a receipt; a coordinate does not imply
that the submission applied. Text output is unchanged.

### `echo-cli wsc causal-history ...`

Export, inspect, and verify WAL causal-history WSC bundles without importing
Expand Down
33 changes: 33 additions & 0 deletions crates/warp-cli/src/wal.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ use warp_core::causal_wal::{
recover_submission_index, RecoveredSubmissionPosture, RecoveryAccessMode, RecoveryTailPosture,
SubmissionRetryPosture,
};
use warp_core::CausalTickReceiptRef;

use crate::cli::OutputFormat;
use crate::output::{emit, hex_hash};
Expand All @@ -33,10 +34,39 @@ pub(crate) struct WalSubmissionPostureOutput {
pub(crate) retry_posture: &'static str,
pub(crate) recovered_posture: Option<&'static str>,
pub(crate) receipt_ref: Option<String>,
pub(crate) receipt_coordinate: Option<WalReceiptCoordinateOutput>,
pub(crate) receipt_digest: Option<String>,
pub(crate) ticket_digest: Option<String>,
}

/// Named fields of the canonical receipt coordinate retained in the WAL.
#[derive(Debug, Serialize)]
pub(crate) struct WalReceiptCoordinateOutput {
identity_digest: String,
worldline_id: String,
worldline_tick_after: u64,
commit_global_tick: u64,
commit_hash: String,
submission_id: String,
ticket_digest: String,
receipt_content_digest: String,
}

impl From<CausalTickReceiptRef> for WalReceiptCoordinateOutput {
fn from(receipt: CausalTickReceiptRef) -> Self {
Self {
identity_digest: hex_hash(&receipt.identity_digest()),
worldline_id: hex_hash(receipt.worldline_id.as_bytes()),
worldline_tick_after: receipt.worldline_tick_after.as_u64(),
commit_global_tick: receipt.commit_global_tick.as_u64(),
commit_hash: hex_hash(&receipt.commit_hash),
submission_id: hex_hash(&receipt.submission_id),
ticket_digest: hex_hash(&receipt.ticket_digest),
receipt_content_digest: hex_hash(&receipt.receipt_content_digest),
}
}
}

/// Runs `echo-cli wal doctor`.
pub(crate) fn doctor(root: &Path, format: &OutputFormat) -> Result<()> {
let report = doctor_filesystem_store(root)?;
Expand Down Expand Up @@ -88,6 +118,9 @@ pub(crate) fn submission_posture(
.receipt_ref
.map(|receipt_ref| hex::encode(receipt_ref.to_canonical_bytes()))
}),
receipt_coordinate: matching_entry
.and_then(|entry| entry.receipt_ref)
.map(WalReceiptCoordinateOutput::from),
receipt_digest: matching_entry.and_then(|entry| {
entry
.receipt_ref
Expand Down
36 changes: 36 additions & 0 deletions crates/warp-cli/tests/cli_integration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -586,6 +586,10 @@ fn wal_submission_posture_runtime_ack_root_reports_accepted_pending_json() -> Te
assert_eq!(json["retry_posture"], "AlreadyAcceptedPending");
assert_eq!(json["recovered_posture"], "AcceptedPending");
assert!(json["receipt_ref"].is_null());
assert_eq!(
json.get("receipt_coordinate"),
Some(&serde_json::Value::Null)
);
assert!(json["receipt_digest"].is_null());
assert!(json["ticket_digest"].is_null());
Ok(())
Expand All @@ -602,6 +606,22 @@ fn wal_submission_posture_runtime_ack_root_reports_decided_applied_json() -> Tes

assert_eq!(json["retry_posture"], "AlreadyDecidedApplied");
assert_eq!(json["recovered_posture"], "DecidedApplied");
let receipt = fixture
.receipt_ref
.ok_or("missing applied receipt reference")?;
assert_eq!(
json["receipt_coordinate"],
serde_json::json!({
"identity_digest": hex::encode(receipt.identity_digest()),
"worldline_id": hex::encode(receipt.worldline_id.as_bytes()),
"worldline_tick_after": receipt.worldline_tick_after.as_u64(),
"commit_global_tick": receipt.commit_global_tick.as_u64(),
"commit_hash": hex::encode(receipt.commit_hash),
"submission_id": hex::encode(receipt.submission_id),
"ticket_digest": hex::encode(receipt.ticket_digest),
"receipt_content_digest": hex::encode(receipt.receipt_content_digest),
})
);
assert_eq!(
json["receipt_ref"],
hex::encode(
Expand Down Expand Up @@ -637,6 +657,10 @@ fn wal_submission_posture_json_reports_obstructed_recovered_status() -> TestResu

assert_eq!(json["retry_posture"], "AlreadyObstructed");
assert_eq!(json["recovered_posture"], "Obstructed");
assert_eq!(
json["receipt_coordinate"]["submission_id"],
hex::encode(digest("submission:obstructed"))
);
assert_eq!(
json["receipt_digest"],
hex::encode(digest("receipt:obstructed"))
Expand All @@ -662,6 +686,10 @@ fn wal_submission_posture_json_reports_decided_rejected_status() -> TestResult {

assert_eq!(json["retry_posture"], "AlreadyDecidedRejected");
assert_eq!(json["recovered_posture"], "DecidedRejected");
assert_eq!(
json["receipt_coordinate"]["submission_id"],
hex::encode(digest("submission:rejected"))
);
assert_eq!(
json["receipt_digest"],
hex::encode(digest("receipt:rejected"))
Expand Down Expand Up @@ -883,6 +911,10 @@ fn wal_submission_posture_json_reports_not_accepted_without_app_nouns() -> TestR
let json: serde_json::Value = serde_json::from_slice(&assert.get_output().stdout)?;

assert_eq!(json["retry_posture"], "NotAccepted");
assert_eq!(
json.get("receipt_coordinate"),
Some(&serde_json::Value::Null)
);
assert!(json["recovered_posture"].is_null());
assert!(json["receipt_digest"].is_null());
assert!(json["ticket_digest"].is_null());
Expand All @@ -909,6 +941,10 @@ fn wal_submission_posture_json_suppresses_recovered_fields_for_envelope_conflict
let json: serde_json::Value = serde_json::from_slice(&assert.get_output().stdout)?;

assert_eq!(json["retry_posture"], "ConflictSameIdDifferentEnvelope");
assert_eq!(
json.get("receipt_coordinate"),
Some(&serde_json::Value::Null)
);
assert!(json["recovered_posture"].is_null());
assert!(json["receipt_digest"].is_null());
assert!(json["ticket_digest"].is_null());
Expand Down
15 changes: 15 additions & 0 deletions docs/topics/WAL.md
Original file line number Diff line number Diff line change
Expand Up @@ -317,6 +317,21 @@ Action parent-state validation indexes verification obligations by exact worldli

Each validation call retains one replay cursor per needed worldline and one transient Tick simulation. Replay-patch applications stay within two history sweeps. This bounds repeated prefix work and private snapshot retention; it does not claim linear elapsed time. State hashing and Tick simulation still depend on graph size. Test-only counters measure applied patches and logical retained data without becoming causal authority.

## Inspecting a receipt coordinate

`echo-cli --format json wal submission-posture` projects a matching recovered
receipt into named `receipt_coordinate` fields: its identity digest,
worldline, worldline and global ticks, commit, submission, ticket, and receipt
content digest. The existing `receipt_ref` remains the hex-encoded canonical
coordinate. Both come from the recovered receipt; neither grants admission or
changes history. A coordinate can name a rejected or obstructed decision.
Without a matching receipt, including an envelope conflict, the structured
coordinate is `null`.

The CLI integration tests compare all named fields with a real host-produced
receipt after filesystem recovery and check pending, unknown, conflicting,
rejected, and obstructed postures.

## Recovery Postures

The useful postures are:
Expand Down
Loading