Skip to main content

code_system_graph_core/
test_links.rs

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/// Declared cross-language test case and its validated HTTP target.
12#[derive(Debug, Clone, PartialEq)]
13pub struct DeclaredTestCase {
14    /// Federated test-case node.
15    pub node: Node,
16    /// Canonical target HTTP method.
17    pub method: String,
18    /// Canonical target HTTP path.
19    pub path: String,
20    /// Evidence from the test source declaration.
21    pub evidence: Evidence,
22}
23
24/// Declared source symbol and the HTTP contract it implements.
25#[derive(Debug, Clone, PartialEq)]
26pub struct DeclaredImplementation {
27    /// Lightweight source symbol node.
28    pub node: Node,
29    /// Canonical implemented HTTP method.
30    pub method: String,
31    /// Canonical implemented HTTP path.
32    pub path: String,
33    /// Evidence from the source declaration and file fingerprint.
34    pub evidence: Evidence,
35}
36
37/// Creates a test-case node from strict declared metadata.
38#[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/// Creates a lightweight implementation anchor from strict declared metadata.
86#[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
132/// Links declared tests to exact HTTP providers with bilateral evidence.
133///
134/// Tests with no observed provider remain unlinked instead of inventing a target.
135///
136/// # Errors
137///
138/// Returns [`LinkError::AmbiguousProvider`] when multiple providers expose the same target.
139pub 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
199/// Links HTTP provider contracts to declared source implementations.
200///
201/// # Errors
202///
203/// Returns [`LinkError::AmbiguousProvider`] when a repository exposes duplicate exact providers.
204pub 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}