from __future__ import annotations import argparse import json from pathlib import Path from typing import Any def read_json(path: Path) -> dict[str, Any]: return json.loads(path.read_text(encoding="utf-8")) def registry_targets(registry: dict[str, Any], *, status: str) -> set[str]: return { str(rule.get("target")) for rule in registry.get("rules") or [] if isinstance(rule, dict) and rule.get("read_status") == status and rule.get("target") } def has_zero_sample_named_range_verification(verification: dict[str, Any] | None) -> bool: if not verification: return False matched = False for result in verification.get("results") or []: if not isinstance(result, dict) or not str(result.get("target") or "").startswith("moxel.named_range."): continue matched = True for probe in result.get("probes") or []: if not isinstance(probe, dict): continue evidence = probe.get("evidence") if isinstance(probe.get("evidence"), dict) else {} if int(evidence.get("total") or 0) > 0: return False return matched def build_plan(registry: dict[str, Any], verification: dict[str, Any] | None = None) -> dict[str, Any]: candidate_targets = registry_targets(registry, status="candidate_read") verified_targets = registry_targets(registry, status="verified_read") experiments: list[dict[str, Any]] = [] named_range_targets = {target for target in candidate_targets if target.startswith("moxel.named_range.")} if named_range_targets: if has_zero_sample_named_range_verification(verification): experiments.append( { "id": "named_range_presence_probe", "goal": "Capture at least one decodable named range sample before splitting coordinate indexes.", "target": "named_range", "manual_action": "Create a visible named range `RANGE_RECT_TEST` through the 1C table named-area/range UI, save the template, and do not change cell text or formatting.", "expected_signal": "The next probe should report `named_ranges` or `named_range_candidates` with name `RANGE_RECT_TEST` and non-empty raw_scalars.", "rules_unblocked": sorted(named_range_targets), "capture_command": "python scripts/watch_1c_moxel_property_experiment.py --property NamedRangePresence --target-name RANGE_RECT_TEST --target-text \"\" --timeout-seconds 600", } ) experiments.extend( [ { "id": "named_range_rectangular_area", "goal": "Split left/right and top/bottom raw scalar indexes.", "target": "named_range", "manual_action": "Create or move a named range `RANGE_RECT_TEST` to a non-square rectangle, for example R8C4:R10C7, without changing other cells.", "expected_signal": "raw_scalars should contain distinct left, top, right, bottom values so candidate indexes can be assigned one-to-one.", "rules_unblocked": sorted(named_range_targets), "capture_command": "python scripts/watch_1c_moxel_property_experiment.py --property NamedRangeRectangle --target-name RANGE_RECT_TEST --target-text \"\" --timeout-seconds 600", }, { "id": "named_range_horizontal_resize", "goal": "Separate right from left.", "target": "named_range", "manual_action": "Resize only the right edge of `RANGE_RECT_TEST`, for example R8C4:R10C7 -> R8C4:R10C9.", "expected_signal": "Only the raw scalar index for right should change.", "rules_unblocked": ["moxel.named_range.right"], "capture_command": "python scripts/watch_1c_moxel_property_experiment.py --property NamedRangeRightResize --target-name RANGE_RECT_TEST --target-text \"\" --timeout-seconds 600", }, { "id": "named_range_vertical_resize", "goal": "Separate bottom from top.", "target": "named_range", "manual_action": "Resize only the bottom edge of `RANGE_RECT_TEST`, for example R8C4:R10C7 -> R8C4:R12C7.", "expected_signal": "Only the raw scalar index for bottom should change.", "rules_unblocked": ["moxel.named_range.bottom"], "capture_command": "python scripts/watch_1c_moxel_property_experiment.py --property NamedRangeBottomResize --target-name RANGE_RECT_TEST --target-text \"\" --timeout-seconds 600", }, ] ) property_plan = [ ("cell_horizontal_align", "ГоризонтальноеПоложение", "Change only horizontal alignment on the tracked cell."), ("cell_vertical_align", "ВертикальноеПоложение", "Change only vertical alignment on the tracked cell."), ("cell_font_size", "Шрифт.Размер", "Change only font size on the tracked cell."), ("cell_text_color", "ЦветТекста", "Change only text color on the tracked cell."), ("column_width", "ШиринаКолонки", "Change only width of the tracked column."), ("row_height", "ВысотаСтроки", "Change only height of the tracked row."), ] for experiment_id, property_name, action in property_plan: experiments.append( { "id": experiment_id, "goal": f"Map MOXCEL scalar path for `{property_name}`.", "target": "cell_or_layout_property", "manual_action": action, "expected_signal": "One local candidate path should dominate in analyze_1c_moxel_property_experiments.py.", "rules_unblocked": [], "capture_command": f"python scripts/watch_1c_moxel_property_experiment.py --property {property_name} --target-text \"Ячейка 7 - 2\" --target-name R7C2_TEST --timeout-seconds 600", } ) verification_summary = None if verification: verification_summary = verification.get("counts") or {} return { "schema": "codex_1c_moxel_next_experiments_plan.v1", "status": "ok", "basis": { "verified_targets": sorted(verified_targets), "candidate_targets": sorted(candidate_targets), "verification_counts": verification_summary, }, "experiments": experiments, "counts": { "experiments": len(experiments), "named_range_experiments": sum(1 for item in experiments if item.get("target") == "named_range"), "property_experiments": sum(1 for item in experiments if item.get("target") == "cell_or_layout_property"), }, } def render_markdown(plan: dict[str, Any]) -> str: lines = ["# 1C MOXCEL Next Experiments", ""] counts = plan.get("counts") or {} lines.append(f"- Experiments: `{counts.get('experiments')}`") lines.append(f"- Named range: `{counts.get('named_range_experiments')}`") lines.append(f"- Properties: `{counts.get('property_experiments')}`") lines.append("") lines.append("| Experiment | Target | Goal |") lines.append("| --- | --- | --- |") for item in plan.get("experiments") or []: lines.append(f"| `{item.get('id')}` | `{item.get('target')}` | {item.get('goal')} |") lines.append("") lines.append("## Commands") lines.append("") for item in plan.get("experiments") or []: lines.append(f"- `{item.get('id')}`") lines.append("") lines.append("```powershell") lines.append(str(item.get("capture_command") or "")) lines.append("```") lines.append("") return "\n".join(lines) def main() -> int: parser = argparse.ArgumentParser(description="Plan the next controlled 1C MOXCEL experiments from registry gaps.") parser.add_argument("--registry", default="plugins/1c/metadata/moxel-schema-registry.json") parser.add_argument("--verification", default="reports/1c-template-baselines/moxel-schema-registry-verification.json") parser.add_argument("--output-json", default="reports/1c-template-baselines/moxel-next-experiments.json") parser.add_argument("--output-markdown", default="reports/1c-template-baselines/moxel-next-experiments.md") args = parser.parse_args() registry = read_json(Path(args.registry)) verification_path = Path(args.verification) verification = read_json(verification_path) if verification_path.exists() else None plan = build_plan(registry, verification) 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(plan, ensure_ascii=False, indent=2), encoding="utf-8") md_path.write_text(render_markdown(plan), encoding="utf-8") print(json.dumps({"status": "ok", "json": str(json_path), "markdown": str(md_path), "counts": plan["counts"]}, ensure_ascii=False, indent=2)) return 0 if __name__ == "__main__": raise SystemExit(main())