Complete name-first MCP write-plan composition
This commit is contained in:
@@ -262,8 +262,11 @@ Client, MCP, and agent code must not add conditions for concrete object names;
|
||||
the adapter owns generic selector normalization.
|
||||
|
||||
Saved-state client calls use the same name-first selectors together with
|
||||
`layer=base_saved_state|extension_saved_state`. SQL tables, file names, GUID
|
||||
owners, and module handles are diagnostic continuations exposed only with
|
||||
`layer=base_saved_state|extension_saved_state`. Public module search results
|
||||
include a name-first `write_plan_target` (`ref`, form/module names, and a
|
||||
1-based stream ordinal); `metadata.write.plan` resolves its physical handle
|
||||
internally. SQL tables, file names, GUID owners, module handles, and payload
|
||||
hashes remain diagnostic continuations exposed only with
|
||||
`include_storage=true`. Every public RPC follow-up is shaped as
|
||||
`{"method": "...", "params": {...}}`; `payload` is not the arguments field of
|
||||
`next_call` or `next_resolution`.
|
||||
|
||||
@@ -761,7 +761,7 @@ METHODS = [
|
||||
{"name": "metadata.saved_state.diff", "transport": "POST /rpc", "description": "Agent-facing read-only comparison of a saved-state module with its active source. Prefer a 1C object ref or kind/name plus module_ordinal; generated module_ref and table/file_name remain accepted for follow-up tooling. Reports changed/unchanged, needs_prepare, hashes, and compact payload diff."},
|
||||
{"name": "metadata.saved_state.changes.list", "transport": "POST /rpc", "description": "Read-only name-first list of pending saved-state changes. Filter by semantic layer; public rows resolve 1C object/form/module context and hide SQL coordinates; include_storage=true exposes per-file diff/write diagnostics."},
|
||||
{"name": "metadata.saved_state.forms.search", "transport": "POST /rpc", "description": "Fast name-first saved-state form search by public owner ref, form, element, command, attribute, or text. Public rows expose 1C names and semantic selectors; include_storage=true opts into SQL files, GUIDs, brace paths, markers, and write diagnostics."},
|
||||
{"name": "metadata.saved_state.modules.search", "transport": "POST /rpc", "description": "Fast name-first saved-state BSL module search over ConfigSave/ConfigCASSave. Accepts public ref or kind/name selectors and resolves SQL routes internally. Public results expose 1C owner/form/module names and previews; include_storage=true opts into module_ref handles and low-level write targets."},
|
||||
{"name": "metadata.saved_state.modules.search", "transport": "POST /rpc", "description": "Fast name-first saved-state BSL module search over ConfigSave/ConfigCASSave. Accepts public ref or kind/name selectors and resolves SQL routes internally. Public results expose 1C owner/form/module names, previews, and name-first metadata.write.plan targets; include_storage=true additionally exposes module_ref handles and physical write guards."},
|
||||
{"name": "metadata.form.write_matrix.build", "transport": "POST /rpc", "description": "Build a source-aware matrix of decoded saved-state form scalar properties and safe write-smoke candidates."},
|
||||
{"name": "metadata.form.write_matrix.smoke", "transport": "POST /rpc", "description": "Run apply_and_rollback smoke writes for safe entries from metadata.form.write_matrix.build and report verified write routes."},
|
||||
{"name": "metadata.form.element.write", "transport": "POST /rpc", "description": "Saved-state form element write planner. Resolves a decoded form element and builds a reviewable changes.propose payload for ConfigSave/ConfigCASSave. Requires allow_saved_state_write=true and does not write SQL."},
|
||||
@@ -41818,7 +41818,7 @@ def public_saved_state_modules_search_row(row: dict[str, Any]) -> dict[str, Any]
|
||||
**({"qualified_name": qualified_name} if qualified_name else {}),
|
||||
}
|
||||
public_streams = []
|
||||
for stream in row.get("streams") or []:
|
||||
for stream_ordinal, stream in enumerate(row.get("streams") or [], start=1):
|
||||
if not isinstance(stream, dict):
|
||||
continue
|
||||
match = stream.get("match") if isinstance(stream.get("match"), dict) else {}
|
||||
@@ -41832,6 +41832,16 @@ def public_saved_state_modules_search_row(row: dict[str, Any]) -> dict[str, Any]
|
||||
stream_owner = stream.get("owner") if isinstance(stream.get("owner"), dict) else owner
|
||||
stream_form = stream.get("form") if isinstance(stream.get("form"), dict) else form
|
||||
stream_module = stream.get("module") if isinstance(stream.get("module"), dict) else module
|
||||
stream_owner_ref = object_selector_ref(stream_owner.get("kind"), stream_owner.get("name"))
|
||||
stream_qualified_name = str(stream.get("qualified_name") or stream.get("display_name") or qualified_name).strip()
|
||||
write_plan_target = {
|
||||
"kind": "module",
|
||||
**({"ref": stream_owner_ref} if stream_owner_ref else {}),
|
||||
**({"form": stream_form.get("name")} if stream_form.get("name") else {}),
|
||||
**({"module": stream_module.get("name")} if stream_module.get("name") else {}),
|
||||
**({"qualified_name": stream_qualified_name} if stream_qualified_name else {}),
|
||||
"stream_ordinal": stream_ordinal,
|
||||
}
|
||||
public_stream = {
|
||||
"preview": stream.get("preview"),
|
||||
**({"encoding_repaired": True} if stream.get("encoding_repaired") else {}),
|
||||
@@ -41868,8 +41878,9 @@ def public_saved_state_modules_search_row(row: dict[str, Any]) -> dict[str, Any]
|
||||
if stream_module
|
||||
else {}
|
||||
),
|
||||
**({"qualified_name": qualified_name, "display_name": qualified_name} if qualified_name else {}),
|
||||
**({"qualified_name": stream_qualified_name, "display_name": stream_qualified_name} if stream_qualified_name else {}),
|
||||
**({"selector": selector} if selector else {}),
|
||||
"write_plan_target": write_plan_target,
|
||||
"match": {"query": query or None, "in_text": in_text, "in_name": in_name},
|
||||
}
|
||||
public_streams.append(public_stream)
|
||||
@@ -46553,9 +46564,120 @@ def metadata_write_concrete_reference_problem(reference_info: dict[str, str], ta
|
||||
}
|
||||
|
||||
|
||||
def metadata_write_plan_resolve_public_module_target(
|
||||
payload: dict[str, Any],
|
||||
target: dict[str, Any],
|
||||
) -> tuple[dict[str, Any], dict[str, Any]]:
|
||||
"""Resolve a public 1C module selector to an internal saved-state handle."""
|
||||
selector_ref = str(target.get("ref") or target.get("object_ref") or payload.get("ref") or payload.get("object_ref") or "").strip()
|
||||
selector_kind = str(target.get("object_kind") or target.get("object_type") or payload.get("object_kind") or payload.get("object_type") or "").strip()
|
||||
selector_name = str(target.get("object_name") or payload.get("object_name") or "").strip()
|
||||
requested_form = str(target.get("form") or target.get("form_name") or payload.get("form") or payload.get("form_name") or "").strip()
|
||||
requested_module = str(target.get("module") or target.get("module_name") or payload.get("module") or payload.get("module_name") or "").strip()
|
||||
requested_qualified_name = str(target.get("qualified_name") or payload.get("qualified_name") or "").strip()
|
||||
requested_ordinal_raw = target.get("stream_ordinal") if target.get("stream_ordinal") is not None else payload.get("stream_ordinal")
|
||||
try:
|
||||
requested_ordinal = int(requested_ordinal_raw) if requested_ordinal_raw is not None else None
|
||||
except (TypeError, ValueError):
|
||||
requested_ordinal = None
|
||||
public_selector = {
|
||||
**({"ref": selector_ref} if selector_ref else {}),
|
||||
**({"kind": canonical_kind(selector_kind)} if selector_kind else {}),
|
||||
**({"name": selector_name} if selector_name else {}),
|
||||
**({"form": requested_form} if requested_form else {}),
|
||||
**({"module": requested_module} if requested_module else {}),
|
||||
**({"qualified_name": requested_qualified_name} if requested_qualified_name else {}),
|
||||
**({"stream_ordinal": requested_ordinal} if requested_ordinal is not None else {}),
|
||||
}
|
||||
if not (selector_ref or (selector_kind and selector_name)):
|
||||
return target, {
|
||||
"status": "not_attempted",
|
||||
"error": "public_object_selector_missing",
|
||||
"selector": public_selector,
|
||||
}
|
||||
|
||||
search_payload: dict[str, Any] = {
|
||||
"base_id": payload.get("base_id"),
|
||||
"include_storage": True,
|
||||
"limit": int(payload.get("search_limit") or 50),
|
||||
"scan_limit": int(payload.get("scan_limit") or 1000),
|
||||
"timeout_seconds": int(payload.get("timeout_seconds") or 60),
|
||||
}
|
||||
if selector_ref:
|
||||
search_payload["ref"] = selector_ref
|
||||
else:
|
||||
search_payload["object_type"] = selector_kind
|
||||
search_payload["object_name"] = selector_name
|
||||
for key in ("extension", "layer", "tables"):
|
||||
value = target.get(key) if target.get(key) is not None else payload.get(key)
|
||||
if value is not None:
|
||||
search_payload[key] = value
|
||||
search = metadata_saved_state_modules_search(search_payload)
|
||||
if search.get("status") != "ok":
|
||||
return target, {
|
||||
"status": "error",
|
||||
"error": "saved_state_module_search_failed",
|
||||
"selector": public_selector,
|
||||
"search_status": search.get("status"),
|
||||
}
|
||||
|
||||
matches: list[tuple[dict[str, Any], dict[str, Any]]] = []
|
||||
for module_row in search.get("modules") or []:
|
||||
if not isinstance(module_row, dict):
|
||||
continue
|
||||
row_owner = module_row.get("owner") if isinstance(module_row.get("owner"), dict) else {}
|
||||
row_form = module_row.get("form") if isinstance(module_row.get("form"), dict) else {}
|
||||
row_module = module_row.get("module") if isinstance(module_row.get("module"), dict) else {}
|
||||
row_qualified_name = str(module_row.get("qualified_name") or module_row.get("display_name") or "").strip()
|
||||
if requested_form and normalize(str(row_form.get("name") or "")) != normalize(requested_form):
|
||||
continue
|
||||
if requested_module and normalize(str(row_module.get("name") or "")) != normalize(requested_module):
|
||||
continue
|
||||
if requested_qualified_name and normalize(row_qualified_name) != normalize(requested_qualified_name):
|
||||
continue
|
||||
for stream in module_row.get("streams") or []:
|
||||
if not isinstance(stream, dict) or not stream.get("module_ref"):
|
||||
continue
|
||||
stream_form = stream.get("form") if isinstance(stream.get("form"), dict) else row_form
|
||||
stream_module = stream.get("module") if isinstance(stream.get("module"), dict) else row_module
|
||||
stream_qualified_name = str(stream.get("qualified_name") or stream.get("display_name") or row_qualified_name).strip()
|
||||
if requested_form and normalize(str(stream_form.get("name") or "")) != normalize(requested_form):
|
||||
continue
|
||||
if requested_module and normalize(str(stream_module.get("name") or "")) != normalize(requested_module):
|
||||
continue
|
||||
if requested_qualified_name and normalize(stream_qualified_name) != normalize(requested_qualified_name):
|
||||
continue
|
||||
matches.append((module_row, stream))
|
||||
|
||||
if requested_ordinal is not None:
|
||||
matches = [matches[requested_ordinal - 1]] if 1 <= requested_ordinal <= len(matches) else []
|
||||
if len(matches) != 1:
|
||||
return target, {
|
||||
"status": "not_found" if not matches else "ambiguous",
|
||||
"error": "public_module_target_not_resolved",
|
||||
"selector": public_selector,
|
||||
"counts": {"matches": len(matches)},
|
||||
}
|
||||
|
||||
module_row, stream = matches[0]
|
||||
payload_meta = module_row.get("payload") if isinstance(module_row.get("payload"), dict) else {}
|
||||
resolved_target = {
|
||||
**target,
|
||||
"module_ref": stream.get("module_ref"),
|
||||
**({"module_path": stream.get("module_path")} if stream.get("module_path") else {}),
|
||||
**({"expected_sha1": payload_meta.get("sha1")} if payload_meta.get("sha1") else {}),
|
||||
}
|
||||
return resolved_target, {
|
||||
"status": "resolved",
|
||||
"method": SAVED_STATE_MODULES_SEARCH_METHOD,
|
||||
"selector": public_selector,
|
||||
"counts": {"matches": 1},
|
||||
}
|
||||
|
||||
|
||||
def metadata_write_plan(payload: dict[str, Any]) -> dict[str, Any]:
|
||||
method = METADATA_WRITE_PLAN_METHOD
|
||||
target = payload.get("target") if isinstance(payload.get("target"), dict) else {}
|
||||
target = dict(payload.get("target")) if isinstance(payload.get("target"), dict) else {}
|
||||
intent = payload.get("intent") if isinstance(payload.get("intent"), dict) else {}
|
||||
target_kind_raw = (
|
||||
payload.get("target_kind")
|
||||
@@ -46565,6 +46687,19 @@ def metadata_write_plan(payload: dict[str, Any]) -> dict[str, Any]:
|
||||
or payload.get("area")
|
||||
or "metadata"
|
||||
)
|
||||
target_kind_hint = str(target_kind_raw or "").strip().casefold()
|
||||
module_name_resolution: dict[str, Any] | None = None
|
||||
has_public_module_selector = bool(
|
||||
target.get("ref")
|
||||
or target.get("object_ref")
|
||||
or ((target.get("object_kind") or target.get("object_type")) and target.get("object_name"))
|
||||
) and bool(target.get("module") or target.get("module_name") or target.get("qualified_name"))
|
||||
if (
|
||||
target_kind_hint in {"module", "модуль", "bsl"}
|
||||
and not metadata_write_concrete_reference_info(payload, target)
|
||||
and has_public_module_selector
|
||||
):
|
||||
target, module_name_resolution = metadata_write_plan_resolve_public_module_target(payload, target)
|
||||
|
||||
canonical_path = str(
|
||||
target.get("canonical_path")
|
||||
@@ -46608,6 +46743,14 @@ def metadata_write_plan(payload: dict[str, Any]) -> dict[str, Any]:
|
||||
provided_origin = provided_origin if isinstance(provided_origin, dict) else None
|
||||
|
||||
problems = []
|
||||
if module_name_resolution and module_name_resolution.get("status") != "resolved":
|
||||
problems.append(
|
||||
{
|
||||
"code": str(module_name_resolution.get("error") or "public_module_target_not_resolved"),
|
||||
"message": "The public 1C module selector did not resolve to exactly one saved-state module stream.",
|
||||
"resolution_status": module_name_resolution.get("status"),
|
||||
}
|
||||
)
|
||||
if not path_resolution.get("is_full_path") and not concrete_reference:
|
||||
problems.append(
|
||||
{
|
||||
@@ -46682,6 +46825,8 @@ def metadata_write_plan(payload: dict[str, Any]) -> dict[str, Any]:
|
||||
"write_surface": "saved_state" if concrete_reference else "requires_origin_lookup",
|
||||
"apply_method": None,
|
||||
}
|
||||
if module_name_resolution:
|
||||
route["name_resolution"] = module_name_resolution
|
||||
if extension_action:
|
||||
route["extension_action"] = extension_action
|
||||
if operation_was_inferred_from_extension_action:
|
||||
|
||||
Reference in New Issue
Block a user