from __future__ import annotations import argparse import json import subprocess import sys import time from datetime import UTC, datetime from pathlib import Path from typing import Any ROOT = Path(__file__).resolve().parents[1] SCRIPTS = ROOT / "scripts" if str(SCRIPTS) not in sys.path: sys.path.insert(0, str(SCRIPTS)) import capture_1c_template_probe as probe # noqa: E402 def read_json(path: Path) -> dict[str, Any]: return json.loads(path.read_text(encoding="utf-8")) if path.exists() else {} def write_json(path: Path, payload: dict[str, Any]) -> None: path.parent.mkdir(parents=True, exist_ok=True) path.write_text(json.dumps(payload, ensure_ascii=False, indent=2), encoding="utf-8") def snapshot_identity(snapshot: dict[str, Any]) -> dict[str, Any]: return { "file_name": snapshot.get("file_name"), "modified": snapshot.get("modified"), "bytes": snapshot.get("bytes"), } def capture_snapshot( *, adapter_url: str, base_id: str, output_dir: Path, label: str, scan_limit: int, max_cells: int, file_name: str | None = None, compare_to: Path | None = None, ) -> tuple[Path, Path, dict[str, Any]]: output_dir.mkdir(parents=True, exist_ok=True) selected_file, row, structure = probe.choose_latest_moxel(adapter_url, base_id, scan_limit, max_cells, file_name) snapshot = { "schema": "codex_1c_template_probe_snapshot.v1", "captured_at": datetime.now(UTC).isoformat(timespec="seconds").replace("+00:00", "Z"), "adapter_url": adapter_url, "base_id": base_id, "file_name": selected_file, "modified": row.get("Modified"), "bytes": row.get("Bytes"), "label": label, "probe": probe.build_snapshot(structure), } diff: dict[str, Any] | None = None if compare_to and compare_to.exists(): previous = read_json(compare_to) diff = probe.build_diff(previous.get("probe") or {}, snapshot.get("probe") or {}) snapshot["diff"] = {"compare_to": str(compare_to), "summary": diff.get("summary") or {}} timestamp = datetime.now(UTC).strftime("%Y%m%dT%H%M%SZ") stem = f"{base_id}_{label}_{timestamp}_{selected_file[:8]}" json_path = output_dir / f"{stem}.json" md_path = output_dir / f"{stem}.md" write_json(json_path, snapshot) md_path.write_text(probe.render_markdown(snapshot, diff, compare_to if compare_to and compare_to.exists() else None), encoding="utf-8") return json_path, md_path, snapshot def append_manifest_experiment( manifest_path: Path, *, property_name: str, operation: str, target_text: str | None, target_name: str | None, before_path: Path, after_path: Path, ) -> dict[str, Any]: manifest = read_json(manifest_path) if not manifest: manifest = {"schema": "codex_1c_moxel_property_experiment_manifest.v1", "experiments": []} manifest.setdefault("schema", "codex_1c_moxel_property_experiment_manifest.v1") manifest.setdefault("experiments", []) base_dir = manifest_path.parent experiment = { "property": property_name, "operation": operation, "target_text": target_text, "target_name": target_name, "before": str(before_path.resolve().relative_to(base_dir.resolve())) if before_path.resolve().is_relative_to(base_dir.resolve()) else str(before_path), "after": str(after_path.resolve().relative_to(base_dir.resolve())) if after_path.resolve().is_relative_to(base_dir.resolve()) else str(after_path), } manifest["experiments"].append(experiment) write_json(manifest_path, manifest) return experiment def pipeline_command(after_path: Path, manifest_path: Path) -> list[str]: return [ sys.executable, "scripts/run_1c_moxel_discovery_pipeline.py", "--probe", str(after_path), "--property-manifest", str(manifest_path), ] def wait_for_new_moxel( *, adapter_url: str, base_id: str, baseline: dict[str, Any], scan_limit: int, max_cells: int, poll_seconds: float, timeout_seconds: float, ) -> tuple[str, dict[str, Any], dict[str, Any]] | None: started = time.monotonic() baseline_identity = snapshot_identity(baseline) while time.monotonic() - started <= timeout_seconds: file_name, row, structure = probe.choose_latest_moxel(adapter_url, base_id, scan_limit, max_cells, None) current_identity = { "file_name": file_name, "modified": row.get("Modified"), "bytes": row.get("Bytes"), } if current_identity != baseline_identity: return file_name, row, structure time.sleep(max(0.2, poll_seconds)) return None def main() -> int: parser = argparse.ArgumentParser(description="Capture a before/after MOXCEL one-property experiment around a manual 1C save.") parser.add_argument("--adapter-url", default="http://docker-gpu.cin.su:8011") parser.add_argument("--base-id", default="upo_test") parser.add_argument("--property", required=True, help="Property label, for example ВертикальноеПоложение.") parser.add_argument("--operation", default="manual_one_property_save") parser.add_argument("--target-text", default="Ячейка 7 - 2") parser.add_argument("--target-name", default="R7C2_TEST") parser.add_argument("--output-dir", default="reports/1c-template-probes") parser.add_argument("--manifest", default="reports/1c-template-baselines/Primer3_moxel_property_experiments.manifest.json") parser.add_argument("--scan-limit", type=int, default=30) parser.add_argument("--max-cells", type=int, default=800) parser.add_argument("--poll-seconds", type=float, default=5.0) parser.add_argument("--timeout-seconds", type=float, default=600.0) parser.add_argument("--baseline", help="Existing before snapshot JSON. If omitted, capture one before waiting.") parser.add_argument("--after-file-name", help="Explicit after ConfigCAS file name. Skips waiting.") parser.add_argument("--run-pipeline-after", action="store_true", help="Run the MOXCEL discovery pipeline after appending the experiment.") args = parser.parse_args() output_dir = Path(args.output_dir) manifest_path = Path(args.manifest) if args.baseline: before_path = Path(args.baseline) before_snapshot = read_json(before_path) else: before_path, _, before_snapshot = capture_snapshot( adapter_url=args.adapter_url, base_id=args.base_id, output_dir=output_dir, label=f"{args.property}_before", scan_limit=args.scan_limit, max_cells=args.max_cells, ) if args.after_file_name: after_path, after_md, after_snapshot = capture_snapshot( adapter_url=args.adapter_url, base_id=args.base_id, output_dir=output_dir, label=f"{args.property}_after", scan_limit=args.scan_limit, max_cells=args.max_cells, file_name=args.after_file_name, compare_to=before_path, ) else: found = wait_for_new_moxel( adapter_url=args.adapter_url, base_id=args.base_id, baseline=before_snapshot, scan_limit=args.scan_limit, max_cells=args.max_cells, poll_seconds=args.poll_seconds, timeout_seconds=args.timeout_seconds, ) if not found: print( json.dumps( { "status": "no_change", "before": str(before_path), "message": f"No new MOXCEL payload appeared within {args.timeout_seconds:.0f} seconds.", }, ensure_ascii=False, indent=2, ) ) return 2 file_name, _, _ = found after_path, after_md, after_snapshot = capture_snapshot( adapter_url=args.adapter_url, base_id=args.base_id, output_dir=output_dir, label=f"{args.property}_after", scan_limit=args.scan_limit, max_cells=args.max_cells, file_name=file_name, compare_to=before_path, ) experiment = append_manifest_experiment( manifest_path, property_name=args.property, operation=args.operation, target_text=args.target_text, target_name=args.target_name, before_path=before_path, after_path=after_path, ) pipeline: dict[str, Any] | None = None if args.run_pipeline_after: command = pipeline_command(after_path, manifest_path) result = subprocess.run(command, cwd=ROOT, text=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=False) pipeline = { "command": command, "status": "ok" if result.returncode == 0 else "failed", "returncode": result.returncode, "stdout": result.stdout.strip(), "stderr": result.stderr.strip(), } print( json.dumps( { "status": "ok", "before": str(before_path), "after": str(after_path), "after_markdown": str(after_md), "manifest": str(manifest_path), "experiment": experiment, "before_identity": snapshot_identity(before_snapshot), "after_identity": snapshot_identity(after_snapshot), **({"pipeline": pipeline} if pipeline else {}), }, ensure_ascii=False, indent=2, ) ) if pipeline and pipeline.get("returncode") != 0: return int(pipeline.get("returncode") or 1) return 0 if __name__ == "__main__": raise SystemExit(main())