From 0c326e6e5957e174f7f7b9b40ff219f5c1dc1146 Mon Sep 17 00:00:00 2001 From: James Ross Date: Sun, 18 Oct 2026 14:30:49 -0700 Subject: [PATCH] feat(cli): expose named WAL receipt coordinates --- crates/warp-cli/README.md | 19 +++++++++++++ crates/warp-cli/src/wal.rs | 33 ++++++++++++++++++++++ crates/warp-cli/tests/cli_integration.rs | 36 ++++++++++++++++++++++++ docs/topics/WAL.md | 15 ++++++++++ 4 files changed, 103 insertions(+) diff --git a/crates/warp-cli/README.md b/crates/warp-cli/README.md index 599d4b436..0aa77cda8 100644 --- a/crates/warp-cli/README.md +++ b/crates/warp-cli/README.md @@ -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 diff --git a/crates/warp-cli/src/wal.rs b/crates/warp-cli/src/wal.rs index d734c1dbe..fa9ee726a 100644 --- a/crates/warp-cli/src/wal.rs +++ b/crates/warp-cli/src/wal.rs @@ -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}; @@ -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, + pub(crate) receipt_coordinate: Option, pub(crate) receipt_digest: Option, pub(crate) ticket_digest: Option, } +/// 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 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)?; @@ -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 diff --git a/crates/warp-cli/tests/cli_integration.rs b/crates/warp-cli/tests/cli_integration.rs index 428c3425c..aad7aef49 100644 --- a/crates/warp-cli/tests/cli_integration.rs +++ b/crates/warp-cli/tests/cli_integration.rs @@ -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(()) @@ -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( @@ -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")) @@ -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")) @@ -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()); @@ -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()); diff --git a/docs/topics/WAL.md b/docs/topics/WAL.md index e88d568af..b90c36ce9 100644 --- a/docs/topics/WAL.md +++ b/docs/topics/WAL.md @@ -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: