Skip to main content

harn_hostlib/ast/
health.rs

1//! One parser provenance, fitness, and coverage decision for every AST caller.
2//!
3//! The checked-in receipt is generated from the resolved Cargo graph and the
4//! exact versioned corpus bytes. Runtime callers add only the selected grammar
5//! ABI and the observation for their source; policy is derived here once.
6
7use std::sync::{Arc, OnceLock};
8
9use harn_vm::VmValue;
10use serde::Deserialize;
11
12use super::language::Language;
13
14const RECEIPT_JSON: &str = include_str!("../../data/grammar-fitness/receipt.v1.json");
15
16#[derive(Debug, Clone, Copy, PartialEq, Eq)]
17/// The structural operation whose parser evidence is being reported.
18pub enum ParserOperation {
19    /// Parse diagnostics or a syntax tree.
20    Parse,
21    /// Read-only structural search or analysis.
22    Search,
23    /// A structural mutation that must fail closed.
24    SafeEdit,
25}
26
27impl ParserOperation {
28    /// Stable wire name used by schemas and receipts.
29    pub const fn as_str(self) -> &'static str {
30        match self {
31            Self::Parse => "parse",
32            Self::Search => "search",
33            Self::SafeEdit => "safe_edit",
34        }
35    }
36}
37
38#[derive(Debug, Clone, Copy, PartialEq, Eq)]
39/// What the selected grammar observed in the caller's source.
40pub enum SourceObservation {
41    /// The produced syntax tree contained no error node.
42    Clean,
43    /// The produced syntax tree contained at least one error node.
44    Errors,
45    /// No source parse was attempted for this response.
46    NotObserved,
47}
48
49impl SourceObservation {
50    const fn as_str(self) -> &'static str {
51        match self {
52            Self::Clean => "clean",
53            Self::Errors => "errors",
54            Self::NotObserved => "not_observed",
55        }
56    }
57}
58
59#[derive(Debug, Clone, Copy, PartialEq, Eq)]
60/// Truthfulness level callers may rely on.
61pub enum Coverage {
62    /// Artifact fitness passed and any required source observation is authoritative.
63    Verified,
64    /// Read-only results may be useful but the source contains parser errors.
65    Partial,
66    /// No supported, fitted parser artifact was available.
67    Unavailable,
68    /// The parser saw errors and no independent authority resolved their cause.
69    Inconclusive,
70}
71
72impl Coverage {
73    /// Stable wire name used by schemas.
74    pub const fn as_str(self) -> &'static str {
75        match self {
76            Self::Verified => "verified",
77            Self::Partial => "partial",
78            Self::Unavailable => "unavailable",
79            Self::Inconclusive => "inconclusive",
80        }
81    }
82}
83
84#[derive(Debug, Clone)]
85/// One authoritative parser provenance and coverage decision.
86pub struct ParserHealth {
87    operation: ParserOperation,
88    language: Option<Language>,
89    observation: SourceObservation,
90    source_authority: Option<String>,
91    coverage: Coverage,
92    support: &'static str,
93    receipt: Option<&'static LanguageReceipt>,
94    abi_version: Option<usize>,
95}
96
97impl ParserHealth {
98    /// Build health from an actual source parse.
99    pub fn observed(language: Language, operation: ParserOperation, had_errors: bool) -> Self {
100        let observation = if had_errors {
101            SourceObservation::Errors
102        } else {
103            SourceObservation::Clean
104        };
105        Self::new(Some(language), operation, observation)
106    }
107
108    /// Build health for a supported operation that did not inspect source.
109    pub fn not_observed(language: Language, operation: ParserOperation) -> Self {
110        Self::new(Some(language), operation, SourceObservation::NotObserved)
111    }
112
113    /// Report artifact/corpus fitness for capability discovery without making
114    /// a claim about caller source.
115    pub fn fitted(language: Language, operation: ParserOperation) -> Self {
116        let mut health = Self::new(Some(language), operation, SourceObservation::NotObserved);
117        if health.support == "supported" {
118            health.coverage = Coverage::Verified;
119        }
120        health
121    }
122
123    /// Build an explicit unavailable result without implying a clean parse.
124    pub fn unavailable(language: Option<Language>, operation: ParserOperation) -> Self {
125        let receipt = language.and_then(receipt_for);
126        let ts_language = language.and_then(Language::ts_language);
127        let fitted = receipt.is_some_and(|row| {
128            row.operations
129                .iter()
130                .any(|candidate| candidate == operation.as_str())
131        });
132        Self {
133            operation,
134            language,
135            observation: SourceObservation::NotObserved,
136            source_authority: None,
137            coverage: Coverage::Unavailable,
138            support: if ts_language.is_some() && fitted {
139                "supported"
140            } else {
141                "unsupported"
142            },
143            receipt,
144            abi_version: ts_language.as_ref().map(tree_sitter::Language::abi_version),
145        }
146    }
147
148    fn new(
149        language: Option<Language>,
150        operation: ParserOperation,
151        observation: SourceObservation,
152    ) -> Self {
153        let receipt = language.and_then(receipt_for);
154        let ts_language = language.and_then(Language::ts_language);
155        let fitted = receipt.is_some_and(|row| {
156            row.operations
157                .iter()
158                .any(|candidate| candidate == operation.as_str())
159        });
160        let supported = ts_language.is_some() && fitted;
161        let coverage = if !supported {
162            Coverage::Unavailable
163        } else {
164            match (operation, observation) {
165                (_, SourceObservation::Clean) => Coverage::Verified,
166                (_, SourceObservation::NotObserved) => Coverage::Inconclusive,
167                (ParserOperation::Search, SourceObservation::Errors) => Coverage::Partial,
168                (ParserOperation::Parse | ParserOperation::SafeEdit, SourceObservation::Errors) => {
169                    Coverage::Inconclusive
170                }
171            }
172        };
173        Self {
174            operation,
175            language,
176            observation,
177            source_authority: (!matches!(observation, SourceObservation::NotObserved))
178                .then(|| "tree_sitter".to_string()),
179            coverage,
180            support: if supported {
181                "supported"
182            } else {
183                "unsupported"
184            },
185            receipt,
186            abi_version: ts_language.as_ref().map(tree_sitter::Language::abi_version),
187        }
188    }
189
190    /// Return the derived coverage state.
191    pub const fn coverage(&self) -> Coverage {
192        self.coverage
193    }
194
195    /// Whether a structural write may rely on this health result.
196    pub const fn is_verified(&self) -> bool {
197        matches!(self.coverage, Coverage::Verified)
198    }
199
200    /// Record an independent configured checker that verified source rejected
201    /// by Tree-sitter. Callers must name the authority; span shape is never an
202    /// authority.
203    pub fn externally_verified(
204        language: Language,
205        operation: ParserOperation,
206        authority: impl Into<String>,
207    ) -> Self {
208        let mut health = Self::new(Some(language), operation, SourceObservation::Errors);
209        if health.support == "supported" {
210            health.coverage = Coverage::Verified;
211            health.source_authority = Some(authority.into());
212        }
213        health
214    }
215
216    pub(crate) fn attach_to(&self, response: VmValue) -> VmValue {
217        match response {
218            VmValue::Dict(map) => {
219                let mut fields = (*map).clone();
220                fields.insert("health".into(), self.to_vm_value());
221                VmValue::dict(fields)
222            }
223            other => other,
224        }
225    }
226
227    /// Serialize the versioned public health contract.
228    pub fn to_vm_value(&self) -> VmValue {
229        let receipt = fitness_receipt();
230        let grammar = self.receipt.map_or_else(
231            || {
232                dict([
233                    (
234                        "language",
235                        self.language
236                            .map_or(VmValue::Nil, |language| VmValue::string(language.name())),
237                    ),
238                    ("package", VmValue::Nil),
239                    (
240                        "runtime_version",
241                        VmValue::string(&receipt.tree_sitter_runtime),
242                    ),
243                    ("abi_version", VmValue::Nil),
244                    ("enabled_features", VmValue::List(Arc::new(Vec::new()))),
245                ])
246            },
247            |language_receipt| {
248                let package = &language_receipt.package;
249                dict([
250                    ("language", VmValue::string(&language_receipt.language)),
251                    (
252                        "package",
253                        dict([
254                            ("name", VmValue::string(&package.name)),
255                            ("version", VmValue::string(&package.version)),
256                            ("source", VmValue::string(&package.source)),
257                            (
258                                "checksum",
259                                package
260                                    .checksum
261                                    .as_deref()
262                                    .map_or(VmValue::Nil, VmValue::string),
263                            ),
264                            ("artifact_digest", VmValue::string(&package.artifact_digest)),
265                        ]),
266                    ),
267                    (
268                        "runtime_version",
269                        VmValue::string(&receipt.tree_sitter_runtime),
270                    ),
271                    (
272                        "abi_version",
273                        self.abi_version
274                            .map_or(VmValue::Nil, |abi| VmValue::Int(abi as i64)),
275                    ),
276                    (
277                        "enabled_features",
278                        VmValue::List(Arc::new(vec![VmValue::string(grammar_feature(
279                            self.language.expect("receipt has language"),
280                        ))])),
281                    ),
282                ])
283            },
284        );
285        let corpus_fitness = if self.receipt.is_some_and(|row| {
286            row.operations
287                .iter()
288                .any(|candidate| candidate == self.operation.as_str())
289        }) {
290            "verified"
291        } else {
292            "unavailable"
293        };
294        dict([
295            ("schema_version", VmValue::Int(receipt.schema_version)),
296            ("operation", VmValue::string(self.operation.as_str())),
297            ("grammar", grammar),
298            (
299                "corpus",
300                dict([
301                    (
302                        "schema_version",
303                        VmValue::Int(receipt.corpus.schema_version),
304                    ),
305                    ("digest", VmValue::string(&receipt.corpus.digest)),
306                    ("fitness", VmValue::string(corpus_fitness)),
307                    (
308                        "authority",
309                        self.receipt
310                            .map_or(VmValue::Nil, |row| VmValue::string(&row.authority)),
311                    ),
312                ]),
313            ),
314            (
315                "source",
316                dict([
317                    ("observation", VmValue::string(self.observation.as_str())),
318                    (
319                        "authority",
320                        self.source_authority
321                            .as_deref()
322                            .map_or(VmValue::Nil, VmValue::string),
323                    ),
324                ]),
325            ),
326            ("support", VmValue::string(self.support)),
327            ("coverage", VmValue::string(self.coverage.as_str())),
328        ])
329    }
330}
331
332#[derive(Debug, Deserialize)]
333#[serde(deny_unknown_fields)]
334struct FitnessReceipt {
335    schema_version: i64,
336    corpus: CorpusReceipt,
337    tree_sitter_runtime: String,
338    languages: Vec<LanguageReceipt>,
339}
340
341#[derive(Debug, Deserialize)]
342#[serde(deny_unknown_fields)]
343struct CorpusReceipt {
344    schema_version: i64,
345    digest: String,
346}
347
348#[derive(Debug, Deserialize)]
349#[serde(deny_unknown_fields)]
350struct LanguageReceipt {
351    language: String,
352    package: PackageReceipt,
353    operations: Vec<String>,
354    authority: String,
355}
356
357#[derive(Debug, Deserialize)]
358#[serde(deny_unknown_fields)]
359struct PackageReceipt {
360    name: String,
361    version: String,
362    source: String,
363    checksum: Option<String>,
364    artifact_digest: String,
365}
366
367fn fitness_receipt() -> &'static FitnessReceipt {
368    static RECEIPT: OnceLock<FitnessReceipt> = OnceLock::new();
369    RECEIPT.get_or_init(|| {
370        let receipt: FitnessReceipt =
371            serde_json::from_str(RECEIPT_JSON).expect("grammar fitness receipt must be valid");
372        assert_eq!(receipt.schema_version, 1, "unsupported fitness receipt");
373        assert!(
374            !receipt.tree_sitter_runtime.is_empty(),
375            "fitness receipt must identify the tree-sitter runtime"
376        );
377        receipt
378    })
379}
380
381fn receipt_for(language: Language) -> Option<&'static LanguageReceipt> {
382    fitness_receipt()
383        .languages
384        .iter()
385        .find(|row| row.language == language.name())
386}
387
388const fn grammar_feature(language: Language) -> &'static str {
389    match language {
390        Language::Harn => "grammar-harn",
391        Language::TypeScript
392        | Language::Tsx
393        | Language::JavaScript
394        | Language::Jsx
395        | Language::Html
396        | Language::Css => "grammar-web",
397        Language::Rust | Language::C | Language::Cpp | Language::Go | Language::Zig => {
398            "grammar-systems"
399        }
400        Language::Python
401        | Language::Ruby
402        | Language::Bash
403        | Language::Lua
404        | Language::Php
405        | Language::R => "grammar-scripting",
406        Language::Java | Language::Kotlin | Language::Scala => "grammar-jvm",
407        Language::CSharp | Language::Swift | Language::Elixir | Language::Haskell => {
408            "grammar-enterprise"
409        }
410        Language::Json | Language::Yaml | Language::Toml | Language::Sql | Language::Markdown => {
411            "grammar-data"
412        }
413    }
414}
415
416fn dict<const N: usize>(entries: [(&str, VmValue); N]) -> VmValue {
417    VmValue::dict(
418        entries
419            .into_iter()
420            .map(|(key, value)| (key.to_string(), value))
421            .collect::<harn_vm::value::DictMap>(),
422    )
423}
424
425#[cfg(test)]
426mod tests {
427    use super::*;
428
429    #[test]
430    fn receipt_covers_every_registered_language_and_operation() {
431        for language in Language::all() {
432            let receipt = receipt_for(*language)
433                .unwrap_or_else(|| panic!("missing receipt for {}", language.name()));
434            for operation in [
435                ParserOperation::Parse,
436                ParserOperation::Search,
437                ParserOperation::SafeEdit,
438            ] {
439                assert!(
440                    receipt
441                        .operations
442                        .iter()
443                        .any(|candidate| candidate == operation.as_str()),
444                    "{} lacks {} fitness",
445                    language.name(),
446                    operation.as_str()
447                );
448            }
449        }
450    }
451
452    #[test]
453    fn coverage_policy_is_operation_specific() {
454        let parse = ParserHealth::observed(Language::Swift, ParserOperation::Parse, true);
455        let search = ParserHealth::observed(Language::Swift, ParserOperation::Search, true);
456        let edit = ParserHealth::observed(Language::Swift, ParserOperation::SafeEdit, false);
457        assert_eq!(parse.coverage(), Coverage::Inconclusive);
458        assert_eq!(search.coverage(), Coverage::Partial);
459        assert_eq!(edit.coverage(), Coverage::Verified);
460        let VmValue::Dict(parse_health) = parse.to_vm_value() else {
461            panic!("health dict")
462        };
463        let Some(VmValue::Dict(source)) = parse_health.get("source") else {
464            panic!("source dict")
465        };
466        assert!(matches!(
467            source.get("authority"),
468            Some(VmValue::String(authority)) if authority.as_str() == "tree_sitter"
469        ));
470    }
471
472    #[test]
473    fn only_a_named_external_authority_can_verify_a_parser_limitation() {
474        let tree_sitter_only =
475            ParserHealth::observed(Language::Scala, ParserOperation::SafeEdit, true);
476        let externally_verified = ParserHealth::externally_verified(
477            Language::Scala,
478            ParserOperation::SafeEdit,
479            "scalac 3.7.1",
480        );
481        assert_eq!(tree_sitter_only.coverage(), Coverage::Inconclusive);
482        assert_eq!(externally_verified.coverage(), Coverage::Verified);
483        let value = externally_verified.to_vm_value();
484        let VmValue::Dict(health) = value else {
485            panic!("health dict")
486        };
487        let Some(VmValue::Dict(source)) = health.get("source") else {
488            panic!("source dict")
489        };
490        assert!(matches!(
491            source.get("authority"),
492            Some(VmValue::String(authority)) if authority.as_str() == "scalac 3.7.1"
493        ));
494    }
495}