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