zig-mobile-runner 0.1.0

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 (225) hide show
  1. package/CHANGELOG.md +484 -0
  2. package/CONTRIBUTING.md +42 -0
  3. package/FEATURES.md +112 -0
  4. package/LICENSE +21 -0
  5. package/README.md +255 -0
  6. package/SECURITY.md +34 -0
  7. package/build.zig +38 -0
  8. package/build.zig.zon +7 -0
  9. package/clients/README.md +144 -0
  10. package/clients/go/README.md +24 -0
  11. package/clients/go/examples/fake-session/main.go +93 -0
  12. package/clients/go/go.mod +3 -0
  13. package/clients/go/zmr/client.go +432 -0
  14. package/clients/kotlin/README.md +35 -0
  15. package/clients/kotlin/build.gradle.kts +35 -0
  16. package/clients/kotlin/settings.gradle.kts +15 -0
  17. package/clients/kotlin/src/main/kotlin/dev/zmr/FakeSession.kt +86 -0
  18. package/clients/kotlin/src/main/kotlin/dev/zmr/ZmrClient.kt +67 -0
  19. package/clients/python/README.md +29 -0
  20. package/clients/python/examples/fake_session.py +48 -0
  21. package/clients/python/pyproject.toml +13 -0
  22. package/clients/python/zmr_client.py +202 -0
  23. package/clients/rust/Cargo.lock +107 -0
  24. package/clients/rust/Cargo.toml +10 -0
  25. package/clients/rust/README.md +19 -0
  26. package/clients/rust/examples/fake_session.rs +70 -0
  27. package/clients/rust/src/lib.rs +461 -0
  28. package/clients/swift/Package.swift +16 -0
  29. package/clients/swift/README.md +36 -0
  30. package/clients/swift/Sources/ZMRClient/ZMRClient.swift +114 -0
  31. package/clients/swift/Sources/ZMRFakeSession/main.swift +86 -0
  32. package/clients/typescript/README.md +34 -0
  33. package/clients/typescript/examples/fake-session.mjs +36 -0
  34. package/clients/typescript/index.d.ts +144 -0
  35. package/clients/typescript/index.mjs +192 -0
  36. package/clients/typescript/package.json +8 -0
  37. package/docs/adr/0001-agent-native-runner-boundary.md +31 -0
  38. package/docs/adr/0002-app-local-zmr-contract.md +39 -0
  39. package/docs/adr/0003-ios-simulator-xctest-shim.md +41 -0
  40. package/docs/adr/0004-benchmark-claims-and-baseline-collection.md +37 -0
  41. package/docs/adr/README.md +12 -0
  42. package/docs/ai-agents.md +156 -0
  43. package/docs/app-integration.md +316 -0
  44. package/docs/benchmarking.md +275 -0
  45. package/docs/client-installation.md +141 -0
  46. package/docs/clients.md +98 -0
  47. package/docs/config.md +175 -0
  48. package/docs/demo.md +259 -0
  49. package/docs/dsl.md +57 -0
  50. package/docs/install.md +233 -0
  51. package/docs/market-positioning.md +70 -0
  52. package/docs/npm.md +359 -0
  53. package/docs/protocol-fixtures/README.md +8 -0
  54. package/docs/protocol-fixtures/core-session.requests.jsonl +8 -0
  55. package/docs/protocol-fixtures/core-session.responses.jsonl +8 -0
  56. package/docs/protocol-versioning.md +65 -0
  57. package/docs/protocol.md +560 -0
  58. package/docs/publication.md +77 -0
  59. package/docs/release-audit.md +99 -0
  60. package/docs/release-candidate.md +111 -0
  61. package/docs/release-evidence.md +188 -0
  62. package/docs/release-notes-template.md +58 -0
  63. package/docs/roadmap.md +334 -0
  64. package/docs/scenario-authoring.md +88 -0
  65. package/docs/shipping.md +170 -0
  66. package/docs/trace-privacy.md +88 -0
  67. package/docs/troubleshooting.md +256 -0
  68. package/examples/android-app-auth-probe.json +89 -0
  69. package/examples/android-app-error-state.json +13 -0
  70. package/examples/android-app-login-smoke.json +192 -0
  71. package/examples/android-app-onboarding.json +12 -0
  72. package/examples/android-app-referral-deep-link.json +12 -0
  73. package/examples/android-shim-smoke.json +19 -0
  74. package/examples/demo-failure.json +12 -0
  75. package/examples/demo-fake.json +14 -0
  76. package/examples/ios-dev-client-open-link.json +26 -0
  77. package/examples/ios-dev-client-route-snapshot.json +24 -0
  78. package/examples/ios-shim-smoke.json +23 -0
  79. package/examples/ios-smoke.json +9 -0
  80. package/go.work +3 -0
  81. package/npm/agents.mjs +183 -0
  82. package/npm/app-config.mjs +95 -0
  83. package/npm/build-zmr.mjs +21 -0
  84. package/npm/commands.mjs +104 -0
  85. package/npm/generated-files.mjs +50 -0
  86. package/npm/index.mjs +75 -0
  87. package/npm/init-app.mjs +80 -0
  88. package/npm/package-scripts.mjs +72 -0
  89. package/npm/postinstall.mjs +21 -0
  90. package/npm/scaffold.mjs +179 -0
  91. package/npm/scenarios.mjs +93 -0
  92. package/npm/setup.mjs +69 -0
  93. package/npm/wizard.mjs +117 -0
  94. package/npm/zmr.mjs +23 -0
  95. package/package.json +114 -0
  96. package/prebuilds/darwin-arm64/zmr +0 -0
  97. package/prebuilds/darwin-x64/zmr +0 -0
  98. package/prebuilds/linux-arm64/zmr +0 -0
  99. package/prebuilds/linux-x64/zmr +0 -0
  100. package/schemas/README.md +26 -0
  101. package/schemas/action-result.schema.json +27 -0
  102. package/schemas/capabilities-output.schema.json +98 -0
  103. package/schemas/devices-output.schema.json +25 -0
  104. package/schemas/doctor-output.schema.json +51 -0
  105. package/schemas/explain-output.schema.json +51 -0
  106. package/schemas/import-output.schema.json +23 -0
  107. package/schemas/init-output.schema.json +71 -0
  108. package/schemas/json-rpc.schema.json +55 -0
  109. package/schemas/release-manifest.schema.json +43 -0
  110. package/schemas/release-readiness-output.schema.json +127 -0
  111. package/schemas/run-output.schema.json +43 -0
  112. package/schemas/scenario.schema.json +128 -0
  113. package/schemas/schemas-output.schema.json +26 -0
  114. package/schemas/semantic-snapshot.schema.json +116 -0
  115. package/schemas/snapshot.schema.json +60 -0
  116. package/schemas/trace-event.schema.json +14 -0
  117. package/schemas/trace-manifest.schema.json +59 -0
  118. package/schemas/validate-output.schema.json +42 -0
  119. package/schemas/version-output.schema.json +23 -0
  120. package/schemas/zmr-config.schema.json +75 -0
  121. package/scripts/android-emulator.sh +126 -0
  122. package/scripts/assert-ios-physical-ready.sh +213 -0
  123. package/scripts/benchmark-command.sh +307 -0
  124. package/scripts/benchmark.sh +359 -0
  125. package/scripts/benchmark_gate.py +117 -0
  126. package/scripts/benchmark_result_row.py +88 -0
  127. package/scripts/compare-benchmarks.py +288 -0
  128. package/scripts/create-android-demo-app.sh +342 -0
  129. package/scripts/create-ios-demo-app.sh +261 -0
  130. package/scripts/demo-android-real.sh +232 -0
  131. package/scripts/demo-ios-real.sh +270 -0
  132. package/scripts/demo.sh +464 -0
  133. package/scripts/device-matrix.sh +338 -0
  134. package/scripts/ensure-ios-shim-target.rb +237 -0
  135. package/scripts/install-android-shim.sh +281 -0
  136. package/scripts/install-ios-shim.sh +589 -0
  137. package/scripts/pilot-gate.sh +560 -0
  138. package/scripts/release-readiness.py +838 -0
  139. package/scripts/release-readiness.sh +91 -0
  140. package/scripts/run-android-pilot.sh +561 -0
  141. package/scripts/run-ios-pilot.sh +509 -0
  142. package/shims/android/README.md +21 -0
  143. package/shims/android/ZMRShimInstrumentedTest.java +152 -0
  144. package/shims/android/protocol.md +18 -0
  145. package/shims/ios/README.md +50 -0
  146. package/shims/ios/ZMRShim.swift +110 -0
  147. package/shims/ios/ZMRShimUITestCase.swift +475 -0
  148. package/shims/ios/protocol.md +74 -0
  149. package/skills/zmr-mobile-testing/SKILL.md +127 -0
  150. package/src/android.zig +344 -0
  151. package/src/android_device_info.zig +99 -0
  152. package/src/android_emulator.zig +154 -0
  153. package/src/android_screen_recording.zig +112 -0
  154. package/src/android_shell.zig +112 -0
  155. package/src/bundle.zig +124 -0
  156. package/src/bundle_redaction.zig +272 -0
  157. package/src/bundle_tar.zig +123 -0
  158. package/src/cli_devices.zig +97 -0
  159. package/src/cli_doctor.zig +114 -0
  160. package/src/cli_import.zig +70 -0
  161. package/src/cli_info.zig +39 -0
  162. package/src/cli_init.zig +72 -0
  163. package/src/cli_output.zig +467 -0
  164. package/src/cli_run.zig +259 -0
  165. package/src/cli_serve.zig +287 -0
  166. package/src/cli_trace.zig +111 -0
  167. package/src/cli_validate.zig +41 -0
  168. package/src/command.zig +211 -0
  169. package/src/config.zig +305 -0
  170. package/src/config_diagnostics.zig +212 -0
  171. package/src/config_paths.zig +49 -0
  172. package/src/device_registry.zig +37 -0
  173. package/src/doctor.zig +412 -0
  174. package/src/doctor_hints.zig +52 -0
  175. package/src/errors.zig +55 -0
  176. package/src/fake_device.zig +163 -0
  177. package/src/health.zig +28 -0
  178. package/src/importer.zig +343 -0
  179. package/src/importer_json.zig +100 -0
  180. package/src/importer_model.zig +103 -0
  181. package/src/ios.zig +399 -0
  182. package/src/ios_devices.zig +219 -0
  183. package/src/ios_lifecycle.zig +72 -0
  184. package/src/ios_shim.zig +242 -0
  185. package/src/ios_snapshot.zig +20 -0
  186. package/src/json_fields.zig +80 -0
  187. package/src/json_rpc.zig +150 -0
  188. package/src/json_rpc_methods.zig +318 -0
  189. package/src/json_rpc_observation.zig +31 -0
  190. package/src/json_rpc_params.zig +52 -0
  191. package/src/json_rpc_protocol.zig +110 -0
  192. package/src/json_rpc_trace.zig +73 -0
  193. package/src/main.zig +135 -0
  194. package/src/mcp.zig +234 -0
  195. package/src/mcp_protocol.zig +64 -0
  196. package/src/mcp_trace.zig +83 -0
  197. package/src/report.zig +346 -0
  198. package/src/report_html.zig +63 -0
  199. package/src/report_values.zig +27 -0
  200. package/src/run_options.zig +152 -0
  201. package/src/runner.zig +280 -0
  202. package/src/runner_actions.zig +109 -0
  203. package/src/runner_config.zig +6 -0
  204. package/src/runner_diagnostics.zig +268 -0
  205. package/src/runner_events.zig +170 -0
  206. package/src/runner_native.zig +88 -0
  207. package/src/runner_waits.zig +300 -0
  208. package/src/scaffold.zig +472 -0
  209. package/src/scenario.zig +346 -0
  210. package/src/scenario_fields.zig +50 -0
  211. package/src/schema_registry.zig +53 -0
  212. package/src/selector.zig +84 -0
  213. package/src/semantic.zig +171 -0
  214. package/src/trace.zig +315 -0
  215. package/src/trace_json.zig +340 -0
  216. package/src/trace_summary.zig +218 -0
  217. package/src/trace_summary_diagnostic.zig +202 -0
  218. package/src/types.zig +120 -0
  219. package/src/uiautomator.zig +164 -0
  220. package/src/validation.zig +187 -0
  221. package/src/version.zig +22 -0
  222. package/viewer/app.js +373 -0
  223. package/viewer/index.html +126 -0
  224. package/viewer/parser.js +233 -0
  225. package/viewer/styles.css +585 -0
