From 5b71d08b22887fdc9e2e97873638a5e96da2d26e Mon Sep 17 00:00:00 2001 From: Enrico Piovesan Date: Mon, 10 Aug 2026 20:54:05 -0600 Subject: [PATCH] Make core.transition-action-status really emit status-transitioned. Bump to 1.3.0 as Subscribable with required action_item_id, call traverse_host::emit_event on accepted transitions, and cover the path with executor + smoke validation. Co-authored-by: Cursor --- .../traverse-runtime/tests/executor_tests.rs | 120 ++++++++++++++++++ .../core-transition-action-status.wasm | Bin 11298 -> 12698 bytes .../contract.json | 58 ++++++--- .../manifest.json | 10 +- .../uc01-open-to-in-progress.json | 3 +- .../uc02-done-to-open-illegal.json | 3 +- .../uc03-open-to-snoozed.json | 3 +- .../uc04-non-owner-denied.json | 3 +- .../uc05-in-progress-to-blocked.json | 3 +- .../uc06-in-progress-to-done.json | 3 +- .../uc07-blocked-to-in-progress.json | 3 +- .../uc08-snoozed-to-open.json | 3 +- .../uc09-open-to-cancelled.json | 3 +- .../uc10-cancelled-to-open-illegal.json | 3 +- .../runtime-requests/uc11-invalid-status.json | 3 +- .../src/agent.rs | 47 ++++++- .../transition-action-status/workflow.json | 6 +- ..._transition_action_status_example_smoke.sh | 14 +- 18 files changed, 241 insertions(+), 47 deletions(-) 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 aa17d3843cf4a2b0b987c6346d33bce6f2f5b65b..d96d51b76d14a1edb68bdc93c22cffa55110bda4 100755 GIT binary patch literal 12698 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 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"