#!/usr/bin/env python3 """Create a read-only 1C change proposal from a task evidence bundle.""" from __future__ import annotations import argparse import base64 import json import re from pathlib import Path from typing import Any def decode_arg(value: str | None, encoded: str | None) -> str | None: if encoded: return base64.b64decode(encoded).decode("utf-8") return value def load_json(path: Path) -> dict[str, Any]: return json.loads(path.read_text(encoding="utf-8-sig")) def norm(value: Any) -> str: return re.sub(r"[\s._-]+", "", str(value or "")).casefold() def words(text: str) -> set[str]: return set(re.findall(r"[A-Za-zА-Яа-яЁё0-9_]{3,}", text.casefold())) def infer_intents(text: str) -> list[str]: w = words(text) intents = [] if w & {"добавить", "создать", "вывести", "показать"}: intents.append("add_or_expose") if w & {"изменить", "исправить", "доработать", "поменять"}: intents.append("modify") if w & {"проверить", "проверка", "проанализировать"}: intents.append("inspect_or_verify") if w & {"кнопка", "кнопку", "команда", "команду"}: intents.append("form_command") if w & {"форма", "форму"}: intents.append("form") if w & {"реквизит", "реквизиты", "поле", "поля"}: intents.append("attribute") if w & {"проведение", "провести", "запись", "записать"}: intents.append("object_lifecycle") return intents or ["inspect_or_verify"] def unique_list(values: list[Any]) -> list[Any]: result = [] seen = set() for value in values: key = json.dumps(value, ensure_ascii=False, sort_keys=True) if key in seen: continue seen.add(key) result.append(value) return result def collect_search_findings(investigation: dict[str, Any]) -> list[dict[str, Any]]: findings = [] for search in investigation.get("searches") or []: for match in search.get("matches") or []: evidence = match.get("evidence") or {} findings.append( { "search_text": search.get("text"), "area": match.get("area"), "name": match.get("name"), "title": match.get("title"), "form": match.get("form"), "origin": match.get("origin"), "effective_action": match.get("effective_action"), "path": evidence.get("path") or evidence.get("form_xml_path"), "line": evidence.get("line"), "id": evidence.get("id"), "text": evidence.get("text"), } ) return unique_list(findings) def collect_file_targets(investigation: dict[str, Any]) -> list[dict[str, Any]]: targets = [] for form_context in investigation.get("forms") or []: for form in form_context.get("forms") or []: targets.append( { "kind": "form_xml", "name": form.get("name"), "origin": form.get("origin"), "path": form.get("form_xml_path"), "module_path": form.get("module_path"), "reason": "selected_form_context", } ) for overlay in form.get("extension_overlays") or []: targets.append( { "kind": "form_xml", "name": overlay.get("name"), "origin": overlay.get("origin"), "path": overlay.get("form_xml_path"), "module_path": overlay.get("module_path"), "reason": "active_extension_form_overlay", } ) for module_context in investigation.get("modules") or []: for module in module_context.get("modules") or []: targets.append( { "kind": "bsl_module", "name": module.get("name"), "origin": module.get("origin"), "path": module.get("path"), "relative_path": module.get("relative_path"), "reason": "selected_module_context", } ) for snippet in investigation.get("code_snippets") or []: targets.append( { "kind": "bsl_module", "name": snippet.get("module"), "origin": snippet.get("origin"), "path": snippet.get("path"), "line": snippet.get("focus_line"), "reason": f"code_search_hit:{snippet.get('search_text')}", } ) return unique_list([target for target in targets if target.get("path") or target.get("module_path")]) def extension_names_from_targets(targets: list[dict[str, Any]]) -> list[str]: names = [] for target in targets: origin = target.get("origin") or {} if origin.get("layer") == "extension" and origin.get("extension"): names.append(origin["extension"]) return sorted(set(names)) def choose_write_strategy(investigation: dict[str, Any], targets: list[dict[str, Any]]) -> dict[str, Any]: extension_names = extension_names_from_targets(targets) active_extensions = ((investigation.get("brief") or {}).get("active_extensions") or []) if extension_names: preferred = extension_names[0] reason = "task evidence already points to an active extension overlay" elif active_extensions: preferred = active_extensions[0] reason = "active extensions exist; extension-first write path is required" else: preferred = None reason = "no active extension target found; create a new extension in a disposable validation loop" return { "mode": "extension_first_proposal", "preferred_extension": preferred, "reason": reason, "forbidden": [ "direct SQL metadata/data updates", "direct Config/ConfigSave/ConfigCAS writes", "automatic production Designer update/apply", ], } def split_write_targets(targets: list[dict[str, Any]], preferred_extension: str | None) -> dict[str, list[dict[str, Any]]]: write_candidates = [] references = [] for target in targets: origin = target.get("origin") or {} if origin.get("layer") == "extension" and (not preferred_extension or origin.get("extension") == preferred_extension): write_candidates.append(target) else: references.append(target) return { "write_candidates": unique_list(write_candidates), "read_only_reference_files": unique_list(references), } def summarize_existing_state(findings: list[dict[str, Any]], targets: list[dict[str, Any]]) -> dict[str, Any]: form_commands = [row for row in findings if row.get("area") in {"form.command", "form.item"}] code_hits = [row for row in findings if row.get("area") == "module.code"] attributes = [row for row in findings if row.get("area") == "metadata.attribute"] return { "found_form_commands_or_items": form_commands, "found_metadata_attributes": attributes, "found_code_hits": code_hits[:20], "target_files": targets, } def build_steps(task_text: str, intents: list[str], investigation: dict[str, Any], findings: list[dict[str, Any]], targets: list[dict[str, Any]]) -> list[dict[str, Any]]: object_info = ((investigation.get("brief") or {}).get("object") or {}) steps = [ { "order": 1, "action": "confirm_target_object", "object": object_info, "evidence": investigation.get("candidate"), }, { "order": 2, "action": "review_effective_metadata", "purpose": "ensure extension-added attributes and modified types are included before code generation", }, ] next_order = 3 if "form_command" in intents or any(row.get("area") in {"form.command", "form.item"} for row in findings): steps.append( { "order": next_order, "action": "review_form_command", "purpose": "determine whether the command already exists or must be added", "evidence": [row for row in findings if row.get("area") in {"form.command", "form.item"}], } ) next_order += 1 if any(row.get("area") == "module.code" for row in findings): steps.append( { "order": next_order, "action": "review_related_bsl", "purpose": "inspect code snippets before proposing BSL changes", "evidence": [row for row in findings if row.get("area") == "module.code"][:10], } ) next_order += 1 steps.append( { "order": next_order, "action": "draft_extension_patch", "purpose": "prepare a reviewable patch against extension source files only", "candidate_targets": targets[:20], } ) steps.append( { "order": next_order + 1, "action": "validate_in_disposable_base", "purpose": "load/update extension, open affected forms, run smoke scenario, and only then ask for approval", } ) return steps def build_checks(intents: list[str]) -> list[dict[str, Any]]: checks = [ {"name": "syntax_check", "description": "Run 1C/BSL syntax validation for changed modules."}, {"name": "extension_load", "description": "Package/load the extension into a disposable base."}, {"name": "form_open", "description": "Open affected forms and verify command bindings and handlers."}, ] if "form_command" in intents: checks.append({"name": "command_invocation", "description": "Invoke the affected form command and verify expected behavior."}) if "object_lifecycle" in intents: checks.append({"name": "write_posting_smoke", "description": "Create/write/post a test document in a disposable base."}) return checks def proposal_for_investigation(task_text: str, investigation: dict[str, Any]) -> dict[str, Any]: intents = infer_intents(task_text) findings = collect_search_findings(investigation) targets = collect_file_targets(investigation) strategy = choose_write_strategy(investigation, targets) target_split = split_write_targets(targets, strategy.get("preferred_extension")) return { "object": (investigation.get("brief") or {}).get("object"), "candidate": investigation.get("candidate"), "intents": intents, "write_strategy": strategy, "existing_state": summarize_existing_state(findings, targets), "target_policy": target_split, "implementation_steps": build_steps(task_text, intents, investigation, findings, targets), "validation_checks": build_checks(intents), "open_questions": infer_open_questions(task_text, findings), } def infer_open_questions(task_text: str, findings: list[dict[str, Any]]) -> list[str]: questions = [] if "добав" in task_text.casefold() and any(row.get("area") in {"form.command", "form.item"} for row in findings): questions.append("The requested command/item appears to already exist; confirm whether the task is to modify existing behavior rather than add a duplicate.") if not findings: questions.append("No direct matches were found in the evidence bundle; broaden search terms or inspect more objects before generating a patch.") return questions def build_proposal(evidence: dict[str, Any]) -> dict[str, Any]: task_text = ((evidence.get("task") or {}).get("text") or "") proposals = [proposal_for_investigation(task_text, item) for item in evidence.get("investigations") or []] return { "schema": "onec_task_change_proposal.v1", "task": evidence.get("task"), "source_evidence_schema": evidence.get("schema"), "view": evidence.get("view"), "proposals": proposals, "safety": { "mode": "proposal_only", "write_status": "blocked_until_write_gates", "write_contract": "docs/1c-write-path-safety.md", }, "counts": { "proposals": len(proposals), "target_files": sum(len((proposal.get("existing_state") or {}).get("target_files") or []) for proposal in proposals), "write_candidates": sum(len((proposal.get("target_policy") or {}).get("write_candidates") or []) for proposal in proposals), "open_questions": sum(len(proposal.get("open_questions") or []) for proposal in proposals), }, } def main() -> int: parser = argparse.ArgumentParser(description="Create a read-only 1C change proposal from evidence.") parser.add_argument("--evidence", type=Path, required=True) parser.add_argument("--output", type=Path) args = parser.parse_args() result = build_proposal(load_json(args.evidence)) 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"]}, ensure_ascii=False)) else: print(output) return 0 if __name__ == "__main__": raise SystemExit(main())