"""Mechanical section summaries for Config brace trees. This module intentionally does not name sections as attributes, tabular sections, forms, etc. It only reports paths, shapes, strings, and GUIDs so a higher-level validator can attach semantics using XML or other evidence. """ from __future__ import annotations from dataclasses import asdict, dataclass from typing import Any from .payload import GUID_RE, collect_strings, scalar @dataclass(frozen=True) class SectionSummary: path: str node_type: str list_len: int | None first_scalars: list[str] string_count_sampled: int guid_count_sampled: int strings_sample: list[str] guids_sample: list[str] def to_dict(self) -> dict[str, Any]: return asdict(self) def _children(node: Any) -> list[Any]: if isinstance(node, dict) and node.get("type") in {"list", "sequence"}: return node.get("items") or [] return [] def _atoms(node: Any, limit: int) -> list[str]: values: list[str] = [] def walk(value: Any) -> None: if len(values) >= limit: return if isinstance(value, dict) and value.get("type") in {"atom", "string"}: text = scalar(value) if text: values.append(text) return for child in _children(value): walk(child) walk(node) return values def summarize_section(node: Any, path: str, *, limit: int = 200) -> SectionSummary: children = _children(node) atoms = _atoms(node, limit) strings = collect_strings(node, limit=limit) guids = sorted(set(value.lower() for value in atoms if GUID_RE.fullmatch(value))) return SectionSummary( path=path, node_type=node.get("type") if isinstance(node, dict) else type(node).__name__, list_len=len(children) if children else None, first_scalars=[scalar(child) for child in children[:12]], string_count_sampled=len(strings), guid_count_sampled=len(guids), strings_sample=strings[:50], guids_sample=guids[:50], ) def summarize_sections(tree: Any, *, max_depth: int = 2, limit: int = 200) -> list[SectionSummary]: summaries: list[SectionSummary] = [] def walk(node: Any, path: list[int], depth: int) -> None: if depth > max_depth: return if path: summaries.append(summarize_section(node, ".".join(str(part) for part in path), limit=limit)) for index, child in enumerate(_children(node)): walk(child, [*path, index], depth + 1) walk(tree, [], 0) return summaries