diff --git a/AGENTS.md b/AGENTS.md index 58e59bd..1bd1f8c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -14,7 +14,7 @@ gh project item-list 2 --owner traverse-framework --format json --limit 300 \ - **Phase 3 embedded runtime** ([#109](https://github.com/traverse-framework/reference-apps/issues/109)–[#118](https://github.com/traverse-framework/reference-apps/issues/118)) — see `docs/embedded-runtime-plan.md`; blocked on a **consumable platform embedder SDK** (Traverse [#553](https://github.com/traverse-framework/Traverse/issues/553) closed via [#578](https://github.com/traverse-framework/Traverse/pull/578) with manifest `execution_mode` only — no web/Swift/etc. embedder package yet). Traverse [#615](https://github.com/traverse-framework/Traverse/pull/615) added wasm32 core build only — not a platform embedder package. - **doc-approval multi-capability showcase** ([#111](https://github.com/traverse-framework/reference-apps/issues/111), [#112](https://github.com/traverse-framework/reference-apps/issues/112)) — blocked on Traverse [#538](https://github.com/traverse-framework/Traverse/issues/538) / [#555](https://github.com/traverse-framework/Traverse/issues/555) (`extract` / `recommend` agents) -Ready: [#110](https://github.com/traverse-framework/reference-apps/issues/110) traverse-starter.pipeline; [#73](https://github.com/traverse-framework/reference-apps/issues/73) doc-approval-core-rs ([#58](https://github.com/traverse-framework/reference-apps/issues/58)/[#59](https://github.com/traverse-framework/reference-apps/issues/59)/[#72](https://github.com/traverse-framework/reference-apps/issues/72) Done). +Ready: (none — shared packages shipped; [#110](https://github.com/traverse-framework/reference-apps/issues/110) pipeline showcase in progress). Update this section when a PR changes platform status (see PR template checklist). diff --git a/README.md b/README.md index ead4d5d..a52a439 100644 --- a/README.md +++ b/README.md @@ -173,6 +173,6 @@ Active blockers on [Project 2](https://github.com/orgs/traverse-framework/projec - **Phase 3 embedded runtime** ([#109](https://github.com/traverse-framework/reference-apps/issues/109)–[#118](https://github.com/traverse-framework/reference-apps/issues/118)) — all platform clients must bundle the WASM runtime host; blocked on a **consumable platform embedder SDK** (Traverse [#553](https://github.com/traverse-framework/Traverse/issues/553) closed via [#578](https://github.com/traverse-framework/Traverse/pull/578) with manifest `execution_mode` only; [#615](https://github.com/traverse-framework/Traverse/pull/615) is wasm32 core build only). HTTP sidecar is dev-only. - **doc-approval multi-capability showcase** ([#111](https://github.com/traverse-framework/reference-apps/issues/111), [#112](https://github.com/traverse-framework/reference-apps/issues/112)) — blocked on Traverse [#538](https://github.com/traverse-framework/Traverse/issues/538) / [#555](https://github.com/traverse-framework/Traverse/issues/555) (`extract` / `recommend` agents). -- **traverse-starter.pipeline showcase** ([#110](https://github.com/traverse-framework/reference-apps/issues/110)) — Ready (Traverse [#620](https://github.com/traverse-framework/Traverse/issues/620) closed; pipeline on Traverse `main`). +- **traverse-starter.pipeline showcase** ([#110](https://github.com/traverse-framework/reference-apps/issues/110)) — In Progress (Traverse [#620](https://github.com/traverse-framework/Traverse/issues/620)/[#554](https://github.com/traverse-framework/Traverse/issues/554) closed). -Ready on Project 2: [#110](https://github.com/traverse-framework/reference-apps/issues/110) pipeline showcase ([#58](https://github.com/traverse-framework/reference-apps/issues/58)/[#59](https://github.com/traverse-framework/reference-apps/issues/59)/[#72](https://github.com/traverse-framework/reference-apps/issues/72) Done; [#73](https://github.com/traverse-framework/reference-apps/issues/73) in progress). +Ready on Project 2: shared HTTP/SSE packages Done ([#58](https://github.com/traverse-framework/reference-apps/issues/58)–[#73](https://github.com/traverse-framework/reference-apps/issues/73)). diff --git a/apps/traverse-starter/TraverseCore/Sources/TraverseCore/TraverseOutput.swift b/apps/traverse-starter/TraverseCore/Sources/TraverseCore/TraverseOutput.swift index 907c9be..9b0c7dc 100644 --- a/apps/traverse-starter/TraverseCore/Sources/TraverseCore/TraverseOutput.swift +++ b/apps/traverse-starter/TraverseCore/Sources/TraverseCore/TraverseOutput.swift @@ -1,6 +1,16 @@ import Foundation -public struct TraverseStarterOutput: Equatable, Sendable, Codable { +public struct ValidateOutput: Equatable, Sendable, Codable { + public let valid: Bool + public let issues: [String] + + public init(valid: Bool, issues: [String]) { + self.valid = valid + self.issues = issues + } +} + +public struct ProcessOutput: Equatable, Sendable, Codable { public let title: String public let tags: [String] public let noteType: String @@ -22,6 +32,41 @@ public struct TraverseStarterOutput: Equatable, Sendable, Codable { } } +public struct SummarizeOutput: Equatable, Sendable, Codable { + public let summary: String + public let wordCount: Int + + public init(summary: String, wordCount: Int) { + self.summary = summary + self.wordCount = wordCount + } +} + +/// Combined pipeline final output (validate → process → summarize). +public struct TraverseStarterOutput: Equatable, Sendable, Codable { + public let validate: ValidateOutput + public let process: ProcessOutput + public let summarize: SummarizeOutput + + public init(validate: ValidateOutput, process: ProcessOutput, summarize: SummarizeOutput) { + self.validate = validate + self.process = process + self.summarize = summarize + } + + public static let empty = TraverseStarterOutput( + validate: ValidateOutput(valid: false, issues: []), + process: ProcessOutput( + title: "", + tags: [], + noteType: "", + suggestedNextAction: "", + status: "" + ), + summarize: SummarizeOutput(summary: "", wordCount: 0) + ) +} + public struct TraceEvent: Equatable, Sendable, Codable { public let event_type: String public let timestamp: String @@ -136,20 +181,12 @@ public enum JSONValue: Equatable, Sendable, Codable { public enum TraverseOutputParser { public static func parse(_ raw: Any?) -> TraverseStarterOutput? { guard let dict = raw as? [String: Any], - let title = dict["title"] as? String, - let tags = dict["tags"] as? [String], - let noteType = dict["noteType"] as? String, - let suggestedNextAction = dict["suggestedNextAction"] as? String, - let status = dict["status"] as? String else { + let validate = parseValidate(dict["validate"]), + let process = parseProcess(dict["process"]), + let summarize = parseSummarize(dict["summarize"]) else { return nil } - return TraverseStarterOutput( - title: title, - tags: tags, - noteType: noteType, - suggestedNextAction: suggestedNextAction, - status: status - ) + return TraverseStarterOutput(validate: validate, process: process, summarize: summarize) } public static func parseEventPayload(_ raw: Any?) -> AppStateEventPayload? { @@ -173,4 +210,47 @@ public enum TraverseOutputParser { errorMessage: errorMessage ) } + + private static func parseValidate(_ raw: Any?) -> ValidateOutput? { + guard let dict = raw as? [String: Any], + let valid = dict["valid"] as? Bool, + let issues = dict["issues"] as? [String] else { + return nil + } + return ValidateOutput(valid: valid, issues: issues) + } + + private static func parseProcess(_ raw: Any?) -> ProcessOutput? { + guard let dict = raw as? [String: Any], + let title = dict["title"] as? String, + let tags = dict["tags"] as? [String], + let noteType = dict["noteType"] as? String, + let suggestedNextAction = dict["suggestedNextAction"] as? String, + let status = dict["status"] as? String else { + return nil + } + return ProcessOutput( + title: title, + tags: tags, + noteType: noteType, + suggestedNextAction: suggestedNextAction, + status: status + ) + } + + private static func parseSummarize(_ raw: Any?) -> SummarizeOutput? { + guard let dict = raw as? [String: Any], + let summary = dict["summary"] as? String, + let wordCount = parseWordCount(dict["wordCount"]) else { + return nil + } + return SummarizeOutput(summary: summary, wordCount: wordCount) + } + + private static func parseWordCount(_ raw: Any?) -> Int? { + if let value = raw as? Int { return value } + if let value = raw as? Double { return Int(value) } + if let value = raw as? String, let parsed = Int(value) { return parsed } + return nil + } } diff --git a/apps/traverse-starter/TraverseCore/Tests/TraverseCoreTests/TraverseCoreTests.swift b/apps/traverse-starter/TraverseCore/Tests/TraverseCoreTests/TraverseCoreTests.swift index fa07e33..eba2444 100644 --- a/apps/traverse-starter/TraverseCore/Tests/TraverseCoreTests/TraverseCoreTests.swift +++ b/apps/traverse-starter/TraverseCore/Tests/TraverseCoreTests/TraverseCoreTests.swift @@ -81,6 +81,25 @@ final class TraverseClientTests: XCTestCase { final class TraverseOutputTests: XCTestCase { func testParseOutput() { + let raw: [String: Any] = [ + "validate": ["valid": true, "issues": [] as [String]], + "process": [ + "title": "T", + "tags": ["a"], + "noteType": "n", + "suggestedNextAction": "x", + "status": "done", + ], + "summarize": ["summary": "A short summary", "wordCount": 3], + ] + let output = TraverseOutputParser.parse(raw) + XCTAssertEqual(output?.process.title, "T") + XCTAssertEqual(output?.process.tags, ["a"]) + XCTAssertEqual(output?.validate.valid, true) + XCTAssertEqual(output?.summarize.wordCount, 3) + } + + func testParseRejectsFlatLegacyOutput() { let raw: [String: Any] = [ "title": "T", "tags": ["a"], @@ -88,9 +107,7 @@ final class TraverseOutputTests: XCTestCase { "suggestedNextAction": "x", "status": "done", ] - let output = TraverseOutputParser.parse(raw) - XCTAssertEqual(output?.title, "T") - XCTAssertEqual(output?.tags, ["a"]) + XCTAssertNil(TraverseOutputParser.parse(raw)) } func testParseEventPayload() { @@ -99,16 +116,20 @@ final class TraverseOutputTests: XCTestCase { "session_id": "sess-1", "execution_id": "exec-1", "output": [ - "title": "T", - "tags": [] as [String], - "noteType": "n", - "suggestedNextAction": "x", - "status": "done", + "validate": ["valid": true, "issues": [] as [String]], + "process": [ + "title": "T", + "tags": [] as [String], + "noteType": "n", + "suggestedNextAction": "x", + "status": "done", + ], + "summarize": ["summary": "Summary", "wordCount": 1], ], ] let payload = TraverseOutputParser.parseEventPayload(raw) XCTAssertEqual(payload?.state, "results") - XCTAssertEqual(payload?.output?.title, "T") + XCTAssertEqual(payload?.output?.process.title, "T") } } @@ -177,16 +198,20 @@ final class AppStateViewModelTests: XCTestCase { sessionId: "sess-1", executionId: "exec-1", output: TraverseStarterOutput( - title: "T", - tags: [], - noteType: "n", - suggestedNextAction: "x", - status: "done" + validate: ValidateOutput(valid: true, issues: []), + process: ProcessOutput( + title: "T", + tags: [], + noteType: "n", + suggestedNextAction: "x", + status: "done" + ), + summarize: SummarizeOutput(summary: "Summary", wordCount: 1) ) ) ) XCTAssertEqual(vm.currentState, "results") - XCTAssertEqual(vm.output?.title, "T") + XCTAssertEqual(vm.output?.process.title, "T") XCTAssertEqual(vm.executionId, "exec-1") } diff --git a/apps/traverse-starter/android-compose/app/src/main/java/com/traverseframework/starter/ExecutionViewModel.kt b/apps/traverse-starter/android-compose/app/src/main/java/com/traverseframework/starter/ExecutionViewModel.kt index eaf7cf7..e430a30 100644 --- a/apps/traverse-starter/android-compose/app/src/main/java/com/traverseframework/starter/ExecutionViewModel.kt +++ b/apps/traverse-starter/android-compose/app/src/main/java/com/traverseframework/starter/ExecutionViewModel.kt @@ -106,13 +106,7 @@ class ExecutionViewModel( } catch (_: Exception) { emptyList() } - val output = result.output ?: TraverseStarterOutput( - title = "", - tags = emptyList(), - noteType = "", - suggestedNextAction = "", - status = "", - ) + val output = result.output ?: TraverseStarterOutput.EMPTY _uiState.update { it.copy(phase = ExecutionPhase.Succeeded(output, trace)) } return } diff --git a/apps/traverse-starter/android-compose/app/src/main/java/com/traverseframework/starter/Models.kt b/apps/traverse-starter/android-compose/app/src/main/java/com/traverseframework/starter/Models.kt index dcb8885..529fe25 100644 --- a/apps/traverse-starter/android-compose/app/src/main/java/com/traverseframework/starter/Models.kt +++ b/apps/traverse-starter/android-compose/app/src/main/java/com/traverseframework/starter/Models.kt @@ -3,7 +3,13 @@ package com.traverseframework.starter import kotlinx.serialization.json.JsonElement @kotlinx.serialization.Serializable -data class TraverseStarterOutput( +data class ValidateOutput( + val valid: Boolean, + val issues: List, +) + +@kotlinx.serialization.Serializable +data class ProcessOutput( val title: String, val tags: List, val noteType: String, @@ -11,6 +17,34 @@ data class TraverseStarterOutput( val status: String, ) +@kotlinx.serialization.Serializable +data class SummarizeOutput( + val summary: String, + val wordCount: Int, +) + +/** Combined pipeline final output (validate → process → summarize). */ +@kotlinx.serialization.Serializable +data class TraverseStarterOutput( + val validate: ValidateOutput, + val process: ProcessOutput, + val summarize: SummarizeOutput, +) { + companion object { + val EMPTY = TraverseStarterOutput( + validate = ValidateOutput(valid = false, issues = emptyList()), + process = ProcessOutput( + title = "", + tags = emptyList(), + noteType = "", + suggestedNextAction = "", + status = "", + ), + summarize = SummarizeOutput(summary = "", wordCount = 0), + ) + } +} + @kotlinx.serialization.Serializable data class TraceEvent( val event_type: String, @@ -39,7 +73,7 @@ data class ExecutionPollResult( ) object AppConstants { - const val CAPABILITY_ID = "traverse-starter.process" + const val CAPABILITY_ID = "traverse-starter.pipeline" const val DEFAULT_BASE_URL = "http://10.0.2.2:8787" const val DEFAULT_WORKSPACE = "local-default" const val NOTE_MAX_LENGTH = 2000 diff --git a/apps/traverse-starter/android-compose/app/src/main/java/com/traverseframework/starter/ui/MainScreen.kt b/apps/traverse-starter/android-compose/app/src/main/java/com/traverseframework/starter/ui/MainScreen.kt index 90cbb62..6c48501 100644 --- a/apps/traverse-starter/android-compose/app/src/main/java/com/traverseframework/starter/ui/MainScreen.kt +++ b/apps/traverse-starter/android-compose/app/src/main/java/com/traverseframework/starter/ui/MainScreen.kt @@ -168,11 +168,15 @@ private fun OutputCard( @Composable private fun OutputFields(output: TraverseStarterOutput) { - Field("Title", output.title) - Field("Tags", output.tags.joinToString(", ")) - Field("Note type", output.noteType) - Field("Next action", output.suggestedNextAction) - Field("Status", output.status) + Field("Valid", if (output.validate.valid) "yes" else "no") + Field("Issues", output.validate.issues.joinToString(", ").ifEmpty { "None" }) + Field("Title", output.process.title) + Field("Note type", output.process.noteType) + Field("Status", output.process.status) + Field("Next action", output.process.suggestedNextAction) + Field("Tags", output.process.tags.joinToString(", ")) + Field("Summary", output.summarize.summary) + Field("Word count", output.summarize.wordCount.toString()) } @Composable diff --git a/apps/traverse-starter/android-compose/app/src/test/java/com/traverseframework/starter/TraverseClientTest.kt b/apps/traverse-starter/android-compose/app/src/test/java/com/traverseframework/starter/TraverseClientTest.kt index 26efe1a..2760fd3 100644 --- a/apps/traverse-starter/android-compose/app/src/test/java/com/traverseframework/starter/TraverseClientTest.kt +++ b/apps/traverse-starter/android-compose/app/src/test/java/com/traverseframework/starter/TraverseClientTest.kt @@ -44,7 +44,7 @@ class TraverseClientTest { val engine = MockEngine { respond( content = """ - {"status":"succeeded","output":{"title":"T","tags":["a"],"noteType":"n","suggestedNextAction":"x","status":"done"}} + {"status":"succeeded","output":{"validate":{"valid":true,"issues":[]},"process":{"title":"T","tags":["a"],"noteType":"n","suggestedNextAction":"x","status":"done"},"summarize":{"summary":"Summary","wordCount":1}}} """.trimIndent(), status = HttpStatusCode.OK, headers = headersOf(HttpHeaders.ContentType, "application/json"), @@ -53,6 +53,8 @@ class TraverseClientTest { val client = TraverseClient(engine) val result = client.pollExecution("http://10.0.2.2:8787", "local-default", "exec_abc") assertEquals("succeeded", result.status) - assertEquals("T", result.output?.title) + assertEquals("T", result.output?.process?.title) + assertEquals(true, result.output?.validate?.valid) + assertEquals(1, result.output?.summarize?.wordCount) } } diff --git a/apps/traverse-starter/cli-rust/src/commands/run.rs b/apps/traverse-starter/cli-rust/src/commands/run.rs index 214ac6e..c566901 100644 --- a/apps/traverse-starter/cli-rust/src/commands/run.rs +++ b/apps/traverse-starter/cli-rust/src/commands/run.rs @@ -63,13 +63,7 @@ pub fn execute(base_url: &str, workspace: &str, note: &str, json: bool) -> i32 { .execution_id .or(accepted.execution_id.clone()) .unwrap_or_default(); - let output = event.output.unwrap_or(TraverseStarterOutput { - title: String::new(), - tags: vec![], - note_type: String::new(), - suggested_next_action: String::new(), - status: String::new(), - }); + let output = event.output.unwrap_or_else(TraverseStarterOutput::empty); let trace = if execution_id.is_empty() { Vec::new() } else { diff --git a/apps/traverse-starter/cli-rust/src/main.rs b/apps/traverse-starter/cli-rust/src/main.rs index 8d3b9af..6b438ec 100644 --- a/apps/traverse-starter/cli-rust/src/main.rs +++ b/apps/traverse-starter/cli-rust/src/main.rs @@ -18,7 +18,7 @@ struct Cli { #[derive(Subcommand)] enum Commands { - /// Execute traverse-starter.process with a note + /// Execute traverse-starter.pipeline with a note Run { #[arg(long)] note: String, diff --git a/apps/traverse-starter/cli-rust/src/output.rs b/apps/traverse-starter/cli-rust/src/output.rs index c86a041..b15ff5e 100644 --- a/apps/traverse-starter/cli-rust/src/output.rs +++ b/apps/traverse-starter/cli-rust/src/output.rs @@ -34,11 +34,25 @@ pub fn print_run_result(result: &RunResultJson, json: bool) { } let output = &result.output; - println!("Title: {}", output.title.bold()); - println!("Tags: {}", output.tags.join(", ")); - println!("Note type: {}", output.note_type); - println!("Next action: {}", output.suggested_next_action); - println!("Status: {}", output.status); + println!( + "Valid: {}", + if output.validate.valid { "yes" } else { "no" }.bold() + ); + println!( + "Issues: {}", + if output.validate.issues.is_empty() { + "None".to_string() + } else { + output.validate.issues.join(", ") + } + ); + println!("Title: {}", output.process.title.bold()); + println!("Note type: {}", output.process.note_type); + println!("Status: {}", output.process.status); + println!("Next action: {}", output.process.suggested_next_action); + println!("Tags: {}", output.process.tags.join(", ")); + println!("Summary: {}", output.summarize.summary); + println!("Word count: {}", output.summarize.word_count); if !result.trace.is_empty() { println!("Trace ({} events):", result.trace.len()); for event in &result.trace { diff --git a/apps/traverse-starter/cli-rust/tests/integration_tests.rs b/apps/traverse-starter/cli-rust/tests/integration_tests.rs index d5e9dd4..cd79b12 100644 --- a/apps/traverse-starter/cli-rust/tests/integration_tests.rs +++ b/apps/traverse-starter/cli-rust/tests/integration_tests.rs @@ -30,7 +30,7 @@ fn run_flow_succeeds_against_mock_server() { ) .create(); - let sse_body = "event: capability_result\ndata: {\"state\":\"results\",\"session_id\":\"sess-1\",\"execution_id\":\"exec_abc\",\"output\":{\"title\":\"Title\",\"tags\":[\"tag\"],\"noteType\":\"meeting\",\"suggestedNextAction\":\"follow up\",\"status\":\"processed\"}}\n\n"; + let sse_body = "event: capability_result\ndata: {\"state\":\"results\",\"session_id\":\"sess-1\",\"execution_id\":\"exec_abc\",\"output\":{\"validate\":{\"valid\":true,\"issues\":[]},\"process\":{\"title\":\"Title\",\"tags\":[\"tag\"],\"noteType\":\"meeting\",\"suggestedNextAction\":\"follow up\",\"status\":\"processed\"},\"summarize\":{\"summary\":\"A short summary\",\"wordCount\":3}}}\n\n"; let _events = server .mock( "GET", diff --git a/apps/traverse-starter/ios-swift/TraverseStarter/ContentView.swift b/apps/traverse-starter/ios-swift/TraverseStarter/ContentView.swift index 992910d..ba28908 100644 --- a/apps/traverse-starter/ios-swift/TraverseStarter/ContentView.swift +++ b/apps/traverse-starter/ios-swift/TraverseStarter/ContentView.swift @@ -137,11 +137,18 @@ struct ContentView: View { private func outputFields(_ output: TraverseStarterOutput) -> some View { VStack(alignment: .leading, spacing: 8) { - labeledField("Title", output.title) - labeledField("Tags", output.tags.joined(separator: ", ")) - labeledField("Note type", output.noteType) - labeledField("Suggested next action", output.suggestedNextAction) - labeledField("Status", output.status) + labeledField("Valid", output.validate.valid ? "yes" : "no") + labeledField( + "Issues", + output.validate.issues.isEmpty ? "None" : output.validate.issues.joined(separator: ", ") + ) + labeledField("Title", output.process.title) + labeledField("Note type", output.process.noteType) + labeledField("Status", output.process.status) + labeledField("Suggested next action", output.process.suggestedNextAction) + labeledField("Tags", output.process.tags.joined(separator: ", ")) + labeledField("Summary", output.summarize.summary) + labeledField("Word count", String(output.summarize.wordCount)) } } diff --git a/apps/traverse-starter/linux-gtk/src/ui/main_window.rs b/apps/traverse-starter/linux-gtk/src/ui/main_window.rs index da3298b..68d6d14 100644 --- a/apps/traverse-starter/linux-gtk/src/ui/main_window.rs +++ b/apps/traverse-starter/linux-gtk/src/ui/main_window.rs @@ -147,12 +147,20 @@ impl MainWindow { } ExecutionPhase::Succeeded { output, trace } => { output_label.set_text(&format!( - "Title: {}\nTags: {}\nNote type: {}\nNext action: {}\nStatus: {}", - output.title, - output.tags.join(", "), - output.note_type, - output.suggested_next_action, - output.status + "Valid: {}\nIssues: {}\nTitle: {}\nNote type: {}\nStatus: {}\nNext action: {}\nTags: {}\nSummary: {}\nWord count: {}", + if output.validate.valid { "yes" } else { "no" }, + if output.validate.issues.is_empty() { + "None".to_string() + } else { + output.validate.issues.join(", ") + }, + output.process.title, + output.process.note_type, + output.process.status, + output.process.suggested_next_action, + output.process.tags.join(", "), + output.summarize.summary, + output.summarize.word_count )); output_label.remove_css_class("dim-label"); if !trace.is_empty() { @@ -401,13 +409,7 @@ async fn wait_for_result( } if matches!(event.state.as_ref(), Some(AppState::Results)) { state.lock().unwrap().phase = ExecutionPhase::Succeeded { - output: TraverseStarterOutput { - title: String::new(), - tags: vec![], - note_type: String::new(), - suggested_next_action: String::new(), - status: String::new(), - }, + output: TraverseStarterOutput::empty(), trace: Vec::new(), }; refresh_ui(); diff --git a/apps/traverse-starter/macos-swift/TraverseStarterMac/ContentView.swift b/apps/traverse-starter/macos-swift/TraverseStarterMac/ContentView.swift index aa0de4b..dadb685 100644 --- a/apps/traverse-starter/macos-swift/TraverseStarterMac/ContentView.swift +++ b/apps/traverse-starter/macos-swift/TraverseStarterMac/ContentView.swift @@ -128,24 +128,40 @@ struct ContentView: View { private func outputFields(_ output: TraverseStarterOutput) -> some View { Grid(alignment: .leading, horizontalSpacing: 16, verticalSpacing: 8) { GridRow { - Text("Title").foregroundStyle(.secondary) - Text(output.title) + Text("Valid").foregroundStyle(.secondary) + Text(output.validate.valid ? "yes" : "no") } GridRow { - Text("Tags").foregroundStyle(.secondary) - Text(output.tags.joined(separator: ", ")) + Text("Issues").foregroundStyle(.secondary) + Text(output.validate.issues.isEmpty ? "None" : output.validate.issues.joined(separator: ", ")) + } + GridRow { + Text("Title").foregroundStyle(.secondary) + Text(output.process.title) } GridRow { Text("Note type").foregroundStyle(.secondary) - Text(output.noteType) + Text(output.process.noteType) + } + GridRow { + Text("Status").foregroundStyle(.secondary) + Text(output.process.status) } GridRow { Text("Next action").foregroundStyle(.secondary) - Text(output.suggestedNextAction) + Text(output.process.suggestedNextAction) } GridRow { - Text("Status").foregroundStyle(.secondary) - Text(output.status) + Text("Tags").foregroundStyle(.secondary) + Text(output.process.tags.joined(separator: ", ")) + } + GridRow { + Text("Summary").foregroundStyle(.secondary) + Text(output.summarize.summary) + } + GridRow { + Text("Word count").foregroundStyle(.secondary) + Text(String(output.summarize.wordCount)) } } } diff --git a/apps/traverse-starter/traverse-core-rs/src/client.rs b/apps/traverse-starter/traverse-core-rs/src/client.rs index 7250a86..011db10 100644 --- a/apps/traverse-starter/traverse-core-rs/src/client.rs +++ b/apps/traverse-starter/traverse-core-rs/src/client.rs @@ -6,7 +6,13 @@ use crate::state::StateEvent; use crate::DEFAULT_APP_ID; #[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] -pub struct TraverseStarterOutput { +pub struct ValidateOutput { + pub valid: bool, + pub issues: Vec, +} + +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] +pub struct ProcessOutput { pub title: String, pub tags: Vec, #[serde(rename = "noteType")] @@ -16,6 +22,43 @@ pub struct TraverseStarterOutput { pub status: String, } +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] +pub struct SummarizeOutput { + pub summary: String, + #[serde(rename = "wordCount")] + pub word_count: u64, +} + +/// Combined pipeline final output (validate → process → summarize). +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] +pub struct TraverseStarterOutput { + pub validate: ValidateOutput, + pub process: ProcessOutput, + pub summarize: SummarizeOutput, +} + +impl TraverseStarterOutput { + pub fn empty() -> Self { + Self { + validate: ValidateOutput { + valid: false, + issues: vec![], + }, + process: ProcessOutput { + title: String::new(), + tags: vec![], + note_type: String::new(), + suggested_next_action: String::new(), + status: String::new(), + }, + summarize: SummarizeOutput { + summary: String::new(), + word_count: 0, + }, + } + } +} + #[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] pub struct TraceEvent { pub event_type: String, diff --git a/apps/traverse-starter/traverse-core-rs/src/lib.rs b/apps/traverse-starter/traverse-core-rs/src/lib.rs index 84b5278..1fcc263 100644 --- a/apps/traverse-starter/traverse-core-rs/src/lib.rs +++ b/apps/traverse-starter/traverse-core-rs/src/lib.rs @@ -6,7 +6,10 @@ mod discovery; mod sse; mod state; -pub use client::{CommandAccepted, TraverseClient, TraverseClientError, TraverseStarterOutput, TraceEvent}; +pub use client::{ + CommandAccepted, ProcessOutput, SummarizeOutput, TraceEvent, TraverseClient, + TraverseClientError, TraverseStarterOutput, ValidateOutput, +}; pub use discovery::{ServerDiscovery, ServerInfo}; pub use sse::subscribe_events; pub use state::{AppState, StateEvent}; diff --git a/apps/traverse-starter/traverse-core-rs/src/sse.rs b/apps/traverse-starter/traverse-core-rs/src/sse.rs index 8e16faa..3f6c853 100644 --- a/apps/traverse-starter/traverse-core-rs/src/sse.rs +++ b/apps/traverse-starter/traverse-core-rs/src/sse.rs @@ -92,9 +92,12 @@ mod tests { #[test] fn parses_capability_result_block() { - let block = "event: capability_result\ndata: {\"state\":\"results\",\"session_id\":\"s1\",\"execution_id\":\"e1\",\"output\":{\"title\":\"T\",\"tags\":[],\"noteType\":\"n\",\"suggestedNextAction\":\"x\",\"status\":\"done\"}}"; + let block = "event: capability_result\ndata: {\"state\":\"results\",\"session_id\":\"s1\",\"execution_id\":\"e1\",\"output\":{\"validate\":{\"valid\":true,\"issues\":[]},\"process\":{\"title\":\"T\",\"tags\":[],\"noteType\":\"n\",\"suggestedNextAction\":\"x\",\"status\":\"done\"},\"summarize\":{\"summary\":\"Summary\",\"wordCount\":1}}}"; let event = parse_block(block).expect("event"); assert_eq!(event.event_type, "capability_result"); - assert_eq!(event.output.as_ref().map(|o| o.title.as_str()), Some("T")); + assert_eq!( + event.output.as_ref().map(|o| o.process.title.as_str()), + Some("T") + ); } } diff --git a/apps/traverse-starter/traverse-core-rs/tests/client_tests.rs b/apps/traverse-starter/traverse-core-rs/tests/client_tests.rs index fe0c21e..9ea9988 100644 --- a/apps/traverse-starter/traverse-core-rs/tests/client_tests.rs +++ b/apps/traverse-starter/traverse-core-rs/tests/client_tests.rs @@ -61,10 +61,10 @@ fn server_discovery_reads_json() { fn state_event_from_sse() { let event = StateEvent::from_sse( "capability_result", - r#"{"state":"results","output":{"title":"T","tags":[],"noteType":"n","suggestedNextAction":"x","status":"done"}}"#, + r#"{"state":"results","output":{"validate":{"valid":true,"issues":[]},"process":{"title":"T","tags":[],"noteType":"n","suggestedNextAction":"x","status":"done"},"summarize":{"summary":"Summary","wordCount":1}}}"#, ) .unwrap(); - assert_eq!(event.output.unwrap().title, "T"); + assert_eq!(event.output.unwrap().process.title, "T"); } fn tempfile_dir() -> std::path::PathBuf { diff --git a/apps/traverse-starter/traverse-core-rs/tests/sse_tests.rs b/apps/traverse-starter/traverse-core-rs/tests/sse_tests.rs index 332ed00..31691fd 100644 --- a/apps/traverse-starter/traverse-core-rs/tests/sse_tests.rs +++ b/apps/traverse-starter/traverse-core-rs/tests/sse_tests.rs @@ -22,12 +22,12 @@ fn parses_state_changed() { fn parses_capability_result_payload() { let event = StateEvent::from_sse( "capability_result", - r#"{"state":"results","session_id":"s1","execution_id":"e1","output":{"title":"Hello","tags":["a"],"noteType":"n","suggestedNextAction":"x","status":"done"}}"#, + r#"{"state":"results","session_id":"s1","execution_id":"e1","output":{"validate":{"valid":true,"issues":[]},"process":{"title":"Hello","tags":["a"],"noteType":"n","suggestedNextAction":"x","status":"done"},"summarize":{"summary":"A short summary","wordCount":3}}}"#, ) .unwrap(); assert_eq!(event.event_type, "capability_result"); assert_eq!(event.execution_id.as_deref(), Some("e1")); - assert_eq!(event.output.unwrap().title, "Hello"); + assert_eq!(event.output.unwrap().process.title, "Hello"); } #[test] diff --git a/apps/traverse-starter/web-react/src/App.test.tsx b/apps/traverse-starter/web-react/src/App.test.tsx index ec4d625..dd3c60b 100644 --- a/apps/traverse-starter/web-react/src/App.test.tsx +++ b/apps/traverse-starter/web-react/src/App.test.tsx @@ -86,11 +86,15 @@ describe('App', () => { it('submits on Cmd+Enter and renders SSE capability_result output', async () => { const output = { - title: 'T', - tags: [], - noteType: 'fleeting', - suggestedNextAction: 'review', - status: 'complete', + validate: { valid: true, issues: [] as string[] }, + process: { + title: 'T', + tags: [], + noteType: 'fleeting', + suggestedNextAction: 'review', + status: 'complete', + }, + summarize: { summary: 'Short', wordCount: 1 }, } vi.spyOn(globalThis, 'fetch').mockImplementation((url, init) => { const path = String(url) @@ -145,5 +149,7 @@ describe('App', () => { expect(screen.getByText('Title')).toBeInTheDocument() expect(screen.getByText('T')).toBeInTheDocument() + expect(screen.getByText('Summary')).toBeInTheDocument() + expect(screen.getByText('Short')).toBeInTheDocument() }) }) diff --git a/apps/traverse-starter/web-react/src/App.tsx b/apps/traverse-starter/web-react/src/App.tsx index b0de3ad..3719f60 100644 --- a/apps/traverse-starter/web-react/src/App.tsx +++ b/apps/traverse-starter/web-react/src/App.tsx @@ -224,20 +224,27 @@ function App() { {state === 'results' && parsed && (
- - - - + + + + + +
Tags
- {parsed.tags.map(tag => ( + {parsed.process.tags.map(tag => ( {tag} ))}
+ + {trace.length > 0 && (
diff --git a/apps/traverse-starter/web-react/src/client/traverseOutput.test.ts b/apps/traverse-starter/web-react/src/client/traverseOutput.test.ts index 5194086..78a3c51 100644 --- a/apps/traverse-starter/web-react/src/client/traverseOutput.test.ts +++ b/apps/traverse-starter/web-react/src/client/traverseOutput.test.ts @@ -1,36 +1,46 @@ import { parseOutput } from './traverseOutput' const valid = { - title: 'My Note', - tags: ['work', 'idea'], - noteType: 'fleeting', - suggestedNextAction: 'review', - status: 'complete', + validate: { valid: true, issues: [] as string[] }, + process: { + title: 'Meeting notes', + tags: ['work'], + noteType: 'meeting', + suggestedNextAction: 'review', + status: 'complete', + }, + summarize: { summary: 'A short summary', wordCount: 3 }, } describe('parseOutput', () => { - it('returns structured output for valid shape', () => { + it('parses pipeline namespaced output', () => { const result = parseOutput(valid) - expect(result).toEqual(valid) + expect(result?.process.title).toBe('Meeting notes') + expect(result?.summarize.wordCount).toBe(3) + expect(result?.validate.valid).toBe(true) }) - it('returns null for null input', () => { + it('returns null for null', () => { expect(parseOutput(null)).toBeNull() }) - it('returns null when title is missing', () => { - expect(parseOutput({ ...valid, title: undefined })).toBeNull() + it('returns null when process is missing', () => { + expect(parseOutput({ ...valid, process: undefined })).toBeNull() }) - it('returns null when tags is not an array', () => { - expect(parseOutput({ ...valid, tags: 'work' })).toBeNull() + it('returns null for flat legacy process-only payloads', () => { + expect( + parseOutput({ + title: 'T', + tags: [], + noteType: 'n', + suggestedNextAction: 'x', + status: 'done', + }), + ).toBeNull() }) - it('returns null when noteType is missing', () => { - expect(parseOutput({ ...valid, noteType: undefined })).toBeNull() - }) - - it('returns null for non-object input', () => { + it('returns null for non-objects', () => { expect(parseOutput('string')).toBeNull() expect(parseOutput(42)).toBeNull() }) diff --git a/apps/traverse-starter/web-react/src/client/traverseOutput.ts b/apps/traverse-starter/web-react/src/client/traverseOutput.ts index e427a27..aa41ff7 100644 --- a/apps/traverse-starter/web-react/src/client/traverseOutput.ts +++ b/apps/traverse-starter/web-react/src/client/traverseOutput.ts @@ -1,4 +1,5 @@ -export interface TraverseStarterOutput { +/** Process step fields (still used when process is nested under pipeline output). */ +export interface ProcessOutput { title: string tags: string[] noteType: string @@ -6,7 +7,24 @@ export interface TraverseStarterOutput { status: string } -export function parseOutput(raw: unknown): TraverseStarterOutput | null { +export interface ValidateOutput { + valid: boolean + issues: string[] +} + +export interface SummarizeOutput { + summary: string + wordCount: number +} + +/** Combined pipeline final output (validate → process → summarize). */ +export interface TraverseStarterOutput { + validate: ValidateOutput + process: ProcessOutput + summarize: SummarizeOutput +} + +function parseProcess(raw: unknown): ProcessOutput | null { if (!raw || typeof raw !== 'object') return null const o = raw as Record if ( @@ -15,7 +33,9 @@ export function parseOutput(raw: unknown): TraverseStarterOutput | null { typeof o.noteType !== 'string' || typeof o.suggestedNextAction !== 'string' || typeof o.status !== 'string' - ) return null + ) { + return null + } return { title: o.title, tags: o.tags as string[], @@ -24,3 +44,40 @@ export function parseOutput(raw: unknown): TraverseStarterOutput | null { status: o.status, } } + +function parseValidate(raw: unknown): ValidateOutput | null { + if (!raw || typeof raw !== 'object') return null + const o = raw as Record + if (typeof o.valid !== 'boolean' || !Array.isArray(o.issues)) return null + if (!o.issues.every((i) => typeof i === 'string')) return null + return { valid: o.valid, issues: o.issues as string[] } +} + +function parseSummarize(raw: unknown): SummarizeOutput | null { + if (!raw || typeof raw !== 'object') return null + const o = raw as Record + if (typeof o.summary !== 'string') return null + const wordCount = + typeof o.wordCount === 'number' + ? o.wordCount + : typeof o.wordCount === 'string' + ? Number(o.wordCount) + : NaN + if (!Number.isFinite(wordCount)) return null + return { summary: o.summary, wordCount } +} + +/** + * Parses pipeline namespaced final output (validate → process → summarize). + */ +export function parseOutput(raw: unknown): TraverseStarterOutput | null { + if (!raw || typeof raw !== 'object') return null + const o = raw as Record + + const validate = parseValidate(o.validate) + const process = parseProcess(o.process) + const summarize = parseSummarize(o.summarize) + + if (!validate || !process || !summarize) return null + return { validate, process, summarize } +} diff --git a/apps/traverse-starter/web-react/src/hooks/useAppState.test.ts b/apps/traverse-starter/web-react/src/hooks/useAppState.test.ts index fce0ee4..cc21f38 100644 --- a/apps/traverse-starter/web-react/src/hooks/useAppState.test.ts +++ b/apps/traverse-starter/web-react/src/hooks/useAppState.test.ts @@ -120,12 +120,18 @@ describe('useAppState', () => { execution_id: 'exec-1', state: 'results', previous_state: 'processing', - output: { title: 'T', tags: [], noteType: 'fleeting', suggestedNextAction: 'review', status: 'complete' }, + output: { + validate: { valid: true, issues: [] }, + process: { title: 'T', tags: [], noteType: 'fleeting', suggestedNextAction: 'review', status: 'complete' }, + summarize: { summary: 'S', wordCount: 1 }, + }, }) }) expect(result.current.state).toBe('results') expect(result.current.output).toEqual( - expect.objectContaining({ title: 'T' }), + expect.objectContaining({ + process: expect.objectContaining({ title: 'T' }), + }), ) }) diff --git a/apps/traverse-starter/windows-winui/TraverseStarter.Tests/ExecutionViewModelTests.cs b/apps/traverse-starter/windows-winui/TraverseStarter.Tests/ExecutionViewModelTests.cs index 2512ed3..dc15091 100644 --- a/apps/traverse-starter/windows-winui/TraverseStarter.Tests/ExecutionViewModelTests.cs +++ b/apps/traverse-starter/windows-winui/TraverseStarter.Tests/ExecutionViewModelTests.cs @@ -73,7 +73,10 @@ public async Task SubmitTransitionsToSucceeded() new ExecutionPollResult( "exec_test", "succeeded", - new TraverseStarterOutput("Title", ["tag"], "meeting", "follow up", "processed"), + new TraverseStarterOutput( + new ValidateOutput(true, Array.Empty()), + new ProcessOutput("Title", ["tag"], "meeting", "follow up", "processed"), + new SummarizeOutput("A short summary", 3)), null), ], }; @@ -88,7 +91,7 @@ public async Task SubmitTransitionsToSucceeded() await Task.Delay(1500); Assert.Equal(ExecutionPhase.Succeeded, vm.Phase); - Assert.Equal("Title", vm.Output?.Title); + Assert.Equal("Title", vm.Output?.Process.Title); } [Fact] diff --git a/apps/traverse-starter/windows-winui/TraverseStarter.Tests/TraverseClientTests.cs b/apps/traverse-starter/windows-winui/TraverseStarter.Tests/TraverseClientTests.cs index ea203de..c16fab1 100644 --- a/apps/traverse-starter/windows-winui/TraverseStarter.Tests/TraverseClientTests.cs +++ b/apps/traverse-starter/windows-winui/TraverseStarter.Tests/TraverseClientTests.cs @@ -47,7 +47,7 @@ public async Task PollExecutionParsesOutput() { Content = new StringContent( """ - {"status":"succeeded","output":{"title":"T","tags":["a"],"noteType":"n","suggestedNextAction":"x","status":"done"}} + {"status":"succeeded","output":{"validate":{"valid":true,"issues":[]},"process":{"title":"T","tags":["a"],"noteType":"n","suggestedNextAction":"x","status":"done"},"summarize":{"summary":"Summary","wordCount":1}}} """, Encoding.UTF8, "application/json"), @@ -55,6 +55,8 @@ public async Task PollExecutionParsesOutput() var result = await client.PollExecutionAsync("http://127.0.0.1:8787", "local-default", "exec_abc"); Assert.Equal("succeeded", result.Status); - Assert.Equal("T", result.Output?.Title); + Assert.Equal("T", result.Output?.Process.Title); + Assert.True(result.Output?.Validate.Valid); + Assert.Equal(1, result.Output?.Summarize.WordCount); } } diff --git a/apps/traverse-starter/windows-winui/TraverseStarter/AppConstants.cs b/apps/traverse-starter/windows-winui/TraverseStarter/AppConstants.cs index 9635616..b9eab3b 100644 --- a/apps/traverse-starter/windows-winui/TraverseStarter/AppConstants.cs +++ b/apps/traverse-starter/windows-winui/TraverseStarter/AppConstants.cs @@ -2,7 +2,7 @@ namespace TraverseStarter; public static class AppConstants { - public const string CapabilityId = "traverse-starter.process"; + public const string CapabilityId = "traverse-starter.pipeline"; public const string DefaultBaseUrl = "http://127.0.0.1:8787"; public const string DefaultWorkspace = "local-default"; public const int NoteMaxLength = 2000; diff --git a/apps/traverse-starter/windows-winui/TraverseStarter/ExecutionViewModel.cs b/apps/traverse-starter/windows-winui/TraverseStarter/ExecutionViewModel.cs index c999920..cf0f622 100644 --- a/apps/traverse-starter/windows-winui/TraverseStarter/ExecutionViewModel.cs +++ b/apps/traverse-starter/windows-winui/TraverseStarter/ExecutionViewModel.cs @@ -186,12 +186,7 @@ private async Task PollUntilTerminalAsync( trace = Array.Empty(); } - Output = result.Output ?? new TraverseStarterOutput( - string.Empty, - Array.Empty(), - string.Empty, - string.Empty, - string.Empty); + Output = result.Output ?? TraverseStarterOutput.Empty; Trace = trace; Phase = ExecutionPhase.Succeeded; } diff --git a/apps/traverse-starter/windows-winui/TraverseStarter/HomePage.xaml b/apps/traverse-starter/windows-winui/TraverseStarter/HomePage.xaml index 39c3142..4300a45 100644 --- a/apps/traverse-starter/windows-winui/TraverseStarter/HomePage.xaml +++ b/apps/traverse-starter/windows-winui/TraverseStarter/HomePage.xaml @@ -43,17 +43,25 @@ - - - - - - - - - + + + + + + + + + + + + + + + + + diff --git a/apps/traverse-starter/windows-winui/TraverseStarter/HomePage.xaml.cs b/apps/traverse-starter/windows-winui/TraverseStarter/HomePage.xaml.cs index 8fe404c..970265f 100644 --- a/apps/traverse-starter/windows-winui/TraverseStarter/HomePage.xaml.cs +++ b/apps/traverse-starter/windows-winui/TraverseStarter/HomePage.xaml.cs @@ -94,13 +94,21 @@ private void UpdateUi() break; case ExecutionPhase.Succeeded: OutputGrid.Visibility = Visibility.Visible; - TitleValue.Text = _viewModel.Output?.Title ?? string.Empty; + ValidValue.Text = _viewModel.Output?.Validate.Valid == true ? "yes" : "no"; + IssuesValue.Text = _viewModel.Output is null || _viewModel.Output.Validate.Issues.Count == 0 + ? "None" + : string.Join(", ", _viewModel.Output.Validate.Issues); + TitleValue.Text = _viewModel.Output?.Process.Title ?? string.Empty; + NoteTypeValue.Text = _viewModel.Output?.Process.NoteType ?? string.Empty; + StatusValue.Text = _viewModel.Output?.Process.Status ?? string.Empty; + NextActionValue.Text = _viewModel.Output?.Process.SuggestedNextAction ?? string.Empty; TagsValue.Text = _viewModel.Output is null ? string.Empty - : string.Join(", ", _viewModel.Output.Tags); - NoteTypeValue.Text = _viewModel.Output?.NoteType ?? string.Empty; - NextActionValue.Text = _viewModel.Output?.SuggestedNextAction ?? string.Empty; - StatusValue.Text = _viewModel.Output?.Status ?? string.Empty; + : string.Join(", ", _viewModel.Output.Process.Tags); + SummaryValue.Text = _viewModel.Output?.Summarize.Summary ?? string.Empty; + WordCountValue.Text = _viewModel.Output is null + ? string.Empty + : _viewModel.Output.Summarize.WordCount.ToString(); if (_viewModel.Trace.Count > 0) { diff --git a/apps/traverse-starter/windows-winui/TraverseStarter/Models.cs b/apps/traverse-starter/windows-winui/TraverseStarter/Models.cs index d0a674b..ffbb81b 100644 --- a/apps/traverse-starter/windows-winui/TraverseStarter/Models.cs +++ b/apps/traverse-starter/windows-winui/TraverseStarter/Models.cs @@ -3,13 +3,33 @@ namespace TraverseStarter; -public sealed record TraverseStarterOutput( +public sealed record ValidateOutput( + [property: JsonPropertyName("valid")] bool Valid, + [property: JsonPropertyName("issues")] IReadOnlyList Issues); + +public sealed record ProcessOutput( [property: JsonPropertyName("title")] string Title, [property: JsonPropertyName("tags")] IReadOnlyList Tags, [property: JsonPropertyName("noteType")] string NoteType, [property: JsonPropertyName("suggestedNextAction")] string SuggestedNextAction, [property: JsonPropertyName("status")] string Status); +public sealed record SummarizeOutput( + [property: JsonPropertyName("summary")] string Summary, + [property: JsonPropertyName("wordCount")] int WordCount); + +/// Combined pipeline final output (validate → process → summarize). +public sealed record TraverseStarterOutput( + [property: JsonPropertyName("validate")] ValidateOutput Validate, + [property: JsonPropertyName("process")] ProcessOutput Process, + [property: JsonPropertyName("summarize")] SummarizeOutput Summarize) +{ + public static TraverseStarterOutput Empty { get; } = new( + new ValidateOutput(false, Array.Empty()), + new ProcessOutput(string.Empty, Array.Empty(), string.Empty, string.Empty, string.Empty), + new SummarizeOutput(string.Empty, 0)); +} + public sealed record TraceEvent( [property: JsonPropertyName("event_type")] string EventType, string Timestamp, diff --git a/manifests/traverse-starter/app.manifest.json b/manifests/traverse-starter/app.manifest.json index 1a5ae15..ce24524 100644 --- a/manifests/traverse-starter/app.manifest.json +++ b/manifests/traverse-starter/app.manifest.json @@ -1,24 +1,36 @@ { "app_id": "traverse-starter", - "version": "1.0.0", + "version": "1.1.0", "schema_version": "1.0.0", "workspace_defaults": { "workspace_id": "local-default", "registry_scope": "private" }, "components": [ + { + "component_id": "traverse-starter.validate-component", + "version": "1.0.0", + "digest": "sha256:5647c39a1d25d8728350f9619025292a62e78a602068a2ad9b6f075751c93d99", + "manifest_path": "components/validate/component.manifest.json" + }, { "component_id": "traverse-starter.process-component", "version": "1.0.0", "digest": "sha256:5647c39a1d25d8728350f9619025292a62e78a602068a2ad9b6f075751c93d99", "manifest_path": "components/process/component.manifest.json" + }, + { + "component_id": "traverse-starter.summarize-component", + "version": "1.0.0", + "digest": "sha256:5647c39a1d25d8728350f9619025292a62e78a602068a2ad9b6f075751c93d99", + "manifest_path": "components/summarize/component.manifest.json" } ], "workflows": [ { - "workflow_id": "traverse-starter.process", + "workflow_id": "traverse-starter.pipeline", "workflow_version": "1.0.0", - "path": "_traverse/workflows/examples/traverse-starter/process/workflow.json" + "path": "_traverse/workflows/examples/traverse-starter/pipeline/workflow.json" } ], "model_dependencies": [], @@ -53,5 +65,70 @@ "public_surfaces": [ "cli", "http_json" - ] + ], + "state_machine": { + "initial_state": "idle", + "list_context_fields": [ + "output.validate.valid", + "output.validate.issues", + "output.process.title", + "output.process.tags", + "output.process.noteType", + "output.process.suggestedNextAction", + "output.process.status", + "output.summarize.summary", + "output.summarize.wordCount" + ], + "states": [ + { + "id": "idle", + "transitions": [ + { + "on": "submit", + "to": "processing" + } + ] + }, + { + "id": "processing", + "invoke": { + "capability_id": "traverse-starter.pipeline", + "input_from": "command.payload" + }, + "transitions": [ + { + "on": "capability_succeeded", + "to": "results" + }, + { + "on": "capability_failed", + "to": "error" + } + ] + }, + { + "id": "results", + "transitions": [ + { + "on": "reset", + "to": "idle" + } + ] + }, + { + "id": "error", + "transitions": [ + { + "on": "retry", + "to": "processing", + "with_last_payload": true + }, + { + "on": "reset", + "to": "idle" + } + ] + } + ] + } } diff --git a/manifests/traverse-starter/components/summarize/component.manifest.json b/manifests/traverse-starter/components/summarize/component.manifest.json new file mode 100644 index 0000000..c6c0a08 --- /dev/null +++ b/manifests/traverse-starter/components/summarize/component.manifest.json @@ -0,0 +1,28 @@ +{ + "component_id": "traverse-starter.summarize-component", + "version": "1.0.0", + "schema_version": "1.0.0", + "capability_id": "traverse-starter.summarize", + "capability_version": "1.0.0", + "contract_path": "../../_traverse/contracts/examples/traverse-starter/capabilities/summarize/contract.json", + "wasm_binary_path": "../../_traverse/examples/traverse-starter/summarize-agent/artifacts/summarize-agent.wasm", + "wasm_digest": "sha256:5647c39a1d25d8728350f9619025292a62e78a602068a2ad9b6f075751c93d99", + "runtime_constraints": { + "host_api_access": "none", + "network_access": "forbidden", + "filesystem_access": "none" + }, + "permitted_targets": [ + "local", + "device" + ], + "dependencies": [], + "connector_requirements": [], + "validation_evidence": [ + { + "evidence_type": "checked_in_fixture", + "status": "passed", + "produced_by": "traverse_starter_example_smoke" + } + ] +} diff --git a/manifests/traverse-starter/components/validate/component.manifest.json b/manifests/traverse-starter/components/validate/component.manifest.json new file mode 100644 index 0000000..aa255eb --- /dev/null +++ b/manifests/traverse-starter/components/validate/component.manifest.json @@ -0,0 +1,28 @@ +{ + "component_id": "traverse-starter.validate-component", + "version": "1.0.0", + "schema_version": "1.0.0", + "capability_id": "traverse-starter.validate", + "capability_version": "1.0.0", + "contract_path": "../../_traverse/contracts/examples/traverse-starter/capabilities/validate/contract.json", + "wasm_binary_path": "../../_traverse/examples/traverse-starter/validate-agent/artifacts/validate-agent.wasm", + "wasm_digest": "sha256:5647c39a1d25d8728350f9619025292a62e78a602068a2ad9b6f075751c93d99", + "runtime_constraints": { + "host_api_access": "none", + "network_access": "forbidden", + "filesystem_access": "none" + }, + "permitted_targets": [ + "local", + "device" + ], + "dependencies": [], + "connector_requirements": [], + "validation_evidence": [ + { + "evidence_type": "checked_in_fixture", + "status": "passed", + "produced_by": "traverse_starter_example_smoke" + } + ] +} diff --git a/scripts/ci/phase1_smoke.sh b/scripts/ci/phase1_smoke.sh index 2c44ec3..415af50 100755 --- a/scripts/ci/phase1_smoke.sh +++ b/scripts/ci/phase1_smoke.sh @@ -8,14 +8,14 @@ # # Set TRAVERSE_RUNTIME_URL to override default discovery. # Set TRAVERSE_CAPABILITY_ID to override the capability being tested -# (default: traverse-starter.process, input field: note). +# (default: traverse-starter.pipeline, input field: note). # Exit 0 on pass. Exit 1 on failure. set -euo pipefail REPO_ROOT="${REPO_ROOT:-$(git rev-parse --show-toplevel)}" SERVER_JSON="${TRAVERSE_SERVER_JSON:-.traverse/server.json}" TIMEOUT="${SMOKE_TIMEOUT:-30}" -CAPABILITY_ID="${TRAVERSE_CAPABILITY_ID:-traverse-starter.process}" +CAPABILITY_ID="${TRAVERSE_CAPABILITY_ID:-traverse-starter.pipeline}" echo "=== Phase 1 smoke test ===" @@ -151,11 +151,14 @@ assert_field() { } # Fields are runtime-owned — UI must not compute these -assert_field "title" -assert_field "tags" -assert_field "noteType" -assert_field "suggestedNextAction" -assert_field "status" +assert_field "validate.valid" +assert_field "process.title" +assert_field "process.tags" +assert_field "process.noteType" +assert_field "process.suggestedNextAction" +assert_field "process.status" +assert_field "summarize.summary" +assert_field "summarize.wordCount" echo "" if [ "$ASSERT_FAIL" -eq 1 ]; then diff --git a/scripts/ci/phase2_smoke.sh b/scripts/ci/phase2_smoke.sh index 8112bfe..1e58f6c 100755 --- a/scripts/ci/phase2_smoke.sh +++ b/scripts/ci/phase2_smoke.sh @@ -16,7 +16,7 @@ REPO_ROOT="${REPO_ROOT:-$(git rev-parse --show-toplevel)}" MANIFEST="$REPO_ROOT/manifests/traverse-starter/app.manifest.json" SERVER_JSON="${TRAVERSE_SERVER_JSON:-.traverse/server.json}" WORKSPACE_ID="${TRAVERSE_WORKSPACE_ID:-local-default}" -CAPABILITY_ID="${TRAVERSE_CAPABILITY_ID:-traverse-starter.process}" +CAPABILITY_ID="${TRAVERSE_CAPABILITY_ID:-traverse-starter.pipeline}" echo "=== Phase 2 smoke test ===" @@ -128,7 +128,7 @@ fi echo "" echo "Step 4: asserting runtime-provided output fields..." ASSERT_FAIL=0 -for field in title tags noteType suggestedNextAction status; do +for field in validate.valid process.title process.tags process.noteType process.suggestedNextAction process.status summarize.summary summarize.wordCount; do value=$(echo "$RESULT" | jq -r ".output.$field // empty" 2>/dev/null || echo "") if [ -n "$value" ] && [ "$value" != "null" ]; then echo "OK: output.$field present"