diff --git a/crates/traverse-runtime/tests/executor_tests.rs b/crates/traverse-runtime/tests/executor_tests.rs index 108411ff..bfec3d04 100644 --- a/crates/traverse-runtime/tests/executor_tests.rs +++ b/crates/traverse-runtime/tests/executor_tests.rs @@ -1127,6 +1127,126 @@ fn wasm_executor_emit_event_handles_module_without_memory_export() -> Result<(), Ok(()) } +#[test] +fn core_transition_action_status_emits_status_transitioned_on_accepted_transition() +-> Result<(), String> { + let wasm_path = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join( + "../../examples/core-transition-action-status/artifacts/core-transition-action-status.wasm", + ); + let wasm_bytes = std::fs::read(&wasm_path).map_err(|e| { + format!( + "failed to read {}: {e} (run examples/core-transition-action-status/build-fixture.sh first)", + wasm_path.display() + ) + })?; + + let executor = WasmExecutor::new().map_err(|e| format!("{e:?}"))?; + let input = json!({ + "action_item_id": "item-emit-001", + "current_status": "open", + "requested_status": "in_progress", + "actor_id": "user-ada", + "owner_id": "user-ada", + "transition_config": { + "version": "1.0", + "allowed_transitions": { + "open": ["in_progress", "cancelled", "snoozed"], + "in_progress": ["blocked", "done", "cancelled", "snoozed"], + "blocked": ["in_progress", "cancelled"], + "snoozed": ["open", "in_progress", "cancelled"], + "done": [], + "cancelled": [] + }, + "owner_only": true + } + }); + + let output = executor + .run_bytes_with_capability( + &wasm_bytes, + &input, + "core.transition-action-status", + &[EventReference { + event_id: "core.action-item.status-transitioned".to_string(), + version: "1.0.0".to_string(), + }], + ServiceType::Subscribable, + ) + .map_err(|e| format!("{e:?}"))?; + + assert_eq!(output.value["allowed"], json!(true)); + assert_eq!(output.value["reason_code"], json!("ok")); + assert_eq!(output.value["new_status"], json!("in_progress")); + assert_eq!( + output.emitted_events.len(), + 1, + "accepted transition must emit exactly one governed event" + ); + let event = &output.emitted_events[0]; + assert_eq!(event.event_type, "core.action-item.status-transitioned"); + assert_eq!(event.version, "1.0.0"); + assert_eq!(event.data["action_item_id"], json!("item-emit-001")); + assert_eq!(event.data["from_status"], json!("open")); + assert_eq!(event.data["to_status"], json!("in_progress")); + assert_eq!(event.data["actor_id"], json!("user-ada")); + Ok(()) +} + +#[test] +fn core_transition_action_status_does_not_emit_on_rejected_transition() -> Result<(), String> { + let wasm_path = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join( + "../../examples/core-transition-action-status/artifacts/core-transition-action-status.wasm", + ); + let wasm_bytes = std::fs::read(&wasm_path).map_err(|e| { + format!( + "failed to read {}: {e} (run examples/core-transition-action-status/build-fixture.sh first)", + wasm_path.display() + ) + })?; + + let executor = WasmExecutor::new().map_err(|e| format!("{e:?}"))?; + let input = json!({ + "action_item_id": "item-emit-002", + "current_status": "done", + "requested_status": "open", + "actor_id": "user-ada", + "owner_id": "user-ada", + "transition_config": { + "version": "1.0", + "allowed_transitions": { + "open": ["in_progress", "cancelled", "snoozed"], + "in_progress": ["blocked", "done", "cancelled", "snoozed"], + "blocked": ["in_progress", "cancelled"], + "snoozed": ["open", "in_progress", "cancelled"], + "done": [], + "cancelled": [] + }, + "owner_only": true + } + }); + + let output = executor + .run_bytes_with_capability( + &wasm_bytes, + &input, + "core.transition-action-status", + &[EventReference { + event_id: "core.action-item.status-transitioned".to_string(), + version: "1.0.0".to_string(), + }], + ServiceType::Subscribable, + ) + .map_err(|e| format!("{e:?}"))?; + + assert_eq!(output.value["allowed"], json!(false)); + assert_eq!(output.value["reason_code"], json!("illegal_transition")); + assert!( + output.emitted_events.is_empty(), + "rejected transition must not emit" + ); + Ok(()) +} + // --- helpers --- fn native_capability(id: &str) -> ExecutorCapability { diff --git a/examples/core-transition-action-status/artifacts/core-transition-action-status.wasm b/examples/core-transition-action-status/artifacts/core-transition-action-status.wasm index aa17d384..d96d51b7 100755 Binary files a/examples/core-transition-action-status/artifacts/core-transition-action-status.wasm and b/examples/core-transition-action-status/artifacts/core-transition-action-status.wasm differ diff --git a/examples/core-transition-action-status/contract.json b/examples/core-transition-action-status/contract.json index b733bc6a..b4da37bb 100644 --- a/examples/core-transition-action-status/contract.json +++ b/examples/core-transition-action-status/contract.json @@ -4,18 +4,19 @@ "id": "core.transition-action-status", "namespace": "core", "name": "transition-action-status", - "version": "1.2.0", + "version": "1.3.0", "lifecycle": "active", "owner": { "team": "loop", "contact": "founders@loop.dev" }, - "summary": "Deterministic state machine for action item status transitions (open \u2192 in_progress \u2192 blocked \u2192 snoozed \u2192 done / cancelled).", - "description": "Pure state-machine for action-item status transitions under a caller-supplied transition_config (allowed_transitions + owner_only).\n\nImplemented and smoke-tested matrix (use_cases):\n- open \u2192 in_progress (ok)\n- done \u2192 open illegal (illegal_transition)\n- open \u2192 snoozed (ok)\n- non-owner denied (not_owner)\n- in_progress \u2192 blocked (ok)\n- in_progress \u2192 done (ok)\n- blocked \u2192 in_progress (ok)\n- snoozed \u2192 open (ok)\n- open \u2192 cancelled (ok)\n- cancelled \u2192 open illegal (covers current_status=cancelled)\n- missing current_status \u2192 invalid_status\n\nKnown limitations (intentional in 1.2.0):\n- Transition graph is entirely caller-supplied; no built-in default policy\n- Does not persist status changes\n- Declares governed event core.action-item.status-transitioned@1.0.0 on accepted transitions; the WASM guest does not call the host emit ABI (declaration-only honesty with the published event product)", + "summary": "Deterministic state machine for action item status transitions (open → in_progress → blocked → snoozed → done / cancelled).", + "description": "Action-item status transitions under a caller-supplied transition_config (allowed_transitions + owner_only).\n\nImplemented and smoke-tested matrix (use_cases):\n- open → in_progress (ok)\n- done → open illegal (illegal_transition)\n- open → snoozed (ok)\n- non-owner denied (not_owner)\n- in_progress → blocked (ok)\n- in_progress → done (ok)\n- blocked → in_progress (ok)\n- snoozed → open (ok)\n- open → cancelled (ok)\n- cancelled → open illegal (covers current_status=cancelled)\n- missing current_status → invalid_status\n\nOn accepted transitions (reason_code=ok), the WASM guest calls traverse_host::emit_event for core.action-item.status-transitioned@1.0.0 with payload {action_item_id, from_status, to_status, actor_id}.\n\nKnown limitations (intentional in 1.3.0):\n- Transition graph is entirely caller-supplied; no built-in default policy\n- Does not persist status changes", "use_cases": [ { "scenario": "As an owner, I want to move an item from open to in_progress, so that active work is reflected accurately.", "input_example": { + "action_item_id": "item-001", "current_status": "open", "requested_status": "in_progress", "actor_id": "user-ada", @@ -54,7 +55,7 @@ "new_status": "in_progress", "reason_code": "ok", "evaluation_trace": [ - "open \u2192 in_progress is allowed", + "open → in_progress is allowed", "actor is owner" ] }, @@ -62,8 +63,9 @@ "persona_ref": "meeting-organizer" }, { - "scenario": "As a system, I want an illegal transition (done \u2192 open) rejected, so that completed work cannot silently reopen.", + "scenario": "As a system, I want an illegal transition (done → open) rejected, so that completed work cannot silently reopen.", "input_example": { + "action_item_id": "item-002", "current_status": "done", "requested_status": "open", "actor_id": "user-ada", @@ -111,6 +113,7 @@ { "scenario": "As an owner, I want to snooze an open item, so that I can pause follow-up without losing the item.", "input_example": { + "action_item_id": "item-003", "current_status": "open", "requested_status": "snoozed", "actor_id": "user-ada", @@ -149,7 +152,7 @@ "new_status": "snoozed", "reason_code": "ok", "evaluation_trace": [ - "open \u2192 snoozed is allowed", + "open → snoozed is allowed", "actor is owner" ] }, @@ -159,6 +162,7 @@ { "scenario": "As a system, I want a non-owner blocked when owner_only is true, so that only the owner can change status.", "input_example": { + "action_item_id": "item-004", "current_status": "open", "requested_status": "in_progress", "actor_id": "user-bob", @@ -204,8 +208,9 @@ "persona_ref": "runtime-engineer" }, { - "scenario": "As an owner, I want in_progress \u2192 blocked, so that blockers are an explicit status, not tribal knowledge.", + "scenario": "As an owner, I want in_progress → blocked, so that blockers are an explicit status, not tribal knowledge.", "input_example": { + "action_item_id": "item-005", "current_status": "in_progress", "requested_status": "blocked", "actor_id": "user-ada", @@ -244,15 +249,16 @@ "new_status": "blocked", "reason_code": "ok", "evaluation_trace": [ - "in_progress \u2192 blocked is allowed" + "in_progress → blocked is allowed" ] }, "happy": true, "persona_ref": "meeting-organizer" }, { - "scenario": "As an owner, I want in_progress \u2192 done, so that finished work can leave the active queue.", + "scenario": "As an owner, I want in_progress → done, so that finished work can leave the active queue.", "input_example": { + "action_item_id": "item-006", "current_status": "in_progress", "requested_status": "done", "actor_id": "user-ada", @@ -291,15 +297,16 @@ "new_status": "done", "reason_code": "ok", "evaluation_trace": [ - "in_progress \u2192 done is allowed" + "in_progress → done is allowed" ] }, "happy": true, "persona_ref": "meeting-organizer" }, { - "scenario": "As an owner, I want blocked \u2192 in_progress, so that cleared blockers resume cleanly.", + "scenario": "As an owner, I want blocked → in_progress, so that cleared blockers resume cleanly.", "input_example": { + "action_item_id": "item-007", "current_status": "blocked", "requested_status": "in_progress", "actor_id": "user-ada", @@ -338,15 +345,16 @@ "new_status": "in_progress", "reason_code": "ok", "evaluation_trace": [ - "blocked \u2192 in_progress is allowed" + "blocked → in_progress is allowed" ] }, "happy": true, "persona_ref": "meeting-organizer" }, { - "scenario": "As an owner, I want snoozed \u2192 open, so that snoozed items re-enter the follow-up pool.", + "scenario": "As an owner, I want snoozed → open, so that snoozed items re-enter the follow-up pool.", "input_example": { + "action_item_id": "item-008", "current_status": "snoozed", "requested_status": "open", "actor_id": "user-ada", @@ -385,15 +393,16 @@ "new_status": "open", "reason_code": "ok", "evaluation_trace": [ - "snoozed \u2192 open is allowed" + "snoozed → open is allowed" ] }, "happy": true, "persona_ref": "meeting-organizer" }, { - "scenario": "As an owner, I want open \u2192 cancelled, so that abandoned work can exit without fake completion.", + "scenario": "As an owner, I want open → cancelled, so that abandoned work can exit without fake completion.", "input_example": { + "action_item_id": "item-009", "current_status": "open", "requested_status": "cancelled", "actor_id": "user-ada", @@ -432,7 +441,7 @@ "new_status": "cancelled", "reason_code": "ok", "evaluation_trace": [ - "open \u2192 cancelled is allowed" + "open → cancelled is allowed" ] }, "happy": true, @@ -441,6 +450,7 @@ { "scenario": "As a system, I want cancelled items to reject further transitions, so that cancelled work stays terminal.", "input_example": { + "action_item_id": "item-010", "current_status": "cancelled", "requested_status": "open", "actor_id": "user-ada", @@ -488,6 +498,7 @@ { "scenario": "As a system, I want missing current_status rejected with invalid_status, so that incomplete transition requests fail closed.", "input_example": { + "action_item_id": "item-011", "current_status": "", "requested_status": "open", "actor_id": "user-ada", @@ -537,12 +548,16 @@ "schema": { "type": "object", "required": [ + "action_item_id", "current_status", "requested_status", "actor_id", "transition_config" ], "properties": { + "action_item_id": { + "type": "string" + }, "current_status": { "type": "string", "enum": [ @@ -647,7 +662,7 @@ }, { "kind": "event_emission", - "description": "Declares domain fact core.action-item.status-transitioned when a transition is accepted (ok)" + "description": "Emits core.action-item.status-transitioned@1.0.0 via traverse_host::emit_event when a transition is accepted (ok)" } ], "emits": [ @@ -689,7 +704,7 @@ "source": "ai-assisted", "author": "loop-founders + traverse-capability-author", "created_at": "2026-08-08T06:00:00Z", - "spec_ref": "core.transition-action-status@1.2.0", + "spec_ref": "core.transition-action-status@1.3.0", "adr_refs": [ "persona-council-review-2026-08-08" ], @@ -697,17 +712,18 @@ }, "evidence": [ { - "evidence_id": "core-transition-action-status-1.2.0-contract-validation", + "evidence_id": "core-transition-action-status-1.3.0-contract-validation", "type": "contract_validation", "status": "passed" } ], - "service_type": "stateless", + "service_type": "subscribable", "permitted_targets": [ "local", "edge", "browser", "cloud" ], - "artifact_type": "native" + "artifact_type": "native", + "event_trigger": "core.transition-action-status.triggered" } diff --git a/examples/core-transition-action-status/manifest.json b/examples/core-transition-action-status/manifest.json index 5d9ad058..72518023 100644 --- a/examples/core-transition-action-status/manifest.json +++ b/examples/core-transition-action-status/manifest.json @@ -2,17 +2,17 @@ "kind": "capability_package", "schema_version": "1.0.0", "package_id": "core.transition-action-status-agent", - "version": "1.2.0", - "summary": "Capability package for core.transition-action-status@1.2.0.", + "version": "1.3.0", + "summary": "Capability package for core.transition-action-status@1.3.0.", "capability_ref": { "id": "core.transition-action-status", - "version": "1.2.0", + "version": "1.3.0", "contract_path": "./contract.json" }, "workflow_refs": [ { "workflow_id": "core.transition-action-status", - "workflow_version": "1.2.0" + "workflow_version": "1.3.0" } ], "source": { @@ -23,7 +23,7 @@ "binary": { "path": "./artifacts/core-transition-action-status.wasm", "format": "wasm", - "expected_digest": "fnv1a64:f392f46763fdd2af", + "expected_digest": "fnv1a64:3ac908d226fb7954", "abi_version": "1.0.0" }, "constraints": { diff --git a/examples/core-transition-action-status/runtime-requests/uc01-open-to-in-progress.json b/examples/core-transition-action-status/runtime-requests/uc01-open-to-in-progress.json index ef6812fc..0ff3a148 100644 --- a/examples/core-transition-action-status/runtime-requests/uc01-open-to-in-progress.json +++ b/examples/core-transition-action-status/runtime-requests/uc01-open-to-in-progress.json @@ -4,9 +4,10 @@ "request_id": "core-transition-action-status-01", "intent": { "capability_id": "core.transition-action-status", - "capability_version": "1.2.0" + "capability_version": "1.3.0" }, "input": { + "action_item_id": "item-01", "current_status": "open", "requested_status": "in_progress", "actor_id": "user-ada", diff --git a/examples/core-transition-action-status/runtime-requests/uc02-done-to-open-illegal.json b/examples/core-transition-action-status/runtime-requests/uc02-done-to-open-illegal.json index d703b7e7..9a40fdeb 100644 --- a/examples/core-transition-action-status/runtime-requests/uc02-done-to-open-illegal.json +++ b/examples/core-transition-action-status/runtime-requests/uc02-done-to-open-illegal.json @@ -4,9 +4,10 @@ "request_id": "core-transition-action-status-02", "intent": { "capability_id": "core.transition-action-status", - "capability_version": "1.2.0" + "capability_version": "1.3.0" }, "input": { + "action_item_id": "item-02", "current_status": "done", "requested_status": "open", "actor_id": "user-ada", diff --git a/examples/core-transition-action-status/runtime-requests/uc03-open-to-snoozed.json b/examples/core-transition-action-status/runtime-requests/uc03-open-to-snoozed.json index d3f8e9d2..75f0b4d1 100644 --- a/examples/core-transition-action-status/runtime-requests/uc03-open-to-snoozed.json +++ b/examples/core-transition-action-status/runtime-requests/uc03-open-to-snoozed.json @@ -4,9 +4,10 @@ "request_id": "core-transition-action-status-03", "intent": { "capability_id": "core.transition-action-status", - "capability_version": "1.2.0" + "capability_version": "1.3.0" }, "input": { + "action_item_id": "item-03", "current_status": "open", "requested_status": "snoozed", "actor_id": "user-ada", diff --git a/examples/core-transition-action-status/runtime-requests/uc04-non-owner-denied.json b/examples/core-transition-action-status/runtime-requests/uc04-non-owner-denied.json index ffeb6ff6..8885f545 100644 --- a/examples/core-transition-action-status/runtime-requests/uc04-non-owner-denied.json +++ b/examples/core-transition-action-status/runtime-requests/uc04-non-owner-denied.json @@ -4,9 +4,10 @@ "request_id": "core-transition-action-status-04", "intent": { "capability_id": "core.transition-action-status", - "capability_version": "1.2.0" + "capability_version": "1.3.0" }, "input": { + "action_item_id": "item-04", "current_status": "open", "requested_status": "in_progress", "actor_id": "user-bob", diff --git a/examples/core-transition-action-status/runtime-requests/uc05-in-progress-to-blocked.json b/examples/core-transition-action-status/runtime-requests/uc05-in-progress-to-blocked.json index ba0b816f..262bd628 100644 --- a/examples/core-transition-action-status/runtime-requests/uc05-in-progress-to-blocked.json +++ b/examples/core-transition-action-status/runtime-requests/uc05-in-progress-to-blocked.json @@ -4,9 +4,10 @@ "request_id": "core-transition-action-status-05", "intent": { "capability_id": "core.transition-action-status", - "capability_version": "1.2.0" + "capability_version": "1.3.0" }, "input": { + "action_item_id": "item-05", "current_status": "in_progress", "requested_status": "blocked", "actor_id": "user-ada", diff --git a/examples/core-transition-action-status/runtime-requests/uc06-in-progress-to-done.json b/examples/core-transition-action-status/runtime-requests/uc06-in-progress-to-done.json index 817c69c7..9d9dfdb0 100644 --- a/examples/core-transition-action-status/runtime-requests/uc06-in-progress-to-done.json +++ b/examples/core-transition-action-status/runtime-requests/uc06-in-progress-to-done.json @@ -4,9 +4,10 @@ "request_id": "core-transition-action-status-06", "intent": { "capability_id": "core.transition-action-status", - "capability_version": "1.2.0" + "capability_version": "1.3.0" }, "input": { + "action_item_id": "item-06", "current_status": "in_progress", "requested_status": "done", "actor_id": "user-ada", diff --git a/examples/core-transition-action-status/runtime-requests/uc07-blocked-to-in-progress.json b/examples/core-transition-action-status/runtime-requests/uc07-blocked-to-in-progress.json index e7c5c5b0..3aca8105 100644 --- a/examples/core-transition-action-status/runtime-requests/uc07-blocked-to-in-progress.json +++ b/examples/core-transition-action-status/runtime-requests/uc07-blocked-to-in-progress.json @@ -4,9 +4,10 @@ "request_id": "core-transition-action-status-07", "intent": { "capability_id": "core.transition-action-status", - "capability_version": "1.2.0" + "capability_version": "1.3.0" }, "input": { + "action_item_id": "item-07", "current_status": "blocked", "requested_status": "in_progress", "actor_id": "user-ada", diff --git a/examples/core-transition-action-status/runtime-requests/uc08-snoozed-to-open.json b/examples/core-transition-action-status/runtime-requests/uc08-snoozed-to-open.json index bf4493de..e64435e5 100644 --- a/examples/core-transition-action-status/runtime-requests/uc08-snoozed-to-open.json +++ b/examples/core-transition-action-status/runtime-requests/uc08-snoozed-to-open.json @@ -4,9 +4,10 @@ "request_id": "core-transition-action-status-08", "intent": { "capability_id": "core.transition-action-status", - "capability_version": "1.2.0" + "capability_version": "1.3.0" }, "input": { + "action_item_id": "item-08", "current_status": "snoozed", "requested_status": "open", "actor_id": "user-ada", diff --git a/examples/core-transition-action-status/runtime-requests/uc09-open-to-cancelled.json b/examples/core-transition-action-status/runtime-requests/uc09-open-to-cancelled.json index cea9a8ae..efd8fbb7 100644 --- a/examples/core-transition-action-status/runtime-requests/uc09-open-to-cancelled.json +++ b/examples/core-transition-action-status/runtime-requests/uc09-open-to-cancelled.json @@ -4,9 +4,10 @@ "request_id": "core-transition-action-status-09", "intent": { "capability_id": "core.transition-action-status", - "capability_version": "1.2.0" + "capability_version": "1.3.0" }, "input": { + "action_item_id": "item-09", "current_status": "open", "requested_status": "cancelled", "actor_id": "user-ada", diff --git a/examples/core-transition-action-status/runtime-requests/uc10-cancelled-to-open-illegal.json b/examples/core-transition-action-status/runtime-requests/uc10-cancelled-to-open-illegal.json index 07bbcc90..03893420 100644 --- a/examples/core-transition-action-status/runtime-requests/uc10-cancelled-to-open-illegal.json +++ b/examples/core-transition-action-status/runtime-requests/uc10-cancelled-to-open-illegal.json @@ -4,9 +4,10 @@ "request_id": "core-transition-action-status-10", "intent": { "capability_id": "core.transition-action-status", - "capability_version": "1.2.0" + "capability_version": "1.3.0" }, "input": { + "action_item_id": "item-10", "current_status": "cancelled", "requested_status": "open", "actor_id": "user-ada", diff --git a/examples/core-transition-action-status/runtime-requests/uc11-invalid-status.json b/examples/core-transition-action-status/runtime-requests/uc11-invalid-status.json index 16755745..e84604ee 100644 --- a/examples/core-transition-action-status/runtime-requests/uc11-invalid-status.json +++ b/examples/core-transition-action-status/runtime-requests/uc11-invalid-status.json @@ -4,9 +4,10 @@ "request_id": "core-transition-action-status-11", "intent": { "capability_id": "core.transition-action-status", - "capability_version": "1.2.0" + "capability_version": "1.3.0" }, "input": { + "action_item_id": "item-11", "current_status": "", "requested_status": "open", "actor_id": "user-ada", diff --git a/examples/core-transition-action-status/src/agent.rs b/examples/core-transition-action-status/src/agent.rs index ff51d5ca..b05a1e51 100644 --- a/examples/core-transition-action-status/src/agent.rs +++ b/examples/core-transition-action-status/src/agent.rs @@ -1,8 +1,9 @@ -//! core.transition-action-status — pure action-item status state machine. +//! core.transition-action-status — action-item status state machine with governed emit. //! //! Evaluates whether `current_status → requested_status` is allowed under a //! caller-supplied `transition_config`, optionally requiring the actor to be -//! the owner. Covers open/in_progress/blocked/snoozed/done/cancelled. +//! the owner. On accepted transitions, emits +//! `core.action-item.status-transitioned@1.0.0` via `traverse_host::emit_event`. #![no_std] #![no_main] @@ -23,8 +24,14 @@ unsafe extern "C" { fn fd_write(fd: u32, vectors: *const IoVec, count: usize, written: *mut usize) -> u32; } +#[link(wasm_import_module = "traverse_host")] +unsafe extern "C" { + fn emit_event(ptr: i32, len: i32) -> i32; +} + static mut INPUT_BUF: [u8; 8192] = [0; 8192]; static mut OUTPUT_BUF: [u8; 4096] = [0; 4096]; +static mut EVENT_BUF: [u8; 1024] = [0; 1024]; #[unsafe(no_mangle)] pub extern "C" fn _start() { @@ -55,13 +62,19 @@ pub extern "C" fn _start() { } unsafe fn evaluate(input: &[u8], out: &mut [u8]) -> usize { + let action_item_id = extract_string_at_depth(input, b"\"action_item_id\"", 1); let current = extract_string_at_depth(input, b"\"current_status\"", 1); let requested = extract_string_at_depth(input, b"\"requested_status\"", 1); let actor = extract_string_at_depth(input, b"\"actor_id\"", 1); let owner = extract_string_at_depth(input, b"\"owner_id\"", 1); let config = object_after_key(input, b"\"transition_config\"").unwrap_or(b""); - if current.is_empty() || requested.is_empty() || actor.is_empty() || config.is_empty() { + if action_item_id.is_empty() + || current.is_empty() + || requested.is_empty() + || actor.is_empty() + || config.is_empty() + { return write_result( out, false, @@ -143,9 +156,37 @@ unsafe fn evaluate(input: &[u8], out: &mut [u8]) -> usize { t = copy(&mut trace, t, b", \"actor is owner\""); } t = copy(&mut trace, t, b"]"); + + emit_status_transitioned(action_item_id, current, requested, actor); + write_result(out, true, requested, b"ok", &trace[..t]) } +/// Build and emit the governed status-transitioned event. Best-effort: a +/// non-zero host status does not change the capability's own evaluation result. +unsafe fn emit_status_transitioned( + action_item_id: &[u8], + from_status: &[u8], + to_status: &[u8], + actor_id: &[u8], +) { + let buf = &mut EVENT_BUF; + let mut i = 0usize; + i = copy(buf, i, br#"{"event_id":"core.action-item.status-transitioned","version":"1.0.0","payload":{"action_item_id":""#); + i = copy(buf, i, action_item_id); + i = copy(buf, i, br#"","from_status":""#); + i = copy(buf, i, from_status); + i = copy(buf, i, br#"","to_status":""#); + i = copy(buf, i, to_status); + i = copy(buf, i, br#"","actor_id":""#); + i = copy(buf, i, actor_id); + i = copy(buf, i, br#""}}"#); + if i == 0 || i > buf.len() { + return; + } + let _ = emit_event(buf.as_ptr() as i32, i as i32); +} + fn is_known_status(s: &[u8]) -> bool { matches!( s, diff --git a/examples/core-transition-action-status/workflows/transition-action-status/workflow.json b/examples/core-transition-action-status/workflows/transition-action-status/workflow.json index d42b14d2..ea901226 100644 --- a/examples/core-transition-action-status/workflows/transition-action-status/workflow.json +++ b/examples/core-transition-action-status/workflows/transition-action-status/workflow.json @@ -3,7 +3,7 @@ "schema_version": "1.0.0", "id": "core.transition-action-status", "name": "transition-action-status", - "version": "1.2.0", + "version": "1.3.0", "lifecycle": "active", "owner": { "team": "loop", @@ -14,6 +14,7 @@ "schema": { "type": "object", "required": [ + "action_item_id", "current_status", "requested_status", "actor_id", @@ -37,9 +38,10 @@ { "node_id": "transition", "capability_id": "core.transition-action-status", - "capability_version": "1.2.0", + "capability_version": "1.3.0", "input": { "from_workflow_input": [ + "action_item_id", "current_status", "requested_status", "actor_id", diff --git a/scripts/ci/core_transition_action_status_example_smoke.sh b/scripts/ci/core_transition_action_status_example_smoke.sh index 01b807fb..9e97b98a 100755 --- a/scripts/ci/core_transition_action_status_example_smoke.sh +++ b/scripts/ci/core_transition_action_status_example_smoke.sh @@ -1,5 +1,5 @@ #!/usr/bin/env bash -# End-to-end smoke for examples/core-transition-action-status (core.transition-action-status@1.2.0). +# End-to-end smoke for examples/core-transition-action-status (core.transition-action-status@1.3.0). set -euo pipefail @@ -28,7 +28,8 @@ echo "==> capability inspect" contract_out="$("${cli[@]}" capability inspect "$pkg/contract.json")" printf '%s\n' "$contract_out" require_match "$contract_out" "id: core.transition-action-status" "contract inspect id" -require_match "$contract_out" "version: 1.2.0" "contract inspect version" +require_match "$contract_out" "version: 1.3.0" "contract inspect version" +require_match "$contract_out" "service_type: subscribable" "contract inspect service_type" echo "==> wasm abi verify" abi_out="$("${cli[@]}" wasm abi verify "$pkg/artifacts/core-transition-action-status.wasm")" @@ -39,7 +40,7 @@ echo "==> capability-package inspect" pkg_out="$("${cli[@]}" capability-package inspect "$pkg/manifest.json")" printf '%s\n' "$pkg_out" require_match "$pkg_out" "package_id: core.transition-action-status-agent" "package_id" -require_match "$pkg_out" "capability_version: 1.2.0" "capability_version" +require_match "$pkg_out" "capability_version: 1.3.0" "capability_version" assert_execute() { local request="$1" @@ -53,7 +54,7 @@ assert_execute() { out="$("${cli[@]}" capability-package execute "$pkg/manifest.json" "$request")" printf '%s\n' "$out" require_match "$out" "status: completed" "$label status" - require_match "$out" "capability_version: 1.2.0" "$label capability_version" + require_match "$out" "capability_version: 1.3.0" "$label capability_version" require_match "$out" "\"allowed\": $allowed" "$label allowed" require_match "$out" "\"reason_code\": \"$code\"" "$label reason_code" if [[ -n "$extra" ]]; then @@ -73,4 +74,7 @@ assert_execute "$pkg/runtime-requests/uc09-open-to-cancelled.json" "true" "ok" " assert_execute "$pkg/runtime-requests/uc10-cancelled-to-open-illegal.json" "false" "illegal_transition" "UC-10" '"new_status": "cancelled"' assert_execute "$pkg/runtime-requests/uc11-invalid-status.json" "false" "invalid_status" "UC-11" -echo "OK: core.transition-action-status 1.2.0 E2E smoke passed" +echo "==> real emit_event via WasmExecutor" +cargo test -q -p traverse-runtime --test executor_tests core_transition_action_status_ + +echo "OK: core.transition-action-status 1.3.0 E2E smoke passed"