优化生成流程
This commit is contained in:
@@ -6,22 +6,19 @@ The package exposes the CDSL-only rebuild API used by the backend Agent.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from .convert_to_cdsl import convert_sw_json_to_cdsl, write_cdsl_outputs
|
||||
from .llm_compiler import compile_cdsl
|
||||
from .llm_engine import run_engine_plan
|
||||
from .rebuild import compare_with_gold, compile_cdsl_to_pack, run_cdsl_only, run_engine, run_rebuild
|
||||
from .semantic_validation import validate_semantic_cdsl
|
||||
from .sketch_solver import SHAPE_GENERATORS, resolve_all_sketches, resolve_required_sketches
|
||||
from .sketch_solver import CORE_SHAPE_GENERATORS, resolve_all_sketches, resolve_required_sketches
|
||||
from .runtime import ALL_ATOMIC_IDS, EXECUTORS, RuntimeExecutionError, analyze_cdsl, rebuild_cdsl
|
||||
from .design_intent import DesignIntentError, design_intent_from_cdsl, validate_design_intent, validate_intent_cdsl
|
||||
|
||||
# The package-level runtime contract is the session executor registry. The
|
||||
# older llm_engine dispatcher remains available only for legacy engine packs.
|
||||
SUPPORTED_ATOMIC_IDS = ALL_ATOMIC_IDS
|
||||
SHAPE_GENERATORS = CORE_SHAPE_GENERATORS
|
||||
|
||||
__all__ = [
|
||||
"convert_sw_json_to_cdsl",
|
||||
"write_cdsl_outputs",
|
||||
"compile_cdsl",
|
||||
"compile_cdsl_to_pack",
|
||||
"run_engine_plan",
|
||||
@@ -31,6 +28,7 @@ __all__ = [
|
||||
"compare_with_gold",
|
||||
"resolve_all_sketches",
|
||||
"resolve_required_sketches",
|
||||
"CORE_SHAPE_GENERATORS",
|
||||
"SHAPE_GENERATORS",
|
||||
"SUPPORTED_ATOMIC_IDS",
|
||||
"validate_semantic_cdsl",
|
||||
@@ -39,10 +37,6 @@ __all__ = [
|
||||
"rebuild_cdsl",
|
||||
"analyze_cdsl",
|
||||
"RuntimeExecutionError",
|
||||
"DesignIntentError",
|
||||
"validate_design_intent",
|
||||
"validate_intent_cdsl",
|
||||
"design_intent_from_cdsl",
|
||||
]
|
||||
|
||||
__version__ = "1.0.0"
|
||||
|
||||
@@ -13,6 +13,7 @@ from collections import Counter
|
||||
from pathlib import Path
|
||||
from typing import Any, Iterable
|
||||
|
||||
from cdsl_importer.legacy_profile_adapter import lower_legacy_profiles
|
||||
from .semantic_validation import validate_semantic_cdsl
|
||||
|
||||
|
||||
@@ -225,7 +226,7 @@ def analyze_document(
|
||||
"failure_category": "preflight_blocked",
|
||||
}
|
||||
try:
|
||||
cdsl = json.loads(path.read_text(encoding="utf-8"))
|
||||
cdsl = lower_legacy_profiles(json.loads(path.read_text(encoding="utf-8")))
|
||||
report["part_id"] = str(cdsl.get("part_id") or report["part_id"])
|
||||
semantic = validate_semantic_cdsl(cdsl)
|
||||
report["semantic_valid"] = True
|
||||
|
||||
@@ -5,7 +5,7 @@ from __future__ import annotations
|
||||
import math
|
||||
from typing import Any, Iterable
|
||||
|
||||
from build123d import Axis, Edge, Face, Plane, Solid, Vector, Wire, export_step
|
||||
from build123d import Axis, Compound, Edge, Face, Plane, Solid, Vector, Wire, export_step
|
||||
|
||||
from .runtime_types import AxisSpec, HoleSpec, PlaneSpec, TopologyRecord, Vector3, canonical_plane_signature
|
||||
|
||||
@@ -201,13 +201,23 @@ class Build123dGeometryAdapter:
|
||||
def revolve(face: Face, angle_deg: float, axis: AxisSpec) -> Solid:
|
||||
return Solid.revolve(face, angle_deg, Build123dGeometryAdapter.axis(axis))
|
||||
|
||||
@staticmethod
|
||||
def _body_shape(value: Any) -> Any:
|
||||
"""Normalize boolean results, including disconnected ShapeList values."""
|
||||
if hasattr(value, "bounding_box"):
|
||||
return value
|
||||
shapes = list(value)
|
||||
if not shapes:
|
||||
raise ValueError("Boolean operation produced no shapes")
|
||||
return shapes[0] if len(shapes) == 1 else Compound(shapes)
|
||||
|
||||
@staticmethod
|
||||
def fuse(body: Any | None, solid: Solid) -> Any:
|
||||
return solid if body is None else body.fuse(solid)
|
||||
return solid if body is None else Build123dGeometryAdapter._body_shape(body.fuse(solid))
|
||||
|
||||
@staticmethod
|
||||
def cut(body: Any, tool: Any) -> Any:
|
||||
return body.cut(tool)
|
||||
return Build123dGeometryAdapter._body_shape(body.cut(tool))
|
||||
|
||||
@staticmethod
|
||||
def sphere(radius_mm: float, center_mm: Vector3) -> Solid:
|
||||
|
||||
@@ -258,24 +258,18 @@
|
||||
"analyticSegment": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"type": {"enum": ["line", "arc", "circle", "bspline"]},
|
||||
"type": {"enum": ["line", "arc", "circle"]},
|
||||
"start": {"$ref": "#/$defs/point2"},
|
||||
"end": {"$ref": "#/$defs/point2"},
|
||||
"center": {"$ref": "#/$defs/point2"},
|
||||
"radius_mm": {"$ref": "#/$defs/positive"},
|
||||
"clockwise": {"type": "boolean"},
|
||||
"degree": {"type": "integer", "minimum": 1},
|
||||
"control_points": {"type": "array", "items": {"$ref": "#/$defs/point2"}},
|
||||
"knots": {"type": "array", "items": {"$ref": "#/$defs/number"}},
|
||||
"weights": {"type": "array", "items": {"$ref": "#/$defs/positive"}},
|
||||
"periodic": {"type": "boolean"}
|
||||
"clockwise": {"type": "boolean"}
|
||||
},
|
||||
"required": ["type"],
|
||||
"allOf": [
|
||||
{"if": {"properties": {"type": {"const": "line"}}}, "then": {"required": ["start", "end"]}},
|
||||
{"if": {"properties": {"type": {"const": "arc"}}}, "then": {"required": ["start", "end", "center", "radius_mm"]}},
|
||||
{"if": {"properties": {"type": {"const": "circle"}}}, "then": {"required": ["center", "radius_mm"]}},
|
||||
{"if": {"properties": {"type": {"const": "bspline"}}}, "then": {"required": ["degree", "control_points", "knots"]}}
|
||||
{"if": {"properties": {"type": {"const": "circle"}}}, "then": {"required": ["center", "radius_mm"]}}
|
||||
],
|
||||
"additionalProperties": false
|
||||
},
|
||||
@@ -334,87 +328,11 @@
|
||||
{"if": {"properties": {"atomic_id": {"const": "hole_wizard"}}}, "then": {"properties": {"params": {"$ref": "#/$defs/holeWizardParams"}}}}
|
||||
]
|
||||
},
|
||||
"rectangleBounds": {
|
||||
"type": "object",
|
||||
"properties": {"center": {"$ref": "#/$defs/point2"}, "width_mm": {"$ref": "#/$defs/positive"}, "height_mm": {"$ref": "#/$defs/positive"}, "min_mm": {"$ref": "#/$defs/point2"}, "max_mm": {"$ref": "#/$defs/point2"}},
|
||||
"allOf": [
|
||||
{"oneOf": [
|
||||
{"required": ["center", "width_mm", "height_mm"], "not": {"anyOf": [{"required": ["min_mm"]}, {"required": ["max_mm"]}]}},
|
||||
{"required": ["min_mm", "max_mm"], "not": {"anyOf": [{"required": ["center"]}, {"required": ["width_mm"]}, {"required": ["height_mm"]}]}}
|
||||
]}
|
||||
]
|
||||
},
|
||||
"rectangleBoundary": {
|
||||
"type": "object",
|
||||
"properties": {"type": {"enum": ["rectangle", "rectangle_with_fillets"]}, "center": {"$ref": "#/$defs/point2"}, "width_mm": {"$ref": "#/$defs/positive"}, "height_mm": {"$ref": "#/$defs/positive"}, "min_mm": {"$ref": "#/$defs/point2"}, "max_mm": {"$ref": "#/$defs/point2"}, "fillet_radius_mm": {"$ref": "#/$defs/positive"}},
|
||||
"allOf": [{"$ref": "#/$defs/rectangleBounds"}],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"profile_type": {"enum": ["circle", "annulus", "circles", "circle_grid", "rectangle", "rectangle_with_circles", "rectangle_with_fillets", "obround", "polygon", "ibone", "rectangle_with_symmetric_notches", "revolve_chamfer", "revolve_chamfer_slanted", "circle_with_arc_notches", "circular_sector_slot", "circle_with_radial_tabs", "filleted_rect_side_slots", "d_shape", "partial_ring", "partial_ring_with_arc_island", "radial_slot", "arc_chain", "patterned_cutouts", "compound_patterned_cutouts", "analytic_contours"]},
|
||||
"motif_type": {"enum": ["circle", "square", "rectangle", "obround", "cross", "d_shape_polygon", "regular_hexagon", "skew_hexagon", "triangle", "teardrop_polygon", "trapezoid", "annular_sector_polygon"]},
|
||||
"layout_type": {"enum": ["ring", "angular", "concentric_rings", "disc_grid", "open_arc", "spiral", "cross_lines", "x_field", "twin_strips", "corner_clusters", "diamond_field"]},
|
||||
"motif": {
|
||||
"oneOf": [
|
||||
{"type": "object", "properties": {"type": {"const": "circle"}, "radius_mm": {"$ref": "#/$defs/positive"}}, "required": ["type", "radius_mm"], "additionalProperties": false},
|
||||
{"type": "object", "properties": {"type": {"const": "square"}, "width_mm": {"$ref": "#/$defs/positive"}}, "required": ["type", "width_mm"], "additionalProperties": false},
|
||||
{"type": "object", "properties": {"type": {"const": "rectangle"}, "width_mm": {"$ref": "#/$defs/positive"}, "height_mm": {"$ref": "#/$defs/positive"}}, "required": ["type", "width_mm", "height_mm"], "additionalProperties": false},
|
||||
{"type": "object", "properties": {"type": {"const": "obround"}, "length_mm": {"$ref": "#/$defs/positive"}, "width_mm": {"$ref": "#/$defs/positive"}}, "required": ["type", "length_mm", "width_mm"], "additionalProperties": false},
|
||||
{"type": "object", "properties": {"type": {"const": "cross"}, "size_mm": {"$ref": "#/$defs/positive"}, "arm_width_mm": {"$ref": "#/$defs/positive"}}, "required": ["type", "size_mm", "arm_width_mm"], "additionalProperties": false},
|
||||
{"type": "object", "properties": {"type": {"const": "d_shape_polygon"}, "stem_length_mm": {"$ref": "#/$defs/positive"}, "nose_depth_mm": {"$ref": "#/$defs/positive"}, "half_height_mm": {"$ref": "#/$defs/positive"}, "arc_segments": {"$ref": "#/$defs/positiveInteger"}}, "required": ["type", "stem_length_mm", "nose_depth_mm", "half_height_mm"], "additionalProperties": false},
|
||||
{"type": "object", "properties": {"type": {"const": "regular_hexagon"}, "radius_mm": {"$ref": "#/$defs/positive"}}, "required": ["type", "radius_mm"], "additionalProperties": false},
|
||||
{"type": "object", "properties": {"type": {"const": "skew_hexagon"}, "nominal_radius_mm": {"$ref": "#/$defs/positive"}}, "required": ["type", "nominal_radius_mm"], "additionalProperties": false},
|
||||
{"type": "object", "properties": {"type": {"const": "triangle"}, "radius_mm": {"$ref": "#/$defs/positive"}}, "required": ["type", "radius_mm"], "additionalProperties": false},
|
||||
{"type": "object", "properties": {"type": {"const": "teardrop_polygon"}, "width_mm": {"$ref": "#/$defs/positive"}, "height_mm": {"$ref": "#/$defs/positive"}, "shoulder_fraction": {"type": "number", "exclusiveMinimum": 0, "exclusiveMaximum": 1}, "left_width_mm": {"$ref": "#/$defs/positive"}, "right_width_mm": {"$ref": "#/$defs/positive"}, "tip_height_mm": {"$ref": "#/$defs/positive"}, "bottom_depth_mm": {"$ref": "#/$defs/positive"}, "shoulder_height_mm": {"$ref": "#/$defs/positive"}}, "required": ["type"], "oneOf": [{"required": ["width_mm", "height_mm"], "not": {"anyOf": [{"required": ["left_width_mm"]}, {"required": ["right_width_mm"]}, {"required": ["tip_height_mm"]}, {"required": ["bottom_depth_mm"]}, {"required": ["shoulder_height_mm"]}]}}, {"required": ["left_width_mm", "right_width_mm", "tip_height_mm", "bottom_depth_mm", "shoulder_height_mm"], "not": {"anyOf": [{"required": ["width_mm"]}, {"required": ["height_mm"]}, {"required": ["shoulder_fraction"]}]}}], "additionalProperties": false},
|
||||
{"type": "object", "properties": {"type": {"const": "trapezoid"}, "bottom_width_mm": {"$ref": "#/$defs/positive"}, "top_width_mm": {"$ref": "#/$defs/positive"}, "height_mm": {"$ref": "#/$defs/positive"}}, "required": ["type", "bottom_width_mm", "top_width_mm", "height_mm"], "additionalProperties": false},
|
||||
{"type": "object", "properties": {"type": {"const": "annular_sector_polygon"}, "inner_radius_mm": {"$ref": "#/$defs/positive"}, "outer_radius_mm": {"$ref": "#/$defs/positive"}, "half_angle_deg": {"$ref": "#/$defs/positive"}, "arc_segments": {"$ref": "#/$defs/positiveInteger"}}, "required": ["type", "inner_radius_mm", "outer_radius_mm", "half_angle_deg"], "additionalProperties": false}
|
||||
]
|
||||
},
|
||||
"layoutCommon": {
|
||||
"type": "object",
|
||||
"properties": {"orientation": {"enum": ["fixed", "radial", "tangential", "snapped_radial", "diagonal_axes"]}, "orientation_offset_deg": {"$ref": "#/$defs/number"}, "orientation_snap_deg": {"$ref": "#/$defs/positive"}}
|
||||
},
|
||||
"layout": {
|
||||
"oneOf": [
|
||||
{"type": "object", "properties": {"type": {"const": "ring"}, "radius_mm": {"$ref": "#/$defs/positive"}, "count": {"$ref": "#/$defs/positiveInteger"}, "start_angle_deg": {"$ref": "#/$defs/number"}, "angle_step_deg": {"$ref": "#/$defs/number"}, "orientation": {"enum": ["fixed", "radial", "tangential", "snapped_radial", "diagonal_axes"]}, "orientation_offset_deg": {"$ref": "#/$defs/number"}, "orientation_snap_deg": {"$ref": "#/$defs/positive"}}, "required": ["type", "radius_mm", "count"], "additionalProperties": false},
|
||||
{"type": "object", "properties": {"type": {"const": "angular"}, "radius_mm": {"$ref": "#/$defs/number"}, "count": {"$ref": "#/$defs/positiveInteger"}, "start_angle_deg": {"$ref": "#/$defs/number"}, "angle_step_deg": {"$ref": "#/$defs/number"}, "orientation": {"enum": ["fixed", "radial", "tangential", "snapped_radial", "diagonal_axes"]}, "orientation_offset_deg": {"$ref": "#/$defs/number"}, "orientation_snap_deg": {"$ref": "#/$defs/positive"}}, "required": ["type", "count"], "additionalProperties": false},
|
||||
{"type": "object", "properties": {"type": {"const": "concentric_rings"}, "rings": {"type": "array", "minItems": 1, "items": {"type": "object", "properties": {"radius_mm": {"$ref": "#/$defs/positive"}, "count": {"$ref": "#/$defs/positiveInteger"}, "start_angle_deg": {"$ref": "#/$defs/number"}, "angle_step_deg": {"$ref": "#/$defs/number"}}, "required": ["radius_mm", "count"], "additionalProperties": false}}, "orientation": {"enum": ["fixed", "radial", "tangential", "snapped_radial", "diagonal_axes"]}, "orientation_offset_deg": {"$ref": "#/$defs/number"}, "orientation_snap_deg": {"$ref": "#/$defs/positive"}}, "required": ["type", "rings"], "additionalProperties": false},
|
||||
{"type": "object", "properties": {"type": {"const": "disc_grid"}, "count_x": {"$ref": "#/$defs/positiveInteger"}, "count_y": {"$ref": "#/$defs/positiveInteger"}, "spacing_x_mm": {"$ref": "#/$defs/positive"}, "spacing_y_mm": {"$ref": "#/$defs/positive"}, "center_mm": {"$ref": "#/$defs/point2"}, "max_center_radius_mm": {"$ref": "#/$defs/positive"}, "orientation_offset_deg": {"$ref": "#/$defs/number"}}, "required": ["type", "count_x", "count_y", "spacing_x_mm", "spacing_y_mm"], "additionalProperties": false},
|
||||
{"type": "object", "properties": {"type": {"const": "open_arc"}, "radius_mm": {"$ref": "#/$defs/positive"}, "count": {"$ref": "#/$defs/positiveInteger"}, "start_angle_deg": {"$ref": "#/$defs/number"}, "end_angle_deg": {"$ref": "#/$defs/number"}, "orientation": {"enum": ["fixed", "radial", "tangential", "snapped_radial", "diagonal_axes"]}, "orientation_offset_deg": {"$ref": "#/$defs/number"}, "orientation_snap_deg": {"$ref": "#/$defs/positive"}}, "required": ["type", "radius_mm", "count", "start_angle_deg", "end_angle_deg"], "additionalProperties": false},
|
||||
{"type": "object", "properties": {"type": {"const": "spiral"}, "count": {"$ref": "#/$defs/positiveInteger"}, "start_radius_mm": {"$ref": "#/$defs/positive"}, "radius_step_mm": {"$ref": "#/$defs/number"}, "start_angle_deg": {"$ref": "#/$defs/number"}, "angle_step_deg": {"$ref": "#/$defs/number"}, "orientation": {"enum": ["fixed", "radial", "tangential", "snapped_radial", "diagonal_axes"]}, "orientation_offset_deg": {"$ref": "#/$defs/number"}, "orientation_snap_deg": {"$ref": "#/$defs/positive"}}, "required": ["type", "count", "start_radius_mm", "radius_step_mm", "angle_step_deg"], "additionalProperties": false},
|
||||
{"type": "object", "properties": {"type": {"const": "cross_lines"}, "count_per_axis": {"$ref": "#/$defs/positiveInteger"}, "spacing_mm": {"$ref": "#/$defs/positive"}, "orientation_offset_deg": {"$ref": "#/$defs/number"}}, "required": ["type", "count_per_axis", "spacing_mm"], "additionalProperties": false},
|
||||
{"type": "object", "properties": {"type": {"const": "x_field"}, "levels": {"$ref": "#/$defs/positiveInteger"}, "spacing_mm": {"$ref": "#/$defs/positive"}, "orientation": {"enum": ["fixed", "radial", "tangential", "snapped_radial", "diagonal_axes"]}, "orientation_offset_deg": {"$ref": "#/$defs/number"}, "orientation_snap_deg": {"$ref": "#/$defs/positive"}}, "required": ["type", "levels", "spacing_mm"], "additionalProperties": false},
|
||||
{"type": "object", "properties": {"type": {"const": "twin_strips"}, "x_offset_mm": {"$ref": "#/$defs/positive"}, "count_y": {"$ref": "#/$defs/positiveInteger"}, "y_start_mm": {"$ref": "#/$defs/number"}, "y_end_mm": {"$ref": "#/$defs/number"}, "orientation_offset_deg": {"$ref": "#/$defs/number"}}, "required": ["type", "x_offset_mm", "count_y", "y_start_mm", "y_end_mm"], "additionalProperties": false},
|
||||
{"type": "object", "properties": {"type": {"const": "corner_clusters"}, "levels_mm": {"type": "array", "minItems": 1, "items": {"$ref": "#/$defs/positive"}}, "orientation_offset_deg": {"$ref": "#/$defs/number"}}, "required": ["type", "levels_mm"], "additionalProperties": false},
|
||||
{"type": "object", "properties": {"type": {"const": "diamond_field"}, "manhattan_radius": {"type": "integer", "minimum": 0}, "spacing_mm": {"$ref": "#/$defs/positive"}, "orientation_offset_deg": {"$ref": "#/$defs/number"}}, "required": ["type", "manhattan_radius", "spacing_mm"], "additionalProperties": false}
|
||||
]
|
||||
},
|
||||
"pattern": {"type": "object", "properties": {"motif": {"$ref": "#/$defs/motif"}, "layout": {"$ref": "#/$defs/layout"}}, "required": ["motif", "layout"], "additionalProperties": false},
|
||||
"profile_type": {"enum": ["circle", "polygon", "analytic_contours"]},
|
||||
"profile": {
|
||||
"oneOf": [
|
||||
{"type": "object", "properties": {"type": {"const": "circle"}, "center": {"$ref": "#/$defs/point2"}, "radius_mm": {"$ref": "#/$defs/positive"}}, "required": ["type", "radius_mm"], "additionalProperties": false},
|
||||
{"type": "object", "properties": {"type": {"const": "annulus"}, "center": {"$ref": "#/$defs/point2"}, "inner_radius_mm": {"$ref": "#/$defs/positive"}, "outer_radius_mm": {"$ref": "#/$defs/positive"}}, "required": ["type", "inner_radius_mm", "outer_radius_mm"], "additionalProperties": false},
|
||||
{"type": "object", "properties": {"type": {"const": "circles"}, "items": {"type": "array", "minItems": 1, "items": {"$ref": "#/$defs/circleItem"}}}, "required": ["type", "items"], "additionalProperties": false},
|
||||
{"type": "object", "properties": {"type": {"const": "circle_grid"}, "radius_mm": {"$ref": "#/$defs/positive"}, "count_x": {"$ref": "#/$defs/positiveInteger"}, "count_y": {"$ref": "#/$defs/positiveInteger"}, "spacing_x_mm": {"$ref": "#/$defs/positive"}, "spacing_y_mm": {"$ref": "#/$defs/positive"}, "origin_mm": {"$ref": "#/$defs/point2"}, "center_mm": {"$ref": "#/$defs/point2"}}, "required": ["type", "radius_mm", "count_x", "count_y", "spacing_x_mm", "spacing_y_mm"], "additionalProperties": false, "oneOf": [{"required": ["origin_mm"], "not": {"required": ["center_mm"]}}, {"required": ["center_mm"], "not": {"required": ["origin_mm"]}}]},
|
||||
{"type": "object", "properties": {"type": {"const": "rectangle"}, "center": {"$ref": "#/$defs/point2"}, "width_mm": {"$ref": "#/$defs/positive"}, "height_mm": {"$ref": "#/$defs/positive"}, "min_mm": {"$ref": "#/$defs/point2"}, "max_mm": {"$ref": "#/$defs/point2"}}, "required": ["type"], "allOf": [{"$ref": "#/$defs/rectangleBounds"}], "additionalProperties": false},
|
||||
{"type": "object", "properties": {"type": {"const": "rectangle_with_circles"}, "boundary": {"$ref": "#/$defs/rectangleBoundary"}, "circles": {"type": "array", "items": {"$ref": "#/$defs/circleItem"}}}, "required": ["type", "boundary"], "additionalProperties": false},
|
||||
{"type": "object", "properties": {"type": {"const": "rectangle_with_fillets"}, "center": {"$ref": "#/$defs/point2"}, "width_mm": {"$ref": "#/$defs/positive"}, "height_mm": {"$ref": "#/$defs/positive"}, "min_mm": {"$ref": "#/$defs/point2"}, "max_mm": {"$ref": "#/$defs/point2"}, "fillet_radius_mm": {"$ref": "#/$defs/positive"}, "circles": {"type": "array", "items": {"$ref": "#/$defs/circleItem"}}}, "required": ["type"], "allOf": [{"$ref": "#/$defs/rectangleBounds"}], "additionalProperties": false},
|
||||
{"type": "object", "properties": {"type": {"const": "obround"}, "center": {"$ref": "#/$defs/point2"}, "length_mm": {"$ref": "#/$defs/positive"}, "width_mm": {"$ref": "#/$defs/positive"}}, "required": ["type", "length_mm", "width_mm"], "additionalProperties": false},
|
||||
{"type": "object", "properties": {"type": {"const": "polygon"}, "vertices": {"type": "array", "minItems": 3, "items": {"$ref": "#/$defs/point2"}}}, "required": ["type", "vertices"], "additionalProperties": false},
|
||||
{"type": "object", "properties": {"type": {"const": "ibone"}, "body_width_mm": {"$ref": "#/$defs/positive"}, "body_height_mm": {"$ref": "#/$defs/positive"}, "flange_width_mm": {"$ref": "#/$defs/positive"}, "flange_height_mm": {"$ref": "#/$defs/positive"}, "corner_radius_mm": {"$ref": "#/$defs/positive"}, "hole_radius_mm": {"$ref": "#/$defs/positive"}}, "required": ["type", "body_width_mm", "body_height_mm", "flange_width_mm", "flange_height_mm", "corner_radius_mm"], "additionalProperties": false},
|
||||
{"type": "object", "properties": {"type": {"const": "rectangle_with_symmetric_notches"}, "width_mm": {"$ref": "#/$defs/positive"}, "height_mm": {"$ref": "#/$defs/positive"}, "notch": {"type": "object", "properties": {"y_start": {"$ref": "#/$defs/number"}, "y_end": {"$ref": "#/$defs/number"}, "depth_mm": {"$ref": "#/$defs/positive"}, "inner_radius_mm": {"$ref": "#/$defs/positive"}, "corner_radius_mm": {"$ref": "#/$defs/positive"}}, "required": ["y_start", "y_end", "depth_mm"], "additionalProperties": false}}, "required": ["type", "width_mm", "height_mm", "notch"], "additionalProperties": false},
|
||||
{"type": "object", "properties": {"type": {"const": "revolve_chamfer"}, "axis_height_mm": {"$ref": "#/$defs/positive"}, "top_width_mm": {"$ref": "#/$defs/positive"}, "bottom_width_mm": {"$ref": "#/$defs/positive"}, "wall_inset_mm": {"$ref": "#/$defs/number"}, "step_inset_mm": {"$ref": "#/$defs/number"}, "on_axis_side": {"enum": ["left", "right"]}}, "required": ["type", "axis_height_mm", "top_width_mm", "bottom_width_mm"], "additionalProperties": false},
|
||||
{"type": "object", "properties": {"type": {"const": "revolve_chamfer_slanted"}, "axis_height_mm": {"$ref": "#/$defs/positive"}, "top_width_mm": {"$ref": "#/$defs/positive"}, "wall_inset_mm": {"$ref": "#/$defs/number"}, "wall_height_mm": {"$ref": "#/$defs/positive"}, "wall_width_mm": {"$ref": "#/$defs/positive"}, "on_axis_side": {"enum": ["left", "right"]}}, "required": ["type", "axis_height_mm", "top_width_mm", "wall_height_mm", "wall_width_mm"], "additionalProperties": false},
|
||||
{"type": "object", "properties": {"type": {"const": "circle_with_arc_notches"}, "outer_radius_mm": {"$ref": "#/$defs/positive"}, "notch_radius_mm": {"$ref": "#/$defs/positive"}, "notch_angles_deg": {"type": "array", "minItems": 1, "items": {"$ref": "#/$defs/number"}}, "circles": {"type": "array", "items": {"$ref": "#/$defs/circleItem"}}}, "required": ["type", "outer_radius_mm", "notch_radius_mm"], "additionalProperties": false},
|
||||
{"type": "object", "properties": {"type": {"const": "circular_sector_slot"}, "arc_radius_mm": {"$ref": "#/$defs/positive"}, "slot_half_width_mm": {"$ref": "#/$defs/positive"}, "chord_half_mm": {"$ref": "#/$defs/positive"}, "circles": {"type": "array", "items": {"$ref": "#/$defs/circleItem"}}}, "required": ["type", "arc_radius_mm", "slot_half_width_mm", "chord_half_mm"], "additionalProperties": false},
|
||||
{"type": "object", "properties": {"type": {"const": "circle_with_radial_tabs"}, "outer_radius_mm": {"$ref": "#/$defs/positive"}, "tab_u_half_mm": {"$ref": "#/$defs/positive"}, "tab_v_offset_mm": {"$ref": "#/$defs/number"}, "circles": {"type": "array", "items": {"$ref": "#/$defs/circleItem"}}}, "required": ["type", "outer_radius_mm"], "additionalProperties": false},
|
||||
{"type": "object", "properties": {"type": {"const": "filleted_rect_side_slots"}, "half_width_mm": {"$ref": "#/$defs/positive"}, "half_height_mm": {"$ref": "#/$defs/positive"}, "corner_radius_mm": {"$ref": "#/$defs/positive"}, "slot_radius_mm": {"$ref": "#/$defs/positive"}, "circles": {"type": "array", "items": {"$ref": "#/$defs/circleItem"}}}, "required": ["type", "half_width_mm", "half_height_mm", "corner_radius_mm"], "additionalProperties": false},
|
||||
{"type": "object", "properties": {"type": {"const": "d_shape"}, "radius_mm": {"$ref": "#/$defs/positive"}, "chord_sign": {"enum": ["left", "right"]}, "chord_x_mm": {"$ref": "#/$defs/positive"}, "circles": {"type": "array", "items": {"$ref": "#/$defs/circleItem"}}}, "required": ["type", "radius_mm"], "additionalProperties": false},
|
||||
{"type": "object", "properties": {"type": {"const": "partial_ring"}, "inner_radius_mm": {"$ref": "#/$defs/positive"}, "outer_radius_mm": {"$ref": "#/$defs/positive"}, "half_angle_deg": {"$ref": "#/$defs/positive"}, "circles": {"type": "array", "items": {"$ref": "#/$defs/circleItem"}}}, "required": ["type", "inner_radius_mm", "outer_radius_mm"], "additionalProperties": false},
|
||||
{"type": "object", "properties": {"type": {"const": "partial_ring_with_arc_island"}, "inner_radius_mm": {"$ref": "#/$defs/positive"}, "outer_radius_mm": {"$ref": "#/$defs/positive"}, "half_angle_deg": {"$ref": "#/$defs/positive"}, "island_radius_mm": {"$ref": "#/$defs/positive"}, "island_gap_mm": {"$ref": "#/$defs/positive"}, "center_angles_deg": {"type": "array", "minItems": 1, "items": {"$ref": "#/$defs/number"}}, "replicas": {"type": "array", "minItems": 1, "items": {"type": "object", "properties": {"center_angle_deg": {"$ref": "#/$defs/number"}}, "required": ["center_angle_deg"], "additionalProperties": false}}}, "required": ["type", "inner_radius_mm", "outer_radius_mm", "island_radius_mm"], "additionalProperties": false, "oneOf": [{"required": ["center_angles_deg"], "not": {"required": ["replicas"]}}, {"required": ["replicas"], "not": {"required": ["center_angles_deg"]}}]},
|
||||
{"type": "object", "properties": {"type": {"const": "radial_slot"}, "inner_radius_mm": {"$ref": "#/$defs/positive"}, "outer_radius_mm": {"$ref": "#/$defs/positive"}, "start_angle_deg": {"$ref": "#/$defs/number"}, "end_angle_deg": {"$ref": "#/$defs/number"}, "circles": {"type": "array", "items": {"$ref": "#/$defs/circleItem"}}}, "required": ["type", "inner_radius_mm", "outer_radius_mm", "start_angle_deg", "end_angle_deg"], "additionalProperties": false},
|
||||
{"type": "object", "properties": {"type": {"const": "arc_chain"}, "arcs": {"type": "array", "minItems": 2, "items": {"type": "object", "properties": {"center": {"$ref": "#/$defs/point2"}, "radius_mm": {"$ref": "#/$defs/positive"}, "start_angle_deg": {"$ref": "#/$defs/number"}, "end_angle_deg": {"$ref": "#/$defs/number"}}, "required": ["radius_mm", "start_angle_deg", "end_angle_deg"], "additionalProperties": false}}, "circles": {"type": "array", "items": {"$ref": "#/$defs/circleItem"}}}, "required": ["type", "arcs"], "additionalProperties": false},
|
||||
{"type": "object", "properties": {"type": {"const": "patterned_cutouts"}, "motif": {"$ref": "#/$defs/motif"}, "layout": {"$ref": "#/$defs/layout"}}, "required": ["type", "motif", "layout"], "additionalProperties": false},
|
||||
{"type": "object", "properties": {"type": {"const": "compound_patterned_cutouts"}, "patterns": {"type": "array", "minItems": 1, "items": {"$ref": "#/$defs/pattern"}}}, "required": ["type", "patterns"], "additionalProperties": false},
|
||||
{"$ref": "#/$defs/analyticProfile"}
|
||||
]
|
||||
},
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,331 +0,0 @@
|
||||
"""Validation for the semantic planning artifact between a request and CDSL."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from copy import deepcopy
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from jsonschema import Draft202012Validator
|
||||
from jsonschema.exceptions import SchemaError
|
||||
|
||||
|
||||
class DesignIntentError(ValueError):
|
||||
"""A plan-contract violation with a stable error code for the Agent."""
|
||||
|
||||
def __init__(self, code: str, message: str) -> None:
|
||||
super().__init__(message)
|
||||
self.code = code
|
||||
|
||||
|
||||
def _schema_path(engine: Any) -> Path:
|
||||
return Path(str(engine.__file__)).with_name("design_intent_schema.json")
|
||||
|
||||
|
||||
def load_design_intent_schema(engine: Any) -> dict[str, Any]:
|
||||
path = _schema_path(engine)
|
||||
try:
|
||||
schema = json.loads(path.read_text(encoding="utf-8"))
|
||||
Draft202012Validator.check_schema(schema)
|
||||
except (OSError, json.JSONDecodeError, SchemaError) as error:
|
||||
raise RuntimeError("The local DesignIntent JSON Schema is unavailable or invalid") from error
|
||||
return schema
|
||||
|
||||
|
||||
def _location(error: Any) -> str:
|
||||
return "$" + "".join(
|
||||
f"[{item}]" if isinstance(item, int) else f".{item}"
|
||||
for item in error.absolute_path
|
||||
)
|
||||
|
||||
|
||||
def _schema_validate(intent: dict[str, Any], engine: Any) -> None:
|
||||
errors = sorted(
|
||||
Draft202012Validator(load_design_intent_schema(engine)).iter_errors(intent),
|
||||
key=lambda error: (list(error.absolute_path), error.message),
|
||||
)
|
||||
if errors:
|
||||
error = errors[0]
|
||||
raise DesignIntentError(
|
||||
"INVALID_DESIGN_INTENT",
|
||||
f"DesignIntent schema violation at {_location(error)}: {error.message}",
|
||||
)
|
||||
|
||||
|
||||
def _runtime_capabilities(engine: Any) -> tuple[set[str], set[str], dict[str, dict[str, Any]]]:
|
||||
try:
|
||||
profile_schema = json.loads(Path(str(engine.__file__)).with_name("profile_schema.json").read_text(encoding="utf-8"))
|
||||
except (OSError, json.JSONDecodeError) as error:
|
||||
raise RuntimeError("The local CDSL profile schema is unavailable or invalid") from error
|
||||
atomic_ids = set(profile_schema.get("runtime_supported_atomic_ids") or getattr(engine, "SUPPORTED_ATOMIC_IDS", ()))
|
||||
profiles = {
|
||||
name
|
||||
for name, contract in (profile_schema.get("profiles") or {}).items()
|
||||
if isinstance(contract, dict) and contract.get("agent_allowed") is True
|
||||
}
|
||||
atomic_contracts = {
|
||||
str(atomic_id): contract
|
||||
for atomic_id, contract in (profile_schema.get("feature_atomic_ids") or {}).items()
|
||||
if isinstance(contract, dict)
|
||||
}
|
||||
return {str(item) for item in atomic_ids}, {str(item) for item in profiles}, atomic_contracts
|
||||
|
||||
|
||||
def _blocking(intent: dict[str, Any]) -> bool:
|
||||
questions = intent.get("open_questions") or []
|
||||
gaps = intent.get("capability_gaps") or []
|
||||
return any(isinstance(item, dict) and item.get("blocking") is True for item in [*questions, *gaps])
|
||||
|
||||
|
||||
def validate_design_intent(
|
||||
intent: dict[str, Any],
|
||||
engine: Any,
|
||||
*,
|
||||
current_revision_id: str = "",
|
||||
) -> dict[str, Any]:
|
||||
"""Validate DesignIntent syntax and semantic ordering against runtime capability."""
|
||||
if not isinstance(intent, dict):
|
||||
raise DesignIntentError("INVALID_DESIGN_INTENT", "DesignIntent must be a JSON object")
|
||||
_schema_validate(intent, engine)
|
||||
|
||||
normalized = deepcopy(intent)
|
||||
mode = str(normalized["mode"])
|
||||
base_revision_id = str(normalized["base_revision_id"] or "")
|
||||
if mode == "create" and base_revision_id:
|
||||
raise DesignIntentError("INVALID_DESIGN_INTENT", "create DesignIntent must not set base_revision_id")
|
||||
if mode == "revise":
|
||||
if not base_revision_id:
|
||||
raise DesignIntentError("INVALID_DESIGN_INTENT", "revise DesignIntent requires base_revision_id")
|
||||
if current_revision_id and base_revision_id != current_revision_id:
|
||||
raise DesignIntentError(
|
||||
"DESIGN_INTENT_BASE_REVISION_MISMATCH",
|
||||
f"revise DesignIntent base_revision_id must be {current_revision_id}, the current successful revision",
|
||||
)
|
||||
|
||||
structures = normalized["structures"]
|
||||
ids = [str(item["id"]) for item in structures]
|
||||
feature_ids = [str(item["cdsl_feature_id"]) for item in structures]
|
||||
if len(set(ids)) != len(ids):
|
||||
raise DesignIntentError("INVALID_DESIGN_INTENT", "DesignIntent structure ids must be unique")
|
||||
if len(set(feature_ids)) != len(feature_ids):
|
||||
raise DesignIntentError("INVALID_DESIGN_INTENT", "DesignIntent cdsl_feature_id values must be unique")
|
||||
|
||||
feature_order = [str(item) for item in normalized["feature_order"]]
|
||||
if len(set(feature_order)) != len(feature_order) or set(feature_order) != set(ids):
|
||||
raise DesignIntentError(
|
||||
"INVALID_DESIGN_INTENT",
|
||||
"feature_order must list every DesignIntent structure exactly once",
|
||||
)
|
||||
order = {structure_id: index for index, structure_id in enumerate(feature_order)}
|
||||
atomic_ids, profile_types, atomic_contracts = _runtime_capabilities(engine)
|
||||
for structure in structures:
|
||||
structure_id = str(structure["id"])
|
||||
for dependency in structure["depends_on"]:
|
||||
dependency_id = str(dependency)
|
||||
if dependency_id not in order:
|
||||
raise DesignIntentError(
|
||||
"INVALID_DESIGN_INTENT",
|
||||
f"Structure {structure_id} depends on an unknown structure {dependency_id}",
|
||||
)
|
||||
if order[dependency_id] >= order[structure_id]:
|
||||
raise DesignIntentError(
|
||||
"INVALID_DESIGN_INTENT",
|
||||
f"Structure {structure_id} must depend only on earlier feature_order entries",
|
||||
)
|
||||
strategy = structure["cdsl_strategy"]
|
||||
atomic_id = str(strategy["atomic_id"])
|
||||
if atomic_id not in atomic_ids:
|
||||
raise DesignIntentError(
|
||||
"INVALID_DESIGN_INTENT",
|
||||
f"Structure {structure_id} uses unsupported atomic_id {atomic_id}",
|
||||
)
|
||||
profile_type = str(strategy.get("profile_type") or "")
|
||||
if atomic_contracts.get(atomic_id, {}).get("requires_sketch") and not profile_type:
|
||||
raise DesignIntentError(
|
||||
"INVALID_DESIGN_INTENT",
|
||||
f"Structure {structure_id} uses {atomic_id} and requires a profile_type",
|
||||
)
|
||||
if profile_type and profile_type not in profile_types:
|
||||
raise DesignIntentError(
|
||||
"INVALID_DESIGN_INTENT",
|
||||
f"Structure {structure_id} uses unsupported or agent-disallowed profile_type {profile_type}",
|
||||
)
|
||||
|
||||
if normalized["status"] == "ready" and _blocking(normalized):
|
||||
raise DesignIntentError(
|
||||
"DESIGN_INTENT_BLOCKED",
|
||||
"A DesignIntent with blocking questions or capability gaps must need clarification",
|
||||
)
|
||||
if normalized["status"] == "needs_clarification" and not _blocking(normalized):
|
||||
raise DesignIntentError(
|
||||
"INVALID_DESIGN_INTENT",
|
||||
"needs_clarification requires a blocking question or capability gap",
|
||||
)
|
||||
if normalized["status"] == "ready" and not structures:
|
||||
raise DesignIntentError("INVALID_DESIGN_INTENT", "A ready DesignIntent needs at least one structure")
|
||||
if normalized["status"] == "ready" and mode == "create" and not any(item["role"] == "base" for item in structures):
|
||||
raise DesignIntentError("INVALID_DESIGN_INTENT", "A ready create DesignIntent needs a base structure")
|
||||
nonblocking_gaps = [item for item in normalized["capability_gaps"] if not item["blocking"]]
|
||||
if nonblocking_gaps and not normalized["assumptions"]:
|
||||
raise DesignIntentError(
|
||||
"INVALID_DESIGN_INTENT",
|
||||
"Non-blocking capability gaps require an explicit approximation assumption",
|
||||
)
|
||||
for gap in normalized["capability_gaps"]:
|
||||
if str(gap["structure_id"]) not in set(ids):
|
||||
raise DesignIntentError(
|
||||
"INVALID_DESIGN_INTENT",
|
||||
f"Capability gap references an unknown structure {gap['structure_id']}",
|
||||
)
|
||||
valid_expectation_ids = set(ids) | set(feature_ids)
|
||||
for expectation in normalized["verification_expectations"]:
|
||||
if expectation["type"] == "feature_count" and str(expectation["feature_id"]) not in valid_expectation_ids:
|
||||
raise DesignIntentError(
|
||||
"INVALID_DESIGN_INTENT",
|
||||
f"feature_count expectation references an unknown structure or CDSL feature {expectation['feature_id']}",
|
||||
)
|
||||
return normalized
|
||||
|
||||
|
||||
def _has_selector_evidence(value: Any, role: str) -> bool:
|
||||
if isinstance(value, dict):
|
||||
for key, child in value.items():
|
||||
if role in {"selector", "selectors"} and key == "selectors" and isinstance(child, list) and child:
|
||||
return True
|
||||
if key == role and child not in (None, "", [], {}):
|
||||
if not (isinstance(child, dict) and set(child) == {"unresolved"}):
|
||||
return True
|
||||
if _has_selector_evidence(child, role):
|
||||
return True
|
||||
elif isinstance(value, list):
|
||||
return any(_has_selector_evidence(item, role) for item in value)
|
||||
return False
|
||||
|
||||
|
||||
def validate_intent_cdsl(
|
||||
intent: dict[str, Any],
|
||||
cdsl: dict[str, Any],
|
||||
engine: Any,
|
||||
*,
|
||||
current_cdsl: dict[str, Any] | None = None,
|
||||
) -> None:
|
||||
"""Ensure an executable CDSL document faithfully realizes one accepted plan."""
|
||||
normalized = validate_design_intent(intent, engine)
|
||||
if normalized["status"] != "ready":
|
||||
raise DesignIntentError("DESIGN_INTENT_BLOCKED", "CDSL generation is blocked until the DesignIntent is ready")
|
||||
if not isinstance(cdsl, dict):
|
||||
raise DesignIntentError("INTENT_CDSL_MISMATCH", "CDSL must be a JSON object")
|
||||
features = cdsl.get("features")
|
||||
sketches = (cdsl.get("geometry") or {}).get("sketches")
|
||||
if not isinstance(features, list) or not isinstance(sketches, list):
|
||||
raise DesignIntentError("INTENT_CDSL_MISMATCH", "CDSL must contain features and sketches")
|
||||
feature_by_id = {str(feature.get("id") or ""): feature for feature in features if isinstance(feature, dict)}
|
||||
if len(feature_by_id) != len(features):
|
||||
raise DesignIntentError("INTENT_CDSL_MISMATCH", "CDSL features must have unique ids")
|
||||
structure_by_id = {str(item["id"]): item for item in normalized["structures"]}
|
||||
expected_feature_ids = [str(structure_by_id[item]["cdsl_feature_id"]) for item in normalized["feature_order"]]
|
||||
if normalized["mode"] == "revise" and isinstance(current_cdsl, dict):
|
||||
base_feature_ids = {
|
||||
str(feature.get("id") or "")
|
||||
for feature in current_cdsl.get("features") or []
|
||||
if isinstance(feature, dict) and feature.get("id")
|
||||
}
|
||||
if not base_feature_ids.issubset(set(expected_feature_ids)):
|
||||
missing = ", ".join(sorted(base_feature_ids - set(expected_feature_ids)))
|
||||
raise DesignIntentError(
|
||||
"INTENT_CDSL_MISMATCH",
|
||||
f"revise DesignIntent must explicitly preserve current features: {missing}",
|
||||
)
|
||||
actual_feature_ids = [str(feature.get("id") or "") for feature in features]
|
||||
if actual_feature_ids != expected_feature_ids:
|
||||
raise DesignIntentError(
|
||||
"INTENT_CDSL_MISMATCH",
|
||||
"CDSL feature ids and order must exactly match DesignIntent feature_order",
|
||||
)
|
||||
sketch_by_id = {str(sketch.get("id") or ""): sketch for sketch in sketches if isinstance(sketch, dict)}
|
||||
feature_for_structure = {
|
||||
str(structure["id"]): feature_by_id[str(structure["cdsl_feature_id"])]
|
||||
for structure in normalized["structures"]
|
||||
}
|
||||
for structure in normalized["structures"]:
|
||||
structure_id = str(structure["id"])
|
||||
feature = feature_for_structure[structure_id]
|
||||
strategy = structure["cdsl_strategy"]
|
||||
if feature.get("atomic_id") != strategy["atomic_id"]:
|
||||
raise DesignIntentError(
|
||||
"INTENT_CDSL_MISMATCH",
|
||||
f"CDSL feature {feature.get('id')} atomic_id does not match structure {structure_id}",
|
||||
)
|
||||
expected_dependencies = {
|
||||
str(structure_by_id[dependency]["cdsl_feature_id"])
|
||||
for dependency in structure["depends_on"]
|
||||
}
|
||||
actual_dependencies = {str(item) for item in feature.get("depends_on") or []}
|
||||
if not expected_dependencies.issubset(actual_dependencies):
|
||||
raise DesignIntentError(
|
||||
"INTENT_CDSL_MISMATCH",
|
||||
f"CDSL feature {feature.get('id')} is missing planned dependencies",
|
||||
)
|
||||
profile_type = str(strategy.get("profile_type") or "")
|
||||
if profile_type:
|
||||
sketch_id = str(feature.get("sketch_id") or "")
|
||||
sketch = sketch_by_id.get(sketch_id)
|
||||
actual_profile_type = str(((sketch or {}).get("profile") or {}).get("type") or "")
|
||||
if actual_profile_type != profile_type:
|
||||
raise DesignIntentError(
|
||||
"INTENT_CDSL_MISMATCH",
|
||||
f"CDSL feature {feature.get('id')} must use profile_type {profile_type}",
|
||||
)
|
||||
for selector_role in strategy["selector_roles"]:
|
||||
if not _has_selector_evidence(feature, str(selector_role)):
|
||||
raise DesignIntentError(
|
||||
"INTENT_CDSL_MISMATCH",
|
||||
f"CDSL feature {feature.get('id')} is missing selector evidence for {selector_role}",
|
||||
)
|
||||
|
||||
|
||||
def design_intent_from_cdsl(cdsl: dict[str, Any], *, request: str, mode: str = "create", base_revision_id: str = "") -> dict[str, Any]:
|
||||
"""Create a conservative audit-only plan for legacy revisions when needed."""
|
||||
sketches = (cdsl.get("geometry") or {}).get("sketches") or []
|
||||
profile_by_sketch_id = {
|
||||
str(sketch.get("id") or ""): str((sketch.get("profile") or {}).get("type") or "")
|
||||
for sketch in sketches
|
||||
if isinstance(sketch, dict)
|
||||
}
|
||||
structures: list[dict[str, Any]] = []
|
||||
for index, feature in enumerate(cdsl.get("features") or []):
|
||||
if not isinstance(feature, dict):
|
||||
continue
|
||||
feature_id = str(feature.get("id") or f"feature_{index + 1}")
|
||||
atomic_id = str(feature.get("atomic_id") or "")
|
||||
profile_type = profile_by_sketch_id.get(str(feature.get("sketch_id") or ""), "")
|
||||
strategy: dict[str, Any] = {
|
||||
"atomic_id": atomic_id,
|
||||
"parameter_roles": sorted(str(key) for key in (feature.get("params") or {}).keys()),
|
||||
"selector_roles": [],
|
||||
}
|
||||
if profile_type:
|
||||
strategy["profile_type"] = profile_type
|
||||
structures.append({
|
||||
"id": feature_id,
|
||||
"cdsl_feature_id": feature_id,
|
||||
"role": "base" if index == 0 else "subtractive" if "cut" in atomic_id or atomic_id.startswith("hole") else "additive",
|
||||
"purpose": str(feature.get("name") or feature_id),
|
||||
"depends_on": [str(item) for item in feature.get("depends_on") or []],
|
||||
"cdsl_strategy": strategy,
|
||||
})
|
||||
return {
|
||||
"schema": "cad.cdsl.design-intent.v1",
|
||||
"schema_version": "1.0",
|
||||
"mode": mode,
|
||||
"request": request,
|
||||
"base_revision_id": base_revision_id,
|
||||
"structures": structures,
|
||||
"feature_order": [item["id"] for item in structures],
|
||||
"assumptions": ["Synthesized from a legacy CDSL revision."],
|
||||
"open_questions": [],
|
||||
"capability_gaps": [],
|
||||
"verification_expectations": [],
|
||||
"status": "ready",
|
||||
}
|
||||
@@ -1,124 +0,0 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "https://cdsl.local/schema/cad.cdsl.design-intent.v1",
|
||||
"title": "CDSL design intent",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"schema": {"const": "cad.cdsl.design-intent.v1"},
|
||||
"schema_version": {"const": "1.0"},
|
||||
"mode": {"enum": ["create", "revise"]},
|
||||
"request": {"type": "string", "minLength": 1},
|
||||
"base_revision_id": {"type": "string"},
|
||||
"structures": {"type": "array", "items": {"$ref": "#/$defs/structure"}},
|
||||
"feature_order": {"type": "array", "items": {"type": "string", "pattern": "^[A-Za-z0-9_-]{1,80}$"}},
|
||||
"assumptions": {"type": "array", "items": {"type": "string", "minLength": 1}},
|
||||
"open_questions": {"type": "array", "items": {"$ref": "#/$defs/openQuestion"}},
|
||||
"capability_gaps": {"type": "array", "items": {"$ref": "#/$defs/capabilityGap"}},
|
||||
"verification_expectations": {"type": "array", "items": {"$ref": "#/$defs/verificationExpectation"}},
|
||||
"status": {"enum": ["ready", "needs_clarification"]},
|
||||
"intent_id": {"type": "string", "pattern": "^intent_[a-z0-9]{12}$"},
|
||||
"created_at": {"type": "string", "minLength": 1},
|
||||
"part_skill_ids": {"type": "array", "items": {"type": "string", "minLength": 1}},
|
||||
"part_skill_selection": {"type": "object"}
|
||||
},
|
||||
"required": [
|
||||
"schema",
|
||||
"schema_version",
|
||||
"mode",
|
||||
"request",
|
||||
"base_revision_id",
|
||||
"structures",
|
||||
"feature_order",
|
||||
"assumptions",
|
||||
"open_questions",
|
||||
"capability_gaps",
|
||||
"verification_expectations",
|
||||
"status"
|
||||
],
|
||||
"additionalProperties": false,
|
||||
"$defs": {
|
||||
"structure": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {"type": "string", "pattern": "^[A-Za-z0-9_-]{1,80}$"},
|
||||
"cdsl_feature_id": {"type": "string", "pattern": "^[A-Za-z0-9_-]{1,80}$"},
|
||||
"role": {"enum": ["base", "reference", "additive", "subtractive", "dressup", "pattern"]},
|
||||
"purpose": {"type": "string", "minLength": 1},
|
||||
"depends_on": {"type": "array", "items": {"type": "string", "pattern": "^[A-Za-z0-9_-]{1,80}$"}},
|
||||
"cdsl_strategy": {"$ref": "#/$defs/cdslStrategy"}
|
||||
},
|
||||
"required": ["id", "cdsl_feature_id", "role", "purpose", "depends_on", "cdsl_strategy"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"cdslStrategy": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"atomic_id": {"type": "string", "minLength": 1},
|
||||
"profile_type": {"type": "string", "minLength": 1},
|
||||
"parameter_roles": {"type": "array", "items": {"type": "string", "minLength": 1}},
|
||||
"selector_roles": {"type": "array", "items": {"type": "string", "minLength": 1}}
|
||||
},
|
||||
"required": ["atomic_id", "parameter_roles", "selector_roles"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"openQuestion": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {"type": "string", "pattern": "^[A-Za-z0-9_-]{1,80}$"},
|
||||
"question": {"type": "string", "minLength": 1},
|
||||
"blocking": {"type": "boolean"}
|
||||
},
|
||||
"required": ["id", "question", "blocking"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"capabilityGap": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"code": {"type": "string", "pattern": "^[A-Za-z0-9_-]{1,100}$"},
|
||||
"structure_id": {"type": "string", "pattern": "^[A-Za-z0-9_-]{1,80}$"},
|
||||
"message": {"type": "string", "minLength": 1},
|
||||
"blocking": {"type": "boolean"}
|
||||
},
|
||||
"required": ["code", "structure_id", "message", "blocking"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"verificationExpectation": {
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"type": {"const": "feature_count"},
|
||||
"feature_id": {"type": "string", "pattern": "^[A-Za-z0-9_-]{1,80}$"},
|
||||
"expected": {"type": "integer", "minimum": 0}
|
||||
},
|
||||
"required": ["type", "feature_id", "expected"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {"type": {"const": "symmetry"}, "axis": {"enum": ["x", "y", "z"]}},
|
||||
"required": ["type", "axis"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"type": {"const": "bbox"},
|
||||
"expected_mm": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"x": {"type": "number", "exclusiveMinimum": 0},
|
||||
"y": {"type": "number", "exclusiveMinimum": 0},
|
||||
"z": {"type": "number", "exclusiveMinimum": 0}
|
||||
},
|
||||
"required": ["x", "y", "z"],
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"required": ["type", "expected_mm"],
|
||||
"additionalProperties": false
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,349 +0,0 @@
|
||||
"""将 output3 的程序化圆柱样本蒸馏为自足的短 CDSL。
|
||||
|
||||
这些样本的 SolidWorks 历史把每个切口存成独立草图/切除特征。本脚本按
|
||||
文件名中的设计族选择一个可复用的 motif + layout 语义描述;不会复制
|
||||
草图 entities、逐切口坐标、compiler_context 或任何编码后的几何。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import re
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
|
||||
SPECS: dict[str, dict[str, Any]] = {
|
||||
"cylinder_001": {
|
||||
"motif": {"type": "annular_sector_polygon", "inner_radius_mm": 16.0, "outer_radius_mm": 25.0, "half_angle_deg": 4.583662},
|
||||
"layout": {"type": "angular", "count": 12, "start_angle_deg": 15.0, "orientation": "radial"},
|
||||
},
|
||||
"cylinder_002": {
|
||||
"motif": {"type": "annular_sector_polygon", "inner_radius_mm": 8.0, "outer_radius_mm": 12.5, "half_angle_deg": 4.583662},
|
||||
"layout": {"type": "angular", "count": 12, "start_angle_deg": 15.0, "orientation": "radial"},
|
||||
},
|
||||
"cylinder_003": {
|
||||
"motif": {"type": "circle", "radius_mm": 2.4},
|
||||
"layout": {"type": "ring", "radius_mm": 34.0, "count": 32},
|
||||
},
|
||||
"cylinder_004": {
|
||||
"motif": {"type": "square", "width_mm": 3.3},
|
||||
"layout": {"type": "ring", "radius_mm": 32.0, "count": 24},
|
||||
},
|
||||
"cylinder_005": {
|
||||
"motif": {"type": "square", "width_mm": 4.242641},
|
||||
"layout": {"type": "ring", "radius_mm": 32.0, "count": 24},
|
||||
},
|
||||
"cylinder_006": {
|
||||
"motif": {"type": "circle", "radius_mm": 2.2},
|
||||
"layout": {
|
||||
"type": "disc_grid", "count_x": 7, "count_y": 7,
|
||||
"spacing_x_mm": 10.0, "spacing_y_mm": 10.0,
|
||||
"center_mm": [0.0, 0.0], "max_center_radius_mm": 36.1,
|
||||
},
|
||||
},
|
||||
"cylinder_007": {
|
||||
"motif": {"type": "annular_sector_polygon", "inner_radius_mm": 12.0, "outer_radius_mm": 20.0, "half_angle_deg": 4.583662},
|
||||
"layout": {
|
||||
"type": "concentric_rings", "orientation": "radial",
|
||||
"rings": [
|
||||
{"type": "angular", "count": 12, "start_angle_deg": 15.0},
|
||||
{"type": "angular", "count": 24, "start_angle_deg": 7.5},
|
||||
],
|
||||
},
|
||||
"ring_motifs": [
|
||||
{"inner_radius_mm": 12.0, "outer_radius_mm": 20.0, "half_angle_deg": 4.583662, "count": 12, "start_angle_deg": 15.0},
|
||||
{"inner_radius_mm": 28.0, "outer_radius_mm": 40.0, "half_angle_deg": 2.864789, "count": 24, "start_angle_deg": 7.5},
|
||||
],
|
||||
},
|
||||
"cylinder_008": {
|
||||
"motif": {"type": "obround", "length_mm": 7.0, "width_mm": 2.8},
|
||||
"layout": {"type": "ring", "radius_mm": 34.0, "count": 24, "orientation": "radial"},
|
||||
},
|
||||
"cylinder_009": {
|
||||
"motif": {"type": "annular_sector_polygon", "inner_radius_mm": 10.0, "outer_radius_mm": 42.0, "half_angle_deg": 3.437747},
|
||||
"layout": {"type": "angular", "count": 12, "start_angle_deg": 15.0, "orientation": "radial"},
|
||||
},
|
||||
"cylinder_010": {
|
||||
"motif": {"type": "circle", "radius_mm": 2.3},
|
||||
"layout": {
|
||||
"type": "concentric_rings",
|
||||
"rings": [
|
||||
{"radius_mm": 18.0, "count": 12, "start_angle_deg": 0.0},
|
||||
{"radius_mm": 36.0, "count": 24, "start_angle_deg": 0.0},
|
||||
],
|
||||
},
|
||||
},
|
||||
"cylinder_011": {
|
||||
"motif": {"type": "annular_sector_polygon", "inner_radius_mm": 10.0, "outer_radius_mm": 20.0, "half_angle_deg": 4.010705},
|
||||
"layout": {"type": "angular", "count": 8, "start_angle_deg": 22.5, "orientation": "radial"},
|
||||
},
|
||||
"cylinder_012": {
|
||||
"motif": {"type": "circle", "radius_mm": 1.9},
|
||||
"layout": {"type": "ring", "radius_mm": 26.0, "count": 24},
|
||||
},
|
||||
"cylinder_013": {
|
||||
"motif": {"type": "square", "width_mm": 2.7},
|
||||
"layout": {"type": "ring", "radius_mm": 25.0, "count": 16},
|
||||
},
|
||||
"cylinder_014": {
|
||||
"motif": {"type": "square", "width_mm": 3.535534},
|
||||
"layout": {"type": "ring", "radius_mm": 25.0, "count": 16},
|
||||
},
|
||||
"cylinder_015": {
|
||||
"motif": {"type": "circle", "radius_mm": 1.8},
|
||||
"layout": {
|
||||
"type": "disc_grid", "count_x": 6, "count_y": 5,
|
||||
"spacing_x_mm": 9.0, "spacing_y_mm": 9.0,
|
||||
"center_mm": [0.0, 0.0],
|
||||
},
|
||||
},
|
||||
"cylinder_016": {
|
||||
"motif": {"type": "d_shape_polygon", "stem_length_mm": 1.9, "nose_depth_mm": 1.9, "half_height_mm": 1.3775},
|
||||
"layout": {
|
||||
"type": "open_arc", "radius_mm": 35.0, "count": 18,
|
||||
"start_angle_deg": 18.0, "end_angle_deg": 262.8,
|
||||
},
|
||||
},
|
||||
"cylinder_017": {
|
||||
"motif": {"type": "cross", "size_mm": 4.62, "arm_width_mm": 2.9568},
|
||||
"layout": {
|
||||
"type": "open_arc", "radius_mm": 31.0, "count": 14,
|
||||
"start_angle_deg": 189.0, "end_angle_deg": 387.0,
|
||||
},
|
||||
},
|
||||
"cylinder_018": {
|
||||
"motif": {"type": "obround", "length_mm": 5.75, "width_mm": 2.3},
|
||||
"layout": {"type": "ring", "radius_mm": 27.0, "count": 16, "orientation": "radial"},
|
||||
},
|
||||
"cylinder_019": {
|
||||
"motif": {"type": "circle", "radius_mm": 1.8},
|
||||
"layout": {
|
||||
"type": "concentric_rings",
|
||||
"rings": [
|
||||
{"radius_mm": 14.0, "count": 8, "start_angle_deg": 0.0},
|
||||
{"radius_mm": 30.0, "count": 16, "start_angle_deg": 0.0},
|
||||
],
|
||||
},
|
||||
},
|
||||
"cylinder_020": {
|
||||
"motif": {"type": "annular_sector_polygon", "inner_radius_mm": 9.0, "outer_radius_mm": 36.0, "half_angle_deg": 3.151268},
|
||||
"layout": {"type": "angular", "count": 10, "start_angle_deg": 18.0, "orientation": "radial"},
|
||||
},
|
||||
"cylinder_021": {
|
||||
"motif": {"type": "annular_sector_polygon", "inner_radius_mm": 12.0, "outer_radius_mm": 20.0, "half_angle_deg": 4.010705},
|
||||
"layout": {"type": "angular", "count": 8, "start_angle_deg": 22.5, "orientation": "radial"},
|
||||
"ring_motifs": [
|
||||
{"inner_radius_mm": 12.0, "outer_radius_mm": 20.0, "half_angle_deg": 4.010705, "count": 8, "start_angle_deg": 22.5},
|
||||
{"inner_radius_mm": 28.0, "outer_radius_mm": 39.0, "half_angle_deg": 2.57831, "count": 16, "start_angle_deg": 11.25},
|
||||
],
|
||||
},
|
||||
"cylinder_022": {
|
||||
"motif": {"type": "skew_hexagon", "nominal_radius_mm": 1.875},
|
||||
"layout": {
|
||||
"type": "spiral", "count": 28, "start_radius_mm": 8.054,
|
||||
"radius_step_mm": 0.851, "start_angle_deg": -0.105, "angle_step_deg": 33.2394,
|
||||
"orientation": "snapped_radial", "orientation_snap_deg": 45.0,
|
||||
"orientation_offset_deg": 0.0,
|
||||
},
|
||||
},
|
||||
"cylinder_023": {
|
||||
"motif": {"type": "skew_hexagon", "nominal_radius_mm": 1.9},
|
||||
"layout": {
|
||||
"type": "cross_lines", "count_per_axis": 6, "spacing_mm": 13.6,
|
||||
"orientation_offset_deg": 0.0,
|
||||
},
|
||||
},
|
||||
"cylinder_024": {
|
||||
"motif": {"type": "triangle", "radius_mm": 2.3},
|
||||
"layout": {
|
||||
"type": "x_field", "levels": 5, "spacing_mm": 11.0,
|
||||
"orientation": "diagonal_axes",
|
||||
},
|
||||
},
|
||||
"cylinder_025": {
|
||||
"motif": {
|
||||
"type": "teardrop_polygon",
|
||||
"left_width_mm": 1.116, "right_width_mm": 1.548,
|
||||
"tip_height_mm": 2.79, "bottom_depth_mm": 1.476,
|
||||
"shoulder_height_mm": 1.242,
|
||||
},
|
||||
"layout": {
|
||||
"type": "twin_strips", "x_offset_mm": 15.0, "count_y": 12,
|
||||
"y_start_mm": -34.0, "y_end_mm": 34.0,
|
||||
},
|
||||
},
|
||||
"cylinder_026": {
|
||||
"motif": {"type": "trapezoid", "bottom_width_mm": 4.32, "top_width_mm": 2.376, "height_mm": 3.24},
|
||||
"layout": {
|
||||
"type": "concentric_rings", "orientation": "radial",
|
||||
"rings": [
|
||||
{"radius_mm": 12.0, "count": 8, "start_angle_deg": 0.0},
|
||||
{"radius_mm": 31.0, "count": 16, "start_angle_deg": 0.0},
|
||||
],
|
||||
},
|
||||
},
|
||||
"cylinder_027": {
|
||||
"motif": {"type": "d_shape_polygon", "stem_length_mm": 1.8, "nose_depth_mm": 1.8, "half_height_mm": 1.305},
|
||||
"layout": {"type": "corner_clusters", "levels_mm": [12.0, 18.5, 25.0]},
|
||||
},
|
||||
"cylinder_028": {
|
||||
"motif": {"type": "square", "width_mm": 2.969848},
|
||||
"layout": {"type": "diamond_field", "manhattan_radius": 3, "spacing_mm": 8.0},
|
||||
},
|
||||
"cylinder_029": {
|
||||
"motif": {"type": "annular_sector_polygon", "inner_radius_mm": 11.0, "outer_radius_mm": 22.0, "half_angle_deg": 4.010705},
|
||||
"layout": {"type": "angular", "count": 10, "start_angle_deg": 18.0, "orientation": "radial"},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def _pattern_sketches(spec: dict[str, Any]) -> list[dict[str, Any]]:
|
||||
base = {
|
||||
"id": "sketch_001",
|
||||
"name": "草图1",
|
||||
"workplane": {
|
||||
"origin_mm": [0.0, 0.0, 0.0],
|
||||
"x_dir": [1.0, 0.0, 0.0],
|
||||
"y_dir": [0.0, 1.0, 0.0],
|
||||
"normal": [0.0, 0.0, 1.0],
|
||||
},
|
||||
"profile": {"type": "circle", "center": [0.0, 0.0], "radius_mm": 50.0},
|
||||
}
|
||||
cut_workplane = {
|
||||
"origin_mm": [0.0, 0.0, 20.0],
|
||||
"x_dir": [1.0, 0.0, 0.0],
|
||||
"y_dir": [0.0, 1.0, 0.0],
|
||||
"normal": [0.0, 0.0, 1.0],
|
||||
}
|
||||
|
||||
ring_motifs = spec.get("ring_motifs")
|
||||
if not ring_motifs:
|
||||
cut = {
|
||||
"id": "sketch_002",
|
||||
"name": "图案草图",
|
||||
"workplane": cut_workplane,
|
||||
"profile": {
|
||||
"type": "patterned_cutouts",
|
||||
"motif": spec["motif"],
|
||||
"layout": spec["layout"],
|
||||
},
|
||||
}
|
||||
return [base, cut]
|
||||
|
||||
# 两组扇区的内外半径不同,仍合并成一个切除特征;组合只包含
|
||||
# 两个有名字的程序化子图案,不保存任何逐实例坐标。
|
||||
cut = {
|
||||
"id": "sketch_002",
|
||||
"name": "双环图案草图",
|
||||
"workplane": cut_workplane,
|
||||
"profile": {
|
||||
"type": "compound_patterned_cutouts",
|
||||
"patterns": [
|
||||
{
|
||||
"motif": {
|
||||
"type": "annular_sector_polygon",
|
||||
"inner_radius_mm": item["inner_radius_mm"],
|
||||
"outer_radius_mm": item["outer_radius_mm"],
|
||||
"half_angle_deg": item["half_angle_deg"],
|
||||
},
|
||||
"layout": {
|
||||
"type": "angular",
|
||||
"count": item["count"],
|
||||
"start_angle_deg": item["start_angle_deg"],
|
||||
"orientation": "radial",
|
||||
},
|
||||
}
|
||||
for item in ring_motifs
|
||||
],
|
||||
},
|
||||
}
|
||||
return [base, cut]
|
||||
|
||||
|
||||
def make_cdsl(source: Path, spec: dict[str, Any]) -> dict[str, Any]:
|
||||
part_id = source.name.removesuffix(".solidworks_evidence_v2.json")
|
||||
return {
|
||||
"schema": "cad.cdsl.llm.v1",
|
||||
"schema_version": "1.0.0",
|
||||
"kind": "part",
|
||||
"part_id": part_id,
|
||||
"features": [
|
||||
{
|
||||
"id": "f01",
|
||||
"atomic_id": "extrude_add_blind",
|
||||
"depends_on": [],
|
||||
"name": "凸台-拉伸1",
|
||||
"params": {"distance_mm": 20.0},
|
||||
"sketch_id": "sketch_001",
|
||||
},
|
||||
{
|
||||
"id": "f02",
|
||||
"atomic_id": "extrude_cut_blind",
|
||||
"depends_on": ["f01"],
|
||||
"name": "程序化图案切除",
|
||||
"params": {"distance_mm": 8.0, "reverse": True},
|
||||
"sketch_id": "sketch_002",
|
||||
},
|
||||
],
|
||||
"geometry": {"sketches": _pattern_sketches(spec)},
|
||||
"meta": {
|
||||
"source": source.name,
|
||||
"parameterized_sketches": 2,
|
||||
"profile_from_sketches": 0,
|
||||
"notes": [
|
||||
"CDSL-only drawable: repeated source sketches are represented by one procedural motif and layout",
|
||||
"No compiler_context, source entities, per-instance coordinates, or encoded geometry",
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def distill(input_dir: Path, output_dir: Path) -> list[Path]:
|
||||
sources = sorted(input_dir.glob("cylinder_*.solidworks_evidence_v2.json"))
|
||||
written: list[Path] = []
|
||||
missing: list[str] = []
|
||||
for source in sources:
|
||||
key = source.name[:12]
|
||||
spec = SPECS.get(key)
|
||||
if spec is None:
|
||||
missing.append(source.name)
|
||||
continue
|
||||
part_id = source.name.removesuffix(".solidworks_evidence_v2.json")
|
||||
part_dir = output_dir / part_id
|
||||
part_dir.mkdir(parents=True, exist_ok=True)
|
||||
out = part_dir / f"{part_id}.cdsl.json"
|
||||
text = json.dumps(make_cdsl(source, spec), ensure_ascii=False, indent=2)
|
||||
# 与 b005/b006 一致:短标量数组保持单行,结构数组仍按层级展开。
|
||||
scalar_array = re.compile(
|
||||
r"\[\n(?P<body>(?:[ \t]+(?:-?\d+(?:\.\d+)?|true|false|null|\"[^\"\\n]*\"),?\n)+)[ \t]*\]"
|
||||
)
|
||||
|
||||
def compact(match: re.Match[str]) -> str:
|
||||
values = [line.strip().rstrip(",") for line in match.group("body").splitlines()]
|
||||
inline = "[" + ", ".join(values) + "]"
|
||||
return inline if len(inline) <= 100 else match.group(0)
|
||||
|
||||
text = scalar_array.sub(compact, text)
|
||||
out.write_text(
|
||||
text + "\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
written.append(out)
|
||||
if missing:
|
||||
raise RuntimeError("No semantic specification for: " + ", ".join(missing))
|
||||
return written
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("input_dir", type=Path)
|
||||
parser.add_argument("--out", type=Path, required=True)
|
||||
args = parser.parse_args()
|
||||
paths = distill(args.input_dir, args.out)
|
||||
print(f"wrote {len(paths)} CDSL files to {args.out}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,212 +0,0 @@
|
||||
"""Deterministic GenerationSpec -> CDSL compilers.
|
||||
|
||||
The LLM supplies semantic parameters and feature intent. This module owns
|
||||
the CDSL details so tool calls do not need to contain sketch IDs, selectors, or
|
||||
runtime-specific parameter wrappers.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import copy
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, Protocol
|
||||
|
||||
from .generation_spec import GenerationSpecError, validate_generation_spec
|
||||
|
||||
|
||||
def _workplane(z: float = 0.0, normal: list[float] | None = None) -> dict[str, list[float]]:
|
||||
return {
|
||||
"origin_mm": [0.0, 0.0, float(z)],
|
||||
"x_dir": [1.0, 0.0, 0.0],
|
||||
"y_dir": [0.0, 1.0, 0.0],
|
||||
"normal": list(normal or [0.0, 0.0, 1.0]),
|
||||
}
|
||||
|
||||
|
||||
def _value(spec: dict[str, Any], name: str, default: float | int | str) -> Any:
|
||||
parameter = (spec.get("parameters") or {}).get(name)
|
||||
if isinstance(parameter, dict) and "value" in parameter:
|
||||
return parameter["value"]
|
||||
return default
|
||||
|
||||
|
||||
def _number(spec: dict[str, Any], name: str, default: float) -> float:
|
||||
value = _value(spec, name, default)
|
||||
if isinstance(value, bool) or not isinstance(value, (int, float)):
|
||||
raise GenerationSpecError(f"Parameter {name} must be numeric", f"$.parameters.{name}.value")
|
||||
number = float(value)
|
||||
if not number == number or number in {float("inf"), float("-inf")}:
|
||||
raise GenerationSpecError(f"Parameter {name} must be finite", f"$.parameters.{name}.value")
|
||||
return number
|
||||
|
||||
|
||||
def _integer(spec: dict[str, Any], name: str, default: int) -> int:
|
||||
value = _number(spec, name, default)
|
||||
if int(value) != value:
|
||||
raise GenerationSpecError(f"Parameter {name} must be an integer", f"$.parameters.{name}.value")
|
||||
return int(value)
|
||||
|
||||
|
||||
def _positive(number: float, name: str) -> float:
|
||||
if number <= 0:
|
||||
raise GenerationSpecError(f"Parameter {name} must be greater than zero", f"$.parameters.{name}.value")
|
||||
return number
|
||||
|
||||
|
||||
def _acceptance(spec: dict[str, Any], generated: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
||||
existing = copy.deepcopy(spec.get("acceptance") or [])
|
||||
known = {str(item.get("id")) for item in existing}
|
||||
for item in generated:
|
||||
if item["id"] not in known:
|
||||
existing.append(item)
|
||||
known.add(item["id"])
|
||||
return existing
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class CompiledGeneration:
|
||||
cdsl: dict[str, Any]
|
||||
acceptance: list[dict[str, Any]]
|
||||
provenance: dict[str, Any]
|
||||
assumptions: list[str]
|
||||
approximations: list[dict[str, Any]]
|
||||
|
||||
|
||||
class Compiler(Protocol):
|
||||
family: str
|
||||
|
||||
def compile(self, spec: dict[str, Any], references: list[dict[str, Any]]) -> CompiledGeneration:
|
||||
...
|
||||
|
||||
|
||||
class MountingPlateCompiler:
|
||||
family = "mounting_plate"
|
||||
|
||||
def compile(self, spec: dict[str, Any], references: list[dict[str, Any]]) -> CompiledGeneration:
|
||||
width = _positive(_number(spec, "width", 100.0), "width")
|
||||
height = _positive(_number(spec, "height", 60.0), "height")
|
||||
thickness = _positive(_number(spec, "thickness", 6.0), "thickness")
|
||||
hole_diameter = _positive(_number(spec, "hole_diameter", 4.5), "hole_diameter")
|
||||
hole_offset = _positive(_number(spec, "hole_edge_offset", 10.0), "hole_edge_offset")
|
||||
hole_count = _integer(spec, "hole_count", 4)
|
||||
counterbore_diameter = _positive(_number(spec, "counterbore_diameter", hole_diameter * 2), "counterbore_diameter")
|
||||
counterbore_depth = _positive(_number(spec, "counterbore_depth", min(thickness / 2, 3.0)), "counterbore_depth")
|
||||
slot_length = _positive(_number(spec, "slot_length", 20.0), "slot_length")
|
||||
slot_width = _positive(_number(spec, "slot_width", 12.0), "slot_width")
|
||||
|
||||
if hole_count != 4:
|
||||
raise GenerationSpecError("mounting_plate compiler currently requires four corner holes", "$.parameters.hole_count.value")
|
||||
if counterbore_diameter <= hole_diameter:
|
||||
raise GenerationSpecError("counterbore_diameter must exceed hole_diameter", "$.parameters.counterbore_diameter.value")
|
||||
if counterbore_depth > thickness:
|
||||
raise GenerationSpecError("counterbore_depth must not exceed thickness", "$.parameters.counterbore_depth.value")
|
||||
if hole_offset * 2 >= min(width, height):
|
||||
raise GenerationSpecError("hole_edge_offset leaves no usable plate area", "$.parameters.hole_edge_offset.value")
|
||||
if slot_length < slot_width:
|
||||
raise GenerationSpecError("slot_length must be at least slot_width", "$.parameters.slot_length.value")
|
||||
|
||||
centers = [
|
||||
[-width / 2 + hole_offset, -height / 2 + hole_offset],
|
||||
[width / 2 - hole_offset, -height / 2 + hole_offset],
|
||||
[width / 2 - hole_offset, height / 2 - hole_offset],
|
||||
[-width / 2 + hole_offset, height / 2 - hole_offset],
|
||||
]
|
||||
frame = _workplane(thickness)
|
||||
cdsl = {
|
||||
"schema": "cad.cdsl.llm.v1",
|
||||
"schema_version": "1.1.0",
|
||||
"kind": "part",
|
||||
"part_id": "generation-spec-preflight",
|
||||
"meta": {"unit": "mm", "name": str(spec["part"]["name"])},
|
||||
"geometry": {"sketches": [
|
||||
{"id": "base_plate", "workplane": _workplane(), "profile": {
|
||||
"type": "rectangle", "center": [0.0, 0.0], "width_mm": width, "height_mm": height,
|
||||
}},
|
||||
{"id": "mounting_holes", "workplane": frame, "profile": {
|
||||
"type": "circles", "items": [{"center": center, "radius_mm": hole_diameter / 2} for center in centers],
|
||||
}},
|
||||
{"id": "center_slot", "workplane": frame, "profile": {
|
||||
"type": "obround", "center": [0.0, 0.0], "length_mm": slot_length, "width_mm": slot_width,
|
||||
}},
|
||||
]},
|
||||
"features": [
|
||||
{"id": "base_plate", "atomic_id": "extrude_add_blind", "depends_on": [], "sketch_id": "base_plate", "params": {"distance_mm": thickness}},
|
||||
{"id": "mounting_holes", "atomic_id": "extrude_cut_blind", "depends_on": ["base_plate"], "sketch_id": "mounting_holes", "params": {"distance_mm": thickness, "reverse": True}},
|
||||
{"id": "mounting_counterbores", "atomic_id": "hole_counterbore", "depends_on": ["mounting_holes"], "sketch_id": "mounting_holes", "params": {
|
||||
"diameter_mm": hole_diameter, "depth_mm": thickness, "counterbore_diameter_mm": counterbore_diameter,
|
||||
"counterbore_depth_mm": counterbore_depth, "positions": [{"mm": [x, y, 0.0]} for x, y in centers], "host_face": {"frame": frame},
|
||||
}},
|
||||
{"id": "center_slot", "atomic_id": "extrude_cut_blind", "depends_on": ["mounting_counterbores"], "sketch_id": "center_slot", "params": {"distance_mm": thickness, "reverse": True}},
|
||||
],
|
||||
}
|
||||
assumptions = [str(item) for item in spec.get("assumptions") or []]
|
||||
if "hole_diameter" not in (spec.get("parameters") or {}):
|
||||
assumptions.append("M4 clearance hole defaults to 4.5 mm")
|
||||
if "counterbore_depth" not in (spec.get("parameters") or {}):
|
||||
assumptions.append("Counterbore depth defaults to the lesser of 3 mm and half the plate thickness")
|
||||
acceptance = _acceptance(spec, [
|
||||
{"id": "overall_bbox", "type": "bbox", "expected": [width, height, thickness], "tolerance": 0.1, "severity": "blocking"},
|
||||
{"id": "mounting_hole_count", "type": "feature_count", "feature": "mounting_holes", "expected": 4, "tolerance": 0, "severity": "blocking"},
|
||||
{"id": "mounting_hole_diameter", "type": "hole_diameter", "feature": "mounting_holes", "expected": hole_diameter, "tolerance": 0.05, "severity": "blocking"},
|
||||
{"id": "center_slot_size", "type": "overall_length", "feature": "center_slot", "expected": slot_length, "tolerance": 0.1, "severity": "blocking"},
|
||||
])
|
||||
provenance = {
|
||||
"compiler": self.family,
|
||||
"compiler_version": "1.0",
|
||||
"references": references,
|
||||
"feature_sources": {item["id"]: item["id"] for item in spec.get("features") or []},
|
||||
"parameter_paths": {name: f"parameters.{name}.value" for name in (spec.get("parameters") or {})},
|
||||
}
|
||||
return CompiledGeneration(cdsl, acceptance, provenance, list(dict.fromkeys(assumptions)), list(spec.get("approximations") or []))
|
||||
|
||||
|
||||
class FlangeSleeveCompiler:
|
||||
family = "flange_sleeve"
|
||||
|
||||
def compile(self, spec: dict[str, Any], references: list[dict[str, Any]]) -> CompiledGeneration:
|
||||
from app.services.flange_sleeve_template import build_flange_sleeve_cdsl, TEMPLATE_ID
|
||||
|
||||
aliases = {
|
||||
"width": "flange_width_mm", "height": "flange_height_mm", "thickness": "flange_thickness_mm",
|
||||
"tube_outer_diameter": "tube_outer_diameter_mm", "bore_diameter": "bore_diameter_mm",
|
||||
"hole_diameter": "mount_hole_diameter_mm", "counterbore_diameter": "mount_counterbore_diameter_mm",
|
||||
"counterbore_depth": "mount_counterbore_depth_mm", "hole_offset_x": "mount_hole_u_mm", "hole_offset_y": "mount_hole_v_mm",
|
||||
}
|
||||
plan: dict[str, Any] = {"template": TEMPLATE_ID, "name": spec["part"]["name"]}
|
||||
for source, target in aliases.items():
|
||||
if source in (spec.get("parameters") or {}):
|
||||
plan[target] = _value(spec, source, 0)
|
||||
cdsl, values = build_flange_sleeve_cdsl(plan)
|
||||
acceptance = _acceptance(spec, [
|
||||
{"id": "flange_bbox", "type": "bbox", "expected": [float(values["flange_width_mm"]), float(values["flange_height_mm"]), float(values["flange_thickness_mm"])], "tolerance": 0.1, "severity": "blocking"},
|
||||
])
|
||||
provenance = {"compiler": self.family, "compiler_version": "1.0", "template_id": TEMPLATE_ID, "references": references}
|
||||
assumptions = list(dict.fromkeys([*map(str, spec.get("assumptions") or []), "Flange sleeve dimensions use the verified parameterized template defaults for omitted fields."]))
|
||||
return CompiledGeneration(cdsl, acceptance, provenance, assumptions, list(spec.get("approximations") or []))
|
||||
|
||||
|
||||
COMPILERS: dict[str, Compiler] = {
|
||||
"mounting_plate": MountingPlateCompiler(),
|
||||
"flange_sleeve": FlangeSleeveCompiler(),
|
||||
}
|
||||
|
||||
|
||||
def compiler_for(family: str) -> Compiler:
|
||||
try:
|
||||
return COMPILERS[family]
|
||||
except KeyError as error:
|
||||
raise GenerationSpecError(f"No GenerationSpec compiler registered for part family: {family}", "$.part.family") from error
|
||||
|
||||
|
||||
def compile_generation_spec(spec: dict[str, Any], references: list[dict[str, Any]] | None = None) -> dict[str, Any]:
|
||||
normalized = validate_generation_spec(spec)
|
||||
compiler = compiler_for(str(normalized["part"]["family"]))
|
||||
compiled = compiler.compile(normalized, references or [])
|
||||
return {
|
||||
"spec": normalized,
|
||||
"cdsl": compiled.cdsl,
|
||||
"acceptance": compiled.acceptance,
|
||||
"provenance": compiled.provenance,
|
||||
"assumptions": compiled.assumptions,
|
||||
"approximations": compiled.approximations,
|
||||
}
|
||||
@@ -1,177 +0,0 @@
|
||||
"""Validation and normalization for the semantic GenerationSpec contract."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import copy
|
||||
import json
|
||||
from functools import lru_cache
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from jsonschema import Draft202012Validator
|
||||
|
||||
|
||||
KNOWN_PART_FAMILIES = frozenset({
|
||||
"mounting_plate",
|
||||
"flange",
|
||||
"flange_sleeve",
|
||||
"simple_shaft",
|
||||
"bearing_housing",
|
||||
"mounting_bracket",
|
||||
"hex_nut",
|
||||
"slotted_plate",
|
||||
})
|
||||
|
||||
KNOWN_FEATURE_KINDS = frozenset({
|
||||
"base_extrusion",
|
||||
"base_revolve",
|
||||
"boss",
|
||||
"through_hole",
|
||||
"blind_hole",
|
||||
"counterbored_hole",
|
||||
"countersunk_hole",
|
||||
"hole_pattern",
|
||||
"counterbored_hole_pattern",
|
||||
"obround_cut",
|
||||
"pocket",
|
||||
"revolve_profile",
|
||||
"coaxial_bore",
|
||||
"fillet",
|
||||
"chamfer",
|
||||
})
|
||||
|
||||
|
||||
class GenerationSpecError(ValueError):
|
||||
"""A stable, user-repairable GenerationSpec validation error."""
|
||||
|
||||
def __init__(self, message: str, path: str = "$") -> None:
|
||||
super().__init__(message)
|
||||
self.path = path
|
||||
|
||||
|
||||
@lru_cache(maxsize=1)
|
||||
def generation_spec_schema() -> dict[str, Any]:
|
||||
path = Path(__file__).with_name("generation_spec_schema.json")
|
||||
schema = json.loads(path.read_text(encoding="utf-8"))
|
||||
Draft202012Validator.check_schema(schema)
|
||||
return schema
|
||||
|
||||
|
||||
def _schema_error(spec: dict[str, Any]) -> GenerationSpecError | None:
|
||||
errors = sorted(
|
||||
Draft202012Validator(generation_spec_schema()).iter_errors(spec),
|
||||
key=lambda error: (list(error.absolute_path), error.message),
|
||||
)
|
||||
if not errors:
|
||||
return None
|
||||
error = errors[0]
|
||||
location = "$" + "".join(
|
||||
f"[{item}]" if isinstance(item, int) else f".{item}"
|
||||
for item in error.absolute_path
|
||||
)
|
||||
return GenerationSpecError(error.message, location)
|
||||
|
||||
|
||||
def _parameter_value(spec: dict[str, Any], name: str) -> Any:
|
||||
value = (spec.get("parameters") or {}).get(name)
|
||||
if not isinstance(value, dict):
|
||||
raise GenerationSpecError(f"Unknown parameter: {name}", f"$.parameters.{name}")
|
||||
return value.get("value")
|
||||
|
||||
|
||||
def _numeric(value: Any, path: str) -> float:
|
||||
if isinstance(value, bool) or not isinstance(value, (int, float)):
|
||||
raise GenerationSpecError("Expected a finite numeric value", path)
|
||||
number = float(value)
|
||||
if number != number or number in {float("inf"), float("-inf")}:
|
||||
raise GenerationSpecError("Expected a finite numeric value", path)
|
||||
return number
|
||||
|
||||
|
||||
def _check_constraints(spec: dict[str, Any]) -> None:
|
||||
for index, constraint in enumerate(spec.get("constraints") or []):
|
||||
path = f"$.constraints[{index}]"
|
||||
kind = constraint.get("type")
|
||||
if kind in {"less_than", "less_equal", "greater_than", "greater_equal", "equal"}:
|
||||
left = _numeric(_parameter_value(spec, str(constraint.get("left") or "")), f"{path}.left")
|
||||
right_name = constraint.get("right")
|
||||
right = _numeric(_parameter_value(spec, str(right_name)), f"{path}.right") if right_name else _numeric(constraint.get("value"), f"{path}.value")
|
||||
ok = {
|
||||
"less_than": left < right,
|
||||
"less_equal": left <= right,
|
||||
"greater_than": left > right,
|
||||
"greater_equal": left >= right,
|
||||
"equal": abs(left - right) <= 1e-9,
|
||||
}[kind]
|
||||
if not ok:
|
||||
raise GenerationSpecError(constraint.get("message") or f"Constraint {kind} failed", path)
|
||||
|
||||
|
||||
def _check_graph(spec: dict[str, Any]) -> None:
|
||||
features = spec.get("features") or []
|
||||
ids = [str(item.get("id")) for item in features]
|
||||
if len(ids) != len(set(ids)):
|
||||
raise GenerationSpecError("Feature ids must be unique", "$.features")
|
||||
known: set[str] = set()
|
||||
for index, feature in enumerate(features):
|
||||
feature_id = str(feature.get("id"))
|
||||
kind = str(feature.get("kind"))
|
||||
if kind not in KNOWN_FEATURE_KINDS:
|
||||
raise GenerationSpecError(f"Unsupported feature kind: {kind}", f"$.features[{index}].kind")
|
||||
for dependency in feature.get("depends_on") or []:
|
||||
if dependency not in known:
|
||||
raise GenerationSpecError(
|
||||
f"Feature {feature_id} has a forward or missing dependency: {dependency}",
|
||||
f"$.features[{index}].depends_on",
|
||||
)
|
||||
known.add(feature_id)
|
||||
|
||||
acceptance_ids = {str(item.get("id")) for item in spec.get("acceptance") or []}
|
||||
if len(acceptance_ids) != len(spec.get("acceptance") or []):
|
||||
raise GenerationSpecError("Acceptance ids must be unique", "$.acceptance")
|
||||
for index, item in enumerate(spec.get("acceptance") or []):
|
||||
feature = item.get("feature")
|
||||
if feature and feature not in known:
|
||||
raise GenerationSpecError(f"Acceptance refers to missing feature: {feature}", f"$.acceptance[{index}].feature")
|
||||
|
||||
|
||||
def normalize_generation_spec(spec: dict[str, Any], *, request: str = "") -> dict[str, Any]:
|
||||
if not isinstance(spec, dict):
|
||||
raise GenerationSpecError("GenerationSpec must be a JSON object")
|
||||
normalized = copy.deepcopy(spec)
|
||||
normalized.setdefault("schema", "cad.generation-spec.v1")
|
||||
normalized.setdefault("schema_version", "1.0")
|
||||
normalized.setdefault("mode", "create")
|
||||
normalized.setdefault("base_revision_id", "")
|
||||
normalized.setdefault("parameters", {})
|
||||
normalized.setdefault("features", [])
|
||||
normalized.setdefault("constraints", [])
|
||||
normalized.setdefault("acceptance", [])
|
||||
normalized.setdefault("assumptions", [])
|
||||
normalized.setdefault("approximations", [])
|
||||
normalized.setdefault("references", [])
|
||||
normalized.setdefault("patch_intent", request)
|
||||
return normalized
|
||||
|
||||
|
||||
def validate_generation_spec(
|
||||
spec: dict[str, Any],
|
||||
*,
|
||||
known_families: set[str] | frozenset[str] = KNOWN_PART_FAMILIES,
|
||||
) -> dict[str, Any]:
|
||||
normalized = normalize_generation_spec(spec)
|
||||
error = _schema_error(normalized)
|
||||
if error:
|
||||
raise error
|
||||
family = str(normalized["part"]["family"])
|
||||
if family not in known_families:
|
||||
raise GenerationSpecError(f"Unsupported part family: {family}", "$.part.family")
|
||||
for name, parameter in normalized["parameters"].items():
|
||||
source = parameter["source"]
|
||||
if source == "user" and not parameter["locked"]:
|
||||
raise GenerationSpecError("User parameters must be locked", f"$.parameters.{name}.locked")
|
||||
if source == "image_estimate" and not parameter.get("assumption"):
|
||||
raise GenerationSpecError("Image estimates require an assumption", f"$.parameters.{name}.assumption")
|
||||
_check_graph(normalized)
|
||||
_check_constraints(normalized)
|
||||
return normalized
|
||||
@@ -1,100 +0,0 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "https://cdsl.local/schema/cad.generation-spec.v1",
|
||||
"title": "CDSL GenerationSpec",
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"schema": {"const": "cad.generation-spec.v1"},
|
||||
"schema_version": {"const": "1.0"},
|
||||
"mode": {"enum": ["create", "revise"]},
|
||||
"base_revision_id": {"type": "string"},
|
||||
"patch_intent": {"type": "string"},
|
||||
"part": {"$ref": "#/$defs/part"},
|
||||
"parameters": {"type": "object", "additionalProperties": {"$ref": "#/$defs/parameter"}},
|
||||
"features": {"type": "array", "items": {"$ref": "#/$defs/feature"}},
|
||||
"constraints": {"type": "array", "items": {"$ref": "#/$defs/constraint"}},
|
||||
"acceptance": {"type": "array", "items": {"$ref": "#/$defs/acceptance"}},
|
||||
"assumptions": {"type": "array", "items": {"type": "string", "minLength": 1}},
|
||||
"approximations": {"type": "array", "items": {"$ref": "#/$defs/approximation"}},
|
||||
"references": {"type": "array", "items": {"type": "object"}}
|
||||
},
|
||||
"required": ["schema", "schema_version", "mode", "base_revision_id", "part", "parameters", "features", "constraints", "acceptance", "assumptions", "approximations", "references"],
|
||||
"$defs": {
|
||||
"part": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"family": {"type": "string", "minLength": 1},
|
||||
"name": {"type": "string", "minLength": 1},
|
||||
"units": {"const": "mm"},
|
||||
"coordinate_system": {"type": "string", "minLength": 1}
|
||||
},
|
||||
"required": ["family", "name", "units", "coordinate_system"]
|
||||
},
|
||||
"parameter": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"value": {},
|
||||
"unit": {"enum": ["mm", "deg", "rad", "count", ""]},
|
||||
"source": {"enum": ["user", "technical_drawing", "image_estimate", "template_default", "derived_standard", "assumption"]},
|
||||
"confidence": {"type": "number", "minimum": 0, "maximum": 1},
|
||||
"locked": {"type": "boolean"},
|
||||
"assumption": {"type": "string"}
|
||||
},
|
||||
"required": ["value", "unit", "source", "confidence", "locked", "assumption"]
|
||||
},
|
||||
"feature": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"id": {"type": "string", "pattern": "^[A-Za-z0-9_-]{1,80}$"},
|
||||
"kind": {"type": "string", "minLength": 1},
|
||||
"depends_on": {"type": "array", "items": {"type": "string", "pattern": "^[A-Za-z0-9_-]{1,80}$"}},
|
||||
"parameters": {"type": "object"},
|
||||
"semantic_role": {"type": "string"}
|
||||
},
|
||||
"required": ["id", "kind", "depends_on", "parameters"]
|
||||
},
|
||||
"constraint": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"type": {"enum": ["less_than", "less_equal", "greater_than", "greater_equal", "equal", "inside_boundary", "symmetric", "same_axis", "distance", "count"]},
|
||||
"left": {"type": "string"},
|
||||
"right": {"type": "string"},
|
||||
"value": {},
|
||||
"feature": {"type": "string"},
|
||||
"margin": {"type": "number"},
|
||||
"ratio": {"type": "number"},
|
||||
"message": {"type": "string"}
|
||||
},
|
||||
"required": ["type"]
|
||||
},
|
||||
"acceptance": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"id": {"type": "string", "pattern": "^[A-Za-z0-9_-]{1,80}$"},
|
||||
"type": {"enum": ["bbox", "solid_count", "feature_count", "hole_diameter", "hole_center", "hole_spacing", "wall_thickness", "overall_length", "overall_diameter", "coaxiality", "symmetry", "through_condition", "blind_depth"]},
|
||||
"feature": {"type": "string"},
|
||||
"expected": {},
|
||||
"tolerance": {"type": "number", "minimum": 0},
|
||||
"severity": {"enum": ["blocking", "warning", "informational"]}
|
||||
},
|
||||
"required": ["id", "type", "expected", "tolerance", "severity"]
|
||||
},
|
||||
"approximation": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"request": {"type": "string", "minLength": 1},
|
||||
"status": {"enum": ["approximated", "expanded", "omitted", "blocked"]},
|
||||
"translation": {"type": "string", "minLength": 1},
|
||||
"reason": {"type": "string", "minLength": 1}
|
||||
},
|
||||
"required": ["request", "status", "translation", "reason"]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,7 @@ from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from .capabilities import sketch_ids_required_by_contract
|
||||
from cdsl_importer.legacy_profile_adapter import lower_legacy_profiles
|
||||
from .semantic_validation import validate_semantic_cdsl
|
||||
from .sketch_solver import resolve_required_sketches
|
||||
|
||||
@@ -21,7 +22,7 @@ P3_ATOMIC_IDS = frozenset({
|
||||
})
|
||||
P4_ATOMIC_IDS = P3_ATOMIC_IDS | frozenset({"hole_wizard"})
|
||||
P6_ATOMIC_IDS = P4_ATOMIC_IDS | frozenset({"pattern_linear", "pattern_mirror"})
|
||||
P3_PROFILE_TYPES = frozenset({"analytic_contours", "circle", "circles", "annulus"})
|
||||
P3_PROFILE_TYPES = frozenset({"analytic_contours", "circle", "polygon"})
|
||||
# Static pool membership asks whether the exported history is an extrude/
|
||||
# revolve history. Whether a first cut has a preceding active body remains a
|
||||
# runtime preflight question, not a reason to erase it from the input pool.
|
||||
@@ -66,6 +67,9 @@ def is_static_phase_ready(cdsl: dict[str, Any], phase: str) -> bool:
|
||||
allowed = _PHASE_ATOMIC_IDS.get(phase)
|
||||
if allowed is None:
|
||||
raise ValueError(f"Unknown CDSL runtime phase {phase!r}")
|
||||
# Corpus pools retain historical profiles on disk but evaluate them after
|
||||
# the importer compatibility lowering used by the batch path.
|
||||
cdsl = lower_legacy_profiles(cdsl)
|
||||
semantic = validate_semantic_cdsl(cdsl)
|
||||
if semantic["unresolved"]:
|
||||
return False
|
||||
|
||||
@@ -2,10 +2,10 @@
|
||||
"schema": "cdsl.engine.schema.v1",
|
||||
"schema_version": "1.3.0",
|
||||
"cdsl_json_schema_file": "cdsl_schema.json",
|
||||
"maintenance_rule": "The semantic CDSL contract is a superset of the current runtime. runtime_supported_atomic_ids and runtime_supported_profiles must stay synchronized with runtime.py EXECUTORS, sketch_solver.py SHAPE_GENERATORS, and the package-level capability tests. Legacy llm_compiler.py and llm_engine.py are not the CDSL-only runtime contract.",
|
||||
"maintenance_rule": "The CDSL-only runtime contract is limited to direct generic profiles and runtime.py EXECUTORS. Legacy macro profiles are importer compatibility syntax and must be lowered by cdsl_importer.legacy_profile_adapter before generic runtime validation.",
|
||||
"coordinate_convention": "All profile dimensions use millimetres. Two-dimensional points are [u, v] in the sketch workplane.",
|
||||
"runtime_supported_atomic_ids": ["extrude_add_blind", "extrude_add_two_sided", "extrude_cut_blind", "revolve_add", "revolve_cut", "hole_blind", "hole_countersink", "hole_counterbore", "sphere_add", "reference_plane", "reference_axis", "hole_wizard", "fillet", "chamfer", "pattern_linear", "pattern_mirror"],
|
||||
"runtime_supported_profiles": ["circle", "annulus", "circles", "circle_grid", "rectangle", "rectangle_with_circles", "rectangle_with_fillets", "obround", "polygon", "ibone", "rectangle_with_symmetric_notches", "revolve_chamfer", "revolve_chamfer_slanted", "circle_with_arc_notches", "circular_sector_slot", "circle_with_radial_tabs", "filleted_rect_side_slots", "d_shape", "partial_ring", "partial_ring_with_arc_island", "arc_chain", "radial_slot", "patterned_cutouts", "compound_patterned_cutouts", "analytic_contours", "complex_arc_shape", "unknown_shape"],
|
||||
"runtime_supported_profiles": ["circle", "polygon", "analytic_contours"],
|
||||
"feature_atomic_ids": {
|
||||
"extrude_add_blind": {"summary": "Add the closed profile by one signed extrusion distance.", "required_params": ["distance_mm"], "optional_params": ["reverse"], "requires_sketch": true},
|
||||
"extrude_add_two_sided": {"summary": "Add the closed profile with independently captured forward and reverse terminations.", "required_params": ["distance_mm", "reverse_distance_mm"], "optional_params": ["reverse", "end_condition", "reverse_end_condition"], "requires_sketch": true},
|
||||
@@ -26,202 +26,18 @@
|
||||
},
|
||||
"profiles": {
|
||||
"circle": {
|
||||
"agent_allowed": true,
|
||||
"summary": "A single circular closed profile.",
|
||||
"required": ["radius_mm"],
|
||||
"optional": ["center"],
|
||||
"constraints": ["radius_mm > 0", "center defaults to [0, 0]"]
|
||||
},
|
||||
"annulus": {
|
||||
"agent_allowed": true,
|
||||
"summary": "A concentric ring.",
|
||||
"required": ["inner_radius_mm", "outer_radius_mm"],
|
||||
"optional": ["center"],
|
||||
"constraints": ["0 < inner_radius_mm < outer_radius_mm"]
|
||||
},
|
||||
"circles": {
|
||||
"agent_allowed": true,
|
||||
"summary": "A non-empty list of circles.",
|
||||
"required": ["items"],
|
||||
"item_schema": {"required": ["radius_mm"], "optional": ["center"]}
|
||||
},
|
||||
"circle_grid": {
|
||||
"agent_allowed": true,
|
||||
"summary": "A rectangular grid of equal circles.",
|
||||
"required": ["radius_mm", "count_x", "count_y", "spacing_x_mm", "spacing_y_mm"],
|
||||
"optional": ["origin_mm", "center_mm"],
|
||||
"constraints": ["radius_mm > 0", "count_x and count_y are integers >= 1", "use origin_mm or center_mm, not both"]
|
||||
},
|
||||
"rectangle": {
|
||||
"agent_allowed": true,
|
||||
"summary": "An axis-aligned rectangle.",
|
||||
"one_of": [["center", "width_mm", "height_mm"], ["min_mm", "max_mm"]],
|
||||
"constraints": ["width_mm > 0 and height_mm > 0 when using center"]
|
||||
},
|
||||
"rectangle_with_circles": {
|
||||
"agent_allowed": true,
|
||||
"summary": "A rectangle or filleted rectangle with optional internal circles.",
|
||||
"required": ["boundary"],
|
||||
"optional": ["circles"],
|
||||
"nested": {"boundary": "rectangle schema plus optional type=rectangle|rectangle_with_fillets and fillet_radius_mm"}
|
||||
},
|
||||
"rectangle_with_fillets": {
|
||||
"agent_allowed": true,
|
||||
"summary": "An axis-aligned rectangle with corner fillets and optional internal circles.",
|
||||
"one_of": [["center", "width_mm", "height_mm"], ["min_mm", "max_mm"]],
|
||||
"optional": ["fillet_radius_mm", "circles"],
|
||||
"constraints": ["fillet_radius_mm defaults to 0"]
|
||||
},
|
||||
"obround": {
|
||||
"agent_allowed": true,
|
||||
"summary": "A horizontal capsule/slot.",
|
||||
"required": ["length_mm", "width_mm"],
|
||||
"optional": ["center"],
|
||||
"constraints": ["length_mm > 0", "width_mm > 0", "center defaults to [0, 0]"]
|
||||
},
|
||||
"polygon": {
|
||||
"agent_allowed": true,
|
||||
"summary": "A closed straight-edge polygon.",
|
||||
"required": ["vertices"],
|
||||
"constraints": ["vertices contains at least three [u, v] points"]
|
||||
},
|
||||
"ibone": {
|
||||
"agent_allowed": true,
|
||||
"summary": "I-shaped lug with optional four holes.",
|
||||
"required": ["body_width_mm", "body_height_mm", "flange_width_mm", "flange_height_mm", "corner_radius_mm"],
|
||||
"optional": ["hole_radius_mm"]
|
||||
},
|
||||
"rectangle_with_symmetric_notches": {
|
||||
"agent_allowed": true,
|
||||
"summary": "A rectangular plate with four symmetric side notches.",
|
||||
"required": ["width_mm", "height_mm", "notch"],
|
||||
"nested": {"notch": {"required": ["y_start", "y_end", "depth_mm"], "optional": ["inner_radius_mm", "corner_radius_mm"]}}
|
||||
},
|
||||
"revolve_chamfer": {
|
||||
"agent_allowed": true,
|
||||
"summary": "Five-edge trapezoid profile for a revolve cut.",
|
||||
"required": ["axis_height_mm", "top_width_mm", "bottom_width_mm"],
|
||||
"optional": ["wall_inset_mm", "step_inset_mm", "on_axis_side"],
|
||||
"constraints": ["on_axis_side is left or right"]
|
||||
},
|
||||
"revolve_chamfer_slanted": {
|
||||
"agent_allowed": true,
|
||||
"summary": "Five-edge slanted profile for a revolve cut.",
|
||||
"required": ["axis_height_mm", "top_width_mm", "wall_height_mm", "wall_width_mm"],
|
||||
"optional": ["wall_inset_mm", "on_axis_side"],
|
||||
"constraints": ["on_axis_side is left or right"]
|
||||
},
|
||||
"circle_with_arc_notches": {
|
||||
"agent_allowed": true,
|
||||
"summary": "A circular boundary with arc-shaped notches.",
|
||||
"required": ["outer_radius_mm", "notch_radius_mm"],
|
||||
"optional": ["notch_angles_deg", "circles"],
|
||||
"constraints": ["notch_angles_deg defaults to [0, 90, 180, 270]"]
|
||||
},
|
||||
"circular_sector_slot": {
|
||||
"agent_allowed": true,
|
||||
"summary": "A circular-sector boundary with a central rectangular slot.",
|
||||
"required": ["arc_radius_mm", "slot_half_width_mm", "chord_half_mm"],
|
||||
"optional": ["circles"]
|
||||
},
|
||||
"circle_with_radial_tabs": {
|
||||
"agent_allowed": true,
|
||||
"summary": "A circle with two mirrored radial tabs.",
|
||||
"required": ["outer_radius_mm"],
|
||||
"optional": ["tab_u_half_mm", "tab_v_offset_mm", "circles"]
|
||||
},
|
||||
"filleted_rect_side_slots": {
|
||||
"agent_allowed": true,
|
||||
"summary": "A filleted rectangle with one semicircular slot at each side centre.",
|
||||
"required": ["half_width_mm", "half_height_mm", "corner_radius_mm"],
|
||||
"optional": ["slot_radius_mm", "circles"]
|
||||
},
|
||||
"d_shape": {
|
||||
"agent_allowed": true,
|
||||
"summary": "A D-shaped boundary made from one chord and one arc.",
|
||||
"required": ["radius_mm"],
|
||||
"optional": ["chord_sign", "chord_x_mm", "circles"],
|
||||
"constraints": ["chord_sign is left or right"]
|
||||
},
|
||||
"partial_ring": {
|
||||
"agent_allowed": true,
|
||||
"summary": "An annular sector.",
|
||||
"required": ["inner_radius_mm", "outer_radius_mm"],
|
||||
"optional": ["half_angle_deg", "circles"],
|
||||
"constraints": ["0 < inner_radius_mm < outer_radius_mm"]
|
||||
},
|
||||
"partial_ring_with_arc_island": {
|
||||
"agent_allowed": true,
|
||||
"summary": "One or more annular sectors with an arc island.",
|
||||
"required": ["inner_radius_mm", "outer_radius_mm", "island_radius_mm"],
|
||||
"optional": ["half_angle_deg", "island_gap_mm", "center_angles_deg", "replicas"],
|
||||
"nested": {"replicas": {"required": ["center_angle_deg"]}}
|
||||
},
|
||||
"arc_chain": {
|
||||
"agent_allowed": true,
|
||||
"summary": "A closed chain of two or more arcs, commonly used for revolve sections.",
|
||||
"required": ["arcs"],
|
||||
"optional": ["circles"],
|
||||
"item_schema": {"required": ["radius_mm", "start_angle_deg", "end_angle_deg"], "optional": ["center"]},
|
||||
"constraints": ["arcs contains at least two endpoint-connected arcs"]
|
||||
},
|
||||
"radial_slot": {
|
||||
"agent_allowed": true,
|
||||
"summary": "A rounded annular sector slot.",
|
||||
"required": ["inner_radius_mm", "outer_radius_mm", "start_angle_deg", "end_angle_deg"],
|
||||
"optional": ["circles"],
|
||||
"constraints": ["0 < inner_radius_mm < outer_radius_mm"]
|
||||
},
|
||||
"patterned_cutouts": {
|
||||
"agent_allowed": true,
|
||||
"summary": "One procedural cutout motif repeated by one procedural layout.",
|
||||
"required": ["motif", "layout"],
|
||||
"nested": {"motif": "See motif_types", "layout": "See layout_types"}
|
||||
},
|
||||
"compound_patterned_cutouts": {
|
||||
"agent_allowed": true,
|
||||
"summary": "Multiple procedural motif/layout pairs merged into one cutout sketch.",
|
||||
"required": ["patterns"],
|
||||
"nested": {"patterns": "Non-empty list of {motif, layout}; see patterned_cutouts."}
|
||||
},
|
||||
"complex_arc_shape": {
|
||||
"agent_allowed": false,
|
||||
"summary": "Legacy compiler-context fallback only. Never generate it."
|
||||
},
|
||||
"analytic_contours": {
|
||||
"agent_allowed": false,
|
||||
"summary": "Exact analytic line, arc and circle contours emitted by the Evidence v2 converter. The runtime resolves them into closed regions without compiler_context; B-spline currently produces an explicit unsupported diagnostic."
|
||||
},
|
||||
"unknown_shape": {
|
||||
"agent_allowed": false,
|
||||
"summary": "Legacy compiler-context fallback only. Never generate it."
|
||||
"summary": "Closed direct line, arc and circle contours. B-splines are unsupported."
|
||||
}
|
||||
},
|
||||
"motif_types": {
|
||||
"circle": ["radius_mm"],
|
||||
"square": ["width_mm"],
|
||||
"rectangle": ["width_mm", "height_mm"],
|
||||
"obround": ["length_mm", "width_mm"],
|
||||
"cross": ["size_mm", "arm_width_mm"],
|
||||
"d_shape_polygon": ["stem_length_mm", "nose_depth_mm", "half_height_mm"],
|
||||
"regular_hexagon": ["radius_mm"],
|
||||
"skew_hexagon": ["nominal_radius_mm"],
|
||||
"triangle": ["radius_mm"],
|
||||
"teardrop_polygon": ["width_mm", "height_mm"],
|
||||
"trapezoid": ["bottom_width_mm", "top_width_mm", "height_mm"],
|
||||
"annular_sector_polygon": ["inner_radius_mm", "outer_radius_mm", "half_angle_deg"]
|
||||
},
|
||||
"layout_types": {
|
||||
"ring": ["radius_mm", "count"],
|
||||
"angular": ["count"],
|
||||
"concentric_rings": ["rings"],
|
||||
"disc_grid": ["count_x", "count_y", "spacing_x_mm", "spacing_y_mm"],
|
||||
"open_arc": ["radius_mm", "count", "start_angle_deg", "end_angle_deg"],
|
||||
"spiral": ["count", "start_radius_mm", "radius_step_mm", "angle_step_deg"],
|
||||
"cross_lines": ["count_per_axis", "spacing_mm"],
|
||||
"x_field": ["levels", "spacing_mm"],
|
||||
"twin_strips": ["x_offset_mm", "count_y", "y_start_mm", "y_end_mm"],
|
||||
"corner_clusters": ["levels_mm"],
|
||||
"diamond_field": ["manhattan_radius", "spacing_mm"]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,25 +15,30 @@ from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
try:
|
||||
from .sketch_solver import SHAPE_GENERATORS, resolve_all_sketches
|
||||
from cdsl_importer.legacy_profile_adapter import lower_legacy_profiles
|
||||
from .sketch_solver import SHAPE_GENERATORS, resolve_all_sketches
|
||||
from .llm_compiler import compile_cdsl
|
||||
from .llm_engine import run_engine_plan
|
||||
from .translator import generate_build123d_code, normalize_to_ir
|
||||
except ImportError: # 允许直接 python rebuild.py
|
||||
from sketch_solver import SHAPE_GENERATORS, resolve_all_sketches
|
||||
from cdsl_importer.legacy_profile_adapter import lower_legacy_profiles
|
||||
from sketch_solver import SHAPE_GENERATORS, resolve_all_sketches
|
||||
from llm_compiler import compile_cdsl
|
||||
from llm_engine import run_engine_plan
|
||||
from translator import generate_build123d_code, normalize_to_ir
|
||||
|
||||
|
||||
def run_rebuild(cdsl: dict[str, Any], out_step: Path, ctx_file: Path | None = None, gold_step: Path | None = None,
|
||||
force_exact: bool = False) -> dict[str, Any]:
|
||||
def run_rebuild(cdsl: dict[str, Any], out_step: Path, ctx_file: Path | None = None, gold_step: Path | None = None,
|
||||
force_exact: bool = False) -> dict[str, Any]:
|
||||
"""主重建入口。
|
||||
|
||||
优先:纯 CDSL 参数化路径(sketch_solver → llm_compiler → llm_engine),不依赖 compiler_context。
|
||||
回退:CDSL + compiler_context 的 translator 路径。
|
||||
"""
|
||||
sketches = cdsl.get("geometry", {}).get("sketches", [])
|
||||
# Compatibility entry point only: new CDSL-only runtime calls do not use
|
||||
# macro profiles and therefore never invoke this adapter.
|
||||
cdsl = lower_legacy_profiles(cdsl)
|
||||
sketches = cdsl.get("geometry", {}).get("sketches", [])
|
||||
all_drawable = bool(sketches) and all(
|
||||
_sketch_is_cdsl_drawable(s) for s in sketches
|
||||
)
|
||||
|
||||
@@ -15,7 +15,7 @@ from .runtime_types import (
|
||||
RuntimeDiagnostic, SelectorResolution, TopologyRecord, TopologyRegistry,
|
||||
vector_add, vector_cross, vector_dot, vector_scale, vector_subtract, vector_unit,
|
||||
)
|
||||
from .sketch_solver import SHAPE_GENERATORS, resolve_required_sketches
|
||||
from .sketch_solver import CORE_SHAPE_GENERATORS, resolve_required_sketches
|
||||
|
||||
|
||||
ALL_ATOMIC_IDS = frozenset({
|
||||
@@ -758,7 +758,7 @@ def analyze_cdsl(cdsl: dict[str, Any]):
|
||||
resolved = resolve_required_sketches(
|
||||
deepcopy(cdsl), sketch_ids_required_by_contract(cdsl), errors=sketch_errors,
|
||||
)
|
||||
analyzer = CapabilityAnalyzer(atomic_ids=EXECUTORS, profile_types=SHAPE_GENERATORS)
|
||||
analyzer = CapabilityAnalyzer(atomic_ids=EXECUTORS, profile_types=CORE_SHAPE_GENERATORS)
|
||||
return analyzer.analyze(resolved, sketch_errors=sketch_errors)
|
||||
|
||||
|
||||
@@ -768,7 +768,7 @@ def rebuild_cdsl(cdsl: dict[str, Any], out_step: Path, *, strict: bool = True) -
|
||||
resolved = resolve_required_sketches(
|
||||
deepcopy(cdsl), sketch_ids_required_by_contract(cdsl), errors=sketch_errors,
|
||||
)
|
||||
analysis = CapabilityAnalyzer(atomic_ids=EXECUTORS, profile_types=SHAPE_GENERATORS).analyze(
|
||||
analysis = CapabilityAnalyzer(atomic_ids=EXECUTORS, profile_types=CORE_SHAPE_GENERATORS).analyze(
|
||||
resolved, sketch_errors=sketch_errors,
|
||||
)
|
||||
if strict and not analysis.runtime_eligible:
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user