Skip to main content

sim_kernel/
hint.rs

1//! Open hint metadata for diagnostics, exports, and runtime operations.
2//!
3//! Hints are data records: libraries choose the `kind` symbol and optional
4//! fields, while the kernel only defines the common transport shape. A hint can
5//! be attached to existing diagnostics through a related diagnostic carrier,
6//! keeping older diagnostic constructors source-compatible.
7
8use crate::{
9    capability::CapabilityName,
10    datum::Datum,
11    env::Cx,
12    error::{Diagnostic, Result, Severity},
13    id::Symbol,
14    value::Value,
15};
16
17/// Open metadata that helps tools explain or route a diagnostic or operation.
18///
19/// The `kind` symbol is intentionally open. The remaining fields are common
20/// slots used by shape checkers, runtime libraries, and agent-facing indexes to
21/// expose expected inputs, argument names, capability requirements, examples,
22/// and codec-safe surface forms without adding closed kernel enums.
23#[derive(Clone, Debug, PartialEq, Eq)]
24pub struct HintMetadata {
25    /// Open symbol naming the hint kind.
26    pub kind: Symbol,
27    /// Short human-readable title.
28    pub title: String,
29    /// Optional longer detail text.
30    pub detail: Option<String>,
31    /// Searchable open tags.
32    pub tags: Vec<Symbol>,
33    /// Argument names or positions the hint describes.
34    pub arguments: Vec<Symbol>,
35    /// Capabilities required to follow the hint.
36    pub capabilities: Vec<CapabilityName>,
37    /// Codec-safe forms related to this hint.
38    pub codec_forms: Vec<Symbol>,
39    /// Short examples a tool can show or index.
40    pub examples: Vec<String>,
41}
42
43impl HintMetadata {
44    /// Builds a hint with a kind and title.
45    pub fn new(kind: Symbol, title: impl Into<String>) -> Self {
46        Self {
47            kind,
48            title: title.into(),
49            detail: None,
50            tags: Vec::new(),
51            arguments: Vec::new(),
52            capabilities: Vec::new(),
53            codec_forms: Vec::new(),
54            examples: Vec::new(),
55        }
56    }
57
58    /// Adds longer detail text.
59    pub fn with_detail(mut self, detail: impl Into<String>) -> Self {
60        self.detail = Some(detail.into());
61        self
62    }
63
64    /// Adds a searchable tag.
65    pub fn with_tag(mut self, tag: Symbol) -> Self {
66        self.tags.push(tag);
67        self
68    }
69
70    /// Adds an argument name or position.
71    pub fn with_argument(mut self, argument: Symbol) -> Self {
72        self.arguments.push(argument);
73        self
74    }
75
76    /// Adds a capability requirement.
77    pub fn with_capability(mut self, capability: CapabilityName) -> Self {
78        self.capabilities.push(capability);
79        self
80    }
81
82    /// Adds a codec-safe form tag.
83    pub fn with_codec_form(mut self, form: Symbol) -> Self {
84        self.codec_forms.push(form);
85        self
86    }
87
88    /// Adds a short example.
89    pub fn with_example(mut self, example: impl Into<String>) -> Self {
90        self.examples.push(example.into());
91        self
92    }
93
94    /// Appends this hint to a diagnostic as a related metadata carrier.
95    pub fn attach_to(self, mut diagnostic: Diagnostic) -> Diagnostic {
96        diagnostic.related.push(self.to_diagnostic());
97        diagnostic
98    }
99
100    /// Converts this hint into a related diagnostic carrier.
101    pub fn to_diagnostic(&self) -> Diagnostic {
102        let mut diagnostic = Diagnostic::info(self.title.clone());
103        diagnostic.code = Some(hint_metadata_code());
104        diagnostic
105            .related
106            .push(hint_field("kind", self.kind.to_string()));
107        if let Some(detail) = &self.detail {
108            diagnostic
109                .related
110                .push(hint_field("detail", detail.clone()));
111        }
112        for tag in &self.tags {
113            diagnostic.related.push(hint_field("tag", tag.to_string()));
114        }
115        for argument in &self.arguments {
116            diagnostic
117                .related
118                .push(hint_field("argument", argument.to_string()));
119        }
120        for capability in &self.capabilities {
121            diagnostic
122                .related
123                .push(hint_field("capability", capability.as_str().to_owned()));
124        }
125        for form in &self.codec_forms {
126            diagnostic
127                .related
128                .push(hint_field("codec-form", form.to_string()));
129        }
130        for example in &self.examples {
131            diagnostic
132                .related
133                .push(hint_field("example", example.clone()));
134        }
135        diagnostic
136    }
137
138    /// Reconstructs hint metadata from a related diagnostic carrier.
139    pub fn from_diagnostic(diagnostic: &Diagnostic) -> Option<Self> {
140        if !Self::is_hint_diagnostic(diagnostic) {
141            return None;
142        }
143
144        let mut kind = None;
145        let mut detail = None;
146        let mut tags = Vec::new();
147        let mut arguments = Vec::new();
148        let mut capabilities = Vec::new();
149        let mut codec_forms = Vec::new();
150        let mut examples = Vec::new();
151
152        for field in &diagnostic.related {
153            let Some(name) = hint_field_name(field) else {
154                continue;
155            };
156            match name {
157                "kind" => kind = Some(parse_symbol(&field.message)),
158                "detail" => detail = Some(field.message.clone()),
159                "tag" => tags.push(parse_symbol(&field.message)),
160                "argument" => arguments.push(parse_symbol(&field.message)),
161                "capability" => capabilities.push(CapabilityName::new(field.message.clone())),
162                "codec-form" => codec_forms.push(parse_symbol(&field.message)),
163                "example" => examples.push(field.message.clone()),
164                _ => {}
165            }
166        }
167
168        Some(Self {
169            kind: kind?,
170            title: diagnostic.message.clone(),
171            detail,
172            tags,
173            arguments,
174            capabilities,
175            codec_forms,
176            examples,
177        })
178    }
179
180    /// Returns whether a diagnostic is a hint metadata carrier.
181    pub fn is_hint_diagnostic(diagnostic: &Diagnostic) -> bool {
182        let expected = hint_metadata_code();
183        diagnostic.code.as_ref() == Some(&expected)
184    }
185
186    /// Collects the direct hint carriers attached to a diagnostic.
187    pub fn collect_from_diagnostic(diagnostic: &Diagnostic) -> Vec<Self> {
188        diagnostic
189            .related
190            .iter()
191            .filter_map(Self::from_diagnostic)
192            .collect()
193    }
194
195    /// Builds a text field suitable for simple agent or Radar-style indexing.
196    pub fn radar_text(&self) -> String {
197        let mut parts = vec![self.kind.to_string(), self.title.clone()];
198        if let Some(detail) = &self.detail {
199            parts.push(detail.clone());
200        }
201        parts.extend(self.tags.iter().map(ToString::to_string));
202        parts.extend(self.arguments.iter().map(ToString::to_string));
203        parts.extend(
204            self.capabilities
205                .iter()
206                .map(|capability| capability.as_str().to_owned()),
207        );
208        parts.extend(self.codec_forms.iter().map(ToString::to_string));
209        parts.extend(self.examples.iter().cloned());
210        parts.join(" ")
211    }
212
213    /// Projects the hint as a runtime table value.
214    pub fn as_value(&self, cx: &mut Cx) -> Result<Value> {
215        let tags = symbol_list_value(cx, &self.tags)?;
216        let arguments = symbol_list_value(cx, &self.arguments)?;
217        let capabilities = cx.factory().list(
218            self.capabilities
219                .iter()
220                .map(|capability| cx.factory().symbol(capability.as_symbol()))
221                .collect::<Result<Vec<_>>>()?,
222        )?;
223        let codec_forms = symbol_list_value(cx, &self.codec_forms)?;
224        let examples = cx.factory().list(
225            self.examples
226                .iter()
227                .map(|example| cx.factory().string(example.clone()))
228                .collect::<Result<Vec<_>>>()?,
229        )?;
230        let detail = match &self.detail {
231            Some(detail) => cx.factory().string(detail.clone())?,
232            None => cx.factory().nil()?,
233        };
234        cx.factory().table(vec![
235            (Symbol::new("kind"), cx.factory().symbol(self.kind.clone())?),
236            (
237                Symbol::new("title"),
238                cx.factory().string(self.title.clone())?,
239            ),
240            (Symbol::new("detail"), detail),
241            (Symbol::new("tags"), tags),
242            (Symbol::new("arguments"), arguments),
243            (Symbol::new("capabilities"), capabilities),
244            (Symbol::new("codec-forms"), codec_forms),
245            (Symbol::new("examples"), examples),
246            (
247                Symbol::new("radar-text"),
248                cx.factory().string(self.radar_text())?,
249            ),
250        ])
251    }
252
253    /// Projects the hint as a datum node.
254    pub fn as_datum(&self) -> Datum {
255        let mut fields = vec![
256            (Symbol::new("kind"), Datum::Symbol(self.kind.clone())),
257            (Symbol::new("title"), Datum::String(self.title.clone())),
258            (
259                Symbol::new("tags"),
260                Datum::Vector(self.tags.iter().cloned().map(Datum::Symbol).collect()),
261            ),
262            (
263                Symbol::new("arguments"),
264                Datum::Vector(self.arguments.iter().cloned().map(Datum::Symbol).collect()),
265            ),
266            (
267                Symbol::new("capabilities"),
268                Datum::Vector(
269                    self.capabilities
270                        .iter()
271                        .map(|capability| Datum::Symbol(capability.as_symbol()))
272                        .collect(),
273                ),
274            ),
275            (
276                Symbol::new("codec-forms"),
277                Datum::Vector(
278                    self.codec_forms
279                        .iter()
280                        .cloned()
281                        .map(Datum::Symbol)
282                        .collect(),
283                ),
284            ),
285            (
286                Symbol::new("examples"),
287                Datum::Vector(self.examples.iter().cloned().map(Datum::String).collect()),
288            ),
289            (Symbol::new("radar-text"), Datum::String(self.radar_text())),
290        ];
291        if let Some(detail) = &self.detail {
292            fields.push((Symbol::new("detail"), Datum::String(detail.clone())));
293        }
294        Datum::Node {
295            tag: Symbol::qualified("core", "HintMetadata"),
296            fields,
297        }
298    }
299}
300
301/// Builds a runtime list value containing the hints attached to `diagnostic`.
302pub(crate) fn diagnostic_hints_value(cx: &mut Cx, diagnostic: &Diagnostic) -> Result<Value> {
303    let values = HintMetadata::collect_from_diagnostic(diagnostic)
304        .into_iter()
305        .map(|hint| hint.as_value(cx))
306        .collect::<Result<Vec<_>>>()?;
307    cx.factory().list(values)
308}
309
310fn hint_metadata_code() -> Symbol {
311    Symbol::qualified("hint", "metadata")
312}
313
314fn hint_field(name: &'static str, value: String) -> Diagnostic {
315    let mut field = Diagnostic::info(value);
316    field.severity = Severity::Note;
317    field.code = Some(Symbol::qualified("hint-field", name));
318    field
319}
320
321fn hint_field_name(diagnostic: &Diagnostic) -> Option<&str> {
322    let code = diagnostic.code.as_ref()?;
323    if code.namespace.as_deref() != Some("hint-field") {
324        return None;
325    }
326    Some(code.name.as_ref())
327}
328
329fn parse_symbol(value: &str) -> Symbol {
330    match value.split_once('/') {
331        Some((namespace, name)) => Symbol::qualified(namespace.to_owned(), name.to_owned()),
332        None => Symbol::new(value.to_owned()),
333    }
334}
335
336fn symbol_list_value(cx: &mut Cx, symbols: &[Symbol]) -> Result<Value> {
337    let values = symbols
338        .iter()
339        .cloned()
340        .map(|symbol| cx.factory().symbol(symbol))
341        .collect::<Result<Vec<_>>>()?;
342    cx.factory().list(values)
343}