diff --git a/plugins/1c/parser/scd_payload.py b/plugins/1c/parser/scd_payload.py index 140846c..f3baa7e 100644 --- a/plugins/1c/parser/scd_payload.py +++ b/plugins/1c/parser/scd_payload.py @@ -75,22 +75,20 @@ def query_without_line_comments(text: str) -> str: def node_path(root: ET.Element, target: ET.Element) -> str: """Produce a stable, human-readable evidence path without XML prefixes.""" - def visit(node: ET.Element, prefix: str) -> str | None: - name = local_name(node.tag) - current = f"{prefix}/{name}" if prefix else f"/{name}" + def visit(node: ET.Element, current: str) -> str | None: if node is target: return current positions: dict[str, int] = {} for child in node: child_name = local_name(child.tag) positions[child_name] = positions.get(child_name, 0) + 1 - child_prefix = f"{current}[{positions[child_name]}]" - found = visit(child, child_prefix) + suffix = f"[{positions[child_name]}]" if positions[child_name] > 1 else "" + found = visit(child, f"{current}/{child_name}{suffix}") if found: return found 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]]: @@ -184,6 +182,21 @@ def inspect_scd_payload(data: bytes, *, sections: list[str] | None = None) -> di "container": {**container, "code": "SCD_SCHEMA_NODE_NOT_FOUND"}, "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 = { "parameters": {"parameter"}, "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}) analysis = { "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, "query_parameter_references": query_references, "referenced_not_declared_in_schema": sorted( diff --git a/tests/1c/test_payload_codec.py b/tests/1c/test_payload_codec.py index 210c4bd..7f12e0b 100644 --- a/tests/1c/test_payload_codec.py +++ b/tests/1c/test_payload_codec.py @@ -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"]["variants"][0]["name"] == "Основной" 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"]["total_field_references"] == {"fields": ["Сумма"], "missing_from_declared_fields": [], "status": "checked"} assert result["analysis"]["settings_context"] == {"status": "not_present", "sections": {}}