Files
llm/scripts/check_1c_extension_action_contract.py
T

232 lines
14 KiB
Python

from __future__ import annotations
import argparse
import json
import sys
from pathlib import Path
from typing import Any
ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT))
sys.path.insert(0, str(ROOT / "plugins" / "1c"))
sys.path.insert(0, str(ROOT / "plugins" / "1c" / "connector"))
import adapter_1c_server as adapter_server # noqa: E402
from parser.payload import compress_payload # noqa: E402
def require(condition: bool, message: str, failures: list[str]) -> None:
if not condition:
failures.append(message)
def run_override(action_evidence: dict[str, Any] | None) -> dict[str, Any]:
adapter_server.metadata_object_modules = lambda payload: {
"status": "ok",
"object": {"kind": "Catalog", "name": "Номенклатура"},
"modules": [{"module_id": "ConfigCAS:ext-guid__module-guid.0", "name": "object"}],
}
selection = {
"routine_name": "ПередЗаписью",
"line_start": 1,
"line_end": 3,
"match_by": "routine_exact",
}
if action_evidence:
selection.update(action_evidence)
adapter_server.read_module = lambda payload: {"status": "ok", "selection": selection}
return adapter_server.call_method(
"metadata.resolve_overrides",
{
"base_id": "upo_test",
"object_type": "Catalog",
"object_name": "Номенклатура",
"method_name": "ПередЗаписью",
},
)
def run_base_override() -> dict[str, Any]:
adapter_server.metadata_object_modules = lambda payload: {
"status": "ok",
"object": {"kind": "Catalog", "name": "Номенклатура"},
"modules": [{"module_id": "Config:object-module.0", "name": "object"}],
}
adapter_server.read_module = lambda payload: {
"status": "ok",
"selection": {
"routine_name": "ПередЗаписью",
"line_start": 1,
"line_end": 3,
"match_by": "routine_exact",
},
}
return adapter_server.call_method(
"metadata.resolve_overrides",
{
"base_id": "upo_test",
"object_type": "Catalog",
"object_name": "Номенклатура",
"method_name": "ПередЗаписью",
},
)
def run_saved_state_search_with_object_name(params: dict[str, Any] | None = None) -> dict[str, Any]:
module_text = "Процедура ПередЗаписью(Отказ)\nКонецПроцедуры".encode("utf-8")
header = f"\r\n{len(module_text):08x} {len(module_text):08x} 7fffffff \r\n".encode("ascii")
stored = compress_payload(b"prefix" + header + module_text, "raw_deflate")
adapter_server.metadata_cache_lookup_row = lambda base_id, kind, name: {
"guid": "owner-guid",
"kind": kind,
"kind_ru": "Справочник",
"public_kind": "catalog",
"name": name,
"source": "base",
}
adapter_server.storage_files_list = lambda payload: {
"status": "ok",
"files": [{"FileName": "owner-guid__module-guid.0", "PartCount": 1, "Bytes": len(stored)}],
}
adapter_server.read_storage_file_bytes = lambda base_id, table, file_name, timeout_seconds=30: (stored, {"database": base_id}, None)
request_payload = {
"base_id": "upo_test",
"layer": "extension_saved_state",
"object_type": "Catalog",
"object_name": "Номенклатура",
"query": "ПередЗаписью",
"limit": 10,
}
request_payload.update(params or {})
request_payload["include_storage"] = True
return adapter_server.call_method(
adapter_server.SAVED_STATE_MODULES_SEARCH_METHOD,
request_payload,
)
def first_action(result: dict[str, Any]) -> dict[str, Any]:
chain = result.get("chain") if isinstance(result.get("chain"), list) else []
first = chain[0] if chain and isinstance(chain[0], dict) else {}
action = first.get("extension_action") if isinstance(first.get("extension_action"), dict) else {}
return action
def run_checks() -> dict[str, Any]:
failures: list[str] = []
unknown = run_override(None)
unknown_action = first_action(unknown)
unknown_evidence = unknown.get("write_plan_evidence") if isinstance(unknown.get("write_plan_evidence"), dict) else {}
unknown_next = unknown_evidence.get("next_resolution") if isinstance(unknown_evidence.get("next_resolution"), dict) else {}
unknown_next_params = unknown_next.get("params") if isinstance(unknown_next.get("params"), dict) else {}
require(unknown.get("status") == "ok", "override chain with extension routine must resolve", failures)
require(unknown_action.get("status") == "unknown", "extension routine without action evidence must be unknown", failures)
require(unknown_action.get("operation_class") == "unknown_extension_action", "unknown action must not become replace", failures)
require((unknown_evidence.get("target") or {}).get("extension_action") == unknown_action, "unknown action must be carried into write_plan_evidence target", failures)
require("intent" not in unknown_evidence, "unknown action must not infer write intent", failures)
require(unknown_next.get("method") == adapter_server.SAVED_STATE_MODULES_SEARCH_METHOD, "write_plan_evidence must expose saved-state module resolver", failures)
require(unknown_next_params.get("layer") == "extension_saved_state", "extension write_plan_evidence must use extension_saved_state", failures)
require(unknown_next_params.get("ref") == "Catalog.Номенклатура", "extension write_plan_evidence must use a public object ref", failures)
require("tables" not in unknown_next_params and "owner_guid" not in unknown_next_params, "public extension evidence must hide storage selectors", failures)
controlled = run_override({"operation_class": "replace_with_control"})
controlled_action = first_action(controlled)
controlled_evidence = controlled.get("write_plan_evidence") if isinstance(controlled.get("write_plan_evidence"), dict) else {}
controlled_next = controlled_evidence.get("next_resolution") if isinstance(controlled_evidence.get("next_resolution"), dict) else {}
controlled_next_params = controlled_next.get("params") if isinstance(controlled_next.get("params"), dict) else {}
require(controlled_action.get("status") == "ok", "known extension action must be ok", failures)
require(controlled_action.get("operation_class") == "replace_with_control", "replace_with_control must be preserved", failures)
require(controlled_action.get("requires_control_fragment") is True, "replace_with_control must require control fragment", failures)
require((controlled_evidence.get("target") or {}).get("extension_action") == controlled_action, "known action must be carried into write_plan_evidence target", failures)
require((controlled_evidence.get("intent") or {}).get("operation") == "replace_with_control", "known action must infer write_plan_evidence intent", failures)
require(controlled_next.get("method") == adapter_server.SAVED_STATE_MODULES_SEARCH_METHOD, "known action evidence must expose saved-state module resolver", failures)
require(controlled_next_params.get("query") == "ПередЗаписью", "known action resolver params must carry routine query", failures)
before = run_override({"action": "вставить до"})
before_action = first_action(before)
require(before_action.get("operation_class") == "insert_before", "Russian insert-before action must normalize", failures)
base = run_base_override()
base_action = first_action(base)
base_evidence = base.get("write_plan_evidence") if isinstance(base.get("write_plan_evidence"), dict) else {}
base_next = base_evidence.get("next_resolution") if isinstance(base_evidence.get("next_resolution"), dict) else {}
base_next_params = base_next.get("params") if isinstance(base_next.get("params"), dict) else {}
require(base_action.get("operation_class") == "base_definition", "base routine must be marked as base_definition", failures)
require(base_action.get("requires_control_fragment") is False, "base routine must not require control fragment", failures)
require(base_next_params.get("layer") == "base_saved_state", "base write_plan_evidence must use base_saved_state", failures)
require(base_next_params.get("ref") == "Catalog.Номенклатура", "base write_plan_evidence must use a public object ref", failures)
saved_state = run_saved_state_search_with_object_name(controlled_next_params)
saved_state_owner = saved_state.get("owner_resolution") if isinstance(saved_state.get("owner_resolution"), dict) else {}
saved_state_modules = saved_state.get("modules") if isinstance(saved_state.get("modules"), list) else []
saved_state_streams = saved_state_modules[0].get("streams") if saved_state_modules and isinstance(saved_state_modules[0], dict) and isinstance(saved_state_modules[0].get("streams"), list) else []
saved_state_target = saved_state_streams[0].get("write_plan_target") if saved_state_streams and isinstance(saved_state_streams[0], dict) and isinstance(saved_state_streams[0].get("write_plan_target"), dict) else {}
require(saved_state.get("status") == "ok", "saved-state module search with object name must be accepted", failures)
require(saved_state_owner.get("owner_guid") == "owner-guid", "saved-state module search must resolve object name to owner_guid", failures)
require(bool(saved_state_modules), "saved-state module search with resolved owner must find module", failures)
require(saved_state_target.get("module_ref") == "ConfigCASSave:owner-guid__module-guid.0#stream:0", "saved-state stream must expose write_plan_target.module_ref", failures)
require(saved_state_target.get("expected_sha1"), "saved-state stream must expose write_plan_target.expected_sha1", failures)
concrete_plan_target = {
**(controlled_evidence.get("target") if isinstance(controlled_evidence.get("target"), dict) else {}),
**saved_state_target,
}
concrete_plan_intent = {
**(controlled_evidence.get("intent") if isinstance(controlled_evidence.get("intent"), dict) else {}),
"control_fragment": "Процедура ПередЗаписью",
"new": "Процедура ПередЗаписью(Отказ)\n\t// smoke\nКонецПроцедуры",
}
concrete_plan = adapter_server.call_method(
adapter_server.METADATA_WRITE_PLAN_METHOD,
{
"base_id": "upo_test",
"target": concrete_plan_target,
"intent": concrete_plan_intent,
"resolve_origin": False,
},
)
concrete_route = concrete_plan.get("route") if isinstance(concrete_plan.get("route"), dict) else {}
concrete_hint = concrete_route.get("apply_payload_hint") if isinstance(concrete_route.get("apply_payload_hint"), dict) else {}
concrete_hint_payload = concrete_hint.get("payload") if isinstance(concrete_hint.get("payload"), dict) else {}
require(concrete_plan.get("allowed") is True, "write_plan_evidence plus saved-state write_plan_target must produce an allowed concrete plan", failures)
require(concrete_route.get("apply_method") == adapter_server.MODULE_WRITE_APPLY_METHOD, "concrete override write plan must route to module write apply", failures)
require(concrete_route.get("operation_class") == "replace_with_control", "concrete override write plan must preserve extension operation class", failures)
require(concrete_hint.get("ready_for_apply_method") is True, "concrete override write plan hint must be ready for apply method", failures)
require(concrete_hint_payload.get("module_ref") == saved_state_target.get("module_ref"), "concrete override write plan hint must carry module_ref", failures)
require(concrete_hint_payload.get("expected_sha1") == saved_state_target.get("expected_sha1"), "concrete override write plan hint must carry expected_sha1", failures)
return {
"schema": "onec_extension_action_contract_check.v1",
"status": "ok" if not failures else "failed",
"failures": failures,
"checks": {
"unknown_extension_action": "extension routine without action evidence stays unknown",
"replace_with_control": "controlled replacement action requires control fragment",
"russian_insert_before": "Russian action names normalize to stable classes",
"base_definition": "base routines are not treated as extension actions",
"write_plan_evidence": "override results include a ready metadata.write.plan evidence fragment",
"write_plan_next_resolution": "write_plan_evidence points to the saved-state module resolver",
"saved_state_module_name_selector": "saved-state module search resolves object_type/object_name to owner_guid",
"saved_state_stream_write_plan_target": "saved-state module streams expose concrete metadata.write.plan target",
"override_to_concrete_write_plan": "write_plan_evidence and write_plan_target compose into an allowed concrete module plan",
},
}
def main() -> int:
parser = argparse.ArgumentParser(description="Check extension routine action contract invariants.")
parser.add_argument("--print", action="store_true")
args = parser.parse_args()
report = run_checks()
if args.print or report["status"] != "ok":
print(json.dumps(report, ensure_ascii=False, indent=2))
else:
print("1C extension action contract status: ok")
return 0 if report["status"] == "ok" else 1
if __name__ == "__main__":
raise SystemExit(main())