179 lines
9.2 KiB
Python
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())
|