"""Manifest dataclasses for collect-changes.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"] Tool = Literal["terragrunt", "tofu"] Action = Literal["create", "update", "delete", "replace", "read", "no-op", "unknown"] Source = Literal["plan", "diff", "both"] @dataclass class InitResult: ok: bool stdout_tail: str stderr_tail: str def to_dict(self) -> dict: return asdict(self) @dataclass class PlanResult: ok: bool stdout_path: str exit_code: int summary: str def to_dict(self) -> dict: return asdict(self) @dataclass class PlanUnit: plan_dir: str tool: Tool init: InitResult plan: PlanResult triggered_by: list[str] terragrunt_changed: bool = False changed_files: list[str] = field(default_factory=list) def to_dict(self) -> dict: return { "plan_dir": self.plan_dir, "tool": self.tool, "init": self.init.to_dict(), "plan": self.plan.to_dict(), "triggered_by": list(self.triggered_by), "terragrunt_changed": self.terragrunt_changed, "changed_files": list(self.changed_files), } @dataclass class TrivyFinding: check_id: str title: str severity: str message: str file: str start_line: int = 0 end_line: int = 0 resource_type: str = "" source: str = "trivy" def to_dict(self) -> dict: return asdict(self) @dataclass class TflintFinding: rule: str severity: str message: str file: str start_line: int = 0 end_line: int = 0 link: str = "" source: str = "tflint" def to_dict(self) -> dict: return asdict(self) @dataclass class CatalogInstance: plan_dir: str address_at_plan: str action: Action def to_dict(self) -> dict: return asdict(self) @dataclass class CatalogEntry: source_dir: str local_address: str type: str source: Source instances: list[CatalogInstance] = field(default_factory=list) block_header: str = "" evidence_line: str = "" key_attributes: dict[str, str | bool | int | list[str]] = field(default_factory=dict) review_context: dict[str, object] = field(default_factory=dict) block_file: str = "" block_start: int = 0 block_end: int = 0 def to_dict(self) -> dict: return { "source_dir": self.source_dir, "local_address": self.local_address, "type": self.type, "source": self.source, "instances": [i.to_dict() for i in self.instances], "block_header": self.block_header, "evidence_line": self.evidence_line, "key_attributes": dict(self.key_attributes), "review_context": dict(self.review_context), "block_file": self.block_file, "block_start": self.block_start, "block_end": self.block_end, } @dataclass class ModuleGraphEntry: callsites: list[str] sibling_modules_at_callsites: list[str] = field(default_factory=list) callsite_local_names: dict[str, dict[str, str]] = field(default_factory=dict) def to_dict(self) -> dict: return { "callsites": list(self.callsites), "sibling_modules_at_callsites": list(self.sibling_modules_at_callsites), "callsite_local_names": { callsite: dict(local_names) for callsite, local_names in self.callsite_local_names.items() }, } @dataclass class Manifest: base_ref: str head_ref: str mode: Mode default_branch: str changed_source_dirs: list[str] plan_units: list[PlanUnit] catalog: list[CatalogEntry] trivy_findings: list[TrivyFinding] module_graph: dict[str, ModuleGraphEntry] errors: list[str] tflint_findings: list[TflintFinding] = field(default_factory=list) tools_unavailable: dict[str, str] = field(default_factory=dict) def to_dict(self) -> dict: return { "base_ref": self.base_ref, "head_ref": self.head_ref, "mode": self.mode, "default_branch": self.default_branch, "changed_source_dirs": list(self.changed_source_dirs), "plan_units": [p.to_dict() for p in self.plan_units], "catalog": [c.to_dict() for c in self.catalog], "trivy_findings": [f.to_dict() for f in self.trivy_findings], "tflint_findings": [f.to_dict() for f in self.tflint_findings], "module_graph": {k: v.to_dict() for k, v in self.module_graph.items()}, "errors": list(self.errors), "tools_unavailable": dict(self.tools_unavailable), } def to_json(self, indent: int = 2) -> str: return json.dumps(self.to_dict(), indent=indent, sort_keys=False)