1use std::collections::BTreeMap;
2
3use code_system_graph_model::{
4 Edge, EdgeId, EdgeKind, EpistemicStatus, Evidence, EvidenceId, Node, NodeId, NodeKind, Provenance, RepoId, stable_id
5};
6
7use crate::linker::{http_link_ambiguity, sort_http_ambiguities};
8use crate::{
9 BoundaryRole, ContractImplementationConfig, HttpBoundary, HttpLinkResolution, IntegrationTestConfig, LinkError, normalize_http_path
10};
11
12#[derive(Debug, Clone, PartialEq)]
14pub struct DeclaredTestCase {
15 pub node: Node,
17 pub method: String,
19 pub path: String,
21 pub evidence: Evidence,
23}
24
25#[derive(Debug, Clone, PartialEq)]
27pub struct DeclaredImplementation {
28 pub node: Node,
30 pub method: String,
32 pub path: String,
34 pub evidence: Evidence,
36}
37
38#[must_use]
40pub fn declared_test_case(
41 repo_id: RepoId,
42 config: &IntegrationTestConfig,
43 content_hash: Option<String>,
44) -> DeclaredTestCase {
45 let method = config.validates.method.trim().to_ascii_uppercase();
46 let path = normalize_http_path(&config.validates.path);
47 let stable_key = format!(
48 "test:{}:{}:{}:{}:{}",
49 repo_id.as_str(),
50 config.language.trim().to_ascii_lowercase(),
51 config.framework.trim().to_ascii_lowercase(),
52 config.path,
53 config.name
54 );
55 let evidence_key = format!("{stable_key}:{method}:{path}");
56 DeclaredTestCase {
57 node: Node {
58 id: NodeId::new(stable_id("node", &stable_key)),
59 kind: NodeKind::TestCase,
60 repo_id: Some(repo_id.clone()),
61 stable_key,
62 label: format!("{}/{}::{}", config.language, config.framework, config.name),
63 },
64 method,
65 path,
66 evidence: Evidence {
67 id: EvidenceId::new(stable_id("evidence", &evidence_key)),
68 repo_id: Some(repo_id),
69 file_path: Some(config.path.clone()),
70 start_line: None,
71 end_line: None,
72 extractor: "code-system-graph.tests.declared".to_owned(),
73 extractor_version: env!("CARGO_PKG_VERSION").to_owned(),
74 provenance: Provenance::Declared,
75 confidence: 1.0,
76 observed_at_commit: None,
77 content_hash,
78 note: Some(format!(
79 "{} test declared with {}",
80 config.language, config.framework
81 )),
82 },
83 }
84}
85
86#[must_use]
88pub fn declared_implementation(
89 repo_id: RepoId,
90 config: &ContractImplementationConfig,
91 content_hash: Option<String>,
92) -> DeclaredImplementation {
93 let method = config.implements.method.trim().to_ascii_uppercase();
94 let path = normalize_http_path(&config.implements.path);
95 let stable_key = format!(
96 "symbol:{}:{}:{}:{}",
97 repo_id.as_str(),
98 config.language.trim().to_ascii_lowercase(),
99 config.path,
100 config.symbol
101 );
102 let evidence_key = format!("{stable_key}:{method}:{path}");
103 DeclaredImplementation {
104 node: Node {
105 id: NodeId::new(stable_id("node", &stable_key)),
106 kind: NodeKind::SymbolRef,
107 repo_id: Some(repo_id.clone()),
108 stable_key,
109 label: format!("{}::{}", config.language, config.symbol),
110 },
111 method,
112 path,
113 evidence: Evidence {
114 id: EvidenceId::new(stable_id("evidence", &evidence_key)),
115 repo_id: Some(repo_id),
116 file_path: Some(config.path.clone()),
117 start_line: None,
118 end_line: None,
119 extractor: "code-system-graph.implementations.declared".to_owned(),
120 extractor_version: env!("CARGO_PKG_VERSION").to_owned(),
121 provenance: Provenance::Declared,
122 confidence: 1.0,
123 observed_at_commit: None,
124 content_hash,
125 note: Some(format!(
126 "{} symbol declared as contract implementation",
127 config.language
128 )),
129 },
130 }
131}
132
133pub fn link_declared_tests(
145 tests: &[DeclaredTestCase],
146 boundaries: &[HttpBoundary],
147) -> Result<Vec<Edge>, LinkError> {
148 link_declared_tests_with_ambiguities(tests, boundaries).into_legacy_result()
149}
150
151#[must_use]
153pub fn link_declared_tests_with_ambiguities(
154 tests: &[DeclaredTestCase],
155 boundaries: &[HttpBoundary],
156) -> HttpLinkResolution {
157 let mut providers: BTreeMap<(&str, &str), Vec<&HttpBoundary>> = BTreeMap::new();
158 for provider in boundaries
159 .iter()
160 .filter(|boundary| boundary.role == BoundaryRole::Provider)
161 {
162 let candidates = providers
163 .entry((&provider.method, &provider.path))
164 .or_default();
165 if let Some(existing) = candidates
166 .iter()
167 .position(|candidate| candidate.node.id == provider.node.id)
168 {
169 if provider.evidence.confidence > candidates[existing].evidence.confidence {
170 candidates[existing] = provider;
171 }
172 } else {
173 candidates.push(provider);
174 }
175 }
176 let mut edges = Vec::new();
177 let mut ambiguities = Vec::new();
178 for test in tests {
179 let Some(candidates) = providers.get(&(test.method.as_str(), test.path.as_str())) else {
180 continue;
181 };
182 if candidates.len() > 1 {
183 ambiguities.push(http_link_ambiguity(
184 &test.method,
185 &test.path,
186 candidates.iter().map(|candidate| &candidate.node.id),
187 ));
188 continue;
189 }
190 let provider = candidates[0];
191 let edge_key = format!(
192 "{}:validates:{}",
193 test.node.id.as_str(),
194 provider.node.id.as_str()
195 );
196 edges.push(Edge {
197 id: EdgeId::new(stable_id("edge", &edge_key)),
198 source: test.node.id.clone(),
199 target: provider.node.id.clone(),
200 kind: EdgeKind::Validates,
201 confidence: test.evidence.confidence.min(provider.evidence.confidence),
202 status: consensus_status(test.evidence.confidence.min(provider.evidence.confidence)),
203 evidence: vec![test.evidence.id.clone(), provider.evidence.id.clone()],
204 });
205 }
206 edges.sort_by(|left, right| left.id.cmp(&right.id));
207 sort_http_ambiguities(&mut ambiguities);
208 HttpLinkResolution { edges, ambiguities }
209}
210
211pub fn link_declared_implementations(
221 implementations: &[DeclaredImplementation],
222 boundaries: &[HttpBoundary],
223) -> Result<Vec<Edge>, LinkError> {
224 link_declared_implementations_with_ambiguities(implementations, boundaries).into_legacy_result()
225}
226
227#[must_use]
229pub fn link_declared_implementations_with_ambiguities(
230 implementations: &[DeclaredImplementation],
231 boundaries: &[HttpBoundary],
232) -> HttpLinkResolution {
233 let mut edges = Vec::new();
234 let mut ambiguities = Vec::new();
235 for implementation in implementations {
236 let candidates = boundaries
237 .iter()
238 .filter(|boundary| {
239 boundary.role == BoundaryRole::Provider
240 && boundary.node.repo_id == implementation.node.repo_id
241 && boundary.method == implementation.method
242 && boundary.path == implementation.path
243 })
244 .fold(
245 BTreeMap::<NodeId, &HttpBoundary>::new(),
246 |mut candidates, boundary| {
247 candidates
248 .entry(boundary.node.id.clone())
249 .and_modify(|existing| {
250 if boundary.evidence.confidence > existing.evidence.confidence {
251 *existing = boundary;
252 }
253 })
254 .or_insert(boundary);
255 candidates
256 },
257 )
258 .into_values()
259 .collect::<Vec<_>>();
260 if candidates.len() > 1 {
261 ambiguities.push(http_link_ambiguity(
262 &implementation.method,
263 &implementation.path,
264 candidates.iter().map(|candidate| &candidate.node.id),
265 ));
266 continue;
267 }
268 let Some(provider) = candidates.first() else {
269 continue;
270 };
271 let edge_key = format!(
272 "{}:implemented_by:{}",
273 provider.node.id.as_str(),
274 implementation.node.id.as_str()
275 );
276 let confidence = provider
277 .evidence
278 .confidence
279 .min(implementation.evidence.confidence);
280 edges.push(Edge {
281 id: EdgeId::new(stable_id("edge", &edge_key)),
282 source: provider.node.id.clone(),
283 target: implementation.node.id.clone(),
284 kind: EdgeKind::ImplementedBy,
285 confidence,
286 status: consensus_status(confidence),
287 evidence: vec![
288 provider.evidence.id.clone(),
289 implementation.evidence.id.clone(),
290 ],
291 });
292 }
293 edges.sort_by(|left, right| left.id.cmp(&right.id));
294 sort_http_ambiguities(&mut ambiguities);
295 HttpLinkResolution { edges, ambiguities }
296}
297
298fn consensus_status(confidence: f32) -> EpistemicStatus {
299 if confidence >= 1.0 {
300 EpistemicStatus::Confirmed
301 } else {
302 EpistemicStatus::Inferred
303 }
304}
305
306#[cfg(test)]
307mod tests {
308 use code_system_graph_model::{EdgeKind, EpistemicStatus, RepoId};
309
310 use super::{declared_test_case, link_declared_implementations, link_declared_tests};
311 use crate::{
312 HttpContractConfig, IntegrationTestConfig, extract_openapi, parse_rust_source, source_observations_to_graph
313 };
314
315 #[test]
316 fn declared_python_test_should_validate_rust_provider_with_bilateral_evidence() {
317 let test = declared_test_case(
318 RepoId::new("repo:tests"),
319 &IntegrationTestConfig {
320 name: "test_create_order".to_owned(),
321 path: "tests/test_orders.py".to_owned(),
322 framework: "pytest".to_owned(),
323 language: "python".to_owned(),
324 validates: HttpContractConfig {
325 method: "POST".to_owned(),
326 path: "/api/orders".to_owned(),
327 },
328 },
329 Some("content:test".to_owned()),
330 );
331 let providers = extract_openapi(
332 &RepoId::new("repo:rust-api"),
333 "openapi.yaml",
334 "openapi: 3.1.0\npaths:\n /api/orders:\n post: {}\n",
335 );
336 let result = providers
337 .map_err(|error| error.to_string())
338 .and_then(|providers| {
339 link_declared_tests(&[test], &providers).map_err(|error| error.to_string())
340 });
341
342 assert!(matches!(
343 result,
344 Ok(edges)
345 if edges.len() == 1
346 && edges[0].kind == EdgeKind::Validates
347 && edges[0].evidence.len() == 2
348 ));
349 }
350
351 #[test]
352 fn executable_route_should_outweigh_divergent_utoipa_advisory() {
353 let repo = RepoId::new("repo:rust-api");
354 let observations = parse_rust_source(
355 r#"
356use actix_web::get;
357
358#[utoipa::path(get, path = "/documented")]
359#[get("/runtime")]
360async fn handler() {}
361"#,
362 );
363 let facts =
364 source_observations_to_graph(&repo, "src/routes.rs", "content:routes", &observations);
365 let edges = link_declared_implementations(&facts.implementations, &facts.boundaries)
366 .expect("both exact declarations should remain linkable");
367 let mut consensus = edges
368 .iter()
369 .filter_map(|edge| {
370 let path = facts
371 .boundaries
372 .iter()
373 .find(|boundary| boundary.node.id == edge.source)?
374 .path
375 .clone();
376 Some((path, edge.confidence, edge.status))
377 })
378 .collect::<Vec<_>>();
379 consensus.sort_by(|left, right| left.0.cmp(&right.0));
380
381 assert_eq!(
382 consensus,
383 vec![
384 ("/documented".to_owned(), 0.75, EpistemicStatus::Inferred),
385 ("/runtime".to_owned(), 1.0, EpistemicStatus::Confirmed),
386 ]
387 );
388 }
389}