1use crate::ISSUE_STATUS_VALUES_NEXT_STEP;
4use std::borrow::Cow;
5
6#[derive(Debug, thiserror::Error, PartialEq, Eq)]
8pub enum CliError {
9 #[error("unknown or missing command")]
11 UnknownCommand,
12 #[error("unknown command: {command}")]
14 UnknownCommandName {
15 command: String,
17 next: &'static str,
19 },
20 #[error("missing argument: {argument}")]
22 MissingArgument {
23 argument: &'static str,
25 next: &'static str,
27 },
28 #[error("missing value for {flag}")]
30 MissingFlagValue {
31 flag: &'static str,
33 next: &'static str,
35 },
36 #[error("duplicate flag: {flag}")]
38 DuplicateFlag {
39 flag: &'static str,
41 next: &'static str,
43 },
44 #[error("unexpected argument for {command}: {argument}")]
46 UnexpectedArgument {
47 argument: String,
49 command: &'static str,
51 next: &'static str,
53 },
54 #[error("unknown flag: {flag}")]
56 UnknownFlag {
57 flag: String,
59 next: &'static str,
61 },
62 #[error("unsupported flag for {command}: {flag}")]
64 UnsupportedFlag {
65 flag: String,
67 command: &'static str,
69 next: &'static str,
71 },
72 #[error("unknown resource: {resource}")]
74 UnknownResource {
75 resource: String,
77 next: &'static str,
79 },
80 #[error("unknown issue status: {0}")]
82 UnknownStatus(String),
83 #[error("unknown trace status: {0}")]
85 UnknownTraceStatus(String),
86 #[error("unknown log level: {0}")]
88 UnknownLogLevel(String),
89 #[error("invalid limit: {0}")]
91 InvalidLimit(String),
92 #[error("invalid minimum duration: {0}")]
94 InvalidMinDuration(String),
95 #[error("unknown pagination mode")]
97 UnknownPagination,
98 #[error("invalid action cursor: {0}")]
100 InvalidActionCursor(String),
101 #[error("invalid log cursor: {0}")]
103 InvalidLogCursor(String),
104 #[error("invalid issue cursor: {0}")]
106 InvalidIssueCursor(String),
107 #[error("invalid support cursor: {0}")]
109 InvalidSupportCursor(String),
110 #[error("unknown support category")]
112 UnknownSupportCategory,
113 #[error("invalid support ticket id")]
115 InvalidSupportTicketId,
116 #[error("invalid support retry key")]
118 InvalidSupportRetryKey,
119 #[error("invalid support context reply")]
121 InvalidSupportContextReply,
122 #[error("invalid support context command")]
124 InvalidSupportContextCommand,
125 #[error("invalid support context")]
127 InvalidSupportContext,
128 #[error("invalid issue investigation command")]
130 InvalidInvestigationCommand,
131 #[error("invalid project doctor command")]
133 InvalidDoctorCommand,
134 #[error("invalid project create command")]
136 InvalidProjectCreateCommand,
137 #[error("invalid projects command")]
139 InvalidProjectsCommand,
140 #[error("invalid login provider")]
142 InvalidLoginProvider,
143 #[error("invalid usage command")]
145 InvalidUsageCommand,
146 #[error("invalid native debug-artifact command")]
148 InvalidNativeDebugCommand,
149 #[error("invalid native debug-artifact identity")]
151 InvalidNativeDebugIdentity,
152 #[error("invalid setup source: {0}")]
154 InvalidSetupSource(String),
155}
156
157#[derive(Debug, thiserror::Error)]
159pub enum RuntimeError {
160 #[error(transparent)]
162 Cli(#[from] CliError),
163 #[error(transparent)]
165 Io(#[from] std::io::Error),
166 #[error(transparent)]
168 Http(#[from] reqwest::Error),
169 #[error("not logged in: run logbrew login")]
171 MissingToken,
172 #[error("api returned status {status}: {body}")]
174 Api {
175 status: u16,
177 body: String,
179 auth_source: &'static str,
181 auth_label: &'static str,
183 },
184 #[error("LogBrew API unreachable: {message}")]
186 StatusUnavailable {
187 api_url: String,
189 status_code: Option<u16>,
191 body: Option<String>,
193 authenticated: bool,
195 auth_source: &'static str,
197 auth_label: &'static str,
199 message: String,
201 },
202 #[error("{message}")]
204 Unavailable {
205 message: &'static str,
207 next: &'static str,
209 },
210 #[error("issue investigation returned an invalid response")]
212 InvestigationResponseInvalid,
213 #[error("native debug artifact is invalid")]
215 NativeDebugArtifactInvalid,
216 #[error("native debug-artifact response is invalid")]
218 NativeDebugResponseInvalid,
219 #[error("native debug-artifact verification failed")]
221 NativeDebugVerificationFailed,
222}
223
224pub fn write_cli_error<W: std::io::Write>(
230 error: &CliError,
231 json: bool,
232 output: &mut W,
233) -> Result<(), std::io::Error> {
234 if json {
235 let body = serde_json::json!({
236 "ok": false,
237 "error": cli_error_code(error),
238 "message": error.to_string(),
239 "next": cli_error_next_step(error),
240 });
241 writeln!(output, "{body}")
242 } else {
243 writeln!(output, "{error}")?;
244 writeln!(output, "Next: {}", cli_error_next_step(error))
245 }
246}
247
248pub fn write_runtime_error<W: std::io::Write>(
254 error: &RuntimeError,
255 json: bool,
256 output: &mut W,
257) -> Result<(), std::io::Error> {
258 if json {
259 let body = runtime_error_json(error);
260 writeln!(output, "{body}")
261 } else {
262 write_human_runtime_error(error, output)
263 }
264}
265
266pub fn write_native_debug_runtime_error<W: std::io::Write>(
272 error: &RuntimeError,
273 output: &mut W,
274) -> Result<(), std::io::Error> {
275 let body = match error {
276 RuntimeError::Api { status, body, .. } => {
277 let details = ApiErrorDetails::parse(body);
278 let (fallback_code, fallback_next) = native_debug_api_recovery(*status);
279 let code = details
280 .code
281 .as_deref()
282 .and_then(|value| allowed_native_debug_code(*status, value))
283 .unwrap_or(fallback_code);
284 let next = details
285 .next
286 .as_deref()
287 .and_then(|value| allowed_native_debug_next(*status, value))
288 .unwrap_or(fallback_next);
289 serde_json::json!({
290 "ok": false,
291 "error": code,
292 "status": status,
293 "next": next,
294 })
295 }
296 RuntimeError::Cli(_)
297 | RuntimeError::Io(_)
298 | RuntimeError::Http(_)
299 | RuntimeError::MissingToken
300 | RuntimeError::StatusUnavailable { .. }
301 | RuntimeError::Unavailable { .. }
302 | RuntimeError::InvestigationResponseInvalid
303 | RuntimeError::NativeDebugArtifactInvalid
304 | RuntimeError::NativeDebugResponseInvalid
305 | RuntimeError::NativeDebugVerificationFailed => serde_json::json!({
306 "ok": false,
307 "error": runtime_error_code(error),
308 "next": fallback_runtime_error_next_step(error),
309 }),
310 };
311 writeln!(output, "{body}")
312}
313
314const fn native_debug_api_recovery(status: u16) -> (&'static str, &'static str) {
316 match status {
317 400 => (
318 "validation_failed",
319 "check the artifact identity and request scope, then retry",
320 ),
321 401 | 403 => (
322 "unauthorized",
323 "sign in and retry the native debug-artifact command",
324 ),
325 404 => (
326 "not_found",
327 "check the exact project, release, environment, service, UUID, and architecture",
328 ),
329 405 => (
330 "method_not_allowed",
331 "use the supported native debug-artifact request method",
332 ),
333 408 => (
334 "request_timeout",
335 "retry the same native debug-artifact request",
336 ),
337 413 => (
338 "payload_too_large",
339 "reduce the native debug-artifact upload below the documented size limits and retry",
340 ),
341 422 => (
342 "validation_failed",
343 "send manifest and debug_file_N multipart parts from LogBrew Apple release tooling",
344 ),
345 429 => (
346 "rate_limited",
347 "retry the same native debug-artifact command later",
348 ),
349 500..=599 => (
350 "server_error",
351 "retry the same native debug-artifact command later",
352 ),
353 _ => (
354 "unexpected_response",
355 "retry the native debug-artifact command",
356 ),
357 }
358}
359
360fn allowed_native_debug_code(status: u16, value: &str) -> Option<&'static str> {
362 match (status, value) {
363 (400 | 422, "validation_failed") => Some("validation_failed"),
364 (401 | 403, "unauthorized") => Some("unauthorized"),
365 (404, "not_found") => Some("not_found"),
366 (405, "method_not_allowed") => Some("method_not_allowed"),
367 (408, "request_timeout") => Some("request_timeout"),
368 (413, "payload_too_large") => Some("payload_too_large"),
369 (429, "rate_limited") => Some("rate_limited"),
370 (500..=599, "server_error") => Some("server_error"),
371 _ => None,
372 }
373}
374
375fn allowed_native_debug_next(status: u16, value: &str) -> Option<&'static str> {
377 match (status, value) {
378 (400, "check the artifact identity and request scope, then retry") => {
379 Some("check the artifact identity and request scope, then retry")
380 }
381 (401 | 403, "sign in and retry the native debug-artifact command") => {
382 Some("sign in and retry the native debug-artifact command")
383 }
384 (404, "check the exact project, release, environment, service, UUID, and architecture") => {
385 Some("check the exact project, release, environment, service, UUID, and architecture")
386 }
387 (404, "check the exact project and upload scope") => {
388 Some("check the exact project and upload scope")
389 }
390 (404, "start the native debug artifact upload session again with the same manifest") => {
391 Some("start the native debug artifact upload session again with the same manifest")
392 }
393 (405, "use the supported native debug-artifact request method") => {
394 Some("use the supported native debug-artifact request method")
395 }
396 (408, "retry the same native debug-artifact request") => {
397 Some("retry the same native debug-artifact request")
398 }
399 (
400 413,
401 "reduce the native debug-artifact upload below the documented size limits and retry",
402 ) => Some(
403 "reduce the native debug-artifact upload below the documented size limits and retry",
404 ),
405 (
406 422,
407 "send manifest and debug_file_N multipart parts from LogBrew Apple release tooling",
408 ) => Some(
409 "send manifest and debug_file_N multipart parts from LogBrew Apple release tooling",
410 ),
411 (422, "check the native debug-artifact manifest and retry") => {
412 Some("check the native debug-artifact manifest and retry")
413 }
414 (422, "retry only the missing native debug artifact chunk with its exact digest") => {
415 Some("retry only the missing native debug artifact chunk with its exact digest")
416 }
417 (
418 422,
419 "wait briefly, then retry the same upload completion or verify the exact artifact lookup",
420 ) => Some(
421 "wait briefly, then retry the same upload completion or verify the exact artifact lookup",
422 ),
423 (422, "check the native debug-artifact manifest and upload session, then retry") => {
424 Some("check the native debug-artifact manifest and upload session, then retry")
425 }
426 (429 | 500..=599, "retry the same native debug-artifact request later") => {
427 Some("retry the same native debug-artifact request later")
428 }
429 (429 | 500..=599, "retry the same native debug-artifact command later") => {
430 Some("retry the same native debug-artifact command later")
431 }
432 _ => None,
433 }
434}
435
436fn write_human_runtime_error<W: std::io::Write>(
438 error: &RuntimeError,
439 output: &mut W,
440) -> Result<(), std::io::Error> {
441 match error {
442 RuntimeError::StatusUnavailable {
443 api_url,
444 status_code,
445 body,
446 auth_label,
447 message,
448 ..
449 } => {
450 writeln!(output, "LogBrew API unreachable.")?;
451 writeln!(output, "API: {api_url}")?;
452 writeln!(output, "Auth: {auth_label}")?;
453 if let Some(status_code) = status_code {
454 writeln!(output, "Status: {status_code}")?;
455 }
456 if let Some(body) = body.as_ref().filter(|body| !body.is_empty()) {
457 writeln!(output, "Body: {body}")?;
458 } else {
459 writeln!(output, "Reason: {message}")?;
460 }
461 writeln!(output, "Next: {STATUS_UNAVAILABLE_NEXT_STEP}")?;
462 Ok(())
463 }
464 RuntimeError::Api {
465 status,
466 body,
467 auth_label,
468 ..
469 } => {
470 let api_details = ApiErrorDetails::parse(body);
471 writeln!(output, "{error}")?;
472 if let Some(code) = api_details.code.as_deref() {
473 writeln!(output, "Code: {code}")?;
474 }
475 writeln!(output, "Auth: {auth_label}")?;
476 writeln!(output, "Next: {}", api_next_step(*status, &api_details))
477 }
478 RuntimeError::Cli(_)
479 | RuntimeError::Io(_)
480 | RuntimeError::Http(_)
481 | RuntimeError::MissingToken
482 | RuntimeError::InvestigationResponseInvalid
483 | RuntimeError::NativeDebugArtifactInvalid
484 | RuntimeError::NativeDebugResponseInvalid
485 | RuntimeError::NativeDebugVerificationFailed
486 | RuntimeError::Unavailable { .. } => {
487 writeln!(output, "{error}")?;
488 writeln!(output, "Next: {}", runtime_error_next_step(error))?;
489 Ok(())
490 }
491 }
492}
493
494fn runtime_error_json(error: &RuntimeError) -> serde_json::Value {
496 match error {
497 RuntimeError::MissingToken
498 | RuntimeError::Unavailable { .. }
499 | RuntimeError::InvestigationResponseInvalid
500 | RuntimeError::NativeDebugArtifactInvalid
501 | RuntimeError::NativeDebugResponseInvalid
502 | RuntimeError::NativeDebugVerificationFailed
503 | RuntimeError::Io(_)
504 | RuntimeError::Http(_) => serde_json::json!({
505 "ok": false,
506 "error": runtime_error_code(error),
507 "message": error.to_string(),
508 "next": runtime_error_next_step(error),
509 }),
510 RuntimeError::Api {
511 status,
512 body,
513 auth_source,
514 ..
515 } => {
516 let api_details = ApiErrorDetails::parse(body);
517 let next = api_next_step(*status, &api_details);
518 serde_json::json!({
519 "ok": false,
520 "error": runtime_error_code(error),
521 "message": error.to_string(),
522 "status": status,
523 "body": body,
524 "api_error": api_details.error.as_deref(),
525 "api_code": api_details.code.as_deref(),
526 "api_next": api_details.next.as_deref(),
527 "auth_source": auth_source,
528 "next": next,
529 })
530 }
531 RuntimeError::StatusUnavailable {
532 api_url,
533 status_code,
534 body,
535 authenticated,
536 auth_source,
537 message,
538 ..
539 } => serde_json::json!({
540 "ok": false,
541 "error": runtime_error_code(error),
542 "status": "unreachable",
543 "status_code": status_code,
544 "body": body,
545 "api_url": api_url,
546 "authenticated": authenticated,
547 "auth_source": auth_source,
548 "message": message,
549 "next": runtime_error_next_step(error),
550 }),
551 RuntimeError::Cli(error) => serde_json::json!({
552 "ok": false,
553 "error": cli_error_code(error),
554 "message": error.to_string(),
555 "next": cli_error_next_step(error),
556 }),
557 }
558}
559
560const fn cli_error_code(error: &CliError) -> &'static str {
562 match error {
563 CliError::UnknownCommand | CliError::UnknownCommandName { .. } => "unknown_command",
564 CliError::MissingArgument { .. } => "missing_argument",
565 CliError::MissingFlagValue { .. } => "missing_flag_value",
566 CliError::DuplicateFlag { .. } => "duplicate_flag",
567 CliError::UnexpectedArgument { .. } => "unexpected_argument",
568 CliError::UnknownFlag { .. } => "unknown_flag",
569 CliError::UnsupportedFlag { .. } => "unsupported_flag",
570 CliError::UnknownResource { .. } => "unknown_resource",
571 CliError::UnknownStatus(_) => "unknown_status",
572 CliError::UnknownTraceStatus(_) => "unknown_trace_status",
573 CliError::UnknownLogLevel(_) => "unknown_log_level",
574 CliError::InvalidLimit(_) => "invalid_limit",
575 CliError::InvalidMinDuration(_) => "invalid_min_duration",
576 CliError::UnknownPagination => "unknown_pagination",
577 CliError::InvalidActionCursor(_) => "invalid_action_cursor",
578 CliError::InvalidLogCursor(_) => "invalid_log_cursor",
579 CliError::InvalidIssueCursor(_) => "invalid_issue_cursor",
580 CliError::InvalidSupportCursor(_) => "invalid_support_cursor",
581 CliError::UnknownSupportCategory => "unknown_support_category",
582 CliError::InvalidSupportTicketId => "invalid_support_ticket_id",
583 CliError::InvalidSupportRetryKey => "invalid_support_retry_key",
584 CliError::InvalidSupportContextReply => "invalid_support_context_reply",
585 CliError::InvalidSupportContextCommand => "invalid_support_context_command",
586 CliError::InvalidSupportContext => "invalid_support_context",
587 CliError::InvalidInvestigationCommand => "invalid_investigation_command",
588 CliError::InvalidDoctorCommand => "invalid_doctor_command",
589 CliError::InvalidProjectCreateCommand => "invalid_project_create_command",
590 CliError::InvalidProjectsCommand => "invalid_projects_command",
591 CliError::InvalidLoginProvider => "invalid_login_provider",
592 CliError::InvalidUsageCommand => "invalid_usage_command",
593 CliError::InvalidNativeDebugCommand | CliError::InvalidNativeDebugIdentity => {
594 "invalid_native_debug_command"
595 }
596 CliError::InvalidSetupSource(_) => "invalid_setup_source",
597 }
598}
599
600const fn cli_error_next_step(error: &CliError) -> &'static str {
602 match error {
603 CliError::InvalidLimit(_) => "use --limit with a positive whole number",
604 CliError::InvalidMinDuration(_) => "use --min-duration-ms with a non-negative whole number",
605 CliError::UnknownPagination
606 | CliError::InvalidActionCursor(_)
607 | CliError::InvalidLogCursor(_)
608 | CliError::InvalidIssueCursor(_)
609 | CliError::InvalidSupportCursor(_) => {
610 "use --pagination cursor alone for the first page, then use --cursor-time and --cursor-id together from next_cursor"
611 }
612 CliError::UnknownSupportCategory => {
613 "use sdk_install_failure, ingest_failure, auth_failure, project_setup, dashboard_issue, docs_confusion, cli_issue, mobile_issue, billing_question, or other"
614 }
615 CliError::InvalidSupportTicketId => {
616 "use the ticket_id returned by logbrew support create or list"
617 }
618 CliError::InvalidSupportRetryKey => {
619 "use --retry-key with 1 to 128 visible ASCII characters and reuse it only for an exact retry"
620 }
621 CliError::InvalidSupportContextReply => {
622 "use support reply <ticket_id> --context <text> --retry-key <key>"
623 }
624 CliError::InvalidSupportContextCommand => {
625 "use support context <ticket_id> with optional --json"
626 }
627 CliError::InvalidSupportContext => {
628 "use --context with 1 to 4000 characters after trimming whitespace"
629 }
630 CliError::InvalidInvestigationCommand => {
631 "use logbrew investigate issue <issue_id> with optional --json"
632 }
633 CliError::InvalidDoctorCommand => {
634 "use logbrew doctor --project <project_id> with optional --json"
635 }
636 CliError::InvalidProjectCreateCommand => {
637 "use logbrew projects create <name> --ingest-key-file <path> with optional --runtime, --environment, --abandon-retry, and --json"
638 }
639 CliError::InvalidProjectsCommand => {
640 "use logbrew projects with optional --json, or logbrew projects --help"
641 }
642 CliError::InvalidLoginProvider => "use --provider github, gitlab, or bitbucket",
643 CliError::InvalidUsageCommand => "use logbrew usage with optional --json",
644 CliError::InvalidNativeDebugCommand => {
645 "use logbrew debug-artifacts upload <path> --project <project_id> --release <release> --environment <environment> --service <service> with optional --expect-image-uuid, --dry-run, and --json"
646 }
647 CliError::InvalidNativeDebugIdentity => {
648 "use a UUID in 8-4-4-4-12 form and architecture arm64, arm64e, or x86_64"
649 }
650 CliError::InvalidSetupSource(_) => "use --source api, cli, or sdk",
651 CliError::MissingArgument { next, .. }
652 | CliError::MissingFlagValue { next, .. }
653 | CliError::DuplicateFlag { next, .. }
654 | CliError::UnexpectedArgument { next, .. }
655 | CliError::UnsupportedFlag { next, .. }
656 | CliError::UnknownFlag { next, .. }
657 | CliError::UnknownResource { next, .. }
658 | CliError::UnknownCommandName { next, .. } => next,
659 CliError::UnknownCommand => "run logbrew --help",
660 CliError::UnknownStatus(_) => ISSUE_STATUS_VALUES_NEXT_STEP,
661 CliError::UnknownTraceStatus(_) => "use --status error or --status ok",
662 CliError::UnknownLogLevel(_) => "use one of info, warning, error, critical",
663 }
664}
665
666const fn runtime_error_code(error: &RuntimeError) -> &'static str {
668 match error {
669 RuntimeError::Cli(error) => cli_error_code(error),
670 RuntimeError::Io(_) => "io_error",
671 RuntimeError::Http(_) => "http_error",
672 RuntimeError::MissingToken => "not_logged_in",
673 RuntimeError::Api { .. } => "api_error",
674 RuntimeError::StatusUnavailable { .. } => "status_unreachable",
675 RuntimeError::Unavailable { .. } => "unavailable",
676 RuntimeError::InvestigationResponseInvalid => "investigation_response_invalid",
677 RuntimeError::NativeDebugArtifactInvalid => "native_debug_artifact_invalid",
678 RuntimeError::NativeDebugResponseInvalid => "native_debug_response_invalid",
679 RuntimeError::NativeDebugVerificationFailed => "native_debug_verification_failed",
680 }
681}
682
683const STATUS_UNAVAILABLE_NEXT_STEP: &str = "check LOGBREW_API_URL or network";
685
686#[derive(Debug, Clone, Default, PartialEq, Eq)]
688struct ApiErrorDetails {
689 error: Option<String>,
691 code: Option<String>,
693 next: Option<String>,
695}
696
697impl ApiErrorDetails {
698 fn parse(body: &str) -> Self {
700 let Ok(value) = serde_json::from_str::<serde_json::Value>(body) else {
701 return Self::default();
702 };
703
704 Self {
705 error: json_string_field(&value, "error"),
706 code: json_string_field(&value, "code"),
707 next: json_string_field(&value, "next"),
708 }
709 }
710}
711
712fn json_string_field(value: &serde_json::Value, key: &str) -> Option<String> {
714 value
715 .get(key)
716 .and_then(serde_json::Value::as_str)
717 .map(str::trim)
718 .filter(|value| !value.is_empty())
719 .map(ToOwned::to_owned)
720}
721
722fn runtime_error_next_step(error: &RuntimeError) -> Cow<'static, str> {
724 match error {
725 RuntimeError::Api { status, body, .. } => {
726 let api_details = ApiErrorDetails::parse(body);
727 api_next_step(*status, &api_details)
728 }
729 RuntimeError::Cli(_)
730 | RuntimeError::Io(_)
731 | RuntimeError::Http(_)
732 | RuntimeError::MissingToken
733 | RuntimeError::InvestigationResponseInvalid
734 | RuntimeError::NativeDebugArtifactInvalid
735 | RuntimeError::NativeDebugResponseInvalid
736 | RuntimeError::NativeDebugVerificationFailed
737 | RuntimeError::StatusUnavailable { .. }
738 | RuntimeError::Unavailable { .. } => {
739 Cow::Borrowed(fallback_runtime_error_next_step(error))
740 }
741 }
742}
743
744fn api_next_step(status: u16, api_details: &ApiErrorDetails) -> Cow<'static, str> {
746 api_details.next.as_ref().map_or_else(
747 || Cow::Borrowed(fallback_api_next_step(status)),
748 |next| Cow::Owned(next.clone()),
749 )
750}
751
752const fn fallback_api_next_step(status: u16) -> &'static str {
754 match status {
755 401 | 403 => "run logbrew login",
756 400 | 422 => "check command arguments or filters",
757 404 => "check the resource id or filters",
758 429 => "retry later",
759 500..=599 => "check LOGBREW_API_URL or retry later",
760 _ => "check command arguments or retry later",
761 }
762}
763
764const fn fallback_runtime_error_next_step(error: &RuntimeError) -> &'static str {
766 match error {
767 RuntimeError::Cli(error) => cli_error_next_step(error),
768 RuntimeError::MissingToken => "run logbrew login",
769 RuntimeError::Api { status, .. } => fallback_api_next_step(*status),
770 RuntimeError::StatusUnavailable { .. } | RuntimeError::Http(_) => {
771 STATUS_UNAVAILABLE_NEXT_STEP
772 }
773 RuntimeError::Unavailable { next, .. } => next,
774 RuntimeError::InvestigationResponseInvalid => {
775 "retry the issue investigation; if it repeats, report the public response contract"
776 }
777 RuntimeError::NativeDebugArtifactInvalid => {
778 "provide one validated Apple dSYM, ZIP, or Mach-O object matching every --expect-image-uuid value"
779 }
780 RuntimeError::NativeDebugResponseInvalid => {
781 "retry the native debug-artifact request; if it repeats, report the public response contract"
782 }
783 RuntimeError::NativeDebugVerificationFailed => {
784 "retry exact native debug-artifact lookup before using native symbolication"
785 }
786 RuntimeError::Io(_) => "check local files and permissions",
787 }
788}