Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).

Expand Down
21 changes: 10 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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/) | — |

Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -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).
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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? {
Expand Down Expand Up @@ -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
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand All @@ -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() {
Expand Down Expand Up @@ -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 {
Expand Down
2 changes: 1 addition & 1 deletion apps/doc-approval/android-compose/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<String>,
val amounts: List<String>,
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,
Expand Down Expand Up @@ -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"
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading
Loading