Skip to main content

relay_knowledge/domain/code/
framework.rs

1//! Framework-aware component and template graph contracts.
2
3use serde::{Deserialize, Serialize};
4
5use super::{
6    DomainError, FreshnessPolicy,
7    error::required_text,
8    repository::{CodeRepositorySelector, RepositoryCodeRange},
9};
10
11const MAX_FRAMEWORK_FILTERS: usize = 2;
12const MAX_FRAMEWORK_KIND_FILTERS: usize = 16;
13
14/// Supported frontend framework families.
15#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
16#[serde(rename_all = "snake_case")]
17pub enum FrameworkKind {
18    Angular,
19    Vue,
20}
21
22impl FrameworkKind {
23    /// Stable storage and interface representation.
24    pub const fn as_str(self) -> &'static str {
25        match self {
26            Self::Angular => "angular",
27            Self::Vue => "vue",
28        }
29    }
30}
31
32/// Framework graph node category.
33#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
34#[serde(rename_all = "snake_case")]
35pub enum FrameworkNodeKind {
36    Component,
37    Directive,
38    Pipe,
39    Template,
40    Input,
41    Output,
42    Prop,
43    Emit,
44    Model,
45    Slot,
46    TemplateVariable,
47    ControlFlow,
48}
49
50impl FrameworkNodeKind {
51    /// Stable storage and interface representation.
52    pub const fn as_str(self) -> &'static str {
53        match self {
54            Self::Component => "component",
55            Self::Directive => "directive",
56            Self::Pipe => "pipe",
57            Self::Template => "template",
58            Self::Input => "input",
59            Self::Output => "output",
60            Self::Prop => "prop",
61            Self::Emit => "emit",
62            Self::Model => "model",
63            Self::Slot => "slot",
64            Self::TemplateVariable => "template_variable",
65            Self::ControlFlow => "control_flow",
66        }
67    }
68}
69
70/// Framework graph relationship category.
71#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
72#[serde(rename_all = "snake_case")]
73pub enum FrameworkEdgeKind {
74    OwnsTemplate,
75    Declares,
76    Imports,
77    Renders,
78    BindsInput,
79    HandlesOutput,
80    Reads,
81    Writes,
82    UsesDirective,
83    ProvidesSlot,
84}
85
86impl FrameworkEdgeKind {
87    /// Stable storage and interface representation.
88    pub const fn as_str(self) -> &'static str {
89        match self {
90            Self::OwnsTemplate => "owns_template",
91            Self::Declares => "declares",
92            Self::Imports => "imports",
93            Self::Renders => "renders",
94            Self::BindsInput => "binds_input",
95            Self::HandlesOutput => "handles_output",
96            Self::Reads => "reads",
97            Self::Writes => "writes",
98            Self::UsesDirective => "uses_directive",
99            Self::ProvidesSlot => "provides_slot",
100        }
101    }
102}
103
104/// One indexed framework component, declaration, or template construct.
105#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
106pub struct CodeFrameworkNodeRecord {
107    pub repository_id: String,
108    pub source_scope: String,
109    pub node_id: String,
110    pub file_id: String,
111    pub path: String,
112    pub framework: FrameworkKind,
113    pub kind: FrameworkNodeKind,
114    pub name: String,
115    #[serde(skip_serializing_if = "Option::is_none")]
116    pub detail: Option<String>,
117    #[serde(skip_serializing_if = "Option::is_none")]
118    pub symbol_snapshot_id: Option<String>,
119    pub byte_range: RepositoryCodeRange,
120    pub line_range: RepositoryCodeRange,
121}
122
123/// One indexed relationship between framework nodes or an unresolved target hint.
124#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
125pub struct CodeFrameworkEdgeRecord {
126    pub repository_id: String,
127    pub source_scope: String,
128    pub edge_id: String,
129    pub file_id: String,
130    pub path: String,
131    pub framework: FrameworkKind,
132    pub kind: FrameworkEdgeKind,
133    pub source_node_id: String,
134    #[serde(skip_serializing_if = "Option::is_none")]
135    pub target_node_id: Option<String>,
136    #[serde(skip_serializing_if = "Option::is_none")]
137    pub target_hint: Option<String>,
138    pub resolution_state: String,
139    pub confidence_basis_points: u16,
140    pub confidence_tier: String,
141    pub byte_range: RepositoryCodeRange,
142    pub line_range: RepositoryCodeRange,
143}
144
145/// Bounded framework graph query over one indexed repository scope.
146#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
147pub struct FrameworkGraphRequest {
148    #[serde(skip_serializing_if = "Option::is_none")]
149    pub query: Option<String>,
150    pub repository: CodeRepositorySelector,
151    #[serde(default, skip_serializing_if = "Vec::is_empty")]
152    pub frameworks: Vec<FrameworkKind>,
153    #[serde(default, skip_serializing_if = "Vec::is_empty")]
154    pub kinds: Vec<FrameworkNodeKind>,
155    pub limit: usize,
156    pub freshness_policy: FreshnessPolicy,
157}
158
159impl FrameworkGraphRequest {
160    /// Validates optional search text and bounds filters and result fan-out.
161    pub fn new(
162        query: Option<String>,
163        repository: CodeRepositorySelector,
164        frameworks: Vec<FrameworkKind>,
165        kinds: Vec<FrameworkNodeKind>,
166        limit: usize,
167        freshness_policy: FreshnessPolicy,
168    ) -> Result<Self, DomainError> {
169        if !(1..=100).contains(&limit) {
170            return Err(DomainError::invalid("limit", "must be between 1 and 100"));
171        }
172        if frameworks.len() > MAX_FRAMEWORK_FILTERS {
173            return Err(DomainError::invalid(
174                "frameworks",
175                "must contain 2 or fewer entries",
176            ));
177        }
178        if kinds.len() > MAX_FRAMEWORK_KIND_FILTERS {
179            return Err(DomainError::invalid(
180                "kinds",
181                "must contain 16 or fewer entries",
182            ));
183        }
184        let query = query
185            .map(|value| required_text("query", value))
186            .transpose()?;
187
188        Ok(Self {
189            query,
190            repository,
191            frameworks,
192            kinds,
193            limit,
194            freshness_policy,
195        })
196    }
197}
198
199/// Framework graph rows returned from one repository scope.
200#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
201pub struct FrameworkGraph {
202    pub nodes: Vec<CodeFrameworkNodeRecord>,
203    pub edges: Vec<CodeFrameworkEdgeRecord>,
204    pub truncated: bool,
205}
206
207#[cfg(test)]
208#[path = "framework_tests.rs"]
209mod tests;