projectatlas_core/
support_catalog.rs

1//! Own the fixed complete-support schema and public ecosystem catalog projections.
2
3use 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
12/// Version of the fixed complete-support evidence schema.
13pub const COMPLETE_SUPPORT_SCHEMA_VERSION: u32 = 1;
14
15/// Version of the public language and ecosystem catalog.
16pub const SUPPORT_CATALOG_VERSION: u32 = 1;
17
18/// Stable identity of one support profile.
19#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd, Serialize)]
20pub struct SupportProfileId(&'static str);
21
22impl SupportProfileId {
23    /// Construct a static support-profile identity.
24    #[must_use]
25    const fn new(value: &'static str) -> Self {
26        Self(value)
27    }
28
29    /// Return the stable profile identity.
30    #[must_use]
31    pub const fn as_str(self) -> &'static str {
32        self.0
33    }
34}
35
36/// Closed semantic kind of one support profile.
37#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
38#[serde(rename_all = "snake_case")]
39pub enum SupportProfileKind {
40    /// A language with its own detection and navigation contract.
41    Language,
42    /// A language dialect requiring evidence beyond a shared extension.
43    Dialect,
44    /// A non-language source or configuration format.
45    DomainFormat,
46    /// A framework projection bound to one exact host profile.
47    FrameworkProjection,
48}
49
50impl SupportProfileKind {
51    /// Return the stable public label.
52    #[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    /// Return this kind's fixed evidence contract.
63    #[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/// Stable user-facing catalog grouping, separate from semantic profile kind.
79#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
80#[serde(rename_all = "kebab-case")]
81pub enum PresentationCategory {
82    /// Server and service implementation languages.
83    Backend,
84    /// Browser, component, and web source.
85    FrontendWeb,
86    /// Systems implementation languages.
87    Systems,
88    /// Mobile application source.
89    Mobile,
90    /// Data and scientific source.
91    DataScientific,
92    /// Enterprise and legacy-modernization source.
93    EnterpriseLegacyModernization,
94    /// Database and query source.
95    DatabaseQuery,
96    /// Infrastructure and cloud configuration.
97    InfrastructureCloud,
98    /// Build, configuration, and template source.
99    BuildConfigTemplate,
100    /// Test framework projections.
101    TestingFrameworks,
102    /// Hardware design languages, separate from software systems source.
103    HardwareDesign,
104}
105
106impl PresentationCategory {
107    /// Every category in public rendering order.
108    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    /// Return the public heading.
123    #[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    /// Return the stable digest label.
141    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/// Optional presentation tags that never change semantic capability counts.
159#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
160#[serde(rename_all = "kebab-case")]
161pub enum PresentationTag {
162    /// Source where exact evidence is useful for modernization assessment.
163    LegacyModernization,
164}
165
166impl PresentationTag {
167    /// Return the stable public tag.
168    const fn as_str(self) -> &'static str {
169        match self {
170            Self::LegacyModernization => "legacy-modernization",
171        }
172    }
173}
174
175/// Independent evidence used to distinguish a dialect from shared syntax.
176#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
177#[serde(rename_all = "snake_case")]
178pub enum DialectEvidenceKind {
179    /// A project or workspace manifest selects the dialect.
180    ProjectManifest,
181    /// A repository-relative path convention selects the dialect.
182    PathConvention,
183    /// A bounded configuration value selects the dialect.
184    Configuration,
185    /// A bounded content signature selects the dialect.
186    ContentSignature,
187}
188
189impl DialectEvidenceKind {
190    /// Return the stable evidence label.
191    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/// Fixed evidence slots owned by the complete-support schema.
202#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
203#[serde(rename_all = "snake_case")]
204pub enum EvidenceSlot {
205    /// Deterministic detection evidence.
206    Detection,
207    /// Independent dialect-disambiguation evidence.
208    DialectEvidence,
209    /// Grammar or format parsing evidence.
210    Parsing,
211    /// Malformed or partial-input behavior.
212    MalformedPartial,
213    /// Symbols or domain facts.
214    Facts,
215    /// At least one applicable non-empty accepted relation family.
216    Relations,
217    /// Exact source occurrences.
218    Occurrences,
219    /// Resolved, ambiguous, unresolved, and external outcomes.
220    ResolutionOutcomes,
221    /// Real `SQLite` publish, reopen, and incremental convergence.
222    Publication,
223    /// Owning unit fixtures.
224    UnitFixtures,
225    /// Owning integration fixtures.
226    IntegrationFixtures,
227    /// Representative-repository measurement.
228    RepresentativeRepository,
229    /// Bounded agent-navigation evaluation.
230    AgentNavigation,
231}
232
233impl EvidenceSlot {
234    /// Every fixed evidence slot in machine-stable order.
235    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    /// Return the stable evidence-slot label.
252    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/// Schema-admitted reason for a reviewed `not_applicable` slot.
272#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
273#[serde(rename_all = "snake_case")]
274pub enum NotApplicableReason {
275    /// The profile is not a dialect.
276    ProfileIsNotDialect,
277    /// The exact host profile owns syntax parsing for this projection.
278    ExactHostOwnsParsing,
279    /// This format has no meaningful navigable relation semantics.
280    NoRelationSemantics,
281    /// This format has no static resolution outcomes.
282    NoStaticResolution,
283}
284
285impl NotApplicableReason {
286    /// Return the stable reviewed-reason label.
287    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/// One schema-admitted evidence-slot exception.
298#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
299pub struct AdmittedNotApplicable {
300    /// Evidence slot that may not apply.
301    pub slot: EvidenceSlot,
302    /// Exact admitted reason.
303    pub reason: NotApplicableReason,
304}
305
306/// Kind-owned fixed evidence contract.
307#[derive(Clone, Copy, Debug, Eq, PartialEq)]
308pub struct ProfileEvidenceContract {
309    /// Mandatory slots in machine-stable order.
310    pub required_slots: &'static [EvidenceSlot; 13],
311    /// Narrow typed exceptions accepted for this kind.
312    pub admitted_not_applicable: &'static [AdmittedNotApplicable],
313}
314
315/// Typed exceptions admitted for language profiles.
316const LANGUAGE_NOT_APPLICABLE: &[AdmittedNotApplicable] = &[AdmittedNotApplicable {
317    slot: EvidenceSlot::DialectEvidence,
318    reason: NotApplicableReason::ProfileIsNotDialect,
319}];
320/// Dialect profiles admit no evidence omissions.
321const DIALECT_NOT_APPLICABLE: &[AdmittedNotApplicable] = &[];
322/// Typed exceptions admitted for domain-format profiles.
323const 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];
337/// Typed exceptions admitted for exact-host framework projections.
338const 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/// Machine-checkable evidence bound to every capability owner relevant to navigation.
354#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
355pub struct EvidenceReference {
356    /// Versioned language registry identity.
357    pub registry: &'static str,
358    /// Parser or format-parser identity.
359    pub parser: &'static str,
360    /// Fact or semantic-provider identity.
361    pub provider: &'static str,
362    /// Accepted relation-family identity.
363    pub relation: &'static str,
364    /// Publication contract identity.
365    pub publication: &'static str,
366    /// Final navigation contract identity.
367    pub navigation: &'static str,
368    /// Test, fixture, workflow, or measurement locator.
369    pub artifact: &'static str,
370}
371
372/// Independently reviewed typed `not_applicable` evidence.
373#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
374pub struct ReviewedNotApplicable {
375    /// Schema-admitted reason.
376    pub reason: NotApplicableReason,
377    /// Stable independent review locator.
378    pub review: &'static str,
379}
380
381/// State of one mandatory complete-support evidence slot.
382#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
383#[serde(rename_all = "snake_case")]
384pub enum ProfileEvidence {
385    /// Evidence has not passed; valid only for candidates.
386    Pending,
387    /// Evidence passed and is bound to every applicable owner identity.
388    Passed(EvidenceReference),
389    /// Evidence does not apply for a schema-admitted independently reviewed reason.
390    NotApplicable(ReviewedNotApplicable),
391}
392
393/// One accepted complete-support profile.
394#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
395pub struct CompleteSupportProfile {
396    /// Stable profile identity.
397    pub id: SupportProfileId,
398    /// Closed semantic profile kind.
399    pub kind: SupportProfileKind,
400    /// Exact host profile, required for framework projections.
401    pub host: Option<SupportProfileId>,
402    /// Independent dialect evidence, required for dialect profiles.
403    pub dialect_evidence: Option<DialectEvidenceKind>,
404    /// Evidence values aligned exactly with [`EvidenceSlot::ALL`].
405    pub evidence: [ProfileEvidence; 13],
406    /// Stable independent review locator confirming the complete claim.
407    pub independent_review: &'static str,
408}
409
410/// Accepted complete-support inventory. It stays empty until task 7.3 revalidates
411/// candidates against the final MCP navigation surface and independent review.
412pub const ACCEPTED_COMPLETE_SUPPORT_PROFILES: &[CompleteSupportProfile] = &[];
413
414/// Documentation assessment kept separate from runtime capability truth.
415#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
416#[serde(rename_all = "snake_case")]
417pub enum CatalogAssessment {
418    /// A lower-tier runtime row exists and remains a candidate, never complete.
419    RuntimeCandidate {
420        /// Exact language registry identity.
421        registry_id: &'static str,
422    },
423    /// A future profile is classified but has no runtime capability claim.
424    Planned,
425    /// The profile is classified but unavailable at the fixed support contract.
426    Unavailable {
427        /// Typed reason that prevents a runtime/complete claim.
428        reason: CatalogUnavailableReason,
429    },
430}
431
432/// Why a documentation-only ecosystem row is unavailable.
433#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
434#[serde(rename_all = "snake_case")]
435pub enum CatalogUnavailableReason {
436    /// No runtime capability row owns the profile.
437    NoRuntimeCapability,
438    /// Independent dialect evidence has not been admitted.
439    MissingIndependentDialectEvidence,
440    /// No exact framework projection has been admitted.
441    MissingFrameworkProjection,
442}
443
444impl CatalogUnavailableReason {
445    /// Return the stable unavailable-reason label.
446    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/// One documentation and candidate-profile catalog row.
456#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
457pub struct EcosystemCatalogRow {
458    /// Stable catalog profile identity.
459    pub id: SupportProfileId,
460    /// Human-facing family name.
461    pub label: &'static str,
462    /// Closed semantic profile kind.
463    pub kind: SupportProfileKind,
464    /// Presentation-only category.
465    pub category: PresentationCategory,
466    /// Presentation-only tags.
467    pub tags: &'static [PresentationTag],
468    /// Exact host profile for dialects and framework projections.
469    pub host: Option<SupportProfileId>,
470    /// Required independent dialect-evidence class.
471    pub dialect_evidence: Option<DialectEvidenceKind>,
472    /// Runtime candidate or documentation-only state.
473    pub assessment: CatalogAssessment,
474}
475
476/// Shared legacy-modernization presentation tag.
477const LEGACY: &[PresentationTag] = &[PresentationTag::LegacyModernization];
478/// Empty presentation-tag set.
479const NO_TAGS: &[PresentationTag] = &[];
480
481/// Construct one static catalog row without a builder abstraction.
482const 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
507/// Construct a lower-tier runtime candidate assessment.
508const fn candidate(registry_id: &'static str) -> CatalogAssessment {
509    CatalogAssessment::RuntimeCandidate { registry_id }
510}
511
512/// Construct a documentation-only unavailable assessment.
513const fn unavailable(reason: CatalogUnavailableReason) -> CatalogAssessment {
514    CatalogAssessment::Unavailable { reason }
515}
516
517/// Versioned documentation and candidate-profile catalog.
518pub 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/// One lower-tier candidate derived from a live registry row.
1352#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
1353pub struct CandidateSupportProfile {
1354    /// Catalog profile identity.
1355    pub id: SupportProfileId,
1356    /// Closed profile kind.
1357    pub kind: SupportProfileKind,
1358    /// Exact host profile when applicable.
1359    pub host: Option<SupportProfileId>,
1360    /// Exact runtime registry identity.
1361    pub registry_id: &'static str,
1362    /// Runtime-achieved capability axes; never a complete-support claim.
1363    pub achieved: LanguageCapabilitySupport,
1364}
1365
1366/// Derive lower-tier candidates from catalog rows with exact live registry owners.
1367#[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/// Shared identity emitted by the Markdown and Pages projections.
1386#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
1387pub struct SupportCatalogIdentity {
1388    /// Fixed complete-support schema version.
1389    pub schema_version: u32,
1390    /// Documentation catalog version.
1391    pub catalog_version: u32,
1392    /// Digest binding the catalog rows and live registry identity.
1393    pub digest: String,
1394}
1395
1396/// Return the catalog identity shared by every public projection.
1397#[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
1406/// Return the cached deterministic catalog digest.
1407fn 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
1459/// Hash one length-delimited catalog value.
1460fn 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/// Catalog validation error.
1466#[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
1477/// Validate the fixed schema, accepted inventory, and documentation catalog.
1478///
1479/// # Errors
1480///
1481/// Returns a deterministic error for duplicate/empty identities, invalid host
1482/// or dialect bindings, missing runtime candidates, or incomplete accepted
1483/// complete-support evidence.
1484pub 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
1544/// Validate host and dialect evidence required by a profile kind.
1545fn 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
1567/// Validate one accepted complete profile against its fixed evidence contract.
1568fn 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
1625/// Validate that evidence is bound to every applicable owner identity.
1626fn 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
1650/// Construct one deterministic catalog validation error.
1651fn invalid(message: impl Into<String>) -> SupportCatalogError {
1652    SupportCatalogError(message.into())
1653}
1654
1655/// Append the grouped catalog projection to the generated Markdown authority.
1656///
1657/// # Errors
1658///
1659/// Returns a formatting error if writing to the owned `String` fails.
1660pub 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
1723/// Render the grouped GitHub Pages projection from the same catalog identity.
1724///
1725/// # Errors
1726///
1727/// Returns a formatting error if writing to the owned `String` fails.
1728pub 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 &amp; 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 &amp; 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
1778/// Count catalog rows for one semantic profile kind.
1779fn count_kind(kind: SupportProfileKind) -> usize {
1780    ECOSYSTEM_CATALOG
1781        .iter()
1782        .filter(|row| row.kind == kind)
1783        .count()
1784}
1785
1786/// Render one assessment without promoting documentation-only rows.
1787fn 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
1811/// Render presentation-only tags.
1812fn 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
1823/// One durable architecture route rendered in both projections.
1824struct ArchitectureLink {
1825    /// Human-facing link label.
1826    label: &'static str,
1827    /// Repository-relative Markdown target.
1828    markdown_href: &'static str,
1829    /// Absolute GitHub target used by Pages.
1830    pages_href: &'static str,
1831}
1832
1833/// Architecture links required by the catalog contract.
1834const 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 &amp; 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    /// Return a test error without panicking inside a result-returning test.
2186    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}