163 lines
8.0 KiB
Python
163 lines
8.0 KiB
Python
#!/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())
|