Skip to content

Explain

jnkn.analysis.explain

Match Explanation Generator for jnkn.

This module provides detailed explanations of why matches were made, including all signals considered, their scores, and alternative matches that were rejected.

Features: - Detailed breakdown of matching process - Shows all signals and their contributions - Lists alternative matches that were rejected - Human-readable formatted output

Classes

AlternativeMatch dataclass

An alternative match that was considered but rejected.

Source code in src/jnkn/analysis/explain.py
@dataclass
class AlternativeMatch:
    """An alternative match that was considered but rejected."""

    node_id: str
    node_name: str
    score: float
    rejection_reason: str
    matched_tokens: List[str] = field(default_factory=list)

ExplanationGenerator

Generate detailed explanations for dependency matches.

Usage

generator = ExplanationGenerator(graph) explanation = generator.explain("env:PAYMENT_DB_HOST", "infra:payment_db_host") print(generator.format(explanation))

Source code in src/jnkn/analysis/explain.py
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
class ExplanationGenerator:
    """
    Generate detailed explanations for dependency matches.

    Usage:
        generator = ExplanationGenerator(graph)
        explanation = generator.explain("env:PAYMENT_DB_HOST", "infra:payment_db_host")
        print(generator.format(explanation))
    """

    def __init__(
        self,
        graph: DependencyGraph | None = None,
        calculator: ConfidenceCalculator | None = None,
        min_confidence: float = 0.5,
    ):
        """
        Initialize the explanation generator.

        Args:
            graph: DependencyGraph instance for looking up nodes
            calculator: ConfidenceCalculator for scoring matches
            min_confidence: Minimum confidence threshold for matches
        """
        self.graph = graph
        self.calculator = calculator or create_default_calculator()
        self.min_confidence = min_confidence

    def explain(
        self,
        source_id: str,
        target_id: str,
        find_alternatives: bool = True,
    ) -> MatchExplanation:
        """
        Generate a detailed explanation for a match.

        Args:
            source_id: Source node ID (e.g., "env:PAYMENT_DB_HOST")
            target_id: Target node ID (e.g., "infra:payment_db_host")
            find_alternatives: Whether to find alternative matches

        Returns:
            MatchExplanation with all details
        """
        # Get source node info
        source_info = self._get_node_info(source_id)
        target_info = self._get_node_info(target_id)

        # Calculate confidence
        confidence_result = self.calculator.calculate(
            source_name=source_info.name,
            target_name=target_info.name,
            source_tokens=source_info.tokens,
            target_tokens=target_info.tokens,
            source_node_id=source_id,
            target_node_id=target_id,
        )

        # Check if edge exists
        edge_exists = False
        edge_metadata = {}
        if self.graph:
            edge = self.graph.get_edge(source_id, target_id)
            if edge:
                edge_exists = True
                edge_metadata = edge.metadata or {}

        # Find alternative matches
        alternatives = []
        if find_alternatives and self.graph:
            alternatives = self._find_alternatives(source_info, target_id)

        return MatchExplanation(
            source=source_info,
            target=target_info,
            confidence_result=confidence_result,
            alternatives=alternatives,
            edge_exists=edge_exists,
            edge_metadata=edge_metadata,
        )

    def explain_why_not(
        self,
        source_id: str,
        target_id: str,
    ) -> str:
        """
        Explain why a match was NOT made.

        Useful for debugging missing connections.

        Args:
            source_id: Source node ID
            target_id: Target node ID

        Returns:
            Human-readable explanation
        """
        explanation = self.explain(source_id, target_id, find_alternatives=False)

        lines = []
        lines.append("=" * 60)
        lines.append("WHY NO MATCH?")
        lines.append("=" * 60)
        lines.append("")

        score = explanation.confidence_result.score

        if score < self.min_confidence:
            lines.append(f"X Score ({score:.2f}) is below threshold ({self.min_confidence:.2f})")
            lines.append("")
            lines.append("Details:")
            lines.append(f"  Source: {source_id}")
            lines.append(f"  Target: {target_id}")
            lines.append(f"  Source tokens: {explanation.source.tokens}")
            lines.append(f"  Target tokens: {explanation.target.tokens}")
            lines.append("")

            # Find common tokens
            source_set = set(explanation.source.tokens)
            target_set = set(explanation.target.tokens)
            common = source_set & target_set

            if not common:
                lines.append("  ! No overlapping tokens found")
            else:
                lines.append(f"  Common tokens: {list(common)}")

            # Show what would need to change
            needed = self.min_confidence - score
            lines.append("")
            lines.append(f"  To reach threshold, need +{needed:.2f} confidence")

            if explanation.confidence_result.penalties:
                lines.append("")
                lines.append("  Penalties applied:")
                for p in explanation.confidence_result.penalties:
                    lines.append(f"    - {p.get('penalty_type')}: x{p.get('multiplier', 1.0):.2f}")

        elif explanation.edge_exists:
            lines.append("V Match DOES exist!")
            lines.append(f"  Score: {score:.2f}")

        else:
            lines.append(f"? Score ({score:.2f}) is above threshold, but no edge found")
            lines.append("  This might indicate the stitcher hasn't run yet")

        lines.append("")
        lines.append("=" * 60)

        return "\n".join(lines)

    def format(self, explanation: MatchExplanation) -> str:
        """
        Format an explanation for CLI output.

        Args:
            explanation: MatchExplanation to format

        Returns:
            Formatted string for display
        """
        lines = []

        # Header
        lines.append("=" * 60)
        lines.append("MATCH EXPLANATION")
        lines.append("=" * 60)
        lines.append("")

        # Source info
        lines.append(f"Source: {explanation.source.id}")
        lines.append(f"  Type: {explanation.source.type}")
        lines.append(f"  Tokens: {explanation.source.tokens}")
        if explanation.source.path:
            loc = explanation.source.path
            if explanation.source.line_number:
                loc += f":{explanation.source.line_number}"
            lines.append(f"  Found in: {loc}")
        lines.append("")

        # Target info
        lines.append(f"Target: {explanation.target.id}")
        lines.append(f"  Type: {explanation.target.type}")
        lines.append(f"  Tokens: {explanation.target.tokens}")
        if explanation.target.path:
            loc = explanation.target.path
            if explanation.target.line_number:
                loc += f":{explanation.target.line_number}"
            lines.append(f"  Found in: {loc}")
        lines.append("")

        # Confidence calculation
        lines.append("-" * 60)
        lines.append("CONFIDENCE CALCULATION")
        lines.append("-" * 60)
        lines.append("")

        lines.append("Base signals:")
        if explanation.confidence_result.signals:
            for signal in explanation.confidence_result.signals:
                weight = signal.get("weight", 0)
                name = signal.get("signal", "unknown")
                details = signal.get("details", "")
                matched_tokens = signal.get("matched_tokens", [])

                if matched_tokens:
                    lines.append(f"  [+{weight:.2f}] {name}: {matched_tokens}")
                elif details:
                    lines.append(f"  [+{weight:.2f}] {name}")
                    lines.append(f"         {details}")
                else:
                    lines.append(f"  [+{weight:.2f}] {name}")
        else:
            lines.append("  (none matched)")

        lines.append("")
        lines.append("Penalties:")
        if explanation.confidence_result.penalties:
            for penalty in explanation.confidence_result.penalties:
                multiplier = penalty.get("multiplier", 1.0)
                name = penalty.get("penalty_type", "unknown")
                reason = penalty.get("reason", "")
                lines.append(f"  [x{multiplier:.2f}] {name}")
                if reason:
                    lines.append(f"         {reason}")
        else:
            lines.append("  None applied")

        lines.append("")

        # Final score
        score = explanation.confidence_result.score
        level = self._get_confidence_level(score)
        lines.append(f"Final confidence: {score:.2f} ({level})")

        # Edge status
        if explanation.edge_exists:
            lines.append("")
            lines.append("Edge Status: EXISTS in graph")
            if explanation.edge_metadata:
                for key, value in explanation.edge_metadata.items():
                    if key not in ("rule", "matched_tokens", "explanation"):
                        lines.append(f"  {key}: {value}")
        else:
            lines.append("")
            if score >= self.min_confidence:
                lines.append("Edge Status: Would be created (above threshold)")
            else:
                lines.append("Edge Status: Would be REJECTED (below threshold)")

        # Alternatives
        if explanation.alternatives:
            lines.append("")
            lines.append("-" * 60)
            lines.append("ALTERNATIVE MATCHES CONSIDERED")
            lines.append("-" * 60)
            lines.append("")

            for alt in sorted(explanation.alternatives, key=lambda x: -x.score):
                lines.append(f"{alt.node_id} -- Score: {alt.score:.2f} ({alt.rejection_reason})")
                if alt.matched_tokens:
                    lines.append(f"  Tokens: {alt.matched_tokens}")

        lines.append("")
        lines.append("=" * 60)

        return "\n".join(lines)

    def format_brief(self, explanation: MatchExplanation) -> str:
        """
        Format a brief, single-line explanation.

        Args:
            explanation: MatchExplanation to format

        Returns:
            Brief formatted string
        """
        score = explanation.confidence_result.score
        level = self._get_confidence_level(score)
        status = (
            "EXISTS"
            if explanation.edge_exists
            else "would be created"
            if score >= self.min_confidence
            else "rejected"
        )

        return (
            f"{explanation.source.id} -> {explanation.target.id}: {score:.2f} ({level}, {status})"
        )

    def _get_node_info(self, node_id: str) -> NodeInfo:
        """
        Get node information from graph or infer from ID.

        Args:
            node_id: Node ID to look up

        Returns:
            NodeInfo with available details
        """
        # Try to get from graph
        if self.graph:
            node = self.graph.get_node(node_id)
            if node:
                return NodeInfo(
                    id=node.id,
                    name=node.name,
                    type=node.type.value,
                    tokens=list(node.tokens),
                    path=node.path,
                    line_number=node.metadata.get("line") if node.metadata else None,
                    metadata=dict(node.metadata) if node.metadata else {},
                )

        # Infer from ID
        name = self._extract_name_from_id(node_id)
        node_type = self._infer_type_from_id(node_id)
        tokens = self._tokenize(name)

        return NodeInfo(
            id=node_id,
            name=name,
            type=node_type,
            tokens=tokens,
        )

    def _find_alternatives(
        self,
        source_info: NodeInfo,
        actual_target_id: str,
        max_alternatives: int = 5,
    ) -> List[AlternativeMatch]:
        """
        Find alternative matches that were considered.

        Args:
            source_info: Source node info
            actual_target_id: The target that was actually matched
            max_alternatives: Maximum alternatives to return

        Returns:
            List of alternative matches
        """
        if not self.graph:
            return []

        alternatives = []

        # Determine what type of nodes to look at based on source type
        if source_info.id.startswith("env:"):
            candidate_types = [NodeType.INFRA_RESOURCE]
        elif source_info.id.startswith("infra:"):
            candidate_types = [NodeType.INFRA_RESOURCE, NodeType.ENV_VAR]
        else:
            candidate_types = [NodeType.INFRA_RESOURCE, NodeType.ENV_VAR, NodeType.DATA_ASSET]

        # Get candidate nodes
        candidates = []
        for node_type in candidate_types:
            candidates.extend(self.graph.get_nodes_by_type(node_type))

        # Score each candidate
        for candidate in candidates:
            if candidate.id == actual_target_id:
                continue

            result = self.calculator.calculate(
                source_name=source_info.name,
                target_name=candidate.name,
                source_tokens=source_info.tokens,
                target_tokens=list(candidate.tokens),
            )

            # Only include if there's some signal
            if result.score > 0:
                if result.score < self.min_confidence:
                    reason = f"rejected: below threshold ({self.min_confidence})"
                else:
                    reason = "not selected: lower score than match"

                alternatives.append(
                    AlternativeMatch(
                        node_id=candidate.id,
                        node_name=candidate.name,
                        score=result.score,
                        rejection_reason=reason,
                        matched_tokens=result.matched_tokens,
                    )
                )

        # Sort by score and limit
        alternatives.sort(key=lambda x: -x.score)
        return alternatives[:max_alternatives]

    def _get_confidence_level(self, score: float) -> str:
        """Get human-readable confidence level."""
        if score >= 0.8:
            return "HIGH"
        elif score >= 0.6:
            return "MEDIUM"
        elif score >= 0.4:
            return "LOW"
        else:
            return "VERY LOW"

    @staticmethod
    def _extract_name_from_id(node_id: str) -> str:
        """Extract node name from ID."""
        # Handle common ID formats
        # Check for :// first (e.g. file://path/to/file)
        if "://" in node_id:
            return node_id.split("://", 1)[1]
        # Then check for : (e.g. env:VAR)
        if ":" in node_id:
            return node_id.split(":", 1)[1]
        return node_id

    @staticmethod
    def _infer_type_from_id(node_id: str) -> str:
        """Infer node type from ID prefix."""
        if node_id.startswith("env:"):
            return "env_var"
        elif node_id.startswith("infra:"):
            return "infra_resource"
        elif node_id.startswith("file://"):
            return "code_file"
        elif node_id.startswith("entity:"):
            return "code_entity"
        elif node_id.startswith("data:"):
            return "data_asset"
        else:
            return "unknown"

    @staticmethod
    def _tokenize(name: str) -> List[str]:
        """Split name into tokens."""
        normalized = name.lower()
        for sep in ["_", ".", "-", "/", ":"]:
            normalized = normalized.replace(sep, " ")
        return [t.strip() for t in normalized.split() if t.strip()]
