codehelion_core/semantic/graph.rs
1use super::{BTreeSet, Deserialize, Error, Language, Serialize, TypeTag};
2
3/// Version of the closed SOG vocabulary and normalization contract.
4pub const SOG_SCHEMA_VERSION: &str = "sog-v1";
5
6/// Version of the coarse index used to bound registered SOG comparisons.
7///
8/// The index is deliberately only a candidate filter. A matching rule still
9/// checks all of its conditions after extraction, so changing this version
10/// can affect cost and recall but never turns an unchecked pair into a
11/// finding.
12pub const SEMANTIC_CANDIDATE_INDEX_VERSION: &str = "sog-candidate-index-v1";
13
14/// Version of the bounded source-window extraction for registered SOG rules.
15///
16/// Source ranges are deliberately sidecar evidence: they select the reported
17/// fragment but never enter a SOG fingerprint or a stable finding identity.
18pub const SEMANTIC_WINDOWING_VERSION: &str = "sog-windowing-v1";
19
20/// Version of the opt-in Rust-to-C++ candidate index.
21///
22/// This index is separate from ordinary semantic detection: it accepts only
23/// caller-supplied explicit comparison partitions and never joins a normal
24/// build-variant bucket.
25pub const CROSS_LANGUAGE_CANDIDATE_INDEX_VERSION: &str = "cross-language-sog-candidate-v1";
26
27/// Version of the built-in restricted-semantic rule registry.
28///
29/// This changes when the set of rule identifiers or their default enabled
30/// state changes. A scan records it beside the SOG schema so its evidence is
31/// never interpreted under a different rule selection unnoticed.
32pub const SEMANTIC_RULE_REGISTRY_VERSION: &str = "semantic-rule-registry-v1";
33
34/// One permitted semantic operation.
35#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
36#[serde(rename_all = "snake_case")]
37pub enum OperationKind {
38 /// Read elements from a source sequence or stream.
39 Source,
40 /// Retain elements that satisfy a predicate.
41 Filter,
42 /// Transform each element independently.
43 Map,
44 /// Combine elements into one accumulated value.
45 Reduce,
46 /// Materialize elements into a collection.
47 Collect,
48 /// Check a precondition or select a valid branch.
49 Validate,
50 /// Propagate an absent or erroneous value without handling it here.
51 PropagateError,
52 /// Acquire a resource whose lifetime is tracked in this graph.
53 AcquireResource,
54 /// Release a previously acquired resource.
55 ReleaseResource,
56}
57
58/// A standard fallible container retained by a compiler-confirmed operation.
59///
60/// This closed category lets a rule distinguish `Result` error propagation
61/// from `Option` absence propagation without importing a helper's type model
62/// or guessing from syntax.
63#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
64#[serde(rename_all = "snake_case")]
65pub enum FallibleKind {
66 /// A standard `Option` value.
67 Option,
68 /// A standard `Result` value.
69 Result,
70}
71
72impl FallibleKind {
73 /// Stable identifier used in normalized evidence.
74 #[must_use]
75 pub const fn name(self) -> &'static str {
76 match self {
77 Self::Option => "option",
78 Self::Result => "result",
79 }
80 }
81}
82
83/// A closed propagation form established by a compiler helper.
84#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
85#[serde(rename_all = "snake_case")]
86pub enum DirectPropagation {
87 /// A `Result` error is propagated while its success value is unchanged.
88 ResultAdapter,
89 /// An `Option` absence is propagated while its success value is unchanged.
90 OptionAdapter,
91}
92
93impl DirectPropagation {
94 /// Stable identifier used in normalized evidence.
95 #[must_use]
96 pub const fn name(self) -> &'static str {
97 match self {
98 Self::ResultAdapter => "result_adapter",
99 Self::OptionAdapter => "option_adapter",
100 }
101 }
102}
103
104impl OperationKind {
105 /// Stable identifier used in reports and rule definitions.
106 #[must_use]
107 pub const fn name(self) -> &'static str {
108 match self {
109 Self::Source => "source",
110 Self::Filter => "filter",
111 Self::Map => "map",
112 Self::Reduce => "reduce",
113 Self::Collect => "collect",
114 Self::Validate => "validate",
115 Self::PropagateError => "propagate_error",
116 Self::AcquireResource => "acquire_resource",
117 Self::ReleaseResource => "release_resource",
118 }
119 }
120}
121
122/// Attributes retained for an operation without importing compiler internals.
123#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
124pub struct OperationAttributes {
125 /// Resolved type category of the operated value, when available.
126 pub type_tag: Option<TypeTag>,
127 /// Compiler-resolved API names used by this operation.
128 pub api_names: BTreeSet<String>,
129 /// Registered resource category for acquire/release operations.
130 pub resource_kind: Option<String>,
131 /// Standard fallible container established for a propagation or validation
132 /// operation, when the helper schema retained it.
133 pub fallible_kind: Option<FallibleKind>,
134 /// Closed direct-propagation spelling the compiler confirmed, when any.
135 pub direct_propagation: Option<DirectPropagation>,
136 /// Position-free source structure retained by the Structural frontend.
137 ///
138 /// This keeps same-variant rules from treating different predicates or
139 /// transformations as interchangeable when their compiler-resolved API
140 /// sequence is otherwise identical. It is absent for graphs assembled by
141 /// adapters that cannot provide a source window.
142 pub structure_fingerprint: Option<[u8; 16]>,
143}
144
145/// One operation in source order; its position is a graph-local reference.
146///
147/// The position is never a stable finding identifier. Stable identifiers are
148/// minted only after a versioned normalization and rule application.
149#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
150pub struct OperationNode {
151 /// The fixed semantic operation this node represents.
152 pub kind: OperationKind,
153 /// Compiler-independent evidence used by registered rules.
154 pub attributes: OperationAttributes,
155}
156
157/// Why one operation precedes or is paired with another.
158#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
159#[serde(rename_all = "snake_case")]
160pub enum OperationEdgeKind {
161 /// A value produced by the source operation feeds the target operation.
162 Data,
163 /// Observable side effects require the source operation to precede target.
164 Ordering,
165 /// An acquire operation is paired with its corresponding release.
166 ResourceLifetime,
167}
168
169impl OperationEdgeKind {
170 /// Stable identifier used in canonical semantic evidence.
171 #[must_use]
172 pub const fn name(self) -> &'static str {
173 match self {
174 Self::Data => "data",
175 Self::Ordering => "ordering",
176 Self::ResourceLifetime => "resource_lifetime",
177 }
178 }
179}
180
181/// A directed graph edge addressed by graph-local node positions.
182#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
183pub struct OperationEdge {
184 /// Zero-based source node position.
185 pub from: u32,
186 /// Zero-based target node position.
187 pub to: u32,
188 /// The dependency relation.
189 pub kind: OperationEdgeKind,
190}
191
192/// A versioned, compiler-independent restricted semantic graph.
193#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
194pub struct SemanticOperationGraph {
195 /// Vocabulary and normalization contract version.
196 pub schema_version: String,
197 /// Language that supplied the compiler evidence.
198 pub language: Language,
199 /// Build variant that produced the compiler evidence.
200 pub build_variant_fingerprint: [u8; 32],
201 /// Operations in deterministic source order.
202 pub nodes: Vec<OperationNode>,
203 /// Canonically ordered dependencies between operations.
204 pub edges: Vec<OperationEdge>,
205}
206
207impl SemanticOperationGraph {
208 /// Construct a validated graph under the current schema version.
209 ///
210 /// Edges are sorted into one canonical order. Inputs that attempt to add a
211 /// generic operation, an invalid local reference, or an incoherent
212 /// resource pairing are rejected rather than approximated.
213 ///
214 /// # Errors
215 ///
216 /// Returns [`SemanticGraphError`] when a node attribute or edge is outside
217 /// the restricted graph contract.
218 pub fn new(
219 language: Language,
220 build_variant_fingerprint: [u8; 32],
221 nodes: Vec<OperationNode>,
222 mut edges: Vec<OperationEdge>,
223 ) -> Result<Self, SemanticGraphError> {
224 validate_nodes(&nodes)?;
225 edges.sort();
226 if edges.windows(2).any(|pair| pair[0] == pair[1]) {
227 return Err(SemanticGraphError::DuplicateEdge);
228 }
229 for edge in &edges {
230 validate_edge(&nodes, edge)?;
231 }
232 Ok(Self {
233 schema_version: SOG_SCHEMA_VERSION.to_owned(),
234 language,
235 build_variant_fingerprint,
236 nodes,
237 edges,
238 })
239 }
240}
241
242fn validate_nodes(nodes: &[OperationNode]) -> Result<(), SemanticGraphError> {
243 for (index, node) in nodes.iter().enumerate() {
244 let has_resource_kind = node.attributes.resource_kind.is_some();
245 let resource_node = matches!(
246 node.kind,
247 OperationKind::AcquireResource | OperationKind::ReleaseResource
248 );
249 if resource_node && !has_resource_kind {
250 return Err(SemanticGraphError::ResourceKindMissing { index });
251 }
252 if !resource_node && has_resource_kind {
253 return Err(SemanticGraphError::UnexpectedResourceKind { index });
254 }
255 }
256 Ok(())
257}
258
259fn validate_edge(nodes: &[OperationNode], edge: &OperationEdge) -> Result<(), SemanticGraphError> {
260 let from = usize::try_from(edge.from)
261 .map_err(|_| SemanticGraphError::NodeOutOfRange { index: edge.from })?;
262 let to = usize::try_from(edge.to)
263 .map_err(|_| SemanticGraphError::NodeOutOfRange { index: edge.to })?;
264 let Some(source) = nodes.get(from) else {
265 return Err(SemanticGraphError::NodeOutOfRange { index: edge.from });
266 };
267 let Some(target) = nodes.get(to) else {
268 return Err(SemanticGraphError::NodeOutOfRange { index: edge.to });
269 };
270 if edge.from == edge.to {
271 return Err(SemanticGraphError::SelfEdge { index: edge.from });
272 }
273 if edge.kind == OperationEdgeKind::ResourceLifetime
274 && (source.kind != OperationKind::AcquireResource
275 || target.kind != OperationKind::ReleaseResource
276 || source.attributes.resource_kind != target.attributes.resource_kind)
277 {
278 return Err(SemanticGraphError::InvalidResourceLifetime);
279 }
280 Ok(())
281}
282
283/// A graph rejected an operation or relationship outside the restricted model.
284#[derive(Debug, Error, PartialEq, Eq)]
285pub enum SemanticGraphError {
286 /// A caller supplied a source range whose end precedes its start.
287 #[error("semantic source range ends before it starts")]
288 InvalidSourceRange,
289 /// Sidecar source ranges no longer align with the normalized graph.
290 #[error("semantic source range count does not match graph node count")]
291 SourceRangeCountMismatch,
292 /// The graph cannot represent more nodes than its local references allow.
293 #[error("semantic graph has more nodes than local references can represent")]
294 GraphTooLarge,
295 /// An edge referenced no operation in this graph.
296 #[error("operation index {index} is outside the graph")]
297 NodeOutOfRange {
298 /// Graph-local node position that no node occupies.
299 index: u32,
300 },
301 /// An operation cannot depend on itself.
302 #[error("operation index {index} has a self edge")]
303 SelfEdge {
304 /// Graph-local node position used as both endpoints.
305 index: u32,
306 },
307 /// The same edge was supplied more than once.
308 #[error("semantic graph has a duplicate edge")]
309 DuplicateEdge,
310 /// An acquire or release omitted the category required to pair it safely.
311 #[error("resource operation at index {index} has no resource kind")]
312 ResourceKindMissing {
313 /// Graph-local node position of the incomplete resource operation.
314 index: usize,
315 },
316 /// A non-resource operation attempted to carry a resource category.
317 #[error("non-resource operation at index {index} has a resource kind")]
318 UnexpectedResourceKind {
319 /// Graph-local node position carrying unsupported resource metadata.
320 index: usize,
321 },
322 /// A resource edge must pair matching acquire and release operations.
323 #[error("resource lifetime edge does not join matching acquire and release operations")]
324 InvalidResourceLifetime,
325}