85 lines
2.5 KiB
Python
85 lines
2.5 KiB
Python
"""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
|