Functions
__init__(graph=None, calculator=None, min_confidence=0.5)

Initialize the explanation generator.

Parameters:

Name Type Description Default
graph DependencyGraph | None

DependencyGraph instance for looking up nodes

None
calculator ConfidenceCalculator | None

ConfidenceCalculator for scoring matches

None
min_confidence float

Minimum confidence threshold for matches

0.5
Source code in src/jnkn/analysis/explain.py
def __init__(
    self,
    graph: DependencyGraph | None = None,
    calculator: ConfidenceCalculator | None = None,
    min_confidence: float = 0.5,
):
    """
    Initialize the explanation generator.

    Args:
        graph: DependencyGraph instance for looking up nodes
        calculator: ConfidenceCalculator for scoring matches
        min_confidence: Minimum confidence threshold for matches
    """
    self.graph = graph
    self.calculator = calculator or create_default_calculator()
    self.min_confidence = min_confidence
explain(source_id, target_id, find_alternatives=True)

Generate a detailed explanation for a match.

Parameters:

Name Type Description Default
source_id str

Source node ID (e.g., "env:PAYMENT_DB_HOST")

required
target_id str

Target node ID (e.g., "infra:payment_db_host")

required
find_alternatives bool

Whether to find alternative matches

True

Returns:

Type Description
MatchExplanation

