feat: strengthen CAD experience distillation and routing

This commit is contained in:
Jerry
2026-07-23 20:03:26 +08:00
parent a2f3ba6a90
commit 2edf8fb4c8
67 changed files with 16045 additions and 83 deletions
@@ -0,0 +1,149 @@
{
"schema_version": "1.0",
"request": "把明确提供的 015240.step 转换成可修改的 model-spec.json,并保留原始几何作为精确基体",
"route": {
"selected_backend": "build123d",
"runner_skill": "cad",
"project": "text-to-cad",
"fallback_order": [
"simplecadapi",
"cadam"
],
"workflow_profiles": [],
"operation": "modify",
"source_mode": "exact_step_base",
"modification_mode": "model_spec_parameter_edit"
},
"source": {
"path": "source.step",
"format": "step",
"backend": "build123d",
"sha256": "55aee6a0ad74435503a1e7f108b32c9f403ae9838933e2be6728fb5b2f5cfbd4",
"immutable_exact_base": true
},
"artifacts": [
{
"path": "model-spec.json",
"role": "editable_geometry_contract"
},
{
"path": "imported_015240.step",
"role": "primary"
},
{
"path": "imported_015240.stl",
"role": "secondary"
},
{
"path": "source.step",
"role": "immutable_exact_base"
}
],
"parameters": {},
"experience": {
"library": "../../../../cad-experience-library/library.json",
"context_kind": "generalized_cad_experience_query",
"methods_applied": []
},
"assumptions": [
"单位按 STEP 几何和导入工具约定记录为 mm。",
"原 STEP 不包含可可靠恢复的原生草图、约束或特征历史;后续修改通过 model-spec.json 的命名参数和修改特征表达。",
"“选中面厚度增加”按直接建模语义解释为源面 #f22 沿其 +Z 法向外移 1.0 mm。",
"“红圈内的孔缩小”按目标水平板通孔从约 Ø4.8 mm 缩小到 Ø3.6 mm 处理。"
],
"validation": [
{
"check": "exact_base_sha256",
"passed": true,
"value": "55aee6a0ad74435503a1e7f108b32c9f403ae9838933e2be6728fb5b2f5cfbd4"
},
{
"check": "exact_base_geometry_signature",
"passed": true,
"scope": "immutable_source.step",
"size_mm": [
26.200001497,
18.906424738,
28.2
],
"center_mm": [
13.100000748,
7.453212369,
0.0
],
"solid_count": 1,
"face_count": 31,
"edge_count": 87,
"volume_mm3": 2400.466496
},
{
"check": "model_spec_parameterized_rebuild",
"passed": true,
"modified_size_mm": [
26.200001497,
18.906424738,
29.2
],
"modified_center_mm": [
13.100000748,
7.453212369,
0.5
],
"solid_count": 1,
"volume_mm3": 2476.533646
},
{
"check": "selected_face_f22_extension",
"passed": true,
"source_plane_z_mm": 14.1,
"result_plane_z_mm": 15.1,
"extension_mm": 1.0,
"result_face": "#f27"
},
{
"check": "circled_hole_resize",
"passed": true,
"center_mm": [
5.9,
11.006425,
0.0
],
"axis": "Z",
"source_diameter_mm": 4.799996,
"result_diameter_mm": 3.6,
"result_face": "#f22"
},
{
"check": "source_to_modified_cad_diff",
"passed": true,
"geometry_changed": true,
"size_delta_mm": [
0.0,
0.0,
1.0
],
"center_delta_mm": [
0.0,
0.0,
0.5
]
},
{
"check": "cad_inspect_refs_facts_planes_positioning",
"passed": true
},
{
"check": "snapshot_review",
"passed": true,
"views": [
"snapshots/iso_20260723T085707Z.png",
"snapshots/iso_opposite_20260723T085707Z.png",
"snapshots/top_ortho_20260723T085707Z.png",
"snapshots/front_ortho_20260723T085707Z.png"
]
}
],
"viewer_links": [
"http://127.0.0.1:4179/?dir=%2FUsers%2Fjerry%2Flinkhand%2FCadSet%2Ftext-to-cad%2Fmodels%2Fcad-router-v1%2Fimported_015240&file=imported_015240.step"
]
}
@@ -0,0 +1,110 @@
{
"schema_version": "1.0",
"model_spec_kind": "parametric_cad_model",
"model_id": "imported_015240",
"units": "mm",
"reconstruction": {
"mode": "exact_step_base",
"source": {
"path": "source.step",
"sha256": "55aee6a0ad74435503a1e7f108b32c9f403ae9838933e2be6728fb5b2f5cfbd4",
"geometry_signature": {
"size_mm": [
26.200001497,
18.906424738,
28.2
],
"center_mm": [
13.100000748,
7.453212369,
-0.0
],
"solid_count": 1,
"face_count": 31,
"edge_count": 87,
"volume_mm3": 2400.466496
}
},
"history_recovery": {
"status": "not_present_in_step",
"contract": "Preserve the imported B-Rep exactly as the immutable base; represent later edits as named parametric modifications."
}
},
"parameters": {
"selected_face_extension": {
"value": 1.0,
"unit": "mm",
"role": "outward_offset_of_source_face_f22",
"source_face": "#f22",
"direction": "+Z"
},
"target_hole_diameter": {
"value": 3.6,
"unit": "mm",
"role": "diameter_of_circled_horizontal_plate_hole",
"source_diameter_mm": 4.799996,
"axis": "Z"
}
},
"modifications": [
{
"id": "extend_selected_face_f22",
"operation": "add_box",
"source_face": "#f22",
"direction": "+Z",
"center": [
13.1,
-1.0,
{
"parameter": "selected_face_extension",
"scale": 0.5,
"offset": 14.1
}
],
"size": [
22.2,
2.0,
{
"parameter": "selected_face_extension"
}
],
"enabled": true
},
{
"id": "fill_original_circled_hole",
"operation": "add_cylinder",
"axis": "z",
"center": [
5.9,
11.006425,
0.0
],
"diameter": 4.799996,
"length": 4.0,
"enabled": true
},
{
"id": "cut_resized_circled_hole",
"operation": "cut_cylinder",
"axis": "z",
"center": [
5.9,
11.006425,
0.0
],
"diameter": {
"parameter": "target_hole_diameter"
},
"length": 6.0,
"enabled": true
}
],
"outputs": {
"step": "imported_015240.step",
"stl": "imported_015240.stl"
},
"validation": {
"exact_base_required": true,
"geometry_signature_tolerance_mm": 1e-07
}
}
@@ -0,0 +1,29 @@
{
"input": "imported_015240.step",
"mode": "view",
"outputs": [
{
"path": "snapshots/iso.png",
"camera": "iso"
},
{
"path": "snapshots/iso_opposite.png",
"camera": {
"direction": [-1, 1, -0.8]
}
},
{
"path": "snapshots/top_ortho.png",
"camera": "top"
},
{
"path": "snapshots/front_ortho.png",
"camera": "front"
}
],
"render": {
"viewLabels": true,
"padding": 0.12,
"sizeProfile": "diagnostic"
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 299 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 290 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 329 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 328 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 71 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

@@ -0,0 +1,372 @@
{
"ok": true,
"tokens": [
{
"line": 1,
"token": "#",
"cadPath": "targeted_015133",
"stepPath": "targeted_015133.step",
"stepHash": "97f9763fe46700d0a23fa052b08c119e7352275b4e850cb8ed8c77f85e29f4c3",
"summary": {
"kind": "part",
"occurrenceCount": 1,
"leafOccurrenceCount": 1,
"shapeCount": 1,
"faceCount": 199,
"edgeCount": 508,
"vertexCount": 0,
"bounds": {
"min": [
-106.75,
-0.0,
-38.0
],
"max": [
106.75,
78.5,
38.0
]
}
},
"selections": [],
"warnings": [],
"entryFacts": {
"size": [
213.5,
78.5,
76.0
],
"extentAxis": "x",
"center": [
0.0,
39.25,
0.0
],
"diag": 239.8343178112757,
"kind": "part"
},
"planes": [
{
"axis": "y",
"coordinate": 0.0,
"normalSign": -1,
"faceCount": 1,
"totalArea": 12646.876863,
"bbox": {
"min": [
-86.75,
-0.0,
-38.0
],
"max": [
86.75,
0.0,
38.0
]
},
"selectors": [
"f85"
]
},
{
"axis": "y",
"coordinate": 76.0,
"normalSign": 1,
"faceCount": 1,
"totalArea": 12168.786533,
"bbox": {
"min": [
-86.75,
76.0,
-38.0
],
"max": [
86.75,
76.0,
38.0
]
},
"selectors": [
"f59"
]
},
{
"axis": "z",
"coordinate": -38.00000000000001,
"normalSign": -1,
"faceCount": 3,
"totalArea": 11242.8,
"bbox": {
"min": [
-86.75,
0.0,
-38.0
],
"max": [
86.75,
76.0,
-38.0
]
},
"selectors": [
"f110",
"f86",
"f98"
]
},
{
"axis": "z",
"coordinate": 38.00000000000001,
"normalSign": 1,
"faceCount": 3,
"totalArea": 11242.8,
"bbox": {
"min": [
-86.75,
0.0,
38.0
],
"max": [
86.75,
76.0,
38.0
]
},
"selectors": [
"f60",
"f72",
"f84"
]
},
{
"axis": "x",
"coordinate": -106.75000000000001,
"normalSign": -1,
"faceCount": 5,
"totalArea": 5013.75858,
"bbox": {
"min": [
-106.75,
0.35,
-37.65
],
"max": [
-106.75,
75.65,
37.65
]
},
"selectors": [
"f140",
"f166",
"f168",
"f170",
"f172"
]
},
{
"axis": "x",
"coordinate": 106.75000000000001,
"normalSign": 1,
"faceCount": 5,
"totalArea": 5013.75858,
"bbox": {
"min": [
106.75,
0.35,
-37.65
],
"max": [
106.75,
75.65,
37.65
]
},
"selectors": [
"f158",
"f160",
"f162",
"f164",
"f55"
]
}
],
"entryPositioning": {
"kind": "part",
"bbox": {
"min": [
-106.75,
-0.0,
-38.0
],
"max": [
106.75,
78.5,
38.0
]
},
"bboxFacts": {
"size": [
213.5,
78.5,
76.0
],
"extentAxis": "x",
"center": [
0.0,
39.25,
0.0
],
"diag": 239.8343178112757
},
"majorPlanes": [
{
"axis": "y",
"coordinate": 0.0,
"normalSign": -1,
"faceCount": 1,
"totalArea": 12646.876863,
"bbox": {
"min": [
-86.75,
-0.0,
-38.0
],
"max": [
86.75,
0.0,
38.0
]
},
"selectors": [
"f85"
]
},
{
"axis": "y",
"coordinate": 76.0,
"normalSign": 1,
"faceCount": 1,
"totalArea": 12168.786533,
"bbox": {
"min": [
-86.75,
76.0,
-38.0
],
"max": [
86.75,
76.0,
38.0
]
},
"selectors": [
"f59"
]
},
{
"axis": "z",
"coordinate": -38.00000000000001,
"normalSign": -1,
"faceCount": 3,
"totalArea": 11242.8,
"bbox": {
"min": [
-86.75,
0.0,
-38.0
],
"max": [
86.75,
76.0,
-38.0
]
},
"selectors": [
"f110",
"f86",
"f98"
]
},
{
"axis": "z",
"coordinate": 38.00000000000001,
"normalSign": 1,
"faceCount": 3,
"totalArea": 11242.8,
"bbox": {
"min": [
-86.75,
0.0,
38.0
],
"max": [
86.75,
76.0,
38.0
]
},
"selectors": [
"f60",
"f72",
"f84"
]
},
{
"axis": "x",
"coordinate": -106.75000000000001,
"normalSign": -1,
"faceCount": 5,
"totalArea": 5013.75858,
"bbox": {
"min": [
-106.75,
0.35,
-37.65
],
"max": [
-106.75,
75.65,
37.65
]
},
"selectors": [
"f140",
"f166",
"f168",
"f170",
"f172"
]
},
{
"axis": "x",
"coordinate": 106.75000000000001,
"normalSign": 1,
"faceCount": 5,
"totalArea": 5013.75858,
"bbox": {
"min": [
106.75,
0.35,
-37.65
],
"max": [
106.75,
75.65,
37.65
]
},
"selectors": [
"f158",
"f160",
"f162",
"f164",
"f55"
]
}
]
}
}
],
"errors": []
}
@@ -0,0 +1,43 @@
{
"schema_version": "1.0",
"model_spec_kind": "parametric_cad_model",
"model_id": "targeted_015133",
"units": "mm",
"reconstruction": {
"mode": "exact_step_base",
"source": {
"path": "source.step",
"sha256": "97f9763fe46700d0a23fa052b08c119e7352275b4e850cb8ed8c77f85e29f4c3",
"geometry_signature": {
"size_mm": [
213.5,
78.5,
76.0
],
"center_mm": [
0.0,
39.25,
0.0
],
"solid_count": 1,
"face_count": 199,
"edge_count": 488,
"volume_mm3": 1174741.621833
}
},
"history_recovery": {
"status": "not_present_in_step",
"contract": "Preserve the imported B-Rep exactly as the immutable base; represent later edits as named parametric modifications."
}
},
"parameters": {},
"modifications": [],
"outputs": {
"step": "targeted_015133.step",
"stl": "targeted_015133.stl"
},
"validation": {
"exact_base_required": true,
"geometry_signature_tolerance_mm": 1e-07
}
}
@@ -0,0 +1,29 @@
{
"input": "targeted_015133.step",
"mode": "view",
"outputs": [
{
"path": "snapshot_iso.png",
"camera": "iso"
},
{
"path": "snapshot_iso_opposite.png",
"camera": {
"direction": [-1, 1, -0.8]
}
},
{
"path": "snapshot_top.png",
"camera": "top"
},
{
"path": "snapshot_front.png",
"camera": "front"
}
],
"render": {
"viewLabels": true,
"padding": 0.12,
"sizeProfile": "diagnostic"
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 316 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 475 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

+7 -2
View File
@@ -24,7 +24,11 @@ Choose a backend before modeling, keep its source as the editable authority, and
Add explicit signals when known: `--output step`, `--output stl`, `--manufacturing machining|printing|laser-cutting`, `--edit-source path/to/source`, `--existing-model`, `--assembly`, or `--browser-controls`.
The router auto-discovers the workspace `cad-experience-library/library.json`,
infers an experience family and feature roles from the request, and emits a
backend-neutral `design_plan`. Use `--experience-library`,
backend-neutral `design_plan`. When promoted `reconstruction_grammar`
records match, instantiate their symbolic parameter roles, datum roles,
canonical stages, and validation roles in the new part's
`model-spec.json`; fill values from explicit user dimensions first and
dimensionless distributions second. Use `--experience-library`,
`--experience-family`, or repeated `--experience-feature` only to override
those defaults.
3. Follow the selected route unless a hard environmental constraint makes it unavailable. If overriding it, record the reason in the task manifest.
@@ -49,7 +53,8 @@ Choose a backend before modeling, keep its source as the editable authority, and
`--experience-feature`; do not guess across family-scoped experience.
7. Validate against the brief. STEP-first routes must run geometry inspection and snapshot review. Mesh routes must check compilation, bounds, solidity/manifold facts when available, and snapshot review.
For experience-informed generation, record which generalized methods were
applied. Validate against the user's brief and held-out targets, never against
applied, including the reconstruction grammar ID and instantiated symbolic
roles. Validate against the user's brief and held-out targets, never against
parameters exposed from the same source case.
8. Write a `cad-task.json` manifest using the contract in `references/backend-contract.md`. Hand supported artifacts to `$cad-viewer` and return its live links.
@@ -68,7 +68,8 @@ constraints, names, or feature history. Therefore:
## Parametric modification layer
Supported V1 operations are `add_box`, `cut_box`, `add_cylinder`, and
`cut_cylinder`. Values may be literals or parameter references:
`cut_cylinder`. Values may be literals, direct parameter references, or affine
parameter expressions:
```json
{
@@ -82,6 +83,21 @@ Supported V1 operations are `add_box`, `cut_box`, `add_cylinder`, and
}
```
Use `scale` and `offset` when a tool center must stay anchored while its size
changes. For example, a positive-Z face at `z = 14.1` can remain the lower face
of an added box while `extension` changes:
```json
{
"center": [
13.1,
-1.0,
{"parameter": "extension", "scale": 0.5, "offset": 14.1}
],
"size": [22.2, 2.0, {"parameter": "extension"}]
}
```
Change a named value and rebuild:
```bash
@@ -111,11 +111,29 @@ def resolve_number(spec: dict[str, Any], value: Any, field: str) -> float:
raise ModelSpecError(f"{field} must be numeric")
if isinstance(value, (int, float)):
result = float(value)
elif isinstance(value, dict) and set(value) == {"parameter"}:
result = _parameter_value(spec, str(value["parameter"]))
elif isinstance(value, dict) and "parameter" in value:
unexpected = set(value) - {"parameter", "scale", "offset"}
if unexpected:
raise ModelSpecError(
f"{field} parameter expression has unsupported keys: "
+ ", ".join(sorted(unexpected))
)
scale = value.get("scale", 1.0)
offset = value.get("offset", 0.0)
if (
isinstance(scale, bool)
or not isinstance(scale, (int, float))
or isinstance(offset, bool)
or not isinstance(offset, (int, float))
):
raise ModelSpecError(f"{field} scale and offset must be numeric")
result = (
_parameter_value(spec, str(value["parameter"])) * float(scale)
+ float(offset)
)
else:
raise ModelSpecError(
f"{field} must be a number or {{\"parameter\": \"name\"}}"
f"{field} must be a number or a parameter expression"
)
if not math.isfinite(result):
raise ModelSpecError(f"{field} must be finite")
+110 -2
View File
@@ -76,6 +76,11 @@ EXPERIENCE_FAMILY_RULES: tuple[tuple[str, tuple[str, ...]], ...] = (
("shaft", ("shaft", "轴类", "传动轴")),
("flange", ("flange", "法兰")),
("bushing", ("bushing", "sleeve", "轴套", "衬套")),
("plate", ("plate", "mounting plate", "安装板", "平板")),
("bracket", ("bracket", "mounting bracket", "支架", "托架")),
("block", ("block", "rectangular body", "方块", "块体")),
("manifold", ("manifold", "valve body", "housing", "歧管", "阀体", "壳体")),
("rotational_part", ("roller", "pulley", "cylindrical body", "滚轮", "带轮")),
)
EXPERIENCE_FEATURE_RULES: tuple[tuple[str, tuple[str, ...]], ...] = (
@@ -90,8 +95,90 @@ EXPERIENCE_FEATURE_RULES: tuple[tuple[str, tuple[str, ...]], ...] = (
("conical_transition", ("cone", "taper", "锥", "斜面过渡")),
("gear_teeth", ("gear", "teeth", "齿轮", "齿")),
("coaxial_cylindrical_stack", ("stepped shaft", "阶梯轴", "同轴", "轴肩")),
(
"repeated_axial_hole_pattern",
(
"mounting holes",
"corner holes",
"four holes",
"multiple holes",
"bolt holes",
"安装孔",
"四个孔",
"多个孔",
),
),
("planar_dominant_body", ("plate", "bracket", "flat body", "板", "支架")),
(
"multi_level_planar_profile",
("pocket", "recess", "stepped plate", "台阶面", "凹槽", "沉台"),
),
(
"rotational_body",
("cylindrical body", "roller", "pulley", "revolved", "圆柱体", "滚轮", "回转体"),
),
(
"stepped_rotational_profile",
("stepped shaft", "multiple diameters", "shoulder", "阶梯轴", "多段直径", "轴肩"),
),
("shaft_like_body", ("shaft", "axle", "rod", "轴", "长杆")),
(
"flange_like_rotational_body",
("flange", "flanged", "法兰", "带法兰"),
),
(
"cylindrical_feature_network",
("bore", "passage", "drilled holes", "孔系", "流道", "钻孔"),
),
(
"multi_axis_passage",
("cross hole", "intersecting passage", "multi-axis", "交叉孔", "多轴孔"),
),
)
# The router vocabulary is request-facing (plate, bracket, flange), while the
# distilled library vocabulary is geometry-facing (patterned_plate,
# plate_or_bracket, flanged_rotational_part). Keep the query semantic by
# expanding a request family into compatible generalized scopes; this never
# uses held-out case geometry or instance parameters.
EXPERIENCE_FAMILY_SCOPE_ALIASES: dict[str, set[str]] = {
"flanged_hub_adapter": {
"flanged_hub_adapter",
"flanged_rotational_part",
"hollow_or_stepped_shaft",
"rotational_part",
},
"connecting_rod": {
"connecting_rod",
"patterned_prismatic_part",
"plate_or_bracket",
},
"gear": {"gear", "rotational_part"},
"shaft": {"shaft", "hollow_or_stepped_shaft", "rotational_part"},
"flange": {"flange", "flanged_rotational_part", "rotational_part"},
"bushing": {"bushing", "hollow_or_stepped_shaft", "rotational_part"},
"plate": {"plate", "patterned_plate", "plate_or_bracket"},
"bracket": {
"bracket",
"plate_or_bracket",
"patterned_plate",
"patterned_prismatic_part",
},
"block": {"block", "prismatic_block", "patterned_prismatic_part"},
"manifold": {
"manifold",
"multi_axis_manifold",
"prismatic_block",
"patterned_prismatic_part",
},
"rotational_part": {
"rotational_part",
"shaft",
"hollow_or_stepped_shaft",
"flanged_rotational_part",
},
}
def contains_any(text: str, terms: tuple[str, ...]) -> bool:
return any(term in text for term in terms)
@@ -358,13 +445,20 @@ def load_experience_library(
if item.strip()
}
normalized_family = family.strip().lower().replace(" ", "_") if family else None
compatible_scopes = (
EXPERIENCE_FAMILY_SCOPE_ALIASES.get(
normalized_family, {normalized_family}
)
if normalized_family
else set()
)
selected: list[object] = []
for item in payload.get("experiences", []):
if not isinstance(item, dict):
continue
scope = set(item.get("scope", []))
if "global" not in scope:
if not normalized_family or normalized_family not in scope:
if not compatible_scopes or scope.isdisjoint(compatible_scopes):
continue
required = set(item.get("when", {}).get("features", []))
if required and not required.issubset(requested_features):
@@ -374,6 +468,7 @@ def load_experience_library(
"schema_version": "2.0",
"context_kind": "generalized_cad_experience_query",
"family": normalized_family,
"compatible_scopes": sorted(compatible_scopes),
"requested_features": sorted(requested_features),
"experiences": selected,
"policy": {
@@ -526,6 +621,7 @@ def route(args: argparse.Namespace) -> dict[str, object]:
"check",
"repair",
"confidence",
"reconstruction_grammar",
)
if key in item
}
@@ -536,6 +632,17 @@ def route(args: argparse.Namespace) -> dict[str, object]:
)
if isinstance(item, dict)
],
"reconstruction_grammars": [
item["reconstruction_grammar"]
for item in (
experience_context.get("experiences", [])
if experience_context
else []
)
if isinstance(item, dict)
and item.get("kind") == "reconstruction_grammar"
and isinstance(item.get("reconstruction_grammar"), dict)
],
"experience_status": (
"pending_source_classification"
if needs_source_classification
@@ -575,9 +682,10 @@ def route(args: argparse.Namespace) -> dict[str, object]:
if edit_context
else [
"Resolve explicit user dimensions and preserve them as authoritative parameters.",
"Instantiate matched reconstruction grammars as symbolic model-spec.json parameters, datums, and canonical feature stages; never copy teacher dimensions.",
"Translate requested feature roles and matched generalized methods into backend operations.",
"Generate the editable source of truth and STEP-first artifact when requested.",
"Validate topology, dimensions, feature relationships, and export integrity.",
"Validate topology, dimensions, feature relationships, surface mix, and export integrity against the grammar validation roles.",
]
),
},
@@ -17,6 +17,27 @@ MODULE_SPEC.loader.exec_module(MODEL_SPEC)
class ModelSpecTests(unittest.TestCase):
def test_affine_parameter_expression(self) -> None:
payload = {
"parameters": {
"extension": {
"value": 2.0,
}
}
}
self.assertEqual(
MODEL_SPEC.resolve_number(
payload,
{
"parameter": "extension",
"scale": 0.5,
"offset": 14.1,
},
"center[2]",
),
15.1,
)
def test_import_step_exact_base_round_trip(self) -> None:
with tempfile.TemporaryDirectory() as temporary:
root = Path(temporary)
@@ -227,6 +227,115 @@ class CadRouterTests(unittest.TestCase):
plan["generalized_methods"][0]["id"],
)
def test_design_plan_exposes_promoted_reconstruction_grammar(self) -> None:
payload = {
"schema_version": "2.0",
"library_kind": "generalized_cad_experience",
"policy": {
"instance_parameters_allowed": False,
"absolute_coordinates_allowed": False,
"single_case_promotion_allowed": False,
"llm_semantic_review_required": True,
"draft_evidence_verified": True,
"router_consumable": True,
},
"experiences": [
{
"id": "reconstruction.flanged_hub_adapter.primary",
"kind": "reconstruction_grammar",
"scope": ["flanged_hub_adapter"],
"when": {"features": ["base_flange", "hollow_sleeve"]},
"guidance": "Establish shared datums before composing the envelope.",
"reconstruction_grammar": {
"parameter_roles": ["overall_long_span"],
"datum_roles": ["part_center", "primary_axis"],
"feature_roles": ["base_flange", "hollow_sleeve"],
"relation_roles": [],
"canonical_stages": [
{
"id": "establish_reference_frame",
"operation": "define_datums",
"feature_roles": [],
"reference_roles": ["part_center", "primary_axis"],
}
],
"cardinality_classes": ["cylinder_dense"],
"validation_roles": ["closed_solid"],
},
"support": 30,
"confidence": 0.9,
}
],
}
with tempfile.TemporaryDirectory() as directory:
library = Path(directory) / "library.json"
library.write_text(json.dumps(payload), encoding="utf-8")
result = route_module.route(
self.parse(
"生成一个带法兰轴套",
"--output",
"step",
"--experience-library",
str(library),
)
)
grammars = result["design_plan"]["reconstruction_grammars"]
self.assertEqual(1, len(grammars))
self.assertEqual(["overall_long_span"], grammars[0]["parameter_roles"])
def test_request_family_matches_geometry_facing_distilled_scope(self) -> None:
payload = {
"schema_version": "2.0",
"library_kind": "generalized_cad_experience",
"policy": {
"instance_parameters_allowed": False,
"absolute_coordinates_allowed": False,
"single_case_promotion_allowed": False,
"llm_semantic_review_required": True,
"draft_evidence_verified": True,
"router_consumable": True,
},
"experiences": [
{
"id": "motif.patterned_plate.holes",
"kind": "feature_motif",
"scope": ["patterned_plate"],
"when": {
"features": [
"planar_dominant_body",
"repeated_axial_hole_pattern",
]
},
"guidance": "Build the plate first, then place holes from symmetry.",
"support": 40,
"confidence": 0.94,
}
],
}
with tempfile.TemporaryDirectory() as directory:
library = Path(directory) / "library.json"
library.write_text(json.dumps(payload), encoding="utf-8")
result = route_module.route(
self.parse(
"Generate a mounting plate with four corner holes",
"--output",
"step",
"--experience-library",
str(library),
)
)
context = result["experience_context"]
self.assertEqual("plate", context["family"])
self.assertIn("patterned_plate", context["compatible_scopes"])
self.assertIn("planar_dominant_body", context["requested_features"])
self.assertIn(
"repeated_axial_hole_pattern", context["requested_features"]
)
self.assertEqual(
["motif.patterned_plate.holes"],
[item["id"] for item in context["experiences"]],
)
def test_step_edit_uses_source_and_library_not_private_case(self) -> None:
result = route_module.route(
self.parse(