zeno-mobile-runner 0.1.2 → 0.1.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (89) hide show
  1. package/CHANGELOG.md +162 -3
  2. package/FEATURES.md +50 -7
  3. package/README.md +133 -7
  4. package/build.zig.zon +3 -3
  5. package/clients/README.md +60 -3
  6. package/clients/go/README.md +12 -0
  7. package/clients/go/zmr/client.go +142 -0
  8. package/clients/kotlin/README.md +18 -1
  9. package/clients/kotlin/build.gradle.kts +1 -1
  10. package/clients/kotlin/src/main/kotlin/dev/zmr/ZmrClient.kt +76 -1
  11. package/clients/python/README.md +19 -0
  12. package/clients/python/pyproject.toml +1 -1
  13. package/clients/python/zmr_client.py +33 -0
  14. package/clients/rust/Cargo.lock +1 -1
  15. package/clients/rust/Cargo.toml +1 -1
  16. package/clients/rust/README.md +25 -1
  17. package/clients/rust/src/lib.rs +201 -0
  18. package/clients/swift/README.md +18 -0
  19. package/clients/swift/Sources/ZMRClient/ZMRClient.swift +82 -0
  20. package/clients/typescript/README.md +16 -0
  21. package/clients/typescript/index.d.ts +12 -0
  22. package/clients/typescript/index.mjs +16 -0
  23. package/clients/typescript/package.json +1 -1
  24. package/docs/agent-discovery.md +202 -0
  25. package/docs/ai-agents.md +87 -6
  26. package/docs/benchmarking.md +10 -3
  27. package/docs/clients.md +10 -6
  28. package/docs/demo.md +4 -0
  29. package/docs/expo-smoke.md +79 -0
  30. package/docs/install.md +3 -2
  31. package/docs/npm.md +58 -4
  32. package/docs/production-readiness.md +123 -0
  33. package/docs/protocol-fixtures/core-session.responses.jsonl +1 -1
  34. package/docs/protocol.md +215 -16
  35. package/docs/scenario-authoring.md +3 -0
  36. package/docs/troubleshooting.md +1 -1
  37. package/npm/agents.mjs +16 -0
  38. package/npm/build-zmr.mjs +1 -1
  39. package/npm/commands.mjs +9 -5
  40. package/npm/postinstall.mjs +28 -2
  41. package/npm/verify-publish.mjs +36 -0
  42. package/package.json +2 -1
  43. package/prebuilds/darwin-arm64/zmr +0 -0
  44. package/prebuilds/darwin-x64/zmr +0 -0
  45. package/prebuilds/linux-arm64/zmr +0 -0
  46. package/prebuilds/linux-x64/zmr +0 -0
  47. package/schemas/README.md +4 -0
  48. package/schemas/discover-output.schema.json +83 -0
  49. package/schemas/draft-output.schema.json +58 -0
  50. package/schemas/explore-output.schema.json +94 -0
  51. package/schemas/inspect-output.schema.json +88 -0
  52. package/schemas/run-output.schema.json +2 -0
  53. package/scripts/install-ios-shim.sh +79 -14
  54. package/scripts/release-readiness.py +43 -0
  55. package/scripts/run-android-pilot.sh +35 -9
  56. package/scripts/run-ios-pilot.sh +11 -4
  57. package/shims/ios/ZMRShim.swift +3 -0
  58. package/shims/ios/ZMRShimUITestCase.swift +41 -11
  59. package/skills/zmr-mobile-testing/SKILL.md +28 -3
  60. package/src/cli_discover.zig +239 -0
  61. package/src/cli_draft.zig +924 -0
  62. package/src/cli_explore.zig +136 -0
  63. package/src/cli_inspect.zig +310 -0
  64. package/src/cli_output.zig +26 -2
  65. package/src/cli_run.zig +28 -0
  66. package/src/cli_trace.zig +8 -0
  67. package/src/errors.zig +9 -0
  68. package/src/ios.zig +11 -4
  69. package/src/ios_lifecycle.zig +36 -0
  70. package/src/ios_shim.zig +42 -0
  71. package/src/json_rpc_methods.zig +85 -11
  72. package/src/json_rpc_params.zig +8 -0
  73. package/src/json_rpc_protocol.zig +1 -1
  74. package/src/json_rpc_trace.zig +112 -0
  75. package/src/main.zig +24 -2
  76. package/src/mcp.zig +209 -6
  77. package/src/mcp_protocol.zig +29 -1
  78. package/src/mcp_trace.zig +126 -4
  79. package/src/report.zig +186 -0
  80. package/src/runner.zig +26 -4
  81. package/src/runner_actions.zig +10 -0
  82. package/src/runner_diagnostics.zig +31 -1
  83. package/src/runner_events.zig +70 -7
  84. package/src/runner_native.zig +17 -1
  85. package/src/runner_waits.zig +82 -19
  86. package/src/scaffold.zig +28 -12
  87. package/src/scenario.zig +32 -4
  88. package/src/schema_registry.zig +4 -0
  89. package/src/version.zig +1 -1