MatchExplanation with all details

Source code in src/jnkn/analysis/explain.py
def explain(
    self,
    source_id: str,
    target_id: str,
    find_alternatives: bool = True,
) -> MatchExplanation:
    """
    Generate a detailed explanation for a match.

    Args:
        source_id: Source node ID (e.g., "env:PAYMENT_DB_HOST")
        target_id: Target node ID (e.g., "infra:payment_db_host")
        find_alternatives: Whether to find alternative matches

    Returns:
        MatchExplanation with all details
    """
    # Get source node info
    source_info = self._get_node_info(source_id)
    target_info = self._get_node_info(target_id)

    # Calculate confidence
    confidence_result = self.calculator.calculate(
        source_name=source_info.name,
        target_name=target_info.name,
        source_tokens=source_info.tokens,
        target_tokens=target_info.tokens,
        source_node_id=source_id,
        target_node_id=target_id,
    )

    # Check if edge exists
    edge_exists = False
    edge_metadata = {}
    if self.graph:
        edge = self.graph.get_edge(source_id, target_id)
        if edge:
            edge_exists = True
            edge_metadata = edge.metadata or {}

    # Find alternative matches
    alternatives = []
    if find_alternatives and self.graph:
        alternatives = self._find_alternatives(source_info, target_id)

    return MatchExplanation(
        source=source_info,
        target=target_info,
        confidence_result=confidence_result,
        alternatives=alternatives,
        edge_exists=edge_exists,
        edge_metadata=edge_metadata,
    )
