1use serde::{Deserialize, Serialize};
47use std::fmt;
48
49#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
53#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
54#[non_exhaustive]
55pub enum ErrorCode {
56 ConfigNotFound,
61 ConfigReadError,
63 ConfigParseError,
65 ConfigValidationError,
67 ConfigEnvError,
69 ConfigProfileNotFound,
71 ConfigNoWorkers,
73 ConfigInvalidWorker,
75 ConfigSshKeyError,
77 ConfigSocketPathError,
79
80 PathDepManifestParseFailed,
83 PathDepMissing,
85 PathDepCyclic,
87 PathDepPolicyViolation,
89 PathDepMetadataFailed,
91 PathDepMetadataParseFailed,
93
94 ClosurePlanFailed,
97 ClosureFailOpen,
99 ClosureHighRisk,
101 ClosureMissingData,
103 ClosureNonDeterministic,
105 ClosureFingerprintMismatch,
107
108 SshConnectionFailed,
113 SshAuthFailed,
115 SshKeyError,
117 SshHostKeyError,
119 SshTimeout,
121 SshSessionDropped,
123 NetworkDnsError,
125 NetworkUnreachable,
127 NetworkConnectionRefused,
129 NetworkTimeout,
131
132 WorkerNoneAvailable,
137 WorkerAllUnhealthy,
139 WorkerHealthCheckFailed,
141 WorkerSelfTestFailed,
143 WorkerAtCapacity,
145 WorkerMissingToolchain,
147 WorkerStateError,
149 WorkerCircuitOpen,
151 WorkerSelectionFailed,
153 WorkerLoadQueryFailed,
155
156 WorkerDiskPressureCritical,
159 WorkerDiskPressureWarning,
161 WorkerTelemetryGap,
163 WorkerDiskIoHigh,
165 WorkerMemoryPressureHigh,
167 WorkerReclaimFailed,
169 WorkerDiskHeadroomInsufficient,
171 WorkerReclaimProtected,
173
174 BuildCompilationFailed,
179 BuildUnknownCommand,
181 BuildKilledBySignal,
183 BuildTimeout,
185 BuildOutputError,
187 BuildWorkdirError,
189 BuildToolchainError,
191 BuildEnvError,
193 BuildIncrementalError,
195 BuildArtifactMissing,
197 BuildArtifactSyncEmpty,
200 BuildArtifactForeignTarget,
203 BuildCargoWorkspaceInheritance,
205
206 ProcessTriageAdapterUnavailable,
209 ProcessTriageDetectorUncertain,
211 ProcessTriagePolicyViolation,
213 ProcessTriageTransportError,
215 ProcessTriageExecutorError,
217 ProcessTriageTimeout,
219 ProcessTriagePartialResult,
221 ProcessTriageInvalidRequest,
223
224 CancelGracefulSent,
227 CancelEscalatedKill,
229 CancelRemoteKillFailed,
231 CancelCleanupFailed,
233 CancelSlotLeak,
235 CancelTimeoutExceeded,
237
238 TransferRsyncFailed,
243 TransferTimeout,
245 TransferSourceMissing,
247 TransferDestError,
249 TransferDiskFull,
251 TransferPermissionDenied,
253 TransferChecksumError,
255 TransferBinaryFailed,
257 TransferIncomplete,
259 TransferProtocolError,
261 DependencyPreflightMissing,
263 DependencyPreflightStale,
265 DependencyPreflightUnknown,
267 DependencyPreflightPolicyViolation,
269 DependencyPreflightTimeout,
271
272 InternalDaemonSocket,
277 InternalDaemonProtocol,
279 InternalDaemonNotRunning,
281 InternalIpcError,
283 InternalStateError,
285 InternalSerdeError,
287 InternalHookError,
289 InternalMetricsError,
291 InternalLoggingError,
293 InternalUpdateError,
295}
296
297impl ErrorCode {
298 #[must_use]
300 pub const fn code_number(&self) -> u16 {
301 match self {
302 Self::ConfigNotFound => 1,
304 Self::ConfigReadError => 2,
305 Self::ConfigParseError => 3,
306 Self::ConfigValidationError => 4,
307 Self::ConfigEnvError => 5,
308 Self::ConfigProfileNotFound => 6,
309 Self::ConfigNoWorkers => 7,
310 Self::ConfigInvalidWorker => 8,
311 Self::ConfigSshKeyError => 9,
312 Self::ConfigSocketPathError => 10,
313
314 Self::PathDepManifestParseFailed => 13,
316 Self::PathDepMissing => 14,
317 Self::PathDepCyclic => 15,
318 Self::PathDepPolicyViolation => 16,
319 Self::PathDepMetadataFailed => 17,
320 Self::PathDepMetadataParseFailed => 18,
321
322 Self::ClosurePlanFailed => 19,
324 Self::ClosureFailOpen => 20,
325 Self::ClosureHighRisk => 21,
326 Self::ClosureMissingData => 22,
327 Self::ClosureNonDeterministic => 23,
328 Self::ClosureFingerprintMismatch => 24,
329
330 Self::SshConnectionFailed => 100,
332 Self::SshAuthFailed => 101,
333 Self::SshKeyError => 102,
334 Self::SshHostKeyError => 103,
335 Self::SshTimeout => 104,
336 Self::SshSessionDropped => 105,
337 Self::NetworkDnsError => 106,
338 Self::NetworkUnreachable => 107,
339 Self::NetworkConnectionRefused => 108,
340 Self::NetworkTimeout => 109,
341
342 Self::WorkerNoneAvailable => 200,
344 Self::WorkerAllUnhealthy => 201,
345 Self::WorkerHealthCheckFailed => 202,
346 Self::WorkerSelfTestFailed => 203,
347 Self::WorkerAtCapacity => 204,
348 Self::WorkerMissingToolchain => 205,
349 Self::WorkerStateError => 206,
350 Self::WorkerCircuitOpen => 207,
351 Self::WorkerSelectionFailed => 208,
352 Self::WorkerLoadQueryFailed => 209,
353
354 Self::WorkerDiskPressureCritical => 210,
356 Self::WorkerDiskPressureWarning => 211,
357 Self::WorkerTelemetryGap => 212,
358 Self::WorkerDiskIoHigh => 213,
359 Self::WorkerMemoryPressureHigh => 214,
360 Self::WorkerReclaimFailed => 215,
361 Self::WorkerDiskHeadroomInsufficient => 216,
362 Self::WorkerReclaimProtected => 217,
363
364 Self::BuildCompilationFailed => 300,
366 Self::BuildUnknownCommand => 301,
367 Self::BuildKilledBySignal => 302,
368 Self::BuildTimeout => 303,
369 Self::BuildOutputError => 304,
370 Self::BuildWorkdirError => 305,
371 Self::BuildToolchainError => 306,
372 Self::BuildEnvError => 307,
373 Self::BuildIncrementalError => 308,
374 Self::BuildArtifactMissing => 309,
375 Self::BuildArtifactSyncEmpty => 326,
378 Self::BuildArtifactForeignTarget => 327,
379 Self::BuildCargoWorkspaceInheritance => 329,
380
381 Self::ProcessTriageAdapterUnavailable => 310,
383 Self::ProcessTriageDetectorUncertain => 311,
384 Self::ProcessTriagePolicyViolation => 312,
385 Self::ProcessTriageTransportError => 313,
386 Self::ProcessTriageExecutorError => 314,
387 Self::ProcessTriageTimeout => 315,
388 Self::ProcessTriagePartialResult => 316,
389 Self::ProcessTriageInvalidRequest => 317,
390
391 Self::CancelGracefulSent => 320,
393 Self::CancelEscalatedKill => 321,
394 Self::CancelRemoteKillFailed => 322,
395 Self::CancelCleanupFailed => 323,
396 Self::CancelSlotLeak => 324,
397 Self::CancelTimeoutExceeded => 325,
398
399 Self::TransferRsyncFailed => 400,
401 Self::TransferTimeout => 401,
402 Self::TransferSourceMissing => 402,
403 Self::TransferDestError => 403,
404 Self::TransferDiskFull => 404,
405 Self::TransferPermissionDenied => 405,
406 Self::TransferChecksumError => 406,
407 Self::TransferBinaryFailed => 407,
408 Self::TransferIncomplete => 408,
409 Self::TransferProtocolError => 409,
410 Self::DependencyPreflightMissing => 410,
411 Self::DependencyPreflightStale => 411,
412 Self::DependencyPreflightUnknown => 412,
413 Self::DependencyPreflightPolicyViolation => 413,
414 Self::DependencyPreflightTimeout => 414,
415
416 Self::InternalDaemonSocket => 500,
418 Self::InternalDaemonProtocol => 501,
419 Self::InternalDaemonNotRunning => 502,
420 Self::InternalIpcError => 503,
421 Self::InternalStateError => 504,
422 Self::InternalSerdeError => 505,
423 Self::InternalHookError => 506,
424 Self::InternalMetricsError => 507,
425 Self::InternalLoggingError => 508,
426 Self::InternalUpdateError => 509,
427 }
428 }
429
430 #[must_use]
432 pub fn code_string(&self) -> String {
433 format!("RCH-E{:03}", self.code_number())
434 }
435
436 #[must_use]
438 pub const fn category(&self) -> ErrorCategory {
439 match self.code_number() {
440 1..=99 => ErrorCategory::Config,
441 100..=199 => ErrorCategory::Network,
442 200..=299 => ErrorCategory::Worker,
443 300..=399 => ErrorCategory::Build,
444 400..=499 => ErrorCategory::Transfer,
445 500..=599 => ErrorCategory::Internal,
446 _ => ErrorCategory::Internal,
447 }
448 }
449
450 #[must_use]
452 pub fn entry(&self) -> ErrorEntry {
453 ErrorEntry {
454 code: self.code_string(),
455 category: self.category(),
456 message: self.message().to_string(),
457 remediation: self
458 .remediation()
459 .iter()
460 .map(|s| (*s).to_string())
461 .collect(),
462 doc_url: self.doc_url().map(String::from),
463 }
464 }
465
466 #[must_use]
468 pub const fn message(&self) -> &'static str {
469 match self {
470 Self::ConfigNotFound => "Configuration file not found",
472 Self::ConfigReadError => "Failed to read configuration file",
473 Self::ConfigParseError => "Configuration file contains invalid TOML syntax",
474 Self::ConfigValidationError => "Configuration contains invalid values",
475 Self::ConfigEnvError => "Environment variable has invalid value",
476 Self::ConfigProfileNotFound => "Profile not found in configuration",
477 Self::ConfigNoWorkers => "No workers are configured",
478 Self::ConfigInvalidWorker => "Worker configuration is invalid",
479 Self::ConfigSshKeyError => "SSH key path is invalid or inaccessible",
480 Self::ConfigSocketPathError => "Socket path is invalid or inaccessible",
481
482 Self::PathDepManifestParseFailed => {
484 "Cargo manifest parse failure during path-dependency resolution"
485 }
486 Self::PathDepMissing => "Path dependency declared but target directory not found",
487 Self::PathDepCyclic => "Cyclic path dependency detected in dependency graph",
488 Self::PathDepPolicyViolation => {
489 "Path dependency violates canonical-root topology policy"
490 }
491 Self::PathDepMetadataFailed => "cargo metadata invocation failed",
492 Self::PathDepMetadataParseFailed => "cargo metadata output could not be parsed",
493
494 Self::ClosurePlanFailed => "Dependency closure plan computation failed",
496 Self::ClosureFailOpen => {
497 "Dependency closure entered fail-open state due to unverifiable data"
498 }
499 Self::ClosureHighRisk => "High-risk path dependencies detected in closure plan",
500 Self::ClosureMissingData => "Required dependency closure data is missing or incomplete",
501 Self::ClosureNonDeterministic => "Closure sync action ordering is non-deterministic",
502 Self::ClosureFingerprintMismatch => "Closure manifest fingerprint mismatch detected",
503
504 Self::SshConnectionFailed => "SSH connection to worker failed",
506 Self::SshAuthFailed => "SSH authentication failed",
507 Self::SshKeyError => "SSH key not found or has invalid format",
508 Self::SshHostKeyError => "SSH host key verification failed",
509 Self::SshTimeout => "SSH command execution timed out",
510 Self::SshSessionDropped => "SSH session terminated unexpectedly",
511 Self::NetworkDnsError => "DNS resolution failed for worker host",
512 Self::NetworkUnreachable => "Network is unreachable",
513 Self::NetworkConnectionRefused => "Connection refused by remote host",
514 Self::NetworkTimeout => "TCP connection timed out",
515
516 Self::WorkerNoneAvailable => "No workers available for selection",
518 Self::WorkerAllUnhealthy => "All configured workers are unhealthy",
519 Self::WorkerHealthCheckFailed => "Worker failed health check",
520 Self::WorkerSelfTestFailed => "Worker self-test failed",
521 Self::WorkerAtCapacity => "Worker is at maximum capacity",
522 Self::WorkerMissingToolchain => "Worker is missing required toolchain",
523 Self::WorkerStateError => "Worker state is inconsistent",
524 Self::WorkerCircuitOpen => "Worker circuit breaker is open",
525 Self::WorkerSelectionFailed => "Worker selection strategy failed",
526 Self::WorkerLoadQueryFailed => "Failed to query worker load",
527
528 Self::WorkerDiskPressureCritical => "Worker disk usage is critically high",
530 Self::WorkerDiskPressureWarning => "Worker disk usage has exceeded warning threshold",
531 Self::WorkerTelemetryGap => "Worker disk pressure telemetry is stale or missing",
532 Self::WorkerDiskIoHigh => "Worker disk I/O utilization is too high for scheduling",
533 Self::WorkerMemoryPressureHigh => "Worker memory pressure exceeds scheduling threshold",
534 Self::WorkerReclaimFailed => "Disk reclaim operation failed on worker",
535 Self::WorkerDiskHeadroomInsufficient => {
536 "Insufficient disk headroom for build reservation"
537 }
538 Self::WorkerReclaimProtected => "Active build protection prevented reclaim operation",
539
540 Self::BuildCompilationFailed => "Remote compilation failed",
542 Self::BuildUnknownCommand => "Build command not recognized",
543 Self::BuildKilledBySignal => "Build process was killed by signal",
544 Self::BuildTimeout => "Build operation timed out",
545 Self::BuildOutputError => "Failed to capture build output",
546 Self::BuildWorkdirError => "Remote working directory error",
547 Self::BuildToolchainError => "Toolchain wrapper failed",
548 Self::BuildEnvError => "Build environment setup failed",
549 Self::BuildIncrementalError => "Incremental build state is corrupted",
550 Self::BuildArtifactMissing => "Build artifact not found",
551 Self::BuildArtifactSyncEmpty => {
552 "Remote build succeeded but no build outputs were synced back"
553 }
554 Self::BuildArtifactForeignTarget => {
555 "Retrieved build artifacts target a different platform than this host"
556 }
557 Self::BuildCargoWorkspaceInheritance => {
558 "Cargo workspace inheritance failed under remote topology"
559 }
560
561 Self::ProcessTriageAdapterUnavailable => {
563 "Process triage adapter is unavailable or not installed"
564 }
565 Self::ProcessTriageDetectorUncertain => {
566 "Process detector could not classify with sufficient confidence"
567 }
568 Self::ProcessTriagePolicyViolation => {
569 "Process triage action violates safe-action policy"
570 }
571 Self::ProcessTriageTransportError => {
572 "Transport error communicating with process triage adapter"
573 }
574 Self::ProcessTriageExecutorError => {
575 "Process triage executor encountered a runtime error"
576 }
577 Self::ProcessTriageTimeout => "Process triage operation timed out",
578 Self::ProcessTriagePartialResult => {
579 "Process triage returned partial or incomplete results"
580 }
581 Self::ProcessTriageInvalidRequest => "Invalid process triage request",
582
583 Self::CancelGracefulSent => "Graceful cancel signal dispatched",
585 Self::CancelEscalatedKill => "Escalated to forced kill after timeout",
586 Self::CancelRemoteKillFailed => "Failed to kill remote process via SSH",
587 Self::CancelCleanupFailed => "Post-cancel cleanup encountered errors",
588 Self::CancelSlotLeak => "Slots not properly released after cancel",
589 Self::CancelTimeoutExceeded => "Cancellation exceeded policy time budget",
590
591 Self::TransferRsyncFailed => "Rsync transfer failed",
593 Self::TransferTimeout => "File sync operation timed out",
594 Self::TransferSourceMissing => "Source files not found",
595 Self::TransferDestError => "Destination path error",
596 Self::TransferDiskFull => "Insufficient disk space on worker",
597 Self::TransferPermissionDenied => "Permission denied during file transfer",
598 Self::TransferChecksumError => "Transfer checksum mismatch",
599 Self::TransferBinaryFailed => "Binary download failed",
600 Self::TransferIncomplete => "Transfer completed partially",
601 Self::TransferProtocolError => "Transfer protocol error",
602 Self::DependencyPreflightMissing => {
603 "Remote dependency preflight found a missing required path"
604 }
605 Self::DependencyPreflightStale => {
606 "Remote dependency preflight found a stale dependency root"
607 }
608 Self::DependencyPreflightUnknown => {
609 "Remote dependency preflight could not verify dependency state"
610 }
611 Self::DependencyPreflightPolicyViolation => {
612 "Remote dependency preflight blocked a topology policy violation"
613 }
614 Self::DependencyPreflightTimeout => "Remote dependency preflight planning timed out",
615
616 Self::InternalDaemonSocket => "Failed to connect to daemon socket",
618 Self::InternalDaemonProtocol => "Daemon protocol error",
619 Self::InternalDaemonNotRunning => "RCH daemon is not running",
620 Self::InternalIpcError => "Inter-process communication error",
621 Self::InternalStateError => "Unexpected internal state",
622 Self::InternalSerdeError => "Serialization/deserialization error",
623 Self::InternalHookError => "Hook execution failed",
624 Self::InternalMetricsError => "Metrics collection error",
625 Self::InternalLoggingError => "Logging system error",
626 Self::InternalUpdateError => "Update check failed",
627 }
628 }
629
630 #[must_use]
632 pub const fn remediation(&self) -> &'static [&'static str] {
633 match self {
634 Self::ConfigNotFound => &[
636 "Run 'rch init' to create a default configuration",
637 "Check if ~/.config/rch/config.toml exists",
638 "Set RCH_CONFIG_DIR environment variable to specify custom config directory",
639 ],
640 Self::ConfigReadError => &[
641 "Check file permissions on the configuration file",
642 "Verify the file is not corrupted",
643 "Ensure no other process has locked the file",
644 ],
645 Self::ConfigParseError => &[
646 "Run 'rch config validate' to identify syntax errors",
647 "Check TOML syntax at the indicated line",
648 "Ensure all strings are properly quoted",
649 ],
650 Self::ConfigValidationError => &[
651 "Run 'rch config validate' for detailed diagnostics",
652 "Check that all required fields are present",
653 "Verify values are within allowed ranges",
654 ],
655 Self::ConfigEnvError => &[
656 "Check the environment variable value format",
657 "Unset the variable to use config file defaults",
658 "See 'rch help env' for valid environment variables",
659 ],
660 Self::ConfigProfileNotFound => &[
661 "List available profiles with 'rch config profiles'",
662 "Create the profile in your configuration file",
663 "Check for typos in the profile name",
664 ],
665 Self::ConfigNoWorkers => &[
666 "Add at least one worker to your configuration",
667 "Run 'rch discover' to find available workers",
668 "Check the [workers] section in your config",
669 ],
670 Self::ConfigInvalidWorker => &[
671 "Verify worker hostname is correct",
672 "Check SSH username and key configuration",
673 "Ensure remote_base_dir is a valid path",
674 ],
675 Self::ConfigSshKeyError => &[
676 "Check that the SSH key file exists",
677 "Verify file permissions (should be 600)",
678 "Ensure the key format is valid (ssh-keygen -y -f KEY)",
679 ],
680 Self::ConfigSocketPathError => &[
681 "Check directory permissions for socket path",
682 "Ensure parent directory exists",
683 "Try using the default socket path",
684 ],
685
686 Self::PathDepManifestParseFailed => &[
688 "Check Cargo.toml syntax with 'cargo verify-project'",
689 "Ensure all path-dependency Cargo.toml files are valid TOML",
690 "Run 'cargo metadata' manually to see detailed parse errors",
691 ],
692 Self::PathDepMissing => &[
693 "Verify the path in Cargo.toml [dependencies] exists on disk",
694 "Check for typos in the dependency path value",
695 "Ensure all workspace members are checked out",
696 ],
697 Self::PathDepCyclic => &[
698 "Review the path dependency graph for cycles",
699 "Run 'cargo metadata' to visualize the dependency tree",
700 "Break the cycle by restructuring crate boundaries",
701 ],
702 Self::PathDepPolicyViolation => &[
703 "Ensure all path dependencies are under the configured canonical root (path_topology.canonical_root, default /data/projects)",
704 "Check that paths resolve within allowed topology prefixes",
705 "Review the PathTopologyPolicy configuration",
706 ],
707 Self::PathDepMetadataFailed => &[
708 "Verify 'cargo' is installed and on PATH",
709 "Check that Cargo.toml is a valid project manifest",
710 "Try running 'cargo metadata --format-version=1' manually",
711 ],
712 Self::PathDepMetadataParseFailed => &[
713 "Run 'cargo metadata --format-version=1' and check JSON output",
714 "Ensure cargo version is recent enough for the workspace layout",
715 "Check for toolchain incompatibilities with rust-toolchain.toml",
716 ],
717
718 Self::ClosurePlanFailed => &[
720 "Check that all path dependencies are resolvable",
721 "Run 'cargo metadata' to verify dependency graph health",
722 "Review dependency closure planner logs for specific failures",
723 ],
724 Self::ClosureFailOpen => &[
725 "The transfer will proceed with project root only (fail-open semantics)",
726 "Check path dependency graph health to restore full closure",
727 "Review the fail-open reason in structured diagnostics output",
728 ],
729 Self::ClosureHighRisk => &[
730 "Review the high-risk dependencies flagged in the plan",
731 "Ensure all dependency paths are canonical and stable",
732 "Consider pinning dependency versions to reduce risk",
733 ],
734 Self::ClosureMissingData => &[
735 "Ensure Cargo.toml and Cargo.lock are present and valid",
736 "Check that all workspace members are accessible",
737 "Run 'cargo update' to regenerate lock file if needed",
738 ],
739 Self::ClosureNonDeterministic => &[
740 "Report this as a bug — closure ordering must be deterministic",
741 "Check for filesystem race conditions or concurrent modifications",
742 "Retry the operation to see if the ordering stabilizes",
743 ],
744 Self::ClosureFingerprintMismatch => &[
745 "A dependency manifest changed since the plan was computed",
746 "Recompute the closure plan to pick up the latest manifests",
747 "Check for concurrent modifications to Cargo.toml files",
748 ],
749
750 Self::SshConnectionFailed => &[
752 "Verify the worker host is reachable: ping <host>",
753 "Check that SSH service is running on the worker",
754 "Verify firewall allows SSH (port 22)",
755 "Try connecting manually: ssh <user>@<host>",
756 ],
757 Self::SshAuthFailed => &[
758 "Verify SSH key is in authorized_keys on the worker",
759 "Check SSH key passphrase if applicable",
760 "Ensure ssh-agent is running with key loaded",
761 "Try: ssh-add -l to list loaded keys",
762 ],
763 Self::SshKeyError => &[
764 "Check that the SSH key file exists at the configured path",
765 "Verify key file permissions are 600",
766 "Regenerate key if format is corrupted",
767 ],
768 Self::SshHostKeyError => &[
769 "Accept the host key: ssh <user>@<host> (confirm fingerprint)",
770 "Check known_hosts for conflicting entries",
771 "Update known_hosts_policy in config if appropriate",
772 ],
773 Self::SshTimeout => &[
774 "Check network connectivity to the worker",
775 "Increase timeout in configuration",
776 "Verify worker is not overloaded",
777 ],
778 Self::SshSessionDropped => &[
779 "Check network stability",
780 "Verify worker has not rebooted",
781 "Look for keepalive settings in SSH config",
782 ],
783 Self::NetworkDnsError => &[
784 "Verify worker hostname is correct",
785 "Check DNS server configuration",
786 "Try using IP address instead of hostname",
787 ],
788 Self::NetworkUnreachable => &[
789 "Check network connection on local machine",
790 "Verify VPN connection if required",
791 "Check routing to worker network",
792 ],
793 Self::NetworkConnectionRefused => &[
794 "Verify SSH service is running on worker",
795 "Check if worker firewall allows connections",
796 "Ensure correct port is being used",
797 ],
798 Self::NetworkTimeout => &[
799 "Check network latency to worker",
800 "Verify worker is responsive",
801 "Increase connection timeout in config",
802 ],
803
804 Self::WorkerNoneAvailable => &[
806 "Configure at least one worker in config.toml",
807 "Run 'rch discover' to find available workers",
808 "Check that configured workers are enabled",
809 ],
810 Self::WorkerAllUnhealthy => &[
811 "Run 'rch doctor' to diagnose worker issues",
812 "Check individual worker connectivity",
813 "Review worker health check logs",
814 ],
815 Self::WorkerHealthCheckFailed => &[
816 "Verify SSH connectivity to worker",
817 "Check worker disk space and load",
818 "Review health check timeout settings",
819 ],
820 Self::WorkerSelfTestFailed => &[
821 "Run 'rch self-test --worker <name>' for details",
822 "Verify Rust toolchain on worker",
823 "Check worker has required dependencies",
824 ],
825 Self::WorkerAtCapacity => &[
826 "Wait for current builds to complete",
827 "Add more workers to distribute load",
828 "Increase max_concurrent_builds on worker",
829 ],
830 Self::WorkerMissingToolchain => &[
831 "Install required toolchain on worker",
832 "Run 'rustup show' on worker to verify",
833 "Update worker toolchain configuration",
834 ],
835 Self::WorkerStateError => &[
836 "Restart the RCH daemon: rchd restart",
837 "Check for stale lock files",
838 "Review daemon logs for details",
839 ],
840 Self::WorkerCircuitOpen => &[
841 "Wait for circuit breaker reset period",
842 "Check worker health manually",
843 "Review recent build failures on worker",
844 ],
845 Self::WorkerSelectionFailed => &[
846 "Verify at least one worker is healthy",
847 "Check selection strategy configuration",
848 "Review worker weights and priorities",
849 ],
850 Self::WorkerLoadQueryFailed => &[
851 "Verify SSH connectivity to worker",
852 "Check that load query command works on worker",
853 "Review timeout settings for load queries",
854 ],
855
856 Self::WorkerDiskPressureCritical => &[
858 "Worker disk usage is above 95% — builds will not be scheduled here",
859 "Clean up old build caches: rch cache clean --worker <id>",
860 "Check disk usage on worker: ssh <worker> df -h",
861 ],
862 Self::WorkerDiskPressureWarning => &[
863 "Worker disk usage is above 80% — scheduling priority reduced",
864 "Consider cleaning old caches: rch cache clean --worker <id>",
865 "Monitor disk usage trend to prevent critical state",
866 ],
867 Self::WorkerTelemetryGap => &[
868 "Worker disk telemetry is stale — pressure assessment is unreliable",
869 "Check worker health: rch workers probe <id>",
870 "Verify telemetry collection is running on the worker",
871 ],
872 Self::WorkerDiskIoHigh => &[
873 "Worker disk I/O is saturated — builds may experience latency",
874 "Wait for current I/O-heavy operations to complete",
875 "Check for stuck or runaway processes: rch workers probe <id>",
876 ],
877 Self::WorkerMemoryPressureHigh => &[
878 "Worker memory pressure is high — scheduling priority reduced",
879 "Check for memory leaks or over-committed builds on the worker",
880 "Review worker slot count to prevent over-scheduling",
881 ],
882 Self::WorkerReclaimFailed => &[
883 "Disk space reclaim operation failed on the worker",
884 "Check worker filesystem health and permissions",
885 "Inspect both root and temp usage: ssh <worker> 'df -h / /tmp'",
886 ],
887 Self::WorkerDiskHeadroomInsufficient => &[
888 "Estimated build disk requirement exceeds available free space",
889 "Try a different worker with more headroom",
890 "Clean up old build artifacts to free space",
891 ],
892 Self::WorkerReclaimProtected => &[
893 "Active build artifacts were protected from reclaim",
894 "Wait for current builds to complete before retrying reclaim",
895 "Only idle cache entries are eligible for eviction",
896 ],
897
898 Self::BuildCompilationFailed => &[
900 "Review compilation errors in output",
901 "Verify code compiles locally first",
902 "Check for missing dependencies on worker",
903 ],
904 Self::BuildUnknownCommand => &[
905 "Check that the command is supported",
906 "Verify cargo/rustc version compatibility",
907 "Review RCH command pattern configuration",
908 ],
909 Self::BuildKilledBySignal => &[
910 "Check worker system logs for OOM killer",
911 "Review build memory requirements",
912 "Check if build was manually interrupted",
913 ],
914 Self::BuildTimeout => &[
915 "Increase build timeout in configuration",
916 "Check for infinite loops or hangs",
917 "Verify worker is not overloaded",
918 ],
919 Self::BuildOutputError => &[
920 "Check worker disk space",
921 "Verify PTY allocation settings",
922 "Review output buffer configuration",
923 ],
924 Self::BuildWorkdirError => &[
925 "Verify remote_base_dir is writable",
926 "Check directory permissions on worker",
927 "Ensure path does not contain special characters",
928 ],
929 Self::BuildToolchainError => &[
930 "Verify toolchain is installed on worker",
931 "Check rustup default toolchain",
932 "Review toolchain override settings",
933 ],
934 Self::BuildEnvError => &[
935 "Check environment variable configuration",
936 "Verify required environment is set on worker",
937 "Review shell initialization on worker",
938 ],
939 Self::BuildIncrementalError => &[
940 "Run 'cargo clean' on remote workspace",
941 "Delete incremental compilation cache",
942 "Try full rebuild with --release",
943 ],
944 Self::BuildArtifactMissing => &[
945 "Verify build completed successfully",
946 "Check artifact path configuration",
947 "Review build output for artifact location",
948 ],
949 Self::BuildArtifactSyncEmpty => &[
950 "Treat the local build as INCOMPLETE: local binaries may be stale",
951 "Re-run the build (or build locally) before trusting any binary",
952 "Check that the cargo profile's output directory matches the artifact patterns",
953 ],
954 Self::BuildArtifactForeignTarget => &[
955 "Treat the local target directory as POISONED: rebuild for this host",
956 "Pin the build to an explicit --target so the output triple is unambiguous",
957 "Restrict the project to a same-platform worker (RCH_WORKER, or a worker `os` declaration)",
958 ],
959 Self::BuildCargoWorkspaceInheritance => &[
960 "Verify dependency workspace metadata roots are synced",
961 "Isolate remote sync roots from unrelated outer Cargo workspaces",
962 "Check path dependency manifests for workspace-inherited fields",
963 ],
964
965 Self::ProcessTriageAdapterUnavailable => &[
967 "Ensure the process triage adapter binary is installed",
968 "Check PATH includes the adapter binary location",
969 "Verify the adapter version is compatible with this RCH version",
970 ],
971 Self::ProcessTriageDetectorUncertain => &[
972 "Process classification was inconclusive — no action taken",
973 "Review the process list manually for suspicious entries",
974 "Adjust detector confidence threshold if false negatives are common",
975 ],
976 Self::ProcessTriagePolicyViolation => &[
977 "The requested action is blocked by safe-action policy",
978 "Review the escalation level required for this action class",
979 "Use a lower-risk action class or request manual approval",
980 ],
981 Self::ProcessTriageTransportError => &[
982 "Communication with the process triage adapter failed",
983 "Verify the adapter process is running and responsive",
984 "Check for socket/pipe errors in adapter logs",
985 ],
986 Self::ProcessTriageExecutorError => &[
987 "The process triage executor encountered a runtime error",
988 "Check adapter logs for detailed error output",
989 "Verify the target process is still running",
990 ],
991 Self::ProcessTriageTimeout => &[
992 "Process triage operation exceeded the configured timeout",
993 "Increase timeout in ProcessTriageTimeoutPolicy if needed",
994 "Check for adapter hangs or system-level resource contention",
995 ],
996 Self::ProcessTriagePartialResult => &[
997 "Not all requested triage actions completed successfully",
998 "Review the partial result for which actions succeeded",
999 "Retry failed actions individually for better diagnostics",
1000 ],
1001 Self::ProcessTriageInvalidRequest => &[
1002 "The process triage request is malformed or missing required fields",
1003 "Validate request against the ProcessTriage contract schema",
1004 "Check the contract schema version compatibility",
1005 ],
1006
1007 Self::CancelGracefulSent => &[
1009 "Graceful cancellation signal (SIGTERM) was sent to the build process",
1010 "The build should terminate within the grace period",
1011 "Use force cancel if the process does not respond",
1012 ],
1013 Self::CancelEscalatedKill => &[
1014 "Build did not respond to graceful cancel within the grace period",
1015 "SIGKILL was sent to forcefully terminate the process",
1016 "Check worker for orphaned processes if this occurs frequently",
1017 ],
1018 Self::CancelRemoteKillFailed => &[
1019 "SSH kill command to the remote worker failed",
1020 "Verify SSH connectivity to the worker",
1021 "Check that the remote process PID is still valid",
1022 ],
1023 Self::CancelCleanupFailed => &[
1024 "Post-cancellation cleanup did not complete successfully",
1025 "Check worker disk space and permissions",
1026 "Verify remote working directory state manually",
1027 ],
1028 Self::CancelSlotLeak => &[
1029 "Worker slots were not properly released after cancellation",
1030 "Check worker slot accounting for inconsistencies",
1031 "Restart the daemon if slot leak persists",
1032 ],
1033 Self::CancelTimeoutExceeded => &[
1034 "Cancellation did not complete within the policy time budget",
1035 "The build may still be running on the worker",
1036 "Consider increasing cancellation timeouts or force-cancelling",
1037 ],
1038
1039 Self::TransferRsyncFailed => &[
1041 "Verify rsync is installed on both ends",
1042 "Check SSH connectivity to worker",
1043 "Review rsync exclude patterns",
1044 ],
1045 Self::TransferTimeout => &[
1046 "Increase transfer timeout in configuration",
1047 "Check network bandwidth to worker",
1048 "Consider using incremental sync",
1049 ],
1050 Self::TransferSourceMissing => &[
1051 "Verify source files exist locally",
1052 "Check file patterns in configuration",
1053 "Review .rchignore exclusions",
1054 ],
1055 Self::TransferDestError => &[
1056 "Check remote directory permissions and ownership",
1057 "Verify remote_base_dir is valid",
1058 "Ensure sufficient disk space on worker",
1059 ],
1060 Self::TransferDiskFull => &[
1061 "Clean up old builds on worker",
1062 "Check disk usage: df -h on worker",
1063 "Increase disk allocation for worker",
1064 ],
1065 Self::TransferPermissionDenied => &[
1066 "Check ownership of /data/projects/<repo> on the worker",
1067 "Verify SSH user has write permissions",
1068 "Repair drift if needed: sudo chown -R <ssh-user>:<ssh-user> /data/projects/<repo>",
1069 ],
1070 Self::TransferChecksumError => &[
1071 "Retry the transfer",
1072 "Check for network issues",
1073 "Verify file integrity on source",
1074 ],
1075 Self::TransferBinaryFailed => &[
1076 "Check network connectivity",
1077 "Verify binary URL is accessible",
1078 "Try manual download to diagnose",
1079 ],
1080 Self::TransferIncomplete => &[
1081 "Retry the transfer operation",
1082 "Check for network interruptions",
1083 "Review transfer logs for details",
1084 ],
1085 Self::TransferProtocolError => &[
1086 "Verify rsync version compatibility",
1087 "Check SSH protocol settings",
1088 "Review transfer configuration",
1089 ],
1090 Self::DependencyPreflightMissing => &[
1091 "Inspect the dependency preflight evidence for the missing remote path",
1092 "Ensure every dependency root in the closure is synced to the worker",
1093 "Verify Cargo.toml and required source entrypoints exist on the worker",
1094 ],
1095 Self::DependencyPreflightStale => &[
1096 "Rerun after a successful sync of all dependency roots",
1097 "Check whether transfer estimation skipped a dependency root",
1098 "Inspect dependency preflight evidence for the stale root",
1099 ],
1100 Self::DependencyPreflightUnknown => &[
1101 "Inspect sync and SSH logs for the dependency preflight probe",
1102 "Retry after worker connectivity and shell probes are healthy",
1103 "Use verbose output to view the full dependency preflight report",
1104 ],
1105 Self::DependencyPreflightPolicyViolation => &[
1106 "Move path dependencies under the configured canonical project root",
1107 "Use /data/projects or /dp topology aliases consistently",
1108 "Inspect the dependency closure policy diagnostics before retrying",
1109 ],
1110 Self::DependencyPreflightTimeout => &[
1111 "Retry after system load decreases",
1112 "Inspect cargo metadata latency for the workspace",
1113 "Increase planner timeout only after ruling out dependency graph issues",
1114 ],
1115
1116 Self::InternalDaemonSocket => &[
1118 "Start the daemon: rchd start",
1119 "Check socket path permissions",
1120 "Verify no stale socket file exists",
1121 ],
1122 Self::InternalDaemonProtocol => &[
1123 "Restart the daemon: rchd restart",
1124 "Check for version mismatch between rch and rchd",
1125 "Review daemon logs for details",
1126 ],
1127 Self::InternalDaemonNotRunning => &[
1128 "Start the daemon: rchd start",
1129 "Check if daemon crashed: journalctl -u rchd",
1130 "Verify daemon configuration",
1131 ],
1132 Self::InternalIpcError => &[
1133 "Restart the daemon",
1134 "Check system message queue limits",
1135 "Review logs for detailed error",
1136 ],
1137 Self::InternalStateError => &[
1138 "Restart the daemon",
1139 "Clear any lock files",
1140 "Report bug if persists",
1141 ],
1142 Self::InternalSerdeError => &[
1143 "Check for corrupted state files",
1144 "Clear cache and restart",
1145 "Report bug with reproduction steps",
1146 ],
1147 Self::InternalHookError => &[
1148 "Verify hook script exists and is executable",
1149 "Check hook script for errors",
1150 "Review hook timeout settings",
1151 ],
1152 Self::InternalMetricsError => &[
1153 "Check metrics file permissions",
1154 "Verify disk space for metrics",
1155 "Review metrics configuration",
1156 ],
1157 Self::InternalLoggingError => &[
1158 "Check log directory permissions",
1159 "Verify disk space for logs",
1160 "Review logging configuration",
1161 ],
1162 Self::InternalUpdateError => &[
1163 "Check network connectivity",
1164 "Verify update server is reachable",
1165 "Try manual update check",
1166 ],
1167 }
1168 }
1169
1170 #[must_use]
1172 pub const fn doc_url(&self) -> Option<&'static str> {
1173 match self {
1175 Self::PathDepManifestParseFailed
1176 | Self::PathDepMissing
1177 | Self::PathDepCyclic
1178 | Self::PathDepPolicyViolation
1179 | Self::PathDepMetadataFailed
1180 | Self::PathDepMetadataParseFailed => Some("https://rch.dev/docs/path-deps"),
1181
1182 Self::ClosurePlanFailed
1183 | Self::ClosureFailOpen
1184 | Self::ClosureHighRisk
1185 | Self::ClosureMissingData
1186 | Self::ClosureNonDeterministic
1187 | Self::ClosureFingerprintMismatch => Some("https://rch.dev/docs/dependency-closure"),
1188
1189 Self::WorkerDiskPressureCritical
1190 | Self::WorkerDiskPressureWarning
1191 | Self::WorkerTelemetryGap
1192 | Self::WorkerDiskIoHigh
1193 | Self::WorkerMemoryPressureHigh
1194 | Self::WorkerReclaimFailed
1195 | Self::WorkerDiskHeadroomInsufficient
1196 | Self::WorkerReclaimProtected => Some("https://rch.dev/docs/disk-pressure"),
1197
1198 Self::ProcessTriageAdapterUnavailable
1199 | Self::ProcessTriageDetectorUncertain
1200 | Self::ProcessTriagePolicyViolation
1201 | Self::ProcessTriageTransportError
1202 | Self::ProcessTriageExecutorError
1203 | Self::ProcessTriageTimeout
1204 | Self::ProcessTriagePartialResult
1205 | Self::ProcessTriageInvalidRequest => Some("https://rch.dev/docs/process-triage"),
1206
1207 Self::CancelGracefulSent
1208 | Self::CancelEscalatedKill
1209 | Self::CancelRemoteKillFailed
1210 | Self::CancelCleanupFailed
1211 | Self::CancelSlotLeak
1212 | Self::CancelTimeoutExceeded => Some("https://rch.dev/docs/cancellation"),
1213
1214 Self::DependencyPreflightMissing
1215 | Self::DependencyPreflightStale
1216 | Self::DependencyPreflightUnknown
1217 | Self::DependencyPreflightPolicyViolation
1218 | Self::DependencyPreflightTimeout => Some("https://rch.dev/docs/sync"),
1219
1220 _ => match self.category() {
1221 ErrorCategory::Config => Some("https://rch.dev/docs/config"),
1222 ErrorCategory::Network => Some("https://rch.dev/docs/ssh"),
1223 ErrorCategory::Worker => Some("https://rch.dev/docs/workers"),
1224 ErrorCategory::Build => Some("https://rch.dev/docs/builds"),
1225 ErrorCategory::Transfer => Some("https://rch.dev/docs/sync"),
1226 ErrorCategory::Internal => Some("https://rch.dev/docs/troubleshooting"),
1227 },
1228 }
1229 }
1230
1231 #[must_use]
1233 pub const fn all() -> &'static [ErrorCode] {
1234 &[
1235 Self::ConfigNotFound,
1237 Self::ConfigReadError,
1238 Self::ConfigParseError,
1239 Self::ConfigValidationError,
1240 Self::ConfigEnvError,
1241 Self::ConfigProfileNotFound,
1242 Self::ConfigNoWorkers,
1243 Self::ConfigInvalidWorker,
1244 Self::ConfigSshKeyError,
1245 Self::ConfigSocketPathError,
1246 Self::PathDepManifestParseFailed,
1248 Self::PathDepMissing,
1249 Self::PathDepCyclic,
1250 Self::PathDepPolicyViolation,
1251 Self::PathDepMetadataFailed,
1252 Self::PathDepMetadataParseFailed,
1253 Self::ClosurePlanFailed,
1255 Self::ClosureFailOpen,
1256 Self::ClosureHighRisk,
1257 Self::ClosureMissingData,
1258 Self::ClosureNonDeterministic,
1259 Self::ClosureFingerprintMismatch,
1260 Self::SshConnectionFailed,
1262 Self::SshAuthFailed,
1263 Self::SshKeyError,
1264 Self::SshHostKeyError,
1265 Self::SshTimeout,
1266 Self::SshSessionDropped,
1267 Self::NetworkDnsError,
1268 Self::NetworkUnreachable,
1269 Self::NetworkConnectionRefused,
1270 Self::NetworkTimeout,
1271 Self::WorkerNoneAvailable,
1273 Self::WorkerAllUnhealthy,
1274 Self::WorkerHealthCheckFailed,
1275 Self::WorkerSelfTestFailed,
1276 Self::WorkerAtCapacity,
1277 Self::WorkerMissingToolchain,
1278 Self::WorkerStateError,
1279 Self::WorkerCircuitOpen,
1280 Self::WorkerSelectionFailed,
1281 Self::WorkerLoadQueryFailed,
1282 Self::WorkerDiskPressureCritical,
1284 Self::WorkerDiskPressureWarning,
1285 Self::WorkerTelemetryGap,
1286 Self::WorkerDiskIoHigh,
1287 Self::WorkerMemoryPressureHigh,
1288 Self::WorkerReclaimFailed,
1289 Self::WorkerDiskHeadroomInsufficient,
1290 Self::WorkerReclaimProtected,
1291 Self::BuildCompilationFailed,
1293 Self::BuildUnknownCommand,
1294 Self::BuildKilledBySignal,
1295 Self::BuildTimeout,
1296 Self::BuildOutputError,
1297 Self::BuildWorkdirError,
1298 Self::BuildToolchainError,
1299 Self::BuildEnvError,
1300 Self::BuildIncrementalError,
1301 Self::BuildArtifactMissing,
1302 Self::BuildArtifactSyncEmpty,
1303 Self::BuildArtifactForeignTarget,
1304 Self::BuildCargoWorkspaceInheritance,
1305 Self::ProcessTriageAdapterUnavailable,
1307 Self::ProcessTriageDetectorUncertain,
1308 Self::ProcessTriagePolicyViolation,
1309 Self::ProcessTriageTransportError,
1310 Self::ProcessTriageExecutorError,
1311 Self::ProcessTriageTimeout,
1312 Self::ProcessTriagePartialResult,
1313 Self::ProcessTriageInvalidRequest,
1314 Self::CancelGracefulSent,
1316 Self::CancelEscalatedKill,
1317 Self::CancelRemoteKillFailed,
1318 Self::CancelCleanupFailed,
1319 Self::CancelSlotLeak,
1320 Self::CancelTimeoutExceeded,
1321 Self::TransferRsyncFailed,
1323 Self::TransferTimeout,
1324 Self::TransferSourceMissing,
1325 Self::TransferDestError,
1326 Self::TransferDiskFull,
1327 Self::TransferPermissionDenied,
1328 Self::TransferChecksumError,
1329 Self::TransferBinaryFailed,
1330 Self::TransferIncomplete,
1331 Self::TransferProtocolError,
1332 Self::DependencyPreflightMissing,
1333 Self::DependencyPreflightStale,
1334 Self::DependencyPreflightUnknown,
1335 Self::DependencyPreflightPolicyViolation,
1336 Self::DependencyPreflightTimeout,
1337 Self::InternalDaemonSocket,
1339 Self::InternalDaemonProtocol,
1340 Self::InternalDaemonNotRunning,
1341 Self::InternalIpcError,
1342 Self::InternalStateError,
1343 Self::InternalSerdeError,
1344 Self::InternalHookError,
1345 Self::InternalMetricsError,
1346 Self::InternalLoggingError,
1347 Self::InternalUpdateError,
1348 ]
1349 }
1350}
1351
1352impl fmt::Display for ErrorCode {
1353 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1354 write!(f, "{}: {}", self.code_string(), self.message())
1355 }
1356}
1357
1358#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, schemars::JsonSchema)]
1360#[serde(rename_all = "lowercase")]
1361pub enum ErrorCategory {
1362 Config,
1364 Network,
1366 Worker,
1368 Build,
1370 Transfer,
1372 Internal,
1374}
1375
1376impl ErrorCategory {
1377 #[must_use]
1379 pub const fn name(&self) -> &'static str {
1380 match self {
1381 Self::Config => "Configuration",
1382 Self::Network => "Network",
1383 Self::Worker => "Worker",
1384 Self::Build => "Build",
1385 Self::Transfer => "Transfer",
1386 Self::Internal => "Internal",
1387 }
1388 }
1389
1390 #[must_use]
1392 pub const fn description(&self) -> &'static str {
1393 match self {
1394 Self::Config => "Configuration file and environment setup issues",
1395 Self::Network => "SSH connectivity and network communication issues",
1396 Self::Worker => "Remote worker selection, health, and management issues",
1397 Self::Build => "Remote compilation and build process issues",
1398 Self::Transfer => "File synchronization and transfer issues",
1399 Self::Internal => "Internal errors that may indicate bugs",
1400 }
1401 }
1402}
1403
1404impl fmt::Display for ErrorCategory {
1405 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1406 write!(f, "{}", self.name())
1407 }
1408}
1409
1410#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1412pub struct ErrorEntry {
1413 pub code: String,
1415 pub category: ErrorCategory,
1417 pub message: String,
1419 pub remediation: Vec<String>,
1421 pub doc_url: Option<String>,
1423}
1424
1425impl ErrorEntry {
1426 #[must_use]
1428 pub fn format_full(&self) -> String {
1429 let mut output = format!("[{}] {}\n\n", self.code, self.message);
1430
1431 if !self.remediation.is_empty() {
1432 output.push_str("Remediation steps:\n");
1433 for (i, step) in self.remediation.iter().enumerate() {
1434 output.push_str(&format!(" {}. {}\n", i + 1, step));
1435 }
1436 }
1437
1438 if let Some(url) = &self.doc_url {
1439 output.push_str(&format!("\nFor more information: {}\n", url));
1440 }
1441
1442 output
1443 }
1444
1445 #[must_use]
1447 pub fn format_brief(&self) -> String {
1448 format!("[{}] {}", self.code, self.message)
1449 }
1450}
1451
1452impl fmt::Display for ErrorEntry {
1453 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1454 write!(f, "{}", self.format_brief())
1455 }
1456}
1457
1458#[cfg(test)]
1459mod tests {
1460 use super::*;
1461
1462 #[test]
1463 fn test_error_code_numbers_are_unique() {
1464 let mut seen = std::collections::HashSet::new();
1465 for code in ErrorCode::all() {
1466 let num = code.code_number();
1467 assert!(
1468 seen.insert(num),
1469 "Duplicate error code number: {} for {:?}",
1470 num,
1471 code
1472 );
1473 }
1474 }
1475
1476 #[test]
1477 fn test_error_code_format() {
1478 assert_eq!(ErrorCode::ConfigNotFound.code_string(), "RCH-E001");
1479 assert_eq!(ErrorCode::SshConnectionFailed.code_string(), "RCH-E100");
1480 assert_eq!(ErrorCode::WorkerNoneAvailable.code_string(), "RCH-E200");
1481 assert_eq!(ErrorCode::BuildCompilationFailed.code_string(), "RCH-E300");
1482 assert_eq!(ErrorCode::TransferRsyncFailed.code_string(), "RCH-E400");
1483 assert_eq!(ErrorCode::InternalDaemonSocket.code_string(), "RCH-E500");
1484
1485 assert_eq!(
1487 ErrorCode::PathDepManifestParseFailed.code_string(),
1488 "RCH-E013"
1489 );
1490 assert_eq!(ErrorCode::ClosurePlanFailed.code_string(), "RCH-E019");
1491 assert_eq!(
1492 ErrorCode::WorkerDiskPressureCritical.code_string(),
1493 "RCH-E210"
1494 );
1495 assert_eq!(
1496 ErrorCode::ProcessTriageAdapterUnavailable.code_string(),
1497 "RCH-E310"
1498 );
1499 assert_eq!(
1500 ErrorCode::DependencyPreflightMissing.code_string(),
1501 "RCH-E410"
1502 );
1503 }
1504
1505 #[test]
1506 fn test_error_categories() {
1507 assert_eq!(ErrorCode::ConfigNotFound.category(), ErrorCategory::Config);
1508 assert_eq!(
1509 ErrorCode::SshConnectionFailed.category(),
1510 ErrorCategory::Network
1511 );
1512 assert_eq!(
1513 ErrorCode::WorkerNoneAvailable.category(),
1514 ErrorCategory::Worker
1515 );
1516 assert_eq!(
1517 ErrorCode::BuildCompilationFailed.category(),
1518 ErrorCategory::Build
1519 );
1520 assert_eq!(
1521 ErrorCode::TransferRsyncFailed.category(),
1522 ErrorCategory::Transfer
1523 );
1524 assert_eq!(
1525 ErrorCode::InternalDaemonSocket.category(),
1526 ErrorCategory::Internal
1527 );
1528 }
1529
1530 #[test]
1531 fn test_all_errors_have_message() {
1532 for code in ErrorCode::all() {
1533 let msg = code.message();
1534 assert!(!msg.is_empty(), "Error {:?} has empty message", code);
1535 }
1536 }
1537
1538 #[test]
1539 fn test_all_errors_have_remediation() {
1540 for code in ErrorCode::all() {
1541 let steps = code.remediation();
1542 assert!(
1543 !steps.is_empty(),
1544 "Error {:?} has no remediation steps",
1545 code
1546 );
1547 }
1548 }
1549
1550 #[test]
1551 fn test_error_entry_serialization() {
1552 let entry = ErrorCode::ConfigNotFound.entry();
1553 let json = serde_json::to_string(&entry).expect("serialization failed");
1554 assert!(json.contains("RCH-E001"));
1555 assert!(json.contains("config"));
1556
1557 let parsed: ErrorEntry = serde_json::from_str(&json).expect("deserialization failed");
1558 assert_eq!(parsed.code, "RCH-E001");
1559 assert_eq!(parsed.category, ErrorCategory::Config);
1560 }
1561
1562 #[test]
1563 fn test_error_code_serialization() {
1564 let code = ErrorCode::SshAuthFailed;
1565 let json = serde_json::to_string(&code).expect("serialization failed");
1566 assert_eq!(json, "\"SSH_AUTH_FAILED\"");
1567
1568 let parsed: ErrorCode = serde_json::from_str(&json).expect("deserialization failed");
1569 assert_eq!(parsed, ErrorCode::SshAuthFailed);
1570 }
1571
1572 #[test]
1573 fn test_format_full() {
1574 let entry = ErrorCode::ConfigNotFound.entry();
1575 let formatted = entry.format_full();
1576
1577 assert!(formatted.contains("[RCH-E001]"));
1578 assert!(formatted.contains("Configuration file not found"));
1579 assert!(formatted.contains("Remediation steps:"));
1580 assert!(formatted.contains("rch init"));
1581 }
1582
1583 #[test]
1584 fn test_format_brief() {
1585 let entry = ErrorCode::ConfigNotFound.entry();
1586 let formatted = entry.format_brief();
1587
1588 assert_eq!(formatted, "[RCH-E001] Configuration file not found");
1589 }
1590
1591 #[test]
1592 fn test_display_implementations() {
1593 let code = ErrorCode::ConfigNotFound;
1594 let display = format!("{}", code);
1595 assert!(display.contains("RCH-E001"));
1596 assert!(display.contains("Configuration file not found"));
1597
1598 let category = ErrorCategory::Config;
1599 assert_eq!(format!("{}", category), "Configuration");
1600 }
1601
1602 #[test]
1603 fn test_category_ranges() {
1604 for code in ErrorCode::all() {
1606 let num = code.code_number();
1607 let cat = code.category();
1608 match cat {
1609 ErrorCategory::Config => assert!(num < 100, "{:?} should be < 100", code),
1610 ErrorCategory::Network => {
1611 assert!((100..200).contains(&num), "{:?} should be 100-199", code)
1612 }
1613 ErrorCategory::Worker => {
1614 assert!((200..300).contains(&num), "{:?} should be 200-299", code)
1615 }
1616 ErrorCategory::Build => {
1617 assert!((300..400).contains(&num), "{:?} should be 300-399", code)
1618 }
1619 ErrorCategory::Transfer => {
1620 assert!((400..500).contains(&num), "{:?} should be 400-499", code)
1621 }
1622 ErrorCategory::Internal => {
1623 assert!((500..600).contains(&num), "{:?} should be 500-599", code)
1624 }
1625 }
1626 }
1627 }
1628
1629 #[test]
1635 fn test_path_dep_error_codes_stable() {
1636 assert_eq!(ErrorCode::PathDepManifestParseFailed.code_number(), 13);
1637 assert_eq!(ErrorCode::PathDepMissing.code_number(), 14);
1638 assert_eq!(ErrorCode::PathDepCyclic.code_number(), 15);
1639 assert_eq!(ErrorCode::PathDepPolicyViolation.code_number(), 16);
1640 assert_eq!(ErrorCode::PathDepMetadataFailed.code_number(), 17);
1641 assert_eq!(ErrorCode::PathDepMetadataParseFailed.code_number(), 18);
1642 }
1643
1644 #[test]
1646 fn test_closure_error_codes_stable() {
1647 assert_eq!(ErrorCode::ClosurePlanFailed.code_number(), 19);
1648 assert_eq!(ErrorCode::ClosureFailOpen.code_number(), 20);
1649 assert_eq!(ErrorCode::ClosureHighRisk.code_number(), 21);
1650 assert_eq!(ErrorCode::ClosureMissingData.code_number(), 22);
1651 assert_eq!(ErrorCode::ClosureNonDeterministic.code_number(), 23);
1652 assert_eq!(ErrorCode::ClosureFingerprintMismatch.code_number(), 24);
1653 }
1654
1655 #[test]
1657 fn test_disk_pressure_error_codes_stable() {
1658 assert_eq!(ErrorCode::WorkerDiskPressureCritical.code_number(), 210);
1659 assert_eq!(ErrorCode::WorkerDiskPressureWarning.code_number(), 211);
1660 assert_eq!(ErrorCode::WorkerTelemetryGap.code_number(), 212);
1661 assert_eq!(ErrorCode::WorkerDiskIoHigh.code_number(), 213);
1662 assert_eq!(ErrorCode::WorkerMemoryPressureHigh.code_number(), 214);
1663 assert_eq!(ErrorCode::WorkerReclaimFailed.code_number(), 215);
1664 assert_eq!(ErrorCode::WorkerDiskHeadroomInsufficient.code_number(), 216);
1665 assert_eq!(ErrorCode::WorkerReclaimProtected.code_number(), 217);
1666 }
1667
1668 #[test]
1670 fn test_process_triage_error_codes_stable() {
1671 assert_eq!(
1672 ErrorCode::ProcessTriageAdapterUnavailable.code_number(),
1673 310
1674 );
1675 assert_eq!(ErrorCode::ProcessTriageDetectorUncertain.code_number(), 311);
1676 assert_eq!(ErrorCode::ProcessTriagePolicyViolation.code_number(), 312);
1677 assert_eq!(ErrorCode::ProcessTriageTransportError.code_number(), 313);
1678 assert_eq!(ErrorCode::ProcessTriageExecutorError.code_number(), 314);
1679 assert_eq!(ErrorCode::ProcessTriageTimeout.code_number(), 315);
1680 assert_eq!(ErrorCode::ProcessTriagePartialResult.code_number(), 316);
1681 assert_eq!(ErrorCode::ProcessTriageInvalidRequest.code_number(), 317);
1682 }
1683
1684 #[test]
1686 fn test_cancellation_error_codes_stable() {
1687 assert_eq!(ErrorCode::CancelGracefulSent.code_number(), 320);
1688 assert_eq!(ErrorCode::CancelEscalatedKill.code_number(), 321);
1689 assert_eq!(ErrorCode::CancelRemoteKillFailed.code_number(), 322);
1690 assert_eq!(ErrorCode::CancelCleanupFailed.code_number(), 323);
1691 assert_eq!(ErrorCode::CancelSlotLeak.code_number(), 324);
1692 assert_eq!(ErrorCode::CancelTimeoutExceeded.code_number(), 325);
1693 }
1694
1695 #[test]
1697 fn test_build_topology_error_codes_stable() {
1698 assert_eq!(ErrorCode::BuildCargoWorkspaceInheritance.code_number(), 329);
1699 assert_eq!(
1700 ErrorCode::BuildCargoWorkspaceInheritance.category(),
1701 ErrorCategory::Build
1702 );
1703 }
1704
1705 #[test]
1707 fn test_build_artifact_sync_empty_error_code_stable() {
1708 assert_eq!(ErrorCode::BuildArtifactSyncEmpty.code_number(), 326);
1709 assert_eq!(
1710 ErrorCode::BuildArtifactSyncEmpty.category(),
1711 ErrorCategory::Build
1712 );
1713 assert_eq!(ErrorCode::BuildArtifactSyncEmpty.code_string(), "RCH-E326");
1714 assert!(
1716 ErrorCode::BuildArtifactSyncEmpty
1717 .remediation()
1718 .iter()
1719 .any(|step| step.to_lowercase().contains("stale")),
1720 "RCH-E326 remediation must warn about stale local artifacts"
1721 );
1722 }
1723
1724 #[test]
1726 fn test_build_artifact_foreign_target_error_code_stable() {
1727 assert_eq!(ErrorCode::BuildArtifactForeignTarget.code_number(), 327);
1728 assert_eq!(
1729 ErrorCode::BuildArtifactForeignTarget.category(),
1730 ErrorCategory::Build
1731 );
1732 assert_eq!(
1733 ErrorCode::BuildArtifactForeignTarget.code_string(),
1734 "RCH-E327"
1735 );
1736 assert!(
1738 ErrorCode::BuildArtifactForeignTarget
1739 .remediation()
1740 .iter()
1741 .any(|step| step.to_lowercase().contains("rebuild")),
1742 "RCH-E327 remediation must tell the operator to rebuild for this host"
1743 );
1744 }
1745
1746 #[test]
1748 fn test_dependency_preflight_error_codes_stable() {
1749 assert_eq!(ErrorCode::DependencyPreflightMissing.code_number(), 410);
1750 assert_eq!(ErrorCode::DependencyPreflightStale.code_number(), 411);
1751 assert_eq!(ErrorCode::DependencyPreflightUnknown.code_number(), 412);
1752 assert_eq!(
1753 ErrorCode::DependencyPreflightPolicyViolation.code_number(),
1754 413
1755 );
1756 assert_eq!(ErrorCode::DependencyPreflightTimeout.code_number(), 414);
1757 }
1758
1759 #[test]
1761 fn test_new_error_codes_correct_categories() {
1762 assert_eq!(ErrorCode::PathDepCyclic.category(), ErrorCategory::Config);
1764 assert_eq!(
1765 ErrorCode::ClosurePlanFailed.category(),
1766 ErrorCategory::Config
1767 );
1768
1769 assert_eq!(
1771 ErrorCode::WorkerDiskPressureCritical.category(),
1772 ErrorCategory::Worker
1773 );
1774 assert_eq!(
1775 ErrorCode::WorkerReclaimProtected.category(),
1776 ErrorCategory::Worker
1777 );
1778
1779 assert_eq!(
1781 ErrorCode::ProcessTriageTimeout.category(),
1782 ErrorCategory::Build
1783 );
1784 assert_eq!(
1785 ErrorCode::ProcessTriageInvalidRequest.category(),
1786 ErrorCategory::Build
1787 );
1788
1789 assert_eq!(
1791 ErrorCode::CancelGracefulSent.category(),
1792 ErrorCategory::Build
1793 );
1794 assert_eq!(
1795 ErrorCode::CancelTimeoutExceeded.category(),
1796 ErrorCategory::Build
1797 );
1798
1799 assert_eq!(
1801 ErrorCode::DependencyPreflightMissing.category(),
1802 ErrorCategory::Transfer
1803 );
1804 assert_eq!(
1805 ErrorCode::DependencyPreflightTimeout.category(),
1806 ErrorCategory::Transfer
1807 );
1808 }
1809
1810 #[test]
1812 fn test_new_error_codes_doc_urls() {
1813 assert_eq!(
1814 ErrorCode::PathDepCyclic.doc_url(),
1815 Some("https://rch.dev/docs/path-deps")
1816 );
1817 assert_eq!(
1818 ErrorCode::ClosureFailOpen.doc_url(),
1819 Some("https://rch.dev/docs/dependency-closure")
1820 );
1821 assert_eq!(
1822 ErrorCode::WorkerDiskPressureCritical.doc_url(),
1823 Some("https://rch.dev/docs/disk-pressure")
1824 );
1825 assert_eq!(
1826 ErrorCode::ProcessTriageTimeout.doc_url(),
1827 Some("https://rch.dev/docs/process-triage")
1828 );
1829 assert_eq!(
1830 ErrorCode::CancelGracefulSent.doc_url(),
1831 Some("https://rch.dev/docs/cancellation")
1832 );
1833 assert_eq!(
1834 ErrorCode::DependencyPreflightMissing.doc_url(),
1835 Some("https://rch.dev/docs/sync")
1836 );
1837 }
1838
1839 #[test]
1841 fn test_new_error_codes_json_roundtrip() {
1842 let new_codes = [
1843 ErrorCode::PathDepManifestParseFailed,
1844 ErrorCode::ClosurePlanFailed,
1845 ErrorCode::WorkerDiskPressureCritical,
1846 ErrorCode::ProcessTriageAdapterUnavailable,
1847 ErrorCode::DependencyPreflightMissing,
1848 ];
1849
1850 for code in new_codes {
1851 let json = serde_json::to_string(&code).expect("serialization failed");
1852 let parsed: ErrorCode = serde_json::from_str(&json).expect("deserialization failed");
1853 assert_eq!(parsed, code, "Roundtrip failed for {:?}", code);
1854
1855 let entry = code.entry();
1857 let entry_json = serde_json::to_string(&entry).expect("entry serialization failed");
1858 let parsed_entry: ErrorEntry =
1859 serde_json::from_str(&entry_json).expect("entry deserialization failed");
1860 assert_eq!(parsed_entry.code, code.code_string());
1861 }
1862 }
1863
1864 #[test]
1866 fn test_total_error_code_count() {
1867 let total = ErrorCode::all().len();
1868 assert!(
1871 total >= 100,
1872 "Expected at least 100 error codes (was {}); did a code get accidentally removed?",
1873 total,
1874 );
1875 }
1876}