Files
llm/scripts/watch_1c_moxel_property_experiment.py
T
2026-08-14 09:40:51 +03:00

266 lines
9.7 KiB
Python

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