explain_why_not(source_id, target_id)

Explain why a match was NOT made.

Useful for debugging missing connections.

Parameters:

Name Type Description Default
source_id str

Source node ID

required
target_id str

Target node ID

required

Returns:

Type Description
str

Human-readable explanation

Source code in src/jnkn/analysis/explain.py
def explain_why_not(
    self,
    source_id: str,
    target_id: str,
) -> str:
    """
    Explain why a match was NOT made.

    Useful for debugging missing connections.

    Args:
        source_id: Source node ID
        target_id: Target node ID

    Returns:
        Human-readable explanation
    """
    explanation = self.explain(source_id, target_id, find_alternatives=False)

    lines = []
    lines.append("=" * 60)
    lines.append("WHY NO MATCH?")
    lines.append("=" * 60)
    lines.append("")

    score = explanation.confidence_result.score

    if score < self.min_confidence:
        lines.append(f"X Score ({score:.2f}) is below threshold ({self.min_confidence:.2f})")
        lines.append("")
        lines.append("Details:")
        lines.append(f"  Source: {source_id}")
        lines.append(f"  Target: {target_id}")
        lines.append(f"  Source tokens: {explanation.source.tokens}")
        lines.append(f"  Target tokens: {explanation.target.tokens}")
        lines.append("")

        # Find common tokens
        source_set = set(explanation.source.tokens)
        target_set = set(explanation.target.tokens)
        common = source_set & target_set

        if not common:
            lines.append("  ! No overlapping tokens found")
        else:
            lines.append(f"  Common tokens: {list(common)}")

        # Show what would need to change
        needed = self.min_confidence - score
        lines.append("")
        lines.append(f"  To reach threshold, need +{needed:.2f} confidence")

        if explanation.confidence_result.penalties:
            lines.append("")
            lines.append("  Penalties applied:")
            for p in explanation.confidence_result.penalties:
                lines.append(f"    - {p.get('penalty_type')}: x{p.get('multiplier', 1.0):.2f}")

    elif explanation.edge_exists:
        lines.append("V Match DOES exist!")
        lines.append(f"  Score: {score:.2f}")

    else:
        lines.append(f"? Score ({score:.2f}) is above threshold, but no edge found")
        lines.append("  This might indicate the stitcher hasn't run yet")

    lines.append("")
    lines.append("=" * 60)

    return "\n".join(lines)
