添加cadfs_to_cdsl代码,优化cad生成
This commit is contained in:
@@ -124,7 +124,7 @@ class ProfileCadRuntime:
|
||||
result["ref_" + sha256(f"{document_hash}|{feature_id}".encode("utf-8")).hexdigest()[:16]] = feature_id
|
||||
return result
|
||||
|
||||
def materialize_fragment(self, base_cdsl: dict[str, Any] | None, fragment: dict[str, Any], contract: dict[str, Any], selector_tokens: dict[str, dict[str, Any]], reference_tokens: dict[str, str], *, require_through: bool = False) -> tuple[dict[str, Any], dict[str, Any]]:
|
||||
def materialize_fragment(self, base_cdsl: dict[str, Any] | None, fragment: dict[str, Any], contract: dict[str, Any], selector_tokens: dict[str, dict[str, Any]], reference_tokens: dict[str, str], *, require_through: bool = False, depends_on_feature_ids: tuple[str, ...] | list[str] = ()) -> tuple[dict[str, Any], dict[str, Any]]:
|
||||
# ActionCommandHandler validates the exposed schema first, but this
|
||||
# adapter is also used during crash recovery. Keep the runtime boundary
|
||||
# self-contained so a corrupted/replayed staged payload cannot produce
|
||||
@@ -168,7 +168,24 @@ class ProfileCadRuntime:
|
||||
if not isinstance(supplied, list) or not all(token in reference_tokens for token in supplied):
|
||||
raise RuntimeAdapterError("RUNTIME_PRECONDITION_FAILED: reference token is absent or stale")
|
||||
materialized["feature"]["params"][slot] = [reference_tokens[token] for token in supplied]
|
||||
self._semantic_preflight(materialized, contract, selector_tokens, base_cdsl, require_through=require_through)
|
||||
through_normalizations = self._normalize_required_through_cut_depth(
|
||||
materialized,
|
||||
contract,
|
||||
selector_tokens,
|
||||
require_through=require_through,
|
||||
)
|
||||
cut_support_normal = self._semantic_preflight(
|
||||
materialized,
|
||||
contract,
|
||||
selector_tokens,
|
||||
base_cdsl,
|
||||
require_through=require_through,
|
||||
)
|
||||
direction_normalizations = self._normalize_extrude_cut_direction(
|
||||
materialized,
|
||||
contract,
|
||||
cut_support_normal,
|
||||
)
|
||||
document = deepcopy(base_cdsl) if isinstance(base_cdsl, dict) else {
|
||||
"schema": "cad.cdsl.llm.v1", "schema_version": "1.1.0", "kind": "part", "part_id": "agent_preflight", "geometry": {"sketches": []}, "features": [],
|
||||
}
|
||||
@@ -177,9 +194,17 @@ class ProfileCadRuntime:
|
||||
features = document.setdefault("features", [])
|
||||
if not isinstance(sketches, list) or not isinstance(features, list):
|
||||
raise RuntimeAdapterError("RUNTIME_PRECONDITION_FAILED: base CDSL collections are invalid")
|
||||
existing_feature_ids = {
|
||||
str(item.get("id") or "")
|
||||
for item in features
|
||||
if isinstance(item, dict) and str(item.get("id") or "")
|
||||
}
|
||||
direct_dependencies = tuple(str(value) for value in depends_on_feature_ids)
|
||||
if len(direct_dependencies) != len(set(direct_dependencies)) or any(value not in existing_feature_ids for value in direct_dependencies):
|
||||
raise RuntimeAdapterError("RUNTIME_PRECONDITION_FAILED: plan dependency feature is absent from the active checkpoint")
|
||||
index = len(features) + 1
|
||||
feature = materialized["feature"]
|
||||
output = {"id": f"feature_{index:03d}", "atomic_id": contract["atomic_id"], "params": deepcopy(feature["params"]), "depends_on": [str(features[-1].get("id"))] if features else []}
|
||||
output = {"id": f"feature_{index:03d}", "atomic_id": contract["atomic_id"], "params": deepcopy(feature["params"]), "depends_on": list(direct_dependencies)}
|
||||
if contract["fragment_shape"]["sketch"] == "required":
|
||||
sketch_id = f"sketch_{len(sketches) + 1:03d}"
|
||||
sketch = materialized["sketch"]
|
||||
@@ -225,35 +250,69 @@ class ProfileCadRuntime:
|
||||
"RUNTIME_CONTRACT_INVALID: materialized fragment violates the engine CDSL schema: "
|
||||
+ message
|
||||
) from error
|
||||
return document, {"schema_version": "cad.v3.fragment-audit.v1", "atomic_id": contract["atomic_id"], "fragment_hash": canonical_hash(fragment), "contract_hash": contract["contract_hash"], "assigned_feature_ids": [output["id"]], "assigned_sketch_ids": [output["sketch_id"]] if output.get("sketch_id") else [], "selector_snapshot_id": next(iter(selector_tokens.values()), {}).get("snapshot_id", ""), "selector_tokens": list(feature.get("selector_tokens", [])), "reference_snapshot_id": canonical_hash(reference_tokens), "reference_tokens": list(fragment.get("feature", {}).get("params", {}).get(str(reference.get("slot") or "").removeprefix("params."), [])) if reference["mode"] == "snapshot_bound" else []}
|
||||
return document, {"schema_version": "cad.v3.2.fragment-audit.v1", "atomic_id": contract["atomic_id"], "fragment_hash": canonical_hash(fragment), "contract_hash": contract["contract_hash"], "assigned_feature_ids": [output["id"]], "depends_on_feature_ids": list(direct_dependencies), "assigned_sketch_ids": [output["sketch_id"]] if output.get("sketch_id") else [], "selector_snapshot_id": next(iter(selector_tokens.values()), {}).get("snapshot_id", ""), "selector_tokens": list(feature.get("selector_tokens", [])), "reference_snapshot_id": canonical_hash(reference_tokens), "reference_tokens": list(fragment.get("feature", {}).get("params", {}).get(str(reference.get("slot") or "").removeprefix("params."), [])) if reference["mode"] == "snapshot_bound" else [], "server_normalizations": [*through_normalizations, *direction_normalizations]}
|
||||
|
||||
def rebuild(self, cdsl: dict[str, Any], output_dir: str, task_id: str, revision_id: str) -> dict[str, Any]:
|
||||
def build_checkpoint(self, cdsl: dict[str, Any], output_dir: str, task_id: str, revision_id: str) -> dict[str, Any]:
|
||||
"""Build the exact geometry checkpoint required by every DAG node.
|
||||
|
||||
Rendering is deliberately absent here: a renderer outage must never
|
||||
make a geometrically valid atomic feature fail its local acceptance.
|
||||
"""
|
||||
root = Path(output_dir)
|
||||
root.mkdir(parents=True, exist_ok=True)
|
||||
cdsl_copy = deepcopy(cdsl)
|
||||
cdsl_copy["part_id"] = task_id
|
||||
step_path = root / "model.step"
|
||||
glb_path = root / "model.glb"
|
||||
self._write_json(root / "model.cdsl.json", cdsl_copy)
|
||||
try:
|
||||
self._validate_finite_tree(cdsl_copy)
|
||||
validate_cdsl(cdsl_copy, self.engine)
|
||||
except RuntimeAdapterError:
|
||||
raise
|
||||
except Exception as error:
|
||||
raise RuntimeAdapterError(f"RUNTIME_PRECONDITION_FAILED: persisted CDSL failed rebuild preflight: {error}") from error
|
||||
try:
|
||||
engine_result = self.engine.run_cdsl_only(cdsl_copy, step_path)
|
||||
if str(engine_result.get("engine") or "") != "cdsl_only":
|
||||
raise RuntimeAdapterError("engine did not execute CDSL-only rebuild")
|
||||
preview = step_to_glb(step_path, glb_path)
|
||||
health = self._health(engine_result, step_path, glb_path)
|
||||
topology = topology_snapshot(engine_result, task_id=task_id, revision_id=revision_id, preview=preview)
|
||||
health = self._health(engine_result, step_path)
|
||||
topology = topology_snapshot(engine_result, task_id=task_id, revision_id=revision_id)
|
||||
self._write_json(root / "model.topology.json", topology)
|
||||
manifest = render_checkpoint(self.settings, step_path=step_path, output_dir=root / "renders")
|
||||
report = {"engine_result": engine_result, "preview": preview, "health": health, "render_manifest": manifest}
|
||||
report = {"engine_result": engine_result, "preview": {}, "health": health, "render_manifest": {}}
|
||||
self._write_json(root / "rebuild-report.json", report)
|
||||
return {"health": health, "topology": topology, "report": report, "render_manifest": manifest, "paths": {"cdsl": "model.cdsl.json", "step": "model.step", "glb": "model.glb", "topology": "model.topology.json", "report": "rebuild-report.json", "render_manifest": "renders/render-manifest.json"}}
|
||||
return {"health": health, "topology": topology, "report": report, "render_manifest": {}, "paths": {"cdsl": "model.cdsl.json", "step": "model.step", "topology": "model.topology.json", "report": "rebuild-report.json"}}
|
||||
except OSError:
|
||||
raise
|
||||
except RuntimeAdapterError:
|
||||
raise
|
||||
except Exception as error:
|
||||
raise RuntimeAdapterError(f"RUNTIME_EXECUTION_FAILURE: {error}") from error
|
||||
|
||||
def create_preview(self, output_dir: str) -> dict[str, Any]:
|
||||
"""Create an optional GLB preview for a completed checkpoint."""
|
||||
root = Path(output_dir)
|
||||
step_path = root / "model.step"
|
||||
glb_path = root / "model.glb"
|
||||
preview = step_to_glb(step_path, glb_path)
|
||||
report_path = root / "rebuild-report.json"
|
||||
report = json.loads(report_path.read_text(encoding="utf-8")) if report_path.is_file() else {}
|
||||
report["preview"] = preview
|
||||
self._write_json(report_path, report)
|
||||
return {"preview": preview, "path": "model.glb"}
|
||||
|
||||
def render_review_bundle(self, output_dir: str) -> dict[str, Any]:
|
||||
root = Path(output_dir)
|
||||
manifest = render_checkpoint(self.settings, step_path=root / "model.step", output_dir=root / "renders")
|
||||
report_path = root / "rebuild-report.json"
|
||||
report = json.loads(report_path.read_text(encoding="utf-8")) if report_path.is_file() else {}
|
||||
report["render_manifest"] = manifest
|
||||
self._write_json(report_path, report)
|
||||
return manifest
|
||||
|
||||
def rebuild(self, cdsl: dict[str, Any], output_dir: str, task_id: str, revision_id: str) -> dict[str, Any]:
|
||||
# Legacy v3.1 compatibility path. New DAG nodes use build_checkpoint.
|
||||
built = self.build_checkpoint(cdsl, output_dir, task_id, revision_id)
|
||||
root = Path(output_dir)
|
||||
try:
|
||||
preview = self.create_preview(output_dir)
|
||||
manifest = self.render_review_bundle(output_dir)
|
||||
report = json.loads((root / "rebuild-report.json").read_text(encoding="utf-8"))
|
||||
return {**built, "report": report, "preview": preview["preview"], "render_manifest": manifest, "paths": {**built["paths"], "glb": "model.glb", "render_manifest": "renders/render-manifest.json"}}
|
||||
except OSError:
|
||||
# Artifact writes are a recoverable infrastructure outage. Let the
|
||||
# application handler park the same candidate stage for replay.
|
||||
@@ -330,7 +389,7 @@ class ProfileCadRuntime:
|
||||
}
|
||||
return document
|
||||
|
||||
def _semantic_preflight(self, fragment: dict[str, Any], contract: dict[str, Any], selector_tokens: dict[str, dict[str, Any]], base_cdsl: dict[str, Any] | None, *, require_through: bool) -> None:
|
||||
def _semantic_preflight(self, fragment: dict[str, Any], contract: dict[str, Any], selector_tokens: dict[str, dict[str, Any]], base_cdsl: dict[str, Any] | None, *, require_through: bool) -> list[float] | None:
|
||||
if fragment.get("feature", {}).get("atomic_id") != contract.get("atomic_id"):
|
||||
raise RuntimeAdapterError("RUNTIME_PRECONDITION_FAILED: atomic_id does not match active contract")
|
||||
policy = contract["selector_policy"]
|
||||
@@ -340,6 +399,83 @@ class ProfileCadRuntime:
|
||||
raise RuntimeAdapterError("RUNTIME_PRECONDITION_FAILED: selector token is absent, stale, or has the wrong kind")
|
||||
for name in contract["semantic_preflight"]:
|
||||
self._semantic_preflight_handlers[name](fragment, selector_tokens, base_cdsl, require_through)
|
||||
if contract.get("atomic_id") == "extrude_cut_blind":
|
||||
return self._preflight_extrude_cut_contacts_material(fragment, selector_tokens)
|
||||
return None
|
||||
|
||||
def _normalize_extrude_cut_direction(
|
||||
self,
|
||||
fragment: dict[str, Any],
|
||||
contract: dict[str, Any],
|
||||
support_normal: list[float] | None,
|
||||
) -> list[dict[str, Any]]:
|
||||
"""Aim a surface-attached cut into the measured material half-space.
|
||||
|
||||
A sketch extrusion has no host selector. Once preflight proves that its
|
||||
profile lies on an oriented boundary face, the only executable blind
|
||||
cut direction is the material side of that face. This is a coordinate
|
||||
normalization, not a planning decision, and is recorded in the audit.
|
||||
"""
|
||||
if contract.get("atomic_id") != "extrude_cut_blind" or not self._valid_vector3(support_normal, require_nonzero=True):
|
||||
return []
|
||||
sketch = fragment.get("sketch") if isinstance(fragment, dict) else None
|
||||
workplane = sketch.get("workplane") if isinstance(sketch, dict) else None
|
||||
normal = workplane.get("normal") if isinstance(workplane, dict) else None
|
||||
params = fragment.get("feature", {}).get("params") if isinstance(fragment.get("feature"), dict) else None
|
||||
if not self._valid_vector3(normal, require_nonzero=True) or not isinstance(params, dict):
|
||||
return []
|
||||
unit_normal = [float(component) / self._norm(normal) for component in normal]
|
||||
unit_support = [float(component) / self._norm(support_normal) for component in support_normal]
|
||||
alignment = self._dot(unit_normal, unit_support)
|
||||
if abs(abs(alignment) - 1.0) > 1e-6:
|
||||
return []
|
||||
materialized_reverse = alignment > 0
|
||||
submitted_reverse = bool(params.get("reverse", False))
|
||||
params["reverse"] = materialized_reverse
|
||||
return [{
|
||||
"path": "feature.params.reverse",
|
||||
"submitted": submitted_reverse,
|
||||
"materialized": materialized_reverse,
|
||||
"reason": "surface-attached cut must travel into the measured material half-space",
|
||||
"support_normal": unit_support,
|
||||
}]
|
||||
|
||||
def _normalize_required_through_cut_depth(
|
||||
self,
|
||||
fragment: dict[str, Any],
|
||||
contract: dict[str, Any],
|
||||
selector_tokens: dict[str, dict[str, Any]],
|
||||
*,
|
||||
require_through: bool,
|
||||
) -> list[dict[str, Any]]:
|
||||
"""Add the minimum deterministic exit allowance for a through cut.
|
||||
|
||||
The author owns nominal feature geometry. For a through requirement,
|
||||
however, the runtime owns the executable end condition: this engine
|
||||
needs a strictly greater cut distance than the measured host span.
|
||||
Recording the adjustment makes the operational allowance visible
|
||||
without treating it as a user-specified blind-cut depth.
|
||||
"""
|
||||
if not require_through or contract.get("atomic_id") != "extrude_cut_blind":
|
||||
return []
|
||||
params = fragment.get("feature", {}).get("params", {})
|
||||
sketch = fragment.get("sketch") if isinstance(fragment.get("sketch"), dict) else {}
|
||||
workplane = sketch.get("workplane") if isinstance(sketch, dict) else {}
|
||||
normal = workplane.get("normal") if isinstance(workplane, dict) else None
|
||||
thickness = self._span_from_bbox(self._active_body_bbox(selector_tokens), normal)
|
||||
distance = params.get("distance_mm") if isinstance(params, dict) else None
|
||||
if not isinstance(distance, (int, float)) or thickness is None:
|
||||
return []
|
||||
required_distance = float(thickness) + 0.01
|
||||
if float(distance) > float(thickness) + 1e-6:
|
||||
return []
|
||||
params["distance_mm"] = required_distance
|
||||
return [{
|
||||
"path": "/feature/params/distance_mm",
|
||||
"submitted_mm": float(distance),
|
||||
"materialized_mm": required_distance,
|
||||
"reason": "required through-cut exit allowance",
|
||||
}]
|
||||
|
||||
def _preflight_sketch_workplane(self, fragment: dict[str, Any], _selectors: dict[str, dict[str, Any]], _base: dict[str, Any] | None, _require_through: bool) -> None:
|
||||
sketch = fragment.get("sketch")
|
||||
@@ -362,6 +498,123 @@ class ProfileCadRuntime:
|
||||
elif profile.get("type") == "analytic_contours":
|
||||
self._preflight_analytic_contours(profile)
|
||||
|
||||
def _preflight_extrude_cut_contacts_material(self, fragment: dict[str, Any], selectors: dict[str, dict[str, Any]]) -> list[float] | None:
|
||||
"""Reject an extrude cut whose start profile floats above the solid.
|
||||
|
||||
Sketch cuts do not carry a host-face selector, so a model can place a
|
||||
correct 2-D profile on the top of an unrelated boss. The engine then
|
||||
rebuilds successfully but leaves the body unchanged. Detect the common
|
||||
no-contact form using the current planar topology and return a useful
|
||||
coordinate diagnosis before allocating a stage or running the kernel.
|
||||
"""
|
||||
sketch = fragment.get("sketch") if isinstance(fragment, dict) else None
|
||||
workplane = sketch.get("workplane") if isinstance(sketch, dict) else None
|
||||
profile = sketch.get("profile") if isinstance(sketch, dict) else None
|
||||
origin = workplane.get("origin_mm") if isinstance(workplane, dict) else None
|
||||
normal = workplane.get("normal") if isinstance(workplane, dict) else None
|
||||
x_dir = workplane.get("x_dir") if isinstance(workplane, dict) else None
|
||||
if not self._valid_vector3(origin) or not self._valid_vector3(normal, require_nonzero=True) or not self._valid_vector3(x_dir, require_nonzero=True) or not isinstance(profile, dict):
|
||||
return None
|
||||
unit_normal = [float(component) / self._norm(normal) for component in normal]
|
||||
x_projection = self._dot(x_dir, unit_normal)
|
||||
raw_x = [float(x_dir[index]) - x_projection * unit_normal[index] for index in range(3)]
|
||||
if self._norm(raw_x) <= 1e-9:
|
||||
return None
|
||||
unit_x = [value / self._norm(raw_x) for value in raw_x]
|
||||
unit_y = self._cross(unit_normal, unit_x)
|
||||
local_points = self._profile_probe_points(profile)
|
||||
if not local_points:
|
||||
return None
|
||||
world_points = [
|
||||
[
|
||||
float(origin[index]) + local[0] * unit_x[index] + local[1] * unit_y[index]
|
||||
for index in range(3)
|
||||
]
|
||||
for local in local_points
|
||||
]
|
||||
tolerance = 1e-5
|
||||
matching_faces: list[dict[str, Any]] = []
|
||||
available_heights: list[float] = []
|
||||
for value in selectors.values():
|
||||
geometry = value.get("geometry") if isinstance(value, dict) else None
|
||||
face_normal = geometry.get("normal") if isinstance(geometry, dict) else None
|
||||
center = geometry.get("center_mm") if isinstance(geometry, dict) else None
|
||||
loops = geometry.get("boundary_loops_mm") if isinstance(geometry, dict) else None
|
||||
if (
|
||||
not isinstance(geometry, dict)
|
||||
or geometry.get("surface_type") != "plane"
|
||||
or not self._valid_vector3(face_normal, require_nonzero=True)
|
||||
or not self._valid_vector3(center)
|
||||
or not isinstance(loops, list)
|
||||
or not loops
|
||||
):
|
||||
continue
|
||||
unit_face_normal = [float(component) / self._norm(face_normal) for component in face_normal]
|
||||
if abs(abs(self._dot(unit_normal, unit_face_normal)) - 1.0) > 1e-6:
|
||||
continue
|
||||
available_heights.append(self._dot([float(center[index]) for index in range(3)], unit_normal))
|
||||
if abs(self._dot([float(origin[index]) - float(center[index]) for index in range(3)], unit_normal)) <= tolerance:
|
||||
matching_faces.append(geometry)
|
||||
if not matching_faces:
|
||||
return None
|
||||
for face in matching_faces:
|
||||
if any(
|
||||
self._point_in_planar_face(point, face["boundary_loops_mm"], unit_normal, tolerance)
|
||||
for point in world_points
|
||||
):
|
||||
face_normal = face.get("normal")
|
||||
return [float(component) for component in face_normal] if self._valid_vector3(face_normal, require_nonzero=True) else None
|
||||
plane_coordinate = self._dot([float(value) for value in origin], unit_normal)
|
||||
heights = ", ".join(f"{value:g}" for value in sorted(set(round(value, 6) for value in available_heights))[:8])
|
||||
raise RuntimeAdapterError(
|
||||
"RUNTIME_PRECONDITION_FAILED: extrude-cut profile does not contact material on its start plane "
|
||||
f"(plane coordinate {plane_coordinate:g}; available parallel planar faces: [{heights}]); "
|
||||
"place the sketch on the material face containing the intended cut profile"
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _profile_probe_points(profile: dict[str, Any]) -> list[tuple[float, float]]:
|
||||
"""Return inexpensive local points sufficient for contact preflight."""
|
||||
points: list[tuple[float, float]] = []
|
||||
|
||||
def append(value: Any) -> None:
|
||||
point = ProfileCadRuntime._point2(value)
|
||||
if point is not None:
|
||||
points.append(point)
|
||||
|
||||
profile_type = profile.get("type")
|
||||
if profile_type == "circle":
|
||||
append(profile.get("center"))
|
||||
elif profile_type == "rectangle":
|
||||
append(profile.get("center"))
|
||||
elif profile_type == "polygon":
|
||||
vertices = profile.get("vertices")
|
||||
if isinstance(vertices, list):
|
||||
for vertex in vertices:
|
||||
append(vertex)
|
||||
elif profile_type == "analytic_contours":
|
||||
contours = profile.get("contours")
|
||||
if isinstance(contours, list):
|
||||
for contour in contours:
|
||||
segments = contour.get("segments") if isinstance(contour, dict) else None
|
||||
if not isinstance(segments, list):
|
||||
continue
|
||||
contour_points: list[tuple[float, float]] = []
|
||||
for segment in segments:
|
||||
if not isinstance(segment, dict):
|
||||
continue
|
||||
for key in ("start", "end", "center"):
|
||||
point = ProfileCadRuntime._point2(segment.get(key))
|
||||
if point is not None:
|
||||
points.append(point)
|
||||
contour_points.append(point)
|
||||
if contour_points:
|
||||
points.append((
|
||||
sum(point[0] for point in contour_points) / len(contour_points),
|
||||
sum(point[1] for point in contour_points) / len(contour_points),
|
||||
))
|
||||
return points
|
||||
|
||||
def _preflight_host_face_exists(self, fragment: dict[str, Any], selectors: dict[str, dict[str, Any]], _base: dict[str, Any] | None, _require_through: bool) -> None:
|
||||
supplied = fragment.get("feature", {}).get("selector_tokens", [])
|
||||
if len(supplied) != 1 or not isinstance(selectors.get(supplied[0]), dict) or selectors[supplied[0]].get("kind") != "face":
|
||||
@@ -439,7 +692,79 @@ class ProfileCadRuntime:
|
||||
raise RuntimeAdapterError("RUNTIME_PRECONDITION_FAILED: hole position is outside the selected host-face bounds")
|
||||
boundary_loops = geometry.get("boundary_loops_mm")
|
||||
if isinstance(boundary_loops, list) and boundary_loops and not self._point_in_planar_face(point, boundary_loops, unit_normal, tolerance):
|
||||
raise RuntimeAdapterError("RUNTIME_PRECONDITION_FAILED: hole position is outside the selected host face's effective boundary")
|
||||
if not self._counterbore_reuses_existing_pilot(fragment, selectors, point, unit_normal, tolerance):
|
||||
host_z = float(center[2]) if isinstance(center, list) and len(center) == 3 else float("nan")
|
||||
coordinate = ", ".join(f"{float(value):g}" for value in point)
|
||||
raise RuntimeAdapterError(
|
||||
"RUNTIME_PRECONDITION_FAILED: hole position "
|
||||
f"[{coordinate}] lies outside the selected host face material boundary (host center z={host_z:g}); "
|
||||
"select a planar host face that contains every requested hole center"
|
||||
)
|
||||
|
||||
def _counterbore_reuses_existing_pilot(
|
||||
self,
|
||||
fragment: dict[str, Any],
|
||||
selectors: dict[str, dict[str, Any]],
|
||||
point: list[float],
|
||||
host_normal: list[float],
|
||||
tolerance: float,
|
||||
) -> bool:
|
||||
"""Allow a counterbore to start from an existing coaxial pilot bore.
|
||||
|
||||
A top face becomes annular after a through bore, so the pilot centre
|
||||
is deliberately outside its material boundary. Counterboring that
|
||||
pilot is nevertheless a standard valid operation. The exception is
|
||||
deliberately narrow: it only applies to a matching inner cylindrical
|
||||
bore whose axis is normal to the selected host plane and whose open
|
||||
end contains the requested start point.
|
||||
"""
|
||||
feature = fragment.get("feature") if isinstance(fragment, dict) else None
|
||||
params = feature.get("params") if isinstance(feature, dict) and isinstance(feature.get("params"), dict) else {}
|
||||
pilot_diameter = params.get("diameter_mm")
|
||||
counterbore_diameter = params.get("counterbore_diameter_mm")
|
||||
if (
|
||||
not isinstance(feature, dict)
|
||||
or feature.get("atomic_id") != "hole_counterbore"
|
||||
or not isinstance(pilot_diameter, (int, float))
|
||||
or not isinstance(counterbore_diameter, (int, float))
|
||||
or float(pilot_diameter) <= 0
|
||||
or float(counterbore_diameter) <= float(pilot_diameter)
|
||||
):
|
||||
return False
|
||||
diameter_tolerance = max(tolerance, abs(float(pilot_diameter)) * 1e-6)
|
||||
for value in selectors.values():
|
||||
geometry = value.get("geometry") if isinstance(value, dict) else None
|
||||
if (
|
||||
not isinstance(geometry, dict)
|
||||
or geometry.get("surface_type") != "cylinder"
|
||||
or geometry.get("cylinder_role") != "inner"
|
||||
or not bool(geometry.get("through"))
|
||||
):
|
||||
continue
|
||||
radius = geometry.get("radius_mm")
|
||||
axis_origin = geometry.get("axis_origin_mm")
|
||||
axis_direction = geometry.get("axis_direction")
|
||||
bbox = geometry.get("bbox_mm")
|
||||
if (
|
||||
not isinstance(radius, (int, float))
|
||||
or abs(2 * float(radius) - float(pilot_diameter)) > diameter_tolerance
|
||||
or not self._valid_vector3(axis_origin)
|
||||
or not self._valid_vector3(axis_direction, require_nonzero=True)
|
||||
or not self._valid_bbox(bbox)
|
||||
):
|
||||
continue
|
||||
unit_axis = [float(component) / self._norm(axis_direction) for component in axis_direction]
|
||||
if abs(abs(self._dot(unit_axis, host_normal)) - 1.0) > 1e-6:
|
||||
continue
|
||||
offset = [float(point[index]) - float(axis_origin[index]) for index in range(3)]
|
||||
axial = self._dot(offset, unit_axis)
|
||||
radial = [offset[index] - axial * unit_axis[index] for index in range(3)]
|
||||
if self._norm(radial) > tolerance:
|
||||
continue
|
||||
if any(float(point[index]) < float(bbox[index]) - tolerance or float(point[index]) > float(bbox[index + 3]) + tolerance for index in range(3)):
|
||||
continue
|
||||
return True
|
||||
return False
|
||||
|
||||
def _preflight_analytic_contours(self, profile: dict[str, Any]) -> None:
|
||||
contours = profile.get("contours")
|
||||
@@ -750,9 +1075,9 @@ class ProfileCadRuntime:
|
||||
path.write_text(json.dumps(payload, ensure_ascii=False, indent=2), encoding="utf-8")
|
||||
|
||||
@staticmethod
|
||||
def _health(engine_result: dict[str, Any], step_path: Path, glb_path: Path) -> dict[str, Any]:
|
||||
def _health(engine_result: dict[str, Any], step_path: Path, glb_path: Path | None = None) -> dict[str, Any]:
|
||||
bbox = engine_result.get("bbox_mm") if isinstance(engine_result.get("bbox_mm"), dict) else {}
|
||||
minimum, maximum = bbox.get("min"), bbox.get("max")
|
||||
if not isinstance(minimum, list) or not isinstance(maximum, list) or len(minimum) != 3 or len(maximum) != 3 or not step_path.is_file() or not glb_path.is_file():
|
||||
if not isinstance(minimum, list) or not isinstance(maximum, list) or len(minimum) != 3 or len(maximum) != 3 or not step_path.is_file() or (glb_path is not None and not glb_path.is_file()):
|
||||
raise RuntimeAdapterError("rebuild did not produce complete deterministic artifacts")
|
||||
return {"bbox_mm": {"min": [float(item) for item in minimum], "max": [float(item) for item in maximum], "dimensions": [float(maximum[index]) - float(minimum[index]) for index in range(3)]}, "volume_mm3": float(engine_result["volume_mm3"]), "solid_count": int(engine_result["solid_count"]), "feature_count": len(engine_result.get("feature_results") or [])}
|
||||
|
||||
@@ -35,12 +35,12 @@ class SqliteTaskRepository:
|
||||
|
||||
def _initialize(self) -> None:
|
||||
with self._lock, self._connection() as connection:
|
||||
# Protocol 3.1 intentionally has no migration path from the
|
||||
# Protocol 3.2 intentionally has no migration path from the
|
||||
# structured-only / review-loop task model. Deployment starts with
|
||||
# an empty task database, as those tasks do not have immutable
|
||||
# Markdown source artifacts to compile from.
|
||||
existing = connection.execute("SELECT sql FROM sqlite_master WHERE type = 'table' AND name = 'tasks'").fetchone()
|
||||
if existing is not None and "'3.1'" not in str(existing[0] or ""):
|
||||
if existing is not None and "'3.2'" not in str(existing[0] or ""):
|
||||
self.protocol_reset = True
|
||||
connection.executescript("""
|
||||
DROP TABLE IF EXISTS outbox;
|
||||
@@ -55,7 +55,7 @@ class SqliteTaskRepository:
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS tasks (
|
||||
task_id TEXT PRIMARY KEY,
|
||||
protocol_version TEXT NOT NULL CHECK(protocol_version = '3.1'),
|
||||
protocol_version TEXT NOT NULL CHECK(protocol_version = '3.2'),
|
||||
request TEXT NOT NULL,
|
||||
phase TEXT NOT NULL,
|
||||
state_version INTEGER NOT NULL,
|
||||
@@ -70,6 +70,9 @@ class SqliteTaskRepository:
|
||||
requirements_document_path TEXT NOT NULL DEFAULT '',
|
||||
completion_target_path TEXT NOT NULL DEFAULT '',
|
||||
modeling_plan_path TEXT NOT NULL DEFAULT '',
|
||||
feature_plan_path TEXT NOT NULL DEFAULT '',
|
||||
feature_plan_hash TEXT NOT NULL DEFAULT '',
|
||||
feature_stage_id TEXT NOT NULL DEFAULT '',
|
||||
clarification_path TEXT NOT NULL DEFAULT '',
|
||||
requirements_contract_path TEXT NOT NULL DEFAULT '',
|
||||
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
@@ -124,7 +127,7 @@ class SqliteTaskRepository:
|
||||
def create_task(self, task_id: str, request: str) -> TaskState:
|
||||
with self._lock, self._connection() as connection:
|
||||
connection.execute(
|
||||
"INSERT OR IGNORE INTO tasks(task_id, protocol_version, request, phase, state_version) VALUES (?, '3.1', ?, ?, 0)",
|
||||
"INSERT OR IGNORE INTO tasks(task_id, protocol_version, request, phase, state_version) VALUES (?, '3.2', ?, ?, 0)",
|
||||
(task_id, request, TaskPhase.DRAFTING_REQUIREMENTS_DOCUMENT.value),
|
||||
)
|
||||
state = self.get_state(task_id)
|
||||
@@ -145,10 +148,10 @@ class SqliteTaskRepository:
|
||||
revisions = [
|
||||
{
|
||||
"revision_id": item["revision_id"], "status": "success", "visibility": "final" if state.phase == TaskPhase.COMPLETED and item["revision_id"] == state.active_revision else "checkpoint",
|
||||
"cdsl_path": f"revisions/{item['revision_id']}/model.cdsl.json", "step_path": f"revisions/{item['revision_id']}/model.step", "glb_path": f"revisions/{item['revision_id']}/model.glb", "report_path": f"revisions/{item['revision_id']}/rebuild-report.json", "candidate_review_path": item.get("review_path", ""),
|
||||
"cdsl_path": f"revisions/{item['revision_id']}/model.cdsl.json", "step_path": f"revisions/{item['revision_id']}/model.step", "glb_path": "" if item.get("preview_unavailable") else f"revisions/{item['revision_id']}/model.glb", "report_path": f"revisions/{item['revision_id']}/rebuild-report.json", "candidate_review_path": item.get("review_path", ""),
|
||||
}
|
||||
for item in events
|
||||
if item.get("event") == "accepted" and isinstance(item.get("revision_id"), str)
|
||||
if item.get("event") in {"accepted", "feature_node_verified"} and isinstance(item.get("revision_id"), str)
|
||||
]
|
||||
frozen = next((item for item in reversed(events) if item.get("event") == "requirements_compiled"), {})
|
||||
verification_warnings = [
|
||||
@@ -167,7 +170,7 @@ class SqliteTaskRepository:
|
||||
questions = [str(item) for item in status_event.get("questions") or () if str(item)] if isinstance(status_event, dict) else []
|
||||
issues = [str(item) for item in status_event.get("issues") or () if str(item)] if isinstance(status_event, dict) else []
|
||||
return {
|
||||
"schema_version": "3.1",
|
||||
"schema_version": "3.2",
|
||||
"task_id": state.task_id,
|
||||
"phase": state.phase.value,
|
||||
"lifecycle": self._lifecycle(state.phase),
|
||||
@@ -184,6 +187,11 @@ class SqliteTaskRepository:
|
||||
"requirements_document_path": state.requirements_document_path,
|
||||
"completion_target_path": state.completion_target_path,
|
||||
"modeling_plan_path": state.modeling_plan_path,
|
||||
"feature_plan_path": state.feature_plan_path,
|
||||
"feature_plan_hash": state.feature_plan_hash,
|
||||
"current_feature_node_id": state.pending_feature.node_id if state.pending_feature else "",
|
||||
"pending_feature": self._pending_payload(state.pending_feature),
|
||||
"feature_nodes": self._feature_node_projection(events),
|
||||
"clarification_path": state.clarification_path,
|
||||
"requirements_contract_path": state.requirements_contract_path,
|
||||
"verification_status": (
|
||||
@@ -204,6 +212,25 @@ class SqliteTaskRepository:
|
||||
"revisions": revisions,
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def _feature_node_projection(events: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
||||
"""Return ledger-backed execution evidence; API adds immutable plan fields."""
|
||||
nodes: dict[str, dict[str, Any]] = {}
|
||||
for event in events:
|
||||
node_id = str(event.get("node_id") or "")
|
||||
if not node_id:
|
||||
continue
|
||||
item = nodes.setdefault(node_id, {"node_id": node_id, "status": "pending", "attempt": 0})
|
||||
if event.get("event") == "feature_node_scheduled":
|
||||
item.update({"status": "running", "atomic_id": str(event.get("atomic_id") or ""), "priority": event.get("priority"), "claim_ids": event.get("claim_ids") or [], "depends_on": event.get("depends_on") or []})
|
||||
elif event.get("event") == "feature_node_failed":
|
||||
item.update({"status": "failed" if event.get("terminal") else "pending", "attempt": int(event.get("attempt") or 0), "failure_class": str(event.get("failure_class") or ""), "error": str(event.get("message") or "")})
|
||||
elif event.get("event") == "feature_node_verified":
|
||||
item.update({"status": "done", "revision_id": str(event.get("revision_id") or ""), "feature_id": str(event.get("feature_id") or ""), "evidence": event.get("claim_results") or []})
|
||||
elif event.get("event") == "feature_node_invalidated" and item.get("status") != "done":
|
||||
item["status"] = "invalidated"
|
||||
return list(nodes.values())
|
||||
|
||||
def ledger_events(self, task_id: str) -> list[dict[str, Any]]:
|
||||
with self._lock, self._connection() as connection:
|
||||
rows = connection.execute("SELECT sequence, event_json, created_at FROM ledger WHERE task_id = ? ORDER BY sequence", (task_id,)).fetchall()
|
||||
@@ -243,7 +270,7 @@ class SqliteTaskRepository:
|
||||
cursor = connection.execute(
|
||||
"""UPDATE tasks SET phase = ?, state_version = ?, active_revision = ?, pending_action_json = ?,
|
||||
candidate_id = ?, candidate_stage_id = ?, repair_required = ?, last_error = ?, retry_from_phase = ?, requirements_spec_path = ?,
|
||||
requirements_document_path = ?, completion_target_path = ?, modeling_plan_path = ?, clarification_path = ?, requirements_contract_path = ?, updated_at = CURRENT_TIMESTAMP
|
||||
requirements_document_path = ?, completion_target_path = ?, modeling_plan_path = ?, feature_plan_path = ?, feature_plan_hash = ?, feature_stage_id = ?, clarification_path = ?, requirements_contract_path = ?, updated_at = CURRENT_TIMESTAMP
|
||||
WHERE task_id = ? AND state_version = ?""",
|
||||
(
|
||||
state.phase.value, state.version, state.active_revision,
|
||||
@@ -252,6 +279,7 @@ class SqliteTaskRepository:
|
||||
int(state.repair_required), state.last_error.value if state.last_error else None,
|
||||
state.retry_from_phase.value if state.retry_from_phase else "", state.requirements_spec_path,
|
||||
state.requirements_document_path, state.completion_target_path, state.modeling_plan_path,
|
||||
state.feature_plan_path, state.feature_plan_hash, state.feature_stage_id,
|
||||
state.clarification_path, state.requirements_contract_path,
|
||||
state.task_id, previous_version,
|
||||
),
|
||||
@@ -393,6 +421,8 @@ class SqliteTaskRepository:
|
||||
action_id=raw_pending["action_id"], working_head=raw_pending["working_head"], intent=raw_pending["intent"],
|
||||
requirement_ids=tuple(raw_pending["requirement_ids"]), atomic_id=raw_pending["atomic_id"],
|
||||
expected_change=raw_pending["expected_change"], contract_hash=raw_pending["contract_hash"], idempotency_key=raw_pending["idempotency_key"],
|
||||
node_id=str(raw_pending.get("node_id") or ""), plan_hash=str(raw_pending.get("plan_hash") or ""),
|
||||
claim_ids=tuple(raw_pending.get("claim_ids") or ()), depends_on_node_ids=tuple(raw_pending.get("depends_on_node_ids") or ()),
|
||||
) if isinstance(raw_pending, dict) else None
|
||||
return TaskState(
|
||||
task_id=str(row["task_id"]), phase=TaskPhase(str(row["phase"])), version=int(row["state_version"]),
|
||||
@@ -405,6 +435,9 @@ class SqliteTaskRepository:
|
||||
requirements_document_path=str(row["requirements_document_path"] or ""),
|
||||
completion_target_path=str(row["completion_target_path"] or ""),
|
||||
modeling_plan_path=str(row["modeling_plan_path"] or ""),
|
||||
feature_plan_path=str(row["feature_plan_path"] or ""),
|
||||
feature_plan_hash=str(row["feature_plan_hash"] or ""),
|
||||
feature_stage_id=str(row["feature_stage_id"] or ""),
|
||||
clarification_path=str(row["clarification_path"] or ""),
|
||||
requirements_contract_path=str(row["requirements_contract_path"] or ""),
|
||||
)
|
||||
@@ -413,7 +446,7 @@ class SqliteTaskRepository:
|
||||
def _pending_payload(pending: PendingAction | None) -> dict[str, Any] | None:
|
||||
if pending is None:
|
||||
return None
|
||||
return {"action_id": pending.action_id, "working_head": pending.working_head, "intent": pending.intent, "requirement_ids": list(pending.requirement_ids), "atomic_id": pending.atomic_id, "expected_change": pending.expected_change, "contract_hash": pending.contract_hash, "idempotency_key": pending.idempotency_key}
|
||||
return {"action_id": pending.action_id, "working_head": pending.working_head, "intent": pending.intent, "requirement_ids": list(pending.requirement_ids), "atomic_id": pending.atomic_id, "expected_change": pending.expected_change, "contract_hash": pending.contract_hash, "idempotency_key": pending.idempotency_key, "node_id": pending.node_id, "plan_hash": pending.plan_hash, "claim_ids": list(pending.claim_ids), "depends_on_node_ids": list(pending.depends_on_node_ids)}
|
||||
|
||||
@staticmethod
|
||||
def _invocation(row: sqlite3.Row) -> InvocationRecord:
|
||||
|
||||
Reference in New Issue
Block a user