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

1"""SARIF bridge: reconstruct typed AI objects from ToolResult metadata. 

2 

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""" 

7 

8from __future__ import annotations 

9 

10from typing import TYPE_CHECKING, Any 

11 

12from lintro.ai.enums import ConfidenceLevel 

13from lintro.ai.models import AIFixSuggestion, AISummary 

14 

15if TYPE_CHECKING: 

16 from lintro.models.core.tool_result import ToolResult 

17 

18 

19def _coerce_confidence(value: object) -> ConfidenceLevel: 

20 """Coerce a raw confidence value to the ``ConfidenceLevel`` enum. 

21 

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 

33 

34 

35def suggestions_from_results( 

36 all_results: list[ToolResult], 

37) -> list[AIFixSuggestion]: 

38 """Reconstruct AIFixSuggestion objects from ToolResult AI metadata. 

39 

40 Args: 

41 all_results: List of tool results potentially carrying AI metadata. 

42 

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 

81 

82 

83def summary_from_results( 

84 all_results: list[ToolResult], 

85) -> AISummary | None: 

86 """Reconstruct an AISummary from the first ToolResult that carries one. 

87 

88 Args: 

89 all_results: List of tool results potentially carrying AI metadata. 

90 

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 

112 

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)] 

119 

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