#!/usr/bin/env python3 from __future__ import annotations import argparse import json from collections import Counter, defaultdict from pathlib import Path from typing import Any def load_entries(report: dict[str, Any]) -> list[dict[str, Any]]: matrix = report.get("matrix") if isinstance(report.get("matrix"), dict) else {} entries = matrix.get("entries") if isinstance(matrix.get("entries"), list) else [] return [entry for entry in entries if isinstance(entry, dict)] def gap_row(entry: dict[str, Any]) -> dict[str, Any]: requested = entry.get("requested_target") if isinstance(entry.get("requested_target"), dict) else {} effective = entry.get("effective_target") if isinstance(entry.get("effective_target"), dict) else {} source = entry.get("effective_source") if isinstance(entry.get("effective_source"), dict) else {} prop = entry.get("property") if isinstance(entry.get("property"), dict) else {} probe = entry.get("codec_probe") if isinstance(entry.get("codec_probe"), dict) else {} probe_node = probe.get("node") if isinstance(probe.get("node"), dict) else {} return { "reason": entry.get("reason") or "unknown", "requested_section": requested.get("section"), "requested_name": requested.get("name"), "requested_path": requested.get("path"), "effective_section": effective.get("section"), "effective_name": effective.get("name"), "effective_path": effective.get("path"), "source_kind": source.get("kind") or "local", "property": prop.get("semantic_name") or prop.get("canonical_property") or prop.get("property"), "canonical_property": prop.get("canonical_property") or prop.get("property"), "presentation": prop.get("presentation"), "semantic_name": prop.get("semantic_name"), "semantic_group": prop.get("semantic_group"), "semantic_source": prop.get("semantic_source"), "parameter_index": prop.get("parameter_index"), "value_type": prop.get("value_type"), "old": prop.get("old"), "read_path": prop.get("read_path"), "write_path": prop.get("write_path"), "verification": prop.get("verification"), "codec_probe": probe or None, "codec_probe_node_type": probe_node.get("type") or probe.get("error") if probe else None, } def classify_action(reason: str, prop: str | None, value_type: str | None) -> str: if reason == "identity_or_binding_property": return "manual_only_identity_or_binding" if reason == "empty_local_string_requires_codec_probe": return "add_empty_composite_string_codec_probe" if reason == "composite_node_requires_semantic_rule": return "learn_composite_node_semantics" if reason == "value_type_not_smoke_safe": if value_type in {"enum_atom", "bool_or_enum_atom", "color_or_enum_atom"}: return "learn_allowed_enum_values" if prop in {"group"}: return "learn_reference_or_container_write_rule" return "classify_scalar_semantics" return "inspect" def main() -> int: parser = argparse.ArgumentParser(description="Analyze not-smoked entries from a 1C saved-state write matrix report.") parser.add_argument("--matrix-report", type=Path, required=True, help="Report produced by scripts/smoke_1c_write_matrix.py.") parser.add_argument("--output", type=Path, required=True, help="Output JSON gap report.") parser.add_argument("--sample-limit", type=int, default=12, help="Samples per reason/action.") args = parser.parse_args() report = json.loads(args.matrix_report.read_text(encoding="utf-8")) gaps = [] for entry in load_entries(report): if entry.get("can_smoke"): continue row = gap_row(entry) row["next_action"] = classify_action(str(row.get("reason") or ""), row.get("property"), row.get("value_type")) gaps.append(row) by_reason = Counter(row["reason"] for row in gaps) by_action = Counter(row["next_action"] for row in gaps) by_section = Counter(row["effective_section"] for row in gaps) by_property = Counter(row["property"] for row in gaps) by_probe_node_type = Counter(row.get("codec_probe_node_type") for row in gaps if row.get("codec_probe_node_type")) samples_by_reason: dict[str, list[dict[str, Any]]] = defaultdict(list) samples_by_action: dict[str, list[dict[str, Any]]] = defaultdict(list) shape_summary: dict[str, dict[str, Any]] = defaultdict(lambda: {"count": 0, "properties": Counter(), "sections": Counter(), "samples": []}) for row in gaps: reason = str(row["reason"]) action = str(row["next_action"]) probe = row.get("codec_probe") if isinstance(row.get("codec_probe"), dict) else {} node = probe.get("node") if isinstance(probe.get("node"), dict) else {} children = node.get("children") if isinstance(node.get("children"), list) else [] if node: shape = str(node.get("type") or "unknown") + "|" + ",".join(str((child or {}).get("type")) for child in children[:12]) shape_row = shape_summary[shape] shape_row["count"] = int(shape_row["count"]) + 1 shape_row["properties"][row.get("property")] += 1 shape_row["sections"][row.get("effective_section")] += 1 if len(shape_row["samples"]) < args.sample_limit: shape_row["samples"].append( { "target": row.get("requested_name") or row.get("requested_path"), "section": row.get("effective_section"), "property": row.get("presentation") or row.get("property"), "semantic_name": row.get("semantic_name"), "semantic_group": row.get("semantic_group"), "old": row.get("old"), "write_path": row.get("write_path"), } ) sample = { "target": row.get("requested_name") or row.get("requested_path"), "section": row.get("effective_section"), "property": row.get("presentation") or row.get("property"), "semantic_name": row.get("semantic_name"), "semantic_group": row.get("semantic_group"), "value_type": row.get("value_type"), "old": row.get("old"), "write_path": row.get("write_path"), } if len(samples_by_reason[reason]) < args.sample_limit: samples_by_reason[reason].append(sample) if len(samples_by_action[action]) < args.sample_limit: samples_by_action[action].append(sample) result = { "schema": "onec_form_write_matrix_gap_analysis.v1", "status": "ok", "source_report": str(args.matrix_report), "counts": { "gaps": len(gaps), "by_reason": dict(sorted(by_reason.items())), "by_next_action": dict(sorted(by_action.items())), "by_effective_section": dict(sorted(by_section.items())), "by_codec_probe_node_type": dict(sorted(by_probe_node_type.items())), "top_properties": by_property.most_common(40), }, "samples_by_reason": dict(samples_by_reason), "samples_by_next_action": dict(samples_by_action), "codec_probe_shapes": [ { "shape": shape, "count": row["count"], "properties": row["properties"].most_common(20), "sections": dict(row["sections"]), "samples": row["samples"], } for shape, row in sorted(shape_summary.items(), key=lambda item: int(item[1]["count"]), reverse=True) ], "gaps": gaps, } args.output.parent.mkdir(parents=True, exist_ok=True) args.output.write_text(json.dumps(result, ensure_ascii=False, indent=2) + "\n", encoding="utf-8") print(json.dumps({"schema": result["schema"], "status": "ok", "counts": result["counts"], "path": str(args.output)}, ensure_ascii=False, indent=2)) return 0 if __name__ == "__main__": raise SystemExit(main())