Files
cadSet/text-to-cad/skills/cad-router/scripts/reconstruct_step.py

903 lines
30 KiB
Python

#!/usr/bin/env python3
"""Execute one uploaded STEP -> DesignIR 3.0 reconstruction task.
The teacher STEP is passed only to deterministic extraction and acceptance
commands. The independent rebuild subprocess receives the DesignIR path and
output path only.
"""
from __future__ import annotations
import argparse
import json
import math
import os
import re
import shutil
import subprocess
import sys
from pathlib import Path
from typing import Any
SCRIPT_PATH = Path(__file__).resolve()
SKILL_ROOT = SCRIPT_PATH.parents[1]
WORKSPACE_ROOT = SKILL_ROOT.parents[2]
SURFACEIR_PIPELINE = (
WORKSPACE_ROOT / "designir-pipeline" / "scripts" / "surfaceir_pipeline.py"
)
EXPERIENCE_LIBRARY = (
WORKSPACE_ROOT / "designir-pipeline" / "output" / "library.json"
)
DESIGNIR_SCHEMA = (
WORKSPACE_ROOT
/ "designir-pipeline"
/ "contracts"
/ "designir-3.0.schema.json"
)
DESIGNIR_SCRIPTS = WORKSPACE_ROOT / "designir-pipeline" / "scripts"
if str(DESIGNIR_SCRIPTS) not in sys.path:
sys.path.insert(0, str(DESIGNIR_SCRIPTS))
from designir_codec import read_designir, write_designir
EXECUTABLE_EDIT_STATES = {
"executable_enlarge_cut_binding",
"executable_axis_affine_binding",
"executable_uniform_scale_binding",
"validated_executable_binding",
}
FORBIDDEN_DESIGNIR_TOKENS = (
'"source_path"',
'"teacher_step"',
"teacher.step",
"import_step",
"stepcontrol_reader",
'"brep"',
'"mesh"',
)
class ReconstructionError(RuntimeError):
"""Raised when a reconstruction stage cannot produce an accepted result."""
def _runtime_python() -> Path:
configured = os.environ.get("CAD_ROUTER_PYTHON", "").strip()
candidates = [
Path(configured).expanduser() if configured else None,
WORKSPACE_ROOT / "text-to-cad" / ".venv" / "bin" / "python",
Path(sys.executable),
]
for candidate in candidates:
if candidate is not None and candidate.is_file():
# Preserve a virtualenv launcher symlink. Resolving it to the base
# interpreter discards the virtualenv's package search path.
return candidate.absolute()
raise ReconstructionError(
"No CAD runtime Python found; set CAD_ROUTER_PYTHON or create "
"text-to-cad/.venv."
)
def _safe_stem(source: Path) -> str:
stem = re.sub(r"[^a-zA-Z0-9_.-]+", "_", source.stem).strip("._")
return stem or "uploaded_part"
def _write_json(path: Path, payload: dict[str, Any]) -> None:
if path.name.endswith(".designir.json"):
write_designir(path, payload)
return
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(
json.dumps(payload, ensure_ascii=False, indent=2) + "\n",
encoding="utf-8",
errors="replace",
)
def _run_json(
command: list[str],
*,
timeout_seconds: int,
stage: str,
) -> dict[str, Any]:
completed = subprocess.run(
command,
capture_output=True,
text=True,
timeout=timeout_seconds,
check=False,
)
if completed.returncode != 0:
detail = completed.stderr.strip() or completed.stdout.strip()
raise ReconstructionError(
f"{stage} failed with exit code {completed.returncode}: "
f"{detail[-2000:]}"
)
lines = [line for line in completed.stdout.splitlines() if line.strip()]
if not lines:
return {}
try:
return json.loads(lines[-1])
except json.JSONDecodeError as exc:
raise ReconstructionError(
f"{stage} returned non-JSON output: {lines[-1][-1000:]}"
) from exc
def _run_simplecad_brep_compare(
runtime: Path,
source: Path,
rebuilt: Path,
report_path: Path,
*,
timeout_seconds: int,
) -> dict[str, Any]:
"""Run the installed SimpleCADAPI comparator in the acceptance zone.
A topology mismatch is a valid diagnostic result, not a worker failure.
The teacher path is provided only to this acceptance subprocess.
"""
command = [
str(runtime),
"-m",
"simplecadapi.inverse_engineer.brep.cli",
"compare",
str(source),
str(rebuilt),
"--output",
str(report_path),
]
try:
completed = subprocess.run(
command,
capture_output=True,
text=True,
timeout=timeout_seconds,
check=False,
)
except subprocess.TimeoutExpired as exc:
return {
"status": "unavailable",
"reason": "timeout",
"error": str(exc),
}
if report_path.is_file():
try:
payload = json.loads(report_path.read_text(encoding="utf-8"))
except (OSError, json.JSONDecodeError) as exc:
return {
"status": "unavailable",
"reason": "invalid_report",
"error": str(exc),
}
payload["status"] = (
"passed" if payload.get("hard_gate_passed") else "mismatch"
)
payload["engine"] = "simplecadapi.inverse_engineer.brep"
return payload
detail = completed.stderr.strip() or completed.stdout.strip()
return {
"status": "unavailable",
"reason": "worker_failed",
"return_code": completed.returncode,
"error": detail[-2000:],
}
def _validate_designir_contract(payload: dict[str, Any]) -> dict[str, Any]:
required = {
"schema_version",
"designir_kind",
"model_id",
"units",
"document_status",
"reconstruction_mode",
"semantic_layer",
"surface_layer",
"edit_interface",
"validation_contract",
}
missing = sorted(required - set(payload))
if missing:
raise ReconstructionError(
f"DesignIR 3.0 is missing required fields: {', '.join(missing)}"
)
if payload["schema_version"] != "3.0":
raise ReconstructionError("Uploaded STEP did not produce DesignIR 3.0")
if payload["designir_kind"] != "independent_parametric_cad":
raise ReconstructionError("DesignIR is not source-independent")
if payload["validation_contract"].get("source_independence") is not True:
raise ReconstructionError("DesignIR source-independence contract is false")
serialized = json.dumps(payload, ensure_ascii=False).lower()
violations = [
token for token in FORBIDDEN_DESIGNIR_TOKENS if token in serialized
]
if violations:
raise ReconstructionError(
"DesignIR anti-cheat scan failed: " + ", ".join(violations)
)
return {
"schema_contract": "designir-3.0",
"schema_path": str(DESIGNIR_SCHEMA),
"required_fields_present": True,
"source_independence_declared": True,
"anti_cheat_pass": True,
"forbidden_token_matches": 0,
}
def _candidate_parameters(
payload: dict[str, Any], maximum: int
) -> list[tuple[str, float]]:
parameters = payload.get("semantic_layer", {}).get("parameters", {})
names = payload.get("edit_interface", {}).get("semantic_parameters", [])
feature_candidates: list[tuple[str, float]] = []
fallback: tuple[str, float] | None = None
for name in names:
parameter = parameters.get(name)
if not isinstance(parameter, dict):
continue
if parameter.get("edit_state") not in EXECUTABLE_EDIT_STATES:
continue
try:
old_value = float(parameter["value"])
except (KeyError, TypeError, ValueError):
continue
candidate = (name, old_value * 1.1)
if name == "overall_scale":
fallback = candidate
else:
feature_candidates.append(candidate)
selected = feature_candidates[: max(0, maximum)]
if fallback is not None:
selected.append(fallback)
return selected
def _surfaceir_experience_ids() -> set[str]:
if not EXPERIENCE_LIBRARY.is_file():
return set()
try:
payload = json.loads(EXPERIENCE_LIBRARY.read_text(encoding="utf-8"))
except (OSError, json.JSONDecodeError):
return set()
policy = payload.get("policy", {})
if (
payload.get("library_kind") != "generalized_cad_experience"
or policy.get("surfaceir_consumable") is not True
or policy.get("instance_parameters_allowed") is not False
):
return set()
return {
item["id"]
for item in payload.get("experiences", [])
if isinstance(item, dict)
and isinstance(item.get("id"), str)
and set(item.get("scope", [])).intersection(
{"surfaceir_reconstruction", "surfaceir_semantic_editing"}
)
}
def _applied_experience_ids(
available: set[str], accepted_parameters: list[dict[str, Any]]
) -> list[str]:
applied = {
experience_id
for experience_id in (
"failure_repair.surface_topology.bind_face_pcurves_and_group_shells",
"failure_repair.surface_topology.preserve_vertex_tolerance",
)
if experience_id in available
}
names = {parameter["name"] for parameter in accepted_parameters}
if any("hole_diameter" in name or "bore_diameter" in name for name in names):
applied.update(
experience_id
for experience_id in (
"edit_strategy.semantic_cylindrical_cut.enlarge_diameter",
"failure_repair.semantic_edit.merge_duplicate_cylindrical_targets",
"edit_strategy.semantic_edit.preserve_validated_geometry_strategy",
)
if experience_id in available
)
if "overall_scale" in names:
experience_id = (
"edit_strategy.semantic_parameter.source_independent_overall_scale"
)
if experience_id in available:
applied.add(experience_id)
if names.intersection(
{"outer_diameter", "body_length", "plate_thickness"}
):
experience_id = (
"edit_strategy.semantic_parameter.primary_axis_affine_dimensions"
)
if experience_id in available:
applied.add(experience_id)
if any(name.startswith("overall_size_") for name in names):
experience_id = (
"edit_strategy.semantic_parameter.canonical_directional_envelope"
)
if experience_id in available:
applied.add(experience_id)
return sorted(applied)
def _persist_edit_results(
designir_path: Path,
attempts: list[dict[str, Any]],
) -> tuple[dict[str, Any], list[dict[str, Any]]]:
payload = read_designir(designir_path)
parameters = payload.get("semantic_layer", {}).get("parameters", {})
accepted_parameters: list[dict[str, Any]] = []
attempted_names = set()
for attempt in attempts:
name = attempt["parameter"]
attempted_names.add(name)
parameter = parameters[name]
old_value = float(parameter["value"])
requested_value = float(attempt["requested_value"])
if attempt["accepted"]:
parameter["editable"] = True
parameter["edit_state"] = "validated_executable_binding"
parameter["validated_range"] = {
"direction": "increase",
"minimum_tested_inclusive": old_value,
"maximum_tested_inclusive": requested_value,
"acceptance_contract": "designir_3_semantic_edit_acceptance",
}
accepted_parameters.append(
{
"name": name,
"semantic_role": parameter.get("semantic_role", name),
"value": parameter["value"],
"unit": parameter.get("unit", "mm"),
"validated_range": parameter["validated_range"],
"confidence": parameter.get("confidence"),
}
)
else:
parameter["editable"] = False
parameter["edit_state"] = "rejected_by_runtime_validation"
parameter.pop("validated_range", None)
payload["edit_interface"]["semantic_parameters"] = sorted(
parameter["name"] for parameter in accepted_parameters
)
payload["reconstruction_strategy"] = {
"boundary_strategy": "exact_3d_pcurve",
"selection_status": "geometry_validated",
}
_write_json(designir_path, payload)
return payload, accepted_parameters
def reconstruct_uploaded_step(
source: Path,
task_dir: Path,
*,
request: str,
maximum_parameters: int = 8,
timeout_seconds: int = 180,
semantic_backend: str = "build123d",
) -> dict[str, Any]:
source = source.expanduser().resolve()
task_dir = task_dir.expanduser().resolve()
if not source.is_file():
raise ReconstructionError(f"Uploaded STEP does not exist: {source}")
if source.suffix.lower() not in {".step", ".stp"}:
raise ReconstructionError("Uploaded reconstruction source must be STEP/STP")
if source == task_dir or task_dir == source.parent:
raise ReconstructionError(
"Task directory must be separate from the uploaded STEP directory"
)
if source == task_dir or task_dir in source.parents:
raise ReconstructionError(
"Task directory cannot contain the uploaded teacher STEP"
)
if not SURFACEIR_PIPELINE.is_file():
raise ReconstructionError(
f"DesignIR 3.0 runtime is missing: {SURFACEIR_PIPELINE}"
)
task_dir.mkdir(parents=True, exist_ok=True)
stem = _safe_stem(source)
designir_path = task_dir / f"{stem}.designir.json"
rebuilt_path = task_dir / f"{stem}.reconstructed.step"
validation_dir = task_dir / "validation"
geometry_report_path = validation_dir / "geometry.json"
brep_report_path = validation_dir / "simplecad-brep.json"
edit_report_path = validation_dir / "edits.json"
parameters_path = task_dir / "parameters.json"
task_manifest_path = task_dir / "cad-task.json"
collisions = [
path
for path in (
designir_path,
rebuilt_path,
parameters_path,
task_manifest_path,
)
if path.exists()
]
if collisions:
raise ReconstructionError(
"Refusing to overwrite existing task artifacts: "
+ ", ".join(str(path) for path in collisions)
)
runtime = _runtime_python()
common = [str(runtime), str(SURFACEIR_PIPELINE)]
available_experience_ids = _surfaceir_experience_ids()
preferred_boundary_strategy = (
"exact_3d_pcurve"
if (
"failure_repair.surface_topology.bind_face_pcurves_and_group_shells"
in available_experience_ids
)
else "exact_3d"
)
extraction = _run_json(
common
+ [
"extract",
str(source),
"--output",
str(designir_path),
],
timeout_seconds=timeout_seconds,
stage="DesignIR 3.0 extraction",
)
payload = read_designir(designir_path)
contract_checks = _validate_designir_contract(payload)
manifest_path = task_dir / "manifest.json"
_write_json(
manifest_path,
{
"schema_version": "1.0",
"input_count": 1,
"rows": [
{
"source_name": source.name,
"output_name": designir_path.name,
"state": "extracted",
}
],
},
)
validation_dir.mkdir(parents=True, exist_ok=True)
strategy_root = validation_dir / ".strategy-work"
strategies = [preferred_boundary_strategy]
if (
"failure_repair.semantic_edit.boundary_strategy_fallback"
in available_experience_ids
):
strategies.extend(
strategy
for strategy in (
"exact_3d",
"first_pcurve",
"analytic_uv_rect",
"uv_rect_all",
)
if strategy not in strategies
)
strategy_attempts: list[dict[str, Any]] = []
selected: tuple[
tuple[float, ...],
str,
dict[str, Any],
dict[str, Any],
Path,
] | None = None
for strategy in strategies:
attempt_dir = strategy_root / strategy
try:
candidate_result = _run_json(
common
+ [
"validate-folder",
str(source.parent),
"--designir-dir",
str(task_dir),
"--rebuilt-dir",
str(attempt_dir),
"--limit",
"1",
"--boolean",
"--boundary-strategy",
strategy,
],
timeout_seconds=timeout_seconds,
stage=f"Acceptance-Agent geometry validation ({strategy})",
)
report = json.loads(
(attempt_dir / "validation-report.json").read_text(
encoding="utf-8"
)
)
row = (report.get("rows") or [{}])[0]
candidate_steps = sorted(attempt_dir.glob("*.step"))
if not candidate_steps:
raise ReconstructionError(
f"Boundary strategy {strategy} produced no rebuilt STEP"
)
def finite_metric(name: str) -> float:
value = row.get(name)
if isinstance(value, (int, float)) and math.isfinite(value):
return float(value)
return math.inf
score = (
0.0
if row.get("geometry_pass")
else 1.0
if row.get("usable_reconstruction")
else 2.0,
0.0 if row.get("topology_match") else 1.0,
finite_metric("relative_volume_error"),
finite_metric("relative_surface_area_error"),
finite_metric("center_of_mass_error"),
finite_metric("bounds_error"),
finite_metric("bidirectional_sample_distance"),
)
strategy_attempts.append(
{
"strategy": strategy,
"status": "evaluated",
"score": list(score),
"geometry_pass": bool(row.get("geometry_pass")),
"usable_reconstruction": bool(
row.get("usable_reconstruction")
),
"topology_match": bool(row.get("topology_match")),
"relative_volume_error": row.get("relative_volume_error"),
"relative_surface_area_error": row.get(
"relative_surface_area_error"
),
"center_of_mass_error": row.get("center_of_mass_error"),
"bounds_error": row.get("bounds_error"),
"bidirectional_sample_distance": row.get(
"bidirectional_sample_distance"
),
}
)
candidate = (
score,
strategy,
report,
candidate_result,
candidate_steps[0],
)
if selected is None or candidate[0] < selected[0]:
selected = candidate
except (ReconstructionError, subprocess.TimeoutExpired) as exc:
strategy_attempts.append(
{
"strategy": strategy,
"status": "worker_failed",
"error": str(exc),
}
)
strategy_report_path = validation_dir / "strategies.json"
if selected is None:
_write_json(
strategy_report_path,
{
"schema_version": "1.0",
"selected_strategy": None,
"attempts": strategy_attempts,
},
)
raise ReconstructionError("Every SurfaceIR boundary strategy failed")
_, boundary_strategy, geometry_report, geometry_result, selected_step = selected
shutil.copy2(selected_step, rebuilt_path)
_write_json(
strategy_report_path,
{
"schema_version": "1.0",
"selected_strategy": boundary_strategy,
"attempts": strategy_attempts,
},
)
_write_json(geometry_report_path, geometry_report)
geometry_pass = geometry_report.get("geometry_pass_count") == 1
usable_reconstruction = (
geometry_report.get("usable_reconstruction_count") == 1
)
if not geometry_pass and not usable_reconstruction:
raise ReconstructionError(
"Independent reconstruction did not satisfy the geometry contract; "
f"see {geometry_report_path}"
)
brep_comparison = _run_simplecad_brep_compare(
runtime,
source,
rebuilt_path,
brep_report_path,
timeout_seconds=timeout_seconds,
)
shutil.rmtree(strategy_root)
attempts: list[dict[str, Any]] = []
if payload["document_status"] == "geometry_present":
for name, requested_value in _candidate_parameters(
payload, maximum_parameters
):
safe_name = _safe_stem(Path(name))
attempt_dir = validation_dir / "parameter-tests" / safe_name
try:
result = _run_json(
common
+ [
"validate-edit",
str(source),
str(designir_path),
"--parameter",
name,
"--value",
str(requested_value),
"--output-dir",
str(attempt_dir),
],
timeout_seconds=timeout_seconds,
stage=f"parameter validation ({name})",
)
report = json.loads(
(attempt_dir / "acceptance-report.json").read_text(
encoding="utf-8"
)
)
attempts.append(
{
"parameter": name,
"requested_value": requested_value,
"accepted": bool(report.get("accepted")),
"state": report.get("state"),
"report": str(
attempt_dir / "acceptance-report.json"
),
"worker": result,
}
)
except (ReconstructionError, subprocess.TimeoutExpired) as exc:
attempts.append(
{
"parameter": name,
"requested_value": requested_value,
"accepted": False,
"state": "worker_failed",
"error": str(exc),
}
)
payload, accepted_parameters = _persist_edit_results(
designir_path, attempts
)
payload["reconstruction_strategy"] = {
"boundary_strategy": boundary_strategy,
"selection_status": (
"geometry_validated" if geometry_pass else "unresolved"
),
}
_write_json(designir_path, payload)
applied_experience_ids = _applied_experience_ids(
available_experience_ids, accepted_parameters
)
edit_report = {
"schema_version": "1.0",
"report_kind": "cad_router_runtime_parameter_acceptance",
"attempt_count": len(attempts),
"accepted_count": sum(row["accepted"] for row in attempts),
"attempts": attempts,
}
_write_json(edit_report_path, edit_report)
parameter_catalog = {
"schema_version": "1.0",
"model_id": payload["model_id"],
"document_status": payload["document_status"],
"parameter_count": len(accepted_parameters),
"parameters": accepted_parameters,
"policy": "Only parameters that passed an isolated perturbation test are exposed.",
}
_write_json(parameters_path, parameter_catalog)
brep_hard_gate_passed = brep_comparison.get("hard_gate_passed") is True
brep_available = brep_comparison.get("status") in {"passed", "mismatch"}
quality_tier = (
"brep_exact"
if geometry_pass and brep_hard_gate_passed
else "geometry_summary_accepted_brep_mismatch"
if geometry_pass and brep_available
else "geometry_accepted_brep_unavailable"
if geometry_pass
else "review_required"
)
published_artifacts = [
{"path": rebuilt_path.name, "role": "primary_step"},
{"path": designir_path.name, "role": "editable_designir"},
{"path": parameters_path.name, "role": "validated_parameters"},
{
"path": str(geometry_report_path.relative_to(task_dir)),
"role": "geometry_acceptance",
},
{
"path": str(edit_report_path.relative_to(task_dir)),
"role": "parameter_acceptance",
},
{
"path": str(strategy_report_path.relative_to(task_dir)),
"role": "boundary_strategy_acceptance",
},
]
if brep_report_path.is_file():
published_artifacts.append(
{
"path": str(brep_report_path.relative_to(task_dir)),
"role": "strict_brep_acceptance",
}
)
task_manifest = {
"schema_version": "1.0",
"request": request,
"operation": "uploaded_step_reconstruction",
"route": {
"router": "cad-router",
"runtime": "DesignIR 3.0 / SurfaceIR",
"selected_backend": semantic_backend,
"execution_adapter": "surfaceir_occt",
"validation_adapters": [
"surfaceir_geometry_acceptance",
"simplecadapi_brep_compare",
],
"source_of_truth": designir_path.name,
},
"source": {
"format": source.suffix.lower().lstrip("."),
"role": "teacher_and_acceptance_truth_only",
"embedded_in_output": False,
},
"artifacts": published_artifacts,
"experience": {
"library": (
str(EXPERIENCE_LIBRARY)
if EXPERIENCE_LIBRARY.is_file()
else None
),
"runtime_policy": "promoted_generalized_experience_only",
"available_method_count": len(available_experience_ids),
"methods_applied": applied_experience_ids,
},
"validation": {
"geometry_accepted": geometry_pass,
"geometry_usable": usable_reconstruction,
"brep_comparison_status": brep_comparison.get("status"),
"brep_hard_gate_passed": brep_hard_gate_passed,
"quality_tier": quality_tier,
"source_independent_rebuild": True,
"anti_cheat_pass": True,
"validated_parameter_count": len(accepted_parameters),
},
"edit_command_template": [
str(runtime),
str(SURFACEIR_PIPELINE),
"edit",
designir_path.name,
"--parameter",
"<validated-parameter-name>",
"--value",
"<value-within-validated-range>",
"--output-designir",
"<edited.designir.json>",
"--output-step",
"<edited.step>",
],
}
_write_json(task_manifest_path, task_manifest)
manifest_path.unlink(missing_ok=True)
return {
"schema_version": "1.0",
"status": (
(
"accepted"
if geometry_pass
and (
brep_hard_gate_passed
or brep_comparison.get("status") == "unavailable"
)
else "review_required"
)
if payload["document_status"] == "geometry_present"
else "accepted_empty_source"
),
"task_dir": str(task_dir),
"designir": str(designir_path),
"rebuilt_step": str(rebuilt_path),
"parameters": str(parameters_path),
"task_manifest": str(task_manifest_path),
"geometry_report": str(geometry_report_path),
"edit_report": str(edit_report_path),
"geometry_accepted": geometry_pass,
"geometry_usable": usable_reconstruction,
"brep_comparison": brep_comparison,
"brep_report": (
str(brep_report_path) if brep_report_path.is_file() else None
),
"quality_tier": quality_tier,
"document_status": payload["document_status"],
"validated_parameter_count": len(accepted_parameters),
"validated_parameter_names": [
parameter["name"] for parameter in accepted_parameters
],
"experience": {
"available_method_count": len(available_experience_ids),
"methods_applied": applied_experience_ids,
},
"source_independence": {
"rebuild_command_received_teacher_path": False,
**contract_checks,
},
"stages": {
"extraction": extraction,
"geometry_validation": geometry_result,
"brep_validation": {
"engine": "SimpleCADAPI 2.0.2",
"status": brep_comparison.get("status"),
},
},
}
def build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--source", type=Path, required=True)
parser.add_argument("--task-dir", type=Path, required=True)
parser.add_argument("--request", default="重建上传的 STEP")
parser.add_argument("--maximum-parameters", type=int, default=8)
parser.add_argument("--timeout-seconds", type=int, default=180)
parser.add_argument(
"--semantic-backend",
choices=("build123d", "simplecadapi"),
default="build123d",
help="Preferred editable semantic backend after SurfaceIR reconstruction.",
)
return parser
def main(argv: list[str] | None = None) -> int:
args = build_parser().parse_args(argv)
try:
result = reconstruct_uploaded_step(
args.source,
args.task_dir,
request=args.request,
maximum_parameters=args.maximum_parameters,
timeout_seconds=args.timeout_seconds,
semantic_backend=args.semantic_backend,
)
except (ReconstructionError, subprocess.TimeoutExpired) as exc:
print(
json.dumps(
{"status": "failed", "error": str(exc)},
ensure_ascii=False,
),
file=sys.stderr,
)
return 1
print(json.dumps(result, ensure_ascii=False))
return 0
if __name__ == "__main__":
raise SystemExit(main())