#!/usr/bin/env python3 """Build a compact read-only evidence bundle for a 1C development task.""" from __future__ import annotations import argparse import base64 import json import sys from pathlib import Path from typing import Any sys.path.insert(0, str(Path(__file__).resolve().parent)) from get_1c_form_context import build_context as build_form_context # noqa: E402 from get_1c_module import build_module_result # noqa: E402 from get_1c_object_metadata import build_object_metadata # noqa: E402 from plan_1c_task_context import build_plan # noqa: E402 from resolve_1c_object import load_json # noqa: E402 def decode_arg(value: str | None, encoded: str | None) -> str | None: if encoded: return base64.b64decode(encoded).decode("utf-8") return value def collect_target_reads(investigation: dict[str, Any]) -> tuple[list[str], list[str]]: forms = [] modules = [] for read in investigation.get("recommended_reads") or []: form = read.get("form") module = read.get("module") if form and form not in forms: forms.append(form) if module and module not in modules: modules.append(module) for search in investigation.get("searches") or []: for match in search.get("matches") or []: form = match.get("form") if form and form not in forms: forms.append(form) area = match.get("area") module = match.get("name") if area in {"module", "module.code"} and module and module not in modules: modules.append(module) return forms, modules def compact_metadata(metadata: dict[str, Any], *, max_attributes: int) -> dict[str, Any]: attrs = metadata.get("attributes") or [] sections = metadata.get("tabular_sections") or [] return { "schema": metadata.get("schema"), "view": metadata.get("view"), "object": metadata.get("object"), "attributes": attrs[:max_attributes], "attributes_total": len(attrs), "attributes_truncated": len(attrs) > max_attributes, "tabular_sections": sections, "counts": metadata.get("counts"), } def compact_form_context(context: dict[str, Any], *, max_items: int, max_attributes: int, max_commands: int) -> dict[str, Any]: forms = [] for form in context.get("forms") or []: copy = {key: form.get(key) for key in ("name", "synonym", "uuid", "origin", "effective_action", "meta_xml_path", "form_xml_path", "module_path", "extension_overlays") if form.get(key) not in (None, [], "")} structure = form.get("structure") or {} if structure: copy["structure"] = { "origin": structure.get("origin"), "form_xml_path": structure.get("form_xml_path"), "events": structure.get("events") or [], "items": (structure.get("items") or [])[:max_items], "attributes": (structure.get("attributes") or [])[:max_attributes], "commands": (structure.get("commands") or [])[:max_commands], "counts": structure.get("counts"), } overlays = [] for overlay in form.get("extension_overlays") or []: overlay_copy = {key: overlay.get(key) for key in ("name", "synonym", "uuid", "origin", "effective_action", "meta_xml_path", "form_xml_path", "module_path") if overlay.get(key) not in (None, [], "")} structure = overlay.get("structure") or {} if structure: overlay_copy["structure"] = { "origin": structure.get("origin"), "form_xml_path": structure.get("form_xml_path"), "events": structure.get("events") or [], "items": (structure.get("items") or [])[:max_items], "attributes": (structure.get("attributes") or [])[:max_attributes], "commands": (structure.get("commands") or [])[:max_commands], "counts": structure.get("counts"), } overlays.append(overlay_copy) if overlays: copy["extension_overlays"] = overlays forms.append(copy) return { "schema": context.get("schema"), "view": context.get("view"), "object": context.get("object"), "query": context.get("query"), "forms": forms, "counts": context.get("counts"), } def compact_module_result(result: dict[str, Any]) -> dict[str, Any]: return { "schema": result.get("schema"), "view": result.get("view"), "object": result.get("object"), "query": result.get("query"), "modules": result.get("modules") or [], "counts": result.get("counts"), } def read_lines(path: str) -> list[str]: file_path = Path(path) try: return file_path.read_text(encoding="utf-8-sig").splitlines() except UnicodeDecodeError: return file_path.read_text(encoding="cp1251", errors="replace").splitlines() def code_snippet(path: str, line: int, *, radius: int, max_chars: int) -> dict[str, Any] | None: file_path = Path(path) if not file_path.is_file() or line <= 0: return None lines = read_lines(path) start = max(1, line - radius) end = min(len(lines), line + radius) text = "\n".join(lines[start - 1 : end]) truncated = len(text) > max_chars return { "path": path, "line_start": start, "line_end": end, "focus_line": line, "text": text[:max_chars], "truncated": truncated, "char_count": len(text), } def collect_code_snippets(searches: list[dict[str, Any]], *, radius: int, max_chars: int, limit: int) -> list[dict[str, Any]]: snippets = [] seen = set() for search in searches: for match in search.get("matches") or []: if match.get("area") != "module.code": continue evidence = match.get("evidence") or {} path = evidence.get("path") line = evidence.get("line") if not path or not line: continue key = (path, line) if key in seen: continue seen.add(key) snippet = code_snippet(str(path), int(line), radius=radius, max_chars=max_chars) if not snippet: continue snippet.update( { "search_text": search.get("text"), "module": match.get("name"), "origin": match.get("origin"), "effective_action": match.get("effective_action"), } ) snippets.append(snippet) if len(snippets) >= limit: return snippets return snippets def evidence_for_investigation( index: dict[str, Any], investigation: dict[str, Any], *, view: str, max_attributes: int, max_form_items: int, max_form_attributes: int, max_form_commands: int, max_module_chars: int, code_snippet_radius: int, max_code_snippet_chars: int, max_code_snippets: int, max_forms: int, max_modules: int, ) -> dict[str, Any]: candidate = investigation.get("candidate") or {} kind = candidate.get("kind") name = candidate.get("name") metadata = build_object_metadata(index, kind=kind, name=name, view=view, extension=None, include_storage=False) forms, modules = collect_target_reads(investigation) form_contexts = [] for form_name in forms[:max_forms]: form_contexts.append( compact_form_context( build_form_context(index, kind=kind, name=name, form=form_name, view=view, extension=None, max_items=max_form_items), max_items=max_form_items, max_attributes=max_form_attributes, max_commands=max_form_commands, ) ) module_contexts = [] for module_name in modules[:max_modules]: module_contexts.append( compact_module_result( build_module_result( index, kind=kind, name=name, module_name=module_name, view=view, extension=None, max_chars=max_module_chars, routine=None, ) ) ) snippets = collect_code_snippets( investigation.get("searches") or [], radius=code_snippet_radius, max_chars=max_code_snippet_chars, limit=max_code_snippets, ) return { "candidate": candidate, "brief": investigation.get("brief"), "searches": investigation.get("searches") or [], "metadata": compact_metadata(metadata, max_attributes=max_attributes), "forms": form_contexts, "modules": module_contexts, "code_snippets": snippets, "recommended_reads": investigation.get("recommended_reads") or [], "counts": { "forms_materialized": len(form_contexts), "modules_materialized": len(module_contexts), "code_snippets": len(snippets), }, } def build_evidence( index: dict[str, Any], *, text: str, view: str, max_objects: int, max_terms: int, max_matches: int, max_attributes: int, max_form_items: int, max_form_attributes: int, max_form_commands: int, max_module_chars: int, code_snippet_radius: int, max_code_snippet_chars: int, max_code_snippets: int, max_forms: int, max_modules: int, ) -> dict[str, Any]: plan = build_plan(index, text=text, view=view, max_objects=max_objects, max_terms=max_terms, max_matches=max_matches) investigations = [] for investigation in (plan.get("investigations") or [])[:max_objects]: investigations.append( evidence_for_investigation( index, investigation, view=view, max_attributes=max_attributes, max_form_items=max_form_items, max_form_attributes=max_form_attributes, max_form_commands=max_form_commands, max_module_chars=max_module_chars, code_snippet_radius=code_snippet_radius, max_code_snippet_chars=max_code_snippet_chars, max_code_snippets=max_code_snippets, max_forms=max_forms, max_modules=max_modules, ) ) return { "schema": "onec_task_evidence_bundle.v1", "view": view, "task": {"text": text}, "plan": { "schema": plan.get("schema"), "object_candidates": plan.get("object_candidates") or [], "search_terms": plan.get("search_terms") or [], "safety": plan.get("safety"), "counts": plan.get("counts"), }, "investigations": investigations, "limits": { "max_objects": max_objects, "max_terms": max_terms, "max_matches": max_matches, "max_attributes": max_attributes, "max_form_items": max_form_items, "max_form_attributes": max_form_attributes, "max_form_commands": max_form_commands, "max_module_chars": max_module_chars, "code_snippet_radius": code_snippet_radius, "max_code_snippet_chars": max_code_snippet_chars, "max_code_snippets": max_code_snippets, "max_forms": max_forms, "max_modules": max_modules, }, "safety": { "mode": "read_only", "write_status": "blocked_until_write_gates", "write_contract": "docs/1c-write-path-safety.md", }, "counts": { "investigations": len(investigations), "forms_materialized": sum(item.get("counts", {}).get("forms_materialized", 0) for item in investigations), "modules_materialized": sum(item.get("counts", {}).get("modules_materialized", 0) for item in investigations), "code_snippets": sum(item.get("counts", {}).get("code_snippets", 0) for item in investigations), }, } def main() -> int: parser = argparse.ArgumentParser(description="Build read-only 1C task evidence bundle.") parser.add_argument("--index", type=Path, required=True) parser.add_argument("--text") parser.add_argument("--text-b64") parser.add_argument("--view", choices=["effective", "base"], default="effective") parser.add_argument("--max-objects", type=int, default=2) parser.add_argument("--max-terms", type=int, default=8) parser.add_argument("--max-matches", type=int, default=8) parser.add_argument("--max-attributes", type=int, default=120) parser.add_argument("--max-form-items", type=int, default=250) parser.add_argument("--max-form-attributes", type=int, default=120) parser.add_argument("--max-form-commands", type=int, default=80) parser.add_argument("--max-module-chars", type=int, default=12000) parser.add_argument("--code-snippet-radius", type=int, default=8) parser.add_argument("--max-code-snippet-chars", type=int, default=8000) parser.add_argument("--max-code-snippets", type=int, default=20) parser.add_argument("--max-forms", type=int, default=3) parser.add_argument("--max-modules", type=int, default=4) parser.add_argument("--output", type=Path) args = parser.parse_args() text = decode_arg(args.text, args.text_b64) if not text: raise SystemExit("Use --text or --text-b64.") result = build_evidence( load_json(args.index), text=text, view=args.view, max_objects=args.max_objects, max_terms=args.max_terms, max_matches=args.max_matches, max_attributes=args.max_attributes, max_form_items=args.max_form_items, max_form_attributes=args.max_form_attributes, max_form_commands=args.max_form_commands, max_module_chars=args.max_module_chars, code_snippet_radius=args.code_snippet_radius, max_code_snippet_chars=args.max_code_snippet_chars, max_code_snippets=args.max_code_snippets, max_forms=args.max_forms, max_modules=args.max_modules, ) output = json.dumps(result, ensure_ascii=False, indent=2) + "\n" if args.output: args.output.parent.mkdir(parents=True, exist_ok=True) args.output.write_text(output, encoding="utf-8") print(json.dumps({"output": str(args.output), "counts": result["counts"], "view": result["view"]}, ensure_ascii=False)) else: print(output) return 0 if __name__ == "__main__": raise SystemExit(main())