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