Preserve extension names in module preflight

This commit is contained in:
2026-07-26 18:06:43 +03:00
parent ad4bd3ec72
commit 00040e5ce4
7 changed files with 440 additions and 17 deletions
+166
View File
@@ -207,6 +207,57 @@ def first_public_extension_form_candidate(result: dict[str, Any]) -> dict[str, A
return candidate
def first_public_extension_module_candidate(result: dict[str, Any]) -> dict[str, Any] | None:
public_keys = {
"extension",
"ref",
"form",
"module",
"qualified_name",
"stream_ordinal",
}
opaque_keys = {
"table",
"file_name",
"guid",
"form_guid",
"module_guid",
"object_guid",
"module_ref",
"extension_guid",
}
for module in result.get("modules") or []:
if not isinstance(module, dict):
continue
for stream in module.get("streams") or []:
if not isinstance(stream, dict):
continue
target = stream.get("write_plan_target") if isinstance(stream.get("write_plan_target"), dict) else {}
preview = str(stream.get("preview") or "").strip()
control_fragment = next((line.strip() for line in preview.splitlines() if line.strip()), "")
if not target.get("extension") or not target.get("ref") or not target.get("module"):
continue
if not preview or not control_fragment or any(key in target for key in opaque_keys):
continue
public_target = {
"kind": "module",
**{
key: value
for key, value in target.items()
if key in public_keys and value not in (None, "")
},
}
return {
"target": public_target,
"intent": {
"operation": "replace_with_control",
"control_fragment": control_fragment,
"new": preview,
},
}
return None
def different_extension_guid(layer_id: str) -> str | None:
if not EXTENSION_GUID_LAYER_RE.fullmatch(str(layer_id or "")):
return None
@@ -222,6 +273,7 @@ def run_smoke(
*,
transport: str,
require_name_first_extension_form: bool = False,
require_name_first_extension_module: bool = False,
) -> dict[str, Any]:
failures: list[str] = []
checks: dict[str, Any] = {}
@@ -410,6 +462,117 @@ def run_smoke(
require(layer_resolution.get("status") == "conflict", "conflicting extension selector must report conflict resolution status", failures)
require(layer_diagnostics.get("error") == "extension_selector_conflict", "conflicting extension selector must report extension_selector_conflict", failures)
modules = rpc_call(
endpoint_url,
"metadata.saved_state.modules.search",
{
"base_id": base_id,
"tables": ["ConfigCASSave"],
"limit": 20,
"scan_limit": 1000,
"timeout_seconds": int(timeout),
},
timeout,
transport=transport,
session_id=session_id,
)
module_candidate = first_public_extension_module_candidate(modules)
if not module_candidate:
checks["name_first_extension_module_preflight"] = {
"status": "skipped_no_public_extension_module_target",
"search_status": modules.get("status"),
"modules": int((modules.get("counts") or {}).get("modules") or 0),
}
checks["conflicting_extension_module_selector_preflight"] = {
"status": "skipped_no_public_extension_module_target",
}
if require_name_first_extension_module:
failures.append("name-first extension module preflight target is required but was not discovered")
else:
module_preflight = rpc_call(
endpoint_url,
"metadata.write.preflight",
{
"base_id": base_id,
"target": module_candidate["target"],
"intent": module_candidate["intent"],
"resolve_origin": False,
"timeout_seconds": int(timeout),
},
timeout,
transport=transport,
session_id=session_id,
)
plan = module_preflight.get("plan") if isinstance(module_preflight.get("plan"), dict) else {}
repository = module_preflight.get("repository") if isinstance(module_preflight.get("repository"), dict) else {}
support = module_preflight.get("support") if isinstance(module_preflight.get("support"), dict) else {}
repository_layer = str(repository.get("layer_id") or "")
support_layer = str(support.get("layer_id") or "")
name_first = not any(
key in module_candidate["target"]
for key in ("table", "file_name", "guid", "form_guid", "module_guid", "object_guid", "module_ref", "extension_guid")
)
checks["name_first_extension_module_preflight"] = {
"schema": module_preflight.get("schema"),
"status": module_preflight.get("status"),
"allowed": module_preflight.get("allowed"),
"plan_status": plan.get("status"),
"plan_allowed": plan.get("allowed"),
"repository_layer_id": repository_layer or None,
"support_layer_id": support_layer or None,
"name_first": name_first,
"extension": module_candidate["target"].get("extension"),
}
require(module_preflight.get("schema") == "onec_metadata_write_preflight.v1", "name-first extension module preflight must return expected schema", failures)
require(classified_preflight_status(module_preflight.get("status")), "name-first extension module preflight must classify readiness or a safety gate", failures)
require(plan.get("status") == "planned" and plan.get("allowed") is True, "name-first extension module preflight plan must be allowed and planned", failures)
require(name_first, "extension module preflight input must remain name-first", failures)
require(bool(EXTENSION_GUID_LAYER_RE.fullmatch(repository_layer)), "module repository gate must use a resolved extension GUID layer", failures)
require(repository_layer == support_layer, "module repository and support gates must use the same extension layer", failures)
conflicting_guid = different_extension_guid(repository_layer)
if not conflicting_guid:
checks["conflicting_extension_module_selector_preflight"] = {
"status": "skipped_no_resolved_extension_layer",
}
else:
conflicting = rpc_call(
endpoint_url,
"metadata.write.preflight",
{
"base_id": base_id,
"extension_guid": conflicting_guid,
"target": module_candidate["target"],
"intent": module_candidate["intent"],
"resolve_origin": False,
"timeout_seconds": int(timeout),
},
timeout,
transport=transport,
session_id=session_id,
)
layer_resolution = (
conflicting.get("development_layer_resolution")
if isinstance(conflicting.get("development_layer_resolution"), dict)
else {}
)
layer_diagnostics = (
layer_resolution.get("diagnostics")
if isinstance(layer_resolution.get("diagnostics"), dict)
else {}
)
checks["conflicting_extension_module_selector_preflight"] = {
"schema": conflicting.get("schema"),
"status": conflicting.get("status"),
"allowed": conflicting.get("allowed"),
"resolution_status": layer_resolution.get("status"),
"error": layer_diagnostics.get("error"),
"layer_id": layer_resolution.get("layer_id"),
}
require(conflicting.get("schema") == "onec_metadata_write_preflight.v1", "conflicting extension module selector preflight must return expected schema", failures)
require(conflicting.get("status") == "blocked" and conflicting.get("allowed") is False, "conflicting extension module name/GUID must block preflight", failures)
require(layer_resolution.get("status") == "conflict", "conflicting extension module selector must report conflict resolution status", failures)
require(layer_diagnostics.get("error") == "extension_selector_conflict", "conflicting extension module selector must report extension_selector_conflict", failures)
return {
"schema": "onec_write_preflight_smoke.v1",
"status": "ok" if not failures else "failed",
@@ -418,6 +581,7 @@ def run_smoke(
"base_id": base_id,
"requirements": {
"name_first_extension_form": require_name_first_extension_form,
"name_first_extension_module": require_name_first_extension_module,
},
"checks": checks,
"failures": failures,
@@ -434,6 +598,7 @@ def main() -> int:
parser.add_argument("--timeout", type=float, default=30.0)
parser.add_argument("--report", type=Path)
parser.add_argument("--require-name-first-extension-form", action="store_true")
parser.add_argument("--require-name-first-extension-module", action="store_true")
args = parser.parse_args()
endpoint_url = args.mcp_url if args.transport == "mcp" else args.base_url
@@ -444,6 +609,7 @@ def main() -> int:
args.timeout,
transport=args.transport,
require_name_first_extension_form=args.require_name_first_extension_form,
require_name_first_extension_module=args.require_name_first_extension_module,
)
except (TimeoutError, URLError, OSError) as exc:
report = {