"""Manifest dataclasses for collect-findings.py output. The manifest is the contract between the script and the review subagents. Schema mirrors DESIGN.md. """ from __future__ import annotations import json from dataclasses import dataclass, field, asdict from typing import Literal Mode = Literal["local", "ref"] Severity = Literal["critical", "high", "medium", "low", "info"] Language = Literal[ "python", "javascript", "typescript", "csharp", "lua", "powershell", "github-actions", ] @dataclass class Finding: tool: str rule_id: str severity: Severity file: str line: int end_line: int message: str cwe: str | None = None fix_suggestion: str | None = None def to_dict(self) -> dict: return asdict(self) @dataclass class ChangedFile: path: str language: str added_lines: list[tuple[int, int]] def to_dict(self) -> dict: return { "path": self.path, "language": self.language, "added_lines": [list(r) for r in self.added_lines], } @dataclass class LanguageBreakdown: python: int = 0 javascript: int = 0 typescript: int = 0 csharp: int = 0 lua: int = 0 powershell: int = 0 github_actions: int = 0 skipped_files: list[str] = field(default_factory=list) def to_dict(self) -> dict: return asdict(self) @dataclass class PackageEntry: name: str version: str def to_dict(self) -> dict: return asdict(self) @dataclass class PackageUpgrade: name: str from_version: str to_version: str def to_dict(self) -> dict: return {"name": self.name, "from": self.from_version, "to": self.to_version} @dataclass class PackageDiff: added: list[PackageEntry] = field(default_factory=list) removed: list[PackageEntry] = field(default_factory=list) upgraded: list[PackageUpgrade] = field(default_factory=list) def to_dict(self) -> dict: return { "added": [p.to_dict() for p in self.added], "removed": [p.to_dict() for p in self.removed], "upgraded": [u.to_dict() for u in self.upgraded], } @dataclass class ToolStat: ran: bool pre_filter: int = 0 post_filter: int = 0 reason: str | None = None def to_dict(self) -> dict: d: dict = {"ran": self.ran} if self.ran: d["pre_filter"] = self.pre_filter d["post_filter"] = self.post_filter else: d["reason"] = self.reason or "" return d @dataclass class Manifest: mode: Mode base_ref: str head_ref: str default_branch: str language_breakdown: LanguageBreakdown changed_files: list[ChangedFile] findings: list[Finding] package_diffs: dict[str, PackageDiff] tool_stats: dict[str, ToolStat] tools_unavailable: dict[str, str] errors: list[str] def to_dict(self) -> dict: return { "mode": self.mode, "base_ref": self.base_ref, "head_ref": self.head_ref, "default_branch": self.default_branch, "language_breakdown": self.language_breakdown.to_dict(), "changed_files": [c.to_dict() for c in self.changed_files], "findings": [f.to_dict() for f in self.findings], "package_diffs": {k: v.to_dict() for k, v in self.package_diffs.items()}, "tool_stats": {k: v.to_dict() for k, v in self.tool_stats.items()}, "tools_unavailable": dict(self.tools_unavailable), "errors": list(self.errors), } def to_json(self, indent: int = 2) -> str: return json.dumps(self.to_dict(), indent=indent, sort_keys=False)