Files
llm/scripts/plan_1c_moxel_next_experiments.py

179 lines
9.2 KiB
Python

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