@@ -156,6 +156,11 @@ run() {
156
156
  fi
157
157
  }
158
158
 
159
+ run_zmr_report() {
160
+ local trace_dir="$1"
161
+ run "$ZMR_BIN" report "$trace_dir" --out "$trace_dir/report.html" --junit "$trace_dir/junit.xml"
162
+ }
163
+
159
164
  is_retryable_simctl_text() {
160
165
  local text="$1"
161
166
  [[ "$text" == *"CoreSimulatorService connection became invalid"* ]] ||
@@ -455,7 +460,7 @@ if [[ "$RUNS" -eq 1 ]]; then
455
460
  else
456
461
  run "$ZMR_BIN" run examples/ios-smoke.json --platform ios --ios-device-type "$IOS_DEVICE_TYPE" --device "$DEVICE" --app-id "$APP_ID" --xcrun "$XCRUN" --trace-dir "$TRACE_DIR"
457
462
  fi
458
- run "$ZMR_BIN" report "$TRACE_DIR" --out "$TRACE_DIR/report.html"
463
+ run_zmr_report "$TRACE_DIR"
459
464
  run "$ZMR_BIN" export "$TRACE_DIR" --out "$TRACE_ROOT/ios-smoke.zmrtrace"
460
465
  run "$ZMR_BIN" export "$TRACE_DIR" --out "$TRACE_ROOT/ios-smoke-redacted.zmrtrace" --redact
461
466
 
@@ -463,7 +468,7 @@ if [[ "$RUNS" -eq 1 ]]; then
463
468
  SHIM_TRACE_DIR="$TRACE_ROOT/ios-shim-smoke"
464
469
  run rm -rf "$SHIM_TRACE_DIR"
465
470
  run "$ZMR_BIN" run examples/ios-shim-smoke.json --platform ios --ios-device-type "$IOS_DEVICE_TYPE" --device "$DEVICE" --app-id "$APP_ID" --xcrun "$XCRUN" --ios-shim "$IOS_SHIM" --trace-dir "$SHIM_TRACE_DIR"
466
- run "$ZMR_BIN" report "$SHIM_TRACE_DIR" --out "$SHIM_TRACE_DIR/report.html"
471
+ run_zmr_report "$SHIM_TRACE_DIR"
467
472
  run "$ZMR_BIN" export "$SHIM_TRACE_DIR" --out "$TRACE_ROOT/ios-shim-smoke.zmrtrace"
468
473
  run "$ZMR_BIN" export "$SHIM_TRACE_DIR" --out "$TRACE_ROOT/ios-shim-smoke-redacted.zmrtrace" --redact
469
474
  fi
@@ -481,11 +486,11 @@ else
481
486
  else
482
487
  ZMR_BIN="$ZMR_BIN" run "$ROOT/scripts/benchmark.sh" --zmr examples/ios-smoke.json --device "$DEVICE" --platform ios --ios-device-type "$IOS_DEVICE_TYPE" --app-id "$APP_ID" --xcrun "$XCRUN" --runs "$RUNS" --trace-root "$TRACE_ROOT/ios-smoke-benchmark" "${benchmark_gate_args[@]}"
483
488
  fi
484
- run "$ZMR_BIN" report "$TRACE_ROOT/ios-smoke-benchmark" --out "$TRACE_ROOT/ios-smoke-benchmark/report.html"
489
+ run_zmr_report "$TRACE_ROOT/ios-smoke-benchmark"
485
490
 
486
491
  if [[ -n "$IOS_SHIM" ]]; then
487
492
  ZMR_BIN="$ZMR_BIN" run "$ROOT/scripts/benchmark.sh" --zmr examples/ios-shim-smoke.json --device "$DEVICE" --platform ios --ios-device-type "$IOS_DEVICE_TYPE" --app-id "$APP_ID" --xcrun "$XCRUN" --ios-shim "$IOS_SHIM" --runs "$RUNS" --trace-root "$TRACE_ROOT/ios-shim-smoke-benchmark" "${benchmark_gate_args[@]}"
488
- run "$ZMR_BIN" report "$TRACE_ROOT/ios-shim-smoke-benchmark" --out "$TRACE_ROOT/ios-shim-smoke-benchmark/report.html"
493
+ run_zmr_report "$TRACE_ROOT/ios-shim-smoke-benchmark"
489
494
  fi
490
495
  fi
491
496
 
@@ -501,8 +506,10 @@ if [[ "$RUNS" -eq 1 ]]; then
501
506
  else
502
507
  echo "Benchmark reports:"
503
508
  echo " $TRACE_ROOT/ios-smoke-benchmark/report.html"
509
+ echo " $TRACE_ROOT/ios-smoke-benchmark/junit.xml"
504
510
  if [[ -n "$IOS_SHIM" ]]; then
505
511
  echo " $TRACE_ROOT/ios-shim-smoke-benchmark/report.html"
512
+ echo " $TRACE_ROOT/ios-shim-smoke-benchmark/junit.xml"
506
513
  fi
507
514
  fi
508
515
  echo "Viewer:"
@@ -58,6 +58,9 @@ enum ZMRShim {
58
58
  guard nodes.count < 256 else {
59
59
  return nodes
60
60
  }
61
+ guard element.exists else {
62
+ continue
63
+ }
61
64
  nodes.append(node(index: nodes.count, type: type, element: element))
62
65
  }
63
66
  }
