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