Expose SCD XML structure outline

This commit is contained in:
2026-08-14 11:59:52 +03:00
parent 03fed309c6
commit 832d159f37
2 changed files with 40 additions and 6 deletions
+27 -6
View File
@@ -75,22 +75,20 @@ def query_without_line_comments(text: str) -> str:
def node_path(root: ET.Element, target: ET.Element) -> str: def node_path(root: ET.Element, target: ET.Element) -> str:
"""Produce a stable, human-readable evidence path without XML prefixes.""" """Produce a stable, human-readable evidence path without XML prefixes."""
def visit(node: ET.Element, prefix: str) -> str | None: def visit(node: ET.Element, current: str) -> str | None:
name = local_name(node.tag)
current = f"{prefix}/{name}" if prefix else f"/{name}"
if node is target: if node is target:
return current return current
positions: dict[str, int] = {} positions: dict[str, int] = {}
for child in node: for child in node:
child_name = local_name(child.tag) child_name = local_name(child.tag)
positions[child_name] = positions.get(child_name, 0) + 1 positions[child_name] = positions.get(child_name, 0) + 1
child_prefix = f"{current}[{positions[child_name]}]" suffix = f"[{positions[child_name]}]" if positions[child_name] > 1 else ""
found = visit(child, child_prefix) found = visit(child, f"{current}/{child_name}{suffix}")
if found: if found:
return found return found
return None return None
return visit(root, "") or "/" return visit(root, f"/{local_name(root.tag)}[1]") or "/"
def xml_from_scd_payload(data: bytes) -> tuple[ET.Element | None, dict[str, Any]]: def xml_from_scd_payload(data: bytes) -> tuple[ET.Element | None, dict[str, Any]]:
@@ -184,6 +182,21 @@ def inspect_scd_payload(data: bytes, *, sections: list[str] | None = None) -> di
"container": {**container, "code": "SCD_SCHEMA_NODE_NOT_FOUND"}, "container": {**container, "code": "SCD_SCHEMA_NODE_NOT_FOUND"},
"sections": {name: [] for name in requested}, "sections": {name: [] for name in requested},
} }
# Keep a complete, evidence-backed map of the XML shape alongside the
# curated semantic sections below. 1C releases and SCD variants may add
# nodes that this adapter does not yet assign a business meaning to. A
# tag/count/path outline makes those nodes visible to callers without
# guessing that (for example) a form property is a dataset or layout.
top_level_counts: dict[str, int] = {}
top_level_paths: dict[str, list[str]] = {}
for child in schema:
tag = local_name(child.tag)
top_level_counts[tag] = top_level_counts.get(tag, 0) + 1
top_level_paths.setdefault(tag, []).append(node_path(schema, child))
xml_tag_counts: dict[str, int] = {}
for node in schema.iter():
tag = local_name(node.tag)
xml_tag_counts[tag] = xml_tag_counts.get(tag, 0) + 1
node_names = { node_names = {
"parameters": {"parameter"}, "parameters": {"parameter"},
"datasets": {"dataSet"}, "datasets": {"dataSet"},
@@ -220,6 +233,14 @@ def inspect_scd_payload(data: bytes, *, sections: list[str] | None = None) -> di
query_references.append({"dataset": dataset.get("name"), "parameters": references}) query_references.append({"dataset": dataset.get("name"), "parameters": references})
analysis = { analysis = {
"kind": "raw_query_parameter_token_scan", "kind": "raw_query_parameter_token_scan",
"schema_outline": {
"kind": "exact_xml_tag_inventory",
"top_level": [
{"tag": tag, "count": top_level_counts[tag], "paths": top_level_paths[tag]}
for tag in sorted(top_level_counts, key=str.casefold)
],
"all_tag_counts": dict(sorted(xml_tag_counts.items(), key=lambda item: item[0].casefold())),
},
"declared_parameters": declared, "declared_parameters": declared,
"query_parameter_references": query_references, "query_parameter_references": query_references,
"referenced_not_declared_in_schema": sorted( "referenced_not_declared_in_schema": sorted(
+13
View File
@@ -231,6 +231,19 @@ def test_scd_payload_decoder_reads_xml_after_platform_prefix() -> None:
assert result["sections"]["calculated_fields"][0]["expression"] == "Сумма * 1.2" assert result["sections"]["calculated_fields"][0]["expression"] == "Сумма * 1.2"
assert result["sections"]["variants"][0]["name"] == "Основной" assert result["sections"]["variants"][0]["name"] == "Основной"
assert result["analysis"]["query_parameter_references"] == [{"dataset": "ОсновнойНабор", "parameters": ["Период"]}] assert result["analysis"]["query_parameter_references"] == [{"dataset": "ОсновнойНабор", "parameters": ["Период"]}]
assert result["analysis"]["schema_outline"] == {
"kind": "exact_xml_tag_inventory",
"top_level": [
{"tag": "calculatedField", "count": 1, "paths": ["/dataCompositionSchema[1]/calculatedField"]},
{"tag": "dataSet", "count": 1, "paths": ["/dataCompositionSchema[1]/dataSet"]},
{"tag": "field", "count": 2, "paths": ["/dataCompositionSchema[1]/field", "/dataCompositionSchema[1]/field[2]"]},
{"tag": "parameter", "count": 1, "paths": ["/dataCompositionSchema[1]/parameter"]},
{"tag": "resource", "count": 1, "paths": ["/dataCompositionSchema[1]/resource"]},
{"tag": "settingsVariant", "count": 1, "paths": ["/dataCompositionSchema[1]/settingsVariant"]},
{"tag": "totalField", "count": 1, "paths": ["/dataCompositionSchema[1]/totalField"]},
],
"all_tag_counts": {"calculatedField": 1, "dataCompositionSchema": 1, "dataPath": 2, "dataSet": 1, "expression": 2, "field": 2, "name": 5, "parameter": 1, "query": 1, "resource": 1, "settingsVariant": 1, "totalField": 1, "valueType": 1},
}
assert result["analysis"]["referenced_not_declared_in_schema"] == [] assert result["analysis"]["referenced_not_declared_in_schema"] == []
assert result["analysis"]["total_field_references"] == {"fields": ["Сумма"], "missing_from_declared_fields": [], "status": "checked"} assert result["analysis"]["total_field_references"] == {"fields": ["Сумма"], "missing_from_declared_fields": [], "status": "checked"}
assert result["analysis"]["settings_context"] == {"status": "not_present", "sections": {}} assert result["analysis"]["settings_context"] == {"status": "not_present", "sections": {}}