@@ -128,7 +128,7 @@ final class ZMRShimUITestCase: XCTestCase {
128
128
  guard isFastQueryable(parts: parts) else {
129
129
  return error("selector.unsupported", "unsupported query selector: \(selector)")
130
130
  }
131
- let element = resolveFastElement(selector: selector, app: app, preferredTypes: [])
131
+ let element = resolveElement(selector: selector, app: app, preferredTypes: [])
132
132
  return [
133
133
  "status": "ok",
134
134
  "exists": element?.exists ?? false,
@@ -328,19 +328,49 @@ final class ZMRShimUITestCase: XCTestCase {
328
328
  return fast
329
329
  }
330
330
 
331
- let matchedElements = app.descendants(matching: .any).allElementsBoundByIndex.filter { element in
332
- matches(selector: selector, element: element)
333
- }
334
- if let preferred = matchedElements.first(where: { preferredTypes.contains($0.elementType) && $0.isHittable }) {
335
- return preferred
331
+ return resolveBroadElement(selector: selector, app: app)
332
+ }
333
+
334
+ private func resolveBroadElement(selector: String, app: XCUIApplication) -> XCUIElement? {
335
+ guard let parts = selectorParts(selector) else {
336
+ return nil
336
337
  }
337
- if let preferred = matchedElements.first(where: { preferredTypes.contains($0.elementType) }) {
338
- return preferred
338
+
339
+ let query: XCUIElementQuery?
340
+ switch parts.field {
341
+ case "text", "label":
342
+ let predicate = parts.contains
343
+ ? NSPredicate(format: "label CONTAINS[c] %@", parts.value)
344
+ : NSPredicate(format: "label == %@", parts.value)
345
+ query = app.descendants(matching: .any).matching(predicate)
346
+ case "identifier", "resourceId":
347
+ let predicate = parts.contains
348
+ ? NSPredicate(format: "identifier CONTAINS[c] %@", parts.value)
349
+ : NSPredicate(format: "identifier == %@", parts.value)
350
+ query = app.descendants(matching: .any).matching(predicate)
351
+ case "value":
352
+ let predicate = parts.contains
353
+ ? NSPredicate(format: "value CONTAINS[c] %@", parts.value)
354
+ : NSPredicate(format: "value == %@", parts.value)
355
+ query = app.descendants(matching: .any).matching(predicate)
356
+ case "id":
357
+ if parts.value.hasPrefix("id:") {
358
+ let identifier = String(parts.value.dropFirst("id:".count))
359
+ query = app.descendants(matching: .any).matching(identifier: identifier)
360
+ } else if parts.value.hasPrefix("label:") {
361
+ let label = String(parts.value.dropFirst("label:".count))
362
+ query = app.descendants(matching: .any).matching(NSPredicate(format: "label == %@", label))
363
+ } else {
364
+ query = nil
365
+ }
366
+ default:
367
+ query = nil
339
368
  }
340
- if let hittable = matchedElements.first(where: { $0.isHittable }) {
341
- return hittable
369
+
370
+ guard let element = query?.firstMatch, element.exists else {
371
+ return nil
342
372
  }
343
- return matchedElements.first
373
+ return element
344
374
  }
345
375
 
346
376
  private func resolveFastElement(selector: String, app: XCUIApplication, preferredTypes: [XCUIElement.ElementType]) -> XCUIElement? {
@@ -57,9 +57,34 @@ zmr mcp --config .zmr/config.json --trace-dir traces/zmr-agent
57
57
  ```
58
58
 
59
59
  Use the `semantic_snapshot`, `tap`, `type`, `wait_visible`, `trace_events`, and
60
- `trace_export` tools. Prefer `semantic_snapshot` because it normalizes Android
61
- and iOS hierarchy classes into roles, selectors, bounds, and recommended
62
- actions.
60
+ `trace_explore`, `trace_discover`, and `trace_export` tools. Prefer
61
+ `semantic_snapshot` because it normalizes Android and iOS hierarchy classes
62
+ into roles, selectors, bounds, and recommended actions.
63
+
64
+ After a session has produced trace artifacts, prefer the review-first
65
+ exploration handoff when a goal should travel with the generated scenario
66
+ candidate:
67
+
68
+ ```json
69
+ {"method":"trace.explore","params":{"out":".zmr/discovered/login-smoke.json","goal":"find a stable login smoke","includeActions":true,"validate":true,"force":true}}
70
+ ```
71
+
72
+ For MCP agents, call `trace_explore` with the same `out`, `goal`,
73
+ `includeActions`, `validate`, and `force` arguments. The offline CLI equivalent
74
+ is:
75
+
76
+ ```bash
77
+ zmr explore --from-trace traces/zmr-agent \
78
+ --out .zmr/discovered/login-smoke.json \
79
+ --goal "find a stable login smoke" \
80
+ --include-actions \
81
+ --validate \
82
+ --json
83
+ ```
84
+
85
+ Treat the output as a starting point. Its JSON includes `autonomous:false`,
86
+ `reviewRequired:true`, `guardrails`, replay coverage, validation, and next
87
+ commands; it does not crawl, discover credentials, or commit tests.
63
88
 
64
89
  ## Scenario Pattern
65
90
 
@@ -0,0 +1,239 @@
1
+ const std = @import("std");
2
+
3
+ const cli_draft = @import("cli_draft.zig");
4
+ const cli_output = @import("cli_output.zig");
5
+ const trace = @import("trace.zig");
6
+ const validation = @import("validation.zig");
7
+ const version = @import("version.zig");
8
+
9
+ pub const ParsedArgs = struct {
10
+ from_trace: ?[]const u8 = null,
11
+ out_path: ?[]const u8 = null,
12
+ name: ?[]const u8 = null,
13
+ app_id: ?[]const u8 = null,
14
+ include_actions: bool = false,
15
+ validate: bool = false,
16
+ force: bool = false,
17
+ json: bool = false,
18
+ };
19
+
20
+ pub const DiscoverSummary = struct {
21
+ ok: bool,
22
+ draft: cli_draft.DraftSummary,
23
+ validated: bool,
24
+ };
25
+
26
+ pub const JsonOptions = struct {
27
+ mode: []const u8 = "discover",
28
+ goal: ?[]const u8 = null,
29
+ autonomous: ?bool = null,
30
+ review_required: ?bool = null,
31
+ guardrails: []const []const u8 = &.{},
32
+ };
33
+
34
+ pub const OwnedDiscover = struct {
35
+ draft: cli_draft.OwnedDraft,
36
+ validation: ?validation.Result = null,
37
+ summary: DiscoverSummary,
38
+
39
+ pub fn deinit(self: *OwnedDiscover, allocator: std.mem.Allocator) void {
40
+ if (self.validation) |result| result.deinit(allocator);
41
+ self.draft.deinit(allocator);
42
+ }
43
+ };
44
+
45
+ pub fn parseArgs(args: []const []const u8) !ParsedArgs {
46
+ var parsed = ParsedArgs{};
47
+ var index: usize = 0;
48
+ while (index < args.len) : (index += 1) {
49
+ const arg = args[index];
50
+ if (std.mem.eql(u8, arg, "--from-trace")) {
51
+ index += 1;
52
+ parsed.from_trace = if (index < args.len) args[index] else return error.MissingTraceDir;
53
+ } else if (std.mem.eql(u8, arg, "--out")) {
54
+ index += 1;
55
+ parsed.out_path = if (index < args.len) args[index] else return error.MissingDraftOut;
56
+ } else if (std.mem.eql(u8, arg, "--name")) {
57
+ index += 1;
58
+ parsed.name = if (index < args.len) args[index] else return error.MissingParam;
59
+ } else if (std.mem.eql(u8, arg, "--app-id")) {
60
+ index += 1;
61
+ parsed.app_id = if (index < args.len) args[index] else return error.MissingAppId;
62
+ } else if (std.mem.eql(u8, arg, "--include-actions")) {
63
+ parsed.include_actions = true;
64
+ } else if (std.mem.eql(u8, arg, "--validate")) {
65
+ parsed.validate = true;
66
+ } else if (std.mem.eql(u8, arg, "--force")) {
67
+ parsed.force = true;
68
+ } else if (std.mem.eql(u8, arg, "--json")) {
69
+ parsed.json = true;
70
+ } else {
71
+ return error.UnknownFlag;
72
+ }
73
+ }
74
+
75
+ if (parsed.from_trace == null) return error.MissingTraceDir;
76
+ if (parsed.out_path == null) return error.MissingDraftOut;
77
+ return parsed;
78
+ }
79
+
80
+ pub fn run(allocator: std.mem.Allocator, args: *std.process.ArgIterator) !void {
81
+ var raw_args = std.ArrayList([]const u8).empty;
82
+ defer raw_args.deinit(allocator);
83
+ while (args.next()) |arg| try raw_args.append(allocator, arg);
84
+
85
+ const parsed = try parseArgs(raw_args.items);
86
+ var discovered = try discoverFromTrace(allocator, parsed);
87
+ defer discovered.deinit(allocator);
88
+
89
+ const stdout = std.fs.File.stdout().deprecatedWriter();
90
+ if (parsed.json) {
91
+ try writeJson(stdout, discovered.summary, discovered.validation);
92
+ } else {
93
+ try stdout.print("wrote {s}\n", .{discovered.summary.draft.out_path});
94
+ if (discovered.validation) |result| {
95
+ if (result.ok) {
96
+ try stdout.print("validated {s}\n", .{discovered.summary.draft.out_path});
97
+ } else {
98
+ try stdout.print("validation failed {s}\n", .{discovered.summary.draft.out_path});
99
+ }
100
+ }
101
+ try stdout.writeAll("next: zmr validate --json ");
102
+ try cli_output.writeShellArg(stdout, discovered.summary.draft.out_path);
103
+ try stdout.writeAll("\n");
104
+ }
105
+ if (!discovered.summary.ok) std.process.exit(1);
106
+ }
107
+
108
+ pub fn discoverFromTrace(allocator: std.mem.Allocator, parsed: ParsedArgs) !OwnedDiscover {
109
+ var draft = try cli_draft.draftFromTrace(allocator, .{
110
+ .from_trace = parsed.from_trace,
111
+ .out_path = parsed.out_path,
112
+ .name = parsed.name,
113
+ .app_id = parsed.app_id,
114
+ .include_actions = parsed.include_actions,
115
+ .force = parsed.force,
116
+ .json = parsed.json,
117
+ });
118
+ errdefer draft.deinit(allocator);
119
+
120
+ var validation_result: ?validation.Result = null;
121
+ errdefer if (validation_result) |result| result.deinit(allocator);
122
+ if (parsed.validate) {
123
+ validation_result = try validation.validateFile(allocator, draft.summary.out_path);
124
+ }
125
+
126
+ const ok = draft.summary.ok and (validation_result == null or validation_result.?.ok);
127
+ return .{
128
+ .draft = draft,
129
+ .validation = validation_result,
130
+ .summary = .{
131
+ .ok = ok,
132
+ .draft = draft.summary,
133
+ .validated = parsed.validate,
134
+ },
135
+ };
136
+ }
137
+
138
+ pub fn writeJson(writer: anytype, summary: DiscoverSummary, validation_result: ?validation.Result) !void {
139
+ try writeJsonWithOptions(writer, summary, validation_result, .{});
140
+ }
141
+
142
+ pub fn writeJsonWithOptions(writer: anytype, summary: DiscoverSummary, validation_result: ?validation.Result, options: JsonOptions) !void {
143
+ const draft = summary.draft;
144
+ try writer.writeAll("{\"ok\":");
145
+ try writer.writeAll(if (summary.ok) "true" else "false");
146
+ try writer.writeAll(",\"mode\":");
147
+ try trace.writeJsonString(writer, options.mode);
148
+ try writer.writeAll(",\"schemaVersion\":1");
149
+ try writer.writeAll(",\"runnerVersion\":");
150
+ try trace.writeJsonString(writer, version.runner_version);
151
+ try writer.writeAll(",\"protocolVersion\":");
152
+ try trace.writeJsonString(writer, version.protocol_version);
153
+ if (options.goal) |goal| {
154
+ try writer.writeAll(",\"goal\":");
155
+ try trace.writeJsonString(writer, goal);
156
+ }
157
+ if (options.autonomous) |autonomous| {
158
+ try writer.writeAll(",\"autonomous\":");
159
+ try writer.writeAll(if (autonomous) "true" else "false");
160
+ }
161
+ if (options.review_required) |review_required| {
162
+ try writer.writeAll(",\"reviewRequired\":");
163
+ try writer.writeAll(if (review_required) "true" else "false");
164
+ }
165
+ if (options.guardrails.len > 0) {
166
+ try writer.writeAll(",\"guardrails\":[");
167
+ for (options.guardrails, 0..) |guardrail, index| {
168
+ if (index > 0) try writer.writeAll(",");
169
+ try trace.writeJsonString(writer, guardrail);
170
+ }
171
+ try writer.writeAll("]");
172
+ }
173
+ try writer.writeAll(",\"out\":");
174
+ try trace.writeJsonString(writer, draft.out_path);
175
+ try writer.writeAll(",\"traceDir\":");
176
+ try trace.writeJsonString(writer, draft.trace_dir);
177
+ try writer.writeAll(",\"sourceSnapshot\":");
178
+ try trace.writeJsonString(writer, draft.source_snapshot);
179
+ try writer.writeAll(",\"name\":");
180
+ try trace.writeJsonString(writer, draft.name);
181
+ try writer.writeAll(",\"appId\":");
182
+ if (draft.app_id) |actual| {
183
+ try trace.writeJsonString(writer, actual);
184
+ } else {
185
+ try writer.writeAll("null");
186
+ }
187
+ try writer.print(",\"selectorCount\":{d},\"stepCount\":{d}", .{ draft.selector_count, draft.step_count });
188
+ try cli_draft.writeReplayJson(writer, draft.replay);
189
+ try writer.writeAll(",\"warnings\":[");
190
+ for (draft.warnings, 0..) |warning, index| {
191
+ if (index > 0) try writer.writeAll(",");
192
+ try trace.writeJsonString(writer, warning);
193
+ }
194
+ try writer.writeAll("],\"validated\":");
195
+ try writer.writeAll(if (summary.validated) "true" else "false");
196
+ try writer.writeAll(",\"validation\":");
197
+ if (validation_result) |result| {
198
+ try writeValidationObject(writer, draft.out_path, result);
199
+ } else {
200
+ try writer.writeAll("null");
201
+ }
202
+ try writer.writeAll(",\"nextCommands\":[\"zmr validate --json ");
203
+ try cli_output.writeShellArgJsonContent(writer, draft.out_path);
204
+ try writer.writeAll("\",\"zmr run ");
205
+ try cli_output.writeShellArgJsonContent(writer, draft.out_path);
206
+ try writer.writeAll(" --json --trace-dir ");
207
+ try cli_output.writeShellArgJsonContent(writer, draft.trace_dir);
208
+ try writer.writeAll("\"]}\n");
209
+ }
210
+
211
+ fn writeValidationObject(writer: anytype, path: []const u8, result: validation.Result) !void {
212
+ try writer.writeAll("{\"ok\":");
213
+ try writer.writeAll(if (result.ok) "true" else "false");
214
+ try writer.writeAll(",\"path\":");
215
+ try trace.writeJsonString(writer, path);
216
+ if (result.ok) {
217
+ try writer.writeAll(",\"name\":");
218
+ try trace.writeJsonString(writer, result.name.?);
219
+ try writer.writeAll(",\"appId\":");
220
+ if (result.app_id) |app_id| {
221
+ try trace.writeJsonString(writer, app_id);
222
+ } else {
223
+ try writer.writeAll("null");
224
+ }
225
+ try writer.print(",\"stepCount\":{d}", .{result.step_count});
226
+ } else {
227
+ try writer.writeAll(",\"errorCode\":");
228
+ try trace.writeJsonString(writer, result.error_code.?);
229
+ try writer.writeAll(",\"message\":");
230
+ try trace.writeJsonString(writer, result.message.?);
231
+ if (result.path) |field_path| {
232
+ try writer.writeAll(",\"fieldPath\":");
233
+ try trace.writeJsonString(writer, field_path);
234
+ }
235
+ if (result.line) |line| try writer.print(",\"line\":{d}", .{line});
236
+ if (result.column) |column| try writer.print(",\"column\":{d}", .{column});
237
+ }
238
+ try writer.writeAll("}");
239
+ }