1use crate::language::{LanguageCapabilitySupport, language_capability, language_registry_digest};
4use blake3::Hasher;
5use serde::Serialize;
6use std::collections::BTreeSet;
7use std::error::Error;
8use std::fmt;
9use std::fmt::Write as _;
10use std::sync::OnceLock;
11
12pub const COMPLETE_SUPPORT_SCHEMA_VERSION: u32 = 1;
14
15pub const SUPPORT_CATALOG_VERSION: u32 = 1;
17
18#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd, Serialize)]
20pub struct SupportProfileId(&'static str);
21
22impl SupportProfileId {
23 #[must_use]
25 const fn new(value: &'static str) -> Self {
26 Self(value)
27 }
28
29 #[must_use]
31 pub const fn as_str(self) -> &'static str {
32 self.0
33 }
34}
35
36#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
38#[serde(rename_all = "snake_case")]
39pub enum SupportProfileKind {
40 Language,
42 Dialect,
44 DomainFormat,
46 FrameworkProjection,
48}
49
50impl SupportProfileKind {
51 #[must_use]
53 pub const fn as_str(self) -> &'static str {
54 match self {
55 Self::Language => "language",
56 Self::Dialect => "dialect",
57 Self::DomainFormat => "domain_format",
58 Self::FrameworkProjection => "framework_projection",
59 }
60 }
61
62 #[must_use]
64 pub const fn evidence_contract(self) -> ProfileEvidenceContract {
65 let admitted_not_applicable = match self {
66 Self::Language => LANGUAGE_NOT_APPLICABLE,
67 Self::Dialect => DIALECT_NOT_APPLICABLE,
68 Self::DomainFormat => DOMAIN_FORMAT_NOT_APPLICABLE,
69 Self::FrameworkProjection => FRAMEWORK_NOT_APPLICABLE,
70 };
71 ProfileEvidenceContract {
72 required_slots: EvidenceSlot::ALL,
73 admitted_not_applicable,
74 }
75 }
76}
77
78#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
80#[serde(rename_all = "kebab-case")]
81pub enum PresentationCategory {
82 Backend,
84 FrontendWeb,
86 Systems,
88 Mobile,
90 DataScientific,
92 EnterpriseLegacyModernization,
94 DatabaseQuery,
96 InfrastructureCloud,
98 BuildConfigTemplate,
100 TestingFrameworks,
102 HardwareDesign,
104}
105
106impl PresentationCategory {
107 pub const ALL: &'static [Self] = &[
109 Self::Backend,
110 Self::FrontendWeb,
111 Self::Systems,
112 Self::Mobile,
113 Self::DataScientific,
114 Self::EnterpriseLegacyModernization,
115 Self::DatabaseQuery,
116 Self::InfrastructureCloud,
117 Self::BuildConfigTemplate,
118 Self::TestingFrameworks,
119 Self::HardwareDesign,
120 ];
121
122 #[must_use]
124 pub const fn label(self) -> &'static str {
125 match self {
126 Self::Backend => "Backend",
127 Self::FrontendWeb => "Frontend and web",
128 Self::Systems => "Systems",
129 Self::Mobile => "Mobile",
130 Self::DataScientific => "Data and scientific",
131 Self::EnterpriseLegacyModernization => "Enterprise and legacy modernization",
132 Self::DatabaseQuery => "Database and query",
133 Self::InfrastructureCloud => "Infrastructure and cloud",
134 Self::BuildConfigTemplate => "Build, configuration, and template",
135 Self::TestingFrameworks => "Testing frameworks",
136 Self::HardwareDesign => "Hardware design",
137 }
138 }
139
140 const fn as_str(self) -> &'static str {
142 match self {
143 Self::Backend => "backend",
144 Self::FrontendWeb => "frontend-web",
145 Self::Systems => "systems",
146 Self::Mobile => "mobile",
147 Self::DataScientific => "data-scientific",
148 Self::EnterpriseLegacyModernization => "enterprise-legacy-modernization",
149 Self::DatabaseQuery => "database-query",
150 Self::InfrastructureCloud => "infrastructure-cloud",
151 Self::BuildConfigTemplate => "build-config-template",
152 Self::TestingFrameworks => "testing-frameworks",
153 Self::HardwareDesign => "hardware-design",
154 }
155 }
156}
157
158#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
160#[serde(rename_all = "kebab-case")]
161pub enum PresentationTag {
162 LegacyModernization,
164}
165
166impl PresentationTag {
167 const fn as_str(self) -> &'static str {
169 match self {
170 Self::LegacyModernization => "legacy-modernization",
171 }
172 }
173}
174
175#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
177#[serde(rename_all = "snake_case")]
178pub enum DialectEvidenceKind {
179 ProjectManifest,
181 PathConvention,
183 Configuration,
185 ContentSignature,
187}
188
189impl DialectEvidenceKind {
190 const fn as_str(self) -> &'static str {
192 match self {
193 Self::ProjectManifest => "project-manifest",
194 Self::PathConvention => "path-convention",
195 Self::Configuration => "configuration",
196 Self::ContentSignature => "content-signature",
197 }
198 }
199}
200
201#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
203#[serde(rename_all = "snake_case")]
204pub enum EvidenceSlot {
205 Detection,
207 DialectEvidence,
209 Parsing,
211 MalformedPartial,
213 Facts,
215 Relations,
217 Occurrences,
219 ResolutionOutcomes,
221 Publication,
223 UnitFixtures,
225 IntegrationFixtures,
227 RepresentativeRepository,
229 AgentNavigation,
231}
232
233impl EvidenceSlot {
234 pub const ALL: &'static [Self; 13] = &[
236 Self::Detection,
237 Self::DialectEvidence,
238 Self::Parsing,
239 Self::MalformedPartial,
240 Self::Facts,
241 Self::Relations,
242 Self::Occurrences,
243 Self::ResolutionOutcomes,
244 Self::Publication,
245 Self::UnitFixtures,
246 Self::IntegrationFixtures,
247 Self::RepresentativeRepository,
248 Self::AgentNavigation,
249 ];
250
251 const fn as_str(self) -> &'static str {
253 match self {
254 Self::Detection => "detection",
255 Self::DialectEvidence => "dialect-evidence",
256 Self::Parsing => "parsing",
257 Self::MalformedPartial => "malformed-partial",
258 Self::Facts => "facts",
259 Self::Relations => "relations",
260 Self::Occurrences => "occurrences",
261 Self::ResolutionOutcomes => "resolution-outcomes",
262 Self::Publication => "publication",
263 Self::UnitFixtures => "unit-fixtures",
264 Self::IntegrationFixtures => "integration-fixtures",
265 Self::RepresentativeRepository => "representative-repository",
266 Self::AgentNavigation => "agent-navigation",
267 }
268 }
269}
270
271#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
273#[serde(rename_all = "snake_case")]
274pub enum NotApplicableReason {
275 ProfileIsNotDialect,
277 ExactHostOwnsParsing,
279 NoRelationSemantics,
281 NoStaticResolution,
283}
284
285impl NotApplicableReason {
286 const fn as_str(self) -> &'static str {
288 match self {
289 Self::ProfileIsNotDialect => "profile-is-not-dialect",
290 Self::ExactHostOwnsParsing => "exact-host-owns-parsing",
291 Self::NoRelationSemantics => "no-relation-semantics",
292 Self::NoStaticResolution => "no-static-resolution",
293 }
294 }
295}
296
297#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
299pub struct AdmittedNotApplicable {
300 pub slot: EvidenceSlot,
302 pub reason: NotApplicableReason,
304}
305
306#[derive(Clone, Copy, Debug, Eq, PartialEq)]
308pub struct ProfileEvidenceContract {
309 pub required_slots: &'static [EvidenceSlot; 13],
311 pub admitted_not_applicable: &'static [AdmittedNotApplicable],
313}
314
315const LANGUAGE_NOT_APPLICABLE: &[AdmittedNotApplicable] = &[AdmittedNotApplicable {
317 slot: EvidenceSlot::DialectEvidence,
318 reason: NotApplicableReason::ProfileIsNotDialect,
319}];
320const DIALECT_NOT_APPLICABLE: &[AdmittedNotApplicable] = &[];
322const DOMAIN_FORMAT_NOT_APPLICABLE: &[AdmittedNotApplicable] = &[
324 AdmittedNotApplicable {
325 slot: EvidenceSlot::DialectEvidence,
326 reason: NotApplicableReason::ProfileIsNotDialect,
327 },
328 AdmittedNotApplicable {
329 slot: EvidenceSlot::Relations,
330 reason: NotApplicableReason::NoRelationSemantics,
331 },
332 AdmittedNotApplicable {
333 slot: EvidenceSlot::ResolutionOutcomes,
334 reason: NotApplicableReason::NoStaticResolution,
335 },
336];
337const FRAMEWORK_NOT_APPLICABLE: &[AdmittedNotApplicable] = &[
339 AdmittedNotApplicable {
340 slot: EvidenceSlot::DialectEvidence,
341 reason: NotApplicableReason::ProfileIsNotDialect,
342 },
343 AdmittedNotApplicable {
344 slot: EvidenceSlot::Parsing,
345 reason: NotApplicableReason::ExactHostOwnsParsing,
346 },
347 AdmittedNotApplicable {
348 slot: EvidenceSlot::MalformedPartial,
349 reason: NotApplicableReason::ExactHostOwnsParsing,
350 },
351];
352
353#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
355pub struct EvidenceReference {
356 pub registry: &'static str,
358 pub parser: &'static str,
360 pub provider: &'static str,
362 pub relation: &'static str,
364 pub publication: &'static str,
366 pub navigation: &'static str,
368 pub artifact: &'static str,
370}
371
372#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
374pub struct ReviewedNotApplicable {
375 pub reason: NotApplicableReason,
377 pub review: &'static str,
379}
380
381#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
383#[serde(rename_all = "snake_case")]
384pub enum ProfileEvidence {
385 Pending,
387 Passed(EvidenceReference),
389 NotApplicable(ReviewedNotApplicable),
391}
392
393#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
395pub struct CompleteSupportProfile {
396 pub id: SupportProfileId,
398 pub kind: SupportProfileKind,
400 pub host: Option<SupportProfileId>,
402 pub dialect_evidence: Option<DialectEvidenceKind>,
404 pub evidence: [ProfileEvidence; 13],
406 pub independent_review: &'static str,
408}
409
410pub const ACCEPTED_COMPLETE_SUPPORT_PROFILES: &[CompleteSupportProfile] = &[];
413
414#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
416#[serde(rename_all = "snake_case")]
417pub enum CatalogAssessment {
418 RuntimeCandidate {
420 registry_id: &'static str,
422 },
423 Planned,
425 Unavailable {
427 reason: CatalogUnavailableReason,
429 },
430}
431
432#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
434#[serde(rename_all = "snake_case")]
435pub enum CatalogUnavailableReason {
436 NoRuntimeCapability,
438 MissingIndependentDialectEvidence,
440 MissingFrameworkProjection,
442}
443
444impl CatalogUnavailableReason {
445 const fn as_str(self) -> &'static str {
447 match self {
448 Self::NoRuntimeCapability => "no-runtime-capability",
449 Self::MissingIndependentDialectEvidence => "missing-independent-dialect-evidence",
450 Self::MissingFrameworkProjection => "missing-framework-projection",
451 }
452 }
453}
454
455#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
457pub struct EcosystemCatalogRow {
458 pub id: SupportProfileId,
460 pub label: &'static str,
462 pub kind: SupportProfileKind,
464 pub category: PresentationCategory,
466 pub tags: &'static [PresentationTag],
468 pub host: Option<SupportProfileId>,
470 pub dialect_evidence: Option<DialectEvidenceKind>,
472 pub assessment: CatalogAssessment,
474}
475
476const LEGACY: &[PresentationTag] = &[PresentationTag::LegacyModernization];
478const NO_TAGS: &[PresentationTag] = &[];
480
481const fn catalog_row(
483 id: &'static str,
484 label: &'static str,
485 kind: SupportProfileKind,
486 category: PresentationCategory,
487 tags: &'static [PresentationTag],
488 host: Option<&'static str>,
489 dialect_evidence: Option<DialectEvidenceKind>,
490 assessment: CatalogAssessment,
491) -> EcosystemCatalogRow {
492 EcosystemCatalogRow {
493 id: SupportProfileId::new(id),
494 label,
495 kind,
496 category,
497 tags,
498 host: match host {
499 Some(value) => Some(SupportProfileId::new(value)),
500 None => None,
501 },
502 dialect_evidence,
503 assessment,
504 }
505}
506
507const fn candidate(registry_id: &'static str) -> CatalogAssessment {
509 CatalogAssessment::RuntimeCandidate { registry_id }
510}
511
512const fn unavailable(reason: CatalogUnavailableReason) -> CatalogAssessment {
514 CatalogAssessment::Unavailable { reason }
515}
516
517pub const ECOSYSTEM_CATALOG: &[EcosystemCatalogRow] = &[
519 catalog_row(
520 "abap",
521 "ABAP",
522 SupportProfileKind::Language,
523 PresentationCategory::EnterpriseLegacyModernization,
524 LEGACY,
525 None,
526 None,
527 unavailable(CatalogUnavailableReason::NoRuntimeCapability),
528 ),
529 catalog_row(
530 "abl",
531 "OpenEdge ABL",
532 SupportProfileKind::Language,
533 PresentationCategory::EnterpriseLegacyModernization,
534 LEGACY,
535 None,
536 None,
537 candidate("abl"),
538 ),
539 catalog_row(
540 "cobol",
541 "COBOL",
542 SupportProfileKind::Language,
543 PresentationCategory::EnterpriseLegacyModernization,
544 LEGACY,
545 None,
546 None,
547 candidate("cobol"),
548 ),
549 catalog_row(
550 "cobol-fixed",
551 "COBOL fixed form",
552 SupportProfileKind::Dialect,
553 PresentationCategory::EnterpriseLegacyModernization,
554 LEGACY,
555 Some("cobol"),
556 Some(DialectEvidenceKind::ContentSignature),
557 unavailable(CatalogUnavailableReason::MissingIndependentDialectEvidence),
558 ),
559 catalog_row(
560 "cobol-free",
561 "COBOL free form",
562 SupportProfileKind::Dialect,
563 PresentationCategory::EnterpriseLegacyModernization,
564 LEGACY,
565 Some("cobol"),
566 Some(DialectEvidenceKind::ContentSignature),
567 unavailable(CatalogUnavailableReason::MissingIndependentDialectEvidence),
568 ),
569 catalog_row(
570 "fortran",
571 "Fortran",
572 SupportProfileKind::Language,
573 PresentationCategory::DataScientific,
574 LEGACY,
575 None,
576 None,
577 candidate("fortran"),
578 ),
579 catalog_row(
580 "fortran-fixed",
581 "Fortran fixed form",
582 SupportProfileKind::Dialect,
583 PresentationCategory::DataScientific,
584 LEGACY,
585 Some("fortran"),
586 Some(DialectEvidenceKind::ContentSignature),
587 unavailable(CatalogUnavailableReason::MissingIndependentDialectEvidence),
588 ),
589 catalog_row(
590 "fortran-free",
591 "Fortran free form",
592 SupportProfileKind::Dialect,
593 PresentationCategory::DataScientific,
594 LEGACY,
595 Some("fortran"),
596 Some(DialectEvidenceKind::ContentSignature),
597 unavailable(CatalogUnavailableReason::MissingIndependentDialectEvidence),
598 ),
599 catalog_row(
600 "pli",
601 "PL/I",
602 SupportProfileKind::Language,
603 PresentationCategory::EnterpriseLegacyModernization,
604 LEGACY,
605 None,
606 None,
607 unavailable(CatalogUnavailableReason::NoRuntimeCapability),
608 ),
609 catalog_row(
610 "rpg",
611 "RPG",
612 SupportProfileKind::Language,
613 PresentationCategory::EnterpriseLegacyModernization,
614 LEGACY,
615 None,
616 None,
617 unavailable(CatalogUnavailableReason::NoRuntimeCapability),
618 ),
619 catalog_row(
620 "ile-rpg",
621 "ILE RPG",
622 SupportProfileKind::Dialect,
623 PresentationCategory::EnterpriseLegacyModernization,
624 LEGACY,
625 Some("rpg"),
626 Some(DialectEvidenceKind::ProjectManifest),
627 unavailable(CatalogUnavailableReason::MissingIndependentDialectEvidence),
628 ),
629 catalog_row(
630 "jcl",
631 "JCL",
632 SupportProfileKind::DomainFormat,
633 PresentationCategory::EnterpriseLegacyModernization,
634 LEGACY,
635 None,
636 None,
637 unavailable(CatalogUnavailableReason::NoRuntimeCapability),
638 ),
639 catalog_row(
640 "rexx",
641 "REXX",
642 SupportProfileKind::Language,
643 PresentationCategory::EnterpriseLegacyModernization,
644 LEGACY,
645 None,
646 None,
647 unavailable(CatalogUnavailableReason::NoRuntimeCapability),
648 ),
649 catalog_row(
650 "ibmi-cl",
651 "IBM i CL",
652 SupportProfileKind::Language,
653 PresentationCategory::EnterpriseLegacyModernization,
654 LEGACY,
655 None,
656 None,
657 unavailable(CatalogUnavailableReason::NoRuntimeCapability),
658 ),
659 catalog_row(
660 "hlasm",
661 "HLASM",
662 SupportProfileKind::Language,
663 PresentationCategory::EnterpriseLegacyModernization,
664 LEGACY,
665 None,
666 None,
667 unavailable(CatalogUnavailableReason::NoRuntimeCapability),
668 ),
669 catalog_row(
670 "assembler",
671 "Other assembler families",
672 SupportProfileKind::Language,
673 PresentationCategory::Systems,
674 LEGACY,
675 None,
676 None,
677 candidate("asm"),
678 ),
679 catalog_row(
680 "ada",
681 "Ada",
682 SupportProfileKind::Language,
683 PresentationCategory::Systems,
684 LEGACY,
685 None,
686 None,
687 candidate("ada"),
688 ),
689 catalog_row(
690 "pascal",
691 "Pascal",
692 SupportProfileKind::Language,
693 PresentationCategory::EnterpriseLegacyModernization,
694 LEGACY,
695 None,
696 None,
697 candidate("pascal"),
698 ),
699 catalog_row(
700 "object-pascal",
701 "Object Pascal",
702 SupportProfileKind::Dialect,
703 PresentationCategory::EnterpriseLegacyModernization,
704 LEGACY,
705 Some("pascal"),
706 Some(DialectEvidenceKind::ContentSignature),
707 unavailable(CatalogUnavailableReason::MissingIndependentDialectEvidence),
708 ),
709 catalog_row(
710 "delphi",
711 "Delphi",
712 SupportProfileKind::Dialect,
713 PresentationCategory::EnterpriseLegacyModernization,
714 LEGACY,
715 Some("pascal"),
716 Some(DialectEvidenceKind::ProjectManifest),
717 unavailable(CatalogUnavailableReason::MissingIndependentDialectEvidence),
718 ),
719 catalog_row(
720 "visual-basic",
721 "Visual Basic source (unqualified)",
722 SupportProfileKind::Language,
723 PresentationCategory::EnterpriseLegacyModernization,
724 LEGACY,
725 None,
726 None,
727 candidate("vb"),
728 ),
729 catalog_row(
730 "vb6",
731 "Visual Basic 6",
732 SupportProfileKind::Dialect,
733 PresentationCategory::EnterpriseLegacyModernization,
734 LEGACY,
735 Some("visual-basic"),
736 Some(DialectEvidenceKind::ProjectManifest),
737 unavailable(CatalogUnavailableReason::MissingIndependentDialectEvidence),
738 ),
739 catalog_row(
740 "vbnet",
741 "VB.NET",
742 SupportProfileKind::Dialect,
743 PresentationCategory::EnterpriseLegacyModernization,
744 LEGACY,
745 Some("visual-basic"),
746 Some(DialectEvidenceKind::ProjectManifest),
747 unavailable(CatalogUnavailableReason::MissingIndependentDialectEvidence),
748 ),
749 catalog_row(
750 "vba",
751 "VBA",
752 SupportProfileKind::Dialect,
753 PresentationCategory::EnterpriseLegacyModernization,
754 LEGACY,
755 Some("visual-basic"),
756 Some(DialectEvidenceKind::ProjectManifest),
757 unavailable(CatalogUnavailableReason::MissingIndependentDialectEvidence),
758 ),
759 catalog_row(
760 "vbscript",
761 "VBScript",
762 SupportProfileKind::Language,
763 PresentationCategory::EnterpriseLegacyModernization,
764 LEGACY,
765 None,
766 None,
767 unavailable(CatalogUnavailableReason::NoRuntimeCapability),
768 ),
769 catalog_row(
770 "classic-asp",
771 "Classic ASP",
772 SupportProfileKind::FrameworkProjection,
773 PresentationCategory::EnterpriseLegacyModernization,
774 LEGACY,
775 Some("vbscript"),
776 None,
777 unavailable(CatalogUnavailableReason::MissingFrameworkProjection),
778 ),
779 catalog_row(
780 "sas",
781 "SAS",
782 SupportProfileKind::Language,
783 PresentationCategory::DataScientific,
784 LEGACY,
785 None,
786 None,
787 unavailable(CatalogUnavailableReason::NoRuntimeCapability),
788 ),
789 catalog_row(
790 "natural",
791 "Natural",
792 SupportProfileKind::Language,
793 PresentationCategory::EnterpriseLegacyModernization,
794 LEGACY,
795 None,
796 None,
797 unavailable(CatalogUnavailableReason::NoRuntimeCapability),
798 ),
799 catalog_row(
800 "mumps",
801 "MUMPS / M",
802 SupportProfileKind::Language,
803 PresentationCategory::EnterpriseLegacyModernization,
804 LEGACY,
805 None,
806 None,
807 unavailable(CatalogUnavailableReason::NoRuntimeCapability),
808 ),
809 catalog_row(
810 "powerbuilder",
811 "PowerBuilder",
812 SupportProfileKind::FrameworkProjection,
813 PresentationCategory::EnterpriseLegacyModernization,
814 LEGACY,
815 Some("powerscript"),
816 None,
817 unavailable(CatalogUnavailableReason::MissingFrameworkProjection),
818 ),
819 catalog_row(
820 "powerscript",
821 "PowerScript",
822 SupportProfileKind::Language,
823 PresentationCategory::EnterpriseLegacyModernization,
824 LEGACY,
825 None,
826 None,
827 unavailable(CatalogUnavailableReason::NoRuntimeCapability),
828 ),
829 catalog_row(
830 "xbase",
831 "xBase",
832 SupportProfileKind::Language,
833 PresentationCategory::EnterpriseLegacyModernization,
834 LEGACY,
835 None,
836 None,
837 unavailable(CatalogUnavailableReason::NoRuntimeCapability),
838 ),
839 catalog_row(
840 "clipper",
841 "Clipper",
842 SupportProfileKind::Dialect,
843 PresentationCategory::EnterpriseLegacyModernization,
844 LEGACY,
845 Some("xbase"),
846 Some(DialectEvidenceKind::ProjectManifest),
847 unavailable(CatalogUnavailableReason::MissingIndependentDialectEvidence),
848 ),
849 catalog_row(
850 "foxpro",
851 "FoxPro",
852 SupportProfileKind::Dialect,
853 PresentationCategory::EnterpriseLegacyModernization,
854 LEGACY,
855 Some("xbase"),
856 Some(DialectEvidenceKind::ProjectManifest),
857 unavailable(CatalogUnavailableReason::MissingIndependentDialectEvidence),
858 ),
859 catalog_row(
860 "perl",
861 "Perl",
862 SupportProfileKind::Language,
863 PresentationCategory::Backend,
864 LEGACY,
865 None,
866 None,
867 candidate("perl"),
868 ),
869 catalog_row(
870 "cfml",
871 "ColdFusion / CFML",
872 SupportProfileKind::Language,
873 PresentationCategory::Backend,
874 LEGACY,
875 None,
876 None,
877 candidate("cfml"),
878 ),
879 catalog_row(
880 "actionscript",
881 "ActionScript",
882 SupportProfileKind::Language,
883 PresentationCategory::FrontendWeb,
884 LEGACY,
885 None,
886 None,
887 candidate("actionscript"),
888 ),
889 catalog_row(
890 "flex",
891 "Apache Flex",
892 SupportProfileKind::FrameworkProjection,
893 PresentationCategory::FrontendWeb,
894 LEGACY,
895 Some("actionscript"),
896 None,
897 unavailable(CatalogUnavailableReason::MissingFrameworkProjection),
898 ),
899 catalog_row(
900 "swift",
901 "Swift",
902 SupportProfileKind::Language,
903 PresentationCategory::Mobile,
904 NO_TAGS,
905 None,
906 None,
907 candidate("swift"),
908 ),
909 catalog_row(
910 "kotlin",
911 "Kotlin",
912 SupportProfileKind::Language,
913 PresentationCategory::Mobile,
914 NO_TAGS,
915 None,
916 None,
917 candidate("kotlin"),
918 ),
919 catalog_row(
920 "dart",
921 "Dart",
922 SupportProfileKind::Language,
923 PresentationCategory::Mobile,
924 NO_TAGS,
925 None,
926 None,
927 candidate("dart"),
928 ),
929 catalog_row(
930 "vhdl",
931 "VHDL",
932 SupportProfileKind::Language,
933 PresentationCategory::HardwareDesign,
934 NO_TAGS,
935 None,
936 None,
937 candidate("vhdl"),
938 ),
939 catalog_row(
940 "verilog",
941 "Verilog",
942 SupportProfileKind::Language,
943 PresentationCategory::HardwareDesign,
944 NO_TAGS,
945 None,
946 None,
947 candidate("verilog"),
948 ),
949 catalog_row(
950 "systemverilog",
951 "SystemVerilog",
952 SupportProfileKind::Language,
953 PresentationCategory::HardwareDesign,
954 NO_TAGS,
955 None,
956 None,
957 candidate("systemverilog"),
958 ),
959 catalog_row(
960 "sql",
961 "SQL (unqualified)",
962 SupportProfileKind::Language,
963 PresentationCategory::DatabaseQuery,
964 NO_TAGS,
965 None,
966 None,
967 candidate("sql"),
968 ),
969 catalog_row(
970 "oracle-plsql",
971 "Oracle PL/SQL",
972 SupportProfileKind::Dialect,
973 PresentationCategory::DatabaseQuery,
974 NO_TAGS,
975 Some("sql"),
976 Some(DialectEvidenceKind::ContentSignature),
977 unavailable(CatalogUnavailableReason::MissingIndependentDialectEvidence),
978 ),
979 catalog_row(
980 "postgres-plpgsql",
981 "PostgreSQL PL/pgSQL",
982 SupportProfileKind::Dialect,
983 PresentationCategory::DatabaseQuery,
984 NO_TAGS,
985 Some("sql"),
986 Some(DialectEvidenceKind::ContentSignature),
987 unavailable(CatalogUnavailableReason::MissingIndependentDialectEvidence),
988 ),
989 catalog_row(
990 "tsql",
991 "T-SQL",
992 SupportProfileKind::Dialect,
993 PresentationCategory::DatabaseQuery,
994 NO_TAGS,
995 Some("sql"),
996 Some(DialectEvidenceKind::ContentSignature),
997 unavailable(CatalogUnavailableReason::MissingIndependentDialectEvidence),
998 ),
999 catalog_row(
1000 "mysql",
1001 "MySQL SQL",
1002 SupportProfileKind::Dialect,
1003 PresentationCategory::DatabaseQuery,
1004 NO_TAGS,
1005 Some("sql"),
1006 Some(DialectEvidenceKind::Configuration),
1007 unavailable(CatalogUnavailableReason::MissingIndependentDialectEvidence),
1008 ),
1009 catalog_row(
1010 "mariadb",
1011 "MariaDB SQL",
1012 SupportProfileKind::Dialect,
1013 PresentationCategory::DatabaseQuery,
1014 NO_TAGS,
1015 Some("sql"),
1016 Some(DialectEvidenceKind::Configuration),
1017 unavailable(CatalogUnavailableReason::MissingIndependentDialectEvidence),
1018 ),
1019 catalog_row(
1020 "sqlite-sql",
1021 "SQLite SQL",
1022 SupportProfileKind::Dialect,
1023 PresentationCategory::DatabaseQuery,
1024 NO_TAGS,
1025 Some("sql"),
1026 Some(DialectEvidenceKind::Configuration),
1027 unavailable(CatalogUnavailableReason::MissingIndependentDialectEvidence),
1028 ),
1029 catalog_row(
1030 "bigquery-sql",
1031 "BigQuery SQL",
1032 SupportProfileKind::Dialect,
1033 PresentationCategory::DatabaseQuery,
1034 NO_TAGS,
1035 Some("sql"),
1036 Some(DialectEvidenceKind::Configuration),
1037 unavailable(CatalogUnavailableReason::MissingIndependentDialectEvidence),
1038 ),
1039 catalog_row(
1040 "snowflake-sql",
1041 "Snowflake SQL",
1042 SupportProfileKind::Dialect,
1043 PresentationCategory::DatabaseQuery,
1044 NO_TAGS,
1045 Some("sql"),
1046 Some(DialectEvidenceKind::Configuration),
1047 unavailable(CatalogUnavailableReason::MissingIndependentDialectEvidence),
1048 ),
1049 catalog_row(
1050 "redshift-sql",
1051 "Redshift SQL",
1052 SupportProfileKind::Dialect,
1053 PresentationCategory::DatabaseQuery,
1054 NO_TAGS,
1055 Some("sql"),
1056 Some(DialectEvidenceKind::Configuration),
1057 unavailable(CatalogUnavailableReason::MissingIndependentDialectEvidence),
1058 ),
1059 catalog_row(
1060 "dbt-jinja-sql",
1061 "dbt / Jinja SQL",
1062 SupportProfileKind::FrameworkProjection,
1063 PresentationCategory::DatabaseQuery,
1064 NO_TAGS,
1065 Some("sql"),
1066 None,
1067 unavailable(CatalogUnavailableReason::MissingFrameworkProjection),
1068 ),
1069 catalog_row(
1070 "terraform",
1071 "Terraform",
1072 SupportProfileKind::DomainFormat,
1073 PresentationCategory::InfrastructureCloud,
1074 NO_TAGS,
1075 None,
1076 None,
1077 candidate("terraform"),
1078 ),
1079 catalog_row(
1080 "opentofu",
1081 "OpenTofu",
1082 SupportProfileKind::Dialect,
1083 PresentationCategory::InfrastructureCloud,
1084 NO_TAGS,
1085 Some("terraform"),
1086 Some(DialectEvidenceKind::ProjectManifest),
1087 unavailable(CatalogUnavailableReason::MissingIndependentDialectEvidence),
1088 ),
1089 catalog_row(
1090 "hcl",
1091 "HCL",
1092 SupportProfileKind::DomainFormat,
1093 PresentationCategory::InfrastructureCloud,
1094 NO_TAGS,
1095 None,
1096 None,
1097 candidate("hcl"),
1098 ),
1099 catalog_row(
1100 "bicep",
1101 "Bicep",
1102 SupportProfileKind::DomainFormat,
1103 PresentationCategory::InfrastructureCloud,
1104 NO_TAGS,
1105 None,
1106 None,
1107 candidate("bicep"),
1108 ),
1109 catalog_row(
1110 "arm-template",
1111 "Azure ARM template",
1112 SupportProfileKind::DomainFormat,
1113 PresentationCategory::InfrastructureCloud,
1114 NO_TAGS,
1115 None,
1116 None,
1117 unavailable(CatalogUnavailableReason::NoRuntimeCapability),
1118 ),
1119 catalog_row(
1120 "cloudformation",
1121 "AWS CloudFormation",
1122 SupportProfileKind::DomainFormat,
1123 PresentationCategory::InfrastructureCloud,
1124 NO_TAGS,
1125 None,
1126 None,
1127 unavailable(CatalogUnavailableReason::NoRuntimeCapability),
1128 ),
1129 catalog_row(
1130 "sam",
1131 "AWS SAM",
1132 SupportProfileKind::DomainFormat,
1133 PresentationCategory::InfrastructureCloud,
1134 NO_TAGS,
1135 None,
1136 None,
1137 unavailable(CatalogUnavailableReason::NoRuntimeCapability),
1138 ),
1139 catalog_row(
1140 "pulumi-typescript",
1141 "Pulumi TypeScript constructs",
1142 SupportProfileKind::FrameworkProjection,
1143 PresentationCategory::InfrastructureCloud,
1144 NO_TAGS,
1145 Some("typescript"),
1146 None,
1147 CatalogAssessment::Planned,
1148 ),
1149 catalog_row(
1150 "pulumi-python",
1151 "Pulumi Python constructs",
1152 SupportProfileKind::FrameworkProjection,
1153 PresentationCategory::InfrastructureCloud,
1154 NO_TAGS,
1155 Some("python"),
1156 None,
1157 CatalogAssessment::Planned,
1158 ),
1159 catalog_row(
1160 "kubernetes",
1161 "Kubernetes manifests",
1162 SupportProfileKind::DomainFormat,
1163 PresentationCategory::InfrastructureCloud,
1164 NO_TAGS,
1165 None,
1166 None,
1167 unavailable(CatalogUnavailableReason::NoRuntimeCapability),
1168 ),
1169 catalog_row(
1170 "helm",
1171 "Helm charts",
1172 SupportProfileKind::FrameworkProjection,
1173 PresentationCategory::InfrastructureCloud,
1174 NO_TAGS,
1175 Some("yaml"),
1176 None,
1177 unavailable(CatalogUnavailableReason::MissingFrameworkProjection),
1178 ),
1179 catalog_row(
1180 "kustomize",
1181 "Kustomize",
1182 SupportProfileKind::FrameworkProjection,
1183 PresentationCategory::InfrastructureCloud,
1184 NO_TAGS,
1185 Some("yaml"),
1186 None,
1187 unavailable(CatalogUnavailableReason::MissingFrameworkProjection),
1188 ),
1189 catalog_row(
1190 "crossplane",
1191 "Crossplane",
1192 SupportProfileKind::FrameworkProjection,
1193 PresentationCategory::InfrastructureCloud,
1194 NO_TAGS,
1195 Some("yaml"),
1196 None,
1197 unavailable(CatalogUnavailableReason::MissingFrameworkProjection),
1198 ),
1199 catalog_row(
1200 "ansible",
1201 "Ansible",
1202 SupportProfileKind::FrameworkProjection,
1203 PresentationCategory::InfrastructureCloud,
1204 NO_TAGS,
1205 Some("yaml"),
1206 None,
1207 unavailable(CatalogUnavailableReason::MissingFrameworkProjection),
1208 ),
1209 catalog_row(
1210 "nix",
1211 "Nix",
1212 SupportProfileKind::DomainFormat,
1213 PresentationCategory::InfrastructureCloud,
1214 NO_TAGS,
1215 None,
1216 None,
1217 candidate("nix"),
1218 ),
1219 catalog_row(
1220 "cue",
1221 "CUE",
1222 SupportProfileKind::DomainFormat,
1223 PresentationCategory::InfrastructureCloud,
1224 NO_TAGS,
1225 None,
1226 None,
1227 candidate("cue"),
1228 ),
1229 catalog_row(
1230 "dockerfile",
1231 "Dockerfile",
1232 SupportProfileKind::DomainFormat,
1233 PresentationCategory::BuildConfigTemplate,
1234 NO_TAGS,
1235 None,
1236 None,
1237 candidate("dockerfile"),
1238 ),
1239 catalog_row(
1240 "compose",
1241 "Docker Compose",
1242 SupportProfileKind::FrameworkProjection,
1243 PresentationCategory::InfrastructureCloud,
1244 NO_TAGS,
1245 Some("yaml"),
1246 None,
1247 unavailable(CatalogUnavailableReason::MissingFrameworkProjection),
1248 ),
1249 catalog_row(
1250 "aws-cdk-typescript",
1251 "AWS CDK TypeScript constructs",
1252 SupportProfileKind::FrameworkProjection,
1253 PresentationCategory::InfrastructureCloud,
1254 NO_TAGS,
1255 Some("typescript"),
1256 None,
1257 CatalogAssessment::Planned,
1258 ),
1259 catalog_row(
1260 "aws-cdk-python",
1261 "AWS CDK Python constructs",
1262 SupportProfileKind::FrameworkProjection,
1263 PresentationCategory::InfrastructureCloud,
1264 NO_TAGS,
1265 Some("python"),
1266 None,
1267 CatalogAssessment::Planned,
1268 ),
1269 catalog_row(
1270 "playwright-javascript",
1271 "Playwright for JavaScript",
1272 SupportProfileKind::FrameworkProjection,
1273 PresentationCategory::TestingFrameworks,
1274 NO_TAGS,
1275 Some("javascript"),
1276 None,
1277 CatalogAssessment::Planned,
1278 ),
1279 catalog_row(
1280 "playwright-typescript",
1281 "Playwright for TypeScript",
1282 SupportProfileKind::FrameworkProjection,
1283 PresentationCategory::TestingFrameworks,
1284 NO_TAGS,
1285 Some("typescript"),
1286 None,
1287 CatalogAssessment::Planned,
1288 ),
1289 catalog_row(
1290 "jest-javascript",
1291 "Jest for JavaScript",
1292 SupportProfileKind::FrameworkProjection,
1293 PresentationCategory::TestingFrameworks,
1294 NO_TAGS,
1295 Some("javascript"),
1296 None,
1297 CatalogAssessment::Planned,
1298 ),
1299 catalog_row(
1300 "jest-typescript",
1301 "Jest for TypeScript",
1302 SupportProfileKind::FrameworkProjection,
1303 PresentationCategory::TestingFrameworks,
1304 NO_TAGS,
1305 Some("typescript"),
1306 None,
1307 CatalogAssessment::Planned,
1308 ),
1309 catalog_row(
1310 "vitest-typescript",
1311 "Vitest for TypeScript",
1312 SupportProfileKind::FrameworkProjection,
1313 PresentationCategory::TestingFrameworks,
1314 NO_TAGS,
1315 Some("typescript"),
1316 None,
1317 CatalogAssessment::Planned,
1318 ),
1319 catalog_row(
1320 "pytest",
1321 "pytest",
1322 SupportProfileKind::FrameworkProjection,
1323 PresentationCategory::TestingFrameworks,
1324 NO_TAGS,
1325 Some("python"),
1326 None,
1327 CatalogAssessment::Planned,
1328 ),
1329 catalog_row(
1330 "junit",
1331 "JUnit",
1332 SupportProfileKind::FrameworkProjection,
1333 PresentationCategory::TestingFrameworks,
1334 NO_TAGS,
1335 Some("java"),
1336 None,
1337 CatalogAssessment::Planned,
1338 ),
1339 catalog_row(
1340 "xunit-csharp",
1341 "xUnit.net",
1342 SupportProfileKind::FrameworkProjection,
1343 PresentationCategory::TestingFrameworks,
1344 NO_TAGS,
1345 Some("csharp"),
1346 None,
1347 CatalogAssessment::Planned,
1348 ),
1349];
1350
1351#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
1353pub struct CandidateSupportProfile {
1354 pub id: SupportProfileId,
1356 pub kind: SupportProfileKind,
1358 pub host: Option<SupportProfileId>,
1360 pub registry_id: &'static str,
1362 pub achieved: LanguageCapabilitySupport,
1364}
1365
1366#[must_use]
1368pub fn candidate_support_profiles() -> Vec<CandidateSupportProfile> {
1369 ECOSYSTEM_CATALOG
1370 .iter()
1371 .filter_map(|row| match row.assessment {
1372 CatalogAssessment::RuntimeCandidate { registry_id } => language_capability(registry_id)
1373 .map(|capability| CandidateSupportProfile {
1374 id: row.id,
1375 kind: row.kind,
1376 host: row.host,
1377 registry_id,
1378 achieved: capability.support,
1379 }),
1380 CatalogAssessment::Planned | CatalogAssessment::Unavailable { .. } => None,
1381 })
1382 .collect()
1383}
1384
1385#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
1387pub struct SupportCatalogIdentity {
1388 pub schema_version: u32,
1390 pub catalog_version: u32,
1392 pub digest: String,
1394}
1395
1396#[must_use]
1398pub fn support_catalog_identity() -> SupportCatalogIdentity {
1399 SupportCatalogIdentity {
1400 schema_version: COMPLETE_SUPPORT_SCHEMA_VERSION,
1401 catalog_version: SUPPORT_CATALOG_VERSION,
1402 digest: support_catalog_digest().to_owned(),
1403 }
1404}
1405
1406fn support_catalog_digest() -> &'static str {
1408 static DIGEST: OnceLock<String> = OnceLock::new();
1409 DIGEST.get_or_init(|| {
1410 let mut hasher = Hasher::new();
1411 hash_value(&mut hasher, &COMPLETE_SUPPORT_SCHEMA_VERSION.to_string());
1412 hash_value(&mut hasher, &SUPPORT_CATALOG_VERSION.to_string());
1413 hash_value(&mut hasher, &language_registry_digest());
1414 for kind in [
1415 SupportProfileKind::Language,
1416 SupportProfileKind::Dialect,
1417 SupportProfileKind::DomainFormat,
1418 SupportProfileKind::FrameworkProjection,
1419 ] {
1420 hash_value(&mut hasher, kind.as_str());
1421 let contract = kind.evidence_contract();
1422 for slot in contract.required_slots {
1423 hash_value(&mut hasher, slot.as_str());
1424 }
1425 for admitted in contract.admitted_not_applicable {
1426 hash_value(&mut hasher, admitted.slot.as_str());
1427 hash_value(&mut hasher, admitted.reason.as_str());
1428 }
1429 }
1430 for row in ECOSYSTEM_CATALOG {
1431 hash_value(&mut hasher, row.id.as_str());
1432 hash_value(&mut hasher, row.label);
1433 hash_value(&mut hasher, row.kind.as_str());
1434 hash_value(&mut hasher, row.category.as_str());
1435 hash_value(&mut hasher, row.host.map_or("", SupportProfileId::as_str));
1436 hash_value(
1437 &mut hasher,
1438 row.dialect_evidence.map_or("", DialectEvidenceKind::as_str),
1439 );
1440 for tag in row.tags {
1441 hash_value(&mut hasher, tag.as_str());
1442 }
1443 match row.assessment {
1444 CatalogAssessment::RuntimeCandidate { registry_id } => {
1445 hash_value(&mut hasher, "runtime-candidate");
1446 hash_value(&mut hasher, registry_id);
1447 }
1448 CatalogAssessment::Planned => hash_value(&mut hasher, "planned"),
1449 CatalogAssessment::Unavailable { reason } => {
1450 hash_value(&mut hasher, "unavailable");
1451 hash_value(&mut hasher, reason.as_str());
1452 }
1453 }
1454 }
1455 hasher.finalize().to_hex().to_string()
1456 })
1457}
1458
1459fn hash_value(hasher: &mut Hasher, value: &str) {
1461 hasher.update(&(value.len() as u64).to_le_bytes());
1462 hasher.update(value.as_bytes());
1463}
1464
1465#[derive(Clone, Debug, Eq, PartialEq)]
1467pub struct SupportCatalogError(String);
1468
1469impl fmt::Display for SupportCatalogError {
1470 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
1471 formatter.write_str(&self.0)
1472 }
1473}
1474
1475impl Error for SupportCatalogError {}
1476
1477pub fn validate_support_catalog() -> Result<(), SupportCatalogError> {
1485 let mut ids = BTreeSet::new();
1486 for row in ECOSYSTEM_CATALOG {
1487 if row.id.as_str().is_empty() || row.label.is_empty() {
1488 return Err(invalid("catalog identity and label must be non-empty"));
1489 }
1490 if !ids.insert(row.id) {
1491 return Err(invalid(format!(
1492 "duplicate ecosystem catalog profile {:?}",
1493 row.id.as_str()
1494 )));
1495 }
1496 validate_kind_bindings(row.kind, row.host, row.dialect_evidence, row.id)?;
1497 match row.assessment {
1498 CatalogAssessment::RuntimeCandidate { registry_id }
1499 if language_capability(registry_id).is_none() =>
1500 {
1501 return Err(invalid(format!(
1502 "candidate {:?} has no runtime registry owner {registry_id:?}",
1503 row.id.as_str()
1504 )));
1505 }
1506 CatalogAssessment::Planned | CatalogAssessment::Unavailable { .. }
1507 if language_capability(row.id.as_str()).is_some() =>
1508 {
1509 return Err(invalid(format!(
1510 "documentation-only profile {:?} unexpectedly has a runtime registry row",
1511 row.id.as_str()
1512 )));
1513 }
1514 CatalogAssessment::RuntimeCandidate { .. }
1515 | CatalogAssessment::Planned
1516 | CatalogAssessment::Unavailable { .. } => {}
1517 }
1518 }
1519 for row in ECOSYSTEM_CATALOG {
1520 if let Some(host) = row.host
1521 && !ids.contains(&host)
1522 && language_capability(host.as_str()).is_none()
1523 {
1524 return Err(invalid(format!(
1525 "profile {:?} has unknown exact host {:?}",
1526 row.id.as_str(),
1527 host.as_str()
1528 )));
1529 }
1530 }
1531 let mut complete_ids = BTreeSet::new();
1532 for profile in ACCEPTED_COMPLETE_SUPPORT_PROFILES {
1533 if !complete_ids.insert(profile.id) || !ids.contains(&profile.id) {
1534 return Err(invalid(format!(
1535 "accepted complete profile {:?} is duplicate or absent from the catalog",
1536 profile.id.as_str()
1537 )));
1538 }
1539 validate_complete_profile(profile)?;
1540 }
1541 Ok(())
1542}
1543
1544fn validate_kind_bindings(
1546 kind: SupportProfileKind,
1547 host: Option<SupportProfileId>,
1548 dialect_evidence: Option<DialectEvidenceKind>,
1549 id: SupportProfileId,
1550) -> Result<(), SupportCatalogError> {
1551 if matches!(kind, SupportProfileKind::FrameworkProjection) && host.is_none() {
1552 return Err(invalid(format!(
1553 "framework projection {:?} has no exact host profile",
1554 id.as_str()
1555 )));
1556 }
1557 if matches!(kind, SupportProfileKind::Dialect) && (host.is_none() || dialect_evidence.is_none())
1558 {
1559 return Err(invalid(format!(
1560 "dialect {:?} lacks an exact host or independent dialect evidence",
1561 id.as_str()
1562 )));
1563 }
1564 Ok(())
1565}
1566
1567fn validate_complete_profile(profile: &CompleteSupportProfile) -> Result<(), SupportCatalogError> {
1569 if profile.id.as_str().is_empty() {
1570 return Err(invalid("complete profile identity must be non-empty"));
1571 }
1572 validate_kind_bindings(
1573 profile.kind,
1574 profile.host,
1575 profile.dialect_evidence,
1576 profile.id,
1577 )?;
1578 if let Some(host) = profile.host
1579 && !ECOSYSTEM_CATALOG.iter().any(|row| row.id == host)
1580 && language_capability(host.as_str()).is_none()
1581 {
1582 return Err(invalid(format!(
1583 "complete profile {:?} has unknown exact host {:?}",
1584 profile.id.as_str(),
1585 host.as_str()
1586 )));
1587 }
1588 if profile.independent_review.is_empty() {
1589 return Err(invalid(format!(
1590 "complete profile {:?} has no independent review",
1591 profile.id.as_str()
1592 )));
1593 }
1594 let contract = profile.kind.evidence_contract();
1595 for (index, slot) in contract.required_slots.iter().enumerate() {
1596 match profile.evidence[index] {
1597 ProfileEvidence::Pending => {
1598 return Err(invalid(format!(
1599 "complete profile {:?} has pending {} evidence",
1600 profile.id.as_str(),
1601 slot.as_str()
1602 )));
1603 }
1604 ProfileEvidence::Passed(reference) => {
1605 validate_evidence_reference(profile.id, *slot, reference)?;
1606 }
1607 ProfileEvidence::NotApplicable(reviewed) => {
1608 let admitted = contract
1609 .admitted_not_applicable
1610 .iter()
1611 .any(|entry| entry.slot == *slot && entry.reason == reviewed.reason);
1612 if !admitted || reviewed.review.is_empty() {
1613 return Err(invalid(format!(
1614 "complete profile {:?} has unreviewed or unadmitted not_applicable {} evidence",
1615 profile.id.as_str(),
1616 slot.as_str()
1617 )));
1618 }
1619 }
1620 }
1621 }
1622 Ok(())
1623}
1624
1625fn validate_evidence_reference(
1627 profile: SupportProfileId,
1628 slot: EvidenceSlot,
1629 reference: EvidenceReference,
1630) -> Result<(), SupportCatalogError> {
1631 let fields = [
1632 reference.registry,
1633 reference.parser,
1634 reference.provider,
1635 reference.relation,
1636 reference.publication,
1637 reference.navigation,
1638 reference.artifact,
1639 ];
1640 if fields.iter().any(|field| field.is_empty()) {
1641 return Err(invalid(format!(
1642 "complete profile {:?} has unbound {} evidence",
1643 profile.as_str(),
1644 slot.as_str()
1645 )));
1646 }
1647 Ok(())
1648}
1649
1650fn invalid(message: impl Into<String>) -> SupportCatalogError {
1652 SupportCatalogError(message.into())
1653}
1654
1655pub fn append_support_catalog_markdown(output: &mut String) -> fmt::Result {
1661 let identity = support_catalog_identity();
1662 let candidates = candidate_support_profiles();
1663 let planned = ECOSYSTEM_CATALOG
1664 .iter()
1665 .filter(|row| matches!(row.assessment, CatalogAssessment::Planned))
1666 .count();
1667 let unavailable = ECOSYSTEM_CATALOG.len() - candidates.len() - planned;
1668 output.push_str("\n## Language & Ecosystem Support\n\n");
1669 writeln!(
1670 output,
1671 "Complete-support schema version: `{}`. Ecosystem catalog version: `{}`. Catalog digest: `{}`.",
1672 identity.schema_version, identity.catalog_version, identity.digest
1673 )?;
1674 output.push_str(
1675 "\n`Complete` means conformance to the fixed ProjectAtlas navigation contract, not compiler, build-system, runtime, or whole-language completeness. Final v0.4 MCP navigation revalidation retained every runtime candidate at its achieved detected/parsed/symbol/semantic/benchmarked tier: none has the complete schema-bound capability and agent-navigation evidence required for promotion.\n\n",
1676 );
1677 writeln!(
1678 output,
1679 "Catalog profile counts stay separate: **{}** languages, **{}** dialects, **{}** domain formats, and **{}** framework projections. Current assessment: **{}** lower-tier runtime candidates, **{}** planned documentation rows, **{}** unavailable documentation rows, and **{}** accepted complete profiles.",
1680 count_kind(SupportProfileKind::Language),
1681 count_kind(SupportProfileKind::Dialect),
1682 count_kind(SupportProfileKind::DomainFormat),
1683 count_kind(SupportProfileKind::FrameworkProjection),
1684 candidates.len(),
1685 planned,
1686 unavailable,
1687 ACCEPTED_COMPLETE_SUPPORT_PROFILES.len()
1688 )?;
1689 output.push_str(
1690 "\n### Detection-to-navigation pipeline\n\nProjectAtlas first applies deterministic registry-owned detection. Built-in or explicitly enabled contained optional parsing then produces honest parse coverage; fact providers retain their own provenance; typed resolution preserves resolved, ambiguous, unresolved, and external outcomes; one atomic SQLite generation publishes exact occurrences and relations; freshness-aware MCP navigation returns bounded source selectors and exact evidence. We reuse maintained license-compatible Tree-sitter grammars, generated parser/node metadata, and trustworthy standard queries before adding bounded ProjectAtlas queries or concrete Rust semantic logic. ProjectAtlas never executes repository code, and an absent optional pack leaves default-core startup and navigation independent.\n\n",
1691 );
1692 output.push_str(
1693 "The `legacy-modernization` tag identifies source where trustworthy dependency and exact-evidence navigation is valuable. It does **not** claim automatic conversion or select a target language. Planned and unavailable rows below are documentation classifications only: they create no runtime registry row and contribute to no capability or complete-support total.\n\n",
1694 );
1695 output.push_str("### Architecture paths\n\n");
1696 for link in ARCHITECTURE_LINKS {
1697 writeln!(output, "- [{}]({})", link.label, link.markdown_href)?;
1698 }
1699 for category in PresentationCategory::ALL {
1700 writeln!(output, "\n### {}\n", category.label())?;
1701 output.push_str("| Profile | Kind | Host | Assessment | Dialect evidence | Tags |\n");
1702 output.push_str("| --- | --- | --- | --- | --- | --- |\n");
1703 for row in ECOSYSTEM_CATALOG
1704 .iter()
1705 .filter(|row| row.category == *category)
1706 {
1707 writeln!(
1708 output,
1709 "| {} | `{}` | {} | {} | {} | {} |",
1710 row.label,
1711 row.kind.as_str(),
1712 row.host.map_or("—", SupportProfileId::as_str),
1713 assessment_label(*row),
1714 row.dialect_evidence
1715 .map_or("—", DialectEvidenceKind::as_str),
1716 tags_label(row.tags)
1717 )?;
1718 }
1719 }
1720 Ok(())
1721}
1722
1723pub fn render_language_support_html() -> Result<String, fmt::Error> {
1729 let identity = support_catalog_identity();
1730 let mut output = String::new();
1731 output.push_str("<!doctype html>\n<html lang=\"en\"><head><meta charset=\"utf-8\"><meta name=\"viewport\" content=\"width=device-width,initial-scale=1\"><title>ProjectAtlas Language & Ecosystem Support</title><style>body{font:16px/1.55 system-ui,sans-serif;color:#1f2933;background:#f7f9fb;margin:0}main{max-width:1100px;margin:auto;padding:40px 24px}section{background:#fff;border:1px solid #d8e1e8;border-radius:8px;padding:18px;margin:18px 0}table{border-collapse:collapse;width:100%}th,td{border-bottom:1px solid #d8e1e8;padding:8px;text-align:left;vertical-align:top}code{overflow-wrap:anywhere}.meta{color:#52616f}.tag{white-space:nowrap}</style></head><body><main>");
1732 output.push_str("<h1>Language & Ecosystem Support</h1>");
1733 write!(
1734 output,
1735 "<p class=\"meta\">Complete-support schema <code>{}</code>; ecosystem catalog <code>{}</code>; catalog digest <code>{}</code>.</p>",
1736 identity.schema_version, identity.catalog_version, identity.digest
1737 )?;
1738 output.push_str("<p><strong>Complete</strong> means the fixed ProjectAtlas navigation contract, not compiler, runtime, or whole-language completeness. Final v0.4 MCP navigation revalidation retained every runtime candidate at its achieved tier because none has complete schema-bound capability and agent-navigation evidence. Planned and unavailable rows are documentation only and add nothing to runtime capability totals.</p>");
1739 output.push_str("<p>Detection selects a registry row; contained parsing and concrete fact providers retain honest provenance; typed resolution and atomic SQLite publication preserve exact occurrences; freshness-aware MCP navigation returns bounded selectors and exact source evidence. Maintained compatible grammars and standard queries are reused first. ProjectAtlas does not execute repository code, and absent optional packs do not burden core.</p>");
1740 output.push_str("<p>The <code>legacy-modernization</code> tag is navigation context, not an automatic-conversion or target-language claim. See the <a href=\"https://github.com/styler-ai/ProjectAtlas/blob/main/docs/language-support.md\">generated Markdown authority</a>.</p>");
1741 output.push_str("<h2>Architecture paths</h2><ul>");
1742 for link in ARCHITECTURE_LINKS {
1743 write!(
1744 output,
1745 "<li><a href=\"{}\">{}</a></li>",
1746 link.pages_href, link.label
1747 )?;
1748 }
1749 output.push_str("</ul>");
1750 for category in PresentationCategory::ALL {
1751 write!(
1752 output,
1753 "<section><h2>{}</h2><table><thead><tr><th>Profile</th><th>Kind</th><th>Host</th><th>Assessment</th><th>Dialect evidence</th><th>Tags</th></tr></thead><tbody>",
1754 category.label()
1755 )?;
1756 for row in ECOSYSTEM_CATALOG
1757 .iter()
1758 .filter(|row| row.category == *category)
1759 {
1760 write!(
1761 output,
1762 "<tr><td>{}</td><td><code>{}</code></td><td>{}</td><td>{}</td><td>{}</td><td class=\"tag\">{}</td></tr>",
1763 row.label,
1764 row.kind.as_str(),
1765 row.host.map_or("—", SupportProfileId::as_str),
1766 assessment_label(*row),
1767 row.dialect_evidence
1768 .map_or("—", DialectEvidenceKind::as_str),
1769 tags_label(row.tags)
1770 )?;
1771 }
1772 output.push_str("</tbody></table></section>");
1773 }
1774 output.push_str("</main></body></html>\n");
1775 Ok(output)
1776}
1777
1778fn count_kind(kind: SupportProfileKind) -> usize {
1780 ECOSYSTEM_CATALOG
1781 .iter()
1782 .filter(|row| row.kind == kind)
1783 .count()
1784}
1785
1786fn assessment_label(row: EcosystemCatalogRow) -> String {
1788 match row.assessment {
1789 CatalogAssessment::RuntimeCandidate { registry_id } => language_capability(registry_id)
1790 .map_or_else(
1791 || format!("invalid runtime binding `{registry_id}`"),
1792 |capability| {
1793 let support = capability.support;
1794 format!(
1795 "candidate: detected {}, parsed {}, symbols {}, semantic {}, benchmarked {}",
1796 support.detected.as_str(),
1797 support.parsed.as_str(),
1798 support.symbols.as_str(),
1799 support.semantic.as_str(),
1800 support.benchmarked.as_str()
1801 )
1802 },
1803 ),
1804 CatalogAssessment::Planned => "planned (documentation only)".to_owned(),
1805 CatalogAssessment::Unavailable { reason } => {
1806 format!("unavailable: {}", reason.as_str())
1807 }
1808 }
1809}
1810
1811fn tags_label(tags: &[PresentationTag]) -> String {
1813 if tags.is_empty() {
1814 "—".to_owned()
1815 } else {
1816 tags.iter()
1817 .map(|tag| tag.as_str())
1818 .collect::<Vec<_>>()
1819 .join(", ")
1820 }
1821}
1822
1823struct ArchitectureLink {
1825 label: &'static str,
1827 markdown_href: &'static str,
1829 pages_href: &'static str,
1831}
1832
1833const ARCHITECTURE_LINKS: &[ArchitectureLink] = &[
1835 ArchitectureLink {
1836 label: "Canonical Mermaid architecture views",
1837 markdown_href: "projectatlas-3-architecture.md#architecture-views",
1838 pages_href: "https://github.com/styler-ai/ProjectAtlas/blob/main/docs/projectatlas-3-architecture.md#architecture-views",
1839 },
1840 ArchitectureLink {
1841 label: "System and component ownership",
1842 markdown_href: "projectatlas-3-architecture.md#system-and-component-architecture",
1843 pages_href: "https://github.com/styler-ai/ProjectAtlas/blob/main/docs/projectatlas-3-architecture.md#system-and-component-architecture",
1844 },
1845 ArchitectureLink {
1846 label: "Crate dependency and ownership",
1847 markdown_href: "projectatlas-3-architecture.md#crate-dependency-and-ownership",
1848 pages_href: "https://github.com/styler-ai/ProjectAtlas/blob/main/docs/projectatlas-3-architecture.md#crate-dependency-and-ownership",
1849 },
1850 ArchitectureLink {
1851 label: "Database authority",
1852 markdown_href: "projectatlas-3-architecture.md#database-authority-and-responsibility",
1853 pages_href: "https://github.com/styler-ai/ProjectAtlas/blob/main/docs/projectatlas-3-architecture.md#database-authority-and-responsibility",
1854 },
1855 ArchitectureLink {
1856 label: "Graph physical model",
1857 markdown_href: "projectatlas-3-architecture.md#normalized-graph-physical-model",
1858 pages_href: "https://github.com/styler-ai/ProjectAtlas/blob/main/docs/projectatlas-3-architecture.md#normalized-graph-physical-model",
1859 },
1860 ArchitectureLink {
1861 label: "Bounded graph read",
1862 markdown_href: "projectatlas-3-architecture.md#bounded-graph-read-with-purpose-projection",
1863 pages_href: "https://github.com/styler-ai/ProjectAtlas/blob/main/docs/projectatlas-3-architecture.md#bounded-graph-read-with-purpose-projection",
1864 },
1865 ArchitectureLink {
1866 label: "MCP read communication",
1867 markdown_href: "projectatlas-3-architecture.md#mcp-read-communication-sequence",
1868 pages_href: "https://github.com/styler-ai/ProjectAtlas/blob/main/docs/projectatlas-3-architecture.md#mcp-read-communication-sequence",
1869 },
1870 ArchitectureLink {
1871 label: "Transactional publication",
1872 markdown_href: "projectatlas-3-architecture.md#index-and-transactional-publication-flow",
1873 pages_href: "https://github.com/styler-ai/ProjectAtlas/blob/main/docs/projectatlas-3-architecture.md#index-and-transactional-publication-flow",
1874 },
1875 ArchitectureLink {
1876 label: "Language registry to agent navigation",
1877 markdown_href: "projectatlas-3-architecture.md#language-registry-to-agent-navigation",
1878 pages_href: "https://github.com/styler-ai/ProjectAtlas/blob/main/docs/projectatlas-3-architecture.md#language-registry-to-agent-navigation",
1879 },
1880];
1881
1882#[cfg(test)]
1883mod tests {
1884 use super::*;
1885
1886 #[test]
1887 fn incomplete_candidate_profiles_remain_lower_tier() -> Result<(), Box<dyn Error>> {
1888 validate_support_catalog()?;
1889 let candidates = candidate_support_profiles();
1890 for candidate in &candidates {
1891 let achieved = candidate.achieved;
1892 if [
1893 achieved.detected,
1894 achieved.parsed,
1895 achieved.symbols,
1896 achieved.semantic,
1897 achieved.benchmarked,
1898 ]
1899 .into_iter()
1900 .all(|level| level == crate::language::CapabilitySupportLevel::Supported)
1901 {
1902 return Err(Box::new(invalid(format!(
1903 "candidate {:?} reached every capability axis without complete evidence review",
1904 candidate.id.as_str()
1905 ))));
1906 }
1907 }
1908 require_test(
1909 ACCEPTED_COMPLETE_SUPPORT_PROFILES.is_empty(),
1910 "incomplete candidate was promoted after final navigation review",
1911 )?;
1912 require_test(
1913 !candidates.is_empty(),
1914 "runtime candidate projection is unexpectedly empty",
1915 )?;
1916 Ok(())
1917 }
1918
1919 #[test]
1920 fn required_families_are_distinct_and_frameworks_have_exact_hosts() {
1921 let ids = ECOSYSTEM_CATALOG
1922 .iter()
1923 .map(|row| row.id.as_str())
1924 .collect::<BTreeSet<_>>();
1925 for required in [
1926 "abap",
1927 "abl",
1928 "cobol",
1929 "cobol-fixed",
1930 "cobol-free",
1931 "fortran",
1932 "fortran-fixed",
1933 "fortran-free",
1934 "pli",
1935 "rpg",
1936 "ile-rpg",
1937 "jcl",
1938 "rexx",
1939 "ibmi-cl",
1940 "hlasm",
1941 "assembler",
1942 "ada",
1943 "pascal",
1944 "object-pascal",
1945 "delphi",
1946 "vb6",
1947 "vbnet",
1948 "vba",
1949 "vbscript",
1950 "classic-asp",
1951 "sas",
1952 "natural",
1953 "mumps",
1954 "powerbuilder",
1955 "powerscript",
1956 "xbase",
1957 "clipper",
1958 "foxpro",
1959 "perl",
1960 "cfml",
1961 "actionscript",
1962 "flex",
1963 "vhdl",
1964 "verilog",
1965 "systemverilog",
1966 "sql",
1967 "oracle-plsql",
1968 "postgres-plpgsql",
1969 "tsql",
1970 "mysql",
1971 "mariadb",
1972 "sqlite-sql",
1973 "bigquery-sql",
1974 "snowflake-sql",
1975 "redshift-sql",
1976 "dbt-jinja-sql",
1977 "terraform",
1978 "opentofu",
1979 "hcl",
1980 "bicep",
1981 "arm-template",
1982 "cloudformation",
1983 "sam",
1984 "kubernetes",
1985 "helm",
1986 "kustomize",
1987 "crossplane",
1988 "ansible",
1989 "nix",
1990 "cue",
1991 "dockerfile",
1992 "compose",
1993 "pulumi-typescript",
1994 "pulumi-python",
1995 "aws-cdk-typescript",
1996 "aws-cdk-python",
1997 "playwright-javascript",
1998 "playwright-typescript",
1999 "jest-javascript",
2000 "jest-typescript",
2001 "vitest-typescript",
2002 "pytest",
2003 "junit",
2004 "xunit-csharp",
2005 ] {
2006 assert!(ids.contains(required), "missing classification {required}");
2007 }
2008 assert_ne!(SupportProfileId::new("abap"), SupportProfileId::new("abl"));
2009 assert!(
2010 ECOSYSTEM_CATALOG
2011 .iter()
2012 .filter(|row| matches!(row.kind, SupportProfileKind::FrameworkProjection))
2013 .all(|row| row.host.is_some())
2014 );
2015 assert!(
2016 ECOSYSTEM_CATALOG
2017 .iter()
2018 .filter(|row| matches!(row.kind, SupportProfileKind::Dialect))
2019 .all(|row| row.host.is_some() && row.dialect_evidence.is_some())
2020 );
2021 }
2022
2023 #[test]
2024 fn planned_and_unavailable_rows_create_no_runtime_or_complete_claim() {
2025 for row in ECOSYSTEM_CATALOG {
2026 if !matches!(row.assessment, CatalogAssessment::RuntimeCandidate { .. }) {
2027 assert!(
2028 language_capability(row.id.as_str()).is_none(),
2029 "documentation-only profile {} entered the runtime registry",
2030 row.id.as_str()
2031 );
2032 assert!(
2033 !candidate_support_profiles()
2034 .iter()
2035 .any(|candidate| candidate.id == row.id)
2036 );
2037 assert!(
2038 !ACCEPTED_COMPLETE_SUPPORT_PROFILES
2039 .iter()
2040 .any(|profile| profile.id == row.id)
2041 );
2042 }
2043 }
2044 }
2045
2046 #[test]
2047 fn kind_bindings_reject_missing_framework_hosts_and_dialect_evidence() {
2048 assert!(
2049 validate_kind_bindings(
2050 SupportProfileKind::FrameworkProjection,
2051 None,
2052 None,
2053 SupportProfileId::new("framework"),
2054 )
2055 .is_err()
2056 );
2057 assert!(
2058 validate_kind_bindings(
2059 SupportProfileKind::Dialect,
2060 Some(SupportProfileId::new("language")),
2061 None,
2062 SupportProfileId::new("dialect"),
2063 )
2064 .is_err()
2065 );
2066 }
2067
2068 #[test]
2069 fn complete_profile_rejects_pending_and_unadmitted_not_applicable() {
2070 let pending = CompleteSupportProfile {
2071 id: SupportProfileId::new("test-language"),
2072 kind: SupportProfileKind::Language,
2073 host: None,
2074 dialect_evidence: None,
2075 evidence: [ProfileEvidence::Pending; 13],
2076 independent_review: "independent-review",
2077 };
2078 assert!(validate_complete_profile(&pending).is_err());
2079
2080 let reference = EvidenceReference {
2081 registry: "registry-v1",
2082 parser: "parser-v1",
2083 provider: "provider-v1",
2084 relation: "relation-v1",
2085 publication: "publication-v1",
2086 navigation: "navigation-v1",
2087 artifact: "fixture",
2088 };
2089 let mut evidence = [ProfileEvidence::Passed(reference); 13];
2090 evidence[1] = ProfileEvidence::NotApplicable(ReviewedNotApplicable {
2091 reason: NotApplicableReason::ProfileIsNotDialect,
2092 review: "independent-review",
2093 });
2094 let valid = CompleteSupportProfile {
2095 evidence,
2096 ..pending
2097 };
2098 assert!(validate_complete_profile(&valid).is_ok());
2099
2100 let mut unbound_evidence = evidence;
2101 unbound_evidence[0] = ProfileEvidence::Passed(EvidenceReference {
2102 artifact: "",
2103 ..reference
2104 });
2105 assert!(
2106 validate_complete_profile(&CompleteSupportProfile {
2107 evidence: unbound_evidence,
2108 ..valid
2109 })
2110 .is_err()
2111 );
2112
2113 let mut invalid_evidence = evidence;
2114 invalid_evidence[5] = ProfileEvidence::NotApplicable(ReviewedNotApplicable {
2115 reason: NotApplicableReason::NoRelationSemantics,
2116 review: "independent-review",
2117 });
2118 assert!(
2119 validate_complete_profile(&CompleteSupportProfile {
2120 evidence: invalid_evidence,
2121 ..valid
2122 })
2123 .is_err()
2124 );
2125 }
2126
2127 #[test]
2128 fn markdown_and_pages_share_catalog_identity_and_durable_links() -> Result<(), Box<dyn Error>> {
2129 let identity = support_catalog_identity();
2130 let markdown = crate::language::render_language_support_markdown()?;
2131 let html = render_language_support_html()?;
2132 let architecture = include_str!("../../../docs/projectatlas-3-architecture.md");
2133 for projection in [&markdown, &html] {
2134 require_test(
2135 projection.contains(&identity.digest),
2136 "catalog identity missing from a public projection",
2137 )?;
2138 require_test(
2139 projection.contains("legacy-modernization"),
2140 "modernization disclaimer tag missing from a public projection",
2141 )?;
2142 }
2143 for link in ARCHITECTURE_LINKS {
2144 require_test(
2145 markdown.contains(&format!("[{}]({})", link.label, link.markdown_href)),
2146 "architecture link missing from Markdown projection",
2147 )?;
2148 require_test(
2149 html.contains(&format!("href=\"{}\">{}", link.pages_href, link.label)),
2150 "architecture link missing from Pages projection",
2151 )?;
2152 let anchor = link
2153 .markdown_href
2154 .strip_prefix("projectatlas-3-architecture.md#")
2155 .ok_or_else(|| std::io::Error::other("invalid architecture Markdown link"))?;
2156 require_test(
2157 architecture.lines().any(|line| {
2158 line.strip_prefix("## ")
2159 .or_else(|| line.strip_prefix("### "))
2160 .is_some_and(|heading| markdown_anchor(heading) == anchor)
2161 }),
2162 "architecture link does not own a matching Markdown heading",
2163 )?;
2164 }
2165 require_test(
2166 html.contains("Language & Ecosystem Support"),
2167 "Pages projection title is missing",
2168 )?;
2169 require_test(
2170 markdown.contains("## Hardware design"),
2171 "hardware-design category is missing",
2172 )?;
2173 Ok(())
2174 }
2175
2176 #[test]
2177 fn repository_and_pages_landing_links_are_declared() {
2178 let readme = include_str!("../../../README.md");
2179 let workflow = include_str!("../../../.github/workflows/04-docs.yml");
2180 assert!(readme.contains("https://styler-ai.github.io/ProjectAtlas/language-support/"));
2181 assert!(workflow.contains("href=\"language-support/\""));
2182 assert!(workflow.contains("--html target/doc/language-support/index.html"));
2183 }
2184
2185 fn require_test(condition: bool, message: &'static str) -> Result<(), Box<dyn Error>> {
2187 if condition {
2188 Ok(())
2189 } else {
2190 Err(std::io::Error::other(message).into())
2191 }
2192 }
2193
2194 fn markdown_anchor(heading: &str) -> String {
2195 heading
2196 .chars()
2197 .filter_map(|character| {
2198 character
2199 .is_ascii_alphanumeric()
2200 .then(|| character.to_ascii_lowercase())
2201 .or_else(|| character.is_ascii_whitespace().then_some('-'))
2202 })
2203 .collect()
2204 }
2205}