Skip to main content

typr_core/processes/spg/
model.rs

1#![allow(dead_code)]
2
3use serde::{Deserialize, Serialize};
4
5/// Source location for an SPG node (file + byte offset + 1-based line).
6#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
7pub struct SourceLoc {
8    pub file: String,
9    pub offset: usize,
10    pub line: u32,
11}
12
13impl SourceLoc {
14    /// Compute line number by counting '\n' in the source up to `offset`.
15    pub fn from_offset(file: impl Into<String>, offset: usize, source: &str) -> Self {
16        let line = source[..offset.min(source.len())]
17            .chars()
18            .filter(|&c| c == '\n')
19            .count() as u32
20            + 1;
21        SourceLoc {
22            file: file.into(),
23            offset,
24            line,
25        }
26    }
27}
28
29/// Coarse kind of a documented entity.
30#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
31#[serde(rename_all = "snake_case")]
32pub enum NodeKind {
33    Function,
34    Alias,
35    TypeDef,
36    Module,
37    Variable,
38}
39
40impl NodeKind {
41    pub fn prefix(&self) -> &'static str {
42        match self {
43            NodeKind::Function => "function",
44            NodeKind::Alias => "alias",
45            NodeKind::TypeDef => "type",
46            NodeKind::Module => "module",
47            NodeKind::Variable => "variable",
48        }
49    }
50}
51
52/// Visibility of a binding.
53#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
54#[serde(rename_all = "snake_case")]
55pub enum Visibility {
56    Private,
57    Public,
58    Export,
59}
60
61/// Per-kind extra data carried by a node.
62#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
63#[serde(tag = "kind", rename_all = "snake_case")]
64pub enum NodePayload {
65    Function {
66        /// (param_name, type_str) pairs, in declaration order.
67        params: Vec<(String, String)>,
68        returns: String,
69    },
70    Record {
71        /// (field_name, type_str) pairs, sorted alphabetically for snapshot stability.
72        fields: Vec<(String, String)>,
73    },
74    Alias {
75        underlying: String,
76        opaque: bool,
77    },
78    Union {
79        variants: Vec<(String, Option<String>)>,
80    },
81    Module {
82        exports: Vec<String>,
83    },
84    /// A plain (non-function) `let` binding with an explicit type annotation —
85    /// e.g. `@export let PI: num <- 3.14159;`. Un-annotated bindings carry no
86    /// static type on the AST alone (see `builder.rs`'s `Lang::Let` arm), so
87    /// they are not represented here; this mirrors the same annotated-only
88    /// scope `--checked` uses for the same reason (soundness_transpilation.md
89    /// Phase A).
90    Variable {
91        type_str: String,
92    },
93    None,
94}
95
96/// Structured stdlib metadata extracted from `#!` annotations in `.ty` files.
97///
98/// Carried on `Node::meta` when the node originates from the stdlib catalogue.
99/// All fields are optional — partial metadata is valid (e.g. a function with
100/// only `tier` and `param` descriptions but no examples).
101#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
102pub struct StdlibMeta {
103    /// Confidence tier: `T1` (compiler, high confidence), `T2` (MCP/doc,
104    /// medium confidence), `T3` (doc only, not type-safe for compiler).
105    pub tier: Option<String>,
106    /// Per-parameter descriptions: `(param_name, description)`.
107    pub param_docs: Vec<(String, String)>,
108    /// Return-value description.
109    pub ret_doc: Option<String>,
110    /// Coercion / silent-cast notes (e.g. "logical -> num par R").
111    pub coercion_notes: Option<String>,
112    /// Example code blocks (each a valid `typr check` candidate).
113    pub examples: Vec<String>,
114    /// Related function names (comma-separated in source, split into Vec).
115    pub seealso: Vec<String>,
116    /// The R package of origin (e.g. "base", "stats").
117    pub pkg: Option<String>,
118}
119
120impl StdlibMeta {
121    pub fn empty() -> Self {
122        StdlibMeta {
123            tier: None,
124            param_docs: Vec::new(),
125            ret_doc: None,
126            coercion_notes: None,
127            examples: Vec::new(),
128            seealso: Vec::new(),
129            pkg: None,
130        }
131    }
132
133    /// Returns true if this metadata carries any non-empty field.
134    pub fn is_non_empty(&self) -> bool {
135        self.tier.is_some()
136            || !self.param_docs.is_empty()
137            || self.ret_doc.is_some()
138            || self.coercion_notes.is_some()
139            || !self.examples.is_empty()
140            || !self.seealso.is_empty()
141            || self.pkg.is_some()
142    }
143}
144
145/// A node in the Semantic Package Graph.
146#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
147pub struct Node {
148    /// Stable deterministic id: `<kind>:<module_path>/<name>`
149    pub id: String,
150    pub kind: NodeKind,
151    pub name: String,
152    /// Nesting path of module names (empty for top-level).
153    pub module_path: Vec<String>,
154    pub visibility: Visibility,
155    /// Doc-comment attached by `doc_attach.rs` (Phase 4). `None` until then.
156    pub doc: Option<String>,
157    pub source: Option<SourceLoc>,
158    pub payload: NodePayload,
159    /// Structured stdlib metadata from `#!` annotations. `None` for
160    /// non-stdlib nodes or when no annotations were present.
161    #[serde(skip_serializing_if = "Option::is_none")]
162    pub meta: Option<StdlibMeta>,
163}
164
165impl Node {
166    /// Build the canonical `id` from kind, module path, and name.
167    pub fn make_id(kind: &NodeKind, module_path: &[String], name: &str) -> String {
168        if module_path.is_empty() {
169            format!("{}:{}", kind.prefix(), name)
170        } else {
171            format!("{}:{}/{}", kind.prefix(), module_path.join("/"), name)
172        }
173    }
174}
175
176/// Kind of a directed edge in the graph.
177#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
178#[serde(rename_all = "kebab-case")]
179pub enum EdgeKind {
180    HasField,
181    Returns,
182    ProducesType,
183    ParameterOf,
184    ConsumesType,
185    BelongsToModule,
186    Uses,
187    UsedBy,
188}
189
190/// A directed edge between two nodes.
191#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
192pub struct Edge {
193    pub from: String,
194    pub to: String,
195    pub kind: EdgeKind,
196}
197
198/// Root of the Semantic Package Graph (serialises to `spg.json`).
199#[derive(Debug, Clone, Serialize, Deserialize)]
200pub struct Spg {
201    /// JSON-LD `@context` — placeholder until a proper vocabulary is published.
202    #[serde(rename = "@context")]
203    pub context: String,
204    pub package: String,
205    pub version: String,
206    pub nodes: Vec<Node>,
207    pub edges: Vec<Edge>,
208}
209
210impl Spg {
211    pub fn new(package: impl Into<String>, version: impl Into<String>) -> Self {
212        Spg {
213            context: "https://typr-lang.dev/spg/v1/context.jsonld".into(),
214            package: package.into(),
215            version: version.into(),
216            nodes: Vec::new(),
217            edges: Vec::new(),
218        }
219    }
220
221    pub fn add_node(&mut self, node: Node) {
222        self.nodes.push(node);
223    }
224
225    pub fn add_edge(&mut self, edge: Edge) {
226        self.edges.push(edge);
227    }
228}