Coverage for lintro / models / core / tool_result.py: 90%
30 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"""Models for core tool execution results.
3This module defines the canonical result object returned by all tools. It
4supports both check and fix flows and includes standardized fields to report
5fixed vs remaining counts for fix-capable tools.
6"""
8from __future__ import annotations
10from collections.abc import Sequence
11from dataclasses import dataclass, field
12from typing import TYPE_CHECKING, Any
14if TYPE_CHECKING:
15 from lintro.parsers.base_issue import BaseIssue
18@dataclass
19class ToolResult:
20 """Result of running a tool.
22 For check operations:
23 - ``issues_count`` represents the number of issues found.
25 For fix/format operations:
26 - ``initial_issues_count`` is the number of issues detected before fixes
27 - ``fixed_issues_count`` is the number of issues the tool auto-fixed
28 - ``remaining_issues_count`` is the number of issues still remaining
29 - ``issues_count`` should mirror ``remaining_issues_count`` for
30 backward compatibility in format-mode summaries
32 The ``issues`` field can contain parsed issue objects (tool-specific) to
33 support unified table formatting.
35 Convention: for FIX action, remaining issues occupy the tail of the
36 ``issues`` list. Tools append all detected issues in order, so the
37 last ``remaining_issues_count`` entries are the ones still unfixed.
38 """
40 name: str = field(default="")
41 success: bool = field(default=False)
42 output: str | None = field(default=None)
43 issues_count: int = field(default=0)
44 formatted_output: str | None = field(default=None)
45 issues: Sequence[BaseIssue] | None = field(default=None)
47 # Optional standardized counts for fix-capable tools
48 initial_issues_count: int | None = field(default=None)
49 fixed_issues_count: int | None = field(default=None)
50 remaining_issues_count: int | None = field(default=None)
52 # Pre-fix issues detected before applying fixes (for displaying what was fixed)
53 initial_issues: Sequence[BaseIssue] | None = field(default=None)
55 # Optional pytest-specific summary data for display
56 pytest_summary: dict[str, Any] | None = field(default=None)
58 # Optional AI-generated metadata (explanations, fix suggestions).
59 # Expected keys (all optional):
60 # "fix_suggestions": list[AIFixSuggestionPayload] (serialized)
61 # "fixed_count": int
62 # "verified_count": int
63 # "unverified_count": int
64 # "telemetry": dict with api_calls, tokens, cost, latency
65 # Built incrementally via helpers in lintro.ai.metadata.
66 ai_metadata: dict[str, Any] | None = field(default=None)
68 # Working directory used during tool execution (for resolving relative
69 # issue file paths in AI fix generation)
70 cwd: str | None = field(default=None)
72 # Skip tracking for tools that didn't execute
73 skipped: bool = field(default=False)
74 skip_reason: str | None = field(default=None)
76 def __post_init__(self) -> None:
77 """Validate that the issue counts and skip state are consistent.
79 Raises:
80 ValueError: If issue counts are inconsistent or skip state is invalid.
81 """
82 # Skipped tools are not failures — they just didn't run
83 if self.skipped:
84 self.success = True
85 if not self.skip_reason:
86 raise ValueError(
87 "skip_reason is required when skipped=True",
88 )
90 if self.skip_reason and not self.skipped:
91 raise ValueError(
92 "skip_reason can only be set when skipped=True",
93 )
95 if (
96 self.initial_issues_count is not None
97 and self.fixed_issues_count is not None
98 and self.remaining_issues_count is not None
99 and self.initial_issues_count
100 != self.fixed_issues_count + self.remaining_issues_count
101 ):
102 raise ValueError(
103 f"Inconsistent issue counts: "
104 f"initial={self.initial_issues_count}, "
105 f"fixed={self.fixed_issues_count}, "
106 f"remaining={self.remaining_issues_count}. "
107 f"Expected: initial = fixed + remaining",
108 )