diff --git a/CHANGELOG.md b/CHANGELOG.md index 83a753c1..571990e8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,8 @@ ### Fixed +- The generic operation runner names typed obstruction kinds, footprint conflicts, and missing outcomes in bounded summaries. It omits raw outcome records and invocation data on both Action error paths. + - `xtask run-edict-operation` runs outside Git and resolves relative artifact and WAL paths from the caller directory. Repository maintenance commands retain their Git-root behavior. - `WorldlineState::state_root` documentation now states its reachable-state boundary. Detached create-if-absent writes remain bound by patch and commit identities. Hash bytes are unchanged. diff --git a/docs/architecture/application-contract-hosting.md b/docs/architecture/application-contract-hosting.md index f9058ffb..f272e555 100644 --- a/docs/architecture/application-contract-hosting.md +++ b/docs/architecture/application-contract-hosting.md @@ -165,6 +165,8 @@ tests, but it is not the application lifecycle. The create-if-absent profile creates a node and its alpha attachment. It does not create a skeleton edge from the lane root. A detached cell therefore remains outside the reachable-state root hash. Equal roots do not prove equal stores or the absence of a detached write. The retained tick patch and commit identity bind that write; duplicate checks also compare the typed target-value digest. This preserves the [Merkle commit law](../spec/merkle-commit.md). +The generic operation runner distinguishes missing Action outcomes, typed obstructions, and footprint conflicts in its error messages. Both the first Action and unexpected duplicate outcomes use bounded summaries and omit invocation data. Opted-in Rust backtraces remain separate diagnostic output. This diagnostic boundary does not change retained obstruction encoding. + The external-provider schema additionally admits one exact zero-choice `compiler-produced-bounded-pure/v1` target configuration. It contains no application operation, target-specific budget override, or mutation authority. diff --git a/xtask/src/run_edict_operation.rs b/xtask/src/run_edict_operation.rs index 1044b0d9..ee483221 100644 --- a/xtask/src/run_edict_operation.rs +++ b/xtask/src/run_edict_operation.rs @@ -381,7 +381,10 @@ pub fn run(config: RunEdictOperationConfig) -> Result { .echo_operation_action_outcome_v1(&first_submission_id) { Some(EchoOperationActionOutcomeV1::Committed(receipt)) => receipt, - _ => bail!("scheduler did not publish a committed typed Action outcome"), + outcome => bail!( + "scheduler did not publish a committed typed Action outcome: {}", + action_outcome_summary(outcome) + ), }; tick_commit_id = hex::encode(committed_receipt.commit_id()); receipt_digest = hex::encode(committed_receipt.digest()); @@ -526,7 +529,10 @@ pub fn run(config: RunEdictOperationConfig) -> Result { { package.obstruction_coordinate.clone() } - outcome => bail!("duplicate Action produced unexpected outcome: {outcome:?}"), + outcome => bail!( + "duplicate Action produced unexpected outcome: {}", + action_outcome_summary(outcome) + ), }; duplicate = duplicate_report( duplicate_obstruction, @@ -958,6 +964,22 @@ fn validate_package_configuration( Ok(()) } +fn action_outcome_summary(outcome: Option<&EchoOperationActionOutcomeV1>) -> String { + match outcome { + Some(EchoOperationActionOutcomeV1::Committed(_)) => "committed".to_owned(), + Some(EchoOperationActionOutcomeV1::Obstructed(obstruction)) => { + format!("obstructed: {:?}", obstruction.kind()) + } + Some(EchoOperationActionOutcomeV1::RejectedFootprintConflict(conflict)) => { + format!( + "footprint conflict ({} blockers)", + conflict.blocked_by().len() + ) + } + None => "missing typed Action outcome".to_owned(), + } +} + fn parse_input(bytes: &[u8], configuration: &TargetConfiguration) -> Result { if configuration.node_key_field == configuration.replacement_field || configuration.node_key_field == "basis" diff --git a/xtask/tests/run_edict_operation.rs b/xtask/tests/run_edict_operation.rs index 4d6e565b..2e0c2a45 100644 --- a/xtask/tests/run_edict_operation.rs +++ b/xtask/tests/run_edict_operation.rs @@ -103,6 +103,8 @@ fn runner_command_with_closure( ) -> Command { let mut command = Command::new(env!("CARGO_BIN_EXE_xtask")); command + .env_remove("RUST_BACKTRACE") + .env_remove("RUST_LIB_BACKTRACE") .arg("run-edict-operation") .arg("--package") .arg(package) @@ -410,6 +412,40 @@ fn malformed_input_and_nonempty_wal_fail_closed() { ); } +#[test] +fn result_projection_obstruction_reports_a_bounded_kind() { + let run_dir = TempRunDir::new(); + let input_path = run_dir.path().join("wide-projection-input.json"); + let private_key = "private-input-marker".repeat(256); + fs::write( + &input_path, + serde_json::to_vec(&serde_json::json!({ + "basis": "u0", + "key": private_key, + "value": "small", + })) + .expect("wide fixture is JSON"), + ) + .expect("wide fixture is writable"); + let output = runner_command( + &fixture_path("executable-operation-package.cbor"), + &fixture_path("verification-report.cbor"), + &input_path, + &run_dir.path().join("wal"), + ) + .output() + .expect("the obstructed runner starts"); + assert_rejected(&output, "ResultProjectionInvalid"); + assert!( + output.stderr.len() <= 256, + "outcome error must stay bounded" + ); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!(!stderr.contains("private-input-marker")); + assert!(!stderr.contains("invocation_admission")); + assert!(!stderr.contains("EchoOperationObstructionV1 {")); +} + #[test] fn replacement_bound_is_enforced_before_runtime_submission() { let run_dir = TempRunDir::new();