format(explanation)

Format an explanation for CLI output.

Parameters:

Name Type Description Default
explanation MatchExplanation

MatchExplanation to format

required

Returns:

Type Description
str

Formatted string for display

Source code in src/jnkn/analysis/explain.py
def format(self, explanation: MatchExplanation) -> str:
    """
    Format an explanation for CLI output.

    Args:
        explanation: MatchExplanation to format

    Returns:
        Formatted string for display
    """
    lines = []

    # Header
    lines.append("=" * 60)
    lines.append("MATCH EXPLANATION")
    lines.append("=" * 60)
    lines.append("")

    # Source info
    lines.append(f"Source: {explanation.source.id}")
    lines.append(f"  Type: {explanation.source.type}")
    lines.append(f"  Tokens: {explanation.source.tokens}")
    if explanation.source.path:
        loc = explanation.source.path
        if explanation.source.line_number:
            loc += f":{explanation.source.line_number}"
        lines.append(f"  Found in: {loc}")
    lines.append("")

    # Target info
    lines.append(f"Target: {explanation.target.id}")
    lines.append(f"  Type: {explanation.target.type}")
    lines.append(f"  Tokens: {explanation.target.tokens}")
    if explanation.target.path:
        loc = explanation.target.path
        if explanation.target.line_number:
            loc += f":{explanation.target.line_number}"
        lines.append(f"  Found in: {loc}")
    lines.append("")

    # Confidence calculation
    lines.append("-" * 60)
    lines.append("CONFIDENCE CALCULATION")
    lines.append("-" * 60)
    lines.append("")

    lines.append("Base signals:")
    if explanation.confidence_result.signals:
        for signal in explanation.confidence_result.signals:
            weight = signal.get("weight", 0)
            name = signal.get("signal", "unknown")
            details = signal.get("details", "")
            matched_tokens = signal.get("matched_tokens", [])

            if matched_tokens:
                lines.append(f"  [+{weight:.2f}] {name}: {matched_tokens}")
            elif details:
                lines.append(f"  [+{weight:.2f}] {name}")
                lines.append(f"         {details}")
            else:
                lines.append(f"  [+{weight:.2f}] {name}")
    else:
        lines.append("  (none matched)")

    lines.append("")
    lines.append("Penalties:")
    if explanation.confidence_result.penalties:
        for penalty in explanation.confidence_result.penalties:
            multiplier = penalty.get("multiplier", 1.0)
            name = penalty.get("penalty_type", "unknown")
            reason = penalty.get("reason", "")
            lines.append(f"  [x{multiplier:.2f}] {name}")
            if reason:
                lines.append(f"         {reason}")
    else:
        lines.append("  None applied")

    lines.append("")

    # Final score
    score = explanation.confidence_result.score
    level = self._get_confidence_level(score)
    lines.append(f"Final confidence: {score:.2f} ({level})")

    # Edge status
    if explanation.edge_exists:
        lines.append("")
        lines.append("Edge Status: EXISTS in graph")
        if explanation.edge_metadata:
            for key, value in explanation.edge_metadata.items():
                if key not in ("rule", "matched_tokens", "explanation"):
                    lines.append(f"  {key}: {value}")
    else:
        lines.append("")
        if score >= self.min_confidence:
            lines.append("Edge Status: Would be created (above threshold)")
        else:
            lines.append("Edge Status: Would be REJECTED (below threshold)")

    # Alternatives
    if explanation.alternatives:
        lines.append("")
        lines.append("-" * 60)
        lines.append("ALTERNATIVE MATCHES CONSIDERED")
        lines.append("-" * 60)
        lines.append("")

        for alt in sorted(explanation.alternatives, key=lambda x: -x.score):
            lines.append(f"{alt.node_id} -- Score: {alt.score:.2f} ({alt.rejection_reason})")
            if alt.matched_tokens:
                lines.append(f"  Tokens: {alt.matched_tokens}")

    lines.append("")
    lines.append("=" * 60)

    return "\n".join(lines)
