projectatlas_core/
language.rs

1//! Purpose: Own deterministic language capability and detection truth.
2
3use blake3::Hasher;
4use serde::Serialize;
5use std::collections::{BTreeMap, BTreeSet};
6use std::error::Error;
7use std::fmt;
8use std::fmt::Write as _;
9use std::path::Path;
10use std::sync::OnceLock;
11
12/// Version of the generated language registry contract.
13pub const LANGUAGE_CAPABILITY_REGISTRY_VERSION: u32 = 3;
14
15/// Version of the direct and embedded semantic-provider projection.
16pub const SEMANTIC_PROVIDER_CONTRACT_VERSION: u32 = 1;
17
18/// Version of the accepted language capability floor.
19pub const ACCEPTED_LANGUAGE_CAPABILITY_SET_VERSION: u32 = 5;
20
21/// Version of exact detector precedence and content-matching semantics.
22pub const LANGUAGE_DETECTION_POLICY_VERSION: u32 = 1;
23
24/// Historical acceptance seal for capability-set version 1.
25///
26/// Any membership, detector, owner, minimum, fixture, provenance, or platform
27/// change must bump the accepted-set version instead of rewriting this seal.
28pub const ACCEPTED_LANGUAGE_CAPABILITY_SET_V1_DIGEST: &str =
29    "58f2e1e6755464d573df998c1c1cecb2d076c829c0dbe04a463ce14a5a239861";
30
31/// Historical acceptance seal for capability-set version 2.
32///
33/// Version 2 preserves version 1 membership and capability strength while
34/// binding ProjectAtlas-owned parser provenance to the 0.4.0 runtime.
35pub const ACCEPTED_LANGUAGE_CAPABILITY_SET_V2_DIGEST: &str =
36    "8397f73a7593b849d0e83b3892e4721874ac4a4fd93f62b42dac9ffe166a2a7c";
37
38/// Historical acceptance seal for capability-set version 3.
39///
40/// Version 3 adds the independently owned Rust, ECMAScript, Python, and Cargo
41/// semantic provider floor plus typed HTML-like/component/template embedding.
42pub const ACCEPTED_LANGUAGE_CAPABILITY_SET_V3_DIGEST: &str =
43    "a4b69ce4aed2ebf8d28f7b237ead76a53e5363e34c8c97ea5980776ea4217ef4";
44
45/// Historical acceptance seal for capability-set version 4.
46///
47/// Version 4 preserves version 3 membership and capability strength while
48/// binding ProjectAtlas-owned parser provenance to the 0.4.1 runtime.
49pub const ACCEPTED_LANGUAGE_CAPABILITY_SET_V4_DIGEST: &str =
50    "e9a952d0b3bc2d2c5db832130d85b7cdfd656aaa07ebbafab1505da6b87d9084";
51
52/// Historical acceptance seal for capability-set version 5.
53///
54/// Version 5 preserves version 4 membership and capability strength while
55/// binding ProjectAtlas-owned parser provenance to the 0.4.2 runtime.
56pub const ACCEPTED_LANGUAGE_CAPABILITY_SET_V5_DIGEST: &str =
57    "07a3d2c45a4736bc764e44016a6ba9b7f9ea1b769b0100604702160528679bc7";
58
59/// Maximum content prefix inspected by the bounded content/dialect detector.
60pub const LANGUAGE_CONTENT_DETECTION_MAX_BYTES: usize = 512;
61
62/// Pinned metadata catalog used to admit broad detection and fallback rows.
63pub const OPTIONAL_GRAMMAR_CATALOG: &str = "tree-sitter-language-pack";
64
65/// Exact catalog version used by the accepted broad-language registry.
66pub const OPTIONAL_GRAMMAR_CATALOG_VERSION: &str = "1.13.2";
67
68/// Exact upstream release-tag revision used by the accepted broad-language registry.
69pub const OPTIONAL_GRAMMAR_CATALOG_RELEASE_REVISION: &str =
70    "6258abac30304283763a0d2dc8a48cb87fbcf438";
71
72/// Minimum additional non-built-in grammars required for the broad-pack claim.
73pub const OPTIONAL_PACK_MINIMUM_ADDITIONAL_GRAMMARS: usize = 150;
74
75/// Stable owner identity for the one logical optional broad parser pack.
76pub const BROAD_PARSER_PACK_ID: &str = "broad-parser";
77
78/// Maximum JSON bytes for the compact language settings projection.
79pub const LANGUAGE_REGISTRY_REPORT_MAX_BYTES: usize = 32_000;
80
81/// Parser coverage level retained for compatibility with the 0.3.26 API.
82#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
83#[serde(rename_all = "kebab-case")]
84pub enum LanguageParserSupport {
85    /// A native Tree-sitter adapter backs symbol extraction.
86    Native,
87    /// A manifest-specific parser backs package/dependency extraction.
88    Manifest,
89    /// A deterministic structural summarizer backs agent-facing summaries.
90    Structural,
91    /// A conservative fallback parser is the current coverage boundary.
92    Fallback,
93}
94
95/// Independently achieved support for one language capability axis.
96#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd, Serialize)]
97#[serde(rename_all = "kebab-case")]
98pub enum CapabilitySupportLevel {
99    /// The capability has not been accepted or proved.
100    Unavailable,
101    /// Conservative heuristic behavior exists and is reported as fallback.
102    Fallback,
103    /// The capability is supported by its declared owner and fixtures.
104    Supported,
105}
106
107impl CapabilitySupportLevel {
108    /// Return the stable settings and documentation label.
109    #[must_use]
110    pub const fn as_str(self) -> &'static str {
111        match self {
112            Self::Unavailable => "unavailable",
113            Self::Fallback => "fallback",
114            Self::Supported => "supported",
115        }
116    }
117}
118
119/// Independent language capability axes.
120#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
121pub struct LanguageCapabilitySupport {
122    /// The language can be selected deterministically.
123    pub detected: CapabilitySupportLevel,
124    /// Source can be parsed at the declared quality level.
125    pub parsed: CapabilitySupportLevel,
126    /// Definitions and relationships can be extracted at the declared quality level.
127    pub symbols: CapabilitySupportLevel,
128    /// Project-wide semantic resolution has been independently validated.
129    pub semantic: CapabilitySupportLevel,
130    /// Representative benchmark coverage exists.
131    pub benchmarked: CapabilitySupportLevel,
132}
133
134impl LanguageCapabilitySupport {
135    /// Build one independent capability profile.
136    #[must_use]
137    pub const fn new(
138        detected: CapabilitySupportLevel,
139        parsed: CapabilitySupportLevel,
140        symbols: CapabilitySupportLevel,
141        semantic: CapabilitySupportLevel,
142        benchmarked: CapabilitySupportLevel,
143    ) -> Self {
144        Self {
145            detected,
146            parsed,
147            symbols,
148            semantic,
149            benchmarked,
150        }
151    }
152
153    /// Return whether every achieved axis meets the accepted minimum.
154    #[must_use]
155    pub const fn meets(self, minimum: Self) -> bool {
156        self.detected as u8 >= minimum.detected as u8
157            && self.parsed as u8 >= minimum.parsed as u8
158            && self.symbols as u8 >= minimum.symbols as u8
159            && self.semantic as u8 >= minimum.semantic as u8
160            && self.benchmarked as u8 >= minimum.benchmarked as u8
161    }
162}
163
164/// Grammar-backed or purpose-built parsing and symbol support.
165const SUPPORTED_NATIVE: LanguageCapabilitySupport = LanguageCapabilitySupport::new(
166    CapabilitySupportLevel::Supported,
167    CapabilitySupportLevel::Supported,
168    CapabilitySupportLevel::Supported,
169    CapabilitySupportLevel::Unavailable,
170    CapabilitySupportLevel::Unavailable,
171);
172
173/// Grammar-backed or purpose-built support with project-wide semantic resolution.
174const SUPPORTED_SEMANTIC: LanguageCapabilitySupport = LanguageCapabilitySupport::new(
175    CapabilitySupportLevel::Supported,
176    CapabilitySupportLevel::Supported,
177    CapabilitySupportLevel::Supported,
178    CapabilitySupportLevel::Supported,
179    CapabilitySupportLevel::Unavailable,
180);
181
182/// Deterministic structural parsing without a symbol extractor.
183const SUPPORTED_STRUCTURAL: LanguageCapabilitySupport = LanguageCapabilitySupport::new(
184    CapabilitySupportLevel::Supported,
185    CapabilitySupportLevel::Supported,
186    CapabilitySupportLevel::Unavailable,
187    CapabilitySupportLevel::Unavailable,
188    CapabilitySupportLevel::Unavailable,
189);
190
191/// Deterministic detection with conservative parsing and symbol fallback.
192const SUPPORTED_FALLBACK: LanguageCapabilitySupport = LanguageCapabilitySupport::new(
193    CapabilitySupportLevel::Supported,
194    CapabilitySupportLevel::Fallback,
195    CapabilitySupportLevel::Fallback,
196    CapabilitySupportLevel::Unavailable,
197    CapabilitySupportLevel::Unavailable,
198);
199
200/// Closed built-in Tree-sitter grammar identity.
201#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
202#[serde(rename_all = "kebab-case")]
203pub enum TreeSitterGrammar {
204    /// Rust grammar.
205    Rust,
206    /// Python grammar.
207    Python,
208    /// JavaScript grammar.
209    JavaScript,
210    /// TypeScript grammar.
211    TypeScript,
212    /// TSX grammar.
213    Tsx,
214    /// Java grammar.
215    Java,
216    /// Kotlin grammar.
217    Kotlin,
218    /// C# grammar.
219    CSharp,
220    /// Go grammar.
221    Go,
222    /// Objective-C grammar.
223    ObjectiveC,
224    /// Zig grammar.
225    Zig,
226    /// C grammar.
227    C,
228    /// C++ grammar.
229    Cpp,
230}
231
232impl TreeSitterGrammar {
233    /// Every closed built-in grammar identity.
234    pub const ALL: &'static [Self] = &[
235        Self::Rust,
236        Self::Python,
237        Self::JavaScript,
238        Self::TypeScript,
239        Self::Tsx,
240        Self::Java,
241        Self::Kotlin,
242        Self::CSharp,
243        Self::Go,
244        Self::ObjectiveC,
245        Self::Zig,
246        Self::C,
247        Self::Cpp,
248    ];
249
250    /// Return the exact Cargo package that provides this built-in grammar.
251    #[must_use]
252    pub const fn package(self) -> &'static str {
253        match self {
254            Self::Rust => "tree-sitter-rust",
255            Self::Python => "tree-sitter-python",
256            Self::JavaScript => "tree-sitter-javascript",
257            Self::TypeScript | Self::Tsx => "tree-sitter-typescript",
258            Self::Java => "tree-sitter-java",
259            Self::Kotlin => "tree-sitter-kotlin-ng",
260            Self::CSharp => "tree-sitter-c-sharp",
261            Self::Go => "tree-sitter-go",
262            Self::ObjectiveC => "tree-sitter-objc",
263            Self::Zig => "tree-sitter-zig",
264            Self::C => "tree-sitter-c",
265            Self::Cpp => "tree-sitter-cpp",
266        }
267    }
268
269    /// Return the exact workspace-pinned Cargo version of this grammar.
270    #[must_use]
271    pub const fn version(self) -> &'static str {
272        match self {
273            Self::Rust | Self::C => "0.24.2",
274            Self::Python | Self::JavaScript | Self::Go => "0.25.0",
275            Self::TypeScript | Self::Tsx => "0.23.2",
276            Self::Java | Self::CSharp => "0.23.5",
277            Self::Kotlin => "1.1.0",
278            Self::ObjectiveC => "3.0.2",
279            Self::Zig => "1.1.2",
280            Self::Cpp => "0.23.4",
281        }
282    }
283}
284
285/// Closed owner of built-in symbol extraction.
286#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
287#[serde(rename_all = "kebab-case")]
288pub enum SymbolParserOwner {
289    /// A declared Tree-sitter grammar.
290    TreeSitter(TreeSitterGrammar),
291    /// `ProjectAtlas` Cargo manifest/lock extraction.
292    CargoManifest,
293    /// `ProjectAtlas` Vue component extraction.
294    Vue,
295    /// `ProjectAtlas` `PowerShell` extraction.
296    PowerShell,
297    /// Conservative `ProjectAtlas` fallback extraction.
298    Fallback,
299    /// No definition or relationship extractor is available.
300    Unavailable,
301}
302
303/// Closed owner of project-wide semantic resolution.
304#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
305#[serde(rename_all = "kebab-case")]
306pub enum SemanticProviderOwner {
307    /// Rust modules, imports, exports, and references.
308    Rust,
309    /// JavaScript, TypeScript, and TSX modules, imports, exports, and references.
310    EcmaScript,
311    /// Python modules, imports, exports, and references.
312    Python,
313    /// Cargo package and dependency identities.
314    Cargo,
315    /// No independently validated project-wide semantic provider is available.
316    Unavailable,
317}
318
319impl SemanticProviderOwner {
320    /// Return the stable settings and documentation label.
321    #[must_use]
322    pub const fn as_str(self) -> &'static str {
323        match self {
324            Self::Rust => "rust",
325            Self::EcmaScript => "ecma-script",
326            Self::Python => "python",
327            Self::Cargo => "cargo",
328            Self::Unavailable => "unavailable",
329        }
330    }
331
332    /// Return the canonical cross-language resolution family owned by this provider.
333    ///
334    /// The provider label identifies the implementation owner while this family
335    /// keeps compatible source dialects, such as JavaScript, TypeScript, and
336    /// TSX, in one resolution namespace.
337    #[must_use]
338    pub const fn resolution_family(self) -> Option<&'static str> {
339        match self {
340            Self::Rust => Some("rust"),
341            Self::EcmaScript => Some("ecmascript"),
342            Self::Python => Some("python"),
343            Self::Cargo => Some("cargo"),
344            Self::Unavailable => None,
345        }
346    }
347}
348
349/// Closed host class for bounded embedded-source projection.
350#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
351#[serde(rename_all = "kebab-case")]
352pub enum EmbeddedHostKind {
353    /// HTML-like host source.
354    HtmlLike,
355    /// Component host source.
356    Component,
357    /// Template host source.
358    Template,
359}
360
361impl EmbeddedHostKind {
362    /// Return the stable settings and documentation label.
363    #[must_use]
364    pub const fn as_str(self) -> &'static str {
365        match self {
366            Self::HtmlLike => "html-like",
367            Self::Component => "component",
368            Self::Template => "template",
369        }
370    }
371}
372
373/// One accepted host-to-embedded semantic provider pairing.
374#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
375pub struct EmbeddedLanguageCapability {
376    /// Host extraction class used for bounded source reconciliation.
377    pub host_kind: EmbeddedHostKind,
378    /// Provider that interprets accepted embedded source regions.
379    pub semantic_provider: SemanticProviderOwner,
380}
381
382/// Closed owner of deterministic structural summaries.
383#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
384#[serde(rename_all = "kebab-case")]
385pub enum StructuralSummaryOwner {
386    /// Markdown/CommonMark summaries.
387    Markdown,
388    /// JSON and JSONC summaries.
389    Json,
390    /// YAML summaries.
391    Yaml,
392    /// TOML and Cargo manifest summaries.
393    Toml,
394    /// XML summaries.
395    Xml,
396    /// CSS-family summaries.
397    Css,
398    /// HTML summaries.
399    Html,
400    /// TOON summaries.
401    Toon,
402    /// `PowerShell` summaries.
403    PowerShell,
404    /// Configuration and plain-text summaries.
405    ConfigText,
406}
407
408/// Provenance class for an accepted language row.
409#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
410#[serde(rename_all = "kebab-case")]
411pub enum CapabilityProvenance {
412    /// ProjectAtlas-owned deterministic behavior under the workspace license.
413    ProjectAtlas,
414    /// A pinned Cargo Tree-sitter grammar dependency.
415    TreeSitter(TreeSitterGrammar),
416    /// ProjectAtlas-accepted detection metadata from the pinned optional catalog.
417    ///
418    /// This does not claim that the corresponding grammar binary, its subtree
419    /// license, or its required-platform behavior has passed optional-pack gates.
420    PinnedOptionalCatalog,
421}
422
423impl CapabilityProvenance {
424    /// Return the declared license input for settings and generated documentation.
425    #[must_use]
426    pub const fn license(self) -> &'static str {
427        match self {
428            Self::ProjectAtlas | Self::TreeSitter(_) | Self::PinnedOptionalCatalog => "MIT",
429        }
430    }
431
432    /// Return the exact owning package or catalog identity.
433    #[must_use]
434    pub const fn source(self) -> &'static str {
435        match self {
436            Self::ProjectAtlas => "projectatlas",
437            Self::TreeSitter(grammar) => grammar.package(),
438            Self::PinnedOptionalCatalog => OPTIONAL_GRAMMAR_CATALOG,
439        }
440    }
441
442    /// Return the exact owning package or catalog version.
443    #[must_use]
444    pub const fn version(self) -> &'static str {
445        match self {
446            Self::ProjectAtlas => env!("CARGO_PKG_VERSION"),
447            Self::TreeSitter(grammar) => grammar.version(),
448            Self::PinnedOptionalCatalog => OPTIONAL_GRAMMAR_CATALOG_VERSION,
449        }
450    }
451}
452
453/// Required platform applicability for one accepted capability.
454#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
455#[serde(rename_all = "kebab-case")]
456pub enum RequiredPlatformSet {
457    /// Every platform supported by the `ProjectAtlas` release matrix.
458    AllSupported,
459}
460
461/// Natural positive and negative detector fixtures for an accepted row.
462#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
463pub struct LanguageCapabilityFixtures {
464    /// Natural path that must select this language.
465    pub positive_path: &'static str,
466    /// Similar path that must not select this language.
467    pub negative_path: &'static str,
468}
469
470/// One accepted canonical language capability row.
471#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
472pub struct LanguageCapability {
473    /// Stable canonical language or file-family identifier.
474    pub id: &'static str,
475    /// Compatibility aliases accepted only for explicit selection.
476    pub aliases: &'static [&'static str],
477    /// Compatibility parser tier.
478    pub parser_support: LanguageParserSupport,
479    /// Closed symbol parser owner.
480    pub symbol_parser: SymbolParserOwner,
481    /// Closed project-wide semantic provider owner.
482    pub semantic_provider: SemanticProviderOwner,
483    /// Optional bounded host-to-embedded provider pairing.
484    pub embedded_language: Option<EmbeddedLanguageCapability>,
485    /// Optional structural summary owner.
486    pub structural_summary: Option<StructuralSummaryOwner>,
487    /// Optional broad-parser pack owner; absent for the 0.3.26 core floor.
488    pub optional_pack: Option<&'static str>,
489    /// Currently achieved independent support.
490    pub support: LanguageCapabilitySupport,
491    /// Accepted minimum that cannot shrink silently in this set version.
492    pub accepted_minimum: LanguageCapabilitySupport,
493    /// Natural detector fixtures.
494    pub fixtures: LanguageCapabilityFixtures,
495    /// Parser or `ProjectAtlas` provenance input.
496    pub provenance: CapabilityProvenance,
497    /// Required release platforms.
498    pub required_platforms: RequiredPlatformSet,
499}
500
501impl LanguageCapability {
502    /// Return the accepted semantic provider for direct or embedded source.
503    ///
504    /// Direct providers take precedence. Embedded hosts expose their provider
505    /// only for admitted embedded facts; callers remain responsible for not
506    /// treating the host itself as an ordinary module without such facts.
507    #[must_use]
508    pub const fn effective_semantic_provider(self) -> Option<SemanticProviderOwner> {
509        match self.semantic_provider {
510            SemanticProviderOwner::Unavailable => match self.embedded_language {
511                Some(embedded)
512                    if !matches!(
513                        embedded.semantic_provider,
514                        SemanticProviderOwner::Unavailable
515                    ) =>
516                {
517                    Some(embedded.semantic_provider)
518                }
519                _ => None,
520            },
521            provider => Some(provider),
522        }
523    }
524}
525
526/// Static compatibility parser metadata for one detected language family.
527#[derive(Clone, Copy, Debug, Eq, PartialEq)]
528pub struct LanguageSpec {
529    /// Detected language or file-family identifier.
530    pub language: &'static str,
531    /// Parser coverage level.
532    pub parser_support: LanguageParserSupport,
533}
534
535/// Detection rule class and precedence reason.
536#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
537#[serde(rename_all = "kebab-case")]
538pub enum LanguageDetectionReason {
539    /// A validated explicit override selected the language.
540    ExplicitOverride,
541    /// A case-sensitive exact filename selected the language.
542    ExactFilename,
543    /// The longest declared compound extension selected the language.
544    CompoundExtension,
545    /// A case-insensitive ordinary extension selected the language.
546    Extension,
547    /// A bounded declared content/dialect rule selected the language.
548    ContentDialect,
549}
550
551/// Typed language detection result.
552#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
553pub struct LanguageDetection {
554    /// Canonical registry identifier.
555    pub language: &'static str,
556    /// Rule class that selected the identifier.
557    pub reason: LanguageDetectionReason,
558}
559
560/// Typed language detection request.
561#[derive(Clone, Copy, Debug, Default)]
562pub struct LanguageDetectionRequest<'a> {
563    /// Repository or native path.
564    pub path: &'a str,
565    /// Pre-normalized extension supplied by a scanner, when available.
566    pub extension: Option<&'a str>,
567    /// Explicit canonical ID or alias, when configured.
568    pub explicit_override: Option<&'a str>,
569    /// Bounded content prefix captured by the owning source read.
570    pub content_prefix: Option<&'a [u8]>,
571}
572
573impl<'a> LanguageDetectionRequest<'a> {
574    /// Build a request compatible with the 0.3.26 path/extension API.
575    #[must_use]
576    pub const fn new(path: &'a str, extension: Option<&'a str>) -> Self {
577        Self {
578            path,
579            extension,
580            explicit_override: None,
581            content_prefix: None,
582        }
583    }
584}
585
586/// Invalid explicit language selection.
587#[derive(Clone, Debug, Eq, PartialEq)]
588pub struct LanguageDetectionError {
589    /// Invalid canonical ID or alias supplied by the caller.
590    requested: String,
591}
592
593impl fmt::Display for LanguageDetectionError {
594    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
595        write!(
596            formatter,
597            "unknown explicit language override {:?}",
598            self.requested
599        )
600    }
601}
602
603impl Error for LanguageDetectionError {}
604
605/// Registry validation failure with both conflicting owners where applicable.
606#[derive(Clone, Debug, Eq, PartialEq)]
607pub struct LanguageRegistryError {
608    /// Deterministic validation diagnostic.
609    message: String,
610}
611
612impl LanguageRegistryError {
613    /// Build one deterministic registry diagnostic.
614    fn new(message: impl Into<String>) -> Self {
615        Self {
616            message: message.into(),
617        }
618    }
619}
620
621impl fmt::Display for LanguageRegistryError {
622    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
623        formatter.write_str(&self.message)
624    }
625}
626
627impl Error for LanguageRegistryError {}
628
629/// One static exact, compound, or extension detector rule.
630#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
631pub struct LanguageDetectionRule {
632    /// Match value.
633    pub value: &'static str,
634    /// Canonical owner language.
635    pub language: &'static str,
636}
637
638/// One registry-owned shebang interpreter rule.
639#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
640pub struct LanguageContentRule {
641    /// Lowercase interpreter basename matched after optional `env` or `busybox`.
642    pub interpreter: &'static str,
643    /// Whether an ASCII numeric version suffix such as `3.12` is accepted.
644    pub allow_version_suffix: bool,
645    /// Canonical owner language.
646    pub language: &'static str,
647}
648
649/// Default an omitted semantic provider to an honest unavailable claim.
650macro_rules! semantic_provider_or_unavailable {
651    () => {
652        SemanticProviderOwner::Unavailable
653    };
654    ($provider:expr) => {
655        $provider
656    };
657}
658
659/// Default an omitted embedded-language contract to no host projection.
660macro_rules! embedded_language_or_none {
661    () => {
662        None
663    };
664    ($capability:expr) => {
665        Some($capability)
666    };
667}
668
669/// Generate typed capability and detection projections from one local manifest.
670macro_rules! define_language_registry {
671    (
672        capabilities {
673            $(
674                $id:literal => {
675                    aliases: [$($alias:literal),* $(,)?],
676                    parser_support: $parser_support:ident,
677                    symbol_parser: $symbol_parser:expr,
678                    structural_summary: $structural_summary:expr,
679                    support: $support:expr,
680                    $(semantic_provider: $semantic_provider:expr,)?
681                    $(embedded_language: $embedded_language:expr,)?
682                    positive: $positive:literal,
683                    negative: $negative:literal,
684                    provenance: $provenance:expr
685                }
686            ),* $(,)?
687        }
688        optional_capabilities {
689            $(
690                $optional_id:literal => {
691                    aliases: [$($optional_alias:literal),* $(,)?],
692                    extension: $optional_extension:literal
693                }
694            ),* $(,)?
695        }
696        exact_filenames { $($exact:literal => $exact_language:literal),* $(,)? }
697        compound_extensions { $($compound:literal => $compound_language:literal),* $(,)? }
698        broad_extensions { $($extension:literal => $extension_language:literal),* $(,)? }
699        additional_extensions { $($additional:literal => $additional_language:literal),* $(,)? }
700        content_interpreters {
701            $(
702                $interpreter:literal => {
703                    language: $content_language:literal,
704                    version_suffix: $version_suffix:literal
705                }
706            ),* $(,)?
707        }
708    ) => {
709        /// Accepted canonical language capabilities.
710        pub const LANGUAGE_CAPABILITIES: &[LanguageCapability] = &[
711            $(LanguageCapability {
712                id: $id,
713                aliases: &[$($alias),*],
714                parser_support: LanguageParserSupport::$parser_support,
715                symbol_parser: $symbol_parser,
716                semantic_provider: semantic_provider_or_unavailable!($($semantic_provider)?),
717                embedded_language: embedded_language_or_none!($($embedded_language)?),
718                structural_summary: $structural_summary,
719                optional_pack: None,
720                support: $support,
721                accepted_minimum: $support,
722                fixtures: LanguageCapabilityFixtures {
723                    positive_path: $positive,
724                    negative_path: $negative,
725                },
726                provenance: $provenance,
727                required_platforms: RequiredPlatformSet::AllSupported,
728            }),*,
729            $(LanguageCapability {
730                id: $optional_id,
731                aliases: &[$($optional_alias),*],
732                parser_support: LanguageParserSupport::Fallback,
733                symbol_parser: SymbolParserOwner::Fallback,
734                semantic_provider: SemanticProviderOwner::Unavailable,
735                embedded_language: None,
736                structural_summary: None,
737                optional_pack: Some(BROAD_PARSER_PACK_ID),
738                support: SUPPORTED_FALLBACK,
739                accepted_minimum: SUPPORTED_FALLBACK,
740                fixtures: LanguageCapabilityFixtures {
741                    positive_path: concat!("fixture", $optional_extension),
742                    negative_path: concat!("fixture", $optional_extension, ".bak"),
743                },
744                provenance: CapabilityProvenance::PinnedOptionalCatalog,
745                required_platforms: RequiredPlatformSet::AllSupported,
746            }),*
747        ];
748
749        /// Compatibility parser metadata generated from the accepted manifest.
750        pub const LANGUAGE_SPECS: &[LanguageSpec] = &[
751            $(LanguageSpec {
752                language: $id,
753                parser_support: LanguageParserSupport::$parser_support,
754            }),*,
755            $(LanguageSpec {
756                language: $optional_id,
757                parser_support: LanguageParserSupport::Fallback,
758            }),*
759        ];
760
761        /// Broad source extensions supported by the 0.3.26 scanner contract.
762        pub const BROAD_SOURCE_EXTENSIONS: &[&str] = &[$($extension),*];
763
764        /// Complete registry-known extension projection, including recognition-only pack rows.
765        pub const DETECTED_SOURCE_EXTENSIONS: &[&str] = &[
766            $($extension),*,
767            $($optional_extension),*
768        ];
769
770        /// Case-sensitive exact filename rules.
771        pub const EXACT_FILENAME_RULES: &[LanguageDetectionRule] = &[
772            $(LanguageDetectionRule { value: $exact, language: $exact_language }),*
773        ];
774
775        /// Longest-first compound extension rules.
776        pub const COMPOUND_EXTENSION_RULES: &[LanguageDetectionRule] = &[
777            $(LanguageDetectionRule { value: $compound, language: $compound_language }),*
778        ];
779
780        /// Ordinary extension rules, including compatibility-only hidden extensions.
781        pub const EXTENSION_RULES: &[LanguageDetectionRule] = &[
782            $(LanguageDetectionRule { value: $extension, language: $extension_language }),*,
783            $(LanguageDetectionRule { value: $optional_extension, language: $optional_id }),*,
784            $(LanguageDetectionRule { value: $additional, language: $additional_language }),*
785        ];
786
787        /// Bounded shebang interpreter rules owned by the accepted registry.
788        pub const CONTENT_DIALECT_RULES: &[LanguageContentRule] = &[
789            $(LanguageContentRule {
790                interpreter: $interpreter,
791                allow_version_suffix: $version_suffix,
792                language: $content_language,
793            }),*
794        ];
795
796        fn exact_filename_language(file_name: &str) -> Option<&'static str> {
797            match file_name {
798                $($exact => Some($exact_language),)*
799                _ => None,
800            }
801        }
802
803        fn extension_language(extension: &str) -> Option<&'static str> {
804            match extension {
805                $($extension => Some($extension_language),)*
806                $($optional_extension => Some($optional_id),)*
807                $($additional => Some($additional_language),)*
808                _ => None,
809            }
810        }
811
812        /// Resolve a canonical language ID or compatibility alias.
813        #[must_use]
814        pub fn canonical_language_id(value: &str) -> Option<&'static str> {
815            let trimmed = value.trim();
816            if let Some(canonical) = canonical_language_id_exact(trimmed) {
817                return Some(canonical);
818            }
819            let normalized = trimmed.to_ascii_lowercase();
820            canonical_language_id_exact(&normalized)
821        }
822
823        /// Resolve an already-normalized canonical language ID or alias.
824        fn canonical_language_id_exact(value: &str) -> Option<&'static str> {
825            match value {
826                $($id $(| $alias)* => Some($id),)*
827                $($optional_id $(| $optional_alias)* => Some($optional_id),)*
828                _ => None,
829            }
830        }
831    };
832}
833
834define_language_registry! {
835    capabilities {
836        "rust" => { aliases: ["rs"], parser_support: Native, symbol_parser: SymbolParserOwner::TreeSitter(TreeSitterGrammar::Rust), structural_summary: None, support: SUPPORTED_SEMANTIC, semantic_provider: SemanticProviderOwner::Rust, positive: "fixture.rs", negative: "fixture.rs.bak", provenance: CapabilityProvenance::TreeSitter(TreeSitterGrammar::Rust) },
837        "rust-build-script" => { aliases: [], parser_support: Native, symbol_parser: SymbolParserOwner::TreeSitter(TreeSitterGrammar::Rust), structural_summary: None, support: SUPPORTED_SEMANTIC, semantic_provider: SemanticProviderOwner::Rust, positive: "build.rs", negative: "build.rs.bak", provenance: CapabilityProvenance::TreeSitter(TreeSitterGrammar::Rust) },
838        "python" => { aliases: ["py"], parser_support: Native, symbol_parser: SymbolParserOwner::TreeSitter(TreeSitterGrammar::Python), structural_summary: None, support: SUPPORTED_SEMANTIC, semantic_provider: SemanticProviderOwner::Python, positive: "fixture.py", negative: "fixture.py.bak", provenance: CapabilityProvenance::TreeSitter(TreeSitterGrammar::Python) },
839        "javascript" => { aliases: ["js"], parser_support: Native, symbol_parser: SymbolParserOwner::TreeSitter(TreeSitterGrammar::JavaScript), structural_summary: None, support: SUPPORTED_SEMANTIC, semantic_provider: SemanticProviderOwner::EcmaScript, positive: "fixture.js", negative: "fixture.js.bak", provenance: CapabilityProvenance::TreeSitter(TreeSitterGrammar::JavaScript) },
840        "typescript" => { aliases: ["ts"], parser_support: Native, symbol_parser: SymbolParserOwner::TreeSitter(TreeSitterGrammar::TypeScript), structural_summary: None, support: SUPPORTED_SEMANTIC, semantic_provider: SemanticProviderOwner::EcmaScript, positive: "fixture.ts", negative: "fixture.ts.bak", provenance: CapabilityProvenance::TreeSitter(TreeSitterGrammar::TypeScript) },
841        "tsx" => { aliases: [], parser_support: Native, symbol_parser: SymbolParserOwner::TreeSitter(TreeSitterGrammar::Tsx), structural_summary: None, support: SUPPORTED_SEMANTIC, semantic_provider: SemanticProviderOwner::EcmaScript, positive: "fixture.tsx", negative: "fixture.tsx.bak", provenance: CapabilityProvenance::TreeSitter(TreeSitterGrammar::Tsx) },
842        "java" => { aliases: [], parser_support: Native, symbol_parser: SymbolParserOwner::TreeSitter(TreeSitterGrammar::Java), structural_summary: None, support: SUPPORTED_NATIVE, positive: "Fixture.java", negative: "Fixture.java.bak", provenance: CapabilityProvenance::TreeSitter(TreeSitterGrammar::Java) },
843        "kotlin" => { aliases: ["kt"], parser_support: Native, symbol_parser: SymbolParserOwner::TreeSitter(TreeSitterGrammar::Kotlin), structural_summary: None, support: SUPPORTED_NATIVE, positive: "Fixture.kt", negative: "Fixture.kt.bak", provenance: CapabilityProvenance::TreeSitter(TreeSitterGrammar::Kotlin) },
844        "csharp" => { aliases: ["c#", "cs"], parser_support: Native, symbol_parser: SymbolParserOwner::TreeSitter(TreeSitterGrammar::CSharp), structural_summary: None, support: SUPPORTED_NATIVE, positive: "Fixture.cs", negative: "Fixture.cs.bak", provenance: CapabilityProvenance::TreeSitter(TreeSitterGrammar::CSharp) },
845        "go" => { aliases: [], parser_support: Native, symbol_parser: SymbolParserOwner::TreeSitter(TreeSitterGrammar::Go), structural_summary: None, support: SUPPORTED_NATIVE, positive: "fixture.go", negative: "fixture.go.bak", provenance: CapabilityProvenance::TreeSitter(TreeSitterGrammar::Go) },
846        "objective-c" => { aliases: ["objc"], parser_support: Native, symbol_parser: SymbolParserOwner::TreeSitter(TreeSitterGrammar::ObjectiveC), structural_summary: None, support: SUPPORTED_NATIVE, positive: "fixture.m", negative: "fixture.m.bak", provenance: CapabilityProvenance::TreeSitter(TreeSitterGrammar::ObjectiveC) },
847        "zig" => { aliases: [], parser_support: Native, symbol_parser: SymbolParserOwner::TreeSitter(TreeSitterGrammar::Zig), structural_summary: None, support: SUPPORTED_NATIVE, positive: "fixture.zig", negative: "fixture.zig.bak", provenance: CapabilityProvenance::TreeSitter(TreeSitterGrammar::Zig) },
848        "c" => { aliases: [], parser_support: Native, symbol_parser: SymbolParserOwner::TreeSitter(TreeSitterGrammar::C), structural_summary: None, support: SUPPORTED_NATIVE, positive: "fixture.c", negative: "fixture.c.bak", provenance: CapabilityProvenance::TreeSitter(TreeSitterGrammar::C) },
849        "cpp" => { aliases: ["c++"], parser_support: Native, symbol_parser: SymbolParserOwner::TreeSitter(TreeSitterGrammar::Cpp), structural_summary: None, support: SUPPORTED_NATIVE, positive: "fixture.cpp", negative: "fixture.cpp.bak", provenance: CapabilityProvenance::TreeSitter(TreeSitterGrammar::Cpp) },
850        "h" => { aliases: [], parser_support: Native, symbol_parser: SymbolParserOwner::TreeSitter(TreeSitterGrammar::C), structural_summary: None, support: SUPPORTED_NATIVE, positive: "fixture.h", negative: "fixture.h.bak", provenance: CapabilityProvenance::TreeSitter(TreeSitterGrammar::C) },
851        "hpp" => { aliases: [], parser_support: Native, symbol_parser: SymbolParserOwner::TreeSitter(TreeSitterGrammar::Cpp), structural_summary: None, support: SUPPORTED_NATIVE, positive: "fixture.hpp", negative: "fixture.hpp.bak", provenance: CapabilityProvenance::TreeSitter(TreeSitterGrammar::Cpp) },
852        "cargo-manifest" => { aliases: [], parser_support: Manifest, symbol_parser: SymbolParserOwner::CargoManifest, structural_summary: Some(StructuralSummaryOwner::Toml), support: SUPPORTED_SEMANTIC, semantic_provider: SemanticProviderOwner::Cargo, positive: "Cargo.toml", negative: "Cargo.toml.bak", provenance: CapabilityProvenance::ProjectAtlas },
853        "cargo-lock" => { aliases: [], parser_support: Manifest, symbol_parser: SymbolParserOwner::CargoManifest, structural_summary: None, support: SUPPORTED_NATIVE, positive: "Cargo.lock", negative: "Cargo.lock.bak", provenance: CapabilityProvenance::ProjectAtlas },
854        "vue" => { aliases: [], parser_support: Structural, symbol_parser: SymbolParserOwner::Vue, structural_summary: None, support: SUPPORTED_NATIVE, embedded_language: EmbeddedLanguageCapability { host_kind: EmbeddedHostKind::Component, semantic_provider: SemanticProviderOwner::EcmaScript }, positive: "Fixture.vue", negative: "Fixture.vue.bak", provenance: CapabilityProvenance::ProjectAtlas },
855        "markdown" => { aliases: ["md"], parser_support: Structural, symbol_parser: SymbolParserOwner::Unavailable, structural_summary: Some(StructuralSummaryOwner::Markdown), support: SUPPORTED_STRUCTURAL, positive: "fixture.md", negative: "fixture.md.bak", provenance: CapabilityProvenance::ProjectAtlas },
856        "json" => { aliases: [], parser_support: Structural, symbol_parser: SymbolParserOwner::Unavailable, structural_summary: Some(StructuralSummaryOwner::Json), support: SUPPORTED_STRUCTURAL, positive: "fixture.json", negative: "fixture.json.bak", provenance: CapabilityProvenance::ProjectAtlas },
857        "yaml" => { aliases: ["yml"], parser_support: Structural, symbol_parser: SymbolParserOwner::Unavailable, structural_summary: Some(StructuralSummaryOwner::Yaml), support: SUPPORTED_STRUCTURAL, positive: "fixture.yml", negative: "fixture.yml.bak", provenance: CapabilityProvenance::ProjectAtlas },
858        "css" => { aliases: [], parser_support: Structural, symbol_parser: SymbolParserOwner::Unavailable, structural_summary: Some(StructuralSummaryOwner::Css), support: SUPPORTED_STRUCTURAL, positive: "fixture.css", negative: "fixture.css.bak", provenance: CapabilityProvenance::ProjectAtlas },
859        "html" => { aliases: [], parser_support: Structural, symbol_parser: SymbolParserOwner::Unavailable, structural_summary: Some(StructuralSummaryOwner::Html), support: SUPPORTED_STRUCTURAL, embedded_language: EmbeddedLanguageCapability { host_kind: EmbeddedHostKind::HtmlLike, semantic_provider: SemanticProviderOwner::EcmaScript }, positive: "fixture.html", negative: "fixture.html.bak", provenance: CapabilityProvenance::ProjectAtlas },
860        "toon" => { aliases: [], parser_support: Structural, symbol_parser: SymbolParserOwner::Unavailable, structural_summary: Some(StructuralSummaryOwner::Toon), support: SUPPORTED_STRUCTURAL, positive: "fixture.toon", negative: "fixture.toon.bak", provenance: CapabilityProvenance::ProjectAtlas },
861        "dockerfile" => { aliases: [], parser_support: Fallback, symbol_parser: SymbolParserOwner::Fallback, structural_summary: None, support: SUPPORTED_FALLBACK, positive: "Dockerfile", negative: "Dockerfile.bak", provenance: CapabilityProvenance::ProjectAtlas },
862        "makefile" => { aliases: [], parser_support: Fallback, symbol_parser: SymbolParserOwner::Fallback, structural_summary: None, support: SUPPORTED_FALLBACK, positive: "Makefile", negative: "Makefile.bak", provenance: CapabilityProvenance::ProjectAtlas },
863        "text" => { aliases: ["txt"], parser_support: Structural, symbol_parser: SymbolParserOwner::Unavailable, structural_summary: Some(StructuralSummaryOwner::ConfigText), support: SUPPORTED_STRUCTURAL, positive: "fixture.txt", negative: "fixture.txt.bak", provenance: CapabilityProvenance::ProjectAtlas },
864        "toml" => { aliases: [], parser_support: Structural, symbol_parser: SymbolParserOwner::Unavailable, structural_summary: Some(StructuralSummaryOwner::Toml), support: SUPPORTED_STRUCTURAL, positive: "fixture.toml", negative: "fixture.toml.bak", provenance: CapabilityProvenance::ProjectAtlas },
865        "xml" => { aliases: [], parser_support: Structural, symbol_parser: SymbolParserOwner::Unavailable, structural_summary: Some(StructuralSummaryOwner::Xml), support: SUPPORTED_STRUCTURAL, positive: "fixture.xml", negative: "fixture.xml.bak", provenance: CapabilityProvenance::ProjectAtlas },
866        "svelte" => { aliases: [], parser_support: Fallback, symbol_parser: SymbolParserOwner::Fallback, structural_summary: None, support: SUPPORTED_FALLBACK, embedded_language: EmbeddedLanguageCapability { host_kind: EmbeddedHostKind::Template, semantic_provider: SemanticProviderOwner::EcmaScript }, positive: "Fixture.svelte", negative: "Fixture.svelte.bak", provenance: CapabilityProvenance::ProjectAtlas },
867        "astro" => { aliases: [], parser_support: Fallback, symbol_parser: SymbolParserOwner::Fallback, structural_summary: None, support: SUPPORTED_FALLBACK, positive: "Fixture.astro", negative: "Fixture.astro.bak", provenance: CapabilityProvenance::ProjectAtlas },
868        "jsp" => { aliases: [], parser_support: Fallback, symbol_parser: SymbolParserOwner::Fallback, structural_summary: None, support: SUPPORTED_FALLBACK, positive: "fixture.jsp", negative: "fixture.jsp.bak", provenance: CapabilityProvenance::ProjectAtlas },
869        "jsp-tag" => { aliases: [], parser_support: Fallback, symbol_parser: SymbolParserOwner::Fallback, structural_summary: None, support: SUPPORTED_FALLBACK, positive: "fixture.tag", negative: "fixture.tag.bak", provenance: CapabilityProvenance::ProjectAtlas },
870        "gsp" => { aliases: [], parser_support: Fallback, symbol_parser: SymbolParserOwner::Fallback, structural_summary: None, support: SUPPORTED_FALLBACK, positive: "fixture.gsp", negative: "fixture.gsp.bak", provenance: CapabilityProvenance::ProjectAtlas },
871        "groovy" => { aliases: [], parser_support: Fallback, symbol_parser: SymbolParserOwner::Fallback, structural_summary: None, support: SUPPORTED_FALLBACK, positive: "fixture.groovy", negative: "fixture.groovy.bak", provenance: CapabilityProvenance::ProjectAtlas },
872        "protobuf" => { aliases: ["proto"], parser_support: Fallback, symbol_parser: SymbolParserOwner::Fallback, structural_summary: None, support: SUPPORTED_FALLBACK, positive: "fixture.proto", negative: "fixture.proto.bak", provenance: CapabilityProvenance::ProjectAtlas },
873        "handlebars" => { aliases: ["hbs"], parser_support: Fallback, symbol_parser: SymbolParserOwner::Fallback, structural_summary: None, support: SUPPORTED_FALLBACK, positive: "fixture.hbs", negative: "fixture.hbs.bak", provenance: CapabilityProvenance::ProjectAtlas },
874        "ejs" => { aliases: [], parser_support: Fallback, symbol_parser: SymbolParserOwner::Fallback, structural_summary: None, support: SUPPORTED_FALLBACK, positive: "fixture.ejs", negative: "fixture.ejs.bak", provenance: CapabilityProvenance::ProjectAtlas },
875        "pug" => { aliases: [], parser_support: Fallback, symbol_parser: SymbolParserOwner::Fallback, structural_summary: None, support: SUPPORTED_FALLBACK, positive: "fixture.pug", negative: "fixture.pug.bak", provenance: CapabilityProvenance::ProjectAtlas },
876        "freemarker" => { aliases: ["ftl"], parser_support: Fallback, symbol_parser: SymbolParserOwner::Fallback, structural_summary: None, support: SUPPORTED_FALLBACK, positive: "fixture.ftl", negative: "fixture.ftl.bak", provenance: CapabilityProvenance::ProjectAtlas },
877        "mustache" => { aliases: [], parser_support: Fallback, symbol_parser: SymbolParserOwner::Fallback, structural_summary: None, support: SUPPORTED_FALLBACK, positive: "fixture.mustache", negative: "fixture.mustache.bak", provenance: CapabilityProvenance::ProjectAtlas },
878        "liquid" => { aliases: [], parser_support: Fallback, symbol_parser: SymbolParserOwner::Fallback, structural_summary: None, support: SUPPORTED_FALLBACK, positive: "fixture.liquid", negative: "fixture.liquid.bak", provenance: CapabilityProvenance::ProjectAtlas },
879        "erb" => { aliases: [], parser_support: Fallback, symbol_parser: SymbolParserOwner::Fallback, structural_summary: None, support: SUPPORTED_FALLBACK, positive: "fixture.erb", negative: "fixture.erb.bak", provenance: CapabilityProvenance::ProjectAtlas },
880        "sql" => { aliases: [], parser_support: Fallback, symbol_parser: SymbolParserOwner::Fallback, structural_summary: None, support: SUPPORTED_FALLBACK, positive: "fixture.sql", negative: "fixture.sql.bak", provenance: CapabilityProvenance::ProjectAtlas },
881        "graphql" => { aliases: ["gql"], parser_support: Fallback, symbol_parser: SymbolParserOwner::Fallback, structural_summary: None, support: SUPPORTED_FALLBACK, positive: "fixture.gql", negative: "fixture.gql.bak", provenance: CapabilityProvenance::ProjectAtlas },
882        "config" => { aliases: [], parser_support: Structural, symbol_parser: SymbolParserOwner::Unavailable, structural_summary: Some(StructuralSummaryOwner::ConfigText), support: SUPPORTED_STRUCTURAL, positive: "fixture.ini", negative: "fixture.ini.bak", provenance: CapabilityProvenance::ProjectAtlas },
883        "ruby" => { aliases: ["rb"], parser_support: Fallback, symbol_parser: SymbolParserOwner::Fallback, structural_summary: None, support: SUPPORTED_FALLBACK, positive: "fixture.rb", negative: "fixture.rb.bak", provenance: CapabilityProvenance::ProjectAtlas },
884        "php" => { aliases: [], parser_support: Fallback, symbol_parser: SymbolParserOwner::Fallback, structural_summary: None, support: SUPPORTED_FALLBACK, positive: "fixture.php", negative: "fixture.php.bak", provenance: CapabilityProvenance::ProjectAtlas },
885        "swift" => { aliases: [], parser_support: Fallback, symbol_parser: SymbolParserOwner::Fallback, structural_summary: None, support: SUPPORTED_FALLBACK, positive: "fixture.swift", negative: "fixture.swift.bak", provenance: CapabilityProvenance::ProjectAtlas },
886        "scala" => { aliases: [], parser_support: Fallback, symbol_parser: SymbolParserOwner::Fallback, structural_summary: None, support: SUPPORTED_FALLBACK, positive: "fixture.scala", negative: "fixture.scala.bak", provenance: CapabilityProvenance::ProjectAtlas },
887        "shell" => { aliases: ["sh"], parser_support: Fallback, symbol_parser: SymbolParserOwner::Fallback, structural_summary: None, support: SUPPORTED_FALLBACK, positive: "fixture.sh", negative: "fixture.sh.bak", provenance: CapabilityProvenance::ProjectAtlas },
888        "powershell" => { aliases: ["pwsh"], parser_support: Fallback, symbol_parser: SymbolParserOwner::PowerShell, structural_summary: Some(StructuralSummaryOwner::PowerShell), support: SUPPORTED_NATIVE, positive: "fixture.ps1", negative: "fixture.ps1.bak", provenance: CapabilityProvenance::ProjectAtlas },
889        "batch" => { aliases: [], parser_support: Fallback, symbol_parser: SymbolParserOwner::Fallback, structural_summary: None, support: SUPPORTED_FALLBACK, positive: "fixture.bat", negative: "fixture.bat.bak", provenance: CapabilityProvenance::ProjectAtlas },
890        "r" => { aliases: ["rscript"], parser_support: Fallback, symbol_parser: SymbolParserOwner::Fallback, structural_summary: None, support: SUPPORTED_FALLBACK, positive: "fixture.r", negative: "fixture.r.bak", provenance: CapabilityProvenance::ProjectAtlas },
891        "perl" => { aliases: [], parser_support: Fallback, symbol_parser: SymbolParserOwner::Fallback, structural_summary: None, support: SUPPORTED_FALLBACK, positive: "fixture.pl", negative: "fixture.pl.bak", provenance: CapabilityProvenance::ProjectAtlas },
892        "lua" => { aliases: [], parser_support: Fallback, symbol_parser: SymbolParserOwner::Fallback, structural_summary: None, support: SUPPORTED_FALLBACK, positive: "fixture.lua", negative: "fixture.lua.bak", provenance: CapabilityProvenance::ProjectAtlas },
893        "dart" => { aliases: [], parser_support: Fallback, symbol_parser: SymbolParserOwner::Fallback, structural_summary: None, support: SUPPORTED_FALLBACK, positive: "fixture.dart", negative: "fixture.dart.bak", provenance: CapabilityProvenance::ProjectAtlas },
894        "haskell" => { aliases: ["hs"], parser_support: Fallback, symbol_parser: SymbolParserOwner::Fallback, structural_summary: None, support: SUPPORTED_FALLBACK, positive: "fixture.hs", negative: "fixture.hs.bak", provenance: CapabilityProvenance::ProjectAtlas },
895        "ocaml" => { aliases: [], parser_support: Fallback, symbol_parser: SymbolParserOwner::Fallback, structural_summary: None, support: SUPPORTED_FALLBACK, positive: "fixture.ml", negative: "fixture.ml.bak", provenance: CapabilityProvenance::ProjectAtlas },
896        "fsharp" => { aliases: ["f#"], parser_support: Fallback, symbol_parser: SymbolParserOwner::Fallback, structural_summary: None, support: SUPPORTED_FALLBACK, positive: "fixture.fs", negative: "fixture.fs.bak", provenance: CapabilityProvenance::ProjectAtlas },
897        "clojure" => { aliases: ["clj"], parser_support: Fallback, symbol_parser: SymbolParserOwner::Fallback, structural_summary: None, support: SUPPORTED_FALLBACK, positive: "fixture.clj", negative: "fixture.clj.bak", provenance: CapabilityProvenance::ProjectAtlas },
898        "vim" => { aliases: ["vimscript"], parser_support: Fallback, symbol_parser: SymbolParserOwner::Fallback, structural_summary: None, support: SUPPORTED_FALLBACK, positive: "fixture.vim", negative: "fixture.vim.bak", provenance: CapabilityProvenance::ProjectAtlas }
899    }
900    optional_capabilities {
901        "abl" => { aliases: [], extension: ".p" },
902        "actionscript" => { aliases: [], extension: ".as" },
903        "ada" => { aliases: [], extension: ".ada" },
904        "agda" => { aliases: [], extension: ".agda" },
905        "al" => { aliases: [], extension: ".al" },
906        "arduino" => { aliases: [], extension: ".ino" },
907        "asciidoc" => { aliases: [], extension: ".adoc" },
908        "asm" => { aliases: [], extension: ".s" },
909        "awk" => { aliases: [], extension: ".awk" },
910        "beancount" => { aliases: [], extension: ".beancount" },
911        "bibtex" => { aliases: [], extension: ".bib" },
912        "bicep" => { aliases: [], extension: ".bicep" },
913        "bitbake" => { aliases: [], extension: ".bb" },
914        "blade" => { aliases: [], extension: ".blade" },
915        "brightscript" => { aliases: [], extension: ".brs" },
916        "bsl" => { aliases: [], extension: ".bsl" },
917        "c3" => { aliases: [], extension: ".c3" },
918        "caddy" => { aliases: [], extension: ".caddyfile" },
919        "cairo" => { aliases: [], extension: ".cairo" },
920        "capnp" => { aliases: [], extension: ".capnp" },
921        "cedar" => { aliases: [], extension: ".cedar" },
922        "cedarschema" => { aliases: [], extension: ".cedarschema" },
923        "cel" => { aliases: [], extension: ".cel" },
924        "cfml" => { aliases: [], extension: ".cfc" },
925        "chatito" => { aliases: [], extension: ".chatito" },
926        "chuck" => { aliases: [], extension: ".ck" },
927        "circom" => { aliases: [], extension: ".circom" },
928        "clarity" => { aliases: [], extension: ".clar" },
929        "cmake" => { aliases: [], extension: ".cmake" },
930        "cobol" => { aliases: [], extension: ".cobol" },
931        "commonlisp" => { aliases: [], extension: ".lisp" },
932        "cooklang" => { aliases: [], extension: ".cook" },
933        "corn" => { aliases: [], extension: ".corn" },
934        "cpon" => { aliases: [], extension: ".cpon" },
935        "crystal" => { aliases: [], extension: ".cr" },
936        "cst" => { aliases: [], extension: ".cst" },
937        "csv" => { aliases: [], extension: ".csv" },
938        "cuda" => { aliases: [], extension: ".cu" },
939        "cue" => { aliases: [], extension: ".cue" },
940        "cylc" => { aliases: [], extension: ".cylc" },
941        "d" => { aliases: [], extension: ".d" },
942        "desktop" => { aliases: [], extension: ".desktop" },
943        "devicetree" => { aliases: [], extension: ".dts" },
944        "dhall" => { aliases: [], extension: ".dhall" },
945        "diff" => { aliases: [], extension: ".diff" },
946        "djot" => { aliases: [], extension: ".dj" },
947        "dot" => { aliases: [], extension: ".dot" },
948        "dtd" => { aliases: [], extension: ".dtd" },
949        "ebnf" => { aliases: [], extension: ".ebnf" },
950        "eds" => { aliases: [], extension: ".eds" },
951        "eex" => { aliases: [], extension: ".eex" },
952        "elisp" => { aliases: [], extension: ".el" },
953        "elixir" => { aliases: [], extension: ".ex" },
954        "elm" => { aliases: [], extension: ".elm" },
955        "elsa" => { aliases: [], extension: ".lc" },
956        "elvish" => { aliases: [], extension: ".elv" },
957        "enforce" => { aliases: [], extension: ".enforce" },
958        "erlang" => { aliases: [], extension: ".erl" },
959        "facility" => { aliases: [], extension: ".fsd" },
960        "faust" => { aliases: [], extension: ".dsp" },
961        "fennel" => { aliases: [], extension: ".fnl" },
962        "fidl" => { aliases: [], extension: ".fidl" },
963        "firrtl" => { aliases: [], extension: ".fir" },
964        "fish" => { aliases: [], extension: ".fish" },
965        "forth" => { aliases: [], extension: ".fth" },
966        "fortran" => { aliases: [], extension: ".f90" },
967        "fsharp_signature" => { aliases: [], extension: ".fsi" },
968        "func" => { aliases: [], extension: ".fc" },
969        "gap" => { aliases: [], extension: ".g" },
970        "gdscript" => { aliases: [], extension: ".gd" },
971        "gdshader" => { aliases: [], extension: ".gdshader" },
972        "gherkin" => { aliases: [], extension: ".feature" },
973        "gitattributes" => { aliases: [], extension: ".gitattributes" },
974        "gleam" => { aliases: [], extension: ".gleam" },
975        "glsl" => { aliases: [], extension: ".glsl" },
976        "gn" => { aliases: [], extension: ".gn" },
977        "gnuplot" => { aliases: [], extension: ".gp" },
978        "godot_resource" => { aliases: [], extension: ".tres" },
979        "gomod" => { aliases: [], extension: ".mod" },
980        "gotmpl" => { aliases: [], extension: ".gotmpl" },
981        "gren" => { aliases: [], extension: ".gren" },
982        "hack" => { aliases: [], extension: ".hack" },
983        "hare" => { aliases: [], extension: ".hare" },
984        "haxe" => { aliases: [], extension: ".hx" },
985        "hcl" => { aliases: [], extension: ".hcl" },
986        "heex" => { aliases: [], extension: ".heex" },
987        "hjson" => { aliases: [], extension: ".hjson" },
988        "hlsl" => { aliases: [], extension: ".hlsl" },
989        "hocon" => { aliases: [], extension: ".hocon" },
990        "hoon" => { aliases: [], extension: ".hoon" },
991        "http" => { aliases: [], extension: ".http" },
992        "hurl" => { aliases: [], extension: ".hurl" },
993        "idris" => { aliases: [], extension: ".idr" },
994        "ispc" => { aliases: [], extension: ".ispc" },
995        "jai" => { aliases: [], extension: ".jai" },
996        "janet" => { aliases: [], extension: ".janet" },
997        "jinja2" => { aliases: [], extension: ".j2" },
998        "jq" => { aliases: [], extension: ".jq" },
999        "json5" => { aliases: [], extension: ".json5" },
1000        "jsonnet" => { aliases: [], extension: ".jsonnet" },
1001        "julia" => { aliases: [], extension: ".jl" },
1002        "just" => { aliases: [], extension: ".just" },
1003        "kcl" => { aliases: [], extension: ".k" },
1004        "kdl" => { aliases: [], extension: ".kdl" },
1005        "latex" => { aliases: [], extension: ".tex" },
1006        "lean" => { aliases: [], extension: ".lean" },
1007        "ledger" => { aliases: [], extension: ".ldg" },
1008        "linkerscript" => { aliases: [], extension: ".lds" },
1009        "llvm" => { aliases: [], extension: ".ll" },
1010        "luau" => { aliases: [], extension: ".luau" },
1011        "magik" => { aliases: [], extension: ".magik" },
1012        "make" => { aliases: [], extension: ".mk" },
1013        "matlab" => { aliases: [], extension: ".matlab" },
1014        "mermaid" => { aliases: [], extension: ".mmd" },
1015        "meson" => { aliases: [], extension: ".meson" },
1016        "mlir" => { aliases: [], extension: ".mlir" },
1017        "mojo" => { aliases: [], extension: ".mojo" },
1018        "move" => { aliases: [], extension: ".move" },
1019        "nasm" => { aliases: [], extension: ".nasm" },
1020        "netlinx" => { aliases: [], extension: ".axs" },
1021        "nginx" => { aliases: [], extension: ".nginx" },
1022        "nickel" => { aliases: [], extension: ".ncl" },
1023        "nim" => { aliases: [], extension: ".nim" },
1024        "ninja" => { aliases: [], extension: ".ninja" },
1025        "nix" => { aliases: [], extension: ".nix" },
1026        "norg" => { aliases: [], extension: ".norg" },
1027        "nqc" => { aliases: [], extension: ".nqc" },
1028        "nushell" => { aliases: [], extension: ".nu" },
1029        "ocamllex" => { aliases: [], extension: ".mll" },
1030        "odin" => { aliases: [], extension: ".odin" },
1031        "openscad" => { aliases: [], extension: ".scad" },
1032        "org" => { aliases: [], extension: ".org" },
1033        "pascal" => { aliases: [], extension: ".pas" },
1034        "pem" => { aliases: [], extension: ".pem" },
1035        "pgn" => { aliases: [], extension: ".pgn" },
1036        "pkl" => { aliases: [], extension: ".pkl" },
1037        "po" => { aliases: [], extension: ".po" },
1038        "poe_filter" => { aliases: [], extension: ".filter" },
1039        "pony" => { aliases: [], extension: ".pony" },
1040        "postscript" => { aliases: [], extension: ".ps" },
1041        "prisma" => { aliases: [], extension: ".prisma" },
1042        "prolog" => { aliases: [], extension: ".pro" },
1043        "promql" => { aliases: [], extension: ".promql" },
1044        "prql" => { aliases: [], extension: ".prql" },
1045        "psv" => { aliases: [], extension: ".psv" },
1046        "puppet" => { aliases: [], extension: ".pp" },
1047        "purescript" => { aliases: [], extension: ".purs" },
1048        "ql" => { aliases: [], extension: ".ql" },
1049        "qmljs" => { aliases: [], extension: ".qml" },
1050        "racket" => { aliases: [], extension: ".rkt" },
1051        "rasi" => { aliases: [], extension: ".rasi" },
1052        "razor" => { aliases: [], extension: ".razor" },
1053        "rbs" => { aliases: [], extension: ".rbs" },
1054        "re2c" => { aliases: [], extension: ".re" },
1055        "rego" => { aliases: [], extension: ".rego" },
1056        "rescript" => { aliases: [], extension: ".res" },
1057        "robot" => { aliases: [], extension: ".robot" },
1058        "roc" => { aliases: [], extension: ".roc" },
1059        "ron" => { aliases: [], extension: ".ron" },
1060        "rst" => { aliases: [], extension: ".rst" },
1061        "rtf" => { aliases: [], extension: ".rtf" },
1062        "scheme" => { aliases: [], extension: ".scm" },
1063        "slang" => { aliases: [], extension: ".slang" },
1064        "smali" => { aliases: [], extension: ".smali" },
1065        "smalltalk" => { aliases: [], extension: ".st" },
1066        "smithy" => { aliases: [], extension: ".smithy" },
1067        "sml" => { aliases: [], extension: ".sml" },
1068        "snakemake" => { aliases: [], extension: ".smk" },
1069        "solidity" => { aliases: [], extension: ".sol" },
1070        "souffle" => { aliases: [], extension: ".dl" },
1071        "sourcepawn" => { aliases: [], extension: ".sp" },
1072        "sql_bigquery" => { aliases: [], extension: ".bq" },
1073        "squirrel" => { aliases: [], extension: ".squirrel" },
1074        "stan" => { aliases: [], extension: ".stan" },
1075        "starlark" => { aliases: [], extension: ".star" },
1076        "superhtml" => { aliases: [], extension: ".shtml" },
1077        "sway" => { aliases: [], extension: ".sw" },
1078        "systemverilog" => { aliases: [], extension: ".sv" },
1079        "tablegen" => { aliases: [], extension: ".td" },
1080        "tact" => { aliases: [], extension: ".tact" },
1081        "tcl" => { aliases: [], extension: ".tcl" },
1082        "teal" => { aliases: [], extension: ".tl" },
1083        "templ" => { aliases: [], extension: ".templ" },
1084        "tera" => { aliases: [], extension: ".tera" },
1085        "terraform" => { aliases: [], extension: ".tf" },
1086        "textproto" => { aliases: [], extension: ".textproto" },
1087        "thrift" => { aliases: [], extension: ".thrift" },
1088        "tlaplus" => { aliases: [], extension: ".tla" },
1089        "todotxt" => { aliases: [], extension: ".todotxt" },
1090        "tsv" => { aliases: [], extension: ".tsv" },
1091        "turtle" => { aliases: [], extension: ".ttl" },
1092        "twig" => { aliases: [], extension: ".twig" },
1093        "typespec" => { aliases: [], extension: ".tsp" },
1094        "typoscript" => { aliases: [], extension: ".typoscript" },
1095        "typst" => { aliases: [], extension: ".typst" },
1096        "uxntal" => { aliases: [], extension: ".tal" },
1097        "v" => { aliases: [], extension: ".v" },
1098        "vb" => { aliases: [], extension: ".vb" },
1099        "verilog" => { aliases: [], extension: ".verilog" },
1100        "vhdl" => { aliases: [], extension: ".vhdl" },
1101        "vhs" => { aliases: [], extension: ".tape" },
1102        "vrl" => { aliases: [], extension: ".vrl" },
1103        "wast" => { aliases: [], extension: ".wast" },
1104        "wat" => { aliases: [], extension: ".wat" },
1105        "wgsl" => { aliases: [], extension: ".wgsl" },
1106        "wit" => { aliases: [], extension: ".wit" },
1107        "yuck" => { aliases: [], extension: ".yuck" },
1108        "ziggy" => { aliases: [], extension: ".ziggy" },
1109    }
1110    exact_filenames {
1111        "Cargo.toml" => "cargo-manifest",
1112        "Cargo.lock" => "cargo-lock",
1113        "build.rs" => "rust-build-script",
1114        "Dockerfile" => "dockerfile",
1115        "Makefile" => "makefile"
1116    }
1117    compound_extensions { ".d.ts" => "typescript" }
1118    broad_extensions {
1119        ".py" => "python", ".pyw" => "python", ".js" => "javascript", ".jsx" => "javascript", ".ts" => "typescript", ".tsx" => "tsx", ".mjs" => "javascript", ".cjs" => "javascript", ".d.ts" => "typescript", ".java" => "java", ".c" => "c", ".cpp" => "cpp", ".h" => "h", ".hpp" => "hpp", ".cxx" => "cpp", ".cc" => "cpp", ".hxx" => "hpp", ".hh" => "hpp", ".cs" => "csharp", ".go" => "go", ".m" => "objective-c", ".mm" => "objective-c", ".rb" => "ruby", ".php" => "php", ".swift" => "swift", ".kt" => "kotlin", ".kts" => "kotlin", ".rs" => "rust", ".scala" => "scala", ".sh" => "shell", ".bash" => "shell", ".zsh" => "shell", ".ps1" => "powershell", ".psm1" => "powershell", ".psd1" => "powershell", ".bat" => "batch", ".cmd" => "batch", ".r" => "r", ".R" => "r", ".pl" => "perl", ".pm" => "perl", ".lua" => "lua", ".dart" => "dart", ".hs" => "haskell", ".ml" => "ocaml", ".mli" => "ocaml", ".fs" => "fsharp", ".fsx" => "fsharp", ".clj" => "clojure", ".cljs" => "clojure", ".vim" => "vim", ".zig" => "zig", ".zon" => "zig", ".html" => "html", ".htm" => "html", ".css" => "css", ".scss" => "css", ".sass" => "css", ".less" => "css", ".stylus" => "css", ".styl" => "css", ".md" => "markdown", ".mdx" => "markdown", ".json" => "json", ".jsonc" => "json", ".xml" => "xml", ".yml" => "yaml", ".yaml" => "yaml", ".toml" => "toml", ".toon" => "toon", ".txt" => "text", ".ini" => "config", ".cfg" => "config", ".conf" => "config", ".vue" => "vue", ".svelte" => "svelte", ".astro" => "astro", ".jsp" => "jsp", ".jspx" => "jsp", ".jspf" => "jsp", ".tag" => "jsp-tag", ".tagx" => "jsp-tag", ".gsp" => "gsp", ".properties" => "config", ".gradle" => "groovy", ".groovy" => "groovy", ".proto" => "protobuf", ".hbs" => "handlebars", ".handlebars" => "handlebars", ".ejs" => "ejs", ".pug" => "pug", ".ftl" => "freemarker", ".mustache" => "mustache", ".liquid" => "liquid", ".erb" => "erb", ".sql" => "sql", ".ddl" => "sql", ".dml" => "sql", ".mysql" => "sql", ".postgresql" => "sql", ".psql" => "sql", ".sqlite" => "sql", ".mssql" => "sql", ".oracle" => "sql", ".ora" => "sql", ".db2" => "sql", ".proc" => "sql", ".procedure" => "sql", ".func" => "sql", ".function" => "sql", ".view" => "sql", ".trigger" => "sql", ".index" => "sql", ".migration" => "sql", ".seed" => "sql", ".fixture" => "sql", ".schema" => "sql", ".cql" => "sql", ".cypher" => "sql", ".sparql" => "sql", ".gql" => "graphql", ".liquibase" => "sql", ".flyway" => "sql"
1120    }
1121    additional_extensions {
1122        ".env" => "config", ".gitignore" => "config", ".dockerignore" => "config", ".editorconfig" => "config"
1123    }
1124    content_interpreters {
1125        "python" => { language: "python", version_suffix: true },
1126        "pythonw" => { language: "python", version_suffix: true },
1127        "node" => { language: "javascript", version_suffix: false },
1128        "deno" => { language: "javascript", version_suffix: false },
1129        "powershell" => { language: "powershell", version_suffix: false },
1130        "pwsh" => { language: "powershell", version_suffix: false },
1131        "ruby" => { language: "ruby", version_suffix: true },
1132        "perl" => { language: "perl", version_suffix: true },
1133        "lua" => { language: "lua", version_suffix: true },
1134        "rscript" => { language: "r", version_suffix: false },
1135        "sh" => { language: "shell", version_suffix: false },
1136        "bash" => { language: "shell", version_suffix: false },
1137        "dash" => { language: "shell", version_suffix: false },
1138        "ash" => { language: "shell", version_suffix: false },
1139        "zsh" => { language: "shell", version_suffix: false },
1140        "ksh" => { language: "shell", version_suffix: false },
1141        "mksh" => { language: "shell", version_suffix: false },
1142        "fish" => { language: "shell", version_suffix: false }
1143    }
1144}
1145
1146/// Return parser coverage metadata for a detected language family.
1147#[must_use]
1148pub fn language_spec(language: &str) -> Option<&'static LanguageSpec> {
1149    let canonical = canonical_language_id(language)?;
1150    LANGUAGE_SPECS
1151        .iter()
1152        .find(|spec| spec.language == canonical)
1153}
1154
1155/// Return the accepted capability row for a canonical ID or alias.
1156#[must_use]
1157pub fn language_capability(language: &str) -> Option<&'static LanguageCapability> {
1158    static BY_ID: OnceLock<BTreeMap<&'static str, &'static LanguageCapability>> = OnceLock::new();
1159    let canonical = canonical_language_id(language)?;
1160    BY_ID
1161        .get_or_init(|| {
1162            LANGUAGE_CAPABILITIES
1163                .iter()
1164                .map(|capability| (capability.id, capability))
1165                .collect()
1166        })
1167        .get(canonical)
1168        .copied()
1169}
1170
1171/// Return all accepted language rows as generated documentation inputs.
1172#[must_use]
1173pub const fn language_documentation_rows() -> &'static [LanguageCapability] {
1174    LANGUAGE_CAPABILITIES
1175}
1176
1177/// Return the built-in Tree-sitter grammar owner for a language.
1178#[must_use]
1179pub fn tree_sitter_grammar(language: &str) -> Option<TreeSitterGrammar> {
1180    match language_capability(language)?.symbol_parser {
1181        SymbolParserOwner::TreeSitter(grammar) => Some(grammar),
1182        SymbolParserOwner::CargoManifest
1183        | SymbolParserOwner::Vue
1184        | SymbolParserOwner::PowerShell
1185        | SymbolParserOwner::Fallback
1186        | SymbolParserOwner::Unavailable => None,
1187    }
1188}
1189
1190/// Return all canonical language IDs backed by built-in Tree-sitter grammars.
1191#[must_use]
1192pub fn builtin_tree_sitter_language_ids() -> &'static [&'static str] {
1193    static IDS: OnceLock<Box<[&'static str]>> = OnceLock::new();
1194    IDS.get_or_init(|| {
1195        LANGUAGE_CAPABILITIES
1196            .iter()
1197            .filter_map(|capability| {
1198                matches!(capability.symbol_parser, SymbolParserOwner::TreeSitter(_))
1199                    .then_some(capability.id)
1200            })
1201            .collect::<Vec<_>>()
1202            .into_boxed_slice()
1203    })
1204}
1205
1206/// Return whether a path ends in a declared compound language extension.
1207#[must_use]
1208pub fn compound_language_extension(path: &str) -> Option<&'static str> {
1209    let file_name = path.rsplit(['/', '\\']).next().unwrap_or(path);
1210    let lower = file_name.to_ascii_lowercase();
1211    COMPOUND_EXTENSION_RULES
1212        .iter()
1213        .find(|rule| lower.ends_with(rule.value))
1214        .map(|rule| rule.value)
1215}
1216
1217/// Return the normalized extension used by the scanner and detector.
1218#[must_use]
1219pub fn normalized_language_extension(path: &Path) -> Option<String> {
1220    let file_name = path.file_name()?.to_string_lossy();
1221    if let Some(compound) = compound_language_extension(&file_name) {
1222        return Some(compound.to_string());
1223    }
1224    path.extension()
1225        .map(|extension| format!(".{}", extension.to_string_lossy().to_lowercase()))
1226}
1227
1228/// Detect one language through the typed precedence contract.
1229///
1230/// # Errors
1231///
1232/// Returns an error when an explicit override is not a known canonical ID or alias.
1233pub fn detect_language_request(
1234    request: LanguageDetectionRequest<'_>,
1235) -> Result<Option<LanguageDetection>, LanguageDetectionError> {
1236    if let Some(explicit) = request.explicit_override {
1237        let Some(language) = canonical_language_id(explicit) else {
1238            return Err(LanguageDetectionError {
1239                requested: explicit.to_string(),
1240            });
1241        };
1242        return Ok(Some(LanguageDetection {
1243            language,
1244            reason: LanguageDetectionReason::ExplicitOverride,
1245        }));
1246    }
1247
1248    let file_name = request
1249        .path
1250        .rsplit(['/', '\\'])
1251        .next()
1252        .unwrap_or(request.path);
1253    if let Some(language) = exact_filename_language(file_name) {
1254        return Ok(Some(LanguageDetection {
1255            language,
1256            reason: LanguageDetectionReason::ExactFilename,
1257        }));
1258    }
1259
1260    let lower_file_name = file_name.to_ascii_lowercase();
1261    if let Some(rule) = COMPOUND_EXTENSION_RULES
1262        .iter()
1263        .find(|rule| lower_file_name.ends_with(rule.value))
1264    {
1265        return Ok(Some(LanguageDetection {
1266            language: rule.language,
1267            reason: LanguageDetectionReason::CompoundExtension,
1268        }));
1269    }
1270
1271    let normalized_extension = request.extension.map(str::to_ascii_lowercase).or_else(|| {
1272        file_name
1273            .rsplit_once('.')
1274            .map(|(_, extension)| format!(".{}", extension.to_ascii_lowercase()))
1275    });
1276    if let Some(extension) = normalized_extension
1277        && let Some(language) = extension_language(&extension)
1278    {
1279        return Ok(Some(LanguageDetection {
1280            language,
1281            reason: LanguageDetectionReason::Extension,
1282        }));
1283    }
1284
1285    Ok(
1286        content_dialect_language(request.content_prefix).map(|language| LanguageDetection {
1287            language,
1288            reason: LanguageDetectionReason::ContentDialect,
1289        }),
1290    )
1291}
1292
1293/// Detect a language or file family from an extension.
1294#[must_use]
1295pub fn detect_language(extension: Option<&str>) -> Option<String> {
1296    detect_language_request(LanguageDetectionRequest::new("", extension))
1297        .ok()
1298        .flatten()
1299        .map(|detected| detected.language.to_string())
1300}
1301
1302/// Detect a language or file family from a path plus extension.
1303#[must_use]
1304pub fn detect_language_for_path(path: &str, extension: Option<&str>) -> Option<String> {
1305    detect_language_request(LanguageDetectionRequest::new(path, extension))
1306        .ok()
1307        .flatten()
1308        .map(|detected| detected.language.to_string())
1309}
1310
1311/// Apply bounded registry-owned shebang rules after filename/extension rules miss.
1312fn content_dialect_language(content_prefix: Option<&[u8]>) -> Option<&'static str> {
1313    let prefix = content_prefix?;
1314    let bounded = &prefix[..prefix.len().min(LANGUAGE_CONTENT_DETECTION_MAX_BYTES)];
1315    let line_end = bounded
1316        .iter()
1317        .position(|byte| *byte == b'\n')
1318        .unwrap_or(bounded.len());
1319    let first_line = std::str::from_utf8(&bounded[..line_end])
1320        .ok()?
1321        .trim_end_matches('\r');
1322    let shebang = first_line.strip_prefix("#!")?.trim();
1323    let interpreter = shebang_interpreter(shebang)?;
1324    CONTENT_DIALECT_RULES
1325        .iter()
1326        .find(|rule| content_interpreter_matches(&interpreter, rule))
1327        .map(|rule| rule.language)
1328}
1329
1330/// Return the lowercase interpreter basename from a bounded shebang command.
1331fn shebang_interpreter(shebang: &str) -> Option<String> {
1332    let mut tokens = shebang.split_ascii_whitespace();
1333    let first = tokens.next()?;
1334    let first_basename = command_basename(first);
1335    let selected = if first_basename.eq_ignore_ascii_case("env") {
1336        tokens.find(|token| !token.starts_with('-') && !token.contains('='))?
1337    } else if first_basename.eq_ignore_ascii_case("busybox") {
1338        tokens.next()?
1339    } else {
1340        first
1341    };
1342    let normalized = command_basename(selected).to_ascii_lowercase();
1343    Some(
1344        normalized
1345            .strip_suffix(".exe")
1346            .unwrap_or(&normalized)
1347            .to_string(),
1348    )
1349}
1350
1351/// Return a command basename for Unix and Windows-style shebang paths.
1352fn command_basename(command: &str) -> &str {
1353    command.rsplit(['/', '\\']).next().unwrap_or(command)
1354}
1355
1356/// Match one normalized interpreter against its declared exact/versioned rule.
1357fn content_interpreter_matches(interpreter: &str, rule: &LanguageContentRule) -> bool {
1358    if interpreter == rule.interpreter {
1359        return true;
1360    }
1361    rule.allow_version_suffix
1362        && interpreter
1363            .strip_prefix(rule.interpreter)
1364            .is_some_and(|suffix| {
1365                !suffix.is_empty()
1366                    && suffix.split('.').all(|segment| {
1367                        !segment.is_empty() && segment.bytes().all(|byte| byte.is_ascii_digit())
1368                    })
1369            })
1370}
1371
1372/// Count levels for one capability axis.
1373#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Serialize)]
1374pub struct CapabilityLevelCounts {
1375    /// Rows without this capability.
1376    pub unavailable: usize,
1377    /// Rows with conservative fallback behavior.
1378    pub fallback: usize,
1379    /// Rows with supported behavior.
1380    pub supported: usize,
1381}
1382
1383/// Derived accepted language capability counts.
1384#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Serialize)]
1385pub struct LanguageCapabilityCounts {
1386    /// Canonical accepted rows; aliases and extensions do not add to this count.
1387    pub accepted: usize,
1388    /// Rows owned entirely by the default-core runtime.
1389    pub built_in: usize,
1390    /// Broad rows eligible for the separately verified optional pack.
1391    pub optional_candidates: usize,
1392    /// Detection tier counts.
1393    pub detected: CapabilityLevelCounts,
1394    /// Parsing tier counts.
1395    pub parsed: CapabilityLevelCounts,
1396    /// Symbol tier counts.
1397    pub symbols: CapabilityLevelCounts,
1398    /// Semantic tier counts.
1399    pub semantic: CapabilityLevelCounts,
1400    /// Benchmark tier counts.
1401    pub benchmarked: CapabilityLevelCounts,
1402}
1403
1404/// One content-free settings and generated-documentation capability row.
1405#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
1406pub struct LanguageCapabilityReportRow {
1407    /// Stable canonical language or file-family identifier.
1408    pub id: &'static str,
1409    /// Compatibility aliases; these never add to capability counts.
1410    pub aliases: &'static [&'static str],
1411    /// Exact filename detector rules owned by this row.
1412    pub exact_filenames: Vec<&'static str>,
1413    /// Compound extension detector rules owned by this row.
1414    pub compound_extensions: Vec<&'static str>,
1415    /// Ordinary extension detector rules owned by this row.
1416    pub extensions: Vec<&'static str>,
1417    /// Shebang interpreter basenames owned by this row.
1418    pub content_interpreters: Vec<&'static str>,
1419    /// Compatibility parser tier.
1420    pub parser_support: LanguageParserSupport,
1421    /// Closed symbol parser owner.
1422    pub symbol_parser: SymbolParserOwner,
1423    /// Closed project-wide semantic provider owner.
1424    pub semantic_provider: SemanticProviderOwner,
1425    /// Optional bounded host-to-embedded provider pairing.
1426    pub embedded_language: Option<EmbeddedLanguageCapability>,
1427    /// Optional structural summary owner.
1428    pub structural_summary: Option<StructuralSummaryOwner>,
1429    /// Optional broad-parser pack owner.
1430    pub optional_pack: Option<&'static str>,
1431    /// Currently achieved independent support.
1432    pub support: LanguageCapabilitySupport,
1433    /// Accepted minimum support.
1434    pub accepted_minimum: LanguageCapabilitySupport,
1435    /// Natural positive and negative detector fixtures.
1436    pub fixtures: LanguageCapabilityFixtures,
1437    /// Exact provenance package or catalog identity.
1438    pub provenance_source: &'static str,
1439    /// Exact provenance package or catalog version.
1440    pub provenance_version: &'static str,
1441    /// License applying to the declared implementation or catalog metadata.
1442    pub provenance_license: &'static str,
1443    /// Required release platforms.
1444    pub required_platforms: RequiredPlatformSet,
1445}
1446
1447/// Pinned broad-language catalog identity used for detection candidates.
1448#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
1449pub struct LanguageCatalogReport {
1450    /// Catalog package identity.
1451    pub name: &'static str,
1452    /// Exact catalog release.
1453    pub version: &'static str,
1454    /// Exact catalog source revision.
1455    pub revision: &'static str,
1456    /// License applying to the catalog metadata, not every grammar subtree.
1457    pub metadata_license: &'static str,
1458    /// Minimum additional accepted grammars required for the broad-pack claim.
1459    pub minimum_additional_grammars: usize,
1460}
1461
1462/// Content-free language registry settings projection.
1463#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
1464pub struct LanguageRegistryReport {
1465    /// Registry schema version.
1466    pub registry_version: u32,
1467    /// Accepted capability-set version.
1468    pub accepted_set_version: u32,
1469    /// Detector precedence and content-matching policy version.
1470    pub detection_policy_version: u32,
1471    /// Digest of runtime rows and detection rules.
1472    pub registry_digest: String,
1473    /// Digest of accepted minimums.
1474    pub accepted_set_digest: String,
1475    /// Digest of direct/embedded provider ownership and resolution-family truth.
1476    pub semantic_provider_digest: String,
1477    /// Derived independent support counts.
1478    pub counts: LanguageCapabilityCounts,
1479    /// Pinned catalog identity for optional capability candidates.
1480    pub optional_catalog: LanguageCatalogReport,
1481}
1482
1483/// Build a content-free settings/documentation report from the registry authority.
1484#[must_use]
1485pub fn language_registry_report() -> LanguageRegistryReport {
1486    LanguageRegistryReport {
1487        registry_version: LANGUAGE_CAPABILITY_REGISTRY_VERSION,
1488        accepted_set_version: ACCEPTED_LANGUAGE_CAPABILITY_SET_VERSION,
1489        detection_policy_version: LANGUAGE_DETECTION_POLICY_VERSION,
1490        registry_digest: language_registry_digest(),
1491        accepted_set_digest: accepted_language_capability_digest(),
1492        semantic_provider_digest: semantic_provider_digest(),
1493        counts: language_capability_counts(),
1494        optional_catalog: LanguageCatalogReport {
1495            name: OPTIONAL_GRAMMAR_CATALOG,
1496            version: OPTIONAL_GRAMMAR_CATALOG_VERSION,
1497            revision: OPTIONAL_GRAMMAR_CATALOG_RELEASE_REVISION,
1498            metadata_license: "MIT",
1499            minimum_additional_grammars: OPTIONAL_PACK_MINIMUM_ADDITIONAL_GRAMMARS,
1500        },
1501    }
1502}
1503
1504/// Build the complete content-free capability matrix from registry authority.
1505#[must_use]
1506pub fn language_capability_report_rows() -> Vec<LanguageCapabilityReportRow> {
1507    LANGUAGE_CAPABILITIES
1508        .iter()
1509        .map(|capability| LanguageCapabilityReportRow {
1510            id: capability.id,
1511            aliases: capability.aliases,
1512            exact_filenames: rules_for_language(EXACT_FILENAME_RULES, capability.id),
1513            compound_extensions: rules_for_language(COMPOUND_EXTENSION_RULES, capability.id),
1514            extensions: rules_for_language(EXTENSION_RULES, capability.id),
1515            content_interpreters: CONTENT_DIALECT_RULES
1516                .iter()
1517                .filter_map(|rule| (rule.language == capability.id).then_some(rule.interpreter))
1518                .collect(),
1519            parser_support: capability.parser_support,
1520            symbol_parser: capability.symbol_parser,
1521            semantic_provider: capability.semantic_provider,
1522            embedded_language: capability.embedded_language,
1523            structural_summary: capability.structural_summary,
1524            optional_pack: capability.optional_pack,
1525            support: capability.support,
1526            accepted_minimum: capability.accepted_minimum,
1527            fixtures: capability.fixtures,
1528            provenance_source: capability.provenance.source(),
1529            provenance_version: capability.provenance.version(),
1530            provenance_license: capability.provenance.license(),
1531            required_platforms: capability.required_platforms,
1532        })
1533        .collect()
1534}
1535
1536/// Render the checked-in public language support matrix from registry authority.
1537///
1538/// # Errors
1539///
1540/// Returns a formatting error if writing to the owned `String` fails.
1541pub fn render_language_support_markdown() -> Result<String, fmt::Error> {
1542    let report = language_registry_report();
1543    let capability_rows = language_capability_report_rows();
1544    let mut output = String::new();
1545    output.push_str("# ProjectAtlas Language Support\n\n");
1546    output.push_str(
1547        "This document is generated from the versioned Rust language capability registry. \
1548Do not edit the capability table or totals by hand. Canonical rows count once; aliases and \
1549extensions never increase a capability total.\n\n",
1550    );
1551    write!(
1552        &mut output,
1553        "Registry version: `{}`. Accepted capability-set version: `{}`. Detection policy \
1554version: `{}`. \
1555Registry digest: `{}`. Accepted-set digest: `{}`. Semantic-provider digest: `{}`.\n\n",
1556        report.registry_version,
1557        report.accepted_set_version,
1558        report.detection_policy_version,
1559        report.registry_digest,
1560        report.accepted_set_digest,
1561        report.semantic_provider_digest
1562    )?;
1563    write!(
1564        &mut output,
1565        "Optional catalog input: `{}@{}` revision `{}` under `{}` metadata license. \
1566This catalog identity is not a grammar-license or runtime-support claim.\n\n",
1567        report.optional_catalog.name,
1568        report.optional_catalog.version,
1569        report.optional_catalog.revision,
1570        report.optional_catalog.metadata_license
1571    )?;
1572    write!(
1573        &mut output,
1574        "The registry contains **{}** canonical rows: **{}** default-core rows and **{}** \
1575optional-pack candidates. Detection is supported for {} rows. Parsing is supported for {}, \
1576fallback for {}, and unavailable for {}. Symbols are supported for {}, fallback for {}, and \
1577unavailable for {}. Semantic resolution and benchmark coverage are reported independently.\n\n",
1578        report.counts.accepted,
1579        report.counts.built_in,
1580        report.counts.optional_candidates,
1581        report.counts.detected.supported,
1582        report.counts.parsed.supported,
1583        report.counts.parsed.fallback,
1584        report.counts.parsed.unavailable,
1585        report.counts.symbols.supported,
1586        report.counts.symbols.fallback,
1587        report.counts.symbols.unavailable
1588    )?;
1589    output.push_str(
1590        "Rows marked `broad-parser` are detected and, when explicitly admitted to the scan \
1591policy, remain usable through the conservative default-core fallback while the optional pack is \
1592absent. Catalog recognition alone does not add these extensions to the default scan surface. The \
1593pinned catalog is provenance for detection metadata only. A row becomes grammar-backed parsed \
1594support only after its exact \
1595grammar binary, subtree license, ABI/export, fixtures, and every accepted optional-pack target \
1596pass the separate acceptance gates. The v0.4 optional-pack targets are Linux x86-64 and Windows \
1597x86-64; macOS keeps the full built-in surface and reports `unsupported_containment` for optional-\
1598pack activation. Built-in owners always retain precedence.\n\n",
1599    );
1600    output.push_str(
1601        "Broad candidate rows are admitted only when the pinned catalog supplies a stable \
1602canonical grammar identity and at least one ordinary extension that does not conflict with an \
1603already accepted detector owner. Extensionless, ambiguous, duplicate, pseudo, or conflicting \
1604catalog entries remain unadvertised until a separate deterministic rule and evidence exist.\n\n",
1605    );
1606    output.push_str(
1607        "| Language | Aliases | Detection rules | Parser owner | Parsed | Symbols | Semantic | Embedded source | Benchmarked | Optional pack | Provenance | License |\n",
1608    );
1609    output.push_str("| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |\n");
1610    for row in &capability_rows {
1611        let rules = detector_rule_summary(row);
1612        let aliases = list_or_dash(row.aliases.iter().copied());
1613        let optional_pack = row.optional_pack.unwrap_or("—");
1614        let provenance = format!("{}@{}", row.provenance_source, row.provenance_version);
1615        writeln!(
1616            &mut output,
1617            "| `{}` | {} | {} | {} | {} | {} | {} | {} | {} | {} | `{}` | `{}` |",
1618            row.id,
1619            aliases,
1620            rules,
1621            symbol_parser_label(row.symbol_parser),
1622            row.support.parsed.as_str(),
1623            row.support.symbols.as_str(),
1624            semantic_support_label(row.support.semantic, row.semantic_provider),
1625            embedded_language_label(row.embedded_language),
1626            row.support.benchmarked.as_str(),
1627            optional_pack,
1628            provenance,
1629            row.provenance_license
1630        )?;
1631    }
1632    crate::support_catalog::append_support_catalog_markdown(&mut output)?;
1633    Ok(output)
1634}
1635
1636/// Render all detector rules for one documentation row.
1637fn detector_rule_summary(row: &LanguageCapabilityReportRow) -> String {
1638    let mut rules = Vec::new();
1639    rules.extend(
1640        row.exact_filenames
1641            .iter()
1642            .map(|value| format!("exact `{value}`")),
1643    );
1644    rules.extend(
1645        row.compound_extensions
1646            .iter()
1647            .map(|value| format!("compound `{value}`")),
1648    );
1649    rules.extend(row.extensions.iter().map(|value| format!("`{value}`")));
1650    rules.extend(
1651        row.content_interpreters
1652            .iter()
1653            .map(|value| format!("shebang `{value}`")),
1654    );
1655    if rules.is_empty() {
1656        "—".to_string()
1657    } else {
1658        rules.join(", ")
1659    }
1660}
1661
1662/// Render one closed symbol-parser owner without duplicating selection policy.
1663fn symbol_parser_label(owner: SymbolParserOwner) -> String {
1664    match owner {
1665        SymbolParserOwner::TreeSitter(grammar) => {
1666            format!("{}@{}", grammar.package(), grammar.version())
1667        }
1668        SymbolParserOwner::CargoManifest => "projectatlas:cargo-manifest".to_string(),
1669        SymbolParserOwner::Vue => "projectatlas:vue".to_string(),
1670        SymbolParserOwner::PowerShell => "projectatlas:powershell".to_string(),
1671        SymbolParserOwner::Fallback => "projectatlas:fallback".to_string(),
1672        SymbolParserOwner::Unavailable => "unavailable".to_string(),
1673    }
1674}
1675
1676/// Render semantic support with its closed owner when available.
1677fn semantic_support_label(support: CapabilitySupportLevel, owner: SemanticProviderOwner) -> String {
1678    match owner {
1679        SemanticProviderOwner::Unavailable => support.as_str().to_string(),
1680        _ => format!("{} ({})", support.as_str(), owner.as_str()),
1681    }
1682}
1683
1684/// Render one accepted host-to-embedded provider pairing.
1685fn embedded_language_label(capability: Option<EmbeddedLanguageCapability>) -> String {
1686    capability.map_or_else(
1687        || "—".to_string(),
1688        |capability| {
1689            format!(
1690                "{} → {}",
1691                capability.host_kind.as_str(),
1692                capability.semantic_provider.as_str()
1693            )
1694        },
1695    )
1696}
1697
1698/// Join documentation values or render an explicit empty marker.
1699fn list_or_dash<'a>(values: impl Iterator<Item = &'a str>) -> String {
1700    let values = values.collect::<Vec<_>>();
1701    if values.is_empty() {
1702        "—".to_string()
1703    } else {
1704        values
1705            .into_iter()
1706            .map(|value| format!("`{value}`"))
1707            .collect::<Vec<_>>()
1708            .join(", ")
1709    }
1710}
1711
1712/// Collect rule values owned by one canonical language row.
1713fn rules_for_language(rules: &[LanguageDetectionRule], language: &str) -> Vec<&'static str> {
1714    rules
1715        .iter()
1716        .filter_map(|rule| (rule.language == language).then_some(rule.value))
1717        .collect()
1718}
1719
1720/// Derive independent support counts without mutable literals.
1721#[must_use]
1722pub fn language_capability_counts() -> LanguageCapabilityCounts {
1723    let mut counts = LanguageCapabilityCounts {
1724        accepted: LANGUAGE_CAPABILITIES.len(),
1725        ..LanguageCapabilityCounts::default()
1726    };
1727    for capability in LANGUAGE_CAPABILITIES {
1728        if capability.optional_pack.is_some() {
1729            counts.optional_candidates += 1;
1730        } else {
1731            counts.built_in += 1;
1732        }
1733        increment_level(&mut counts.detected, capability.support.detected);
1734        increment_level(&mut counts.parsed, capability.support.parsed);
1735        increment_level(&mut counts.symbols, capability.support.symbols);
1736        increment_level(&mut counts.semantic, capability.support.semantic);
1737        increment_level(&mut counts.benchmarked, capability.support.benchmarked);
1738    }
1739    counts
1740}
1741
1742/// Increment one derived support-level bucket.
1743fn increment_level(counts: &mut CapabilityLevelCounts, level: CapabilitySupportLevel) {
1744    match level {
1745        CapabilitySupportLevel::Unavailable => counts.unavailable += 1,
1746        CapabilitySupportLevel::Fallback => counts.fallback += 1,
1747        CapabilitySupportLevel::Supported => counts.supported += 1,
1748    }
1749}
1750
1751/// Return the deterministic digest of achieved registry truth and detection rules.
1752#[must_use]
1753pub fn language_registry_digest() -> String {
1754    hash_language_registry(false)
1755}
1756
1757/// Return the deterministic digest of accepted minimum capability truth.
1758#[must_use]
1759pub fn accepted_language_capability_digest() -> String {
1760    hash_language_registry(true)
1761}
1762
1763/// Return the deterministic digest of semantic-provider ownership.
1764///
1765/// The projection is deliberately narrower than the complete language registry:
1766/// it binds only provider ownership, canonical resolution families, embedded-host
1767/// admission, and achieved semantic strength. Relation behavior remains owned by
1768/// `projectatlas-symbols` until the accepted relation-family inventory lands.
1769#[must_use]
1770pub fn semantic_provider_digest() -> String {
1771    let mut hasher = Hasher::new();
1772    hasher.update(&SEMANTIC_PROVIDER_CONTRACT_VERSION.to_le_bytes());
1773    for capability in LANGUAGE_CAPABILITIES {
1774        let effective_provider = capability.effective_semantic_provider();
1775        if effective_provider.is_none() && capability.embedded_language.is_none() {
1776            continue;
1777        }
1778        hash_value(&mut hasher, capability.id);
1779        hash_value(&mut hasher, capability.semantic_provider.as_str());
1780        hash_value(
1781            &mut hasher,
1782            capability
1783                .semantic_provider
1784                .resolution_family()
1785                .unwrap_or("unavailable"),
1786        );
1787        hash_value(
1788            &mut hasher,
1789            effective_provider
1790                .and_then(SemanticProviderOwner::resolution_family)
1791                .unwrap_or("unavailable"),
1792        );
1793        if let Some(embedded) = capability.embedded_language {
1794            hash_value(&mut hasher, embedded.host_kind.as_str());
1795            hash_value(&mut hasher, embedded.semantic_provider.as_str());
1796            hash_value(
1797                &mut hasher,
1798                embedded
1799                    .semantic_provider
1800                    .resolution_family()
1801                    .unwrap_or("unavailable"),
1802            );
1803        } else {
1804            hash_value(&mut hasher, "no-embedded-provider");
1805        }
1806        hash_value(&mut hasher, capability.support.semantic.as_str());
1807    }
1808    hasher.finalize().to_hex().to_string()
1809}
1810
1811/// Hash either achieved runtime truth or accepted minimums deterministically.
1812fn hash_language_registry(accepted_only: bool) -> String {
1813    hash_language_registry_with_content_rules(accepted_only, CONTENT_DIALECT_RULES)
1814}
1815
1816/// Hash registry truth with an explicit content-rule projection for validation tests.
1817fn hash_language_registry_with_content_rules(
1818    accepted_only: bool,
1819    content_rules: &[LanguageContentRule],
1820) -> String {
1821    let mut hasher = Hasher::new();
1822    hasher.update(&LANGUAGE_CAPABILITY_REGISTRY_VERSION.to_le_bytes());
1823    hasher.update(&ACCEPTED_LANGUAGE_CAPABILITY_SET_VERSION.to_le_bytes());
1824    hasher.update(&LANGUAGE_DETECTION_POLICY_VERSION.to_le_bytes());
1825    hasher.update(&(LANGUAGE_CONTENT_DETECTION_MAX_BYTES as u64).to_le_bytes());
1826    hash_value(&mut hasher, OPTIONAL_GRAMMAR_CATALOG);
1827    hash_value(&mut hasher, OPTIONAL_GRAMMAR_CATALOG_VERSION);
1828    hash_value(&mut hasher, OPTIONAL_GRAMMAR_CATALOG_RELEASE_REVISION);
1829    hasher.update(&(OPTIONAL_PACK_MINIMUM_ADDITIONAL_GRAMMARS as u64).to_le_bytes());
1830    for capability in LANGUAGE_CAPABILITIES {
1831        hash_value(&mut hasher, capability.id);
1832        for alias in capability.aliases {
1833            hash_value(&mut hasher, alias);
1834        }
1835        let support = if accepted_only {
1836            capability.accepted_minimum
1837        } else {
1838            capability.support
1839        };
1840        hash_support(&mut hasher, support);
1841        hash_value(&mut hasher, &format!("{:?}", capability.parser_support));
1842        hash_value(&mut hasher, &format!("{:?}", capability.symbol_parser));
1843        hash_value(&mut hasher, &format!("{:?}", capability.semantic_provider));
1844        hash_value(&mut hasher, &format!("{:?}", capability.embedded_language));
1845        hash_value(&mut hasher, &format!("{:?}", capability.structural_summary));
1846        hash_value(
1847            &mut hasher,
1848            capability.optional_pack.unwrap_or("default-core"),
1849        );
1850        hash_value(&mut hasher, capability.fixtures.positive_path);
1851        hash_value(&mut hasher, capability.fixtures.negative_path);
1852        hash_value(&mut hasher, capability.provenance.source());
1853        hash_value(&mut hasher, capability.provenance.version());
1854        hash_value(&mut hasher, capability.provenance.license());
1855        hash_value(&mut hasher, &format!("{:?}", capability.required_platforms));
1856    }
1857    for (kind, rules) in [
1858        ("exact", EXACT_FILENAME_RULES),
1859        ("compound", COMPOUND_EXTENSION_RULES),
1860        ("extension", EXTENSION_RULES),
1861    ] {
1862        for rule in rules {
1863            hash_value(&mut hasher, kind);
1864            hash_value(&mut hasher, rule.value);
1865            hash_value(&mut hasher, rule.language);
1866        }
1867    }
1868    for rule in content_rules {
1869        hash_value(&mut hasher, "content-interpreter");
1870        hash_value(&mut hasher, rule.interpreter);
1871        hasher.update(&[u8::from(rule.allow_version_suffix)]);
1872        hash_value(&mut hasher, rule.language);
1873    }
1874    hasher.finalize().to_hex().to_string()
1875}
1876
1877/// Hash the five independent support axes.
1878fn hash_support(hasher: &mut Hasher, support: LanguageCapabilitySupport) {
1879    hasher.update(&[
1880        support.detected as u8,
1881        support.parsed as u8,
1882        support.symbols as u8,
1883        support.semantic as u8,
1884        support.benchmarked as u8,
1885    ]);
1886}
1887
1888/// Hash one length-delimited registry string.
1889fn hash_value(hasher: &mut Hasher, value: &str) {
1890    hasher.update(&(value.len() as u64).to_le_bytes());
1891    hasher.update(value.as_bytes());
1892}
1893
1894/// Validate the accepted registry and all generated projections.
1895///
1896/// # Errors
1897///
1898/// Returns the first deterministic conflict or incomplete accepted row.
1899pub fn validate_language_registry() -> Result<(), LanguageRegistryError> {
1900    let mut canonical = BTreeSet::new();
1901    let mut names = BTreeMap::new();
1902    for capability in LANGUAGE_CAPABILITIES {
1903        if capability.id.is_empty() {
1904            return Err(LanguageRegistryError::new("empty canonical language ID"));
1905        }
1906        if !canonical.insert(capability.id) {
1907            return Err(LanguageRegistryError::new(format!(
1908                "duplicate canonical language ID {:?}",
1909                capability.id
1910            )));
1911        }
1912        register_owner(&mut names, capability.id, capability.id, "language name")?;
1913        for alias in capability.aliases {
1914            register_owner(&mut names, alias, capability.id, "language alias")?;
1915        }
1916        if !capability.support.meets(capability.accepted_minimum) {
1917            return Err(LanguageRegistryError::new(format!(
1918                "language {:?} is weaker than accepted capability set version {}",
1919                capability.id, ACCEPTED_LANGUAGE_CAPABILITY_SET_VERSION
1920            )));
1921        }
1922        if capability.fixtures.positive_path.is_empty()
1923            || capability.fixtures.negative_path.is_empty()
1924            || capability.fixtures.positive_path == capability.fixtures.negative_path
1925        {
1926            return Err(LanguageRegistryError::new(format!(
1927                "language {:?} lacks distinct natural positive and negative fixtures",
1928                capability.id
1929            )));
1930        }
1931        if capability.provenance.license().is_empty() {
1932            return Err(LanguageRegistryError::new(format!(
1933                "language {:?} lacks a provenance license input",
1934                capability.id
1935            )));
1936        }
1937        if capability
1938            .optional_pack
1939            .is_some_and(|owner| owner != BROAD_PARSER_PACK_ID)
1940        {
1941            return Err(LanguageRegistryError::new(format!(
1942                "optional language {:?} is assigned to unknown pack owner {:?}",
1943                capability.id, capability.optional_pack
1944            )));
1945        }
1946        if capability.optional_pack.is_some()
1947            && capability.provenance != CapabilityProvenance::PinnedOptionalCatalog
1948        {
1949            return Err(LanguageRegistryError::new(format!(
1950                "optional language {:?} lacks pinned catalog provenance",
1951                capability.id
1952            )));
1953        }
1954        if capability.optional_pack.is_none()
1955            && capability.provenance == CapabilityProvenance::PinnedOptionalCatalog
1956        {
1957            return Err(LanguageRegistryError::new(format!(
1958                "default-core language {:?} incorrectly claims optional catalog provenance",
1959                capability.id
1960            )));
1961        }
1962        match (capability.support.symbols, capability.symbol_parser) {
1963            (CapabilitySupportLevel::Unavailable, SymbolParserOwner::Unavailable)
1964            | (CapabilitySupportLevel::Fallback, SymbolParserOwner::Fallback)
1965            | (
1966                CapabilitySupportLevel::Supported,
1967                SymbolParserOwner::TreeSitter(_)
1968                | SymbolParserOwner::CargoManifest
1969                | SymbolParserOwner::Vue
1970                | SymbolParserOwner::PowerShell,
1971            ) => {}
1972            (support, owner) => {
1973                return Err(LanguageRegistryError::new(format!(
1974                    "language {:?} advertises symbol support {support:?} with incompatible owner {owner:?}",
1975                    capability.id
1976                )));
1977            }
1978        }
1979        match (capability.support.semantic, capability.semantic_provider) {
1980            (CapabilitySupportLevel::Unavailable, SemanticProviderOwner::Unavailable)
1981            | (
1982                CapabilitySupportLevel::Supported,
1983                SemanticProviderOwner::Rust
1984                | SemanticProviderOwner::EcmaScript
1985                | SemanticProviderOwner::Python
1986                | SemanticProviderOwner::Cargo,
1987            ) => {}
1988            (support, owner) => {
1989                return Err(LanguageRegistryError::new(format!(
1990                    "language {:?} advertises semantic support {support:?} with incompatible owner {owner:?}",
1991                    capability.id
1992                )));
1993            }
1994        }
1995        if let Some(embedded) = capability.embedded_language {
1996            if embedded.semantic_provider == SemanticProviderOwner::Unavailable {
1997                return Err(LanguageRegistryError::new(format!(
1998                    "embedded language host {:?} lacks a semantic provider owner",
1999                    capability.id
2000                )));
2001            }
2002            if capability.support.semantic != CapabilitySupportLevel::Unavailable
2003                || capability.semantic_provider != SemanticProviderOwner::Unavailable
2004            {
2005                return Err(LanguageRegistryError::new(format!(
2006                    "embedded language host {:?} conflates host and embedded semantic support",
2007                    capability.id
2008                )));
2009            }
2010        }
2011        if capability.optional_pack.is_some()
2012            && (capability.semantic_provider != SemanticProviderOwner::Unavailable
2013                || capability.embedded_language.is_some())
2014        {
2015            return Err(LanguageRegistryError::new(format!(
2016                "optional language {:?} advertises unvalidated semantic capability",
2017                capability.id
2018            )));
2019        }
2020    }
2021
2022    validate_rules("exact filename", EXACT_FILENAME_RULES, &canonical, false)?;
2023    validate_rules(
2024        "compound extension",
2025        COMPOUND_EXTENSION_RULES,
2026        &canonical,
2027        true,
2028    )?;
2029    validate_rules("extension", EXTENSION_RULES, &canonical, true)?;
2030    validate_content_rules(&canonical)?;
2031
2032    let mut detected: BTreeSet<_> = EXACT_FILENAME_RULES
2033        .iter()
2034        .chain(COMPOUND_EXTENSION_RULES)
2035        .chain(EXTENSION_RULES)
2036        .map(|rule| rule.language)
2037        .collect();
2038    detected.extend(CONTENT_DIALECT_RULES.iter().map(|rule| rule.language));
2039    for capability in LANGUAGE_CAPABILITIES {
2040        if capability.support.detected == CapabilitySupportLevel::Supported
2041            && !detected.contains(capability.id)
2042        {
2043            return Err(LanguageRegistryError::new(format!(
2044                "accepted language {:?} is a ghost row with no detector rule",
2045                capability.id
2046            )));
2047        }
2048        let detected_fixture = detect_language_request(LanguageDetectionRequest::new(
2049            capability.fixtures.positive_path,
2050            None,
2051        ))
2052        .map_err(|source| LanguageRegistryError::new(source.to_string()))?;
2053        if detected_fixture.map(|result| result.language) != Some(capability.id) {
2054            return Err(LanguageRegistryError::new(format!(
2055                "positive fixture {:?} does not select owning language {:?}",
2056                capability.fixtures.positive_path, capability.id
2057            )));
2058        }
2059        let negative_fixture = detect_language_request(LanguageDetectionRequest::new(
2060            capability.fixtures.negative_path,
2061            None,
2062        ))
2063        .map_err(|source| LanguageRegistryError::new(source.to_string()))?;
2064        if negative_fixture.map(|result| result.language) == Some(capability.id) {
2065            return Err(LanguageRegistryError::new(format!(
2066                "negative fixture {:?} still selects owning language {:?}",
2067                capability.fixtures.negative_path, capability.id
2068            )));
2069        }
2070    }
2071    let optional_candidates = LANGUAGE_CAPABILITIES
2072        .iter()
2073        .filter(|capability| capability.optional_pack.is_some())
2074        .count();
2075    if optional_candidates < OPTIONAL_PACK_MINIMUM_ADDITIONAL_GRAMMARS {
2076        return Err(LanguageRegistryError::new(format!(
2077            "optional catalog exposes only {optional_candidates} distinct candidate rows; at least {OPTIONAL_PACK_MINIMUM_ADDITIONAL_GRAMMARS} are required"
2078        )));
2079    }
2080    let accepted_digest = accepted_language_capability_digest();
2081    let expected_accepted_digest = match ACCEPTED_LANGUAGE_CAPABILITY_SET_VERSION {
2082        1 => ACCEPTED_LANGUAGE_CAPABILITY_SET_V1_DIGEST,
2083        2 => ACCEPTED_LANGUAGE_CAPABILITY_SET_V2_DIGEST,
2084        3 => ACCEPTED_LANGUAGE_CAPABILITY_SET_V3_DIGEST,
2085        4 => ACCEPTED_LANGUAGE_CAPABILITY_SET_V4_DIGEST,
2086        5 => ACCEPTED_LANGUAGE_CAPABILITY_SET_V5_DIGEST,
2087        version => {
2088            return Err(LanguageRegistryError::new(format!(
2089                "accepted language capability-set version {version} lacks a historical digest seal"
2090            )));
2091        }
2092    };
2093    if accepted_digest != expected_accepted_digest {
2094        return Err(LanguageRegistryError::new(format!(
2095            "accepted language capability-set version {ACCEPTED_LANGUAGE_CAPABILITY_SET_VERSION} changed from {expected_accepted_digest} to {accepted_digest}; bump the set version for an explicit compatibility decision"
2096        )));
2097    }
2098    Ok(())
2099}
2100
2101/// Register one normalized ID or alias and report both conflicting owners.
2102fn register_owner<'a>(
2103    owners: &mut BTreeMap<String, &'a str>,
2104    value: &str,
2105    owner: &'a str,
2106    kind: &str,
2107) -> Result<(), LanguageRegistryError> {
2108    let normalized = value.to_ascii_lowercase();
2109    if let Some(previous) = owners.insert(normalized.clone(), owner) {
2110        return Err(LanguageRegistryError::new(format!(
2111            "conflicting {kind} {normalized:?} is owned by both {previous:?} and {owner:?}"
2112        )));
2113    }
2114    Ok(())
2115}
2116
2117/// Validate one precedence-class rule table and its canonical targets.
2118fn validate_rules(
2119    kind: &str,
2120    rules: &[LanguageDetectionRule],
2121    canonical: &BTreeSet<&str>,
2122    normalize_case: bool,
2123) -> Result<(), LanguageRegistryError> {
2124    let mut owners = BTreeMap::new();
2125    let mut previous_compound_length = usize::MAX;
2126    for rule in rules {
2127        if !canonical.contains(rule.language) {
2128            return Err(LanguageRegistryError::new(format!(
2129                "{kind} rule {:?} targets missing language {:?}",
2130                rule.value, rule.language
2131            )));
2132        }
2133        let key = if normalize_case {
2134            rule.value.to_ascii_lowercase()
2135        } else {
2136            rule.value.to_string()
2137        };
2138        if let Some(previous) = owners.insert(key.clone(), rule.language)
2139            && previous != rule.language
2140        {
2141            return Err(LanguageRegistryError::new(format!(
2142                "conflicting {kind} rule {key:?} is owned by both {previous:?} and {:?}",
2143                rule.language
2144            )));
2145        }
2146        if kind == "compound extension" {
2147            if rule.value.len() > previous_compound_length {
2148                return Err(LanguageRegistryError::new(format!(
2149                    "compound extension rules are not longest-first at {:?}",
2150                    rule.value
2151                )));
2152            }
2153            previous_compound_length = rule.value.len();
2154        }
2155    }
2156    Ok(())
2157}
2158
2159/// Validate registry-owned content rules and their canonical targets.
2160fn validate_content_rules(canonical: &BTreeSet<&str>) -> Result<(), LanguageRegistryError> {
2161    let mut owners = BTreeMap::new();
2162    for rule in CONTENT_DIALECT_RULES {
2163        if !canonical.contains(rule.language) {
2164            return Err(LanguageRegistryError::new(format!(
2165                "content interpreter {:?} targets missing language {:?}",
2166                rule.interpreter, rule.language
2167            )));
2168        }
2169        if rule.interpreter.is_empty()
2170            || !rule
2171                .interpreter
2172                .bytes()
2173                .all(|byte| byte.is_ascii_lowercase() || byte.is_ascii_digit() || byte == b'-')
2174        {
2175            return Err(LanguageRegistryError::new(format!(
2176                "content interpreter {:?} is not a normalized basename",
2177                rule.interpreter
2178            )));
2179        }
2180        if let Some(previous) = owners.insert(rule.interpreter, rule.language) {
2181            return Err(LanguageRegistryError::new(format!(
2182                "conflicting content interpreter {:?} is owned by both {previous:?} and {:?}",
2183                rule.interpreter, rule.language
2184            )));
2185        }
2186    }
2187    Ok(())
2188}
2189
2190#[cfg(test)]
2191mod tests {
2192    use super::*;
2193    use serde::Deserialize;
2194    use std::io;
2195
2196    /// Independent frozen detector corpus from the 0.3.26 release.
2197    #[derive(Debug, Deserialize)]
2198    struct DetectionCompatibilityFixture {
2199        /// Ordered broad extensions and their canonical results.
2200        extensions: Vec<String>,
2201        /// Exact filename rules and their canonical results.
2202        exact_filenames: Vec<String>,
2203    }
2204
2205    /// Independently accepted projection of the pinned optional catalog subset.
2206    #[derive(Debug, Deserialize)]
2207    struct OptionalDetectionCatalogFixture {
2208        /// Catalog package identity.
2209        catalog: String,
2210        /// Exact catalog release.
2211        version: String,
2212        /// Exact source revision.
2213        revision: String,
2214        /// Ordered canonical ID to accepted extension mappings.
2215        rows: Vec<String>,
2216    }
2217
2218    fn require_test(condition: bool, message: impl Into<String>) -> Result<(), Box<dyn Error>> {
2219        if condition {
2220            Ok(())
2221        } else {
2222            Err(io::Error::other(message.into()).into())
2223        }
2224    }
2225
2226    #[test]
2227    fn accepted_registry_and_generated_projections_validate() -> Result<(), Box<dyn Error>> {
2228        validate_language_registry()?;
2229        require_test(
2230            LANGUAGE_CAPABILITIES.len() == LANGUAGE_SPECS.len(),
2231            "language capability and parser metadata projections differ",
2232        )?;
2233        let report = language_registry_report();
2234        require_test(
2235            report.counts.accepted == LANGUAGE_CAPABILITIES.len(),
2236            "accepted language count is not registry-derived",
2237        )?;
2238        let canonical_ids = LANGUAGE_CAPABILITIES
2239            .iter()
2240            .map(|capability| capability.id)
2241            .collect::<BTreeSet<_>>();
2242        require_test(
2243            report.counts.accepted == canonical_ids.len(),
2244            "aliases or extensions inflated the canonical capability count",
2245        )?;
2246        require_test(
2247            LANGUAGE_CAPABILITIES
2248                .iter()
2249                .any(|capability| !capability.aliases.is_empty())
2250                && !EXTENSION_RULES.is_empty(),
2251            "alias/extension non-inflation check became vacuous",
2252        )?;
2253        require_test(
2254            report.counts.built_in + report.counts.optional_candidates == report.counts.accepted,
2255            "built-in and optional language counts do not cover the accepted set",
2256        )?;
2257        require_test(
2258            language_capability_report_rows().len() == report.counts.accepted,
2259            "documentation projection does not cover the accepted set",
2260        )?;
2261        require_test(
2262            report.counts.optional_candidates >= OPTIONAL_PACK_MINIMUM_ADDITIONAL_GRAMMARS,
2263            "optional catalog projection is below the accepted breadth floor",
2264        )?;
2265        require_test(
2266            !report.registry_digest.is_empty(),
2267            "language registry digest is empty",
2268        )?;
2269        require_test(
2270            !report.accepted_set_digest.is_empty(),
2271            "accepted language-set digest is empty",
2272        )?;
2273        require_test(
2274            report.semantic_provider_digest.len() == 64
2275                && report
2276                    .semantic_provider_digest
2277                    .bytes()
2278                    .all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte)),
2279            "semantic provider digest is not a bounded lowercase hexadecimal identity",
2280        )?;
2281        let encoded = serde_json::to_vec(&report)?;
2282        require_test(
2283            encoded.len() <= LANGUAGE_REGISTRY_REPORT_MAX_BYTES,
2284            "language registry settings projection exceeded its content-free output budget",
2285        )?;
2286        Ok(())
2287    }
2288
2289    #[test]
2290    fn semantic_provider_and_embedded_host_claims_are_honest() -> Result<(), Box<dyn Error>> {
2291        let report = language_registry_report();
2292        let semantic_rows = LANGUAGE_CAPABILITIES
2293            .iter()
2294            .filter(|capability| capability.support.semantic == CapabilitySupportLevel::Supported)
2295            .count();
2296        require_test(
2297            report.counts.semantic.supported == semantic_rows,
2298            "semantic provider rows did not determine the semantic capability count",
2299        )?;
2300
2301        for capability in LANGUAGE_CAPABILITIES
2302            .iter()
2303            .filter(|capability| capability.optional_pack.is_some())
2304        {
2305            require_test(
2306                capability.semantic_provider == SemanticProviderOwner::Unavailable
2307                    && capability.embedded_language.is_none()
2308                    && capability.support.semantic == CapabilitySupportLevel::Unavailable,
2309                format!(
2310                    "optional candidate {:?} advertised semantic capability",
2311                    capability.id
2312                ),
2313            )?;
2314        }
2315
2316        for (language, expected) in [
2317            (
2318                "html",
2319                EmbeddedLanguageCapability {
2320                    host_kind: EmbeddedHostKind::HtmlLike,
2321                    semantic_provider: SemanticProviderOwner::EcmaScript,
2322                },
2323            ),
2324            (
2325                "vue",
2326                EmbeddedLanguageCapability {
2327                    host_kind: EmbeddedHostKind::Component,
2328                    semantic_provider: SemanticProviderOwner::EcmaScript,
2329                },
2330            ),
2331            (
2332                "svelte",
2333                EmbeddedLanguageCapability {
2334                    host_kind: EmbeddedHostKind::Template,
2335                    semantic_provider: SemanticProviderOwner::EcmaScript,
2336                },
2337            ),
2338        ] {
2339            let capability = language_capability(language)
2340                .ok_or_else(|| io::Error::other(format!("missing {language} capability")))?;
2341            require_test(
2342                capability.embedded_language == Some(expected)
2343                    && capability.semantic_provider == SemanticProviderOwner::Unavailable
2344                    && capability.effective_semantic_provider()
2345                        == Some(SemanticProviderOwner::EcmaScript)
2346                    && capability.support.semantic == CapabilitySupportLevel::Unavailable,
2347                format!("{language} host/embedded capability drifted"),
2348            )?;
2349        }
2350
2351        for language in ["cargo-lock", "java", "c", "go"] {
2352            let capability = language_capability(language)
2353                .ok_or_else(|| io::Error::other(format!("missing {language} capability")))?;
2354            require_test(
2355                capability.semantic_provider == SemanticProviderOwner::Unavailable
2356                    && capability.effective_semantic_provider().is_none()
2357                    && capability.support.semantic == CapabilitySupportLevel::Unavailable,
2358                format!("unsupported semantic language {language:?} was promoted"),
2359            )?;
2360        }
2361        require_test(
2362            SemanticProviderOwner::EcmaScript.resolution_family() == Some("ecmascript")
2363                && SemanticProviderOwner::Unavailable
2364                    .resolution_family()
2365                    .is_none(),
2366            "semantic provider resolution-family identity drifted",
2367        )?;
2368        Ok(())
2369    }
2370
2371    #[test]
2372    fn detects_every_broad_source_extension() {
2373        for extension in BROAD_SOURCE_EXTENSIONS {
2374            assert!(
2375                detect_language(Some(extension)).is_some(),
2376                "missing broad source extension support for {extension}"
2377            );
2378        }
2379    }
2380
2381    #[test]
2382    fn detects_every_accepted_registry_source_extension() {
2383        for extension in DETECTED_SOURCE_EXTENSIONS {
2384            assert!(
2385                detect_language(Some(extension)).is_some(),
2386                "missing accepted source extension support for {extension}"
2387            );
2388        }
2389    }
2390
2391    #[test]
2392    fn generated_language_support_document_is_current() -> Result<(), Box<dyn Error>> {
2393        let rendered = render_language_support_markdown()?;
2394        require_test(
2395            include_str!("../../../docs/language-support.md") == rendered,
2396            "checked-in language support document is stale",
2397        )
2398    }
2399
2400    #[test]
2401    fn built_in_grammar_provenance_matches_workspace_pins() {
2402        let workspace = include_str!("../../../Cargo.toml");
2403        let mut checked = BTreeSet::new();
2404        for grammar in TreeSitterGrammar::ALL {
2405            let package = grammar.package();
2406            if checked.insert(package) {
2407                let pin = format!("{package} = \"={}\"", grammar.version());
2408                assert!(
2409                    workspace.lines().any(|line| line.trim() == pin),
2410                    "workspace dependency pin drifted from registry provenance: {pin}"
2411                );
2412            }
2413        }
2414    }
2415
2416    #[test]
2417    fn frozen_v0326_detection_corpus_remains_exact() -> Result<(), Box<dyn Error>> {
2418        let fixture: DetectionCompatibilityFixture = serde_json::from_str(include_str!(
2419            "../../../fixtures/languages/v0.3.26-detection.json"
2420        ))?;
2421        let ordered_extensions = fixture
2422            .extensions
2423            .iter()
2424            .map(|row| row.split_once('=').map(|(extension, _)| extension))
2425            .collect::<Option<Vec<_>>>()
2426            .ok_or_else(|| io::Error::other("invalid extension compatibility row"))?;
2427        if ordered_extensions != BROAD_SOURCE_EXTENSIONS {
2428            return Err(io::Error::other(
2429                "ordered 0.3.26 broad extension compatibility corpus changed",
2430            )
2431            .into());
2432        }
2433        for row in fixture.extensions {
2434            let (extension, expected) = row
2435                .split_once('=')
2436                .ok_or_else(|| io::Error::other("invalid extension compatibility row"))?;
2437            if detect_language(Some(extension)).as_deref() != Some(expected) {
2438                return Err(io::Error::other(format!(
2439                    "0.3.26 extension {extension:?} no longer selects {expected:?}"
2440                ))
2441                .into());
2442            }
2443        }
2444        for row in fixture.exact_filenames {
2445            let (path, expected) = row
2446                .split_once('=')
2447                .ok_or_else(|| io::Error::other("invalid exact filename compatibility row"))?;
2448            if detect_language_for_path(path, None).as_deref() != Some(expected)
2449                || detect_language_for_path(&format!("folder\\{path}"), None).as_deref()
2450                    != Some(expected)
2451            {
2452                return Err(io::Error::other(format!(
2453                    "0.3.26 exact filename {path:?} no longer selects {expected:?}"
2454                ))
2455                .into());
2456            }
2457        }
2458        Ok(())
2459    }
2460
2461    #[test]
2462    fn accepted_optional_detection_subset_matches_independent_catalog_projection()
2463    -> Result<(), Box<dyn Error>> {
2464        let fixture: OptionalDetectionCatalogFixture = serde_json::from_str(include_str!(
2465            "../../../fixtures/languages/accepted-optional-detection-catalog.json"
2466        ))?;
2467        require_test(
2468            fixture.catalog == OPTIONAL_GRAMMAR_CATALOG,
2469            "optional catalog identity drifted",
2470        )?;
2471        require_test(
2472            fixture.version == OPTIONAL_GRAMMAR_CATALOG_VERSION,
2473            "optional catalog version drifted",
2474        )?;
2475        require_test(
2476            fixture.revision == OPTIONAL_GRAMMAR_CATALOG_RELEASE_REVISION,
2477            "optional catalog revision drifted",
2478        )?;
2479        let projected = LANGUAGE_CAPABILITIES
2480            .iter()
2481            .filter(|capability| capability.optional_pack.is_some())
2482            .map(|capability| {
2483                let extensions = rules_for_language(EXTENSION_RULES, capability.id);
2484                match extensions.as_slice() {
2485                    [extension] => Ok(format!("{}={extension}", capability.id)),
2486                    _ => Err(io::Error::other(format!(
2487                        "optional detection row {:?} must own exactly one accepted extension",
2488                        capability.id
2489                    ))),
2490                }
2491            })
2492            .collect::<Result<Vec<_>, _>>()?;
2493        require_test(
2494            fixture.rows == projected,
2495            "accepted optional catalog projection drifted",
2496        )
2497    }
2498
2499    #[test]
2500    fn preserves_representative_broad_source_extensions() {
2501        assert_eq!(
2502            detect_language(Some(".d.ts")).as_deref(),
2503            Some("typescript")
2504        );
2505        assert_eq!(detect_language(Some(".pyw")).as_deref(), Some("python"));
2506        assert_eq!(detect_language(Some(".kts")).as_deref(), Some("kotlin"));
2507        assert_eq!(
2508            detect_language(Some(".psm1")).as_deref(),
2509            Some("powershell")
2510        );
2511        assert_eq!(detect_language(Some(".zon")).as_deref(), Some("zig"));
2512        assert_eq!(detect_language(Some(".proto")).as_deref(), Some("protobuf"));
2513        assert_eq!(detect_language(Some(".R")).as_deref(), Some("r"));
2514        assert_eq!(detect_language(Some(".ini")).as_deref(), Some("config"));
2515        assert_eq!(detect_language(Some(".liquibase")).as_deref(), Some("sql"));
2516        assert_eq!(detect_language(Some(".toon")).as_deref(), Some("toon"));
2517    }
2518
2519    #[test]
2520    fn preserves_exact_filename_case_and_compound_precedence() {
2521        let exact =
2522            detect_language_request(LanguageDetectionRequest::new("Cargo.toml", Some(".toml")));
2523        assert!(matches!(
2524            exact,
2525            Ok(Some(LanguageDetection {
2526                language: "cargo-manifest",
2527                reason: LanguageDetectionReason::ExactFilename,
2528            }))
2529        ));
2530        assert_eq!(
2531            detect_language_for_path("cargo.toml", Some(".toml")).as_deref(),
2532            Some("toml")
2533        );
2534        let compound = detect_language_request(LanguageDetectionRequest::new(
2535            "types/index.d.ts",
2536            Some(".d.ts"),
2537        ));
2538        assert!(matches!(
2539            compound,
2540            Ok(Some(LanguageDetection {
2541                language: "typescript",
2542                reason: LanguageDetectionReason::CompoundExtension,
2543            }))
2544        ));
2545    }
2546
2547    #[test]
2548    fn typed_precedence_prefers_override_and_bounds_content() -> Result<(), Box<dyn Error>> {
2549        let override_result = detect_language_request(LanguageDetectionRequest {
2550            path: "Cargo.toml",
2551            extension: Some(".toml"),
2552            explicit_override: Some("py"),
2553            content_prefix: Some(b"#!/usr/bin/env node\n"),
2554        })?
2555        .ok_or_else(|| io::Error::other("override detection missing"))?;
2556        require_test(
2557            override_result.language == "python",
2558            "explicit alias override did not select Python",
2559        )?;
2560        require_test(
2561            override_result.reason == LanguageDetectionReason::ExplicitOverride,
2562            "explicit override did not retain its typed reason",
2563        )?;
2564
2565        let content_result = detect_language_request(LanguageDetectionRequest {
2566            path: "tool",
2567            extension: None,
2568            explicit_override: None,
2569            content_prefix: Some(b"#!/usr/bin/env python\nprint('ok')\n"),
2570        })?
2571        .ok_or_else(|| io::Error::other("content detection missing"))?;
2572        require_test(
2573            content_result.language == "python",
2574            "bounded shebang did not select Python",
2575        )?;
2576        require_test(
2577            content_result.reason == LanguageDetectionReason::ContentDialect,
2578            "bounded shebang did not retain its typed reason",
2579        )?;
2580        let extension_result =
2581            detect_language_request(LanguageDetectionRequest::new("module.py", Some(".py")))?
2582                .ok_or_else(|| io::Error::other("extension detection missing"))?;
2583        require_test(
2584            extension_result.language == "python",
2585            "extension did not select Python",
2586        )?;
2587        require_test(
2588            extension_result.reason == LanguageDetectionReason::Extension,
2589            "extension did not retain its typed reason",
2590        )?;
2591        require_test(
2592            detect_language_request(LanguageDetectionRequest {
2593                path: "tool",
2594                extension: None,
2595                explicit_override: Some("missing-language"),
2596                content_prefix: None,
2597            })
2598            .is_err(),
2599            "unknown explicit override did not fail closed",
2600        )?;
2601        Ok(())
2602    }
2603
2604    #[test]
2605    fn every_detected_language_has_compatible_parser_metadata() -> Result<(), Box<dyn Error>> {
2606        for rule in EXACT_FILENAME_RULES
2607            .iter()
2608            .chain(COMPOUND_EXTENSION_RULES)
2609            .chain(EXTENSION_RULES)
2610        {
2611            let spec = language_spec(rule.language).ok_or_else(|| {
2612                io::Error::other(format!("missing parser coverage for {}", rule.language))
2613            })?;
2614            require_test(
2615                spec.language == rule.language,
2616                format!("parser coverage ownership drifted for {}", rule.language),
2617            )?;
2618        }
2619        for rule in CONTENT_DIALECT_RULES {
2620            let spec = language_spec(rule.language).ok_or_else(|| {
2621                io::Error::other(format!("missing parser coverage for {}", rule.language))
2622            })?;
2623            require_test(
2624                spec.language == rule.language,
2625                format!("content detector ownership drifted for {}", rule.language),
2626            )?;
2627        }
2628        Ok(())
2629    }
2630
2631    #[test]
2632    fn structural_summary_rows_do_not_advertise_symbols() {
2633        for capability in LANGUAGE_CAPABILITIES
2634            .iter()
2635            .filter(|capability| capability.parser_support == LanguageParserSupport::Structural)
2636            .filter(|capability| capability.symbol_parser == SymbolParserOwner::Unavailable)
2637        {
2638            assert_eq!(
2639                capability.support.symbols,
2640                CapabilitySupportLevel::Unavailable,
2641                "structural summary row {:?} advertised symbols without an owner",
2642                capability.id
2643            );
2644        }
2645    }
2646
2647    #[test]
2648    fn shebang_rules_are_exact_bounded_and_utf8_boundary_safe() -> Result<(), Box<dyn Error>> {
2649        for rule in CONTENT_DIALECT_RULES {
2650            let source = format!("#!/usr/bin/env {}\n", rule.interpreter);
2651            let detected = detect_language_request(LanguageDetectionRequest {
2652                path: "tool",
2653                extension: None,
2654                explicit_override: None,
2655                content_prefix: Some(source.as_bytes()),
2656            })?
2657            .ok_or_else(|| io::Error::other("declared shebang was not detected"))?;
2658            require_test(
2659                detected.language == rule.language,
2660                format!("shebang owner drifted for {}", rule.interpreter),
2661            )?;
2662            require_test(
2663                detected.reason == LanguageDetectionReason::ContentDialect,
2664                format!("shebang reason drifted for {}", rule.interpreter),
2665            )?;
2666        }
2667
2668        for near_miss in ["wish", "mesh-agent", "python-helper", "nodejs"] {
2669            let source = format!("#!/usr/bin/env {near_miss}\n");
2670            let detected = detect_language_request(LanguageDetectionRequest {
2671                path: "tool",
2672                extension: None,
2673                explicit_override: None,
2674                content_prefix: Some(source.as_bytes()),
2675            })?;
2676            require_test(
2677                detected.is_none(),
2678                format!("near-miss interpreter {near_miss:?} was classified"),
2679            )?;
2680        }
2681
2682        for (interpreter, expected) in [
2683            ("python3", "python"),
2684            ("python3.12", "python"),
2685            ("ruby3.3", "ruby"),
2686            ("lua5.4", "lua"),
2687        ] {
2688            let source = format!("#!/usr/bin/env {interpreter}\n");
2689            let detected = detect_language_request(LanguageDetectionRequest {
2690                path: "tool",
2691                extension: None,
2692                explicit_override: None,
2693                content_prefix: Some(source.as_bytes()),
2694            })?;
2695            require_test(
2696                detected.map(|result| result.language) == Some(expected),
2697                format!("valid versioned interpreter {interpreter:?} was not classified"),
2698            )?;
2699        }
2700
2701        for near_miss in ["python.", "python..3", "ruby...", "lua."] {
2702            let source = format!("#!/usr/bin/env {near_miss}\n");
2703            let detected = detect_language_request(LanguageDetectionRequest {
2704                path: "tool",
2705                extension: None,
2706                explicit_override: None,
2707                content_prefix: Some(source.as_bytes()),
2708            })?;
2709            require_test(
2710                detected.is_none(),
2711                format!("invalid versioned interpreter {near_miss:?} was classified"),
2712            )?;
2713        }
2714
2715        let mut split_utf8_after_shebang = b"#!/usr/bin/env python\n".to_vec();
2716        split_utf8_after_shebang.resize(LANGUAGE_CONTENT_DETECTION_MAX_BYTES, b'x');
2717        split_utf8_after_shebang[LANGUAGE_CONTENT_DETECTION_MAX_BYTES - 1] = 0xc3;
2718        let detected = detect_language_request(LanguageDetectionRequest {
2719            path: "tool",
2720            extension: None,
2721            explicit_override: None,
2722            content_prefix: Some(&split_utf8_after_shebang),
2723        })?;
2724        require_test(
2725            detected.map(|result| result.language) == Some("python"),
2726            "valid shebang was lost at a later UTF-8 split boundary",
2727        )?;
2728
2729        let mut after_bound = vec![b' '; LANGUAGE_CONTENT_DETECTION_MAX_BYTES];
2730        after_bound.extend_from_slice(b"#!/usr/bin/env python\n");
2731        let detected = detect_language_request(LanguageDetectionRequest {
2732            path: "tool",
2733            extension: None,
2734            explicit_override: None,
2735            content_prefix: Some(&after_bound),
2736        })?;
2737        require_test(
2738            detected.is_none(),
2739            "content detection inspected bytes beyond its accepted bound",
2740        )
2741    }
2742
2743    #[test]
2744    fn content_rule_changes_advance_registry_digest() {
2745        let baseline = hash_language_registry_with_content_rules(false, CONTENT_DIALECT_RULES);
2746        let mut changed = CONTENT_DIALECT_RULES.to_vec();
2747        changed[0].language = "ruby";
2748        assert_ne!(
2749            baseline,
2750            hash_language_registry_with_content_rules(false, &changed)
2751        );
2752    }
2753
2754    #[test]
2755    fn built_in_tree_sitter_projection_is_registry_derived() {
2756        let projected = builtin_tree_sitter_language_ids();
2757        let expected = LANGUAGE_CAPABILITIES
2758            .iter()
2759            .filter(|capability| {
2760                matches!(capability.symbol_parser, SymbolParserOwner::TreeSitter(_))
2761            })
2762            .count();
2763        assert_eq!(projected.len(), expected);
2764        assert!(projected.contains(&"rust"));
2765        assert!(!projected.contains(&"markdown"));
2766    }
2767
2768    #[test]
2769    fn conflicting_detection_rules_report_both_owners() -> Result<(), Box<dyn Error>> {
2770        let canonical = BTreeSet::from(["python", "rust"]);
2771        let rules = [
2772            LanguageDetectionRule {
2773                value: ".mixed",
2774                language: "rust",
2775            },
2776            LanguageDetectionRule {
2777                value: ".MIXED",
2778                language: "python",
2779            },
2780        ];
2781        let error = validate_rules("extension", &rules, &canonical, true)
2782            .err()
2783            .ok_or_else(|| io::Error::other("case-normalized conflicting owners must fail"))?;
2784        let message = error.to_string();
2785        require_test(
2786            message.contains("rust") && message.contains("python"),
2787            "conflict diagnostic did not name both owners",
2788        )
2789    }
2790}