From 171994e0ce85ea0567e489eda0f3e2c1111916ad Mon Sep 17 00:00:00 2001 From: Enrico Piovesan Date: Tue, 14 Jul 2026 23:58:02 -0600 Subject: [PATCH] Ship doc-approval.pipeline multi-capability showcase (#111) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Parse and render namespaced analyze → recommend pipeline output across all seven clients; point HTTP execute IDs at doc-approval.pipeline; add web bundle sync; clear #112 Unavailable messaging. Co-authored-by: Cursor --- AGENTS.md | 3 +- README.md | 21 ++-- .../DocApprovalCore/DocApprovalOutput.swift | 108 ++++++++++++++---- .../DocApprovalCoreTests.swift | 60 +++++++--- apps/doc-approval/android-compose/README.md | 2 +- .../docapproval/ExecutionViewModel.kt | 8 +- .../traverseframework/docapproval/Models.kt | 37 +++++- .../docapproval/ui/MainScreen.kt | 16 ++- .../docapproval/TraverseClientTest.kt | 5 +- apps/doc-approval/cli-rust/README.md | 4 +- .../cli-rust/src/commands/submit.rs | 2 +- apps/doc-approval/cli-rust/src/output.rs | 16 ++- .../cli-rust/tests/integration_tests.rs | 21 +++- .../doc-approval-core-rs/src/client.rs | 52 ++++++++- .../doc-approval-core-rs/src/host.rs | 14 +-- .../doc-approval-core-rs/src/lib.rs | 5 +- .../doc-approval-core-rs/src/sse.rs | 4 +- .../tests/client_tests.rs | 4 +- .../doc-approval-core-rs/tests/host_tests.rs | 37 ++++-- .../doc-approval-core-rs/tests/sse_tests.rs | 34 +++--- .../ios-swift/DocApproval/ContentView.swift | 22 ++-- apps/doc-approval/linux-gtk/README.md | 2 +- .../linux-gtk/src/ui/main_window.rs | 21 ++-- .../linux-gtk/src/ui/preferences.rs | 2 +- .../linux-gtk/tests/client_tests.rs | 23 ++-- .../DocApprovalMac/ContentView.swift | 41 ++++--- apps/doc-approval/web-react/README.md | 37 +----- apps/doc-approval/web-react/src/App.test.tsx | 24 ++-- .../src/client/traverseOutput.test.ts | 52 ++++++--- .../web-react/src/client/traverseOutput.ts | 69 +++++++++-- .../src/components/AnalysisResult.tsx | 44 +++++-- .../src/components/DocumentInput.tsx | 2 +- .../web-react/src/host/embeddedHost.ts | 2 +- .../ExecutionViewModelTests.cs | 6 +- .../DocApproval.Tests/TraverseClientTests.cs | 5 +- .../windows-winui/DocApproval/AppConstants.cs | 2 +- .../DocApproval/ExecutionViewModel.cs | 7 +- .../windows-winui/DocApproval/HomePage.xaml | 44 ++++--- .../DocApproval/HomePage.xaml.cs | 13 ++- .../windows-winui/DocApproval/Models.cs | 19 ++- apps/doc-approval/windows-winui/README.md | 2 +- docs/design-language.md | 14 +-- docs/embedded-runtime-plan.md | 11 +- scripts/ci/repository_checks.sh | 2 + scripts/ci/sync_web_doc_approval_bundle.sh | 31 +++++ 45 files changed, 640 insertions(+), 310 deletions(-) create mode 100755 scripts/ci/sync_web_doc_approval_bundle.sh diff --git a/AGENTS.md b/AGENTS.md index cb70c5c..d22eac5 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -12,9 +12,8 @@ gh project item-list 2 --owner traverse-framework --format json --limit 300 \ ### Blocked work summary - **Phase 3 embedded runtime** ([#114](https://github.com/traverse-framework/reference-apps/issues/114)–[#116](https://github.com/traverse-framework/reference-apps/issues/116)) — blocked on remaining consumable embedder SDKs: Traverse [#647](https://github.com/traverse-framework/Traverse/issues/647) (Swift), [#648](https://github.com/traverse-framework/Traverse/issues/648) (Kotlin), [#649](https://github.com/traverse-framework/Traverse/issues/649) (.NET). Rust (#650) and Web/TS (#646) embedder SDKs shipped; App-Refs [#117](https://github.com/traverse-framework/reference-apps/issues/117) Linux/CLI and [#113](https://github.com/traverse-framework/reference-apps/issues/113) web-react embed shipped. -- **doc-approval multi-capability showcase** ([#111](https://github.com/traverse-framework/reference-apps/issues/111)) — Ready after Traverse [#555](https://github.com/traverse-framework/Traverse/issues/555) (`recommend`) and manifests [#112](https://github.com/traverse-framework/reference-apps/issues/112). Spec 069: pipeline is `analyze → recommend` (no extract). -Ready: [#111](https://github.com/traverse-framework/reference-apps/issues/111) (after #112 merges). +Ready: (none — claim from Project 2 when new Ready items appear). Update this section when a PR changes platform status (see PR template checklist). diff --git a/README.md b/README.md index 8618797..2cb6a66 100644 --- a/README.md +++ b/README.md @@ -76,7 +76,7 @@ Developer tool for browsing execution traces — not a domain workflow app. | App | Purpose | Web path | Default capability | |---|---|---|---| | traverse-starter | Flat string output from a short note | [`apps/traverse-starter/web-react/`](apps/traverse-starter/web-react/) | `traverse-starter.process` | -| doc-approval | Document analysis submitter | [`apps/doc-approval/web-react/`](apps/doc-approval/web-react/) | `doc-approval.analyze` | +| doc-approval | Document analysis submitter | [`apps/doc-approval/web-react/`](apps/doc-approval/web-react/) | `doc-approval.pipeline` | | meeting-notes | List-type output from a transcript | [`apps/meeting-notes/web-react/`](apps/meeting-notes/web-react/) | `meeting-notes.process` | | trace-explorer | Execution timeline debugger | [`apps/trace-explorer/web-react/`](apps/trace-explorer/web-react/) | — | @@ -116,13 +116,13 @@ All clients are **native UI shells** separated from business logic. Phase 1/2 us | Platform | Status | Path | |---|---|---| -| Web (React + TypeScript) | Shipped (embedded; bundle waits on #112) | [`apps/doc-approval/web-react/`](apps/doc-approval/web-react/) | -| iOS (SwiftUI) | Shipped | [`apps/doc-approval/ios-swift/`](apps/doc-approval/ios-swift/) | -| macOS (SwiftUI + AppKit) | Shipped | [`apps/doc-approval/macos-swift/`](apps/doc-approval/macos-swift/) | -| Android (Jetpack Compose) | Shipped | [`apps/doc-approval/android-compose/`](apps/doc-approval/android-compose/) | -| Windows (WinUI 3) | Shipped | [`apps/doc-approval/windows-winui/`](apps/doc-approval/windows-winui/) | -| Linux (GTK4 + Rust) | Shipped (embedded; bundle waits on #112) | [`apps/doc-approval/linux-gtk/`](apps/doc-approval/linux-gtk/) | -| CLI (Rust) | Shipped (embedded; bundle waits on #112) | [`apps/doc-approval/cli-rust/`](apps/doc-approval/cli-rust/) | +| Web (React + TypeScript) | Shipped (embedded + pipeline) | [`apps/doc-approval/web-react/`](apps/doc-approval/web-react/) | +| iOS (SwiftUI) | Shipped (pipeline via HTTP) | [`apps/doc-approval/ios-swift/`](apps/doc-approval/ios-swift/) | +| macOS (SwiftUI + AppKit) | Shipped (pipeline via HTTP) | [`apps/doc-approval/macos-swift/`](apps/doc-approval/macos-swift/) | +| Android (Jetpack Compose) | Shipped (pipeline via HTTP) | [`apps/doc-approval/android-compose/`](apps/doc-approval/android-compose/) | +| Windows (WinUI 3) | Shipped (pipeline via HTTP) | [`apps/doc-approval/windows-winui/`](apps/doc-approval/windows-winui/) | +| Linux (GTK4 + Rust) | Shipped (embedded + pipeline) | [`apps/doc-approval/linux-gtk/`](apps/doc-approval/linux-gtk/) | +| CLI (Rust) | Shipped (embedded + pipeline) | [`apps/doc-approval/cli-rust/`](apps/doc-approval/cli-rust/) | ### meeting-notes (list-type output) @@ -171,7 +171,6 @@ Checks 1–5 validate Node, install, typecheck, lint, and tests (no runtime requ Active blockers on [Project 2](https://github.com/orgs/traverse-framework/projects/2): -- **Phase 3 embedded runtime** ([#113](https://github.com/traverse-framework/reference-apps/issues/113)–[#116](https://github.com/traverse-framework/reference-apps/issues/116)) — blocked on remaining embedder SDKs: Traverse [#646](https://github.com/traverse-framework/Traverse/issues/646) (web/TS), [#647](https://github.com/traverse-framework/Traverse/issues/647) (Swift), [#648](https://github.com/traverse-framework/Traverse/issues/648) (Kotlin), [#649](https://github.com/traverse-framework/Traverse/issues/649) (.NET). Rust SDK shipped ([#650](https://github.com/traverse-framework/Traverse/issues/650)); [#117](https://github.com/traverse-framework/reference-apps/issues/117) is Ready. HTTP sidecar remains 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 only on Traverse [#555](https://github.com/traverse-framework/Traverse/issues/555) (`recommend`). Spec 069: pipeline is `analyze → recommend` (no extract; [#538](https://github.com/traverse-framework/Traverse/issues/538) closed). +- **Phase 3 embedded runtime** ([#114](https://github.com/traverse-framework/reference-apps/issues/114)–[#116](https://github.com/traverse-framework/reference-apps/issues/116)) — blocked on remaining embedder SDKs: Traverse [#647](https://github.com/traverse-framework/Traverse/issues/647) (Swift), [#648](https://github.com/traverse-framework/Traverse/issues/648) (Kotlin), [#649](https://github.com/traverse-framework/Traverse/issues/649) (.NET). Rust (#650) and Web/TS (#646) embedder SDKs shipped; App-Refs [#117](https://github.com/traverse-framework/reference-apps/issues/117) and [#113](https://github.com/traverse-framework/reference-apps/issues/113) shipped. HTTP sidecar remains dev-only for platforms without embed SDKs. -Ready on Project 2: [#111](https://github.com/traverse-framework/reference-apps/issues/111), [#112](https://github.com/traverse-framework/reference-apps/issues/112) (after #113 merges). +Ready on Project 2: check the live board (doc-approval pipeline showcase [#111](https://github.com/traverse-framework/reference-apps/issues/111) ships with this change set). diff --git a/apps/doc-approval/DocApprovalCore/Sources/DocApprovalCore/DocApprovalOutput.swift b/apps/doc-approval/DocApprovalCore/Sources/DocApprovalCore/DocApprovalOutput.swift index 8d502e2..33a9fe4 100644 --- a/apps/doc-approval/DocApprovalCore/Sources/DocApprovalCore/DocApprovalOutput.swift +++ b/apps/doc-approval/DocApprovalCore/Sources/DocApprovalCore/DocApprovalOutput.swift @@ -1,17 +1,17 @@ import Foundation -public struct DocApprovalOutput: Equatable, Sendable, Codable { +public struct AnalysisOutput: Equatable, Sendable, Codable { public let docType: String public let parties: [String] public let amounts: [String] - public let confidence: Double + public let confidence: String public let recommendation: String public init( docType: String, parties: [String], amounts: [String], - confidence: Double, + confidence: String, recommendation: String ) { self.docType = docType @@ -22,6 +22,44 @@ public struct DocApprovalOutput: Equatable, Sendable, Codable { } } +public struct RecommendationOutput: Equatable, Sendable, Codable { + public let recommendation: String + public let rationale: String + public let confidence: String + + public init(recommendation: String, rationale: String, confidence: String) { + self.recommendation = recommendation + self.rationale = rationale + self.confidence = confidence + } +} + +/// Combined pipeline final output (analyze → recommend). +public struct DocApprovalOutput: Equatable, Sendable, Codable { + public let analysis: AnalysisOutput + public let recommendation: RecommendationOutput + + public init(analysis: AnalysisOutput, recommendation: RecommendationOutput) { + self.analysis = analysis + self.recommendation = recommendation + } + + public static let empty = DocApprovalOutput( + analysis: AnalysisOutput( + docType: "", + parties: [], + amounts: [], + confidence: "", + recommendation: "" + ), + recommendation: RecommendationOutput( + recommendation: "", + rationale: "", + confidence: "" + ) + ) +} + public struct TraceEvent: Equatable, Sendable, Codable { public let event_type: String public let timestamp: String @@ -155,29 +193,11 @@ public enum JSONValue: Equatable, Sendable, Codable { public enum DocApprovalOutputParser { public static func parse(_ raw: Any?) -> DocApprovalOutput? { guard let dict = raw as? [String: Any], - let docType = dict["docType"] as? String, - let parties = dict["parties"] as? [String], - let amounts = dict["amounts"] as? [String], - let recommendation = dict["recommendation"] as? String else { + let analysis = parseAnalysis(dict["analysis"]), + let recommendation = parseRecommendation(dict["recommendation"]) else { return nil } - let confidence: Double - if let value = dict["confidence"] as? Double { - confidence = value - } else if let value = dict["confidence"] as? Int { - confidence = Double(value) - } else if let value = dict["confidence"] as? NSNumber { - confidence = value.doubleValue - } else { - return nil - } - return DocApprovalOutput( - docType: docType, - parties: parties, - amounts: amounts, - confidence: confidence, - recommendation: recommendation - ) + return DocApprovalOutput(analysis: analysis, recommendation: recommendation) } public static func parseEventPayload(_ raw: Any?) -> AppStateEventPayload? { @@ -222,4 +242,44 @@ public enum DocApprovalOutputParser { ) } } + + private static func parseAnalysis(_ raw: Any?) -> AnalysisOutput? { + guard let dict = raw as? [String: Any], + let docType = dict["docType"] as? String, + let parties = dict["parties"] as? [String], + let amounts = dict["amounts"] as? [String], + let confidence = stringish(dict["confidence"]), + let recommendation = dict["recommendation"] as? String else { + return nil + } + return AnalysisOutput( + docType: docType, + parties: parties, + amounts: amounts, + confidence: confidence, + recommendation: recommendation + ) + } + + private static func parseRecommendation(_ raw: Any?) -> RecommendationOutput? { + guard let dict = raw as? [String: Any], + let recommendation = dict["recommendation"] as? String, + let rationale = dict["rationale"] as? String, + let confidence = stringish(dict["confidence"]) else { + return nil + } + return RecommendationOutput( + recommendation: recommendation, + rationale: rationale, + confidence: confidence + ) + } + + private static func stringish(_ raw: Any?) -> String? { + if let value = raw as? String { return value } + if let value = raw as? Double { return String(value) } + if let value = raw as? Int { return String(value) } + if let value = raw as? NSNumber { return value.stringValue } + return nil + } } diff --git a/apps/doc-approval/DocApprovalCore/Tests/DocApprovalCoreTests/DocApprovalCoreTests.swift b/apps/doc-approval/DocApprovalCore/Tests/DocApprovalCoreTests/DocApprovalCoreTests.swift index e00be74..e4e127c 100644 --- a/apps/doc-approval/DocApprovalCore/Tests/DocApprovalCoreTests/DocApprovalCoreTests.swift +++ b/apps/doc-approval/DocApprovalCore/Tests/DocApprovalCoreTests/DocApprovalCoreTests.swift @@ -130,15 +130,23 @@ final class DocApprovalClientTests: XCTestCase { final class DocApprovalOutputTests: XCTestCase { func testParseOutput() { let raw: [String: Any] = [ - "docType": "nda", - "parties": ["A", "B"], - "amounts": ["$1"], - "confidence": 0.9, - "recommendation": "approve", + "analysis": [ + "docType": "nda", + "parties": ["A", "B"], + "amounts": ["$1"], + "confidence": 0.9, + "recommendation": "approve", + ], + "recommendation": [ + "recommendation": "approve", + "rationale": "Policy match", + "confidence": "high", + ], ] let output = DocApprovalOutputParser.parse(raw) - XCTAssertEqual(output?.docType, "nda") - XCTAssertEqual(output?.recommendation, "approve") + XCTAssertEqual(output?.analysis.docType, "nda") + XCTAssertEqual(output?.recommendation.recommendation, "approve") + XCTAssertEqual(output?.analysis.confidence, "0.9") } func testParseEventPayload() { @@ -147,16 +155,23 @@ final class DocApprovalOutputTests: XCTestCase { "session_id": "sess-1", "execution_id": "exec-1", "output": [ - "docType": "nda", - "parties": [] as [String], - "amounts": [] as [String], - "confidence": 0.5, - "recommendation": "review", + "analysis": [ + "docType": "nda", + "parties": [] as [String], + "amounts": [] as [String], + "confidence": 0.5, + "recommendation": "review", + ], + "recommendation": [ + "recommendation": "review", + "rationale": "Needs human", + "confidence": "medium", + ], ], ] let payload = DocApprovalOutputParser.parseEventPayload(raw) XCTAssertEqual(payload?.state, "results") - XCTAssertEqual(payload?.output?.docType, "nda") + XCTAssertEqual(payload?.output?.analysis.docType, "nda") } func testParseSessionsArray() { @@ -260,16 +275,23 @@ final class AppStateViewModelTests: XCTestCase { sessionId: "sess-1", executionId: "exec-1", output: DocApprovalOutput( - docType: "nda", - parties: [], - amounts: [], - confidence: 0.8, - recommendation: "approve" + analysis: AnalysisOutput( + docType: "nda", + parties: [], + amounts: [], + confidence: "0.8", + recommendation: "approve" + ), + recommendation: RecommendationOutput( + recommendation: "approve", + rationale: "Policy match", + confidence: "high" + ) ) ) ) XCTAssertEqual(vm.currentState, "results") - XCTAssertEqual(vm.output?.docType, "nda") + XCTAssertEqual(vm.output?.analysis.docType, "nda") } func testCanSubmitWhenOnlineWithDocument() async { diff --git a/apps/doc-approval/android-compose/README.md b/apps/doc-approval/android-compose/README.md index 0f1f843..8eacec0 100644 --- a/apps/doc-approval/android-compose/README.md +++ b/apps/doc-approval/android-compose/README.md @@ -43,7 +43,7 @@ Open the project in Android Studio and run on an API 26+ emulator. | `ui/SettingsScreen.kt` | Runtime URL + workspace | | `SettingsRepository.kt` | DataStore persistence | -The UI renders runtime-provided output fields only (`docType`, `parties`, `amounts`, `confidence`, `recommendation`). +The UI renders runtime-provided pipeline fields only (`analysis.*`, `recommendation.*`). ## Phase 2 (not implemented) diff --git a/apps/doc-approval/android-compose/app/src/main/java/com/traverseframework/docapproval/ExecutionViewModel.kt b/apps/doc-approval/android-compose/app/src/main/java/com/traverseframework/docapproval/ExecutionViewModel.kt index 7dca5ae..30e50ce 100644 --- a/apps/doc-approval/android-compose/app/src/main/java/com/traverseframework/docapproval/ExecutionViewModel.kt +++ b/apps/doc-approval/android-compose/app/src/main/java/com/traverseframework/docapproval/ExecutionViewModel.kt @@ -106,13 +106,7 @@ class ExecutionViewModel( } catch (_: Exception) { emptyList() } - val output = result.output ?: DocApprovalOutput( - docType = "", - parties = emptyList(), - amounts = emptyList(), - confidence = 0.0, - recommendation = "", - ) + val output = result.output ?: DocApprovalOutput.EMPTY _uiState.update { it.copy(phase = ExecutionPhase.Succeeded(output, trace)) } return } diff --git a/apps/doc-approval/android-compose/app/src/main/java/com/traverseframework/docapproval/Models.kt b/apps/doc-approval/android-compose/app/src/main/java/com/traverseframework/docapproval/Models.kt index c7e8e2b..8bd6707 100644 --- a/apps/doc-approval/android-compose/app/src/main/java/com/traverseframework/docapproval/Models.kt +++ b/apps/doc-approval/android-compose/app/src/main/java/com/traverseframework/docapproval/Models.kt @@ -3,14 +3,45 @@ package com.traverseframework.docapproval import kotlinx.serialization.json.JsonElement @kotlinx.serialization.Serializable -data class DocApprovalOutput( +data class AnalysisOutput( val docType: String, val parties: List, val amounts: List, - val confidence: Double, + val confidence: String, + val recommendation: String, +) + +@kotlinx.serialization.Serializable +data class RecommendationOutput( val recommendation: String, + val rationale: String, + val confidence: String, ) +/** Combined pipeline final output (analyze → recommend). */ +@kotlinx.serialization.Serializable +data class DocApprovalOutput( + val analysis: AnalysisOutput, + val recommendation: RecommendationOutput, +) { + companion object { + val EMPTY = DocApprovalOutput( + analysis = AnalysisOutput( + docType = "", + parties = emptyList(), + amounts = emptyList(), + confidence = "", + recommendation = "", + ), + recommendation = RecommendationOutput( + recommendation = "", + rationale = "", + confidence = "", + ), + ) + } +} + @kotlinx.serialization.Serializable data class TraceEvent( val event_type: String, @@ -39,7 +70,7 @@ data class ExecutionPollResult( ) object AppConstants { - const val CAPABILITY_ID = "doc-approval.analyze" + const val CAPABILITY_ID = "doc-approval.pipeline" const val DEFAULT_BASE_URL = "http://10.0.2.2:8787" const val DEFAULT_WORKSPACE = "local-default" } diff --git a/apps/doc-approval/android-compose/app/src/main/java/com/traverseframework/docapproval/ui/MainScreen.kt b/apps/doc-approval/android-compose/app/src/main/java/com/traverseframework/docapproval/ui/MainScreen.kt index e2b853d..d15ec31 100644 --- a/apps/doc-approval/android-compose/app/src/main/java/com/traverseframework/docapproval/ui/MainScreen.kt +++ b/apps/doc-approval/android-compose/app/src/main/java/com/traverseframework/docapproval/ui/MainScreen.kt @@ -168,11 +168,17 @@ private fun OutputCard( @Composable private fun OutputFields(output: DocApprovalOutput) { - Field("Document type", output.docType) - Field("Parties", output.parties.joinToString(", ")) - Field("Amounts", output.amounts.joinToString(", ")) - Field("Confidence", output.confidence.toString()) - Field("Recommendation", output.recommendation) + Text("Analysis", style = MaterialTheme.typography.titleSmall) + Field("Document type", output.analysis.docType) + Field("Parties", output.analysis.parties.joinToString(", ")) + Field("Amounts", output.analysis.amounts.joinToString(", ")) + Field("Analyze confidence", output.analysis.confidence) + Field("Analyze recommendation", output.analysis.recommendation) + Spacer(modifier = Modifier.height(8.dp)) + Text("Recommendation", style = MaterialTheme.typography.titleSmall) + Field("Decision", output.recommendation.recommendation) + Field("Rationale", output.recommendation.rationale) + Field("Confidence", output.recommendation.confidence) } @Composable diff --git a/apps/doc-approval/android-compose/app/src/test/java/com/traverseframework/docapproval/TraverseClientTest.kt b/apps/doc-approval/android-compose/app/src/test/java/com/traverseframework/docapproval/TraverseClientTest.kt index a3fa2cd..652bb92 100644 --- a/apps/doc-approval/android-compose/app/src/test/java/com/traverseframework/docapproval/TraverseClientTest.kt +++ b/apps/doc-approval/android-compose/app/src/test/java/com/traverseframework/docapproval/TraverseClientTest.kt @@ -44,7 +44,7 @@ class TraverseClientTest { val engine = MockEngine { respond( content = """ - {"status":"succeeded","output":{"docType":"invoice","parties":["A","B"],"amounts":["$100"],"confidence":0.9,"recommendation":"approve"}} + {"status":"succeeded","output":{"analysis":{"docType":"invoice","parties":["A","B"],"amounts":["$100"],"confidence":"0.9","recommendation":"approve"},"recommendation":{"recommendation":"approve","rationale":"Policy match","confidence":"high"}}} """.trimIndent(), status = HttpStatusCode.OK, headers = headersOf(HttpHeaders.ContentType, "application/json"), @@ -53,6 +53,7 @@ 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("invoice", result.output?.docType) + assertEquals("invoice", result.output?.analysis?.docType) + assertEquals("approve", result.output?.recommendation?.recommendation) } } diff --git a/apps/doc-approval/cli-rust/README.md b/apps/doc-approval/cli-rust/README.md index 36a64c3..5a2c87f 100644 --- a/apps/doc-approval/cli-rust/README.md +++ b/apps/doc-approval/cli-rust/README.md @@ -2,7 +2,7 @@ Terminal client for the `doc-approval` reference app. Uses `doc-approval-core-rs` with the public **`traverse-embedder`** SDK. No `traverse-cli serve` sidecar. -**Note:** production bundle init requires `manifests/doc-approval/` ([#112](https://github.com/traverse-framework/reference-apps/issues/112)). Until then, health/submit report Unavailable; unit tests use `EmbedderTestDouble`. +**Note:** production bundle init uses `manifests/doc-approval/`. Unit tests use `EmbedderTestDouble`. ## Build @@ -21,4 +21,4 @@ doc-approval health --json | Variable | Purpose | |---|---| -| `DOC_APPROVAL_MANIFEST` | Path to `app.manifest.json` when #112 lands | +| `DOC_APPROVAL_MANIFEST` | Path to `app.manifest.json` | diff --git a/apps/doc-approval/cli-rust/src/commands/submit.rs b/apps/doc-approval/cli-rust/src/commands/submit.rs index db02c92..e928eeb 100644 --- a/apps/doc-approval/cli-rust/src/commands/submit.rs +++ b/apps/doc-approval/cli-rust/src/commands/submit.rs @@ -10,7 +10,7 @@ pub fn execute(document: &str, json: bool) -> i32 { Err(err) => { eprintln!("{err}"); eprintln!( - "hint: doc-approval bundle manifests are tracked in reference-apps #112; set DOC_APPROVAL_MANIFEST when available" + "hint: set DOC_APPROVAL_MANIFEST to manifests/doc-approval/app.manifest.json" ); return 1; } diff --git a/apps/doc-approval/cli-rust/src/output.rs b/apps/doc-approval/cli-rust/src/output.rs index cc2bcbe..a1976f1 100644 --- a/apps/doc-approval/cli-rust/src/output.rs +++ b/apps/doc-approval/cli-rust/src/output.rs @@ -22,11 +22,17 @@ pub fn print_submit_result(result: &SubmitResultJson, json: bool) { } let output = &result.output; - println!("Document type: {}", output.doc_type.bold()); - println!("Parties: {}", output.parties.join(", ")); - println!("Amounts: {}", output.amounts.join(", ")); - println!("Confidence: {}", output.confidence); - println!("Recommendation: {}", output.recommendation); + println!("Document type: {}", output.analysis.doc_type.bold()); + println!("Parties: {}", output.analysis.parties.join(", ")); + println!("Amounts: {}", output.analysis.amounts.join(", ")); + println!("Analyze confidence: {}", output.analysis.confidence); + println!("Analyze recommendation: {}", output.analysis.recommendation); + println!( + "Recommendation: {}", + output.recommendation.recommendation.bold() + ); + println!("Rationale: {}", output.recommendation.rationale); + println!("Recommend confidence: {}", output.recommendation.confidence); if !result.trace.is_empty() { println!("Trace ({} events):", result.trace.len()); for event in &result.trace { diff --git a/apps/doc-approval/cli-rust/tests/integration_tests.rs b/apps/doc-approval/cli-rust/tests/integration_tests.rs index 87e0528..b07c7d5 100644 --- a/apps/doc-approval/cli-rust/tests/integration_tests.rs +++ b/apps/doc-approval/cli-rust/tests/integration_tests.rs @@ -1,13 +1,22 @@ use doc_approval_cli::commands; -use doc_approval_core_rs::{DocApprovalOutput, TestEmbeddedRuntime}; +use doc_approval_core_rs::{ + AnalysisOutput, DocApprovalOutput, RecommendationOutput, TestEmbeddedRuntime, +}; fn sample_output() -> DocApprovalOutput { DocApprovalOutput { - doc_type: "invoice".to_string(), - parties: vec!["A".to_string()], - amounts: vec!["$10".to_string()], - confidence: 0.9, - recommendation: "approve".to_string(), + analysis: AnalysisOutput { + doc_type: "invoice".to_string(), + parties: vec!["A".to_string()], + amounts: vec!["$10".to_string()], + confidence: "0.9".to_string(), + recommendation: "approve".to_string(), + }, + recommendation: RecommendationOutput { + recommendation: "approve".to_string(), + rationale: "Amounts within policy".to_string(), + confidence: "high".to_string(), + }, } } diff --git a/apps/doc-approval/doc-approval-core-rs/src/client.rs b/apps/doc-approval/doc-approval-core-rs/src/client.rs index f384524..0ed88fc 100644 --- a/apps/doc-approval/doc-approval-core-rs/src/client.rs +++ b/apps/doc-approval/doc-approval-core-rs/src/client.rs @@ -1,20 +1,66 @@ -use serde::{Deserialize, Serialize}; +use serde::{Deserialize, Deserializer, Serialize}; use serde_json::{json, Value}; use crate::sse; use crate::state::StateEvent; use crate::{list_sessions, SessionSummary, DEFAULT_APP_ID}; +fn stringish<'de, D: Deserializer<'de>>(deserializer: D) -> Result { + let value = Value::deserialize(deserializer)?; + match value { + Value::String(s) => Ok(s), + Value::Number(n) => Ok(n.to_string()), + other => Err(serde::de::Error::custom(format!( + "expected string or number, got {other}" + ))), + } +} + #[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] -pub struct DocApprovalOutput { +pub struct AnalysisOutput { #[serde(rename = "docType")] pub doc_type: String, pub parties: Vec, pub amounts: Vec, - pub confidence: f64, + #[serde(deserialize_with = "stringish")] + pub confidence: String, pub recommendation: String, } +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] +pub struct RecommendationOutput { + pub recommendation: String, + pub rationale: String, + #[serde(deserialize_with = "stringish")] + pub confidence: String, +} + +/// Combined pipeline final output (analyze → recommend). +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] +pub struct DocApprovalOutput { + pub analysis: AnalysisOutput, + pub recommendation: RecommendationOutput, +} + +impl DocApprovalOutput { + pub fn empty() -> Self { + Self { + analysis: AnalysisOutput { + doc_type: String::new(), + parties: vec![], + amounts: vec![], + confidence: String::new(), + recommendation: String::new(), + }, + recommendation: RecommendationOutput { + recommendation: String::new(), + rationale: String::new(), + confidence: String::new(), + }, + } + } +} + #[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] pub struct TraceEvent { pub event_type: String, diff --git a/apps/doc-approval/doc-approval-core-rs/src/host.rs b/apps/doc-approval/doc-approval-core-rs/src/host.rs index 0e2cdb4..7069c90 100644 --- a/apps/doc-approval/doc-approval-core-rs/src/host.rs +++ b/apps/doc-approval/doc-approval-core-rs/src/host.rs @@ -1,8 +1,6 @@ //! Embedded Traverse runtime host for doc-approval shells. //! -//! Production path uses [`BundleEmbedder`]. Bundle manifests land with -//! reference-apps #112; until then `EmbeddedRuntime::init_default` reports -//! Unavailable. Tests use [`EmbedderTestDouble`]. +//! Production path uses [`BundleEmbedder`]. Tests use [`EmbedderTestDouble`]. use serde_json::{json, Value}; use std::path::{Path, PathBuf}; @@ -126,13 +124,7 @@ fn collect_submit( )); } if parsed.event_type == "capability_result" { - let output = parsed.output.unwrap_or(DocApprovalOutput { - doc_type: String::new(), - parties: vec![], - amounts: vec![], - confidence: 0.0, - recommendation: String::new(), - }); + let output = parsed.output.unwrap_or_else(DocApprovalOutput::empty); return Ok(HostRunResult { session_id, output, @@ -182,7 +174,7 @@ impl EmbeddedRuntime { pub fn init_default() -> Result { let path = resolve_manifest_path(None).ok_or_else(|| { HostError::Init(format!( - "could not find manifests/doc-approval/app.manifest.json (set {MANIFEST_ENV}; blocked on reference-apps #112)" + "could not find manifests/doc-approval/app.manifest.json (set {MANIFEST_ENV})" )) })?; Self::init(path) diff --git a/apps/doc-approval/doc-approval-core-rs/src/lib.rs b/apps/doc-approval/doc-approval-core-rs/src/lib.rs index f14f560..ee7a86a 100644 --- a/apps/doc-approval/doc-approval-core-rs/src/lib.rs +++ b/apps/doc-approval/doc-approval-core-rs/src/lib.rs @@ -2,7 +2,7 @@ //! Platform-neutral: no GTK or terminal UI imports. //! //! Phase 3 production path is the embedded host (`host` module). HTTP sidecar -//! helpers remain for migration/debug until manifests (#112) land. +//! helpers remain for migration/debug. mod client; mod discovery; @@ -12,7 +12,8 @@ mod sse; mod state; pub use client::{ - CommandAccepted, DocApprovalClient, DocApprovalClientError, DocApprovalOutput, TraceEvent, + AnalysisOutput, CommandAccepted, DocApprovalClient, DocApprovalClientError, DocApprovalOutput, + RecommendationOutput, TraceEvent, }; pub use discovery::{ServerDiscovery, ServerInfo}; pub use host::{ diff --git a/apps/doc-approval/doc-approval-core-rs/src/sse.rs b/apps/doc-approval/doc-approval-core-rs/src/sse.rs index 5d2b146..df874cc 100644 --- a/apps/doc-approval/doc-approval-core-rs/src/sse.rs +++ b/apps/doc-approval/doc-approval-core-rs/src/sse.rs @@ -93,11 +93,11 @@ mod tests { #[test] fn parses_capability_result_block() { - let block = "event: capability_result\ndata: {\"state\":\"results\",\"session_id\":\"s1\",\"execution_id\":\"e1\",\"output\":{\"docType\":\"nda\",\"parties\":[],\"amounts\":[],\"confidence\":0.9,\"recommendation\":\"approve\"}}"; + let block = "event: capability_result\ndata: {\"state\":\"results\",\"session_id\":\"s1\",\"execution_id\":\"e1\",\"output\":{\"analysis\":{\"docType\":\"nda\",\"parties\":[],\"amounts\":[],\"confidence\":0.9,\"recommendation\":\"approve\"},\"recommendation\":{\"recommendation\":\"approve\",\"rationale\":\"ok\",\"confidence\":\"high\"}}}"; let event = parse_block(block).expect("event"); assert_eq!(event.event_type, "capability_result"); assert_eq!( - event.output.as_ref().map(|o| o.doc_type.as_str()), + event.output.as_ref().map(|o| o.analysis.doc_type.as_str()), Some("nda") ); } diff --git a/apps/doc-approval/doc-approval-core-rs/tests/client_tests.rs b/apps/doc-approval/doc-approval-core-rs/tests/client_tests.rs index 28613b9..2e8dc30 100644 --- a/apps/doc-approval/doc-approval-core-rs/tests/client_tests.rs +++ b/apps/doc-approval/doc-approval-core-rs/tests/client_tests.rs @@ -112,8 +112,8 @@ fn server_discovery_reads_json() { fn state_event_from_sse() { let event = StateEvent::from_sse( "capability_result", - r#"{"state":"results","output":{"docType":"nda","parties":[],"amounts":[],"confidence":0.5,"recommendation":"review"}}"#, + r#"{"state":"results","output":{"analysis":{"docType":"nda","parties":[],"amounts":[],"confidence":0.5,"recommendation":"review"},"recommendation":{"recommendation":"review","rationale":"needs human","confidence":"medium"}}}"#, ) .unwrap(); - assert_eq!(event.output.unwrap().doc_type, "nda"); + assert_eq!(event.output.unwrap().analysis.doc_type, "nda"); } diff --git a/apps/doc-approval/doc-approval-core-rs/tests/host_tests.rs b/apps/doc-approval/doc-approval-core-rs/tests/host_tests.rs index 2c7fb00..01fb3f6 100644 --- a/apps/doc-approval/doc-approval-core-rs/tests/host_tests.rs +++ b/apps/doc-approval/doc-approval-core-rs/tests/host_tests.rs @@ -1,15 +1,28 @@ -use doc_approval_core_rs::{DocApprovalOutput, TestEmbeddedRuntime}; +use doc_approval_core_rs::{ + AnalysisOutput, DocApprovalOutput, RecommendationOutput, TestEmbeddedRuntime, +}; + +fn sample() -> DocApprovalOutput { + DocApprovalOutput { + analysis: AnalysisOutput { + doc_type: "invoice".into(), + parties: vec!["Acme".into()], + amounts: vec!["$100".into()], + confidence: "0.9".into(), + recommendation: "approve".into(), + }, + recommendation: RecommendationOutput { + recommendation: "approve".into(), + rationale: "Amounts within policy".into(), + confidence: "high".into(), + }, + } +} #[test] -fn test_double_submit_document_returns_scripted_output() { - let mut host = TestEmbeddedRuntime::new(DocApprovalOutput { - doc_type: "invoice".to_string(), - parties: vec!["A".to_string()], - amounts: vec!["$10".to_string()], - confidence: 0.9, - recommendation: "approve".to_string(), - }); - let result = host.submit_document("hello").expect("submit"); - assert_eq!(result.output.doc_type, "invoice"); - assert_eq!(result.output.recommendation, "approve"); +fn test_double_submits_pipeline_output() { + let mut host = TestEmbeddedRuntime::new(sample()); + let result = host.submit_document("Invoice").expect("submit"); + assert_eq!(result.output.recommendation.recommendation, "approve"); + assert_eq!(result.output.analysis.doc_type, "invoice"); } diff --git a/apps/doc-approval/doc-approval-core-rs/tests/sse_tests.rs b/apps/doc-approval/doc-approval-core-rs/tests/sse_tests.rs index 1a03c48..2eba96b 100644 --- a/apps/doc-approval/doc-approval-core-rs/tests/sse_tests.rs +++ b/apps/doc-approval/doc-approval-core-rs/tests/sse_tests.rs @@ -1,26 +1,22 @@ -use doc_approval_core_rs::{parse_sessions, StateEvent}; -use serde_json::json; - -#[test] -fn parses_heartbeat() { - let event = StateEvent::from_sse("heartbeat", "").unwrap(); - assert_eq!(event.event_type, "heartbeat"); -} +use doc_approval_core_rs::StateEvent; #[test] fn parses_capability_result_payload() { let event = StateEvent::from_sse( "capability_result", - r#"{"state":"results","session_id":"s1","execution_id":"e1","output":{"docType":"nda","parties":["A"],"amounts":[],"confidence":0.9,"recommendation":"approve"}}"#, + r#"{"state":"results","session_id":"s1","execution_id":"e1","output":{"analysis":{"docType":"nda","parties":["A"],"amounts":[],"confidence":0.9,"recommendation":"approve"},"recommendation":{"recommendation":"approve","rationale":"Policy match","confidence":"high"}}}"#, ) - .unwrap(); - assert_eq!(event.output.unwrap().recommendation, "approve"); -} - -#[test] -fn parses_sessions_wrapper() { - let sessions = parse_sessions(&json!({ - "sessions": [{"session_id":"s1","state":"pending_review","title":"T"}] - })); - assert_eq!(sessions[0].session_id, "s1"); + .expect("event"); + assert_eq!(event.event_type, "capability_result"); + assert_eq!( + event + .output + .as_ref() + .map(|o| o.recommendation.recommendation.as_str()), + Some("approve") + ); + assert_eq!( + event.output.as_ref().map(|o| o.analysis.confidence.as_str()), + Some("0.9") + ); } diff --git a/apps/doc-approval/ios-swift/DocApproval/ContentView.swift b/apps/doc-approval/ios-swift/DocApproval/ContentView.swift index f9101c2..dcff85b 100644 --- a/apps/doc-approval/ios-swift/DocApproval/ContentView.swift +++ b/apps/doc-approval/ios-swift/DocApproval/ContentView.swift @@ -137,21 +137,19 @@ struct ContentView: View { private func outputFields(_ output: DocApprovalOutput) -> some View { VStack(alignment: .leading, spacing: 8) { - labeledField("Document type", output.docType) - labeledField("Recommendation", output.recommendation) - labeledField("Confidence", formatConfidence(output.confidence)) - labeledField("Parties", output.parties.isEmpty ? "None" : output.parties.joined(separator: ", ")) - labeledField("Amounts", output.amounts.isEmpty ? "None" : output.amounts.joined(separator: ", ")) + Text("Analysis").font(.headline) + labeledField("Document type", output.analysis.docType) + labeledField("Analyze recommendation", output.analysis.recommendation) + labeledField("Analyze confidence", output.analysis.confidence) + labeledField("Parties", output.analysis.parties.isEmpty ? "None" : output.analysis.parties.joined(separator: ", ")) + labeledField("Amounts", output.analysis.amounts.isEmpty ? "None" : output.analysis.amounts.joined(separator: ", ")) + Text("Recommendation").font(.headline) + labeledField("Decision", output.recommendation.recommendation) + labeledField("Rationale", output.recommendation.rationale) + labeledField("Confidence", output.recommendation.confidence) } } - private func formatConfidence(_ value: Double) -> String { - if value >= 0 && value <= 1 { - return "\(Int((value * 100).rounded()))%" - } - return String(value) - } - private func labeledField(_ label: String, _ value: String) -> some View { VStack(alignment: .leading, spacing: 2) { Text(label).font(.caption).foregroundStyle(.secondary) diff --git a/apps/doc-approval/linux-gtk/README.md b/apps/doc-approval/linux-gtk/README.md index 9859252..e4ba094 100644 --- a/apps/doc-approval/linux-gtk/README.md +++ b/apps/doc-approval/linux-gtk/README.md @@ -2,7 +2,7 @@ Native Linux client for `doc-approval`. Uses `doc-approval-core-rs` with **`traverse-embedder`** (Phase 3). No sidecar URL. -**Note:** Zone 1 shows Embedded / Unavailable until `manifests/doc-approval/` lands ([#112](https://github.com/traverse-framework/reference-apps/issues/112)). +**Note:** Zone 1 shows Embedded / Ready when `manifests/doc-approval/` resolves (or `DOC_APPROVAL_MANIFEST`). ## Prerequisites diff --git a/apps/doc-approval/linux-gtk/src/ui/main_window.rs b/apps/doc-approval/linux-gtk/src/ui/main_window.rs index 33e7f17..6730b86 100644 --- a/apps/doc-approval/linux-gtk/src/ui/main_window.rs +++ b/apps/doc-approval/linux-gtk/src/ui/main_window.rs @@ -95,7 +95,7 @@ impl MainWindow { content.append(&button_row); let offline_hint = Label::new(Some( - "Embedded runtime unavailable — doc-approval manifests require issue #112.", + "Embedded runtime unavailable — set DOC_APPROVAL_MANIFEST or run from repo root.", )); offline_hint.add_css_class("dim-label"); offline_hint.set_visible(false); @@ -174,9 +174,9 @@ impl MainWindow { match &state.phase { ExecutionPhase::Idle => { output_label.set_text(if ready { - "Submit a document above to start a workflow." + "Submit a document above to run doc-approval.pipeline." } else { - "Initialize the embedded runtime (manifests #112) to see output here." + "Initialize the embedded runtime to see pipeline output here." }); output_label.add_css_class("dim-label"); } @@ -190,12 +190,15 @@ impl MainWindow { } ExecutionPhase::Succeeded { output, trace } => { output_label.set_text(&format!( - "Document type: {}\nParties: {}\nAmounts: {}\nConfidence: {}\nRecommendation: {}", - output.doc_type, - output.parties.join(", "), - output.amounts.join(", "), - output.confidence, - output.recommendation + "Analysis\nDocument type: {}\nParties: {}\nAmounts: {}\nAnalyze confidence: {}\nAnalyze recommendation: {}\n\nRecommendation\nDecision: {}\nRationale: {}\nConfidence: {}", + output.analysis.doc_type, + output.analysis.parties.join(", "), + output.analysis.amounts.join(", "), + output.analysis.confidence, + output.analysis.recommendation, + output.recommendation.recommendation, + output.recommendation.rationale, + output.recommendation.confidence )); output_label.remove_css_class("dim-label"); if !trace.is_empty() { diff --git a/apps/doc-approval/linux-gtk/src/ui/preferences.rs b/apps/doc-approval/linux-gtk/src/ui/preferences.rs index 117b69f..9f3b682 100644 --- a/apps/doc-approval/linux-gtk/src/ui/preferences.rs +++ b/apps/doc-approval/linux-gtk/src/ui/preferences.rs @@ -37,7 +37,7 @@ impl PreferencesDialog { content.append(&manifest_entry); content.append(&Label::new(Some( - "Embedded runtime — requires manifests/doc-approval (issue #112). No sidecar URL.", + "Embedded runtime — uses manifests/doc-approval. No sidecar URL.", ))); let button_row = GtkBox::new(Orientation::Horizontal, 8); diff --git a/apps/doc-approval/linux-gtk/tests/client_tests.rs b/apps/doc-approval/linux-gtk/tests/client_tests.rs index 45b97bd..db650bd 100644 --- a/apps/doc-approval/linux-gtk/tests/client_tests.rs +++ b/apps/doc-approval/linux-gtk/tests/client_tests.rs @@ -1,14 +1,23 @@ -use doc_approval_core_rs::{DocApprovalOutput, TestEmbeddedRuntime}; +use doc_approval_core_rs::{ + AnalysisOutput, DocApprovalOutput, RecommendationOutput, TestEmbeddedRuntime, +}; #[test] fn embedded_submit_returns_output() { let mut host = TestEmbeddedRuntime::new(DocApprovalOutput { - doc_type: "invoice".to_string(), - parties: vec![], - amounts: vec![], - confidence: 0.5, - recommendation: "review".to_string(), + analysis: AnalysisOutput { + doc_type: "invoice".to_string(), + parties: vec![], + amounts: vec![], + confidence: "0.5".to_string(), + recommendation: "review".to_string(), + }, + recommendation: RecommendationOutput { + recommendation: "review".to_string(), + rationale: "Needs human review".to_string(), + confidence: "medium".to_string(), + }, }); let result = host.submit_document("doc").expect("submit"); - assert_eq!(result.output.recommendation, "review"); + assert_eq!(result.output.recommendation.recommendation, "review"); } diff --git a/apps/doc-approval/macos-swift/DocApprovalMac/ContentView.swift b/apps/doc-approval/macos-swift/DocApprovalMac/ContentView.swift index a8e1185..d9593e0 100644 --- a/apps/doc-approval/macos-swift/DocApprovalMac/ContentView.swift +++ b/apps/doc-approval/macos-swift/DocApprovalMac/ContentView.swift @@ -127,36 +127,49 @@ struct ContentView: View { private func outputFields(_ output: DocApprovalOutput) -> some View { Grid(alignment: .leading, horizontalSpacing: 16, verticalSpacing: 8) { + GridRow { + Text("Analysis").font(.headline) + Text("") + } GridRow { Text("Document type").foregroundStyle(.secondary) - Text(output.docType) + Text(output.analysis.docType) } GridRow { - Text("Recommendation").foregroundStyle(.secondary) - Text(output.recommendation) + Text("Analyze recommendation").foregroundStyle(.secondary) + Text(output.analysis.recommendation) } GridRow { - Text("Confidence").foregroundStyle(.secondary) - Text(formatConfidence(output.confidence)) + Text("Analyze confidence").foregroundStyle(.secondary) + Text(output.analysis.confidence) } GridRow { Text("Parties").foregroundStyle(.secondary) - Text(output.parties.isEmpty ? "None" : output.parties.joined(separator: ", ")) + Text(output.analysis.parties.isEmpty ? "None" : output.analysis.parties.joined(separator: ", ")) } GridRow { Text("Amounts").foregroundStyle(.secondary) - Text(output.amounts.isEmpty ? "None" : output.amounts.joined(separator: ", ")) + Text(output.analysis.amounts.isEmpty ? "None" : output.analysis.amounts.joined(separator: ", ")) + } + GridRow { + Text("Recommendation").font(.headline) + Text("") + } + GridRow { + Text("Decision").foregroundStyle(.secondary) + Text(output.recommendation.recommendation) + } + GridRow { + Text("Rationale").foregroundStyle(.secondary) + Text(output.recommendation.rationale) + } + GridRow { + Text("Confidence").foregroundStyle(.secondary) + Text(output.recommendation.confidence) } } } - private func formatConfidence(_ value: Double) -> String { - if value >= 0 && value <= 1 { - return "\(Int((value * 100).rounded()))%" - } - return String(value) - } - private var statusColor: Color { switch viewModel.runtimeStatus { case .online: return .cyan diff --git a/apps/doc-approval/web-react/README.md b/apps/doc-approval/web-react/README.md index 358d2a6..516dd7c 100644 --- a/apps/doc-approval/web-react/README.md +++ b/apps/doc-approval/web-react/README.md @@ -1,40 +1,25 @@ # doc-approval (Web React UI) -React UI shell for the **doc-approval** reference application. Phase 1 covers the **submitter surface** only: paste document text, execute via the Traverse HTTP/JSON API, poll for completion, and render runtime-provided analysis fields. +React UI shell for the **doc-approval** reference application. Submitter surface: paste document text, run `doc-approval.pipeline` via the embedded Traverse host (or HTTP execute on platforms still on sidecar), and render runtime-provided analysis + recommendation fields. ## Core Design Principles -1. **UI is a rendering layer only** — analysis fields (docType, parties, amounts, confidence, recommendation) come from the runtime. The UI displays them; it does not compute them. +1. **UI is a rendering layer only** — pipeline fields (`analysis.*`, `recommendation.*`) come from the runtime. The UI displays them; it does not compute them. 2. **Strict boundary isolation** — no private Traverse internals are imported. All communication uses public runtime surfaces. ## Configuration | Variable | Default | Purpose | |---|---|---| -| `VITE_TRAVERSE_BASE_URL` | `http://127.0.0.1:8787` | Runtime base URL | -| `VITE_TRAVERSE_WORKSPACE` | `local-default` | Workspace ID | -| `VITE_TRAVERSE_CAPABILITY_ID` | `doc-approval.analyze` | Capability to execute | +| `VITE_DOC_APPROVAL_MANIFEST` | `/bundles/doc-approval/app.manifest.json` | FetchBundleLoader manifest path | -Legacy alias: `VITE_TRAVERSE_RUNTIME_URL` is accepted as a fallback for `VITE_TRAVERSE_BASE_URL`. - -Copy or edit `apps/doc-approval/web-react/.env`: - -```bash -VITE_TRAVERSE_BASE_URL=http://127.0.0.1:8787 -VITE_TRAVERSE_WORKSPACE=local-default -VITE_TRAVERSE_CAPABILITY_ID=doc-approval.analyze -``` - -## Start the Traverse Runtime +Sync a local bundle (requires `TRAVERSE_REPO`): ```bash -git clone https://github.com/traverse-framework/Traverse.git /tmp/traverse -cd /tmp/traverse && git checkout v0.6.0 -cargo run -p traverse-cli -- serve +export TRAVERSE_REPO=/path/to/Traverse +bash scripts/ci/sync_web_doc_approval_bundle.sh ``` -The doc-approval capability must be registered in the runtime before execute calls succeed. See the Traverse repo for domain capability setup. - ## Development Commands From the **repository root**: @@ -48,13 +33,3 @@ npm run lint -w apps/doc-approval/web-react npm run test -w apps/doc-approval/web-react npm run test:coverage -w apps/doc-approval/web-react ``` - -## Phase 2 Upgrade Path - -When Traverse ships state machine, SSE, command dispatch, and session listing: - -- Replace polling with SSE subscription -- Add approver surface: session queue, approve/reject commands -- Conditional transitions (e.g. auto-approve when confidence ≥ 0.85) - -Do not implement Phase 2 in this scaffold. diff --git a/apps/doc-approval/web-react/src/App.test.tsx b/apps/doc-approval/web-react/src/App.test.tsx index 99be338..e9980ae 100644 --- a/apps/doc-approval/web-react/src/App.test.tsx +++ b/apps/doc-approval/web-react/src/App.test.tsx @@ -5,11 +5,18 @@ import { createTestEmbedder } from './host/embeddedHost' import type { DocApprovalOutput } from './client/traverseOutput' const sampleOutput: DocApprovalOutput = { - docType: 'invoice', - parties: ['Acme Corp'], - amounts: ['$500.00'], - confidence: 0.88, - recommendation: 'approve', + analysis: { + docType: 'invoice', + parties: ['Acme Corp'], + amounts: ['$500.00'], + confidence: '0.88', + recommendation: 'approve', + }, + recommendation: { + recommendation: 'approve', + rationale: 'Amounts within policy', + confidence: 'high', + }, } describe('App', () => { @@ -25,10 +32,10 @@ describe('App', () => { render() expect(screen.getByText('Unavailable')).toBeInTheDocument() expect(screen.getByRole('button', { name: 'Analyze Document' })).toBeDisabled() - expect(screen.getByText(/doc-approval manifests require issue #112/)).toBeInTheDocument() + expect(screen.getByText(/sync the doc-approval bundle/)).toBeInTheDocument() }) - it('submits document via test double and displays analysis fields', async () => { + it('submits document via test double and displays pipeline fields', async () => { render() fireEvent.change(screen.getByPlaceholderText(/Paste or type document/i), { target: { value: 'Invoice #123' }, @@ -38,7 +45,8 @@ describe('App', () => { }) expect(screen.getByText('Document Type')).toBeInTheDocument() expect(screen.getByText('invoice')).toBeInTheDocument() - expect(screen.getByText('88%')).toBeInTheDocument() + expect(screen.getByText('Amounts within policy')).toBeInTheDocument() + expect(screen.getByText('high')).toBeInTheDocument() }) it('reset returns to idle', async () => { diff --git a/apps/doc-approval/web-react/src/client/traverseOutput.test.ts b/apps/doc-approval/web-react/src/client/traverseOutput.test.ts index eb26b87..d36815b 100644 --- a/apps/doc-approval/web-react/src/client/traverseOutput.test.ts +++ b/apps/doc-approval/web-react/src/client/traverseOutput.test.ts @@ -1,35 +1,51 @@ +import { describe, it, expect } from 'vitest' import { parseAnalysisOutput } from './traverseOutput' const valid = { - docType: 'invoice', - parties: ['Acme Corp', 'Beta LLC'], - amounts: ['$1,200.00', '$300.00'], - confidence: 0.92, - recommendation: 'approve', + analysis: { + docType: 'invoice', + parties: ['Acme', 'Beta'], + amounts: ['$1,200'], + confidence: '0.92', + recommendation: 'approve', + }, + recommendation: { + recommendation: 'approve', + rationale: 'Amounts within policy', + confidence: 'high', + }, } describe('parseAnalysisOutput', () => { - it('returns structured output for valid shape', () => { - expect(parseAnalysisOutput(valid)).toEqual(valid) + it('parses pipeline namespaced output', () => { + const result = parseAnalysisOutput(valid) + expect(result).toEqual(valid) }) - it('returns null for null input', () => { - expect(parseAnalysisOutput(null)).toBeNull() - }) - - it('returns null when docType is missing', () => { - expect(parseAnalysisOutput({ ...valid, docType: undefined })).toBeNull() + it('coerces numeric confidence to string', () => { + expect( + parseAnalysisOutput({ + ...valid, + analysis: { ...valid.analysis, confidence: 0.92 }, + recommendation: { ...valid.recommendation, confidence: 0.8 }, + }), + ).toEqual({ + ...valid, + analysis: { ...valid.analysis, confidence: '0.92' }, + recommendation: { ...valid.recommendation, confidence: '0.8' }, + }) }) - it('returns null when parties is not an array', () => { - expect(parseAnalysisOutput({ ...valid, parties: 'Acme' })).toBeNull() + it('returns null for missing analysis', () => { + expect(parseAnalysisOutput({ recommendation: valid.recommendation })).toBeNull() }) - it('returns null when confidence is not a number', () => { - expect(parseAnalysisOutput({ ...valid, confidence: 'high' })).toBeNull() + it('returns null for missing recommendation', () => { + expect(parseAnalysisOutput({ analysis: valid.analysis })).toBeNull() }) - it('returns null for non-object input', () => { + it('returns null for non-objects', () => { + expect(parseAnalysisOutput(null)).toBeNull() expect(parseAnalysisOutput('string')).toBeNull() expect(parseAnalysisOutput(42)).toBeNull() }) diff --git a/apps/doc-approval/web-react/src/client/traverseOutput.ts b/apps/doc-approval/web-react/src/client/traverseOutput.ts index d172881..1c00b0f 100644 --- a/apps/doc-approval/web-react/src/client/traverseOutput.ts +++ b/apps/doc-approval/web-react/src/client/traverseOutput.ts @@ -1,28 +1,81 @@ -export interface DocApprovalOutput { +/** Analyze step fields (nested under pipeline `analysis`). */ +export interface AnalysisOutput { docType: string parties: string[] amounts: string[] - confidence: number + confidence: string recommendation: string } -export function parseAnalysisOutput(raw: unknown): DocApprovalOutput | null { +/** Recommend step fields (nested under pipeline `recommendation`). */ +export interface RecommendationOutput { + recommendation: string + rationale: string + confidence: string +} + +/** Combined pipeline final output (analyze → recommend). */ +export interface DocApprovalOutput { + analysis: AnalysisOutput + recommendation: RecommendationOutput +} + +function stringish(value: unknown): string | null { + if (typeof value === 'string') return value + if (typeof value === 'number' && Number.isFinite(value)) return String(value) + return null +} + +function parseAnalysis(raw: unknown): AnalysisOutput | null { if (!raw || typeof raw !== 'object') return null const o = raw as Record + const confidence = stringish(o.confidence) if ( typeof o.docType !== 'string' || !Array.isArray(o.parties) || !Array.isArray(o.amounts) || - typeof o.confidence !== 'number' || + confidence === null || typeof o.recommendation !== 'string' - ) return null - if (!o.parties.every(p => typeof p === 'string')) return null - if (!o.amounts.every(a => typeof a === 'string')) return null + ) { + return null + } + if (!o.parties.every((p) => typeof p === 'string')) return null + if (!o.amounts.every((a) => typeof a === 'string')) return null return { docType: o.docType, parties: o.parties as string[], amounts: o.amounts as string[], - confidence: o.confidence, + confidence, recommendation: o.recommendation, } } + +function parseRecommendation(raw: unknown): RecommendationOutput | null { + if (!raw || typeof raw !== 'object') return null + const o = raw as Record + const confidence = stringish(o.confidence) + if ( + typeof o.recommendation !== 'string' || + typeof o.rationale !== 'string' || + confidence === null + ) { + return null + } + return { + recommendation: o.recommendation, + rationale: o.rationale, + confidence, + } +} + +/** + * Parses pipeline namespaced final output (analyze → recommend). + */ +export function parseAnalysisOutput(raw: unknown): DocApprovalOutput | null { + if (!raw || typeof raw !== 'object') return null + const o = raw as Record + const analysis = parseAnalysis(o.analysis) + const recommendation = parseRecommendation(o.recommendation) + if (!analysis || !recommendation) return null + return { analysis, recommendation } +} diff --git a/apps/doc-approval/web-react/src/components/AnalysisResult.tsx b/apps/doc-approval/web-react/src/components/AnalysisResult.tsx index 768f40b..411d327 100644 --- a/apps/doc-approval/web-react/src/components/AnalysisResult.tsx +++ b/apps/doc-approval/web-react/src/components/AnalysisResult.tsx @@ -1,3 +1,4 @@ +import type { ReactNode } from 'react' import type { HostRunResult } from '../host/embeddedHost' interface AnalysisResultProps { @@ -26,7 +27,7 @@ export function AnalysisResult({ marginBottom: '16px', }} > -

Analysis Result

+

Pipeline Result

{showReset && ( {runtimeUnavailable && (

- Embedded runtime unavailable — doc-approval manifests require issue #112. + Embedded runtime unavailable — sync the doc-approval bundle (scripts/ci/sync_web_doc_approval_bundle.sh).

)} diff --git a/apps/doc-approval/web-react/src/host/embeddedHost.ts b/apps/doc-approval/web-react/src/host/embeddedHost.ts index 3b595cb..2d0e4da 100644 --- a/apps/doc-approval/web-react/src/host/embeddedHost.ts +++ b/apps/doc-approval/web-react/src/host/embeddedHost.ts @@ -35,7 +35,7 @@ export function createTestEmbedder(output: DocApprovalOutput): TraverseEmbedderA }).withTargetOutput(DEFAULT_WORKFLOW_ID, output as unknown as JsonValue) } -/** Returns null until manifests/doc-approval lands (#112). */ +/** BundleEmbedder.init; returns null when the local bundle cannot be loaded. */ export async function initProductionEmbedder( manifestPath = import.meta.env.VITE_DOC_APPROVAL_MANIFEST ?? DEFAULT_MANIFEST_PATH, ): Promise { diff --git a/apps/doc-approval/windows-winui/DocApproval.Tests/ExecutionViewModelTests.cs b/apps/doc-approval/windows-winui/DocApproval.Tests/ExecutionViewModelTests.cs index 94d66f1..ab36165 100644 --- a/apps/doc-approval/windows-winui/DocApproval.Tests/ExecutionViewModelTests.cs +++ b/apps/doc-approval/windows-winui/DocApproval.Tests/ExecutionViewModelTests.cs @@ -73,7 +73,9 @@ public async Task SubmitTransitionsToSucceeded() new ExecutionPollResult( "exec_test", "succeeded", - new DocApprovalOutput("invoice", ["A", "B"], ["$100"], 0.9, "approve"), + new DocApprovalOutput( + new AnalysisOutput("invoice", ["A", "B"], ["$100"], "0.9", "approve"), + new RecommendationOutput("approve", "Policy match", "high")), null), ], }; @@ -88,7 +90,7 @@ public async Task SubmitTransitionsToSucceeded() await Task.Delay(1500); Assert.Equal(ExecutionPhase.Succeeded, vm.Phase); - Assert.Equal("invoice", vm.Output?.DocType); + Assert.Equal("invoice", vm.Output?.Analysis.DocType); } [Fact] diff --git a/apps/doc-approval/windows-winui/DocApproval.Tests/TraverseClientTests.cs b/apps/doc-approval/windows-winui/DocApproval.Tests/TraverseClientTests.cs index 715b860..b181ff2 100644 --- a/apps/doc-approval/windows-winui/DocApproval.Tests/TraverseClientTests.cs +++ b/apps/doc-approval/windows-winui/DocApproval.Tests/TraverseClientTests.cs @@ -47,7 +47,7 @@ public async Task PollExecutionParsesOutput() { Content = new StringContent( """ - {"status":"succeeded","output":{"docType":"invoice","parties":["A"],"amounts":["$100"],"confidence":0.9,"recommendation":"approve"}} + {"status":"succeeded","output":{"analysis":{"docType":"invoice","parties":["A"],"amounts":["$100"],"confidence":"0.9","recommendation":"approve"},"recommendation":{"recommendation":"approve","rationale":"Policy match","confidence":"high"}}} """, Encoding.UTF8, "application/json"), @@ -55,6 +55,7 @@ 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("invoice", result.Output?.DocType); + Assert.Equal("invoice", result.Output?.Analysis.DocType); + Assert.Equal("approve", result.Output?.Recommendation.Recommendation); } } diff --git a/apps/doc-approval/windows-winui/DocApproval/AppConstants.cs b/apps/doc-approval/windows-winui/DocApproval/AppConstants.cs index 159a18e..d01a309 100644 --- a/apps/doc-approval/windows-winui/DocApproval/AppConstants.cs +++ b/apps/doc-approval/windows-winui/DocApproval/AppConstants.cs @@ -2,7 +2,7 @@ namespace DocApproval; public static class AppConstants { - public const string CapabilityId = "doc-approval.analyze"; + public const string CapabilityId = "doc-approval.pipeline"; public const string DefaultBaseUrl = "http://127.0.0.1:8787"; public const string DefaultWorkspace = "local-default"; } diff --git a/apps/doc-approval/windows-winui/DocApproval/ExecutionViewModel.cs b/apps/doc-approval/windows-winui/DocApproval/ExecutionViewModel.cs index 3c0d0fd..f5498b7 100644 --- a/apps/doc-approval/windows-winui/DocApproval/ExecutionViewModel.cs +++ b/apps/doc-approval/windows-winui/DocApproval/ExecutionViewModel.cs @@ -178,12 +178,7 @@ private async Task PollUntilTerminalAsync( trace = Array.Empty(); } - Output = result.Output ?? new DocApprovalOutput( - string.Empty, - Array.Empty(), - Array.Empty(), - 0, - string.Empty); + Output = result.Output ?? DocApprovalOutput.Empty; Trace = trace; Phase = ExecutionPhase.Succeeded; } diff --git a/apps/doc-approval/windows-winui/DocApproval/HomePage.xaml b/apps/doc-approval/windows-winui/DocApproval/HomePage.xaml index bacc68f..96a60b5 100644 --- a/apps/doc-approval/windows-winui/DocApproval/HomePage.xaml +++ b/apps/doc-approval/windows-winui/DocApproval/HomePage.xaml @@ -6,7 +6,7 @@ - + - + - + - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/doc-approval/windows-winui/DocApproval/HomePage.xaml.cs b/apps/doc-approval/windows-winui/DocApproval/HomePage.xaml.cs index c31a92b..f7d66f6 100644 --- a/apps/doc-approval/windows-winui/DocApproval/HomePage.xaml.cs +++ b/apps/doc-approval/windows-winui/DocApproval/HomePage.xaml.cs @@ -96,15 +96,18 @@ private void UpdateUi() break; case ExecutionPhase.Succeeded: OutputGrid.Visibility = Visibility.Visible; - DocTypeValue.Text = _viewModel.Output?.DocType ?? string.Empty; + DocTypeValue.Text = _viewModel.Output?.Analysis.DocType ?? string.Empty; PartiesValue.Text = _viewModel.Output is null ? string.Empty - : string.Join(", ", _viewModel.Output.Parties); + : string.Join(", ", _viewModel.Output.Analysis.Parties); AmountsValue.Text = _viewModel.Output is null ? string.Empty - : string.Join(", ", _viewModel.Output.Amounts); - ConfidenceValue.Text = _viewModel.Output?.Confidence.ToString() ?? string.Empty; - RecommendationValue.Text = _viewModel.Output?.Recommendation ?? string.Empty; + : string.Join(", ", _viewModel.Output.Analysis.Amounts); + AnalyzeConfidenceValue.Text = _viewModel.Output?.Analysis.Confidence ?? string.Empty; + AnalyzeRecommendationValue.Text = _viewModel.Output?.Analysis.Recommendation ?? string.Empty; + RecommendationValue.Text = _viewModel.Output?.Recommendation.Recommendation ?? string.Empty; + RationaleValue.Text = _viewModel.Output?.Recommendation.Rationale ?? string.Empty; + ConfidenceValue.Text = _viewModel.Output?.Recommendation.Confidence ?? string.Empty; if (_viewModel.Trace.Count > 0) { diff --git a/apps/doc-approval/windows-winui/DocApproval/Models.cs b/apps/doc-approval/windows-winui/DocApproval/Models.cs index 9c8356f..4e3c50f 100644 --- a/apps/doc-approval/windows-winui/DocApproval/Models.cs +++ b/apps/doc-approval/windows-winui/DocApproval/Models.cs @@ -3,13 +3,28 @@ namespace DocApproval; -public sealed record DocApprovalOutput( +public sealed record AnalysisOutput( [property: JsonPropertyName("docType")] string DocType, [property: JsonPropertyName("parties")] IReadOnlyList Parties, [property: JsonPropertyName("amounts")] IReadOnlyList Amounts, - [property: JsonPropertyName("confidence")] double Confidence, + [property: JsonPropertyName("confidence")] string Confidence, [property: JsonPropertyName("recommendation")] string Recommendation); +public sealed record RecommendationOutput( + [property: JsonPropertyName("recommendation")] string Recommendation, + [property: JsonPropertyName("rationale")] string Rationale, + [property: JsonPropertyName("confidence")] string Confidence); + +/// Combined pipeline final output (analyze → recommend). +public sealed record DocApprovalOutput( + [property: JsonPropertyName("analysis")] AnalysisOutput Analysis, + [property: JsonPropertyName("recommendation")] RecommendationOutput Recommendation) +{ + public static DocApprovalOutput Empty { get; } = new( + new AnalysisOutput(string.Empty, Array.Empty(), Array.Empty(), string.Empty, string.Empty), + new RecommendationOutput(string.Empty, string.Empty, string.Empty)); +} + public sealed record TraceEvent( [property: JsonPropertyName("event_type")] string EventType, string Timestamp, diff --git a/apps/doc-approval/windows-winui/README.md b/apps/doc-approval/windows-winui/README.md index 19c6a5c..04e0ad4 100644 --- a/apps/doc-approval/windows-winui/README.md +++ b/apps/doc-approval/windows-winui/README.md @@ -46,7 +46,7 @@ dotnet run --project DocApproval\DocApproval.csproj | `SettingsPage.xaml` | Runtime URL + workspace (`ApplicationData`) | | `MainWindow.xaml` | Navigation shell with title-bar health strip | -The UI renders runtime-provided output fields only (`docType`, `parties`, `amounts`, `confidence`, `recommendation`). +The UI renders runtime-provided pipeline fields only (`analysis.*`, `recommendation.*`). ## Phase 2 (not implemented) diff --git a/docs/design-language.md b/docs/design-language.md index 4b28865..8eaced2 100644 --- a/docs/design-language.md +++ b/docs/design-language.md @@ -75,13 +75,13 @@ The UI renders these fields exactly as the runtime provides them — never compu | Platform | Path | Status | |---|---|---| -| Web (React) | `apps/doc-approval/web-react/` | Shipped (embedded; bundle waits on #112) | -| iOS (SwiftUI) | `apps/doc-approval/ios-swift/` | Shipped | -| macOS (SwiftUI + AppKit) | `apps/doc-approval/macos-swift/` | Shipped | -| Android (Jetpack Compose) | `apps/doc-approval/android-compose/` | Shipped | -| Windows (WinUI 3) | `apps/doc-approval/windows-winui/` | Shipped | -| Linux (GTK4 + Rust) | `apps/doc-approval/linux-gtk/` | Shipped (embedded; bundle waits on #112) | -| CLI (Rust) | `apps/doc-approval/cli-rust/` | Shipped (embedded; bundle waits on #112) | +| Web (React) | `apps/doc-approval/web-react/` | Shipped (embedded + pipeline) | +| iOS (SwiftUI) | `apps/doc-approval/ios-swift/` | Shipped (pipeline via HTTP) | +| macOS (SwiftUI + AppKit) | `apps/doc-approval/macos-swift/` | Shipped (pipeline via HTTP) | +| Android (Jetpack Compose) | `apps/doc-approval/android-compose/` | Shipped (pipeline via HTTP) | +| Windows (WinUI 3) | `apps/doc-approval/windows-winui/` | Shipped (pipeline via HTTP) | +| Linux (GTK4 + Rust) | `apps/doc-approval/linux-gtk/` | Shipped (embedded + pipeline) | +| CLI (Rust) | `apps/doc-approval/cli-rust/` | Shipped (embedded + pipeline) | ### meeting-notes (list-type output) diff --git a/docs/embedded-runtime-plan.md b/docs/embedded-runtime-plan.md index 70d6e25..bd84157 100644 --- a/docs/embedded-runtime-plan.md +++ b/docs/embedded-runtime-plan.md @@ -65,9 +65,8 @@ Workflow `doc-approval.pipeline` chains: | Step | Capability ID | Notes | |---|---|---| -| 1 | `doc-approval.extract` | Traverse [#538](https://github.com/traverse-framework/Traverse/issues/538) | -| 2 | `doc-approval.analyze` | Existing client capability ID | -| 3 | `doc-approval.recommend` | New capability — approval recommendation | +| 1 | `doc-approval.analyze` | Existing analyze agent | +| 2 | `doc-approval.recommend` | Spec 069 final recommendation (no extract) | Both apps must run the same workflow on all seven platform clients once embedded runtime is available. @@ -127,9 +126,9 @@ Until (1–2) exist, platform migration tickets stay **Blocked**. | [Traverse #553](https://github.com/traverse-framework/Traverse/issues/553) | Implement spec 057 SDK | Blocked → Ready after 057 PR | | [Traverse #558](https://github.com/traverse-framework/Traverse/issues/558) | Implement spec 058 pipeline | Ready | | [Traverse #527](https://github.com/traverse-framework/Traverse/issues/527) | Implement spec 059 command dispatch | Blocked | -| [110](https://github.com/traverse-framework/reference-apps/issues/110) | traverse-starter.pipeline multi-capability workflow | Blocked | -| [111](https://github.com/traverse-framework/reference-apps/issues/111) | doc-approval.pipeline multi-capability workflow | Blocked | -| [112](https://github.com/traverse-framework/reference-apps/issues/112) | doc-approval manifests | Blocked | +| [110](https://github.com/traverse-framework/reference-apps/issues/110) | traverse-starter.pipeline multi-capability workflow | Done | +| [111](https://github.com/traverse-framework/reference-apps/issues/111) | doc-approval.pipeline multi-capability workflow | Done | +| [112](https://github.com/traverse-framework/reference-apps/issues/112) | doc-approval manifests | Done | | [113](https://github.com/traverse-framework/reference-apps/issues/113) | Embed runtime — web-react | Done | | [114](https://github.com/traverse-framework/reference-apps/issues/114) | Embed runtime — Swift (iOS + macOS) | Blocked | | [115](https://github.com/traverse-framework/reference-apps/issues/115) | Embed runtime — Android | Blocked | diff --git a/scripts/ci/repository_checks.sh b/scripts/ci/repository_checks.sh index e008037..33c412e 100644 --- a/scripts/ci/repository_checks.sh +++ b/scripts/ci/repository_checks.sh @@ -81,6 +81,8 @@ check "scripts/ci/coverage_gate.sh" "Coverage gate" check "scripts/ci/phase1_smoke.sh" "Phase 1 smoke" check "scripts/ci/phase2_smoke.sh" "Phase 2 smoke" check "scripts/ci/phase2_link_traverse.sh" "Phase 2 Traverse link" +check "scripts/ci/sync_web_starter_bundle.sh" "Web starter bundle sync" +check "scripts/ci/sync_web_doc_approval_bundle.sh" "Web doc-approval bundle sync" check "scripts/ci/onboarding_check.sh" "Onboarding check" # GitHub Actions diff --git a/scripts/ci/sync_web_doc_approval_bundle.sh b/scripts/ci/sync_web_doc_approval_bundle.sh new file mode 100755 index 0000000..d93bb9f --- /dev/null +++ b/scripts/ci/sync_web_doc_approval_bundle.sh @@ -0,0 +1,31 @@ +#!/usr/bin/env bash +# Sync doc-approval application bundle into web-react/public for FetchBundleLoader. +# Requires TRAVERSE_REPO (or sibling ../Traverse) with example WASM + workflows. +set -euo pipefail + +REPO_ROOT="$(cd "$(dirname "$0")/../.." && pwd)" +TRAVERSE_REPO="${TRAVERSE_REPO:-$REPO_ROOT/../Traverse}" +DEST="$REPO_ROOT/apps/doc-approval/web-react/public/bundles/doc-approval" +MANIFEST_SRC="$REPO_ROOT/manifests/doc-approval" + +if [ ! -d "$TRAVERSE_REPO/examples/doc-approval" ]; then + echo "FAIL: TRAVERSE_REPO examples missing: $TRAVERSE_REPO" + exit 1 +fi + +rm -rf "$DEST" +mkdir -p "$DEST/components/analyze" "$DEST/components/recommend" +mkdir -p "$DEST/_traverse/examples/doc-approval" +mkdir -p "$DEST/_traverse/examples/applications/doc-approval" +mkdir -p "$DEST/_traverse/workflows/examples/doc-approval" +mkdir -p "$DEST/_traverse/contracts/examples/doc-approval" + +cp "$MANIFEST_SRC/app.manifest.json" "$DEST/" +cp "$MANIFEST_SRC/components/analyze/component.manifest.json" "$DEST/components/analyze/" +cp "$MANIFEST_SRC/components/recommend/component.manifest.json" "$DEST/components/recommend/" + +rsync -a "$TRAVERSE_REPO/examples/doc-approval/" "$DEST/_traverse/examples/doc-approval/" +rsync -a "$TRAVERSE_REPO/workflows/examples/doc-approval/" "$DEST/_traverse/workflows/examples/doc-approval/" +rsync -a "$TRAVERSE_REPO/contracts/examples/doc-approval/" "$DEST/_traverse/contracts/examples/doc-approval/" + +echo "OK: synced bundle → $DEST"