Coverage for lintro / ai / output / sarif_bridge.py: 71%
56 statements
« prev ^ index » next coverage.py v7.13.0, created at 2026-04-03 18:53 +0000
« prev ^ index » next coverage.py v7.13.0, created at 2026-04-03 18:53 +0000
1"""SARIF bridge: reconstruct typed AI objects from ToolResult metadata.
3This module provides functions to reconstruct ``AIFixSuggestion`` and
4``AISummary`` instances from the serialized metadata dictionaries that
5are attached to ``ToolResult.ai_metadata`` during AI-enhanced runs.
6"""
8from __future__ import annotations
10from typing import TYPE_CHECKING, Any
12from lintro.ai.enums import ConfidenceLevel
13from lintro.ai.models import AIFixSuggestion, AISummary
15if TYPE_CHECKING:
16 from lintro.models.core.tool_result import ToolResult
19def _coerce_confidence(value: object) -> ConfidenceLevel:
20 """Coerce a raw confidence value to the ``ConfidenceLevel`` enum.
22 Accepts enum members, their string names (case-insensitive), or
23 falls back to ``MEDIUM`` for unrecognised values.
24 """
25 if isinstance(value, ConfidenceLevel):
26 return value
27 if isinstance(value, str):
28 try:
29 return ConfidenceLevel(value.lower())
30 except ValueError:
31 return ConfidenceLevel.MEDIUM
32 return ConfidenceLevel.MEDIUM
35def suggestions_from_results(
36 all_results: list[ToolResult],
37) -> list[AIFixSuggestion]:
38 """Reconstruct AIFixSuggestion objects from ToolResult AI metadata.
40 Args:
41 all_results: List of tool results potentially carrying AI metadata.
43 Returns:
44 List of reconstructed AIFixSuggestion objects across all results.
45 """
46 suggestions: list[AIFixSuggestion] = []
47 for result in all_results:
48 if result.ai_metadata is None:
49 continue
50 raw_suggestions = result.ai_metadata.get("fix_suggestions", [])
51 if not isinstance(raw_suggestions, list):
52 continue
53 for raw in raw_suggestions:
54 if not isinstance(raw, dict):
55 continue
56 try:
57 suggestions.append(
58 AIFixSuggestion(
59 file=str(raw.get("file", "")),
60 line=int(raw.get("line", 0)),
61 code=str(raw.get("code", "")),
62 tool_name=str(raw.get("tool_name", "")),
63 original_code=str(raw.get("original_code", "")),
64 suggested_code=str(raw.get("suggested_code", "")),
65 diff=str(raw.get("diff", "")),
66 explanation=str(raw.get("explanation", "")),
67 confidence=_coerce_confidence(
68 raw.get("confidence", ConfidenceLevel.MEDIUM),
69 ),
70 risk_level=str(raw.get("risk_level", "")),
71 input_tokens=int(raw.get("input_tokens", 0)),
72 output_tokens=int(raw.get("output_tokens", 0)),
73 cost_estimate=float(
74 raw.get("cost_estimate", 0.0),
75 ),
76 ),
77 )
78 except (TypeError, ValueError):
79 continue
80 return suggestions
83def summary_from_results(
84 all_results: list[ToolResult],
85) -> AISummary | None:
86 """Reconstruct an AISummary from the first ToolResult that carries one.
88 Args:
89 all_results: List of tool results potentially carrying AI metadata.
91 Returns:
92 Reconstructed AISummary, or None if no summary metadata is found.
93 """
94 for result in all_results:
95 if result.ai_metadata is None:
96 continue
97 raw_summary: dict[str, Any] | None = result.ai_metadata.get("summary")
98 if not isinstance(raw_summary, dict):
99 continue
100 try:
101 in_tok = int(raw_summary.get("input_tokens", 0))
102 except (TypeError, ValueError):
103 in_tok = 0
104 try:
105 out_tok = int(raw_summary.get("output_tokens", 0))
106 except (TypeError, ValueError):
107 out_tok = 0
108 try:
109 cost = float(raw_summary.get("cost_estimate", 0.0))
110 except (TypeError, ValueError):
111 cost = 0.0
113 def _str_list(val: object) -> list[str]:
114 if isinstance(val, list):
115 return [str(x) for x in val]
116 if val is None:
117 return []
118 return [str(val)]
120 return AISummary(
121 overview=str(raw_summary.get("overview", "")),
122 key_patterns=_str_list(raw_summary.get("key_patterns")),
123 priority_actions=_str_list(raw_summary.get("priority_actions")),
124 triage_suggestions=_str_list(raw_summary.get("triage_suggestions")),
125 estimated_effort=str(raw_summary.get("estimated_effort", "")),
126 input_tokens=in_tok,
127 output_tokens=out_tok,
128 cost_estimate=cost,
129 )
130 return None