#!/usr/bin/env python3 """Render 1C saved-state object report JSON 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 trim(value: str, max_chars: int) -> str: if len(value) <= max_chars: return value return value[: max_chars - 3].rstrip() + "..." def storage_summary(storage: Any) -> str: rows = storage if isinstance(storage, list) else [storage] parts: list[str] = [] for row in rows: if not isinstance(row, dict): continue saved = row.get("saved_table") active = row.get("active_table") name = row.get("file_name") active_exists = row.get("active_exists") parts.append(f"{saved}->{active} `{name}` active_exists=`{active_exists}`") return "; ".join(parts) def render_object_change(change: dict[str, Any]) -> str: full_name = change.get("full_name") or change.get("name") layer = change.get("layer") kind_ru = change.get("kind_ru") or change.get("kind") synonym = change.get("synonym") out = bullet(f"{code(full_name)}: слой={code(layer)}, вид={code(kind_ru)}, состояние={code(change.get('change_state'))}") if synonym: out += f" Синоним: {code(synonym)}\n" storage = storage_summary(change.get("storage") or []) if storage: out += f" Хранилище: {storage}\n" return out def render_system_change(change: dict[str, Any]) -> str: name = change.get("name") layer = change.get("layer") extension = change.get("extension") storage = storage_summary(change.get("storage") or {}) suffix = f", расширение={code(extension)}" if extension else "" if storage: return bullet(f"{code(name)}: слой={code(layer)}{suffix}; {storage}") return bullet(f"{code(name)}: слой={code(layer)}{suffix}") def render_agent_summary(summary: dict[str, Any]) -> str: out = "" objects = summary.get("object_changes") or [] systems = summary.get("system_changes") or [] if not objects and not systems: return out out += line() out += line("## Agent Summary") if summary.get("default_next_action"): out += bullet(f"Next action: {summary.get('default_next_action')}") for item in objects: parts = item.get("parts") or [] added = item.get("added_terms") or item.get("added_words") or [] removed = item.get("removed_terms") or item.get("removed_words") or [] out += bullet( f"{code(item.get('full_name'))}: " f"parts={code(item.get('parts_count'))}, " f"text_diff_parts={code(item.get('text_diff_parts'))}, " f"active_missing_parts={code(item.get('active_missing_parts'))}" ) if added: out += f" Added terms: {', '.join(code(word) for word in added)}\n" if removed: out += f" Removed terms: {', '.join(code(word) for word in removed)}\n" for part in parts: out += ( " Part: " f"{code(part.get('file_name'))}, " f"role={code(part.get('payload_role'))}, " f"active_exists={code(part.get('active_exists'))}, " f"summary={code(part.get('summary'))}, " f"delta_chars={code(part.get('delta_chars'))}\n" ) if systems: names = ", ".join(code(item.get("name")) for item in systems) out += bullet(f"System changes: {names}") return out def render_detail(detail: dict[str, Any], max_diff_lines: int, max_words: int) -> str: out = "" out += line(f"### {detail.get('full_name')}") out += line() out += bullet(f"Слой: {code(detail.get('layer'))}") if detail.get("extension"): out += bullet(f"Расширение: {code(detail.get('extension'))}") out += bullet(f"Вид объекта: {code(detail.get('kind_ru') or detail.get('kind'))}") out += bullet(f"Имя: {code(detail.get('name'))}") out += line() for part in detail.get("details") or []: payload = part.get("payload") or {} diff = payload.get("text_diff") or {} out += bullet( "Часть " f"{code(part.get('file_name'))}: " f"role={code(part.get('payload_role'))}, " f"{part.get('saved_table')}->{part.get('active_table')}, " f"active_exists={code(part.get('active_exists'))}, " f"text_comparable={code(payload.get('text_comparable'))}" ) summary = payload.get("summary") if summary: out += f" Итог: {summary}\n" if part.get("active_cas_key"): out += f" Active CAS key: {code(part.get('active_cas_key'))}\n" if diff: out += ( " Размер текста: " f"active={code(diff.get('active_chars'))}, " f"saved={code(diff.get('saved_chars'))}, " f"delta={code(diff.get('delta_chars'))}\n" ) added = diff.get("added_words") or [] removed = diff.get("removed_words") or [] hints = payload.get("semantic_hints") or {} added_terms = hints.get("added_terms") or [] removed_terms = hints.get("removed_terms") or [] if added_terms: out += f" Добавленные термины: {', '.join(code(w) for w in added_terms[:max_words])}\n" if removed_terms: out += f" Удаленные термины: {', '.join(code(w) for w in removed_terms[:max_words])}\n" if added: out += f" Добавленные слова: {', '.join(code(w) for w in added[:max_words])}\n" if removed: out += f" Удаленные слова: {', '.join(code(w) for w in removed[:max_words])}\n" diff_lines = diff.get("unified_diff") or [] if diff_lines: out += "\n" out += "```diff\n" for diff_line in diff_lines[:max_diff_lines]: out += trim(str(diff_line), 220) + "\n" if len(diff_lines) > max_diff_lines: out += f"... truncated {len(diff_lines) - max_diff_lines} lines\n" out += "```\n" out += "\n" return out def resolve_linked_path(report_path: Path, value: str | None) -> Path | None: if not value: return None path = Path(value) if path.is_absolute(): return path return (report_path.parent / path).resolve() def render(data: dict[str, Any], comparison: dict[str, Any], detail: dict[str, Any], max_diff_lines: int, max_words: int) -> str: counts = data.get("counts") or {} safety = data.get("safety") or {} out = "" out += line("# 1C Saved State Object Report") out += line() out += line(f"Database: `{data.get('database')}`") out += line(f"View: `{comparison.get('view')}`") out += line(f"Report: `{data.get('report')}`") out += line() out += line("## Summary") out += bullet(f"Object changes: `{counts.get('object_changes')}`") out += bullet(f"System changes: `{counts.get('system_changes')}`") out += bullet(f"Detail objects: `{counts.get('detail_objects')}`") out += bullet(f"Detail parts: `{counts.get('detail_parts')}`") out += line() out += line("## Safety") for key in ("read_only", "sql_write_performed", "public_terms_are_1c_objects", "secrets_in_report"): out += bullet(f"{key}: `{safety.get(key)}`") out += render_agent_summary(data.get("agent_summary") or {}) object_changes = comparison.get("object_changes") or [] if object_changes: out += line() out += line("## Object Changes") for change in object_changes: out += render_object_change(change) system_changes = comparison.get("system_changes") or [] if system_changes: out += line() out += line("## System Changes") for change in system_changes: out += render_system_change(change) object_details = detail.get("object_details") or [] if object_details: out += line() out += line("## Details") for item in object_details: out += render_detail(item, max_diff_lines=max_diff_lines, max_words=max_words) active_resolution = detail.get("active_extension_resolution") or {} if active_resolution: out += line() out += line("## Active Extension Resolution") out += bullet(f"Mapped parts: `{active_resolution.get('mapped_parts')}`") if active_resolution.get("extension_manifest_summary"): out += bullet(f"Manifest summary: `{active_resolution.get('extension_manifest_summary')}`") if active_resolution.get("config_cas_all_dir"): out += bullet(f"ConfigCAS all dir: `{active_resolution.get('config_cas_all_dir')}`") return out def main() -> int: parser = argparse.ArgumentParser(description="Render 1C saved-state object report Markdown.") parser.add_argument("--report", type=Path, required=True) parser.add_argument("--comparison", type=Path) parser.add_argument("--detail", type=Path) parser.add_argument("--output", type=Path) parser.add_argument("--max-diff-lines", type=int, default=80) parser.add_argument("--max-words", type=int, default=20) args = parser.parse_args() report = load_json(args.report) comparison_path = args.comparison or resolve_linked_path(args.report, report.get("comparison")) detail_path = args.detail or resolve_linked_path(args.report, report.get("detail")) if comparison_path is None: raise SystemExit("Comparison path is required.") if detail_path is None: raise SystemExit("Detail path is required.") markdown = render( report, load_json(comparison_path), load_json(detail_path), max_diff_lines=args.max_diff_lines, max_words=args.max_words, ) 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())