feat: strengthen CAD experience distillation and routing
This commit is contained in:
@@ -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")
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user