#!/usr/bin/env python3 """Render one-shot 1C saved-state watch manifest as Markdown.""" from __future__ import annotations import argparse import json from pathlib import Path from typing import Any def load_json(path: Path) -> dict[str, Any]: return json.loads(path.read_text(encoding="utf-8-sig")) def line(text: str = "") -> str: return text.rstrip() + "\n" def bullet(text: str) -> str: return f"- {text}\n" def code(value: Any) -> str: if value is None: return "`null`" return f"`{value}`" def linked_path(manifest_path: Path, value: Any) -> Path | None: if not value: return None path = Path(str(value)) if path.is_absolute(): return path return (manifest_path.parent / path).resolve() def render_delta_summary(manifest_path: Path, manifest: dict[str, Any]) -> str: delta_path = linked_path(manifest_path, manifest.get("delta")) if not delta_path or not delta_path.exists(): return "" delta = load_json(delta_path) objects = delta.get("objects") or {} changed = objects.get("changed") or [] added = objects.get("added") or [] removed = objects.get("removed") or [] if not changed and not added and not removed: return "" out = line() out += line("## Delta Objects") for item in added: out += bullet(f"Added {code(item.get('full_name'))}") for item in removed: out += bullet(f"Removed {code(item.get('full_name'))}") for item in changed: out += bullet(f"Changed {code(item.get('full_name'))}") term_delta = item.get("term_delta") or {} added_terms = ((term_delta.get("added_terms") or {}).get("added") or []) removed_terms = ((term_delta.get("removed_terms") or {}).get("added") or []) if added_terms: out += f" New added terms: {', '.join(code(value) for value in added_terms)}\n" if removed_terms: out += f" New removed terms: {', '.join(code(value) for value in removed_terms)}\n" for part in (item.get("part_delta") or {}).get("changed") or []: before = part.get("before") or {} after = part.get("after") or {} out += ( f" Part changed: {code(part.get('file_name'))}, " f"role={code(after.get('payload_role'))}, " f"delta_chars {code(before.get('delta_chars'))}->{code(after.get('delta_chars'))}\n" ) return out def render(manifest_path: Path, manifest: dict[str, Any]) -> str: counts = manifest.get("counts") or {} safety = manifest.get("safety") or {} out = "" out += line("# 1C Saved State Watch Run") out += line() out += line(f"Database: `{manifest.get('database')}`") out += line(f"Run dir: `{manifest.get('run_dir')}`") out += line(f"Previous report: `{manifest.get('previous_report')}`") out += line() out += line("## Artifacts") for key in ("report", "markdown", "check", "manifest_check", "delta", "delta_markdown", "delta_check"): if manifest.get(key): out += bullet(f"{key}: `{manifest.get(key)}`") out += line() out += line("## Counts") for key in ( "object_changes", "system_changes", "delta_objects_added", "delta_objects_removed", "delta_objects_changed", "delta_objects_unchanged", ): out += bullet(f"{key}: `{counts.get(key)}`") out += render_delta_summary(manifest_path, manifest) out += line() out += line("## Safety") for key in ("read_only", "sql_write_performed", "secrets_in_report"): out += bullet(f"{key}: `{safety.get(key)}`") return out def main() -> int: parser = argparse.ArgumentParser(description="Render one-shot 1C saved-state watch Markdown.") parser.add_argument("--manifest", type=Path, required=True) parser.add_argument("--output", type=Path) args = parser.parse_args() markdown = render(args.manifest, load_json(args.manifest)) if args.output: args.output.parent.mkdir(parents=True, exist_ok=True) args.output.write_text(markdown, encoding="utf-8") print(json.dumps({"output": str(args.output)}, ensure_ascii=False)) else: print(markdown) return 0 if __name__ == "__main__": raise SystemExit(main())