1use mif_problem::{
5 Applicability, CodeAction, ProblemDetails, ProblemMeta, SuggestedFix, ToProblem,
6};
7
8#[derive(Debug, thiserror::Error)]
19pub enum MifRhError {
20 #[error("failed to read finding {path}: {source}")]
22 FindingIo {
23 path: String,
25 #[source]
27 source: std::io::Error,
28 },
29 #[error("finding {path} is not valid JSON: {source}")]
31 FindingJson {
32 path: String,
34 #[source]
36 source: serde_json::Error,
37 },
38 #[error("failed to read {path}: {source}")]
41 Io {
42 path: String,
44 #[source]
46 source: std::io::Error,
47 },
48 #[error("failed to parse {path} as JSON: {source}")]
50 Json {
51 path: String,
53 #[source]
55 source: serde_json::Error,
56 },
57 #[error("failed to serialize {path} as JSON: {source}")]
62 JsonSerialize {
63 path: String,
65 #[source]
67 source: serde_json::Error,
68 },
69 #[error("failed to parse ontology pack {path} as YAML: {source}")]
73 OntologyPackYaml {
74 path: String,
76 #[source]
78 source: serde_norway::Error,
79 },
80 #[error("failed to serialize report frontmatter as YAML: {source}")]
85 FrontmatterYamlSerialize {
86 #[source]
88 source: serde_norway::Error,
89 },
90 #[error("catalog file {path} does not exist")]
92 CatalogMissing {
93 path: String,
95 },
96 #[error("config file {path} does not exist")]
98 ConfigMissing {
99 path: String,
101 },
102 #[error("topic '{topic}' binds ontology '{id}', which is not cataloged or version-mismatched")]
105 DirectBindingInvalid {
106 topic: String,
108 id: String,
110 },
111 #[error(transparent)]
115 Ontology(#[from] mif_ontology::OntologyError),
116 #[error(transparent)]
119 Frontmatter(#[from] mif_frontmatter::FrontmatterError),
120 #[error("failed to compile schema {path}: {detail}")]
123 SchemaCompilation {
124 path: String,
126 detail: String,
129 },
130 #[error("schema {path} has no $id — it cannot be registered as a $ref target")]
133 RefSchemaMissingId {
134 path: String,
136 },
137 #[error("{path} failed schema validation against {schema_path}: {detail}")]
139 SchemaValidationFailed {
140 path: String,
142 schema_path: String,
144 detail: String,
146 },
147 #[error("{field} must be one of {allowed} (got '{value}')")]
149 InvalidToggleValue {
150 field: String,
152 value: String,
154 allowed: String,
156 },
157 #[error("empty content (provide --content-file, --content, or stdin)")]
160 EmptySourceContent,
161 #[error("pack '{name}' is not declared in {path} packs[] — declare it first")]
167 PackNotDeclared {
168 name: String,
170 path: String,
172 },
173 #[error("no finding JSON in {path}")]
176 NoFindingsFound {
177 path: String,
179 },
180 #[error("no surviving findings to synthesize in {path}")]
182 NoSurvivingFindings {
183 path: String,
185 },
186 #[error(
189 "artifact from {path} has no publishable content (no surviving findings, or no citations to cite)"
190 )]
191 ArtifactNotPublishable {
192 path: String,
194 },
195 #[error(
198 "{count} finding(s) lack a provenance block; import aborted (provenance must be preserved)"
199 )]
200 MissingProvenance {
201 count: usize,
203 paths: Vec<String>,
205 },
206 #[error(
211 "the known-good sample finding at {sample_path} failed schema validation — the schema/toolchain is broken, refusing to emit a plan"
212 )]
213 ReconcileEnvironmentBroken {
214 sample_path: String,
216 },
217 #[error("topic '{topic}' is not registered in {config_path}")]
220 TopicNotRegistered {
221 topic: String,
223 config_path: String,
225 },
226 #[error("concordance is not a valid graph (no .nodes array): {path}")]
230 InvalidConcordance {
231 path: String,
233 },
234 #[error(
240 "ontology-map.json {reason} for topic '{topic}' — synthesis BLOCKED (fail closed): {path}"
241 )]
242 OntologyMapUnusable {
243 path: String,
245 topic: String,
248 reason: String,
250 },
251 #[error("finding is not valid JSON, cannot check its relationship targets: {path}")]
257 RelationshipTargetFindingUnparseable {
258 path: String,
260 },
261 #[error("subtype_of graph contains a cycle involving entity type '{entity_type}'")]
266 SubtypeOfCycle {
267 entity_type: String,
269 },
270 #[error("entity type '{entity_type}' has a malformed validation schema: {detail}")]
274 EntityTypeSchemaInvalid {
275 entity_type: String,
277 detail: String,
281 },
282 #[error("sqlite index operation failed: {source}")]
284 Index {
285 #[source]
287 source: rusqlite::Error,
288 },
289 #[error("failed to acquire the review lock at {path}: {source}")]
291 LockIo {
292 path: String,
294 #[source]
296 source: std::io::Error,
297 },
298 #[error("another review is already in progress (lock held by pid {holder_pid})")]
300 LockHeld {
301 holder_pid: u32,
303 },
304 #[error("suggestion queue at {path} belongs to topic '{found}', not '{expected}'")]
308 QueueTopicMismatch {
309 path: String,
311 expected: String,
313 found: String,
315 },
316 #[error(transparent)]
318 Embed(#[from] mif_embed::EmbedError),
319 #[error("failed to fetch from ontology source {registry_source}: {detail}")]
323 RegistryFetch {
324 registry_source: String,
330 detail: String,
332 },
333 #[error("ontology registry index at {registry_source} is malformed: {detail}")]
336 RegistryIndexInvalid {
337 registry_source: String,
341 detail: String,
343 },
344 #[error(
347 "ontology '{id}' is not in the registry index — it has no canonical definition yet \
348 (author one with the `ontology author` subcommand)"
349 )]
350 OntologyNotInRegistry {
351 id: String,
353 },
354 #[error(
364 "ontologies.lock.json is pinned to source '{lock_source}', not '{requested_source}' — \
365 refusing to evaluate pin safety against a different registry than the one the lock \
366 trusts"
367 )]
368 LockSourceMismatch {
369 lock_source: String,
371 requested_source: String,
373 },
374 #[error("ontology '{id}' file '{file}' is not valid UTF-8")]
380 OntologyPackNotUtf8 {
381 id: String,
383 file: String,
385 },
386 #[error(
390 "registry index sha256 changed from the pinned value for source {registry_source} \
391 (pinned {pinned}, got {got}) — refusing to trust a moved index (clear index_sha256 \
392 in the lock to re-pin deliberately)"
393 )]
394 IndexPinMismatch {
395 registry_source: String,
398 pinned: String,
400 got: String,
402 },
403 #[error("checksum mismatch for ontology '{id}' ({file}): expected {expected}, got {got}")]
407 ChecksumMismatch {
408 id: String,
410 file: String,
412 expected: String,
414 got: String,
416 },
417 #[error(
421 "registry index entry for '{id}' has an unsafe file path: '{file}' (must be a bare \
422 filename)"
423 )]
424 UnsafeIndexPath {
425 id: String,
427 file: String,
429 },
430 #[error("registry index declares a malformed ontology id: '{id}' (refusing, fail-closed)")]
434 MalformedOntologyId {
435 id: String,
437 },
438 #[error("{path}'s .ontologies is not an array: {detail}")]
441 ConfigMalformed {
442 path: String,
444 detail: String,
446 },
447 #[error(
450 "no entity types found in reports/{topic}/ontology-map.json — nothing to draft an \
451 ontology from"
452 )]
453 NoEntityTypesFound {
454 topic: String,
456 },
457 #[error("no clusters in {path} — nothing to draft an ontology from")]
460 NoClustersFound {
461 path: String,
463 },
464 #[error("version is not well-formed semver: {value}")]
466 VersionNotSemver {
467 value: String,
469 },
470 #[error("{path} has no .version")]
472 VersionMissing {
473 path: String,
475 },
476 #[error("new version equals current ({value}); nothing to bump")]
478 VersionUnchanged {
479 value: String,
481 },
482 #[error(
485 "pack '{name}' not found under packs/<family>/ (ontology packs version independently and are not bumpable here)"
486 )]
487 PackNotFound {
488 name: String,
490 },
491 #[error("pack '{name}' is ambiguous: more than one packs/<family>/{name} directory exists")]
493 PackAmbiguous {
494 name: String,
496 },
497 #[error("pack '{name}': missing or malformed {path}")]
500 PackFileMissing {
501 name: String,
503 path: String,
505 },
506 #[error("pack '{name}' {path} has no valid semver .version (got '{value}')")]
508 PackVersionInvalid {
509 name: String,
511 path: String,
513 value: String,
515 },
516 #[error(
519 "pack '{name}' is at {pack_version}, ahead of the new release {new_version} — refusing to move it backward"
520 )]
521 PackAheadOfRelease {
522 name: String,
524 pack_version: String,
526 new_version: String,
528 },
529 #[error(
532 "{path} has no '## [Unreleased]' anchor to insert the new version under (nor an existing section for it)"
533 )]
534 ChangelogAnchorMissing {
535 path: String,
537 },
538 #[error("verification failed: {path} was not updated as expected")]
541 VerificationFailed {
542 path: String,
544 },
545 #[error("git {command} failed: {stderr}")]
549 GitCommandFailed {
550 command: String,
552 stderr: String,
554 },
555}
556
557impl MifRhError {
558 #[allow(clippy::too_many_lines)]
561 const fn meta(&self) -> ProblemMeta {
562 match self {
563 Self::FindingIo { .. } => ProblemMeta {
564 slug: "finding-io",
565 version: "v1",
566 title: "Failed to read a finding file",
567 status: 500,
568 exit_code: 2,
569 },
570 Self::FindingJson { .. } => ProblemMeta {
571 slug: "finding-invalid-json",
572 version: "v1",
573 title: "Finding file is not valid JSON",
574 status: 400,
575 exit_code: 2,
576 },
577 Self::Io { .. } => ProblemMeta {
578 slug: "mif-rh-io",
579 version: "v1",
580 title: "Failed to read a supporting file",
581 status: 500,
582 exit_code: 1,
583 },
584 Self::Json { .. } => ProblemMeta {
585 slug: "mif-rh-invalid-json",
586 version: "v1",
587 title: "Supporting file is not valid JSON",
588 status: 400,
589 exit_code: 1,
590 },
591 Self::JsonSerialize { .. } => ProblemMeta {
592 slug: "json-serialize-failure",
593 version: "v1",
594 title: "A value could not be serialized to JSON",
595 status: 500,
596 exit_code: 1,
597 },
598 Self::OntologyPackYaml { .. } => ProblemMeta {
599 slug: "ontology-pack-invalid-yaml",
600 version: "v1",
601 title: "Ontology pack YAML failed to parse",
602 status: 422,
603 exit_code: 4,
604 },
605 Self::FrontmatterYamlSerialize { .. } => ProblemMeta {
606 slug: "frontmatter-yaml-serialize-failure",
607 version: "v1",
608 title: "Report frontmatter could not be serialized to YAML",
609 status: 500,
610 exit_code: 1,
611 },
612 Self::CatalogMissing { .. } => ProblemMeta {
613 slug: "catalog-missing",
614 version: "v1",
615 title: "Ontology catalog file does not exist",
616 status: 404,
617 exit_code: 3,
618 },
619 Self::ConfigMissing { .. } => ProblemMeta {
620 slug: "config-missing",
621 version: "v1",
622 title: "Harness config file does not exist",
623 status: 404,
624 exit_code: 2,
625 },
626 Self::DirectBindingInvalid { .. } => ProblemMeta {
627 slug: "direct-binding-invalid",
628 version: "v1",
629 title: "Topic binds an uncataloged or version-mismatched ontology",
630 status: 422,
631 exit_code: 1,
632 },
633 Self::Ontology(_) => ProblemMeta {
634 slug: "delegated-ontology",
635 version: "v1",
636 title: "Delegated ontology error",
637 status: 500,
638 exit_code: 4,
639 },
640 Self::Frontmatter(_) => ProblemMeta {
641 slug: "delegated-frontmatter",
642 version: "v1",
643 title: "Delegated frontmatter error",
644 status: 500,
645 exit_code: 4,
646 },
647 Self::SchemaCompilation { .. } => ProblemMeta {
648 slug: "schema-compilation-failed",
649 version: "v1",
650 title: "Report schema failed to compile",
651 status: 422,
652 exit_code: 2,
653 },
654 Self::RefSchemaMissingId { .. } => ProblemMeta {
655 slug: "ref-schema-missing-id",
656 version: "v1",
657 title: "Ref-target schema has no $id",
658 status: 422,
659 exit_code: 2,
660 },
661 Self::SchemaValidationFailed { .. } => ProblemMeta {
662 slug: "schema-validation-failed",
663 version: "v1",
664 title: "Report failed schema validation",
665 status: 422,
666 exit_code: 1,
667 },
668 Self::InvalidToggleValue { .. } => ProblemMeta {
669 slug: "invalid-toggle-value",
670 version: "v1",
671 title: "Manifest toggle value is not one of the allowed values",
672 status: 422,
673 exit_code: 2,
674 },
675 Self::EmptySourceContent => ProblemMeta {
676 slug: "empty-source-content",
677 version: "v1",
678 title: "No source content was available from any input",
679 status: 422,
680 exit_code: 2,
681 },
682 Self::PackNotDeclared { .. } => ProblemMeta {
683 slug: "pack-not-declared",
684 version: "v1",
685 title: "Pack is not declared in the harness manifest",
686 status: 404,
687 exit_code: 2,
688 },
689 Self::NoFindingsFound { .. } => ProblemMeta {
690 slug: "no-findings-found",
691 version: "v1",
692 title: "Findings directory has no finding JSON",
693 status: 404,
694 exit_code: 2,
695 },
696 Self::NoSurvivingFindings { .. } => ProblemMeta {
697 slug: "no-surviving-findings",
698 version: "v1",
699 title: "Every finding is falsified — nothing to synthesize",
700 status: 422,
701 exit_code: 1,
702 },
703 Self::ArtifactNotPublishable { .. } => ProblemMeta {
704 slug: "artifact-not-publishable",
705 version: "v1",
706 title: "Synthesized artifact has no publishable content",
707 status: 422,
708 exit_code: 1,
709 },
710 Self::MissingProvenance { .. } => ProblemMeta {
711 slug: "missing-provenance",
712 version: "v1",
713 title: "Corpus import contains findings with no provenance block",
714 status: 422,
715 exit_code: 1,
716 },
717 Self::ReconcileEnvironmentBroken { .. } => ProblemMeta {
718 slug: "reconcile-environment-broken",
719 version: "v1",
720 title: "Known-good sample finding failed schema validation",
721 status: 500,
722 exit_code: 3,
723 },
724 Self::TopicNotRegistered { .. } => ProblemMeta {
725 slug: "topic-not-registered",
726 version: "v1",
727 title: "Topic is not registered in harness.config.json",
728 status: 422,
729 exit_code: 2,
730 },
731 Self::InvalidConcordance { .. } => ProblemMeta {
732 slug: "invalid-concordance",
733 version: "v1",
734 title: "Concordance is not a valid graph",
735 status: 422,
736 exit_code: 2,
737 },
738 Self::OntologyMapUnusable { .. } => ProblemMeta {
739 slug: "ontology-map-unusable",
740 version: "v1",
741 title: "ontology-map.json is missing or unparseable — cannot prove typing",
742 status: 422,
743 exit_code: 3,
744 },
745 Self::RelationshipTargetFindingUnparseable { .. } => ProblemMeta {
746 slug: "relationship-target-finding-unparseable",
747 version: "v1",
748 title: "A finding failed to parse while checking relationship targets",
749 status: 400,
750 exit_code: 2,
751 },
752 Self::SubtypeOfCycle { .. } => ProblemMeta {
753 slug: "subtype-of-cycle",
754 version: "v1",
755 title: "subtype_of graph contains a cycle",
756 status: 422,
757 exit_code: 4,
758 },
759 Self::EntityTypeSchemaInvalid { .. } => ProblemMeta {
760 slug: "entity-type-schema-invalid",
761 version: "v1",
762 title: "Entity type validation schema is malformed",
763 status: 422,
764 exit_code: 4,
765 },
766 Self::Index { .. } => ProblemMeta {
767 slug: "index-failure",
768 version: "v1",
769 title: "A SQLite index operation failed",
770 status: 500,
771 exit_code: 1,
772 },
773 Self::LockIo { .. } => ProblemMeta {
774 slug: "lock-io",
775 version: "v1",
776 title: "Failed to acquire the review lock file",
777 status: 500,
778 exit_code: 2,
779 },
780 Self::LockHeld { .. } => ProblemMeta {
781 slug: "lock-held",
782 version: "v1",
783 title: "Another review is already in progress",
784 status: 409,
785 exit_code: 2,
786 },
787 Self::QueueTopicMismatch { .. } => ProblemMeta {
788 slug: "queue-topic-mismatch",
789 version: "v1",
790 title: "Suggestion queue belongs to a different topic",
791 status: 409,
792 exit_code: 2,
793 },
794 Self::Embed(_) => ProblemMeta {
795 slug: "delegated-embed",
796 version: "v1",
797 title: "Delegated embedding error",
798 status: 500,
799 exit_code: 1,
800 },
801 Self::RegistryFetch { .. } => ProblemMeta {
802 slug: "registry-fetch-failed",
803 version: "v1",
804 title: "Failed to fetch from the ontology registry source",
805 status: 502,
806 exit_code: 1,
807 },
808 Self::RegistryIndexInvalid { .. } => ProblemMeta {
809 slug: "registry-index-invalid",
810 version: "v1",
811 title: "Ontology registry index is malformed",
812 status: 502,
813 exit_code: 1,
814 },
815 Self::OntologyNotInRegistry { .. } => ProblemMeta {
816 slug: "ontology-not-in-registry",
817 version: "v1",
818 title: "Ontology id has no registry index entry",
819 status: 404,
820 exit_code: 1,
821 },
822 Self::LockSourceMismatch { .. } => ProblemMeta {
823 slug: "lock-source-mismatch",
824 version: "v1",
825 title: "Lock file is pinned to a different registry source",
826 status: 409,
827 exit_code: 1,
828 },
829 Self::OntologyPackNotUtf8 { .. } => ProblemMeta {
830 slug: "ontology-pack-not-utf8",
831 version: "v1",
832 title: "Fetched ontology pack is not valid UTF-8",
833 status: 422,
834 exit_code: 1,
835 },
836 Self::IndexPinMismatch { .. } => ProblemMeta {
837 slug: "index-pin-mismatch",
838 version: "v1",
839 title: "Registry index sha256 no longer matches the pinned value",
840 status: 409,
841 exit_code: 1,
842 },
843 Self::ChecksumMismatch { .. } => ProblemMeta {
844 slug: "ontology-checksum-mismatch",
845 version: "v1",
846 title: "Fetched ontology file does not match its pinned sha256",
847 status: 422,
848 exit_code: 1,
849 },
850 Self::UnsafeIndexPath { .. } => ProblemMeta {
851 slug: "unsafe-index-path",
852 version: "v1",
853 title: "Registry index names an unsafe file path",
854 status: 422,
855 exit_code: 1,
856 },
857 Self::MalformedOntologyId { .. } => ProblemMeta {
858 slug: "malformed-ontology-id",
859 version: "v1",
860 title: "Registry index declares a malformed ontology id",
861 status: 422,
862 exit_code: 1,
863 },
864 Self::ConfigMalformed { .. } => ProblemMeta {
865 slug: "config-malformed",
866 version: "v1",
867 title: "Harness config's .ontologies field is not an array",
868 status: 422,
869 exit_code: 1,
870 },
871 Self::NoEntityTypesFound { .. } => ProblemMeta {
872 slug: "no-entity-types-found",
873 version: "v1",
874 title: "Topic's ontology map carries no typed entities to draft from",
875 status: 422,
876 exit_code: 1,
877 },
878 Self::NoClustersFound { .. } => ProblemMeta {
879 slug: "no-clusters-found",
880 version: "v1",
881 title: "Expansion-candidates file carries no clusters to draft from",
882 status: 422,
883 exit_code: 1,
884 },
885 Self::VersionNotSemver { .. } => ProblemMeta {
886 slug: "version-not-semver",
887 version: "v1",
888 title: "Version is not well-formed semver",
889 status: 422,
890 exit_code: 2,
891 },
892 Self::VersionMissing { .. } => ProblemMeta {
893 slug: "version-missing",
894 version: "v1",
895 title: "File has no .version field",
896 status: 422,
897 exit_code: 2,
898 },
899 Self::VersionUnchanged { .. } => ProblemMeta {
900 slug: "version-unchanged",
901 version: "v1",
902 title: "New version equals the current version",
903 status: 422,
904 exit_code: 2,
905 },
906 Self::PackNotFound { .. } => ProblemMeta {
907 slug: "pack-not-found",
908 version: "v1",
909 title: "Pack component not found",
910 status: 404,
911 exit_code: 2,
912 },
913 Self::PackAmbiguous { .. } => ProblemMeta {
914 slug: "pack-ambiguous",
915 version: "v1",
916 title: "Pack component name is ambiguous",
917 status: 422,
918 exit_code: 2,
919 },
920 Self::PackFileMissing { .. } => ProblemMeta {
921 slug: "pack-file-missing",
922 version: "v1",
923 title: "Pack is missing a required file or section",
924 status: 422,
925 exit_code: 2,
926 },
927 Self::PackVersionInvalid { .. } => ProblemMeta {
928 slug: "pack-version-invalid",
929 version: "v1",
930 title: "Pack's declared version is not well-formed semver",
931 status: 422,
932 exit_code: 2,
933 },
934 Self::PackAheadOfRelease { .. } => ProblemMeta {
935 slug: "pack-ahead-of-release",
936 version: "v1",
937 title: "Pack's current version is ahead of the release being cut",
938 status: 409,
939 exit_code: 2,
940 },
941 Self::ChangelogAnchorMissing { .. } => ProblemMeta {
942 slug: "changelog-anchor-missing",
943 version: "v1",
944 title: "CHANGELOG has no anchor to insert the new version under",
945 status: 422,
946 exit_code: 2,
947 },
948 Self::VerificationFailed { .. } => ProblemMeta {
949 slug: "verification-failed",
950 version: "v1",
951 title: "Post-write self-verification found an unexpected file",
952 status: 500,
953 exit_code: 2,
954 },
955 Self::GitCommandFailed { .. } => ProblemMeta {
956 slug: "git-command-failed",
957 version: "v1",
958 title: "A git invocation exited non-zero",
959 status: 500,
960 exit_code: 2,
961 },
962 }
963 }
964}
965
966#[allow(clippy::too_many_lines)]
973fn fix_and_action(error: &MifRhError) -> (SuggestedFix, CodeAction) {
974 match error {
975 MifRhError::OntologyPackYaml { .. } => (
976 SuggestedFix::new(
977 "Fix the YAML syntax error in the ontology pack, then retry.",
978 Applicability::MaybeIncorrect,
979 ),
980 CodeAction::new(
981 "Fix the malformed ontology pack YAML",
982 "quickfix",
983 Applicability::MaybeIncorrect,
984 ),
985 ),
986 MifRhError::CatalogMissing { .. } => (
987 SuggestedFix::new(
988 "Run rht's scripts/sync-packs.sh (or mif-rh-cli's equivalent) to generate the \
989 catalog, then retry.",
990 Applicability::MachineApplicable,
991 ),
992 CodeAction::new(
993 "Generate the missing catalog",
994 "quickfix",
995 Applicability::MachineApplicable,
996 ),
997 ),
998 MifRhError::ConfigMissing { .. } => (
999 SuggestedFix::new(
1000 "Supply the correct --config path to harness.config.json, then retry.",
1001 Applicability::MaybeIncorrect,
1002 ),
1003 CodeAction::new(
1004 "Correct the --config path",
1005 "quickfix",
1006 Applicability::MaybeIncorrect,
1007 ),
1008 ),
1009 MifRhError::DirectBindingInvalid { .. } => (
1010 SuggestedFix::new(
1011 "Catalog the missing ontology, correct its pinned version, or remove the \
1012 invalid topic binding, then retry.",
1013 Applicability::MaybeIncorrect,
1014 ),
1015 CodeAction::new(
1016 "Fix the topic's ontology binding",
1017 "quickfix",
1018 Applicability::MaybeIncorrect,
1019 ),
1020 ),
1021 MifRhError::EntityTypeSchemaInvalid { .. } => (
1022 SuggestedFix::new(
1023 "Fix the entity type's schema field in the ontology pack, then retry.",
1024 Applicability::MaybeIncorrect,
1025 ),
1026 CodeAction::new(
1027 "Fix the entity type's schema",
1028 "quickfix",
1029 Applicability::MaybeIncorrect,
1030 ),
1031 ),
1032 MifRhError::Index { .. } => (
1033 SuggestedFix::new(
1034 "This indicates a corrupt or inaccessible index database. Delete it and \
1035 rebuild with `mif-rh-cli review --build-index`, then retry.",
1036 Applicability::Unspecified,
1037 ),
1038 CodeAction::new("Rebuild the index", "quickfix", Applicability::Unspecified),
1039 ),
1040 MifRhError::LockHeld { .. } => (
1041 SuggestedFix::new(
1042 "Wait for the in-progress review to finish, then retry.",
1043 Applicability::MaybeIncorrect,
1044 ),
1045 CodeAction::new("Wait and retry", "quickfix", Applicability::MaybeIncorrect),
1046 ),
1047 MifRhError::QueueTopicMismatch { .. } => (
1048 SuggestedFix::new(
1049 "Point the upsert at reports/_meta/suggestions/<topic>.json for the topic \
1050 being reviewed, or remove the stray queue file that was copied or renamed \
1051 across topics.",
1052 Applicability::MaybeIncorrect,
1053 ),
1054 CodeAction::new(
1055 "Use the topic's own suggestion queue path",
1056 "quickfix",
1057 Applicability::MaybeIncorrect,
1058 ),
1059 ),
1060 MifRhError::JsonSerialize { .. } => (
1061 SuggestedFix::new(
1062 "This indicates a bug in mif-rh: a value could not be serialized to JSON. \
1063 Report it upstream with the record that triggered it; no caller-side fix \
1064 exists.",
1065 Applicability::Unspecified,
1066 ),
1067 CodeAction::new(
1068 "Report the serialization bug",
1069 "quickfix",
1070 Applicability::Unspecified,
1071 ),
1072 ),
1073 MifRhError::FrontmatterYamlSerialize { .. } => (
1074 SuggestedFix::new(
1075 "This indicates a bug in mif-rh: a report's concept frontmatter could not be \
1076 serialized to YAML. Report it upstream with the artifact that triggered it; \
1077 no caller-side fix exists.",
1078 Applicability::Unspecified,
1079 ),
1080 CodeAction::new(
1081 "Report the serialization bug",
1082 "quickfix",
1083 Applicability::Unspecified,
1084 ),
1085 ),
1086 MifRhError::FindingJson { .. } | MifRhError::Json { .. } => (
1090 SuggestedFix::new(
1091 "Correct the file so it is valid JSON, then retry.",
1092 Applicability::MaybeIncorrect,
1093 ),
1094 CodeAction::new(
1095 "Fix the JSON syntax error",
1096 "quickfix",
1097 Applicability::MaybeIncorrect,
1098 ),
1099 ),
1100 MifRhError::RegistryFetch { .. } => (
1101 SuggestedFix::new(
1102 "Check network access to the ontology source (or, for a local directory \
1103 source, that the path exists), then retry.",
1104 Applicability::MaybeIncorrect,
1105 ),
1106 CodeAction::new(
1107 "Check the ontology source is reachable",
1108 "quickfix",
1109 Applicability::MaybeIncorrect,
1110 ),
1111 ),
1112 MifRhError::RegistryIndexInvalid { .. } => (
1113 SuggestedFix::new(
1114 "The registry source's index.json is not valid — fix it upstream, or point \
1115 MIF_ONTOLOGY_SOURCE at a known-good mirror, then retry.",
1116 Applicability::MaybeIncorrect,
1117 ),
1118 CodeAction::new(
1119 "Fix or repoint the registry index",
1120 "quickfix",
1121 Applicability::MaybeIncorrect,
1122 ),
1123 ),
1124 MifRhError::OntologyNotInRegistry { .. } => (
1125 SuggestedFix::new(
1126 "Author the ontology from your research and contribute it upstream with the \
1127 `ontology author` subcommand, then retry.",
1128 Applicability::MaybeIncorrect,
1129 ),
1130 CodeAction::new(
1131 "Author the missing ontology",
1132 "quickfix",
1133 Applicability::MaybeIncorrect,
1134 ),
1135 ),
1136 MifRhError::LockSourceMismatch { .. } => (
1137 SuggestedFix::new(
1138 "Re-run against the source ontologies.lock.json is actually pinned to, or \
1139 delete the lock to intentionally re-pin against the new source.",
1140 Applicability::Unspecified,
1141 ),
1142 CodeAction::new(
1143 "Match the requested source to the pinned lock source",
1144 "quickfix",
1145 Applicability::Unspecified,
1146 ),
1147 ),
1148 MifRhError::OntologyPackNotUtf8 { .. } => (
1149 SuggestedFix::new(
1150 "The registry-published ontology pack file is corrupt or was published with \
1151 non-UTF-8 content; investigate upstream and re-publish a valid pack.",
1152 Applicability::Unspecified,
1153 ),
1154 CodeAction::new(
1155 "Investigate the corrupt registry pack",
1156 "quickfix",
1157 Applicability::Unspecified,
1158 ),
1159 ),
1160 MifRhError::IndexPinMismatch { .. } => (
1161 SuggestedFix::new(
1162 "Confirm the registry source is trustworthy, then clear index_sha256 in \
1163 ontologies.lock.json to re-pin deliberately, or investigate why the trust \
1164 root moved.",
1165 Applicability::Unspecified,
1166 ),
1167 CodeAction::new(
1168 "Investigate the moved trust root",
1169 "quickfix",
1170 Applicability::Unspecified,
1171 ),
1172 ),
1173 MifRhError::ChecksumMismatch { .. } => (
1174 SuggestedFix::new(
1175 "The fetched ontology does not match the pinned registry hash. Do not vendor \
1176 it; investigate the registry source for tampering or corruption.",
1177 Applicability::Unspecified,
1178 ),
1179 CodeAction::new(
1180 "Investigate the checksum mismatch",
1181 "quickfix",
1182 Applicability::Unspecified,
1183 ),
1184 ),
1185 MifRhError::NoEntityTypesFound { .. } => (
1186 SuggestedFix::new(
1187 "Run /ontology-review on the topic first so its findings get typed, then \
1188 retry.",
1189 Applicability::MaybeIncorrect,
1190 ),
1191 CodeAction::new(
1192 "Review the topic before authoring",
1193 "quickfix",
1194 Applicability::MaybeIncorrect,
1195 ),
1196 ),
1197 MifRhError::NoClustersFound { .. } => (
1198 SuggestedFix::new(
1199 "Run `mif-rh-cli ontology expansion-candidates` again once more tier-3 misses \
1200 have recurred across runs, then retry.",
1201 Applicability::MaybeIncorrect,
1202 ),
1203 CodeAction::new(
1204 "Wait for recurring misses before authoring",
1205 "quickfix",
1206 Applicability::MaybeIncorrect,
1207 ),
1208 ),
1209 MifRhError::VersionNotSemver { .. } | MifRhError::PackVersionInvalid { .. } => (
1210 SuggestedFix::new(
1211 "Correct the version to well-formed X.Y.Z semver, then retry.",
1212 Applicability::MaybeIncorrect,
1213 ),
1214 CodeAction::new(
1215 "Fix the malformed version",
1216 "quickfix",
1217 Applicability::MaybeIncorrect,
1218 ),
1219 ),
1220 MifRhError::VersionMissing { .. } => (
1221 SuggestedFix::new(
1222 "Add a .version field to the file, then retry.",
1223 Applicability::MaybeIncorrect,
1224 ),
1225 CodeAction::new(
1226 "Add the missing .version field",
1227 "quickfix",
1228 Applicability::MaybeIncorrect,
1229 ),
1230 ),
1231 MifRhError::VersionUnchanged { .. } => (
1232 SuggestedFix::new(
1233 "Pass a different version or bump keyword — the requested version matches the \
1234 current one, so there is nothing to bump.",
1235 Applicability::MaybeIncorrect,
1236 ),
1237 CodeAction::new(
1238 "Choose a different version",
1239 "quickfix",
1240 Applicability::MaybeIncorrect,
1241 ),
1242 ),
1243 MifRhError::PackNotFound { .. } | MifRhError::PackAmbiguous { .. } => (
1244 SuggestedFix::new(
1245 "Check the component name against packs/<family>/<name>/ and retry.",
1246 Applicability::MaybeIncorrect,
1247 ),
1248 CodeAction::new(
1249 "Correct the pack component name",
1250 "quickfix",
1251 Applicability::MaybeIncorrect,
1252 ),
1253 ),
1254 MifRhError::PackFileMissing { .. } => (
1255 SuggestedFix::new(
1256 "Add the missing file or section the pack's own conventions require \
1257 (plugin.json .version, SKILL.md version: frontmatter, or the family doc's \
1258 **Version:** row), then retry.",
1259 Applicability::MaybeIncorrect,
1260 ),
1261 CodeAction::new(
1262 "Add the missing pack file/section",
1263 "quickfix",
1264 Applicability::MaybeIncorrect,
1265 ),
1266 ),
1267 MifRhError::PackAheadOfRelease { .. } => (
1268 SuggestedFix::new(
1269 "Cut a release at or above the pack's current version, or leave this pack out \
1270 of this bump.",
1271 Applicability::MaybeIncorrect,
1272 ),
1273 CodeAction::new(
1274 "Reconcile the release version with the pack's version",
1275 "quickfix",
1276 Applicability::MaybeIncorrect,
1277 ),
1278 ),
1279 MifRhError::ChangelogAnchorMissing { .. } => (
1280 SuggestedFix::new(
1281 "Add an '## [Unreleased]' section to the CHANGELOG, then retry.",
1282 Applicability::MaybeIncorrect,
1283 ),
1284 CodeAction::new(
1285 "Add the Unreleased anchor",
1286 "quickfix",
1287 Applicability::MaybeIncorrect,
1288 ),
1289 ),
1290 MifRhError::VerificationFailed { .. } => (
1291 SuggestedFix::new(
1292 "Inspect the named file directly — the write may have partially applied. This \
1293 indicates a bug in the bump logic, not a caller-side fix.",
1294 Applicability::Unspecified,
1295 ),
1296 CodeAction::new(
1297 "Inspect the file that failed verification",
1298 "quickfix",
1299 Applicability::Unspecified,
1300 ),
1301 ),
1302 MifRhError::GitCommandFailed { .. } => (
1303 SuggestedFix::new(
1304 "Run the command's directory root through `git status` to confirm it's a real, \
1305 uncorrupted git repository, then retry.",
1306 Applicability::MaybeIncorrect,
1307 ),
1308 CodeAction::new(
1309 "Verify the git repository is valid",
1310 "quickfix",
1311 Applicability::MaybeIncorrect,
1312 ),
1313 ),
1314 MifRhError::ConfigMalformed { .. } => (
1315 SuggestedFix::new(
1316 "Fix harness.config.json so its .ontologies field is an array, then retry.",
1317 Applicability::MaybeIncorrect,
1318 ),
1319 CodeAction::new(
1320 "Fix the .ontologies field's shape",
1321 "quickfix",
1322 Applicability::MaybeIncorrect,
1323 ),
1324 ),
1325 MifRhError::UnsafeIndexPath { .. } | MifRhError::MalformedOntologyId { .. } => (
1326 SuggestedFix::new(
1327 "The registry index entry is malformed or unsafe — fix it upstream in the \
1328 canonical registry before retrying.",
1329 Applicability::Unspecified,
1330 ),
1331 CodeAction::new(
1332 "Fix the malformed registry entry",
1333 "quickfix",
1334 Applicability::Unspecified,
1335 ),
1336 ),
1337 MifRhError::SchemaCompilation { .. } => (
1338 SuggestedFix::new(
1339 "Fix the schema (or its $ref dependency) so it is valid JSON Schema, then \
1340 retry.",
1341 Applicability::MaybeIncorrect,
1342 ),
1343 CodeAction::new(
1344 "Fix the malformed schema",
1345 "quickfix",
1346 Applicability::MaybeIncorrect,
1347 ),
1348 ),
1349 MifRhError::RefSchemaMissingId { .. } => (
1350 SuggestedFix::new(
1351 "Add a $id to the dependency schema, then retry.",
1352 Applicability::MaybeIncorrect,
1353 ),
1354 CodeAction::new(
1355 "Add the missing $id",
1356 "quickfix",
1357 Applicability::MaybeIncorrect,
1358 ),
1359 ),
1360 MifRhError::SchemaValidationFailed { .. } => (
1361 SuggestedFix::new(
1362 "Fix the report to conform to its schema (see the listed validation errors), \
1363 then retry.",
1364 Applicability::MaybeIncorrect,
1365 ),
1366 CodeAction::new(
1367 "Fix the schema violations",
1368 "quickfix",
1369 Applicability::MaybeIncorrect,
1370 ),
1371 ),
1372 MifRhError::InvalidToggleValue { .. } => (
1373 SuggestedFix::new(
1374 "Pass one of the allowed values, then retry.",
1375 Applicability::MaybeIncorrect,
1376 ),
1377 CodeAction::new(
1378 "Correct the toggle value",
1379 "quickfix",
1380 Applicability::MaybeIncorrect,
1381 ),
1382 ),
1383 MifRhError::EmptySourceContent => (
1384 SuggestedFix::new(
1385 "Provide content via --content-file, --content, or stdin.",
1386 Applicability::MaybeIncorrect,
1387 ),
1388 CodeAction::new(
1389 "Provide source content",
1390 "quickfix",
1391 Applicability::MaybeIncorrect,
1392 ),
1393 ),
1394 MifRhError::PackNotDeclared { .. } => (
1395 SuggestedFix::new(
1396 "Declare the pack in the manifest's packs[] array, then retry.",
1397 Applicability::MaybeIncorrect,
1398 ),
1399 CodeAction::new(
1400 "Declare the pack first",
1401 "quickfix",
1402 Applicability::MaybeIncorrect,
1403 ),
1404 ),
1405 MifRhError::NoFindingsFound { .. } => (
1406 SuggestedFix::new(
1407 "Point at a directory containing finding JSON files, then retry.",
1408 Applicability::MaybeIncorrect,
1409 ),
1410 CodeAction::new(
1411 "Point at a non-empty findings directory",
1412 "quickfix",
1413 Applicability::MaybeIncorrect,
1414 ),
1415 ),
1416 MifRhError::NoSurvivingFindings { .. } => (
1417 SuggestedFix::new(
1418 "Nothing to do until at least one finding survives falsification.",
1419 Applicability::MaybeIncorrect,
1420 ),
1421 CodeAction::new(
1422 "Wait for a surviving finding",
1423 "quickfix",
1424 Applicability::MaybeIncorrect,
1425 ),
1426 ),
1427 MifRhError::ArtifactNotPublishable { .. } => (
1428 SuggestedFix::new(
1429 "Ensure at least one surviving finding carries a citation before synthesizing.",
1430 Applicability::MaybeIncorrect,
1431 ),
1432 CodeAction::new(
1433 "Add a citation to a surviving finding",
1434 "quickfix",
1435 Applicability::MaybeIncorrect,
1436 ),
1437 ),
1438 MifRhError::MissingProvenance { .. } => (
1439 SuggestedFix::new(
1440 "Add a provenance block to every listed finding before retrying the import.",
1441 Applicability::MaybeIncorrect,
1442 ),
1443 CodeAction::new(
1444 "Add the missing provenance blocks",
1445 "quickfix",
1446 Applicability::MaybeIncorrect,
1447 ),
1448 ),
1449 MifRhError::ReconcileEnvironmentBroken { .. } => (
1450 SuggestedFix::new(
1451 "Check the schema/$ref files and the jsonschema toolchain — a known-good \
1452 sample should always validate.",
1453 Applicability::Unspecified,
1454 ),
1455 CodeAction::new(
1456 "Diagnose the schema toolchain",
1457 "quickfix",
1458 Applicability::Unspecified,
1459 ),
1460 ),
1461 MifRhError::TopicNotRegistered { .. } => (
1462 SuggestedFix::new(
1463 "Register the topic in harness.config.json's topics[] before building its README.",
1464 Applicability::MaybeIncorrect,
1465 ),
1466 CodeAction::new(
1467 "Register the topic",
1468 "quickfix",
1469 Applicability::MaybeIncorrect,
1470 ),
1471 ),
1472 MifRhError::InvalidConcordance { .. } => (
1473 SuggestedFix::new(
1474 "Build the concordance first (scripts/build-concordance.sh) before synthesizing the corpus atlas.",
1475 Applicability::MaybeIncorrect,
1476 ),
1477 CodeAction::new(
1478 "Build the concordance",
1479 "quickfix",
1480 Applicability::MaybeIncorrect,
1481 ),
1482 ),
1483 MifRhError::OntologyMapUnusable { topic, .. } => (
1484 SuggestedFix::new(
1485 format!(
1486 "Regenerate the ontology map: /ontology-review --topic {topic} --enrich, then /resume --topic {topic}."
1487 ),
1488 Applicability::MaybeIncorrect,
1489 ),
1490 CodeAction::new(
1491 "Regenerate the ontology map",
1492 "quickfix",
1493 Applicability::MaybeIncorrect,
1494 ),
1495 ),
1496 MifRhError::RelationshipTargetFindingUnparseable { .. } => (
1497 SuggestedFix::new(
1498 "Fix the invalid JSON in this finding before re-running the relationship-targets gate.",
1499 Applicability::MachineApplicable,
1500 ),
1501 CodeAction::new(
1502 "Fix the malformed finding JSON",
1503 "quickfix",
1504 Applicability::MachineApplicable,
1505 ),
1506 ),
1507 MifRhError::SubtypeOfCycle { .. } => (
1508 SuggestedFix::new(
1509 "Break the subtype_of cycle in the ontology registry — a type cannot (transitively) subtype itself.",
1510 Applicability::MaybeIncorrect,
1511 ),
1512 CodeAction::new(
1513 "Fix the subtype_of cycle",
1514 "quickfix",
1515 Applicability::MaybeIncorrect,
1516 ),
1517 ),
1518 MifRhError::FindingIo { .. }
1519 | MifRhError::Io { .. }
1520 | MifRhError::LockIo { .. }
1521 | MifRhError::Ontology(_)
1522 | MifRhError::Frontmatter(_)
1523 | MifRhError::Embed(_) => unreachable!(
1524 "to_problem handles the IO-classified and delegated variants before calling \
1525 fix_and_action"
1526 ),
1527 }
1528}
1529
1530impl ToProblem for MifRhError {
1531 fn to_problem(&self) -> ProblemDetails {
1532 match self {
1533 Self::Ontology(inner) => inner.to_problem(),
1534 Self::Frontmatter(inner) => inner.to_problem(),
1535 Self::Embed(inner) => inner.to_problem(),
1536 Self::FindingIo { source, .. }
1537 | Self::Io { source, .. }
1538 | Self::LockIo { source, .. } => {
1539 let (status, fix, action) = mif_problem::classify_io_error(source);
1540 let mut problem = self
1541 .meta()
1542 .into_details(env!("CARGO_PKG_NAME"), self.to_string());
1543 problem.status = status;
1544 problem.with_suggested_fix(fix).with_code_action(action)
1545 },
1546 _ => {
1547 let (fix, action) = fix_and_action(self);
1548 self.meta()
1549 .into_details(env!("CARGO_PKG_NAME"), self.to_string())
1550 .with_suggested_fix(fix)
1551 .with_code_action(action)
1552 },
1553 }
1554 }
1555}
1556
1557impl From<rusqlite::Error> for MifRhError {
1558 fn from(source: rusqlite::Error) -> Self {
1559 Self::Index { source }
1560 }
1561}
1562
1563#[cfg(test)]
1564mod tests {
1565 use super::MifRhError;
1566
1567 fn io_error() -> std::io::Error {
1568 std::io::Error::new(std::io::ErrorKind::NotFound, "not found")
1569 }
1570
1571 fn json_error() -> serde_json::Error {
1572 serde_json::from_str::<serde_json::Value>("not json").unwrap_err()
1573 }
1574
1575 fn yaml_error() -> serde_norway::Error {
1576 serde_norway::from_str::<serde_json::Value>("- a\n bad: [unterminated").unwrap_err()
1577 }
1578
1579 #[allow(clippy::too_many_lines)]
1582 fn every_variant() -> Vec<MifRhError> {
1583 vec![
1584 MifRhError::FindingIo {
1585 path: "f.json".to_string(),
1586 source: io_error(),
1587 },
1588 MifRhError::FindingJson {
1589 path: "f.json".to_string(),
1590 source: json_error(),
1591 },
1592 MifRhError::Io {
1593 path: "x".to_string(),
1594 source: io_error(),
1595 },
1596 MifRhError::Json {
1597 path: "x.json".to_string(),
1598 source: json_error(),
1599 },
1600 MifRhError::JsonSerialize {
1601 path: "x.json".to_string(),
1602 source: json_error(),
1603 },
1604 MifRhError::OntologyPackYaml {
1605 path: "x.yaml".to_string(),
1606 source: yaml_error(),
1607 },
1608 MifRhError::FrontmatterYamlSerialize {
1609 source: yaml_error(),
1610 },
1611 MifRhError::CatalogMissing {
1612 path: "catalog.json".to_string(),
1613 },
1614 MifRhError::ConfigMissing {
1615 path: "config.json".to_string(),
1616 },
1617 MifRhError::DirectBindingInvalid {
1618 topic: "t".to_string(),
1619 id: "o".to_string(),
1620 },
1621 MifRhError::Ontology(mif_ontology::OntologyError::Io {
1622 path: "onto.yaml".to_string(),
1623 source: io_error(),
1624 }),
1625 MifRhError::EntityTypeSchemaInvalid {
1626 entity_type: "widget".to_string(),
1627 detail: "bad schema".to_string(),
1628 },
1629 MifRhError::Index {
1630 source: rusqlite::Error::InvalidParameterName("p".to_string()),
1631 },
1632 MifRhError::LockIo {
1633 path: "lock".to_string(),
1634 source: io_error(),
1635 },
1636 MifRhError::LockHeld { holder_pid: 1234 },
1637 MifRhError::QueueTopicMismatch {
1638 path: "reports/_meta/suggestions/edu.json".to_string(),
1639 expected: "edu".to_string(),
1640 found: "sec".to_string(),
1641 },
1642 MifRhError::Embed(mif_embed::EmbedError::NoCacheDir {
1643 model: "test-model",
1644 }),
1645 MifRhError::RegistryFetch {
1646 registry_source: "https://example.test/ontologies".to_string(),
1647 detail: "connection refused".to_string(),
1648 },
1649 MifRhError::RegistryIndexInvalid {
1650 registry_source: "https://example.test/ontologies".to_string(),
1651 detail: "no .ontologies key".to_string(),
1652 },
1653 MifRhError::OntologyNotInRegistry {
1654 id: "clinical-trials".to_string(),
1655 },
1656 MifRhError::LockSourceMismatch {
1657 lock_source: "https://example.test/ontologies".to_string(),
1658 requested_source: "https://other.test/ontologies".to_string(),
1659 },
1660 MifRhError::OntologyPackNotUtf8 {
1661 id: "edu-fixture".to_string(),
1662 file: "edu-fixture.ontology.yaml".to_string(),
1663 },
1664 MifRhError::IndexPinMismatch {
1665 registry_source: "https://example.test/ontologies".to_string(),
1666 pinned: "aaa".to_string(),
1667 got: "bbb".to_string(),
1668 },
1669 MifRhError::ChecksumMismatch {
1670 id: "edu-fixture".to_string(),
1671 file: "edu-fixture.ontology.yaml".to_string(),
1672 expected: "aaa".to_string(),
1673 got: "bbb".to_string(),
1674 },
1675 MifRhError::UnsafeIndexPath {
1676 id: "edu-fixture".to_string(),
1677 file: "../../etc/passwd".to_string(),
1678 },
1679 MifRhError::MalformedOntologyId {
1680 id: "../etc".to_string(),
1681 },
1682 MifRhError::ConfigMalformed {
1683 path: "harness.config.json".to_string(),
1684 detail: ".ontologies is a string, not an array".to_string(),
1685 },
1686 MifRhError::NoEntityTypesFound {
1687 topic: "edu".to_string(),
1688 },
1689 MifRhError::NoClustersFound {
1690 path: "clusters.json".to_string(),
1691 },
1692 MifRhError::VersionNotSemver {
1693 value: "1.0".to_string(),
1694 },
1695 MifRhError::VersionMissing {
1696 path: "harness.config.json".to_string(),
1697 },
1698 MifRhError::VersionUnchanged {
1699 value: "1.0.0".to_string(),
1700 },
1701 MifRhError::PackNotFound {
1702 name: "pdf".to_string(),
1703 },
1704 MifRhError::PackAmbiguous {
1705 name: "pdf".to_string(),
1706 },
1707 MifRhError::PackFileMissing {
1708 name: "pdf".to_string(),
1709 path: "packs/x/pdf/.claude-plugin/plugin.json".to_string(),
1710 },
1711 MifRhError::PackVersionInvalid {
1712 name: "pdf".to_string(),
1713 path: "packs/x/pdf/.claude-plugin/plugin.json".to_string(),
1714 value: "not-semver".to_string(),
1715 },
1716 MifRhError::PackAheadOfRelease {
1717 name: "pdf".to_string(),
1718 pack_version: "2.0.0".to_string(),
1719 new_version: "1.5.0".to_string(),
1720 },
1721 MifRhError::ChangelogAnchorMissing {
1722 path: "CHANGELOG.md".to_string(),
1723 },
1724 MifRhError::VerificationFailed {
1725 path: "harness.config.json".to_string(),
1726 },
1727 MifRhError::GitCommandFailed {
1728 command: "tag --list v*".to_string(),
1729 stderr: "fatal: not a git repository".to_string(),
1730 },
1731 MifRhError::Frontmatter(mif_frontmatter::FrontmatterError::MissingFrontmatter),
1732 MifRhError::SchemaCompilation {
1733 path: "findings.schema.json".to_string(),
1734 detail: "not valid JSON".to_string(),
1735 },
1736 MifRhError::RefSchemaMissingId {
1737 path: "entity-reference.schema.json".to_string(),
1738 },
1739 MifRhError::SchemaValidationFailed {
1740 path: "reports/x/findings/1.md".to_string(),
1741 schema_path: "schemas/findings.schema.json".to_string(),
1742 detail: "missing required field 'title'".to_string(),
1743 },
1744 MifRhError::InvalidToggleValue {
1745 field: "primarySurface".to_string(),
1746 value: "bogus".to_string(),
1747 allowed: "reports|docs|auto".to_string(),
1748 },
1749 MifRhError::EmptySourceContent,
1750 MifRhError::PackNotDeclared {
1751 name: "pdf".to_string(),
1752 path: "harness.config.json".to_string(),
1753 },
1754 MifRhError::NoFindingsFound {
1755 path: "reports/x/findings".to_string(),
1756 },
1757 MifRhError::NoSurvivingFindings {
1758 path: "reports/x/findings".to_string(),
1759 },
1760 MifRhError::ArtifactNotPublishable {
1761 path: "reports/x/findings".to_string(),
1762 },
1763 MifRhError::MissingProvenance {
1764 count: 1,
1765 paths: vec!["src/findings/f1.json".to_string()],
1766 },
1767 MifRhError::ReconcileEnvironmentBroken {
1768 sample_path: "schemas/samples/finding.sample.json".to_string(),
1769 },
1770 MifRhError::TopicNotRegistered {
1771 topic: "x".to_string(),
1772 config_path: "harness.config.json".to_string(),
1773 },
1774 MifRhError::InvalidConcordance {
1775 path: "reports/concordance.json".to_string(),
1776 },
1777 MifRhError::OntologyMapUnusable {
1778 path: "reports/edu/ontology-map.json".to_string(),
1779 topic: "edu".to_string(),
1780 reason: "is missing".to_string(),
1781 },
1782 MifRhError::RelationshipTargetFindingUnparseable {
1783 path: "reports/edu/findings/bad.json".to_string(),
1784 },
1785 MifRhError::SubtypeOfCycle {
1786 entity_type: "a".to_string(),
1787 },
1788 ]
1789 }
1790
1791 #[test]
1792 fn every_variant_produces_a_distinct_problem_type() {
1793 use mif_problem::ToProblem;
1794
1795 let problem_types: Vec<String> = every_variant()
1796 .iter()
1797 .map(|e| e.to_problem().problem_type)
1798 .collect();
1799 let mut deduped = problem_types.clone();
1800 deduped.sort();
1801 deduped.dedup();
1802 assert_eq!(
1803 problem_types.len(),
1804 deduped.len(),
1805 "expected every variant to produce a distinct problem_type: {problem_types:?}"
1806 );
1807 }
1808
1809 #[test]
1810 fn every_variant_has_a_display_message() {
1811 for error in every_variant() {
1812 assert!(!error.to_string().is_empty());
1813 }
1814 }
1815
1816 #[test]
1817 fn io_classified_variants_delegate_status_to_classify_io_error() {
1818 use mif_problem::ToProblem;
1819
1820 let not_found = MifRhError::Io {
1821 path: "missing".to_string(),
1822 source: io_error(),
1823 };
1824 assert_eq!(not_found.to_problem().status, 404);
1826 }
1827
1828 #[test]
1829 fn delegated_variants_forward_to_the_inner_error() {
1830 use mif_problem::ToProblem;
1831
1832 let ontology_problem = MifRhError::Ontology(mif_ontology::OntologyError::Io {
1833 path: "onto.yaml".to_string(),
1834 source: io_error(),
1835 })
1836 .to_problem();
1837 let inner_problem = mif_ontology::OntologyError::Io {
1838 path: "onto.yaml".to_string(),
1839 source: io_error(),
1840 }
1841 .to_problem();
1842 assert_eq!(ontology_problem.problem_type, inner_problem.problem_type);
1843
1844 let embed_problem = MifRhError::Embed(mif_embed::EmbedError::NoCacheDir {
1845 model: "test-model",
1846 })
1847 .to_problem();
1848 let inner_embed_problem = mif_embed::EmbedError::NoCacheDir {
1849 model: "test-model",
1850 }
1851 .to_problem();
1852 assert_eq!(embed_problem.problem_type, inner_embed_problem.problem_type);
1853 }
1854
1855 #[test]
1856 fn exit_codes_match_the_documented_scheme() {
1857 use mif_problem::ToProblem;
1858
1859 assert_eq!(
1860 MifRhError::CatalogMissing {
1861 path: "c".to_string()
1862 }
1863 .to_problem()
1864 .exit_code,
1865 Some(3)
1866 );
1867 assert_eq!(
1868 MifRhError::LockHeld { holder_pid: 1 }
1869 .to_problem()
1870 .exit_code,
1871 Some(2)
1872 );
1873 assert_eq!(
1874 MifRhError::DirectBindingInvalid {
1875 topic: "t".to_string(),
1876 id: "o".to_string(),
1877 }
1878 .to_problem()
1879 .exit_code,
1880 Some(1)
1881 );
1882 }
1883
1884 #[test]
1885 fn rusqlite_error_converts_into_the_index_variant() {
1886 let source = rusqlite::Error::InvalidParameterName("p".to_string());
1887 let error: MifRhError = source.into();
1888 assert!(matches!(error, MifRhError::Index { .. }));
1889 }
1890}