Files
llm/scripts/analyze_1c_write_matrix_gaps.py
T

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())