Files
llm/scripts/analyze_1c_moxel_named_range_rules.py
T

149 lines
6.0 KiB
Python

from __future__ import annotations
import argparse
import json
from pathlib import Path
from typing import Any
FIELDS = ("left", "right", "top", "bottom")
def read_json(path: Path) -> dict[str, Any]:
return json.loads(path.read_text(encoding="utf-8"))
def as_int(value: Any) -> int | None:
try:
return int(value)
except (TypeError, ValueError):
return None
def probe_ranges(payload: dict[str, Any]) -> list[dict[str, Any]]:
probe = payload.get("probe") if isinstance(payload.get("probe"), dict) else payload
ranges = probe.get("named_ranges") or probe.get("named_range_candidates") or []
return [item for item in ranges if isinstance(item, dict)]
def candidate_indexes(raw_scalars: list[Any], expected_one_based: int) -> list[int]:
result = []
for index, value in enumerate(raw_scalars):
if index < 2 or index > 5:
continue
parsed = as_int(value)
if parsed is not None and parsed + 1 == expected_one_based:
result.append(index)
return result
def analyze_range(item: dict[str, Any]) -> dict[str, Any] | None:
range_info = item.get("range") if isinstance(item.get("range"), dict) else {}
one_based = range_info.get("one_based") if isinstance(range_info.get("one_based"), dict) else {}
raw_scalars = item.get("raw_scalars") if isinstance(item.get("raw_scalars"), list) else []
if not one_based or not raw_scalars:
return None
field_candidates: dict[str, list[int]] = {}
for field in FIELDS:
expected = as_int(one_based.get(field))
if expected is None:
continue
field_candidates[field] = candidate_indexes(raw_scalars, expected)
unique_values = len({one_based.get(field) for field in FIELDS if one_based.get(field) is not None})
return {
"name": item.get("name"),
"kind": item.get("kind"),
"one_based": {field: one_based.get(field) for field in FIELDS if field in one_based},
"raw_scalars": raw_scalars,
"field_candidates": field_candidates,
"distinct_coordinate_values": unique_values,
}
def aggregate_rules(samples: list[dict[str, Any]]) -> list[dict[str, Any]]:
rules = []
for field in FIELDS:
sample_candidates = [set(sample.get("field_candidates", {}).get(field) or []) for sample in samples if sample.get("field_candidates", {}).get(field)]
if not sample_candidates:
continue
intersection = set.intersection(*sample_candidates) if sample_candidates else set()
all_distinct = all(int(sample.get("distinct_coordinate_values") or 0) >= 4 for sample in samples)
confidence = "high" if len(intersection) == 1 and all_distinct else "medium" if intersection else "low"
rules.append(
{
"target": f"moxel.named_range.{field}",
"expression": "one_based = int(raw_scalar) + 1",
"raw_scalar_indexes": sorted(intersection) if intersection else sorted(set.union(*sample_candidates)),
"confidence": confidence,
"evidence": {
"samples": len(sample_candidates),
"distinct_rectangular_samples": sum(1 for sample in samples if int(sample.get("distinct_coordinate_values") or 0) >= 4),
},
}
)
return rules
def analyze(probes: list[dict[str, Any]], target_name: str | None = None) -> dict[str, Any]:
samples = []
for payload in probes:
for item in probe_ranges(payload):
if target_name and str(item.get("name") or "") != target_name:
continue
sample = analyze_range(item)
if sample:
samples.append(sample)
return {
"schema": "codex_1c_moxel_named_range_rule_analysis.v1",
"target_name": target_name,
"status": "ok",
"samples": samples,
"rules": aggregate_rules(samples),
"counts": {
"samples": len(samples),
"rules": 0,
},
}
def render_markdown(payload: dict[str, Any]) -> str:
payload["counts"]["rules"] = len(payload.get("rules") or [])
lines = ["# 1C MOXCEL Named Range Rule Analysis", ""]
lines.append(f"- Samples: `{payload.get('counts', {}).get('samples')}`")
lines.append(f"- Rules: `{payload.get('counts', {}).get('rules')}`")
lines.append("")
lines.append("| Target | Confidence | Raw indexes | Samples |")
lines.append("| --- | --- | --- | --- |")
for rule in payload.get("rules") or []:
evidence = rule.get("evidence") or {}
lines.append(
f"| `{rule.get('target')}` | `{rule.get('confidence')}` | "
f"`{', '.join(map(str, rule.get('raw_scalar_indexes') or []))}` | `{evidence.get('samples')}` |"
)
lines.append("")
return "\n".join(lines)
def main() -> int:
parser = argparse.ArgumentParser(description="Infer MOXCEL named range coordinate scalar indexes from probe snapshots.")
parser.add_argument("--probe", action="append", required=True, help="Probe snapshot JSON. Repeatable.")
parser.add_argument("--target-name", help="Optional named range to analyze.")
parser.add_argument("--output-json", default="reports/1c-template-baselines/moxel-named-range-rules.json")
parser.add_argument("--output-markdown", default="reports/1c-template-baselines/moxel-named-range-rules.md")
args = parser.parse_args()
payload = analyze([read_json(Path(path)) for path in args.probe], args.target_name)
payload["counts"]["rules"] = len(payload.get("rules") or [])
json_path = Path(args.output_json)
md_path = Path(args.output_markdown)
json_path.parent.mkdir(parents=True, exist_ok=True)
md_path.parent.mkdir(parents=True, exist_ok=True)
json_path.write_text(json.dumps(payload, ensure_ascii=False, indent=2), encoding="utf-8")
md_path.write_text(render_markdown(payload), encoding="utf-8")
print(json.dumps({"status": "ok", "json": str(json_path), "markdown": str(md_path), "counts": payload["counts"]}, ensure_ascii=False, indent=2))
return 0
if __name__ == "__main__":
raise SystemExit(main())