Skip to content

Commit 5c54581

Browse files
Ship doc-approval.pipeline multi-capability showcase (#111) (#155)
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 <cursoragent@cursor.com>
1 parent 0842f7b commit 5c54581

45 files changed

Lines changed: 640 additions & 310 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

AGENTS.md

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,8 @@ gh project item-list 2 --owner traverse-framework --format json --limit 300 \
1212
### Blocked work summary
1313

1414
- **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.
15-
- **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).
1615

17-
Ready: [#111](https://github.com/traverse-framework/reference-apps/issues/111) (after #112 merges).
16+
Ready: (none — claim from Project 2 when new Ready items appear).
1817

1918
Update this section when a PR changes platform status (see PR template checklist).
2019

README.md

Lines changed: 10 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ Developer tool for browsing execution traces — not a domain workflow app.
7676
| App | Purpose | Web path | Default capability |
7777
|---|---|---|---|
7878
| traverse-starter | Flat string output from a short note | [`apps/traverse-starter/web-react/`](apps/traverse-starter/web-react/) | `traverse-starter.process` |
79-
| doc-approval | Document analysis submitter | [`apps/doc-approval/web-react/`](apps/doc-approval/web-react/) | `doc-approval.analyze` |
79+
| doc-approval | Document analysis submitter | [`apps/doc-approval/web-react/`](apps/doc-approval/web-react/) | `doc-approval.pipeline` |
8080
| meeting-notes | List-type output from a transcript | [`apps/meeting-notes/web-react/`](apps/meeting-notes/web-react/) | `meeting-notes.process` |
8181
| trace-explorer | Execution timeline debugger | [`apps/trace-explorer/web-react/`](apps/trace-explorer/web-react/) ||
8282

@@ -116,13 +116,13 @@ All clients are **native UI shells** separated from business logic. Phase 1/2 us
116116

117117
| Platform | Status | Path |
118118
|---|---|---|
119-
| Web (React + TypeScript) | Shipped (embedded; bundle waits on #112) | [`apps/doc-approval/web-react/`](apps/doc-approval/web-react/) |
120-
| iOS (SwiftUI) | Shipped | [`apps/doc-approval/ios-swift/`](apps/doc-approval/ios-swift/) |
121-
| macOS (SwiftUI + AppKit) | Shipped | [`apps/doc-approval/macos-swift/`](apps/doc-approval/macos-swift/) |
122-
| Android (Jetpack Compose) | Shipped | [`apps/doc-approval/android-compose/`](apps/doc-approval/android-compose/) |
123-
| Windows (WinUI 3) | Shipped | [`apps/doc-approval/windows-winui/`](apps/doc-approval/windows-winui/) |
124-
| Linux (GTK4 + Rust) | Shipped (embedded; bundle waits on #112) | [`apps/doc-approval/linux-gtk/`](apps/doc-approval/linux-gtk/) |
125-
| CLI (Rust) | Shipped (embedded; bundle waits on #112) | [`apps/doc-approval/cli-rust/`](apps/doc-approval/cli-rust/) |
119+
| Web (React + TypeScript) | Shipped (embedded + pipeline) | [`apps/doc-approval/web-react/`](apps/doc-approval/web-react/) |
120+
| iOS (SwiftUI) | Shipped (pipeline via HTTP) | [`apps/doc-approval/ios-swift/`](apps/doc-approval/ios-swift/) |
121+
| macOS (SwiftUI + AppKit) | Shipped (pipeline via HTTP) | [`apps/doc-approval/macos-swift/`](apps/doc-approval/macos-swift/) |
122+
| Android (Jetpack Compose) | Shipped (pipeline via HTTP) | [`apps/doc-approval/android-compose/`](apps/doc-approval/android-compose/) |
123+
| Windows (WinUI 3) | Shipped (pipeline via HTTP) | [`apps/doc-approval/windows-winui/`](apps/doc-approval/windows-winui/) |
124+
| Linux (GTK4 + Rust) | Shipped (embedded + pipeline) | [`apps/doc-approval/linux-gtk/`](apps/doc-approval/linux-gtk/) |
125+
| CLI (Rust) | Shipped (embedded + pipeline) | [`apps/doc-approval/cli-rust/`](apps/doc-approval/cli-rust/) |
126126

127127
### meeting-notes (list-type output)
128128

@@ -171,7 +171,6 @@ Checks 1–5 validate Node, install, typecheck, lint, and tests (no runtime requ
171171

172172
Active blockers on [Project 2](https://github.com/orgs/traverse-framework/projects/2):
173173

174-
- **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.
175-
- **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).
174+
- **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.
176175

177-
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).
176+
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).

apps/doc-approval/DocApprovalCore/Sources/DocApprovalCore/DocApprovalOutput.swift

Lines changed: 84 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,17 @@
11
import Foundation
22

3-
public struct DocApprovalOutput: Equatable, Sendable, Codable {
3+
public struct AnalysisOutput: Equatable, Sendable, Codable {
44
public let docType: String
55
public let parties: [String]
66
public let amounts: [String]
7-
public let confidence: Double
7+
public let confidence: String
88
public let recommendation: String
99

1010
public init(
1111
docType: String,
1212
parties: [String],
1313
amounts: [String],
14-
confidence: Double,
14+
confidence: String,
1515
recommendation: String
1616
) {
1717
self.docType = docType
@@ -22,6 +22,44 @@ public struct DocApprovalOutput: Equatable, Sendable, Codable {
2222
}
2323
}
2424

25+
public struct RecommendationOutput: Equatable, Sendable, Codable {
26+
public let recommendation: String
27+
public let rationale: String
28+
public let confidence: String
29+
30+
public init(recommendation: String, rationale: String, confidence: String) {
31+
self.recommendation = recommendation
32+
self.rationale = rationale
33+
self.confidence = confidence
34+
}
35+
}
36+
37+
/// Combined pipeline final output (analyze → recommend).
38+
public struct DocApprovalOutput: Equatable, Sendable, Codable {
39+
public let analysis: AnalysisOutput
40+
public let recommendation: RecommendationOutput
41+
42+
public init(analysis: AnalysisOutput, recommendation: RecommendationOutput) {
43+
self.analysis = analysis
44+
self.recommendation = recommendation
45+
}
46+
47+
public static let empty = DocApprovalOutput(
48+
analysis: AnalysisOutput(
49+
docType: "",
50+
parties: [],
51+
amounts: [],
52+
confidence: "",
53+
recommendation: ""
54+
),
55+
recommendation: RecommendationOutput(
56+
recommendation: "",
57+
rationale: "",
58+
confidence: ""
59+
)
60+
)
61+
}
62+
2563
public struct TraceEvent: Equatable, Sendable, Codable {
2664
public let event_type: String
2765
public let timestamp: String
@@ -155,29 +193,11 @@ public enum JSONValue: Equatable, Sendable, Codable {
155193
public enum DocApprovalOutputParser {
156194
public static func parse(_ raw: Any?) -> DocApprovalOutput? {
157195
guard let dict = raw as? [String: Any],
158-
let docType = dict["docType"] as? String,
159-
let parties = dict["parties"] as? [String],
160-
let amounts = dict["amounts"] as? [String],
161-
let recommendation = dict["recommendation"] as? String else {
196+
let analysis = parseAnalysis(dict["analysis"]),
197+
let recommendation = parseRecommendation(dict["recommendation"]) else {
162198
return nil
163199
}
164-
let confidence: Double
165-
if let value = dict["confidence"] as? Double {
166-
confidence = value
167-
} else if let value = dict["confidence"] as? Int {
168-
confidence = Double(value)
169-
} else if let value = dict["confidence"] as? NSNumber {
170-
confidence = value.doubleValue
171-
} else {
172-
return nil
173-
}
174-
return DocApprovalOutput(
175-
docType: docType,
176-
parties: parties,
177-
amounts: amounts,
178-
confidence: confidence,
179-
recommendation: recommendation
180-
)
200+
return DocApprovalOutput(analysis: analysis, recommendation: recommendation)
181201
}
182202

183203
public static func parseEventPayload(_ raw: Any?) -> AppStateEventPayload? {
@@ -222,4 +242,44 @@ public enum DocApprovalOutputParser {
222242
)
223243
}
224244
}
245+
246+
private static func parseAnalysis(_ raw: Any?) -> AnalysisOutput? {
247+
guard let dict = raw as? [String: Any],
248+
let docType = dict["docType"] as? String,
249+
let parties = dict["parties"] as? [String],
250+
let amounts = dict["amounts"] as? [String],
251+
let confidence = stringish(dict["confidence"]),
252+
let recommendation = dict["recommendation"] as? String else {
253+
return nil
254+
}
255+
return AnalysisOutput(
256+
docType: docType,
257+
parties: parties,
258+
amounts: amounts,
259+
confidence: confidence,
260+
recommendation: recommendation
261+
)
262+
}
263+
264+
private static func parseRecommendation(_ raw: Any?) -> RecommendationOutput? {
265+
guard let dict = raw as? [String: Any],
266+
let recommendation = dict["recommendation"] as? String,
267+
let rationale = dict["rationale"] as? String,
268+
let confidence = stringish(dict["confidence"]) else {
269+
return nil
270+
}
271+
return RecommendationOutput(
272+
recommendation: recommendation,
273+
rationale: rationale,
274+
confidence: confidence
275+
)
276+
}
277+
278+
private static func stringish(_ raw: Any?) -> String? {
279+
if let value = raw as? String { return value }
280+
if let value = raw as? Double { return String(value) }
281+
if let value = raw as? Int { return String(value) }
282+
if let value = raw as? NSNumber { return value.stringValue }
283+
return nil
284+
}
225285
}

apps/doc-approval/DocApprovalCore/Tests/DocApprovalCoreTests/DocApprovalCoreTests.swift

Lines changed: 41 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -130,15 +130,23 @@ final class DocApprovalClientTests: XCTestCase {
130130
final class DocApprovalOutputTests: XCTestCase {
131131
func testParseOutput() {
132132
let raw: [String: Any] = [
133-
"docType": "nda",
134-
"parties": ["A", "B"],
135-
"amounts": ["$1"],
136-
"confidence": 0.9,
137-
"recommendation": "approve",
133+
"analysis": [
134+
"docType": "nda",
135+
"parties": ["A", "B"],
136+
"amounts": ["$1"],
137+
"confidence": 0.9,
138+
"recommendation": "approve",
139+
],
140+
"recommendation": [
141+
"recommendation": "approve",
142+
"rationale": "Policy match",
143+
"confidence": "high",
144+
],
138145
]
139146
let output = DocApprovalOutputParser.parse(raw)
140-
XCTAssertEqual(output?.docType, "nda")
141-
XCTAssertEqual(output?.recommendation, "approve")
147+
XCTAssertEqual(output?.analysis.docType, "nda")
148+
XCTAssertEqual(output?.recommendation.recommendation, "approve")
149+
XCTAssertEqual(output?.analysis.confidence, "0.9")
142150
}
143151

144152
func testParseEventPayload() {
@@ -147,16 +155,23 @@ final class DocApprovalOutputTests: XCTestCase {
147155
"session_id": "sess-1",
148156
"execution_id": "exec-1",
149157
"output": [
150-
"docType": "nda",
151-
"parties": [] as [String],
152-
"amounts": [] as [String],
153-
"confidence": 0.5,
154-
"recommendation": "review",
158+
"analysis": [
159+
"docType": "nda",
160+
"parties": [] as [String],
161+
"amounts": [] as [String],
162+
"confidence": 0.5,
163+
"recommendation": "review",
164+
],
165+
"recommendation": [
166+
"recommendation": "review",
167+
"rationale": "Needs human",
168+
"confidence": "medium",
169+
],
155170
],
156171
]
157172
let payload = DocApprovalOutputParser.parseEventPayload(raw)
158173
XCTAssertEqual(payload?.state, "results")
159-
XCTAssertEqual(payload?.output?.docType, "nda")
174+
XCTAssertEqual(payload?.output?.analysis.docType, "nda")
160175
}
161176

162177
func testParseSessionsArray() {
@@ -260,16 +275,23 @@ final class AppStateViewModelTests: XCTestCase {
260275
sessionId: "sess-1",
261276
executionId: "exec-1",
262277
output: DocApprovalOutput(
263-
docType: "nda",
264-
parties: [],
265-
amounts: [],
266-
confidence: 0.8,
267-
recommendation: "approve"
278+
analysis: AnalysisOutput(
279+
docType: "nda",
280+
parties: [],
281+
amounts: [],
282+
confidence: "0.8",
283+
recommendation: "approve"
284+
),
285+
recommendation: RecommendationOutput(
286+
recommendation: "approve",
287+
rationale: "Policy match",
288+
confidence: "high"
289+
)
268290
)
269291
)
270292
)
271293
XCTAssertEqual(vm.currentState, "results")
272-
XCTAssertEqual(vm.output?.docType, "nda")
294+
XCTAssertEqual(vm.output?.analysis.docType, "nda")
273295
}
274296

275297
func testCanSubmitWhenOnlineWithDocument() async {

apps/doc-approval/android-compose/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ Open the project in Android Studio and run on an API 26+ emulator.
4343
| `ui/SettingsScreen.kt` | Runtime URL + workspace |
4444
| `SettingsRepository.kt` | DataStore persistence |
4545

46-
The UI renders runtime-provided output fields only (`docType`, `parties`, `amounts`, `confidence`, `recommendation`).
46+
The UI renders runtime-provided pipeline fields only (`analysis.*`, `recommendation.*`).
4747

4848
## Phase 2 (not implemented)
4949

apps/doc-approval/android-compose/app/src/main/java/com/traverseframework/docapproval/ExecutionViewModel.kt

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -106,13 +106,7 @@ class ExecutionViewModel(
106106
} catch (_: Exception) {
107107
emptyList()
108108
}
109-
val output = result.output ?: DocApprovalOutput(
110-
docType = "",
111-
parties = emptyList(),
112-
amounts = emptyList(),
113-
confidence = 0.0,
114-
recommendation = "",
115-
)
109+
val output = result.output ?: DocApprovalOutput.EMPTY
116110
_uiState.update { it.copy(phase = ExecutionPhase.Succeeded(output, trace)) }
117111
return
118112
}

apps/doc-approval/android-compose/app/src/main/java/com/traverseframework/docapproval/Models.kt

Lines changed: 34 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,45 @@ package com.traverseframework.docapproval
33
import kotlinx.serialization.json.JsonElement
44

55
@kotlinx.serialization.Serializable
6-
data class DocApprovalOutput(
6+
data class AnalysisOutput(
77
val docType: String,
88
val parties: List<String>,
99
val amounts: List<String>,
10-
val confidence: Double,
10+
val confidence: String,
11+
val recommendation: String,
12+
)
13+
14+
@kotlinx.serialization.Serializable
15+
data class RecommendationOutput(
1116
val recommendation: String,
17+
val rationale: String,
18+
val confidence: String,
1219
)
1320

21+
/** Combined pipeline final output (analyze → recommend). */
22+
@kotlinx.serialization.Serializable
23+
data class DocApprovalOutput(
24+
val analysis: AnalysisOutput,
25+
val recommendation: RecommendationOutput,
26+
) {
27+
companion object {
28+
val EMPTY = DocApprovalOutput(
29+
analysis = AnalysisOutput(
30+
docType = "",
31+
parties = emptyList(),
32+
amounts = emptyList(),
33+
confidence = "",
34+
recommendation = "",
35+
),
36+
recommendation = RecommendationOutput(
37+
recommendation = "",
38+
rationale = "",
39+
confidence = "",
40+
),
41+
)
42+
}
43+
}
44+
1445
@kotlinx.serialization.Serializable
1546
data class TraceEvent(
1647
val event_type: String,
@@ -39,7 +70,7 @@ data class ExecutionPollResult(
3970
)
4071

4172
object AppConstants {
42-
const val CAPABILITY_ID = "doc-approval.analyze"
73+
const val CAPABILITY_ID = "doc-approval.pipeline"
4374
const val DEFAULT_BASE_URL = "http://10.0.2.2:8787"
4475
const val DEFAULT_WORKSPACE = "local-default"
4576
}

apps/doc-approval/android-compose/app/src/main/java/com/traverseframework/docapproval/ui/MainScreen.kt

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -168,11 +168,17 @@ private fun OutputCard(
168168

169169
@Composable
170170
private fun OutputFields(output: DocApprovalOutput) {
171-
Field("Document type", output.docType)
172-
Field("Parties", output.parties.joinToString(", "))
173-
Field("Amounts", output.amounts.joinToString(", "))
174-
Field("Confidence", output.confidence.toString())
175-
Field("Recommendation", output.recommendation)
171+
Text("Analysis", style = MaterialTheme.typography.titleSmall)
172+
Field("Document type", output.analysis.docType)
173+
Field("Parties", output.analysis.parties.joinToString(", "))
174+
Field("Amounts", output.analysis.amounts.joinToString(", "))
175+
Field("Analyze confidence", output.analysis.confidence)
176+
Field("Analyze recommendation", output.analysis.recommendation)
177+
Spacer(modifier = Modifier.height(8.dp))
178+
Text("Recommendation", style = MaterialTheme.typography.titleSmall)
179+
Field("Decision", output.recommendation.recommendation)
180+
Field("Rationale", output.recommendation.rationale)
181+
Field("Confidence", output.recommendation.confidence)
176182
}
177183

178184
@Composable

0 commit comments

Comments
 (0)