format_brief(explanation)

Format a brief, single-line explanation.

Parameters:

Name Type Description Default
explanation MatchExplanation

MatchExplanation to format

required

Returns:

Type Description
str

Brief formatted string

Source code in src/jnkn/analysis/explain.py
def format_brief(self, explanation: MatchExplanation) -> str:
    """
    Format a brief, single-line explanation.

    Args:
        explanation: MatchExplanation to format

    Returns:
        Brief formatted string
    """
    score = explanation.confidence_result.score
    level = self._get_confidence_level(score)
    status = (
        "EXISTS"
        if explanation.edge_exists
        else "would be created"
        if score >= self.min_confidence
        else "rejected"
    )

    return (
        f"{explanation.source.id} -> {explanation.target.id}: {score:.2f} ({level}, {status})"
    )

MatchExplanation dataclass

Complete explanation of a match.

Source code in src/jnkn/analysis/explain.py
@dataclass
class MatchExplanation:
    """Complete explanation of a match."""

    source: NodeInfo
    target: NodeInfo
    confidence_result: ConfidenceResult
    alternatives: List[AlternativeMatch] = field(default_factory=list)
    edge_exists: bool = False
    edge_metadata: Dict[str, Any] = field(default_factory=dict)

NodeInfo dataclass

Information about a node for explanation.

Source code in src/jnkn/analysis/explain.py
@dataclass
class NodeInfo:
    """Information about a node for explanation."""

    id: str
    name: str
    type: str
    tokens: List[str]
    path: str | None = None
    line_number: int | None = None
    metadata: Dict[str, Any] = field(default_factory=dict)

Functions

create_explanation_generator(graph=None, min_confidence=0.5)

Factory function to create an ExplanationGenerator.

Parameters:

Name Type Description Default
graph DependencyGraph | None

Optional DependencyGraph

None
min_confidence float

Minimum confidence threshold

0.5

Returns:

Type Description
ExplanationGenerator

Configured ExplanationGenerator

Source code in src/jnkn/analysis/explain.py
def create_explanation_generator(
    graph: DependencyGraph | None = None,
    min_confidence: float = 0.5,
) -> ExplanationGenerator:
    """
    Factory function to create an ExplanationGenerator.

    Args:
        graph: Optional DependencyGraph
        min_confidence: Minimum confidence threshold

    Returns:
        Configured ExplanationGenerator
    """
    return ExplanationGenerator(
        graph=graph,
        calculator=create_default_calculator(),
        min_confidence=min_confidence,
    )