Finding model for graph state and report output (shape aligned with to_dict).
| 63 | |
| 64 | @dataclass |
| 65 | class Finding: |
| 66 | """Finding model for graph state and report output (shape aligned with to_dict).""" |
| 67 | |
| 68 | rule_id: str |
| 69 | message: str |
| 70 | severity: str = "LOW" |
| 71 | confidence: float = 0.5 |
| 72 | file: str = "SKILL.md" |
| 73 | start_line: int = 1 |
| 74 | end_line: int | None = None |
| 75 | category: str | None = None |
| 76 | pattern: str | None = None |
| 77 | finding: str | None = None # short matched snippet |
| 78 | explanation: str | None = None |
| 79 | remediation: str | None = None |
| 80 | code_snippet: str | None = None |
| 81 | intent: str | None = None |
| 82 | tags: list[str] = field(default_factory=list) |
| 83 | context: str | None = None |
| 84 | matched_text: str | None = None |
| 85 | |
| 86 | def to_dict(self) -> dict[str, object]: |
| 87 | """Return a JSON-serializable dict representation (full finding shape).""" |
| 88 | return { |
| 89 | "id": self.rule_id, |
| 90 | "category": self.category, |
| 91 | "pattern": self.pattern, |
| 92 | "severity": self.severity, |
| 93 | "confidence": self.confidence, |
| 94 | "location": { |
| 95 | "file": self.file, |
| 96 | "start_line": self.start_line, |
| 97 | "end_line": self.end_line, |
| 98 | }, |
| 99 | "finding": self.finding, |
| 100 | "explanation": self.explanation or self.message, |
| 101 | "remediation": self.remediation, |
| 102 | "code_snippet": self.code_snippet or self.context, |
| 103 | "intent": self.intent, |
| 104 | # Tags surface markers like "llm-unconfirmed" (a high-severity static |
| 105 | # finding the LLM filter did not confirm but which is preserved anyway). |
| 106 | "tags": list(self.tags), |
| 107 | } |
| 108 | |
| 109 | def __str__(self) -> str: |
| 110 | return f"{self.rule_id}: {self.message} ({self.file}:{self.start_line})" |
| 111 | |
| 112 | |
| 113 | class AnalyzerPlugin(Protocol): |
no outgoing calls