Files
llm/scripts/render_1c_saved_state_object_report_markdown.py

274 lines
10 KiB
Python

#!/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())