266 lines
9.7 KiB
Python
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-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())
|