graphlens-link 0.5.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,6 @@
1
+ Metadata-Version: 2.3
2
+ Name: graphlens-link
3
+ Version: 0.5.0
4
+ Summary: Cross-language boundary linker for graphlens
5
+ Requires-Dist: graphlens
6
+ Requires-Python: >=3.13
@@ -0,0 +1,33 @@
1
+ [project]
2
+ name = "graphlens-link"
3
+ version = "0.5.0"
4
+ description = "Cross-language boundary linker for graphlens"
5
+ requires-python = ">=3.13"
6
+ dependencies = [
7
+ "graphlens",
8
+ ]
9
+
10
+ [build-system]
11
+ requires = ["uv_build>=0.9.18,<0.12.0"]
12
+ build-backend = "uv_build"
13
+
14
+ [tool.uv.sources]
15
+ graphlens = { workspace = true }
16
+
17
+ [tool.bandit]
18
+ skips = ["B101"]
19
+
20
+ [tool.pytest.ini_options]
21
+ testpaths = ["tests"]
22
+
23
+ [tool.coverage.run]
24
+ source = ["graphlens_link"]
25
+
26
+ [tool.coverage.report]
27
+ fail_under = 100
28
+ show_missing = true
29
+ exclude_lines = [
30
+ "pragma: no cover",
31
+ "if TYPE_CHECKING:",
32
+ "\\.\\.\\.",
33
+ ]
@@ -0,0 +1,5 @@
1
+ """Cross-language boundary linker for graphlens graphs."""
2
+
3
+ from graphlens_link._linker import LinkResult, link_graph
4
+
5
+ __all__ = ["LinkResult", "link_graph"]
@@ -0,0 +1,101 @@
1
+ """Cross-language boundary linker: a pure ``graph -> graph`` transform."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ from typing import TYPE_CHECKING
7
+
8
+ from graphlens import NodeKind, Relation, RelationKind
9
+
10
+ if TYPE_CHECKING:
11
+ from graphlens import GraphLens
12
+
13
+
14
+ @dataclass(frozen=True)
15
+ class LinkResult:
16
+ """Summary of a single cross-language link pass."""
17
+
18
+ relations_added: int
19
+ boundaries_total: int
20
+ boundaries_linked: int
21
+ """Boundaries that had at least one provider *and* one consumer."""
22
+
23
+
24
+ def _as_float(value: object, default: float = 1.0) -> float:
25
+ """Coerce a metadata value to a float, falling back to ``default``."""
26
+ if isinstance(value, bool):
27
+ return default
28
+ if isinstance(value, (int, float)):
29
+ return float(value)
30
+ return default
31
+
32
+
33
+ def link_graph(graph: GraphLens, *, min_confidence: float = 0.0) -> LinkResult:
34
+ """
35
+ Add ``COMMUNICATES_WITH`` edges across cross-language boundaries.
36
+
37
+ Adapters emit a shared ``BOUNDARY`` node per contract (id derived purely
38
+ from mechanism + key) with ``EXPOSES`` edges from servers and
39
+ ``CONSUMES`` edges from clients. This pass pairs, for every boundary,
40
+ each consumer with each provider and adds a directed
41
+ ``consumer -> provider`` edge carrying the boundary's ``mechanism`` and
42
+ the product of the two sides' confidences.
43
+
44
+ The graph is mutated in place. The pass is idempotent: an edge for the
45
+ same ``(source, target, boundary)`` is never added twice, so it is safe
46
+ to run after re-analyzing part of the graph. Two distinct boundaries
47
+ between the same consumer/provider pair each get their own edge. Pairs
48
+ whose combined confidence is below ``min_confidence`` are skipped.
49
+ """
50
+ added = 0
51
+ linked = 0
52
+ boundaries = graph.nodes_by_kind(NodeKind.BOUNDARY)
53
+ existing: set[tuple[str, str, str]] = {
54
+ (r.source_id, r.target_id, str(r.metadata.get("boundary_id", "")))
55
+ for r in graph.relations
56
+ if r.kind == RelationKind.COMMUNICATES_WITH
57
+ }
58
+ for boundary in boundaries:
59
+ consumers = graph.incoming(boundary.id, RelationKind.CONSUMES)
60
+ providers = graph.incoming(boundary.id, RelationKind.EXPOSES)
61
+ if not consumers or not providers:
62
+ continue
63
+ linked += 1
64
+ mechanism = str(boundary.metadata.get("mechanism", ""))
65
+ boundary_key = str(boundary.metadata.get("key", ""))
66
+ for consumer in consumers:
67
+ for provider in providers:
68
+ if consumer.source_id == provider.source_id:
69
+ continue
70
+ confidence = _as_float(
71
+ consumer.metadata.get("confidence")
72
+ ) * _as_float(provider.metadata.get("confidence"))
73
+ if confidence < min_confidence:
74
+ continue
75
+ dedupe = (
76
+ consumer.source_id,
77
+ provider.source_id,
78
+ boundary.id,
79
+ )
80
+ if dedupe in existing:
81
+ continue
82
+ existing.add(dedupe)
83
+ graph.add_relation(
84
+ Relation(
85
+ source_id=consumer.source_id,
86
+ target_id=provider.source_id,
87
+ kind=RelationKind.COMMUNICATES_WITH,
88
+ metadata={
89
+ "mechanism": mechanism,
90
+ "boundary_id": boundary.id,
91
+ "boundary_key": boundary_key,
92
+ "confidence": confidence,
93
+ },
94
+ )
95
+ )
96
+ added += 1
97
+ return LinkResult(
98
+ relations_added=added,
99
+ boundaries_total=len(boundaries),
100
+ boundaries_linked=linked,
101
+ )