Compose name-first form write plans

This commit is contained in:
2026-07-26 17:30:04 +03:00
parent c1d5d06f6d
commit 39c8c4ab18
6 changed files with 603 additions and 10 deletions
+154
View File
@@ -126,6 +126,54 @@ def first_write_plan_target_from_saved_state(result: dict[str, Any]) -> dict[str
return None
def first_public_form_write_plan_candidate(result: dict[str, Any]) -> dict[str, Any] | None:
candidates: list[dict[str, Any]] = []
for form in result.get("forms") or []:
if not isinstance(form, dict):
continue
for match in form.get("matches") or []:
if not isinstance(match, dict):
continue
selector = match.get("selector") if isinstance(match.get("selector"), dict) else {}
if not selector.get("ref") or not selector.get("form"):
continue
if not any(selector.get(key) for key in ("command", "element", "attribute")):
continue
if any(key in selector for key in ("table", "file_name", "guid", "form_guid")):
continue
for prop in match.get("writable_properties") or []:
if not isinstance(prop, dict) or prop.get("value_type") != "string":
continue
property_name = str(prop.get("presentation") or prop.get("property") or "").strip()
if not property_name:
continue
old_value = str(prop.get("value") or "")
normalized_property = str(prop.get("property") or "").strip().casefold()
priority = 0 if normalized_property == "title" else (2 if normalized_property in {"id", "name"} else 1)
candidates.append(
{
"priority": priority,
"target": {
"kind": "form",
**{
key: value
for key, value in selector.items()
if key in {"extension", "ref", "form", "command", "element", "attribute"} and value not in (None, "")
},
},
"edit": {
"property": property_name,
"value": f"{old_value} [NAME-FIRST PLAN]",
},
}
)
if not candidates:
return None
candidate = sorted(candidates, key=lambda item: int(item.get("priority") or 0))[0]
candidate.pop("priority", None)
return candidate
def live_coverage_from_steps(steps: list[dict[str, Any]]) -> dict[str, Any]:
def last_step(name: str) -> dict[str, Any]:
for step in reversed(steps):
@@ -136,6 +184,8 @@ def live_coverage_from_steps(steps: list[dict[str, Any]]) -> dict[str, Any]:
resolve = last_step("metadata.resolve_overrides")
saved_state = last_step("metadata.saved_state.modules.search")
write_plan = last_step("metadata.write.plan")
form_search = last_step("metadata.saved_state.forms.search")
form_write_plan = last_step("metadata.write.plan.form")
skips = [
{"step": str(step.get("name")), "status": step.get("status"), **({"reason": step.get("reason")} if step.get("reason") else {})}
for step in steps
@@ -160,6 +210,15 @@ def live_coverage_from_steps(steps: list[dict[str, Any]]) -> dict[str, Any]:
"composed": write_plan.get("allowed") is True,
"from_write_plan_target": bool(write_plan.get("from_write_plan_target")),
},
"form_write_plan_composition": {
"attempted": bool(form_write_plan),
"search_attempted": bool(form_search),
"search_status": form_search.get("status"),
"candidate": bool(form_search.get("write_plan_candidate")),
"status": form_write_plan.get("status"),
"composed": form_write_plan.get("allowed") is True,
"name_first": bool(form_write_plan.get("name_first")),
},
"skips": skips,
}
@@ -175,6 +234,18 @@ def live_write_plan_composition_required_issue(coverage: dict[str, Any]) -> dict
}
def live_form_write_plan_composition_required_issue(coverage: dict[str, Any]) -> dict[str, Any] | None:
composition = coverage.get("form_write_plan_composition") if isinstance(coverage.get("form_write_plan_composition"), dict) else {}
if composition.get("composed") is True and composition.get("name_first") is True:
return None
return {
"code": "live_form_write_plan_composition_required",
"status": composition.get("status"),
"search_status": composition.get("search_status"),
"candidate": composition.get("candidate"),
}
def live_object_summary(*sources: dict[str, Any] | None) -> dict[str, Any]:
summary: dict[str, Any] = {}
for source in sources:
@@ -504,6 +575,7 @@ def build_live_report(
transport: str = "rest",
object_selector: dict[str, Any] | None = None,
require_write_plan_composition: bool = False,
require_form_write_plan_composition: bool = False,
) -> dict[str, Any]:
steps: list[dict[str, Any]] = []
issues: list[dict[str, Any]] = []
@@ -856,11 +928,91 @@ def build_live_report(
if code_read.get("status") not in {"ok", "summary"}:
issues.append({"code": "live_code_read_failed", "status": code_read.get("status"), "diagnostics": code_read.get("diagnostics")})
if require_form_write_plan_composition:
form_search = rpc_call(
endpoint_url,
"metadata.saved_state.forms.search",
{
"base_id": base_id,
"limit": 20,
"scan_limit": 1000,
"timeout_seconds": int(timeout),
},
timeout=timeout,
transport=transport,
session_id=session_id,
)
form_candidate = first_public_form_write_plan_candidate(form_search)
steps.append(
{
"name": "metadata.saved_state.forms.search",
"status": form_search.get("status"),
"forms": int((form_search.get("counts") or {}).get("forms") or 0),
"write_plan_candidate": bool(form_candidate),
"include_storage": False,
}
)
if form_search.get("status") != "ok":
issues.append({"code": "live_saved_state_forms_search_failed", "status": form_search.get("status")})
elif not form_candidate:
steps.append(
{
"name": "metadata.write.plan.form",
"status": "skipped_no_public_form_target",
"allowed": False,
"name_first": False,
}
)
else:
form_plan_payload = {
"base_id": base_id,
"target": form_candidate["target"],
"edits": [form_candidate["edit"]],
"resolve_origin": False,
}
form_plan = rpc_call(
endpoint_url,
"metadata.write.plan",
form_plan_payload,
timeout=timeout,
transport=transport,
session_id=session_id,
)
form_route = form_plan.get("route") if isinstance(form_plan.get("route"), dict) else {}
form_hint = form_route.get("apply_payload_hint") if isinstance(form_route.get("apply_payload_hint"), dict) else {}
form_name_resolution = form_route.get("name_resolution") if isinstance(form_route.get("name_resolution"), dict) else {}
steps.append(
{
"name": "metadata.write.plan.form",
"status": form_plan.get("status"),
"allowed": form_plan.get("allowed"),
"apply_method": form_route.get("apply_method"),
"ready_for_apply_method": form_hint.get("ready_for_apply_method"),
"name_resolution": form_name_resolution.get("status"),
"name_first": not any(
key in form_plan_payload["target"]
for key in ("table", "file_name", "guid", "form_guid", "module_ref")
),
}
)
if form_plan.get("allowed") is not True:
issues.append({"code": "live_form_write_plan_not_allowed", "status": form_plan.get("status"), "problems": form_plan.get("problems")})
if form_route.get("apply_method") != "metadata.form.element.write_apply":
issues.append({"code": "live_form_write_plan_apply_method_mismatch", "actual": form_route.get("apply_method")})
if form_hint.get("ready_for_apply_method") is not True:
issues.append({"code": "live_form_write_plan_hint_not_ready", "hint": form_hint})
if form_name_resolution.get("status") != "resolved":
issues.append({"code": "live_form_write_plan_name_resolution_missing", "name_resolution": form_name_resolution})
coverage = live_coverage_from_steps(steps)
if require_write_plan_composition:
strict_issue = live_write_plan_composition_required_issue(coverage)
if strict_issue:
issues.append(strict_issue)
if require_form_write_plan_composition:
strict_form_issue = live_form_write_plan_composition_required_issue(coverage)
if strict_form_issue:
issues.append(strict_form_issue)
return {
"schema": "onec_mcp_selector_chain_live_smoke.v1",
@@ -1037,6 +1189,7 @@ def main() -> int:
parser.add_argument("--guid", help="Optional concrete metadata object GUID for deterministic --live checks.")
parser.add_argument("--timeout", type=float, default=15.0, help="HTTP timeout for --live.")
parser.add_argument("--require-write-plan-composition", action="store_true", help="Fail live smoke when saved-state lookup does not produce a concrete write_plan_target and composed metadata.write.plan.")
parser.add_argument("--require-form-write-plan-composition", action="store_true", help="Fail live smoke unless a public saved-state form selector composes metadata.write.plan without storage identifiers.")
args = parser.parse_args()
if args.live and not args.base_id:
@@ -1051,6 +1204,7 @@ def main() -> int:
transport=args.transport,
object_selector=object_selector or None,
require_write_plan_composition=args.require_write_plan_composition,
require_form_write_plan_composition=args.require_form_write_plan_composition,
)
else:
report = build_report()