Initial SQL-only 1C adapter baseline
This commit is contained in:
@@ -0,0 +1,273 @@
|
||||
#!/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())
|
||||
Reference in New Issue
Block a user