@@ -0,0 +1,346 @@
1
+ const std = @import("std");
2
+ const fields = @import("scenario_fields.zig");
3
+ const selector = @import("selector.zig");
4
+
5
+ pub const Swipe = struct {
6
+ x1: i32,
7
+ y1: i32,
8
+ x2: i32,
9
+ y2: i32,
10
+ duration_ms: u32 = 300,
11
+ };
12
+
13
+ pub const WaitVisible = struct {
14
+ selector: selector.Selector,
15
+ timeout_ms: u64 = 5000,
16
+ };
17
+
18
+ pub const WaitAny = struct {
19
+ selectors: []selector.Selector,
20
+ timeout_ms: u64 = 5000,
21
+
22
+ pub fn deinit(self: WaitAny, allocator: std.mem.Allocator) void {
23
+ for (self.selectors) |wanted| wanted.deinit(allocator);
24
+ allocator.free(self.selectors);
25
+ }
26
+ };
27
+
28
+ pub const TypeText = struct {
29
+ selector: ?selector.Selector = null,
30
+ text: []const u8,
31
+
32
+ pub fn deinit(self: TypeText, allocator: std.mem.Allocator) void {
33
+ if (self.selector) |wanted| wanted.deinit(allocator);
34
+ allocator.free(self.text);
35
+ }
36
+ };
37
+
38
+ pub const EraseText = struct {
39
+ selector: ?selector.Selector = null,
40
+ max_chars: u32 = 80,
41
+
42
+ pub fn deinit(self: EraseText, allocator: std.mem.Allocator) void {
43
+ if (self.selector) |wanted| wanted.deinit(allocator);
44
+ }
45
+ };
46
+
47
+ pub const StepBlock = struct {
48
+ steps: []Step,
49
+
50
+ pub fn deinit(self: StepBlock, allocator: std.mem.Allocator) void {
51
+ for (self.steps) |step| step.deinit(allocator);
52
+ allocator.free(self.steps);
53
+ }
54
+ };
55
+
56
+ pub const ConditionalBlock = struct {
57
+ selector: selector.Selector,
58
+ timeout_ms: u64 = 0,
59
+ steps: []Step,
60
+
61
+ pub fn deinit(self: ConditionalBlock, allocator: std.mem.Allocator) void {
62
+ self.selector.deinit(allocator);
63
+ for (self.steps) |step| step.deinit(allocator);
64
+ allocator.free(self.steps);
65
+ }
66
+ };
67
+
68
+ pub const RepeatBlock = struct {
69
+ times: u32,
70
+ steps: []Step,
71
+
72
+ pub fn deinit(self: RepeatBlock, allocator: std.mem.Allocator) void {
73
+ for (self.steps) |step| step.deinit(allocator);
74
+ allocator.free(self.steps);
75
+ }
76
+ };
77
+
78
+ pub const ScrollDirection = enum {
79
+ down,
80
+ up,
81
+ };
82
+
83
+ pub const ScrollUntilVisible = struct {
84
+ selector: selector.Selector,
85
+ timeout_ms: u64 = 5000,
86
+ direction: ScrollDirection = .down,
87
+
88
+ pub fn deinit(self: ScrollUntilVisible, allocator: std.mem.Allocator) void {
89
+ self.selector.deinit(allocator);
90
+ }
91
+ };
92
+
93
+ pub const Step = union(enum) {
94
+ launch,
95
+ stop,
96
+ clear_state,
97
+ snapshot,
98
+ open_link: []const u8,
99
+ tap: selector.Selector,
100
+ type_text: TypeText,
101
+ press_back,
102
+ hide_keyboard,
103
+ swipe: Swipe,
104
+ erase_text: EraseText,
105
+ wait_visible: WaitVisible,
106
+ wait_not_visible: WaitVisible,
107
+ wait_any: WaitAny,
108
+ assert_visible: selector.Selector,
109
+ assert_not_visible: selector.Selector,
110
+ assert_none_visible: WaitAny,
111
+ assert_healthy_timeout_ms: u64,
112
+ optional: *Step,
113
+ when_visible: ConditionalBlock,
114
+ repeat: RepeatBlock,
115
+ scroll_until_visible: ScrollUntilVisible,
116
+ sleep_ms: u64,
117
+
118
+ pub fn deinit(self: Step, allocator: std.mem.Allocator) void {
119
+ switch (self) {
120
+ .open_link => |value| allocator.free(value),
121
+ .tap => |value| value.deinit(allocator),
122
+ .type_text => |value| value.deinit(allocator),
123
+ .erase_text => |value| value.deinit(allocator),
124
+ .wait_visible => |value| value.selector.deinit(allocator),
125
+ .wait_not_visible => |value| value.selector.deinit(allocator),
126
+ .wait_any => |value| value.deinit(allocator),
127
+ .assert_visible => |value| value.deinit(allocator),
128
+ .assert_not_visible => |value| value.deinit(allocator),
129
+ .assert_none_visible => |value| value.deinit(allocator),
130
+ .optional => |value| {
131
+ value.deinit(allocator);
132
+ allocator.destroy(value);
133
+ },
134
+ .when_visible => |value| value.deinit(allocator),
135
+ .repeat => |value| value.deinit(allocator),
136
+ .scroll_until_visible => |value| value.deinit(allocator),
137
+ else => {},
138
+ }
139
+ }
140
+ };
141
+
142
+ pub const Scenario = struct {
143
+ name: []const u8,
144
+ app_id: ?[]const u8 = null,
145
+ steps: []Step,
146
+
147
+ pub fn deinit(self: Scenario, allocator: std.mem.Allocator) void {
148
+ allocator.free(self.name);
149
+ if (self.app_id) |value| allocator.free(value);
150
+ for (self.steps) |step| step.deinit(allocator);
151
+ allocator.free(self.steps);
152
+ }
153
+ };
154
+
155
+ pub fn parseFile(allocator: std.mem.Allocator, path: []const u8) !Scenario {
156
+ const content = try std.fs.cwd().readFileAlloc(allocator, path, 16 * 1024 * 1024);
157
+ defer allocator.free(content);
158
+ return try parseSlice(allocator, content);
159
+ }
160
+
161
+ pub fn parseSlice(allocator: std.mem.Allocator, content: []const u8) !Scenario {
162
+ const parsed = try std.json.parseFromSlice(std.json.Value, allocator, content, .{});
163
+ defer parsed.deinit();
164
+ if (parsed.value != .object) return error.ScenarioMustBeObject;
165
+ const root = parsed.value.object;
166
+
167
+ const name = try fields.requiredString(allocator, root, "name");
168
+ errdefer allocator.free(name);
169
+ const app_id = try fields.optionalString(allocator, root, "appId");
170
+ errdefer if (app_id) |value| allocator.free(value);
171
+
172
+ const steps_value = root.get("steps") orelse return error.ScenarioMissingSteps;
173
+ if (steps_value != .array) return error.ScenarioStepsMustBeArray;
174
+ var steps = std.ArrayList(Step).empty;
175
+ errdefer {
176
+ for (steps.items) |step| step.deinit(allocator);
177
+ steps.deinit(allocator);
178
+ }
179
+ try appendParsedSteps(allocator, &steps, steps_value);
180
+
181
+ return .{
182
+ .name = name,
183
+ .app_id = app_id,
184
+ .steps = try steps.toOwnedSlice(allocator),
185
+ };
186
+ }
187
+
188
+ fn parseStep(allocator: std.mem.Allocator, value: std.json.Value) anyerror!Step {
189
+ if (value != .object) return error.StepMustBeObject;
190
+ const object = value.object;
191
+ var parsed = try parseRawStep(allocator, object);
192
+ errdefer parsed.deinit(allocator);
193
+
194
+ if (try fields.optionalBool(object, "optional", false)) {
195
+ const step_ptr = try allocator.create(Step);
196
+ errdefer allocator.destroy(step_ptr);
197
+ step_ptr.* = parsed;
198
+ return .{ .optional = step_ptr };
199
+ }
200
+
201
+ return parsed;
202
+ }
203
+
204
+ fn parseRawStep(allocator: std.mem.Allocator, object: std.json.ObjectMap) anyerror!Step {
205
+ const action_value = object.get("action") orelse return error.StepMissingAction;
206
+ if (action_value != .string) return error.StepActionMustBeString;
207
+ const action = action_value.string;
208
+
209
+ if (std.mem.eql(u8, action, "launch")) return .launch;
210
+ if (std.mem.eql(u8, action, "stop")) return .stop;
211
+ if (std.mem.eql(u8, action, "clearState")) return .clear_state;
212
+ if (std.mem.eql(u8, action, "snapshot")) return .snapshot;
213
+ if (std.mem.eql(u8, action, "pressBack")) return .press_back;
214
+ if (std.mem.eql(u8, action, "hideKeyboard")) return .hide_keyboard;
215
+ if (std.mem.eql(u8, action, "sleep")) return .{ .sleep_ms = try fields.optionalU64(object, "ms", 500) };
216
+ if (std.mem.eql(u8, action, "openLink")) return .{ .open_link = try fields.requiredStringOrError(allocator, object, "url", error.StepMissingUrl) };
217
+ if (std.mem.eql(u8, action, "tap")) return .{ .tap = try fields.parseSelectorField(allocator, object) };
218
+ if (std.mem.eql(u8, action, "typeText")) {
219
+ const wanted = if (object.get("selector")) |selector_value| try selector.parseFromJson(allocator, selector_value) else null;
220
+ errdefer if (wanted) |actual| actual.deinit(allocator);
221
+ return .{ .type_text = .{
222
+ .selector = wanted,
223
+ .text = try fields.requiredStringOrError(allocator, object, "text", error.StepMissingText),
224
+ } };
225
+ }
226
+ if (std.mem.eql(u8, action, "eraseText")) {
227
+ const wanted = if (object.get("selector")) |selector_value| try selector.parseFromJson(allocator, selector_value) else null;
228
+ errdefer if (wanted) |actual| actual.deinit(allocator);
229
+ return .{ .erase_text = .{
230
+ .selector = wanted,
231
+ .max_chars = @as(u32, @intCast(try fields.optionalU64(object, "maxChars", 80))),
232
+ } };
233
+ }
234
+ if (std.mem.eql(u8, action, "swipe")) return .{ .swipe = .{
235
+ .x1 = try fields.requiredI32OrError(object, "x1", error.StepMissingX1),
236
+ .y1 = try fields.requiredI32OrError(object, "y1", error.StepMissingY1),
237
+ .x2 = try fields.requiredI32OrError(object, "x2", error.StepMissingX2),
238
+ .y2 = try fields.requiredI32OrError(object, "y2", error.StepMissingY2),
239
+ .duration_ms = @as(u32, @intCast(try fields.optionalU64(object, "durationMs", 300))),
240
+ } };
241
+ if (std.mem.eql(u8, action, "waitVisible")) {
242
+ const wanted = try fields.parseSelectorField(allocator, object);
243
+ errdefer wanted.deinit(allocator);
244
+ return .{ .wait_visible = .{
245
+ .selector = wanted,
246
+ .timeout_ms = try fields.optionalU64(object, "timeoutMs", 5000),
247
+ } };
248
+ }
249
+ if (std.mem.eql(u8, action, "waitNotVisible")) {
250
+ const wanted = try fields.parseSelectorField(allocator, object);
251
+ errdefer wanted.deinit(allocator);
252
+ return .{ .wait_not_visible = .{
253
+ .selector = wanted,
254
+ .timeout_ms = try fields.optionalU64(object, "timeoutMs", 5000),
255
+ } };
256
+ }
257
+ if (std.mem.eql(u8, action, "waitAny")) {
258
+ const selectors = try fields.parseSelectorArrayField(allocator, object);
259
+ errdefer {
260
+ for (selectors) |wanted| wanted.deinit(allocator);
261
+ allocator.free(selectors);
262
+ }
263
+ return .{ .wait_any = .{
264
+ .selectors = selectors,
265
+ .timeout_ms = try fields.optionalU64(object, "timeoutMs", 5000),
266
+ } };
267
+ }
268
+ if (std.mem.eql(u8, action, "assertVisible")) return .{ .assert_visible = try fields.parseSelectorField(allocator, object) };
269
+ if (std.mem.eql(u8, action, "assertNotVisible")) return .{ .assert_not_visible = try fields.parseSelectorField(allocator, object) };
270
+ if (std.mem.eql(u8, action, "assertHealthy")) return .{ .assert_healthy_timeout_ms = try fields.optionalU64(object, "timeoutMs", 0) };
271
+ if (std.mem.eql(u8, action, "assertNoneVisible")) {
272
+ const selectors = try fields.parseSelectorArrayField(allocator, object);
273
+ errdefer {
274
+ for (selectors) |wanted| wanted.deinit(allocator);
275
+ allocator.free(selectors);
276
+ }
277
+ return .{ .assert_none_visible = .{
278
+ .selectors = selectors,
279
+ .timeout_ms = try fields.optionalU64(object, "timeoutMs", 0),
280
+ } };
281
+ }
282
+ if (std.mem.eql(u8, action, "optional")) {
283
+ const nested_value = object.get("step") orelse return error.OptionalStepMissingStep;
284
+ const nested = try allocator.create(Step);
285
+ errdefer allocator.destroy(nested);
286
+ nested.* = try parseStep(allocator, nested_value);
287
+ return .{ .optional = nested };
288
+ }
289
+ if (std.mem.eql(u8, action, "whenVisible")) {
290
+ const wanted = try fields.parseSelectorField(allocator, object);
291
+ errdefer wanted.deinit(allocator);
292
+ const timeout_ms = try fields.optionalU64(object, "timeoutMs", 0);
293
+ const steps = try parseStepsField(allocator, object);
294
+ errdefer {
295
+ for (steps) |step| step.deinit(allocator);
296
+ allocator.free(steps);
297
+ }
298
+ return .{ .when_visible = .{
299
+ .selector = wanted,
300
+ .timeout_ms = timeout_ms,
301
+ .steps = steps,
302
+ } };
303
+ }
304
+ if (std.mem.eql(u8, action, "repeat")) return .{ .repeat = .{
305
+ .times = @as(u32, @intCast(try fields.optionalU64(object, "times", 1))),
306
+ .steps = try parseStepsField(allocator, object),
307
+ } };
308
+ if (std.mem.eql(u8, action, "scrollUntilVisible")) {
309
+ const wanted = try fields.parseSelectorField(allocator, object);
310
+ errdefer wanted.deinit(allocator);
311
+ return .{ .scroll_until_visible = .{
312
+ .selector = wanted,
313
+ .timeout_ms = try fields.optionalU64(object, "timeoutMs", 5000),
314
+ .direction = try optionalDirection(object, "direction", .down),
315
+ } };
316
+ }
317
+
318
+ return error.UnknownScenarioAction;
319
+ }
320
+
321
+ fn appendParsedSteps(allocator: std.mem.Allocator, steps: *std.ArrayList(Step), value: std.json.Value) anyerror!void {
322
+ if (value != .array) return error.ScenarioStepsMustBeArray;
323
+ for (value.array.items) |step_value| {
324
+ try steps.append(allocator, try parseStep(allocator, step_value));
325
+ }
326
+ }
327
+
328
+ fn parseStepsField(allocator: std.mem.Allocator, object: std.json.ObjectMap) anyerror![]Step {
329
+ const steps_value = object.get("steps") orelse return error.StepBlockMissingSteps;
330
+ if (steps_value != .array) return error.StepBlockStepsMustBeArray;
331
+ var steps = std.ArrayList(Step).empty;
332
+ errdefer {
333
+ for (steps.items) |step| step.deinit(allocator);
334
+ steps.deinit(allocator);
335
+ }
336
+ try appendParsedSteps(allocator, &steps, steps_value);
337
+ return try steps.toOwnedSlice(allocator);
338
+ }
339
+
340
+ fn optionalDirection(object: std.json.ObjectMap, key: []const u8, default_value: ScrollDirection) !ScrollDirection {
341
+ const value = object.get(key) orelse return default_value;
342
+ if (value != .string) return error.OptionalFieldMustBeString;
343
+ if (std.mem.eql(u8, value.string, "down")) return .down;
344
+ if (std.mem.eql(u8, value.string, "up")) return .up;
345
+ return error.UnknownScrollDirection;
346
+ }
@@ -0,0 +1,50 @@
1
+ const std = @import("std");
2
+ const json_fields = @import("json_fields.zig");
3
+ const selector = @import("selector.zig");
4
+
5
+ pub fn requiredString(allocator: std.mem.Allocator, object: std.json.ObjectMap, key: []const u8) ![]const u8 {
6
+ const value = try json_fields.requiredStringFromObject(object, key, error.RequiredStringMissing, error.RequiredFieldMustBeString);
7
+ return try allocator.dupe(u8, value);
8
+ }
9
+
10
+ pub fn requiredStringOrError(allocator: std.mem.Allocator, object: std.json.ObjectMap, key: []const u8, missing_error: anyerror) ![]const u8 {
11
+ const value = try json_fields.requiredStringFromObject(object, key, missing_error, error.RequiredFieldMustBeString);
12
+ return try allocator.dupe(u8, value);
13
+ }
14
+
15
+ pub fn optionalString(allocator: std.mem.Allocator, object: std.json.ObjectMap, key: []const u8) !?[]const u8 {
16
+ const value = try json_fields.optionalStringFromObject(object, key, error.OptionalFieldMustBeString) orelse return null;
17
+ return try allocator.dupe(u8, value);
18
+ }
19
+
20
+ pub fn requiredI32OrError(object: std.json.ObjectMap, key: []const u8, missing_error: anyerror) !i32 {
21
+ return try json_fields.requiredI32FromObject(object, key, missing_error, error.RequiredFieldMustBeInteger);
22
+ }
23
+
24
+ pub fn optionalU64(object: std.json.ObjectMap, key: []const u8, default_value: u64) !u64 {
25
+ return try json_fields.optionalU64FromObject(object, key, default_value, error.OptionalFieldMustBeInteger);
26
+ }
27
+
28
+ pub fn optionalBool(object: std.json.ObjectMap, key: []const u8, default_value: bool) !bool {
29
+ return try json_fields.optionalBoolFromObject(object, key, default_value, error.OptionalFieldMustBeBool);
30
+ }
31
+
32
+ pub fn parseSelectorField(allocator: std.mem.Allocator, object: std.json.ObjectMap) !selector.Selector {
33
+ const selector_value = object.get("selector") orelse return error.StepMissingSelector;
34
+ return try selector.parseFromJson(allocator, selector_value);
35
+ }
36
+
37
+ pub fn parseSelectorArrayField(allocator: std.mem.Allocator, object: std.json.ObjectMap) ![]selector.Selector {
38
+ const selectors_value = object.get("selectors") orelse return error.StepMissingSelectors;
39
+ if (selectors_value != .array) return error.SelectorsMustBeArray;
40
+ var selectors = std.ArrayList(selector.Selector).empty;
41
+ errdefer {
42
+ for (selectors.items) |wanted| wanted.deinit(allocator);
43
+ selectors.deinit(allocator);
44
+ }
45
+ for (selectors_value.array.items) |selector_value| {
46
+ try selectors.append(allocator, try selector.parseFromJson(allocator, selector_value));
47
+ }
48
+ if (selectors.items.len == 0) return error.SelectorsMustNotBeEmpty;
49
+ return try selectors.toOwnedSlice(allocator);
50
+ }
@@ -0,0 +1,53 @@
1
+ const std = @import("std");
2
+ const trace = @import("trace.zig");
3
+
4
+ pub const PublicSchema = struct {
5
+ name: []const u8,
6
+ path: []const u8,
7
+ id: []const u8,
8
+ description: []const u8,
9
+ };
10
+
11
+ const public_schemas = [_]PublicSchema{
12
+ .{ .name = "json-rpc", .path = "schemas/json-rpc.schema.json", .id = "https://zmr.dev/schemas/json-rpc.schema.json", .description = "JSON-RPC requests and responses used by zmr serve" },
13
+ .{ .name = "scenario", .path = "schemas/scenario.schema.json", .id = "https://zmr.dev/schemas/scenario.schema.json", .description = "Scenario files consumed by zmr run and zmr validate" },
14
+ .{ .name = "snapshot", .path = "schemas/snapshot.schema.json", .id = "https://zmr.dev/schemas/snapshot.schema.json", .description = "ObservationSnapshot JSON emitted by live RPC and persisted trace snapshots" },
15
+ .{ .name = "semantic-snapshot", .path = "schemas/semantic-snapshot.schema.json", .id = "https://zmr.dev/schemas/semantic-snapshot.schema.json", .description = "Agent-optimized semantic snapshot emitted by observe.semanticSnapshot and zmr mcp" },
16
+ .{ .name = "action-result", .path = "schemas/action-result.schema.json", .id = "https://zmr.dev/schemas/action-result.schema.json", .description = "Typed action result shape reserved for richer protocol responses" },
17
+ .{ .name = "trace-event", .path = "schemas/trace-event.schema.json", .id = "https://zmr.dev/schemas/trace-event.schema.json", .description = "One JSONL event row from events.jsonl" },
18
+ .{ .name = "trace-manifest", .path = "schemas/trace-manifest.schema.json", .id = "https://zmr.dev/schemas/trace-manifest.schema.json", .description = "trace.json summary for one traced run" },
19
+ .{ .name = "zmr-config", .path = "schemas/zmr-config.schema.json", .id = "https://zmr.dev/schemas/zmr-config.schema.json", .description = "App-local .zmr/config.json defaults used by the CLI and npm wizard" },
20
+ .{ .name = "doctor-output", .path = "schemas/doctor-output.schema.json", .id = "https://zmr.dev/schemas/doctor-output.schema.json", .description = "Machine-readable zmr doctor --json setup diagnostics" },
21
+ .{ .name = "init-output", .path = "schemas/init-output.schema.json", .id = "https://zmr.dev/schemas/init-output.schema.json", .description = "Machine-readable zmr init --json bootstrap output" },
22
+ .{ .name = "import-output", .path = "schemas/import-output.schema.json", .id = "https://zmr.dev/schemas/import-output.schema.json", .description = "Machine-readable zmr import --json migration output" },
23
+ .{ .name = "devices-output", .path = "schemas/devices-output.schema.json", .id = "https://zmr.dev/schemas/devices-output.schema.json", .description = "Machine-readable zmr devices --json discovery output" },
24
+ .{ .name = "validate-output", .path = "schemas/validate-output.schema.json", .id = "https://zmr.dev/schemas/validate-output.schema.json", .description = "Machine-readable zmr validate --json scenario preflight output" },
25
+ .{ .name = "version-output", .path = "schemas/version-output.schema.json", .id = "https://zmr.dev/schemas/version-output.schema.json", .description = "Machine-readable zmr version --json compatibility output" },
26
+ .{ .name = "capabilities-output", .path = "schemas/capabilities-output.schema.json", .id = "https://zmr.dev/schemas/capabilities-output.schema.json", .description = "Machine-readable runner.capabilities JSON-RPC result" },
27
+ .{ .name = "explain-output", .path = "schemas/explain-output.schema.json", .id = "https://zmr.dev/schemas/explain-output.schema.json", .description = "Machine-readable zmr explain --json failure triage output" },
28
+ .{ .name = "run-output", .path = "schemas/run-output.schema.json", .id = "https://zmr.dev/schemas/run-output.schema.json", .description = "Machine-readable zmr run --json terminal summary output" },
29
+ .{ .name = "release-manifest", .path = "schemas/release-manifest.schema.json", .id = "https://zmr.dev/schemas/release-manifest.schema.json", .description = "Machine-readable RELEASE_MANIFEST.json emitted with release archives" },
30
+ .{ .name = "release-readiness-output", .path = "schemas/release-readiness-output.schema.json", .id = "https://zmr.dev/schemas/release-readiness-output.schema.json", .description = "Machine-readable zmr-release-readiness --json release evidence gate output" },
31
+ .{ .name = "schemas-output", .path = "schemas/schemas-output.schema.json", .id = "https://zmr.dev/schemas/schemas-output.schema.json", .description = "Machine-readable zmr schemas --json public schema index" },
32
+ };
33
+
34
+ pub fn all() []const PublicSchema {
35
+ return public_schemas[0..];
36
+ }
37
+
38
+ pub fn writeJson(writer: anytype) !void {
39
+ try writer.print("{{\"ok\":true,\"count\":{d},\"schemas\":[", .{public_schemas.len});
40
+ for (public_schemas, 0..) |schema_info, index| {
41
+ if (index > 0) try writer.writeAll(",");
42
+ try writer.writeAll("{\"name\":");
43
+ try trace.writeJsonString(writer, schema_info.name);
44
+ try writer.writeAll(",\"path\":");
45
+ try trace.writeJsonString(writer, schema_info.path);
46
+ try writer.writeAll(",\"id\":");
47
+ try trace.writeJsonString(writer, schema_info.id);
48
+ try writer.writeAll(",\"description\":");
49
+ try trace.writeJsonString(writer, schema_info.description);
50
+ try writer.writeAll("}");
51
+ }
52
+ try writer.writeAll("]}\n");
53
+ }
@@ -0,0 +1,84 @@
1
+ const std = @import("std");
2
+ const types = @import("types.zig");
3
+
4
+ pub const Selector = struct {
5
+ id: ?[]const u8 = null,
6
+ text: ?[]const u8 = null,
7
+ text_contains: ?[]const u8 = null,
8
+ content_desc: ?[]const u8 = null,
9
+ content_desc_contains: ?[]const u8 = null,
10
+ class_name: ?[]const u8 = null,
11
+
12
+ pub fn deinit(self: Selector, allocator: std.mem.Allocator) void {
13
+ if (self.id) |value| allocator.free(value);
14
+ if (self.text) |value| allocator.free(value);
15
+ if (self.text_contains) |value| allocator.free(value);
16
+ if (self.content_desc) |value| allocator.free(value);
17
+ if (self.content_desc_contains) |value| allocator.free(value);
18
+ if (self.class_name) |value| allocator.free(value);
19
+ }
20
+
21
+ pub fn clone(self: Selector, allocator: std.mem.Allocator) !Selector {
22
+ return .{
23
+ .id = try types.dupeOptional(allocator, self.id),
24
+ .text = try types.dupeOptional(allocator, self.text),
25
+ .text_contains = try types.dupeOptional(allocator, self.text_contains),
26
+ .content_desc = try types.dupeOptional(allocator, self.content_desc),
27
+ .content_desc_contains = try types.dupeOptional(allocator, self.content_desc_contains),
28
+ .class_name = try types.dupeOptional(allocator, self.class_name),
29
+ };
30
+ }
31
+ };
32
+
33
+ pub fn matches(node: types.UiNode, wanted: Selector) bool {
34
+ if (wanted.id) |id| {
35
+ if (node.resource_id == null or !std.mem.eql(u8, node.resource_id.?, id)) return false;
36
+ }
37
+ if (wanted.text) |text| {
38
+ if (node.text == null or !std.mem.eql(u8, node.text.?, text)) return false;
39
+ }
40
+ if (wanted.text_contains) |needle| {
41
+ if (node.text == null or std.mem.indexOf(u8, node.text.?, needle) == null) return false;
42
+ }
43
+ if (wanted.content_desc) |desc| {
44
+ if (node.content_desc == null or !std.mem.eql(u8, node.content_desc.?, desc)) return false;
45
+ }
46
+ if (wanted.content_desc_contains) |needle| {
47
+ if (node.content_desc == null or std.mem.indexOf(u8, node.content_desc.?, needle) == null) return false;
48
+ }
49
+ if (wanted.class_name) |class_name| {
50
+ if (!std.mem.eql(u8, node.class_name, class_name)) return false;
51
+ }
52
+ return node.visible;
53
+ }
54
+
55
+ pub fn find(nodes: []const types.UiNode, wanted: Selector) ?types.UiNode {
56
+ for (nodes) |node| {
57
+ if (matches(node, wanted)) return node;
58
+ }
59
+ return null;
60
+ }
61
+
62
+ pub fn parseFromJson(allocator: std.mem.Allocator, value: std.json.Value) !Selector {
63
+ if (value != .object) return error.SelectorMustBeObject;
64
+ const object = value.object;
65
+ const id = try stringField(allocator, object, "id") orelse try stringField(allocator, object, "resourceId");
66
+ return .{
67
+ .id = id,
68
+ .text = try stringField(allocator, object, "text"),
69
+ .text_contains = try stringField(allocator, object, "textContains"),
70
+ .content_desc = try stringField(allocator, object, "contentDesc"),
71
+ .content_desc_contains = try stringField(allocator, object, "contentDescContains"),
72
+ .class_name = try stringField(allocator, object, "className"),
73
+ };
74
+ }
75
+
76
+ fn stringField(
77
+ allocator: std.mem.Allocator,
78
+ object: std.json.ObjectMap,
79
+ key: []const u8,
80
+ ) !?[]const u8 {
81
+ const value = object.get(key) orelse return null;
82
+ if (value != .string) return error.SelectorFieldMustBeString;
83
+ return try allocator.dupe(u8, value.string);
84
+ }