609 lines
29 KiB
Python
609 lines
29 KiB
Python
from __future__ import annotations
|
|
|
|
import copy
|
|
import json
|
|
import math
|
|
import re
|
|
import shutil
|
|
import sys
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
from jsonschema import Draft202012Validator
|
|
from jsonschema.exceptions import SchemaError
|
|
|
|
from vendor.cdsl_preview_runtime import step_to_glb
|
|
from app.services.quality import evaluate_quality, validate_verification
|
|
from app.services.storage import WorkspaceStore, now_iso, write_json
|
|
from app.settings import Settings
|
|
|
|
|
|
class QualityVerificationError(RuntimeError):
|
|
"""A built CDSL document missed a blocking generic verification rule."""
|
|
|
|
def __init__(self, quality_report: dict[str, Any]) -> None:
|
|
super().__init__("Blocking CDSL verification checks failed")
|
|
self.quality_report = quality_report
|
|
self.task_id = ""
|
|
self.revision_id = ""
|
|
|
|
|
|
def load_engine(settings: Settings) -> Any:
|
|
parent = str(settings.engine_root.parent)
|
|
if parent not in sys.path:
|
|
sys.path.insert(0, parent)
|
|
import cdsl_engine
|
|
|
|
return cdsl_engine
|
|
|
|
|
|
def _walk(value: Any) -> list[tuple[str, Any]]:
|
|
result: list[tuple[str, Any]] = []
|
|
if isinstance(value, dict):
|
|
for key, child in value.items():
|
|
result.append((str(key), child))
|
|
result.extend(_walk(child))
|
|
elif isinstance(value, list):
|
|
for child in value:
|
|
result.extend(_walk(child))
|
|
return result
|
|
|
|
|
|
def _engine_schema(engine: Any) -> dict[str, Any]:
|
|
schema_path = Path(str(engine.__file__)).with_name("profile_schema.json")
|
|
try:
|
|
schema = json.loads(schema_path.read_text(encoding="utf-8"))
|
|
except (OSError, json.JSONDecodeError) as error:
|
|
raise RuntimeError("The local engine schema document is unavailable or invalid") from error
|
|
if not isinstance(schema, dict) or not isinstance(schema.get("feature_atomic_ids"), dict):
|
|
raise RuntimeError("The local engine schema has no feature_atomic_ids contract")
|
|
return schema
|
|
|
|
|
|
def load_cdsl_json_schema(engine: Any) -> dict[str, Any]:
|
|
document = _engine_schema(engine)
|
|
schema_name = str(document.get("cdsl_json_schema_file") or "")
|
|
if not schema_name or Path(schema_name).name != schema_name:
|
|
raise RuntimeError("The local engine schema has an invalid CDSL JSON Schema path")
|
|
schema_path = Path(str(engine.__file__)).with_name(schema_name)
|
|
try:
|
|
schema = json.loads(schema_path.read_text(encoding="utf-8"))
|
|
Draft202012Validator.check_schema(schema)
|
|
except (OSError, json.JSONDecodeError, SchemaError) as error:
|
|
raise RuntimeError("The local CDSL JSON Schema is unavailable or invalid") from error
|
|
return schema
|
|
|
|
|
|
def _validate_cdsl_json_schema(cdsl: dict[str, Any], engine: Any) -> None:
|
|
validator = Draft202012Validator(load_cdsl_json_schema(engine))
|
|
errors = sorted(validator.iter_errors(cdsl), key=lambda error: (list(error.absolute_path), error.message))
|
|
if not errors:
|
|
return
|
|
error = errors[0]
|
|
location = "$" + "".join(
|
|
f"[{item}]" if isinstance(item, int) else f".{item}"
|
|
for item in error.absolute_path
|
|
)
|
|
raise ValueError(f"CDSL schema violation at {location}: {error.message}")
|
|
|
|
|
|
def _legacy_workplane(plane: str, offset: Any) -> dict[str, list[float]] | None:
|
|
if isinstance(offset, bool) or not isinstance(offset, (int, float)):
|
|
return None
|
|
distance = float(offset)
|
|
definitions = {
|
|
"XY": ([0.0, 0.0, distance], [1.0, 0.0, 0.0], [0.0, 0.0, 1.0]),
|
|
"XZ": ([0.0, distance, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]),
|
|
"YZ": ([distance, 0.0, 0.0], [0.0, 1.0, 0.0], [1.0, 0.0, 0.0]),
|
|
}
|
|
definition = definitions.get(plane.upper())
|
|
if definition is None:
|
|
return None
|
|
origin, x_dir, normal = definition
|
|
return {"origin_mm": origin, "x_dir": x_dir, "normal": normal}
|
|
|
|
|
|
def normalize_cdsl_for_engine(cdsl: dict[str, Any]) -> tuple[dict[str, Any], list[str]]:
|
|
"""Convert unambiguous legacy LLM aliases into the current CDSL dialect.
|
|
|
|
This intentionally does not infer dimensions, selectors, or feature
|
|
dependencies. Any non-mechanical error remains visible to the validator.
|
|
"""
|
|
normalized = copy.deepcopy(cdsl)
|
|
repairs: list[str] = []
|
|
geometry = normalized.get("geometry")
|
|
sketches = geometry.get("sketches") if isinstance(geometry, dict) else None
|
|
if isinstance(sketches, list):
|
|
for index, sketch in enumerate(sketches):
|
|
if not isinstance(sketch, dict):
|
|
continue
|
|
if "id" not in sketch and isinstance(sketch.get("sketch_id"), str):
|
|
sketch["id"] = sketch.pop("sketch_id")
|
|
repairs.append(f"geometry.sketches[{index}]: sketch_id -> id")
|
|
|
|
legacy_plane: Any = sketch.get("plane")
|
|
legacy_offset: Any = sketch.get("offset_mm", 0)
|
|
workplane_value = sketch.get("workplane")
|
|
if isinstance(workplane_value, dict) and "origin_mm" not in workplane_value:
|
|
legacy_plane = workplane_value.get("plane")
|
|
legacy_offset = workplane_value.get("offset_mm", 0)
|
|
elif "workplane" in sketch:
|
|
continue
|
|
|
|
workplane = _legacy_workplane(legacy_plane, legacy_offset) if isinstance(legacy_plane, str) else None
|
|
if workplane is None:
|
|
continue
|
|
sketch["workplane"] = workplane
|
|
sketch.pop("plane", None)
|
|
sketch.pop("offset_mm", None)
|
|
repairs.append(f"geometry.sketches[{index}]: legacy plane/offset_mm -> workplane")
|
|
|
|
features = normalized.get("features")
|
|
if isinstance(features, list):
|
|
for index, feature in enumerate(features):
|
|
if not isinstance(feature, dict):
|
|
continue
|
|
if "sketch_id" not in feature and isinstance(feature.get("sketch"), str):
|
|
feature["sketch_id"] = feature.pop("sketch")
|
|
repairs.append(f"features[{index}]: sketch -> sketch_id")
|
|
if "depends_on" not in feature:
|
|
feature["depends_on"] = []
|
|
repairs.append(f"features[{index}]: added empty depends_on")
|
|
params = feature.get("params")
|
|
axis = params.get("axis") if isinstance(params, dict) else None
|
|
if isinstance(axis, dict) and "origin_mm" not in axis and "point_mm" in axis:
|
|
axis["origin_mm"] = axis.pop("point_mm")
|
|
repairs.append(f"features[{index}].params.axis: point_mm -> origin_mm")
|
|
return normalized, repairs
|
|
|
|
|
|
def validate_cdsl(cdsl: dict[str, Any], engine: Any) -> None:
|
|
if not isinstance(cdsl, dict):
|
|
raise ValueError("CDSL must be a JSON object")
|
|
if cdsl.get("schema") != "cad.cdsl.llm.v1":
|
|
raise ValueError("Unsupported CDSL schema")
|
|
_validate_cdsl_json_schema(cdsl, engine)
|
|
part_id = str(cdsl.get("part_id") or "")
|
|
if not re.fullmatch(r"[a-zA-Z0-9_-]{3,80}", part_id):
|
|
raise ValueError("part_id must use letters, numbers, underscores, or hyphens")
|
|
forbidden = {"compiler_context", "unknown_shape", "complex_arc_shape", "contour_edges_mm", "contour_regions_mm", "entities"}
|
|
for key, value in _walk(cdsl):
|
|
if key in forbidden or (isinstance(value, str) and value in {"unknown_shape", "complex_arc_shape"}):
|
|
raise ValueError(f"Training-unsafe CDSL field: {key}")
|
|
features = cdsl.get("features")
|
|
sketches = cdsl.get("geometry", {}).get("sketches")
|
|
if not isinstance(features, list) or not features or not isinstance(sketches, list) or not sketches:
|
|
raise ValueError("CDSL requires features and parameterized sketches")
|
|
sketch_ids = {str(sketch.get("id")) for sketch in sketches}
|
|
semantic_contract = _engine_schema(engine)
|
|
atomic_contracts = semantic_contract["feature_atomic_ids"]
|
|
declared_atomic_ids = {
|
|
str(atomic_id)
|
|
for atomic_id in semantic_contract.get("runtime_supported_atomic_ids", atomic_contracts)
|
|
}
|
|
registered_atomic_ids = {str(atomic_id) for atomic_id in getattr(engine, "SUPPORTED_ATOMIC_IDS", ())}
|
|
supported_atomic_ids = sorted(declared_atomic_ids & registered_atomic_ids)
|
|
feature_ids: set[str] = set()
|
|
for feature in features:
|
|
fid = str(feature.get("id") or "")
|
|
if not fid or fid in feature_ids:
|
|
raise ValueError("Feature ids must be unique")
|
|
feature_ids.add(fid)
|
|
atomic_id = str(feature.get("atomic_id") or "")
|
|
if not atomic_id:
|
|
raise ValueError(f"Feature {fid} has no atomic_id")
|
|
contract = atomic_contracts.get(atomic_id)
|
|
if atomic_id not in supported_atomic_ids or not isinstance(contract, dict):
|
|
if feature.get("execution_status") == "deferred":
|
|
raise ValueError(f"Feature {fid} is deferred and cannot be rebuilt by the current engine")
|
|
raise ValueError(
|
|
f"Unsupported CDSL atomic_id: {atomic_id}. Supported: {', '.join(supported_atomic_ids)}"
|
|
)
|
|
params = feature.get("params")
|
|
if not isinstance(params, dict):
|
|
raise ValueError(f"Feature {fid} params must be an object")
|
|
for parameter_name in contract.get("required_params") or []:
|
|
if params.get(parameter_name) is None:
|
|
raise ValueError(f"Feature {fid} ({atomic_id}) is missing required parameter: {parameter_name}")
|
|
if atomic_id.startswith("extrude_") and float(params.get("distance_mm") or 0) <= 0:
|
|
raise ValueError(f"Feature {fid} ({atomic_id}) requires distance_mm > 0 for runtime rebuild")
|
|
if atomic_id.startswith("revolve_") and float(params.get("angle_deg") or 0) <= 0:
|
|
raise ValueError(f"Feature {fid} ({atomic_id}) requires angle_deg > 0 for runtime rebuild")
|
|
if contract.get("requires_sketch") and str(feature.get("sketch_id") or "") not in sketch_ids:
|
|
raise ValueError(f"Feature {fid} ({atomic_id}) requires a valid sketch_id")
|
|
for dependency in feature.get("depends_on") or []:
|
|
if dependency not in feature_ids:
|
|
raise ValueError(f"Feature {fid} has a forward or missing dependency")
|
|
for sketch in sketches:
|
|
profile = sketch.get("profile")
|
|
if sketch.get("profile_from"):
|
|
continue
|
|
if not isinstance(profile, dict):
|
|
raise ValueError(f"Sketch {sketch.get('id')} has no self-contained profile")
|
|
profile_type = str(profile.get("type") or "")
|
|
if profile_type == "polygon":
|
|
if not profile.get("vertices"):
|
|
raise ValueError("Polygon profiles require vertices")
|
|
elif profile_type not in engine.SHAPE_GENERATORS:
|
|
raise ValueError(f"Unsupported CDSL profile: {profile_type}")
|
|
try:
|
|
analysis = engine.analyze_cdsl(copy.deepcopy(cdsl))
|
|
except Exception as error:
|
|
raise ValueError(f"CDSL engine runtime preflight failed: {error}") from error
|
|
if not analysis.runtime_eligible:
|
|
first = next((result for result in analysis.feature_results if not result.executable), None)
|
|
if first is None:
|
|
raise ValueError(f"CDSL engine runtime preflight failed: {analysis.document_blockers[0].code}")
|
|
blockers = ", ".join(blocker.code for blocker in first.blockers)
|
|
raise ValueError(f"CDSL engine runtime preflight failed: feature {first.feature_id}: {blockers}")
|
|
|
|
|
|
def _parameter_id(path: list[str]) -> str:
|
|
return "param_" + "_".join(re.sub(r"[^a-zA-Z0-9]+", "_", item).strip("_") for item in path)
|
|
|
|
|
|
def _parameter_label(path: list[str]) -> str:
|
|
return " / ".join(path[-2:]).replace("_mm", " (mm)").replace("_", " ")
|
|
|
|
|
|
def _derived_parameters(cdsl: dict[str, Any]) -> list[dict[str, Any]]:
|
|
parameters: list[dict[str, Any]] = []
|
|
|
|
def add(path: list[str], value: Any, group: str) -> None:
|
|
if isinstance(value, bool) or not isinstance(value, (int, float)) or not math.isfinite(float(value)):
|
|
return
|
|
number = float(value)
|
|
magnitude = max(abs(number), 1.0)
|
|
parameters.append({
|
|
"id": _parameter_id(path),
|
|
"name": ".".join(path),
|
|
"display_name": _parameter_label(path),
|
|
"path": path,
|
|
"value": number,
|
|
"default_value": number,
|
|
"minimum": 0.01 if number >= 0 else -magnitude * 10,
|
|
"maximum": magnitude * 10,
|
|
"step": 0.1 if abs(number) < 100 else 1.0,
|
|
"precision": 2,
|
|
"unit": "mm" if path[-1].endswith("_mm") else "",
|
|
"group": group,
|
|
"editable": True,
|
|
})
|
|
|
|
for feature_index, feature in enumerate(cdsl.get("features") or []):
|
|
for key, value in (feature.get("params") or {}).items():
|
|
add(["features", str(feature_index), "params", str(key)], value, "Features")
|
|
for sketch_index, sketch in enumerate(cdsl.get("geometry", {}).get("sketches") or []):
|
|
profile = sketch.get("profile") or {}
|
|
|
|
def walk_profile(value: Any, path: list[str]) -> None:
|
|
if isinstance(value, dict):
|
|
for key, child in value.items():
|
|
walk_profile(child, [*path, str(key)])
|
|
elif isinstance(value, list):
|
|
# Coordinates are topology anchors, not user-facing dimensions.
|
|
return
|
|
else:
|
|
add(path, value, "Sketches")
|
|
|
|
walk_profile(profile, ["geometry", "sketches", str(sketch_index), "profile"])
|
|
return parameters
|
|
|
|
|
|
def parameter_contract(cdsl: dict[str, Any]) -> dict[str, Any]:
|
|
declared = cdsl.get("meta", {}).get("editable_parameters")
|
|
if isinstance(declared, list) and declared:
|
|
values = [item for item in declared if isinstance(item, dict) and isinstance(item.get("path"), list)]
|
|
if values:
|
|
return {"schema_version": "1.0", "parameters": values, "source": "declared"}
|
|
return {"schema_version": "1.0", "parameters": _derived_parameters(cdsl), "source": "derived"}
|
|
|
|
|
|
def topology_sidecars(engine_result: dict[str, Any], preview: dict[str, Any] | None = None) -> tuple[dict[str, Any], dict[str, Any]]:
|
|
topology_faces = (preview or {}).get("topology_faces")
|
|
if isinstance(topology_faces, list) and topology_faces:
|
|
references = []
|
|
for face in topology_faces:
|
|
if not isinstance(face, dict):
|
|
continue
|
|
frame = face.get("frame")
|
|
center = face.get("center")
|
|
normal = face.get("normal")
|
|
if not isinstance(frame, dict) or not isinstance(center, list) or not isinstance(normal, list):
|
|
continue
|
|
references.append({
|
|
"id": str(face.get("id") or f"face_{len(references):03d}"),
|
|
"selectorType": "face",
|
|
"label": str(face.get("surface_type") or "face"),
|
|
"center": center,
|
|
"normal": normal,
|
|
"frame": frame,
|
|
"bbox": face.get("bbox") or {},
|
|
"surface_type": str(face.get("surface_type") or "unknown"),
|
|
"triangle_start": int(face.get("triangle_start") or 0),
|
|
"triangle_count": int(face.get("triangle_count") or 0),
|
|
})
|
|
if references:
|
|
return ({"schema_version": "1.1", "references": references}, {"schema_version": "1.0", "edges": []})
|
|
|
|
bbox = engine_result.get("bbox_mm") or {}
|
|
minimum = [float(value) for value in bbox.get("min") or [0, 0, 0]]
|
|
maximum = [float(value) for value in bbox.get("max") or [0, 0, 0]]
|
|
if len(minimum) != 3 or len(maximum) != 3:
|
|
raise ValueError("Engine result is missing a valid bounding box")
|
|
center = [(minimum[index] + maximum[index]) / 2 for index in range(3)]
|
|
definitions = [
|
|
("top", [center[0], center[1], maximum[2]], [0, 0, 1], [1, 0, 0], [0, 1, 0]),
|
|
("bottom", [center[0], center[1], minimum[2]], [0, 0, -1], [1, 0, 0], [0, -1, 0]),
|
|
("right", [maximum[0], center[1], center[2]], [1, 0, 0], [0, 1, 0], [0, 0, 1]),
|
|
("left", [minimum[0], center[1], center[2]], [-1, 0, 0], [0, 1, 0], [0, 0, -1]),
|
|
("front", [center[0], maximum[1], center[2]], [0, 1, 0], [1, 0, 0], [0, 0, -1]),
|
|
("back", [center[0], minimum[1], center[2]], [0, -1, 0], [1, 0, 0], [0, 0, 1]),
|
|
]
|
|
references = [
|
|
{
|
|
"id": f"face_{name}", "selectorType": "face", "label": name,
|
|
"center": point, "normal": normal,
|
|
"frame": {"origin_mm": point, "normal": normal, "x_dir": x_dir, "y_dir": y_dir},
|
|
"bbox": {"min": minimum, "max": maximum},
|
|
}
|
|
for name, point, normal, x_dir, y_dir in definitions
|
|
]
|
|
return ({"schema_version": "1.0", "references": references}, {"schema_version": "1.0", "edges": []})
|
|
|
|
|
|
def _set_parameter_value(document: dict[str, Any], path: list[str], value: float) -> None:
|
|
target: Any = document
|
|
for index, key in enumerate(path):
|
|
final = index == len(path) - 1
|
|
if isinstance(target, list):
|
|
item_index = int(key)
|
|
if item_index < 0 or item_index >= len(target):
|
|
raise ValueError("Parameter path is no longer valid")
|
|
if final:
|
|
target[item_index] = value
|
|
else:
|
|
target = target[item_index]
|
|
elif isinstance(target, dict):
|
|
if key not in target:
|
|
raise ValueError("Parameter path is no longer valid")
|
|
if final:
|
|
target[key] = value
|
|
else:
|
|
target = target[key]
|
|
else:
|
|
raise ValueError("Parameter path is no longer valid")
|
|
|
|
|
|
def apply_parameter_updates(cdsl: dict[str, Any], values: dict[str, float]) -> tuple[dict[str, Any], dict[str, Any]]:
|
|
contract = parameter_contract(cdsl)
|
|
entries = {str(item.get("id")): item for item in contract["parameters"]}
|
|
updated = copy.deepcopy(cdsl)
|
|
for parameter_id, raw_value in values.items():
|
|
entry = entries.get(parameter_id)
|
|
value = float(raw_value)
|
|
if entry is None or not entry.get("editable", False):
|
|
raise ValueError(f"Unknown editable parameter: {parameter_id}")
|
|
if not math.isfinite(value):
|
|
raise ValueError("Parameter values must be finite")
|
|
minimum, maximum = entry.get("minimum"), entry.get("maximum")
|
|
if isinstance(minimum, (int, float)) and value < float(minimum):
|
|
raise ValueError(f"{parameter_id} is below its minimum")
|
|
if isinstance(maximum, (int, float)) and value > float(maximum):
|
|
raise ValueError(f"{parameter_id} is above its maximum")
|
|
path = entry.get("path")
|
|
if not isinstance(path, list) or not all(isinstance(item, str) for item in path):
|
|
raise ValueError(f"{parameter_id} has an invalid path")
|
|
_set_parameter_value(updated, path, value)
|
|
declared = updated.get("meta", {}).get("editable_parameters")
|
|
if isinstance(declared, list):
|
|
for declared_entry in declared:
|
|
if isinstance(declared_entry, dict) and str(declared_entry.get("id")) == parameter_id:
|
|
declared_entry["value"] = value
|
|
return updated, parameter_contract(updated)
|
|
|
|
|
|
def _part_skill_audit(
|
|
part_skills: dict[str, Any] | None,
|
|
request: str,
|
|
generation_assumptions: list[str] | None,
|
|
) -> dict[str, Any]:
|
|
"""Normalize the planning audit persisted beside a product revision."""
|
|
audit = copy.deepcopy(part_skills) if isinstance(part_skills, dict) else {}
|
|
skills = [item for item in audit.get("skills") or [] if isinstance(item, dict)]
|
|
skill_ids = [str(item) for item in audit.get("skill_ids") or [] if str(item)]
|
|
if not skill_ids:
|
|
skill_ids = [str(item.get("id")) for item in skills if item.get("id")]
|
|
audit.update({
|
|
"schema_version": str(audit.get("schema_version") or "1.0"),
|
|
"request": str(audit.get("request") or request),
|
|
"structural_intent": str(audit.get("structural_intent") or request),
|
|
"skill_ids": skill_ids,
|
|
"skills": skills,
|
|
"inherited_skill_ids": [str(item) for item in audit.get("inherited_skill_ids") or [] if str(item)],
|
|
"assumptions": [str(item) for item in generation_assumptions or []],
|
|
})
|
|
return audit
|
|
|
|
|
|
def _generation_context(
|
|
reference_ids: list[str],
|
|
part_skill_audit: dict[str, Any],
|
|
) -> dict[str, Any]:
|
|
skills = [item for item in part_skill_audit.get("skills") or [] if isinstance(item, dict)]
|
|
return {
|
|
"cdsl_reference_ids": list(reference_ids),
|
|
"part_skill_ids": list(part_skill_audit.get("skill_ids") or []),
|
|
"part_skill_paths": [
|
|
{
|
|
"id": str(item.get("id") or ""),
|
|
"bridge": str(item.get("bridge") or ""),
|
|
"source": str(item.get("source") or ""),
|
|
}
|
|
for item in skills
|
|
],
|
|
"generation_assumptions": list(part_skill_audit.get("assumptions") or []),
|
|
}
|
|
|
|
|
|
def build_revision(
|
|
*,
|
|
settings: Settings,
|
|
store: WorkspaceStore,
|
|
task_id: str | None,
|
|
request: str,
|
|
cdsl: dict[str, Any],
|
|
reference_ids: list[str],
|
|
summary: str,
|
|
parent_revision_id: str | None = None,
|
|
operation: dict[str, Any] | None = None,
|
|
input_attachments: list[dict[str, Any]] | None = None,
|
|
part_skills: dict[str, Any] | None = None,
|
|
generation_assumptions: list[str] | None = None,
|
|
repair_attempts: int = 0,
|
|
verification: dict[str, Any] | None = None,
|
|
reference_records: list[dict[str, Any]] | None = None,
|
|
) -> dict[str, Any]:
|
|
engine = load_engine(settings)
|
|
task = store.ensure_task(task_id, request)
|
|
revision_id, revision_dir = store.next_revision(task["task_id"])
|
|
cdsl_path = revision_dir / "model.cdsl.json"
|
|
step_path = revision_dir / "model.step"
|
|
glb_path = revision_dir / "model.glb"
|
|
report_path = revision_dir / "rebuild-report.json"
|
|
request_path = revision_dir / "request.json"
|
|
references_path = revision_dir / "references.json"
|
|
parameters_path = revision_dir / "parameters.json"
|
|
selector_path = revision_dir / "model.selector.json"
|
|
edges_path = revision_dir / "model.edges.json"
|
|
part_skills_path = revision_dir / "part-skills.json"
|
|
quality_path = revision_dir / "quality-report.json"
|
|
snapshot_manifest_path = revision_dir / "snapshot-manifest.json"
|
|
part_skill_audit = _part_skill_audit(part_skills, request, generation_assumptions)
|
|
generation_context = _generation_context(reference_ids, part_skill_audit)
|
|
write_json(request_path, {"request": request, "created_at": now_iso()})
|
|
write_json(references_path, {"reference_ids": reference_ids, "records": [item for item in reference_records or [] if isinstance(item, dict)]})
|
|
write_json(part_skills_path, part_skill_audit)
|
|
write_json(snapshot_manifest_path, {
|
|
"schema_version": "1.0",
|
|
"status": "unavailable",
|
|
"reason": "Snapshot runner is not attached to this backend build",
|
|
"snapshots": [],
|
|
})
|
|
def revision_record(status: str, *, error: str = "", engine_name: str = "") -> dict[str, Any]:
|
|
record = {
|
|
"revision_id": revision_id,
|
|
"status": status,
|
|
"created_at": now_iso(),
|
|
"request_path": request_path.relative_to(store.task_dir(task["task_id"])).as_posix(),
|
|
"cdsl_path": cdsl_path.relative_to(store.task_dir(task["task_id"])).as_posix(),
|
|
"report_path": report_path.relative_to(store.task_dir(task["task_id"])).as_posix(),
|
|
"parameters_path": parameters_path.relative_to(store.task_dir(task["task_id"])).as_posix(),
|
|
"part_skills_path": part_skills_path.relative_to(store.task_dir(task["task_id"])).as_posix(),
|
|
"quality_path": quality_path.relative_to(store.task_dir(task["task_id"])).as_posix(),
|
|
"snapshot_manifest_path": snapshot_manifest_path.relative_to(store.task_dir(task["task_id"])).as_posix(),
|
|
"snapshot_status": "unavailable",
|
|
"snapshot_paths": [snapshot_manifest_path.relative_to(store.task_dir(task["task_id"])).as_posix()],
|
|
"part_skill_ids": list(part_skill_audit.get("skill_ids") or []),
|
|
"generation_assumptions": list(part_skill_audit.get("assumptions") or []),
|
|
"reference_ids": reference_ids,
|
|
"summary": summary,
|
|
"parent_revision_id": parent_revision_id or "",
|
|
"operation": operation or {},
|
|
"input_attachments": input_attachments or [],
|
|
"repair_attempts": max(0, int(repair_attempts)),
|
|
}
|
|
if status == "success":
|
|
record.update({
|
|
"step_path": step_path.relative_to(store.task_dir(task["task_id"])).as_posix(),
|
|
"glb_path": glb_path.relative_to(store.task_dir(task["task_id"])).as_posix(),
|
|
"selector_path": selector_path.relative_to(store.task_dir(task["task_id"])).as_posix(),
|
|
"edges_path": edges_path.relative_to(store.task_dir(task["task_id"])).as_posix(),
|
|
"engine": engine_name,
|
|
})
|
|
else:
|
|
record["error"] = error
|
|
return record
|
|
|
|
quality_report: dict[str, Any] | None = None
|
|
quality_status = ""
|
|
try:
|
|
cdsl_copy = copy.deepcopy(cdsl)
|
|
if not isinstance(cdsl_copy, dict):
|
|
raise ValueError("CDSL must be a JSON object")
|
|
cdsl_copy["part_id"] = task["task_id"]
|
|
meta = cdsl_copy.setdefault("meta", {})
|
|
if not isinstance(meta, dict):
|
|
raise ValueError("CDSL meta must be an object when present")
|
|
if not isinstance(meta.get("editable_parameters"), list) or not meta["editable_parameters"]:
|
|
meta["editable_parameters"] = _derived_parameters(cdsl_copy)
|
|
write_json(cdsl_path, cdsl_copy)
|
|
write_json(parameters_path, parameter_contract(cdsl_copy))
|
|
validate_cdsl(cdsl_copy, engine)
|
|
# Product revisions are semantic CDSL artifacts. Do not route them
|
|
# through the legacy rebuild entry point, which is allowed to use
|
|
# compiler_context/translator compatibility fallbacks.
|
|
engine_result = engine.run_cdsl_only(cdsl_copy, step_path)
|
|
if engine_result.get("engine") != "cdsl_only" or not step_path.is_file() or step_path.stat().st_size == 0:
|
|
raise RuntimeError("Engine did not produce a CDSL-only STEP artifact")
|
|
preview = step_to_glb(step_path, glb_path)
|
|
selector, edges = topology_sidecars(engine_result, preview)
|
|
write_json(selector_path, selector)
|
|
write_json(edges_path, edges)
|
|
rules = validate_verification(verification, cdsl_copy)
|
|
quality_report = evaluate_quality(rules, cdsl_copy, engine_result)
|
|
quality_report["evaluated_at"] = now_iso()
|
|
write_json(quality_path, quality_report)
|
|
quality_status = (
|
|
"accepted" if rules and quality_report["status"] == "passed"
|
|
else "built_with_warnings" if quality_report["status"] == "passed"
|
|
else "needs_repair"
|
|
)
|
|
if quality_report["status"] != "passed":
|
|
raise QualityVerificationError(quality_report)
|
|
report = {
|
|
"engine_result": engine_result,
|
|
"preview": preview,
|
|
"generation_context": generation_context,
|
|
"validated_at": now_iso(),
|
|
}
|
|
write_json(report_path, report)
|
|
revision = revision_record("success", engine_name=str(engine_result["engine"]))
|
|
revision["quality_status"] = quality_status or "accepted"
|
|
revision["verification_summary"] = {
|
|
"requested": bool(rules),
|
|
"blocking_failures": len(quality_report.get("blocking_failures") or []),
|
|
"warnings": len(quality_report.get("warnings") or []),
|
|
}
|
|
except Exception as error:
|
|
if not isinstance(error, QualityVerificationError):
|
|
# The failed candidate is retained in the conversation diagnostics,
|
|
# not as a task revision. Runtime/schema failures must not create
|
|
# an editable revision that looks like a model version.
|
|
shutil.rmtree(revision_dir, ignore_errors=True)
|
|
raise
|
|
if not cdsl_path.is_file() and isinstance(cdsl, dict):
|
|
write_json(cdsl_path, copy.deepcopy(cdsl))
|
|
if quality_report is not None and not quality_path.is_file():
|
|
write_json(quality_path, quality_report)
|
|
write_json(report_path, {
|
|
"error": str(error),
|
|
"generation_context": generation_context,
|
|
"quality": quality_report,
|
|
"validated_at": now_iso(),
|
|
})
|
|
revision = revision_record("needs_repair" if quality_report is not None else "failed", error=str(error))
|
|
revision["quality_status"] = quality_status or ("needs_repair" if quality_report else "failed")
|
|
if quality_report is not None:
|
|
revision["verification_summary"] = {
|
|
"requested": bool(quality_report.get("verification_requested")),
|
|
"blocking_failures": len(quality_report.get("blocking_failures") or []),
|
|
"warnings": len(quality_report.get("warnings") or []),
|
|
}
|
|
store.update_task(task["task_id"], revision)
|
|
error.task_id = task["task_id"]
|
|
error.revision_id = revision_id
|
|
raise
|
|
store.update_task(task["task_id"], revision)
|
|
return {"task_id": task["task_id"], **revision}
|