refactor: add DesignIR distillation pipeline

This commit is contained in:
Jerry
2026-07-27 17:24:04 +08:00
parent 0adaac2b2a
commit 550b339bcc
1128 changed files with 10248 additions and 426666 deletions
@@ -0,0 +1,26 @@
{
"name": "designir-pipeline",
"version": "0.1.0+codex.20260723115752",
"description": "Reconstruct teacher STEP files as executable DesignIR 2.0, validate them independently, and promote only replay-validated generalized experience.",
"author": {
"name": "Local developer"
},
"skills": "./skills/",
"interface": {
"displayName": "DesignIR Pipeline",
"shortDescription": "Rebuild STEP as independent executable DesignIR.",
"longDescription": "DesignIR Pipeline separates teacher ingestion, isolated reconstruction, independent geometry and editability acceptance, failure clustering, and deterministic experience promotion.",
"developerName": "Local developer",
"category": "Productivity",
"capabilities": [
"Protected STEP teacher ingestion",
"Executable DesignIR 2.0",
"Teacher-free isolated reconstruction",
"Independent geometry acceptance",
"Parameter perturbation acceptance",
"Replay-validated experience promotion",
"Instance-data leakage audits"
],
"defaultPrompt": "Extract private teacher evidence, author executable DesignIR without teacher STEP access, rebuild in isolation, validate independently, and promote only repeated replay-validated experience."
}
}
+101
View File
@@ -0,0 +1,101 @@
# DesignIR Pipeline
This directory owns the STEP reconstruction and distillation boundary:
```text
teacher STEP
-> deterministic private evidence extraction
-> reconstruction agent authors DesignIR 2.0
-> isolated compiler rebuilds STEP without teacher access
-> acceptance agent compares teacher and rebuilt STEP
-> deterministic publisher promotes repeated validated experience
```
## Security boundary
| Component | Teacher STEP | Private evidence | DesignIR | Rebuilt STEP |
| --- | --- | --- | --- | --- |
| Ingestion service | read | write | no | no |
| Reconstruction agent | denied | read | write | request compilation |
| Isolated compiler | denied | no | read | write |
| Acceptance agent | read | read | read | read |
| Publisher | denied | aggregate only | schema only | reports only |
The reconstruction environment must remain functional after the teacher STEP is
removed. `scripts/designir_pipeline.py anti-cheat` rejects source STEP
references, imported B-Reps, embedded meshes, topology references, and generator
source that calls STEP import APIs.
## Workspace
Runtime data is role-separated under `workspace/`:
For the exact input, process, acceptance, and output locations, see
[`workspace/README.md`](workspace/README.md).
```text
workspace/
teacher/inbox/ uploaded STEP/STP files
teacher/evidence/ private deterministic evidence
reconstruction/inbox/ evidence packets for Agent A
reconstruction/designir/ authored DesignIR 2.0
reconstruction/output/ independently rebuilt STEP
acceptance/inbox/ manifests visible to Agent B
acceptance/reports/ geometry and editability reports
promotion/candidates/ untrusted generalized methods
promotion/replay_validated/
promotion/promoted/ deterministic published experience
quarantine/ unsupported vocabulary and failed cases
```
Only `.gitkeep` files are versioned in runtime directories. Teacher geometry,
private evidence, generated parts, and reports stay local.
## DesignIR 2.0
The executable contract is documented by
`contracts/designir-2.0.schema.json`. It describes coordinate systems, datums,
parameters, expressions, sketches, constraints, features, patterns,
attachments, construction stages, edit interfaces, and validation contracts.
It never contains teacher geometry.
The initial compiler deliberately supports a small, honest vocabulary through
both Build123d and SimpleCADAPI adapters:
- `extrude_circle`
- `extrude_rectangle`
- `add_cylinder`
- `through_hole`
- `polar_hole_pattern`
Unsupported geometry is quarantined with explicit missing capabilities instead
of embedding the source B-Rep.
The SimpleCADAPI adapter also emits its replayable
`*.simplecad.model.json` operation graph beside the STEP. DesignIR remains the
backend-neutral source of truth.
## Agents
- `agents/reconstruction-agent.md`: Agent A authors DesignIR from private
evidence and promoted methods, then requests an isolated rebuild.
- `agents/acceptance-agent.md`: Agent B independently checks geometry and
parameter perturbations against the teacher.
Neither agent publishes experience. `config/promotion-policy.json` is evaluated
by deterministic code after several independent cases pass.
## Commands
Use the CAD Python environment:
```bash
../text-to-cad/.venv/bin/python scripts/designir_pipeline.py validate part.designir.json
../text-to-cad/.venv/bin/python scripts/designir_pipeline.py compile \
part.designir.json --output rebuilt.step
../text-to-cad/.venv/bin/python scripts/designir_pipeline.py isolated-rebuild \
part.designir.json --output rebuilt.step --backend simplecadapi
../text-to-cad/.venv/bin/python scripts/designir_pipeline.py accept \
--teacher teacher.step --designir part.designir.json \
--rebuilt rebuilt.step --report acceptance.json
```
@@ -0,0 +1,45 @@
# Agent B — Independent Acceptance
## Mission
Independently verify that DesignIR reconstructs the teacher geometry and remains
meaningfully editable.
## Allowed inputs
- Teacher STEP from the protected teacher vault.
- Candidate DesignIR.
- Reconstructed STEP.
- Deterministic compiler version.
- Modification test contract.
Do not read Agent A chain-of-thought or informal rationale.
## Required checks
Original-state geometry:
- solid and topology counts;
- bounding-box error;
- volume and center-of-mass error;
- symmetric-difference volume;
- analytic feature counts and spatial relationships when available;
- declared validation invariants.
Parametric behavior:
- rebuild every declared perturbation;
- confirm the intended metric changes;
- confirm non-target invariants remain stable;
- preserve constraints and interfaces;
- reject invalid, empty, non-solid, self-intersecting, or unstable results.
## Required output
Write one machine-readable acceptance report with separate geometry,
feature-semantic, parameter-editability, and stability sections. Never collapse
the result into an unexplained similarity score.
The agent may recommend pass, fail, or quarantine. Only the deterministic
publisher changes promotion state.
@@ -0,0 +1,10 @@
name: designir-acceptance-agent
role: accept
teacher_step_access: read
private_evidence_access: read
designir_access: read
reconstruction_output_access: read
acceptance_report_access: write
publish_access: denied
instructions: acceptance-agent.md
@@ -0,0 +1,49 @@
# Agent A — DesignIR Reconstruction
## Mission
Turn deterministic private evidence and promoted generalized experience into a
complete executable DesignIR 2.0 model. Produce design logic, not copied
geometry.
## Allowed inputs
- One private evidence packet produced by the ingestion service.
- The user modification request.
- Promoted generalized experience.
- The DesignIR schema and supported operation vocabulary.
## Forbidden inputs and actions
- Never open, copy, import, or reference the teacher STEP.
- Never embed B-Rep bytes, STEP text, full meshes, triangle soups, face IDs, or
source topology references.
- Never call `import_step`, `read_step`, `STEPControl_Reader`, or equivalent.
- Never represent the teacher as a base feature.
- Never infer unsupported geometry silently.
## Required output
Write one `*.designir.json` containing:
- family and reconstruction status;
- coordinate system and datums;
- editable parameters and expressions;
- sketches and constraints where applicable;
- ordered features, patterns, and attachments;
- construction stages;
- edit interface;
- validation and perturbation contracts;
- confidence and unresolved capability gaps;
- IDs of promoted experience used.
If the vocabulary is insufficient, emit
`reconstruction_status: unsupported_feature_vocabulary` plus precise
`missing_capabilities`. Do not approximate secretly.
## Completion gate
Request `isolated-rebuild`. Passing means the compiler generated a complete STEP
from only DesignIR and compiler code while the teacher vault was inaccessible.
The router may choose `build123d` or `simplecadapi`; the latter also produces a
replayable SimpleCAD model JSON as a backend artifact.
@@ -0,0 +1,10 @@
name: designir-reconstruction-agent
role: reconstruct
teacher_step_access: denied
private_evidence_access: read
promoted_experience_access: read
designir_access: write
reconstruction_output_access: request_only
acceptance_report_access: denied
publish_access: denied
instructions: reconstruction-agent.md
@@ -0,0 +1,308 @@
#!/usr/bin/env python3
"""Evaluate blind baseline/experience STEP reconstructions against held-out STEP."""
from __future__ import annotations
import argparse
import csv
import importlib.util
import itertools
import json
import math
from pathlib import Path
from typing import Any
import numpy as np
from build123d import import_step
from scipy.spatial import cKDTree
HERE = Path(__file__).resolve().parent
EXTRACTOR = (
HERE.parents[1]
/ "skills/cad-experience-builder/scripts/step_to_case.py"
)
def load_extractor():
spec = importlib.util.spec_from_file_location("ab50_step_to_case", EXTRACTOR)
module = importlib.util.module_from_spec(spec)
assert spec and spec.loader
spec.loader.exec_module(module)
return module
EXTRACT = load_extractor()
def rel_error(actual: float, expected: float) -> float:
return abs(actual - expected) / max(abs(expected), 1e-9)
def feature_names(payload: dict[str, Any]) -> set[str]:
result: set[str] = set()
for item in payload.get("design_ir", {}).get("features", []):
if isinstance(item, str):
result.add(item)
elif isinstance(item, dict):
name = item.get("kind") or item.get("type") or item.get("name")
if name:
result.add(str(name))
return result
def f1_score(left: set[str], right: set[str]) -> float:
if not left and not right:
return 1.0
if not left or not right:
return 0.0
overlap = len(left & right)
precision = overlap / len(right)
recall = overlap / len(left)
return 2 * precision * recall / max(precision + recall, 1e-9)
def mesh(shape, tolerance: float = 0.35) -> tuple[np.ndarray, np.ndarray]:
vertices, triangles = shape.tessellate(tolerance)
points = np.asarray([[v.X, v.Y, v.Z] for v in vertices], dtype=float)
faces = np.asarray(triangles, dtype=int)
return points, faces
def sample_surface(
vertices: np.ndarray, faces: np.ndarray, count: int, seed: int
) -> np.ndarray:
tri = vertices[faces]
cross = np.cross(tri[:, 1] - tri[:, 0], tri[:, 2] - tri[:, 0])
areas = np.linalg.norm(cross, axis=1) / 2.0
total = float(areas.sum())
if total <= 1e-12:
return vertices.copy()
rng = np.random.default_rng(seed)
chosen = rng.choice(len(faces), size=count, p=areas / total)
selected = tri[chosen]
u = rng.random(count)
v = rng.random(count)
reflected = u + v > 1.0
u[reflected] = 1.0 - u[reflected]
v[reflected] = 1.0 - v[reflected]
return (
selected[:, 0]
+ u[:, None] * (selected[:, 1] - selected[:, 0])
+ v[:, None] * (selected[:, 2] - selected[:, 0])
)
def cube_transforms() -> list[np.ndarray]:
transforms = []
for permutation in itertools.permutations(range(3)):
base = np.eye(3)[:, permutation]
for signs in itertools.product((-1.0, 1.0), repeat=3):
transforms.append(base @ np.diag(signs))
return transforms
TRANSFORMS = cube_transforms()
def surface_distance(
reference, generated, seed: int, samples: int
) -> tuple[float, float]:
ref_v, ref_f = mesh(reference)
gen_v, gen_f = mesh(generated)
ref = sample_surface(ref_v, ref_f, samples, seed)
gen = sample_surface(gen_v, gen_f, samples, seed + 1)
ref -= (ref.min(axis=0) + ref.max(axis=0)) / 2.0
gen -= (gen.min(axis=0) + gen.max(axis=0)) / 2.0
ref_tree = cKDTree(ref)
best_chamfer = math.inf
best_hausdorff = math.inf
for transform in TRANSFORMS:
candidate = gen @ transform
gen_tree = cKDTree(candidate)
d_gr = ref_tree.query(candidate, workers=-1)[0]
d_rg = gen_tree.query(ref, workers=-1)[0]
chamfer = float((d_gr.mean() + d_rg.mean()) / 2.0)
hausdorff = float(max(np.quantile(d_gr, 0.95), np.quantile(d_rg, 0.95)))
if chamfer < best_chamfer:
best_chamfer = chamfer
best_hausdorff = hausdorff
diagonal = max(float(np.linalg.norm(np.ptp(ref, axis=0))), 1e-9)
return best_chamfer / diagonal, best_hausdorff / diagonal
def task_experience(step_path: Path) -> tuple[int, int]:
task = step_path.parent / "cad-task.json"
if not task.is_file():
return 0, 0
payload = json.loads(task.read_text())
exp = payload.get("experience", {})
returned = exp.get("methods_returned")
if returned is None:
returned = len(exp.get("methods", exp.get("methods_applied", [])))
if isinstance(returned, list):
returned = len(returned)
return int(returned or 0), len(exp.get("methods_applied", []))
def evaluate_one(
case: dict[str, Any], variant: str, samples: int
) -> dict[str, Any]:
generated_path = Path(case[f"{variant}_step"])
row: dict[str, Any] = {
"id": case["id"],
"family": case["family"],
"variant": variant,
"generated": generated_path.is_file(),
}
if not generated_path.is_file():
row["error"] = "missing STEP"
return row
try:
reference = import_step(case["reference_step"])
generated = import_step(str(generated_path))
ref_case = json.loads(Path(case["case_json"]).read_text())
gen_case = EXTRACT.extract_step_case(generated_path)
ref_bbox = sorted(float(v) for v in reference.bounding_box().size)
gen_bbox = sorted(float(v) for v in generated.bounding_box().size)
bbox_error = float(
np.mean([rel_error(a, b) for a, b in zip(gen_bbox, ref_bbox)])
)
volume_error = rel_error(float(generated.volume), float(reference.volume))
area_error = rel_error(float(generated.area), float(reference.area))
face_error = rel_error(len(generated.faces()), len(reference.faces()))
edge_error = rel_error(len(generated.edges()), len(reference.edges()))
chamfer, hausdorff95 = surface_distance(
reference, generated, int(case["id"]), samples
)
feature_f1 = f1_score(feature_names(ref_case), feature_names(gen_case))
family_match = float(
ref_case["design_ir"]["part_family"]
== gen_case["design_ir"]["part_family"]
)
returned, applied = task_experience(generated_path)
# Transparent 0–100 similarity index. It is a benchmark summary, not a
# claim of manufacturing interchangeability or exact B-rep identity.
score = 100.0 * (
0.18 * math.exp(-volume_error)
+ 0.12 * math.exp(-area_error)
+ 0.16 * math.exp(-bbox_error)
+ 0.24 * math.exp(-8.0 * chamfer)
+ 0.10 * math.exp(-(face_error + edge_error) / 2.0)
+ 0.15 * feature_f1
+ 0.05 * family_match
)
row.update(
{
"valid": bool(generated.is_valid),
"solid_count": len(generated.solids()),
"reference_volume": float(reference.volume),
"generated_volume": float(generated.volume),
"volume_rel_error": volume_error,
"surface_area_rel_error": area_error,
"bbox_mape": bbox_error,
"face_rel_error": face_error,
"edge_rel_error": edge_error,
"surface_chamfer_norm": chamfer,
"surface_hausdorff95_norm": hausdorff95,
"feature_f1": feature_f1,
"family_match": family_match,
"experience_methods_returned": returned,
"experience_methods_applied": applied,
"similarity_score": score,
}
)
except Exception as exc:
row["error"] = f"{type(exc).__name__}: {exc}"
return row
def mean(rows: list[dict[str, Any]], key: str) -> float:
values = [float(row[key]) for row in rows if key in row]
return sum(values) / len(values) if values else math.nan
def write_report(rows: list[dict[str, Any]]) -> None:
complete = [row for row in rows if "similarity_score" in row]
baseline = [row for row in complete if row["variant"] == "baseline"]
experience = [row for row in complete if row["variant"] == "experience"]
by_id = {}
for row in complete:
by_id.setdefault(row["id"], {})[row["variant"]] = row
pairs = [
item for item in by_id.values() if {"baseline", "experience"} <= item.keys()
]
paired_baseline = [item["baseline"] for item in pairs]
paired_experience = [item["experience"] for item in pairs]
deltas = [
item["experience"]["similarity_score"]
- item["baseline"]["similarity_score"]
for item in pairs
]
wins = sum(delta > 0.25 for delta in deltas)
ties = sum(abs(delta) <= 0.25 for delta in deltas)
losses = sum(delta < -0.25 for delta in deltas)
methods_returned = sum(
row.get("experience_methods_returned", 0) for row in experience
)
methods_applied = sum(
row.get("experience_methods_applied", 0) for row in experience
)
report = f"""# CAD 经验库 50 件 A/B 盲重建报告
## 口径
- 测试集:固定分层抽取 50 件;这 50 件不进入经验库。
- 训练经验:其余 950 件生成的通用经验库。
- 两组输入完全相同:仅多视图图片和文字描述;生成过程禁止读取参考 STEP 和解析 JSON。
- Baseline:空经验库;Experience:950 件经验库。
- 参考 STEP 只在生成完成后用于评分。
- “相似度”是体积、面积、包围盒、表面距离、拓扑和语义特征的综合诊断分,不代表制造级 1:1 或可互换性。
## 当前完成度
- 有效 Baseline:{len(baseline)}/50
- 有效 Experience:{len(experience)}/50
- 完整配对:{len(pairs)}/50
- 经验方法返回/实际采用:{methods_returned}/{methods_applied}
## 汇总
| 指标 | 无经验 | 有经验 | 差值 |
|---|---:|---:|---:|
| 综合相似度(0–100,越高越好) | {mean(paired_baseline, "similarity_score"):.2f} | {mean(paired_experience, "similarity_score"):.2f} | {mean(paired_experience, "similarity_score") - mean(paired_baseline, "similarity_score"):+.2f} |
| 体积相对误差(越低越好) | {mean(paired_baseline, "volume_rel_error"):.3f} | {mean(paired_experience, "volume_rel_error"):.3f} | {mean(paired_experience, "volume_rel_error") - mean(paired_baseline, "volume_rel_error"):+.3f} |
| 表面积相对误差 | {mean(paired_baseline, "surface_area_rel_error"):.3f} | {mean(paired_experience, "surface_area_rel_error"):.3f} | {mean(paired_experience, "surface_area_rel_error") - mean(paired_baseline, "surface_area_rel_error"):+.3f} |
| 包围盒尺寸 MAPE | {mean(paired_baseline, "bbox_mape"):.3f} | {mean(paired_experience, "bbox_mape"):.3f} | {mean(paired_experience, "bbox_mape") - mean(paired_baseline, "bbox_mape"):+.3f} |
| 归一化表面 Chamfer | {mean(paired_baseline, "surface_chamfer_norm"):.4f} | {mean(paired_experience, "surface_chamfer_norm"):.4f} | {mean(paired_experience, "surface_chamfer_norm") - mean(paired_baseline, "surface_chamfer_norm"):+.4f} |
| 特征 F1 | {mean(paired_baseline, "feature_f1"):.3f} | {mean(paired_experience, "feature_f1"):.3f} | {mean(paired_experience, "feature_f1") - mean(paired_baseline, "feature_f1"):+.3f} |
经验组胜/平/负(±0.25 分视为平):**{wins}/{ties}/{losses}**。
"""
(HERE / "report.md").write_text(report)
def main() -> None:
parser = argparse.ArgumentParser()
parser.add_argument("--samples", type=int, default=1600)
args = parser.parse_args()
manifest = json.loads((HERE / "manifest.json").read_text())
rows = [
evaluate_one(case, variant, args.samples)
for case in manifest["cases"]
for variant in ("baseline", "experience")
]
(HERE / "metrics.json").write_text(
json.dumps(rows, indent=2, ensure_ascii=False) + "\n"
)
keys = sorted({key for row in rows for key in row})
with (HERE / "metrics.csv").open("w", newline="") as stream:
writer = csv.DictWriter(stream, fieldnames=keys)
writer.writeheader()
writer.writerows(rows)
write_report(rows)
completed = sum("similarity_score" in row for row in rows)
print(json.dumps({"evaluated": completed, "expected": 100}))
if __name__ == "__main__":
main()
@@ -0,0 +1,101 @@
bbox_mape,edge_rel_error,error,experience_methods_applied,experience_methods_returned,face_rel_error,family,family_match,feature_f1,generated,generated_volume,id,reference_volume,similarity_score,solid_count,surface_area_rel_error,surface_chamfer_norm,surface_hausdorff95_norm,valid,variant,volume_rel_error
0.0,0.5,,0,0,0.25,general_mechanical_part,1.0,0.8571428571428571,True,125663.70614359173,000166,125663.70614359173,93.81711632834559,1,6.425513440379324e-14,0.004847580881730266,0.00977766538694562,True,baseline,0.0
0.0,0.5,,0,0,0.25,general_mechanical_part,1.0,0.8571428571428571,True,125663.70614359173,000166,125663.70614359173,93.81711632834559,1,6.425513440379324e-14,0.004847580881730266,0.00977766538694562,True,experience,0.0
0.45066264681866697,0.125,,0,0,0.1111111111111111,patterned_plate,0.0,0.8571428571428571,True,106129.48703937844,004764,36749.40939347248,54.33835930595081,1,1.0309687869495758,0.05550481239248205,0.1775660613682025,True,baseline,1.8879236099568286
0.3413730293323282,0.125,,3,18,0.1111111111111111,patterned_plate,0.0,0.8571428571428571,True,88774.08881569223,004764,36749.40939347248,59.488074232257325,1,0.7876989178243013,0.0465090322155671,0.15562347480527094,True,experience,1.4156602862700836
0.5696649029982362,0.0,,0,0,0.0,plate_or_bracket,1.0,1.0,True,19016.0,011680,898496.0,60.445468751589296,1,0.9780420050604243,0.10796723414266624,0.36481967349123434,True,baseline,0.9788357432865589
0.583774250440917,0.0,,4,10,0.0,plate_or_bracket,1.0,1.0,True,13547.999999999998,011680,898496.0,59.90070139134242,1,0.9842171382062035,0.11235674854244815,0.374964117147598,True,experience,0.9849214687655816
0.18571427761876255,0.2631578947368421,,0,0,0.26666666666666666,patterned_plate,1.0,1.0,True,23882.59512641282,016033,44618.37587532573,81.50778192907364,1,0.34116223818430896,0.018452946285241585,0.08713798240651745,True,baseline,0.4647363410728704
0.3238095238095235,0.02631578947368421,,3,10,0.06666666666666667,patterned_plate,1.0,0.9411764705882353,True,13866.369040708962,016033,44618.37587532573,75.91174289423121,1,0.5000404499582691,0.026851094204940862,0.12169100582216402,True,experience,0.6892229094251465
0.5959595959595959,0.2857142857142857,,0,0,0.28125,flanged_rotational_part,1.0,1.0,True,118756.98485692723,020543,24145.808587596228,51.56160371886595,1,1.5105685440071281,0.08451218347517844,0.22747541674825858,True,baseline,3.9183271053483395
0.23232323232323235,0.05194805194805195,,0,0,0.1875,flanged_rotational_part,1.0,0.888888888888889,True,55638.90609255368,020543,24145.808587596228,69.33575293745653,1,0.4640684949891876,0.042969081471212464,0.11104259063963073,True,experience,1.3042883774509648
0.06666666666666685,0.2,,0,0,0.19480519480519481,patterned_prismatic_part,1.0,1.0,True,569438.4483554161,024675,580617.6091315141,91.92371160771847,1,0.2739018704584683,0.011073315847120254,0.07352156562273186,True,baseline,0.019253912730652068
0.5777777777777777,0.8933333333333333,,0,0,1.4025974025974026,patterned_prismatic_part,1.0,0.9333333333333333,True,38876.03946418978,024675,580617.6091315141,56.84188202691976,1,0.8746474563374864,0.07094038930770122,0.33837892456299423,True,experience,0.9330436437807312
0.0980952380952385,0.16666666666666666,,0,0,0.14285714285714285,patterned_plate,1.0,0.9333333333333333,True,73807.58425433026,029190,131254.75510206894,83.74861954252185,1,0.1560183040026604,0.024101504873521428,0.04232654923555514,True,baseline,0.4376768735202429
0.14047619047618823,1.5277777777777777,,0,0,1.6428571428571428,patterned_plate,1.0,0.7999999999999999,True,71846.37021205797,029190,131254.75510206894,73.91053356207678,1,0.20263501345527155,0.02460078307658505,0.04143094591291666,True,experience,0.45261891535901033
0.31241830065359194,0.9285714285714286,,0,0,0.5625,patterned_plate,1.0,0.923076923076923,True,34352.594678913054,030352,60881.15044407846,73.94372815488485,1,0.48742381144539443,0.02510282770499014,0.10902462973941958,True,baseline,0.4357433388111292
0.4306604575163398,2.642857142857143,,3,10,2.0625,patterned_plate,1.0,0.7692307692307692,True,11173.345159683424,030352,60881.15044407846,59.45331217188532,1,0.6732862799472543,0.03957952772551367,0.16850566658178062,True,experience,0.8164728314398964
0.21212121212121218,0.5,,0,0,0.43243243243243246,flanged_rotational_part,1.0,0.9600000000000001,True,73578.89519968569,046014,162271.3445381513,75.92217396437314,1,0.333964798541362,0.033942930350360764,0.05126001418573268,True,baseline,0.5465687709120651
0.13636363636363644,0.5,,4,10,0.43243243243243246,flanged_rotational_part,1.0,0.9600000000000001,True,111532.3028776836,046014,162271.3445381513,81.61765078194381,1,0.24200901291799348,0.02662187229047825,0.043202965297600246,True,experience,0.31268023202050027
0.6851851851851847,0.3709677419354839,,0,0,0.34782608695652173,patterned_prismatic_part,1.0,0.823529411764706,True,167251.53322574112,047048,33461.296317388085,44.79000279500974,1,1.6260155362793598,0.11322328439048983,0.2789388077989153,True,baseline,3.9983578531842254
0.7175925925925921,0.1693548387096774,,3,18,0.2391304347826087,patterned_prismatic_part,1.0,0.823529411764706,True,169277.67589913897,047048,33461.296317388085,44.38628038601891,1,1.587008333438104,0.13259494837120114,0.321530970786483,True,experience,4.058909681606514
1.4575235605335388e-16,0.5,,0,0,0.3125,hollow_or_stepped_shaft,1.0,0.9166666666666666,True,8278002.394429736,048346,8341713.8934443,93.45850460439323,1,0.01545893719801078,0.008802226092164953,0.016975575422477428,True,baseline,0.007637698898380382
1.4575235605335388e-16,0.5,,0,0,0.3125,hollow_or_stepped_shaft,1.0,0.9166666666666666,True,8292610.800268921,048346,8341713.8934443,93.51807327218812,1,0.012238325281746575,0.008857323306102671,0.017730676062993916,True,experience,0.005886451369899948
0.02857142857142833,0.2222222222222222,,0,0,0.21212121212121213,patterned_plate,0.0,0.8571428571428571,True,315117.2806010815,053664,349435.5647059466,85.57701767939263,1,0.07576465816363272,0.012696566244914555,0.02434484580541925,True,baseline,0.0982106218459597
0.07936507936507906,0.0,,0,0,0.030303030303030304,patterned_plate,0.0,0.8571428571428571,True,282776.14120936824,053664,349435.5647059466,83.57268406993335,1,0.167398697885095,0.016320456106002173,0.05162627100789126,True,experience,0.19076313412080106
,,missing STEP,,,,patterned_prismatic_part,,,False,,063487,,,,,,,,baseline,
,,missing STEP,,,,patterned_prismatic_part,,,False,,063487,,,,,,,,experience,
0.05572210294349599,0.420863309352518,,0,0,0.4396551724137931,flanged_rotational_part,1.0,0.923076923076923,True,68680.44493144708,065487,81354.16556588435,87.21779338748289,1,0.060356551334099576,0.022572421105818447,0.053770414665160235,True,baseline,0.15578453231351141
,,missing STEP,,,,flanged_rotational_part,,,False,,065487,,,,,,,,experience,
,,missing STEP,,,,patterned_plate,,,False,,067186,,,,,,,,baseline,
,,missing STEP,,,,patterned_plate,,,False,,067186,,,,,,,,experience,
,,missing STEP,,,,rotational_part,,,False,,073706,,,,,,,,baseline,
,,missing STEP,,,,rotational_part,,,False,,073706,,,,,,,,experience,
,,missing STEP,,,,patterned_prismatic_part,,,False,,077955,,,,,,,,baseline,
,,missing STEP,,,,patterned_prismatic_part,,,False,,077955,,,,,,,,experience,
,,missing STEP,,,,prismatic_block,,,False,,082187,,,,,,,,baseline,
,,missing STEP,,,,prismatic_block,,,False,,082187,,,,,,,,experience,
,,missing STEP,,,,rotational_part,,,False,,089970,,,,,,,,baseline,
,,missing STEP,,,,rotational_part,,,False,,089970,,,,,,,,experience,
,,missing STEP,,,,shaft,,,False,,092092,,,,,,,,baseline,
,,missing STEP,,,,shaft,,,False,,092092,,,,,,,,experience,
,,missing STEP,,,,patterned_prismatic_part,,,False,,095520,,,,,,,,baseline,
,,missing STEP,,,,patterned_prismatic_part,,,False,,095520,,,,,,,,experience,
,,missing STEP,,,,patterned_prismatic_part,,,False,,102128,,,,,,,,baseline,
,,missing STEP,,,,patterned_prismatic_part,,,False,,102128,,,,,,,,experience,
,,missing STEP,,,,patterned_plate,,,False,,104465,,,,,,,,baseline,
,,missing STEP,,,,patterned_plate,,,False,,104465,,,,,,,,experience,
,,missing STEP,,,,patterned_prismatic_part,,,False,,110143,,,,,,,,baseline,
,,missing STEP,,,,patterned_prismatic_part,,,False,,110143,,,,,,,,experience,
,,missing STEP,,,,prismatic_block,,,False,,112069,,,,,,,,baseline,
,,missing STEP,,,,prismatic_block,,,False,,112069,,,,,,,,experience,
0.22361111004444456,0.6538461538461539,,0,0,0.6153846153846154,rotational_part,1.0,0.761904761904762,True,67519.74039233122,116166,147536.10631008807,71.3680012563105,1,0.38925591734814796,0.03424668647245051,0.07518718288900171,True,baseline,0.5423510754010293
0.18888888915554566,0.7307692307692307,,3,16,0.6923076923076923,rotational_part,1.0,0.8571428571428572,True,69565.58402815007,116166,147536.10631008807,73.4520592182128,1,0.365981954180488,0.032489227662340435,0.07289222461786297,True,experience,0.528484343473599
0.6746686351889055,0.45217391304347826,,0,0,0.45,multi_axis_manifold,0.0,0.7692307692307692,True,34234.94531047202,134828,349556.2389887021,48.526262227273214,1,0.9239731944207157,0.10449739414590258,0.3144594900968871,True,baseline,0.9020616956815967
0.7062506154470962,0.5043478260869565,,5,14,0.5,multi_axis_manifold,0.0,0.7692307692307692,True,38190.323227729335,134828,349556.2389887021,48.60371993717729,1,0.9152269986233839,0.09836507300701838,0.29918959165673675,True,experience,0.8907462692177447
7.3809523809523805,0.5,,0,0,0.3333333333333333,rotational_part,1.0,0.9411764705882353,True,25446.900494077883,136930,38.877209088173686,25.720060532485707,1,72.6363636363658,1.666072751413829,2.6857505921882785,True,baseline,653.545454545469
6.936507936507937,0.5,,0,0,0.3333333333333333,rotational_part,1.0,0.9411764705882353,True,21205.750411731566,136930,38.877209088173686,25.725642575854934,1,67.18181818181998,1.6549008981976585,2.6698915132197145,True,experience,544.4545454545574
,,missing STEP,,,,patterned_prismatic_part,,,False,,138022,,,,,,,,baseline,
,,missing STEP,,,,patterned_prismatic_part,,,False,,138022,,,,,,,,experience,
,,missing STEP,,,,patterned_plate,,,False,,141680,,,,,,,,baseline,
,,missing STEP,,,,patterned_plate,,,False,,141680,,,,,,,,experience,
,,missing STEP,,,,patterned_prismatic_part,,,False,,145574,,,,,,,,baseline,
,,missing STEP,,,,patterned_prismatic_part,,,False,,145574,,,,,,,,experience,
,,missing STEP,,,,prismatic_block,,,False,,150196,,,,,,,,baseline,
,,missing STEP,,,,prismatic_block,,,False,,150196,,,,,,,,experience,
,,missing STEP,,,,patterned_prismatic_part,,,False,,150587,,,,,,,,baseline,
,,missing STEP,,,,patterned_prismatic_part,,,False,,150587,,,,,,,,experience,
,,missing STEP,,,,multi_axis_manifold,,,False,,152568,,,,,,,,baseline,
,,missing STEP,,,,multi_axis_manifold,,,False,,152568,,,,,,,,experience,
,,missing STEP,,,,plate_or_bracket,,,False,,153245,,,,,,,,baseline,
,,missing STEP,,,,plate_or_bracket,,,False,,153245,,,,,,,,experience,
,,missing STEP,,,,patterned_prismatic_part,,,False,,158256,,,,,,,,baseline,
,,missing STEP,,,,patterned_prismatic_part,,,False,,158256,,,,,,,,experience,
,,missing STEP,,,,hollow_or_stepped_shaft,,,False,,162558,,,,,,,,baseline,
,,missing STEP,,,,hollow_or_stepped_shaft,,,False,,162558,,,,,,,,experience,
,,missing STEP,,,,multi_axis_manifold,,,False,,167962,,,,,,,,baseline,
,,missing STEP,,,,multi_axis_manifold,,,False,,167962,,,,,,,,experience,
,,missing STEP,,,,rotational_part,,,False,,168239,,,,,,,,baseline,
,,missing STEP,,,,rotational_part,,,False,,168239,,,,,,,,experience,
,,missing STEP,,,,patterned_prismatic_part,,,False,,174675,,,,,,,,baseline,
,,missing STEP,,,,patterned_prismatic_part,,,False,,174675,,,,,,,,experience,
,,missing STEP,,,,patterned_plate,,,False,,183151,,,,,,,,baseline,
,,missing STEP,,,,patterned_plate,,,False,,183151,,,,,,,,experience,
,,missing STEP,,,,patterned_prismatic_part,,,False,,189669,,,,,,,,baseline,
,,missing STEP,,,,patterned_prismatic_part,,,False,,189669,,,,,,,,experience,
,,missing STEP,,,,rotational_part,,,False,,194029,,,,,,,,baseline,
,,missing STEP,,,,rotational_part,,,False,,194029,,,,,,,,experience,
,,missing STEP,,,,prismatic_block,,,False,,201547,,,,,,,,baseline,
,,missing STEP,,,,prismatic_block,,,False,,201547,,,,,,,,experience,
,,missing STEP,,,,patterned_prismatic_part,,,False,,204730,,,,,,,,baseline,
,,missing STEP,,,,patterned_prismatic_part,,,False,,204730,,,,,,,,experience,
,,missing STEP,,,,patterned_prismatic_part,,,False,,211326,,,,,,,,baseline,
,,missing STEP,,,,patterned_prismatic_part,,,False,,211326,,,,,,,,experience,
,,missing STEP,,,,patterned_plate,,,False,,214404,,,,,,,,baseline,
,,missing STEP,,,,patterned_plate,,,False,,214404,,,,,,,,experience,
,,missing STEP,,,,patterned_prismatic_part,,,False,,229115,,,,,,,,baseline,
,,missing STEP,,,,patterned_prismatic_part,,,False,,229115,,,,,,,,experience,
,,missing STEP,,,,hybrid_freeform_part,,,False,,234097,,,,,,,,baseline,
,,missing STEP,,,,hybrid_freeform_part,,,False,,234097,,,,,,,,experience,
,,missing STEP,,,,patterned_prismatic_part,,,False,,242307,,,,,,,,baseline,
,,missing STEP,,,,patterned_prismatic_part,,,False,,242307,,,,,,,,experience,
1 bbox_mape edge_rel_error error experience_methods_applied experience_methods_returned face_rel_error family family_match feature_f1 generated generated_volume id reference_volume similarity_score solid_count surface_area_rel_error surface_chamfer_norm surface_hausdorff95_norm valid variant volume_rel_error
2 0.0 0.5 0 0 0.25 general_mechanical_part 1.0 0.8571428571428571 True 125663.70614359173 000166 125663.70614359173 93.81711632834559 1 6.425513440379324e-14 0.004847580881730266 0.00977766538694562 True baseline 0.0
3 0.0 0.5 0 0 0.25 general_mechanical_part 1.0 0.8571428571428571 True 125663.70614359173 000166 125663.70614359173 93.81711632834559 1 6.425513440379324e-14 0.004847580881730266 0.00977766538694562 True experience 0.0
4 0.45066264681866697 0.125 0 0 0.1111111111111111 patterned_plate 0.0 0.8571428571428571 True 106129.48703937844 004764 36749.40939347248 54.33835930595081 1 1.0309687869495758 0.05550481239248205 0.1775660613682025 True baseline 1.8879236099568286
5 0.3413730293323282 0.125 3 18 0.1111111111111111 patterned_plate 0.0 0.8571428571428571 True 88774.08881569223 004764 36749.40939347248 59.488074232257325 1 0.7876989178243013 0.0465090322155671 0.15562347480527094 True experience 1.4156602862700836
6 0.5696649029982362 0.0 0 0 0.0 plate_or_bracket 1.0 1.0 True 19016.0 011680 898496.0 60.445468751589296 1 0.9780420050604243 0.10796723414266624 0.36481967349123434 True baseline 0.9788357432865589
7 0.583774250440917 0.0 4 10 0.0 plate_or_bracket 1.0 1.0 True 13547.999999999998 011680 898496.0 59.90070139134242 1 0.9842171382062035 0.11235674854244815 0.374964117147598 True experience 0.9849214687655816
8 0.18571427761876255 0.2631578947368421 0 0 0.26666666666666666 patterned_plate 1.0 1.0 True 23882.59512641282 016033 44618.37587532573 81.50778192907364 1 0.34116223818430896 0.018452946285241585 0.08713798240651745 True baseline 0.4647363410728704
9 0.3238095238095235 0.02631578947368421 3 10 0.06666666666666667 patterned_plate 1.0 0.9411764705882353 True 13866.369040708962 016033 44618.37587532573 75.91174289423121 1 0.5000404499582691 0.026851094204940862 0.12169100582216402 True experience 0.6892229094251465
10 0.5959595959595959 0.2857142857142857 0 0 0.28125 flanged_rotational_part 1.0 1.0 True 118756.98485692723 020543 24145.808587596228 51.56160371886595 1 1.5105685440071281 0.08451218347517844 0.22747541674825858 True baseline 3.9183271053483395
11 0.23232323232323235 0.05194805194805195 0 0 0.1875 flanged_rotational_part 1.0 0.888888888888889 True 55638.90609255368 020543 24145.808587596228 69.33575293745653 1 0.4640684949891876 0.042969081471212464 0.11104259063963073 True experience 1.3042883774509648
12 0.06666666666666685 0.2 0 0 0.19480519480519481 patterned_prismatic_part 1.0 1.0 True 569438.4483554161 024675 580617.6091315141 91.92371160771847 1 0.2739018704584683 0.011073315847120254 0.07352156562273186 True baseline 0.019253912730652068
13 0.5777777777777777 0.8933333333333333 0 0 1.4025974025974026 patterned_prismatic_part 1.0 0.9333333333333333 True 38876.03946418978 024675 580617.6091315141 56.84188202691976 1 0.8746474563374864 0.07094038930770122 0.33837892456299423 True experience 0.9330436437807312
14 0.0980952380952385 0.16666666666666666 0 0 0.14285714285714285 patterned_plate 1.0 0.9333333333333333 True 73807.58425433026 029190 131254.75510206894 83.74861954252185 1 0.1560183040026604 0.024101504873521428 0.04232654923555514 True baseline 0.4376768735202429
15 0.14047619047618823 1.5277777777777777 0 0 1.6428571428571428 patterned_plate 1.0 0.7999999999999999 True 71846.37021205797 029190 131254.75510206894 73.91053356207678 1 0.20263501345527155 0.02460078307658505 0.04143094591291666 True experience 0.45261891535901033
16 0.31241830065359194 0.9285714285714286 0 0 0.5625 patterned_plate 1.0 0.923076923076923 True 34352.594678913054 030352 60881.15044407846 73.94372815488485 1 0.48742381144539443 0.02510282770499014 0.10902462973941958 True baseline 0.4357433388111292
17 0.4306604575163398 2.642857142857143 3 10 2.0625 patterned_plate 1.0 0.7692307692307692 True 11173.345159683424 030352 60881.15044407846 59.45331217188532 1 0.6732862799472543 0.03957952772551367 0.16850566658178062 True experience 0.8164728314398964
18 0.21212121212121218 0.5 0 0 0.43243243243243246 flanged_rotational_part 1.0 0.9600000000000001 True 73578.89519968569 046014 162271.3445381513 75.92217396437314 1 0.333964798541362 0.033942930350360764 0.05126001418573268 True baseline 0.5465687709120651
19 0.13636363636363644 0.5 4 10 0.43243243243243246 flanged_rotational_part 1.0 0.9600000000000001 True 111532.3028776836 046014 162271.3445381513 81.61765078194381 1 0.24200901291799348 0.02662187229047825 0.043202965297600246 True experience 0.31268023202050027
20 0.6851851851851847 0.3709677419354839 0 0 0.34782608695652173 patterned_prismatic_part 1.0 0.823529411764706 True 167251.53322574112 047048 33461.296317388085 44.79000279500974 1 1.6260155362793598 0.11322328439048983 0.2789388077989153 True baseline 3.9983578531842254
21 0.7175925925925921 0.1693548387096774 3 18 0.2391304347826087 patterned_prismatic_part 1.0 0.823529411764706 True 169277.67589913897 047048 33461.296317388085 44.38628038601891 1 1.587008333438104 0.13259494837120114 0.321530970786483 True experience 4.058909681606514
22 1.4575235605335388e-16 0.5 0 0 0.3125 hollow_or_stepped_shaft 1.0 0.9166666666666666 True 8278002.394429736 048346 8341713.8934443 93.45850460439323 1 0.01545893719801078 0.008802226092164953 0.016975575422477428 True baseline 0.007637698898380382
23 1.4575235605335388e-16 0.5 0 0 0.3125 hollow_or_stepped_shaft 1.0 0.9166666666666666 True 8292610.800268921 048346 8341713.8934443 93.51807327218812 1 0.012238325281746575 0.008857323306102671 0.017730676062993916 True experience 0.005886451369899948
24 0.02857142857142833 0.2222222222222222 0 0 0.21212121212121213 patterned_plate 0.0 0.8571428571428571 True 315117.2806010815 053664 349435.5647059466 85.57701767939263 1 0.07576465816363272 0.012696566244914555 0.02434484580541925 True baseline 0.0982106218459597
25 0.07936507936507906 0.0 0 0 0.030303030303030304 patterned_plate 0.0 0.8571428571428571 True 282776.14120936824 053664 349435.5647059466 83.57268406993335 1 0.167398697885095 0.016320456106002173 0.05162627100789126 True experience 0.19076313412080106
26 missing STEP patterned_prismatic_part False 063487 baseline
27 missing STEP patterned_prismatic_part False 063487 experience
28 0.05572210294349599 0.420863309352518 0 0 0.4396551724137931 flanged_rotational_part 1.0 0.923076923076923 True 68680.44493144708 065487 81354.16556588435 87.21779338748289 1 0.060356551334099576 0.022572421105818447 0.053770414665160235 True baseline 0.15578453231351141
29 missing STEP flanged_rotational_part False 065487 experience
30 missing STEP patterned_plate False 067186 baseline
31 missing STEP patterned_plate False 067186 experience
32 missing STEP rotational_part False 073706 baseline
33 missing STEP rotational_part False 073706 experience
34 missing STEP patterned_prismatic_part False 077955 baseline
35 missing STEP patterned_prismatic_part False 077955 experience
36 missing STEP prismatic_block False 082187 baseline
37 missing STEP prismatic_block False 082187 experience
38 missing STEP rotational_part False 089970 baseline
39 missing STEP rotational_part False 089970 experience
40 missing STEP shaft False 092092 baseline
41 missing STEP shaft False 092092 experience
42 missing STEP patterned_prismatic_part False 095520 baseline
43 missing STEP patterned_prismatic_part False 095520 experience
44 missing STEP patterned_prismatic_part False 102128 baseline
45 missing STEP patterned_prismatic_part False 102128 experience
46 missing STEP patterned_plate False 104465 baseline
47 missing STEP patterned_plate False 104465 experience
48 missing STEP patterned_prismatic_part False 110143 baseline
49 missing STEP patterned_prismatic_part False 110143 experience
50 missing STEP prismatic_block False 112069 baseline
51 missing STEP prismatic_block False 112069 experience
52 0.22361111004444456 0.6538461538461539 0 0 0.6153846153846154 rotational_part 1.0 0.761904761904762 True 67519.74039233122 116166 147536.10631008807 71.3680012563105 1 0.38925591734814796 0.03424668647245051 0.07518718288900171 True baseline 0.5423510754010293
53 0.18888888915554566 0.7307692307692307 3 16 0.6923076923076923 rotational_part 1.0 0.8571428571428572 True 69565.58402815007 116166 147536.10631008807 73.4520592182128 1 0.365981954180488 0.032489227662340435 0.07289222461786297 True experience 0.528484343473599
54 0.6746686351889055 0.45217391304347826 0 0 0.45 multi_axis_manifold 0.0 0.7692307692307692 True 34234.94531047202 134828 349556.2389887021 48.526262227273214 1 0.9239731944207157 0.10449739414590258 0.3144594900968871 True baseline 0.9020616956815967
55 0.7062506154470962 0.5043478260869565 5 14 0.5 multi_axis_manifold 0.0 0.7692307692307692 True 38190.323227729335 134828 349556.2389887021 48.60371993717729 1 0.9152269986233839 0.09836507300701838 0.29918959165673675 True experience 0.8907462692177447
56 7.3809523809523805 0.5 0 0 0.3333333333333333 rotational_part 1.0 0.9411764705882353 True 25446.900494077883 136930 38.877209088173686 25.720060532485707 1 72.6363636363658 1.666072751413829 2.6857505921882785 True baseline 653.545454545469
57 6.936507936507937 0.5 0 0 0.3333333333333333 rotational_part 1.0 0.9411764705882353 True 21205.750411731566 136930 38.877209088173686 25.725642575854934 1 67.18181818181998 1.6549008981976585 2.6698915132197145 True experience 544.4545454545574
58 missing STEP patterned_prismatic_part False 138022 baseline
59 missing STEP patterned_prismatic_part False 138022 experience
60 missing STEP patterned_plate False 141680 baseline
61 missing STEP patterned_plate False 141680 experience
62 missing STEP patterned_prismatic_part False 145574 baseline
63 missing STEP patterned_prismatic_part False 145574 experience
64 missing STEP prismatic_block False 150196 baseline
65 missing STEP prismatic_block False 150196 experience
66 missing STEP patterned_prismatic_part False 150587 baseline
67 missing STEP patterned_prismatic_part False 150587 experience
68 missing STEP multi_axis_manifold False 152568 baseline
69 missing STEP multi_axis_manifold False 152568 experience
70 missing STEP plate_or_bracket False 153245 baseline
71 missing STEP plate_or_bracket False 153245 experience
72 missing STEP patterned_prismatic_part False 158256 baseline
73 missing STEP patterned_prismatic_part False 158256 experience
74 missing STEP hollow_or_stepped_shaft False 162558 baseline
75 missing STEP hollow_or_stepped_shaft False 162558 experience
76 missing STEP multi_axis_manifold False 167962 baseline
77 missing STEP multi_axis_manifold False 167962 experience
78 missing STEP rotational_part False 168239 baseline
79 missing STEP rotational_part False 168239 experience
80 missing STEP patterned_prismatic_part False 174675 baseline
81 missing STEP patterned_prismatic_part False 174675 experience
82 missing STEP patterned_plate False 183151 baseline
83 missing STEP patterned_plate False 183151 experience
84 missing STEP patterned_prismatic_part False 189669 baseline
85 missing STEP patterned_prismatic_part False 189669 experience
86 missing STEP rotational_part False 194029 baseline
87 missing STEP rotational_part False 194029 experience
88 missing STEP prismatic_block False 201547 baseline
89 missing STEP prismatic_block False 201547 experience
90 missing STEP patterned_prismatic_part False 204730 baseline
91 missing STEP patterned_prismatic_part False 204730 experience
92 missing STEP patterned_prismatic_part False 211326 baseline
93 missing STEP patterned_prismatic_part False 211326 experience
94 missing STEP patterned_plate False 214404 baseline
95 missing STEP patterned_plate False 214404 experience
96 missing STEP patterned_prismatic_part False 229115 baseline
97 missing STEP patterned_prismatic_part False 229115 experience
98 missing STEP hybrid_freeform_part False 234097 baseline
99 missing STEP hybrid_freeform_part False 234097 experience
100 missing STEP patterned_prismatic_part False 242307 baseline
101 missing STEP patterned_prismatic_part False 242307 experience
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,39 @@
# CAD 经验库 50 件 A/B 盲重建报告
> 运行状态:2026-07-23 因 Codex 账户用量上限中断;当前为 15/50
> 完整配对的阶段结果。系统提示可在 2026-07-29 10:24 后继续。
## 口径
- 测试集:固定分层抽取 50 件;这 50 件不进入经验库。
- 训练经验:其余 950 件生成的通用经验库。
- 两组输入完全相同:仅多视图图片和文字描述;生成过程禁止读取参考 STEP 和解析 JSON。
- Baseline:空经验库;Experience:950 件经验库。
- 参考 STEP 只在生成完成后用于评分。
- “相似度”是体积、面积、包围盒、表面距离、拓扑和语义特征的综合诊断分,不代表制造级 1:1 或可互换性。
## 当前完成度
- 有效 Baseline:16/50
- 有效 Experience:15/50
- 完整配对:15/50
- 经验方法返回/实际采用:106/28
## 汇总
| 指标 | 无经验 | 有经验 | 差值 |
|---|---:|---:|---:|
| 综合相似度(0–100,越高越好) | 69.11 | 66.64 | -2.47 |
| 体积相对误差(越低越好) | 44.519 | 37.136 | -7.383 |
| 表面积相对误差 | 5.385 | 4.997 | -0.388 |
| 包围盒尺寸 MAPE | 0.766 | 0.760 | -0.006 |
| 归一化表面 Chamfer | 0.1537 | 0.1559 | +0.0023 |
| 特征 F1 | 0.907 | 0.878 | -0.029 |
经验组胜/平/负(±0.25 分视为平):**4/4/7**。
## 阶段解释
- 经验组的体积、表面积和包围盒平均误差较低,说明通用构造经验对尺度/主体关系有一定帮助。
- 经验组的表面距离、特征 F1 和综合分暂时较差,说明部分经验会诱导多余或错误特征,当前不能宣称整体提升。
- 每组由独立的大模型运行生成;即使经验命中为 0,两次结果也可能因模型随机性不同。因此最终应完成 50 对,并对若干样本做重复运行,才能把经验效果与随机波动分开。
@@ -0,0 +1,10 @@
# STEP reconstruction benchmark
Each case stores only a protected teacher STEP in Git LFS. DesignIR,
reconstructed STEP, acceptance reports, and perturbation artifacts are produced
at runtime outside the case directory.
These two teachers were migrated from the retired Exact Base fixtures. Their old
copied-B-Rep specifications and generated outputs were intentionally removed.
They are now held-out truth for independent DesignIR reconstruction.
@@ -0,0 +1,6 @@
{
"schema_version": "1.0",
"case_id": "015133",
"teacher": "teacher.step",
"purpose": "held_out_designir_reconstruction"
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,6 @@
{
"schema_version": "1.0",
"case_id": "015240",
"teacher": "teacher.step",
"purpose": "held_out_designir_reconstruction"
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,23 @@
{
"schema_version": "1.0",
"minimum_independent_cases": 3,
"required_states": [
"geometry_validated",
"editability_validated",
"replay_validated"
],
"thresholds": {
"relative_volume_error_max": 0.01,
"center_of_mass_error_mm_max": 0.1,
"bounding_box_axis_error_mm_max": 0.1,
"symmetric_difference_ratio_max": 0.02,
"parameter_perturbation_success_rate_min": 1.0
},
"publication_guards": {
"instance_parameters_allowed": false,
"absolute_coordinates_allowed": false,
"teacher_geometry_allowed": false,
"automatic_agent_publish_allowed": false
}
}
@@ -0,0 +1,237 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://cadset.local/schemas/designir-2.0.schema.json",
"title": "DesignIR 2.0",
"type": "object",
"additionalProperties": false,
"required": [
"schema_version",
"designir_kind",
"model_id",
"family",
"units",
"reconstruction_status",
"coordinate_system",
"datums",
"parameters",
"expressions",
"sketches",
"constraints",
"features",
"patterns",
"attachments",
"construction_stages",
"edit_interface",
"validation_contract"
],
"properties": {
"schema_version": {"const": "2.0"},
"designir_kind": {"const": "executable_parametric_design"},
"model_id": {"type": "string", "pattern": "^[A-Za-z0-9._-]+$"},
"family": {"type": "string", "minLength": 1},
"units": {"const": "mm"},
"backend_hint": {
"enum": ["auto", "build123d", "simplecadapi"]
},
"reconstruction_status": {
"enum": ["ready", "partial", "unsupported_feature_vocabulary"]
},
"missing_capabilities": {
"type": "array",
"items": {"type": "string"},
"uniqueItems": true
},
"coordinate_system": {
"type": "object",
"additionalProperties": false,
"required": ["origin", "x_axis", "y_axis", "z_axis"],
"properties": {
"origin": {"$ref": "#/$defs/vector3"},
"x_axis": {"$ref": "#/$defs/vector3"},
"y_axis": {"$ref": "#/$defs/vector3"},
"z_axis": {"$ref": "#/$defs/vector3"}
}
},
"datums": {
"type": "object",
"additionalProperties": {"type": "object"}
},
"parameters": {
"type": "object",
"additionalProperties": {"$ref": "#/$defs/parameter"}
},
"expressions": {
"type": "object",
"additionalProperties": {"type": "string", "minLength": 1}
},
"sketches": {
"type": "array",
"items": {"type": "object"}
},
"constraints": {
"type": "array",
"items": {
"type": "object",
"required": ["kind", "participants"],
"properties": {
"id": {"type": "string"},
"kind": {"type": "string"},
"participants": {
"type": "array",
"items": {"type": "string"}
}
}
}
},
"features": {
"type": "array",
"items": {"$ref": "#/$defs/feature"}
},
"patterns": {
"type": "array",
"items": {"type": "object"}
},
"attachments": {
"type": "array",
"items": {"type": "object"}
},
"construction_stages": {
"type": "array",
"items": {
"type": "object",
"required": ["id", "feature_ids"],
"properties": {
"id": {"type": "string"},
"feature_ids": {
"type": "array",
"items": {"type": "string"}
}
}
}
},
"edit_interface": {
"type": "object",
"required": ["editable_parameters"],
"properties": {
"editable_parameters": {
"type": "array",
"items": {"type": "string"},
"uniqueItems": true
},
"preserved_interfaces": {
"type": "array",
"items": {"type": "string"}
}
}
},
"validation_contract": {
"type": "object",
"required": ["invariants", "perturbations"],
"properties": {
"invariants": {
"type": "array",
"items": {"type": "object"}
},
"perturbations": {
"type": "array",
"items": {
"type": "object",
"required": ["parameter", "scale"],
"properties": {
"parameter": {"type": "string"},
"scale": {"type": "number", "exclusiveMinimum": 0},
"expected_change": {"type": "string"},
"preserve": {
"type": "array",
"items": {"type": "string"}
}
}
}
},
"thresholds": {"type": "object"}
}
},
"experience": {
"type": "object",
"properties": {
"promoted_method_ids": {
"type": "array",
"items": {"type": "string"},
"uniqueItems": true
}
}
},
"confidence": {
"type": "object",
"additionalProperties": {"type": "number", "minimum": 0, "maximum": 1}
}
},
"$defs": {
"vector3": {
"type": "array",
"prefixItems": [
{"type": "number"},
{"type": "number"},
{"type": "number"}
],
"minItems": 3,
"maxItems": 3
},
"parameter": {
"type": "object",
"additionalProperties": false,
"required": ["value", "unit", "editable"],
"properties": {
"value": {"type": ["number", "integer"]},
"unit": {"enum": ["mm", "deg", "count", "ratio"]},
"editable": {"type": "boolean"},
"role": {"type": "string"},
"minimum": {"type": "number"},
"maximum": {"type": "number"}
}
},
"value": {
"oneOf": [
{"type": "number"},
{
"type": "object",
"additionalProperties": false,
"required": ["parameter"],
"properties": {"parameter": {"type": "string"}}
},
{
"type": "object",
"additionalProperties": false,
"required": ["expression"],
"properties": {"expression": {"type": "string"}}
}
]
},
"feature": {
"type": "object",
"required": ["id", "operation"],
"properties": {
"id": {"type": "string"},
"operation": {
"enum": [
"extrude_circle",
"extrude_rectangle",
"add_cylinder",
"through_hole",
"polar_hole_pattern"
]
},
"radius": {"$ref": "#/$defs/value"},
"diameter": {"$ref": "#/$defs/value"},
"height": {"$ref": "#/$defs/value"},
"width": {"$ref": "#/$defs/value"},
"depth": {"$ref": "#/$defs/value"},
"count": {"$ref": "#/$defs/value"},
"pitch_diameter": {"$ref": "#/$defs/value"},
"axis": {"type": "string"},
"center": {"$ref": "#/$defs/vector3"},
"host": {"type": "string"}
}
}
}
}
@@ -0,0 +1,77 @@
{
"schema_version": "2.0",
"designir_kind": "executable_parametric_design",
"model_id": "raised_hub_flange",
"family": "raised_hub_flange",
"units": "mm",
"backend_hint": "build123d",
"reconstruction_status": "ready",
"missing_capabilities": [],
"coordinate_system": {
"origin": [0, 0, 0],
"x_axis": [1, 0, 0],
"y_axis": [0, 1, 0],
"z_axis": [0, 0, 1]
},
"datums": {
"part_center": {"kind": "point", "at": [0, 0, 0]},
"primary_axis": {"kind": "axis", "axis": "z"},
"mounting_plane": {"kind": "plane", "normal": "primary_axis"}
},
"parameters": {
"outer_radius": {"value": 50, "unit": "mm", "editable": true, "role": "outer_radius"},
"plate_thickness": {"value": 4, "unit": "mm", "editable": true, "role": "plate_thickness"},
"bore_diameter": {"value": 20, "unit": "mm", "editable": true, "role": "bore_diameter"},
"hub_radius": {"value": 16, "unit": "mm", "editable": true, "role": "hub_radius"},
"hub_height": {"value": 1, "unit": "mm", "editable": true, "role": "hub_height"},
"hole_count": {"value": 8, "unit": "count", "editable": true, "role": "hole_count"},
"hole_diameter": {"value": 3.4, "unit": "mm", "editable": true, "role": "clearance_hole_diameter"},
"bolt_circle_diameter": {"value": 80, "unit": "mm", "editable": true, "role": "bolt_circle_diameter"}
},
"expressions": {
"total_height": "plate_thickness + hub_height"
},
"sketches": [],
"constraints": [
{"id": "shared_axis", "kind": "concentric", "participants": ["base_flange", "raised_hub", "central_bore", "mounting_pattern"]},
{"id": "uniform_pattern", "kind": "equal_angular_spacing", "participants": ["mounting_pattern"]}
],
"features": [
{"id": "base_flange", "operation": "extrude_circle", "radius": {"parameter": "outer_radius"}, "height": {"parameter": "plate_thickness"}, "axis": "primary_axis"},
{"id": "raised_hub", "operation": "add_cylinder", "radius": {"parameter": "hub_radius"}, "height": {"parameter": "hub_height"}, "center": [0, 0, 4], "axis": "primary_axis"},
{"id": "central_bore", "operation": "through_hole", "diameter": {"parameter": "bore_diameter"}, "axis": "primary_axis", "host": "base_flange"},
{"id": "mounting_pattern", "operation": "polar_hole_pattern", "count": {"parameter": "hole_count"}, "diameter": {"parameter": "hole_diameter"}, "pitch_diameter": {"parameter": "bolt_circle_diameter"}, "axis": "primary_axis", "host": "base_flange"}
],
"patterns": [
{"feature": "mounting_pattern", "kind": "polar", "axis": "primary_axis"}
],
"attachments": [
{"feature": "raised_hub", "host": "base_flange", "relation": "face_attached"}
],
"construction_stages": [
{"id": "primary_envelope", "feature_ids": ["base_flange", "raised_hub"]},
{"id": "passages", "feature_ids": ["central_bore", "mounting_pattern"]}
],
"edit_interface": {
"editable_parameters": ["outer_radius", "plate_thickness", "bore_diameter", "hub_radius", "hub_height", "hole_count", "hole_diameter", "bolt_circle_diameter"],
"preserved_interfaces": ["part_center", "primary_axis", "mounting_plane"]
},
"validation_contract": {
"invariants": [
{"kind": "single_closed_solid"},
{"kind": "concentric", "participants": ["base_flange", "central_bore"]},
{"kind": "equal_angular_spacing", "feature": "mounting_pattern"}
],
"perturbations": [
{"parameter": "plate_thickness", "scale": 1.1, "expected_change": "overall_height_increases", "preserve": ["outer_radius", "hole_count"]},
{"parameter": "bolt_circle_diameter", "scale": 1.15, "expected_change": "mounting_pattern_radius_increases", "preserve": ["outer_radius", "bore_diameter"]},
{"parameter": "hole_count", "scale": 1.125, "expected_change": "mounting_hole_count_changes", "preserve": ["outer_radius", "bore_diameter"]}
],
"thresholds": {
"relative_volume_error_max": 0.01,
"symmetric_difference_ratio_max": 0.02
}
},
"experience": {"promoted_method_ids": []},
"confidence": {"family": 1.0, "features": 1.0, "parameters": 1.0}
}
+15
View File
@@ -0,0 +1,15 @@
#!/usr/bin/env bash
set -euo pipefail
PLUGIN_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
WORKSPACE_ROOT="$(cd "$PLUGIN_ROOT/.." && pwd)"
PYTHON_BIN="${CAD_EXPERIENCE_PYTHON:-}"
if [[ -z "$PYTHON_BIN" && -x "$WORKSPACE_ROOT/text-to-cad/.venv/bin/python" ]]; then
PYTHON_BIN="$WORKSPACE_ROOT/text-to-cad/.venv/bin/python"
fi
if [[ -z "$PYTHON_BIN" ]]; then
PYTHON_BIN="python3"
fi
exec "$PYTHON_BIN" "$PLUGIN_ROOT/skills/cad-experience-builder/scripts/cad_experience.py" "$@"
@@ -0,0 +1,786 @@
#!/usr/bin/env python3
"""Compile and validate DesignIR 2.0 without depending on teacher geometry."""
from __future__ import annotations
import argparse
import ast
import copy
import json
import math
import re
import shutil
import sys
import tempfile
from pathlib import Path
from typing import Any
from build123d import Align, Box, CenterOf, Cylinder, Part, Pos, export_step, import_step
SCHEMA_VERSION = "2.0"
DESIGNIR_KIND = "executable_parametric_design"
SUPPORTED_OPERATIONS = {
"extrude_circle",
"extrude_rectangle",
"add_cylinder",
"through_hole",
"polar_hole_pattern",
}
REQUIRED_TOP_LEVEL = {
"schema_version",
"designir_kind",
"model_id",
"family",
"units",
"reconstruction_status",
"coordinate_system",
"datums",
"parameters",
"expressions",
"sketches",
"constraints",
"features",
"patterns",
"attachments",
"construction_stages",
"edit_interface",
"validation_contract",
}
FORBIDDEN_KEYS = {
"source_step",
"teacher_step",
"source_path",
"brep",
"b_rep",
"step_text",
"mesh",
"triangles",
"vertices",
"face_refs",
"surface_ids",
"topology_refs",
"base_step",
}
FORBIDDEN_TEXT = (
"source.step",
"teacher.step",
".stp",
".step",
"import_step",
"read_step",
"stepcontrol_reader",
)
SAFE_EXPRESSION_NODES = (
ast.Expression,
ast.BinOp,
ast.UnaryOp,
ast.Add,
ast.Sub,
ast.Mult,
ast.Div,
ast.Pow,
ast.Mod,
ast.USub,
ast.UAdd,
ast.Constant,
ast.Name,
ast.Load,
)
class DesignIRError(ValueError):
pass
def read_json(path: Path) -> dict[str, Any]:
try:
value = json.loads(path.read_text(encoding="utf-8"))
except (OSError, json.JSONDecodeError) as exc:
raise DesignIRError(f"Cannot read JSON {path}: {exc}") from exc
if not isinstance(value, dict):
raise DesignIRError("DesignIR must be a JSON object")
return value
def write_json(path: Path, value: dict[str, Any]) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(json.dumps(value, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
def find_forbidden(value: Any, trail: str = "$") -> str | None:
if isinstance(value, dict):
for key, child in value.items():
normalized = str(key).lower()
if normalized in FORBIDDEN_KEYS:
return f"{trail}.{key}"
found = find_forbidden(child, f"{trail}.{key}")
if found:
return found
elif isinstance(value, list):
for index, child in enumerate(value):
found = find_forbidden(child, f"{trail}[{index}]")
if found:
return found
elif isinstance(value, str):
lowered = value.lower()
if any(token in lowered for token in FORBIDDEN_TEXT):
return trail
return None
def _vector3(value: Any, label: str) -> None:
if (
not isinstance(value, list)
or len(value) != 3
or any(not isinstance(item, (int, float)) for item in value)
):
raise DesignIRError(f"{label} must be a three-number vector")
def validate_designir(payload: dict[str, Any]) -> dict[str, Any]:
missing = sorted(REQUIRED_TOP_LEVEL - set(payload))
if missing:
raise DesignIRError("Missing DesignIR fields: " + ", ".join(missing))
if payload["schema_version"] != SCHEMA_VERSION:
raise DesignIRError("DesignIR schema_version must be 2.0")
if payload["designir_kind"] != DESIGNIR_KIND:
raise DesignIRError(f"designir_kind must be {DESIGNIR_KIND}")
if payload["units"] != "mm":
raise DesignIRError("Only millimetres are supported")
if payload["reconstruction_status"] not in {
"ready",
"partial",
"unsupported_feature_vocabulary",
}:
raise DesignIRError("Invalid reconstruction_status")
forbidden = find_forbidden(payload)
if forbidden:
raise DesignIRError(f"Teacher geometry dependency is forbidden at {forbidden}")
coordinate_system = payload["coordinate_system"]
if not isinstance(coordinate_system, dict):
raise DesignIRError("coordinate_system must be an object")
for name in ("origin", "x_axis", "y_axis", "z_axis"):
_vector3(coordinate_system.get(name), f"coordinate_system.{name}")
parameters = payload["parameters"]
if not isinstance(parameters, dict):
raise DesignIRError("parameters must be an object")
for name, parameter in parameters.items():
if not isinstance(parameter, dict) or not isinstance(
parameter.get("value"), (int, float)
):
raise DesignIRError(f"Parameter {name} requires a numeric value")
if parameter.get("unit") not in {"mm", "deg", "count", "ratio"}:
raise DesignIRError(f"Parameter {name} has an unsupported unit")
if not isinstance(parameter.get("editable"), bool):
raise DesignIRError(f"Parameter {name} requires editable=true/false")
features = payload["features"]
if not isinstance(features, list) or not features:
raise DesignIRError("features must be a non-empty array")
feature_ids: set[str] = set()
for feature in features:
if not isinstance(feature, dict):
raise DesignIRError("Every feature must be an object")
feature_id = str(feature.get("id", "")).strip()
if not feature_id or feature_id in feature_ids:
raise DesignIRError("Every feature requires a unique id")
feature_ids.add(feature_id)
operation = feature.get("operation")
if operation not in SUPPORTED_OPERATIONS:
raise DesignIRError(f"Unsupported DesignIR operation: {operation}")
editable = payload["edit_interface"].get("editable_parameters", [])
unknown = sorted(set(editable) - set(parameters))
if unknown:
raise DesignIRError("Unknown editable parameters: " + ", ".join(unknown))
for test in payload["validation_contract"].get("perturbations", []):
if test.get("parameter") not in parameters:
raise DesignIRError(
f"Perturbation references unknown parameter {test.get('parameter')}"
)
return payload
def _safe_expression(expression: str, values: dict[str, float]) -> float:
try:
tree = ast.parse(expression, mode="eval")
except SyntaxError as exc:
raise DesignIRError(f"Invalid expression {expression!r}") from exc
if not all(isinstance(node, SAFE_EXPRESSION_NODES) for node in ast.walk(tree)):
raise DesignIRError(f"Unsafe expression {expression!r}")
unknown = {
node.id
for node in ast.walk(tree)
if isinstance(node, ast.Name) and node.id not in values
}
if unknown:
raise DesignIRError("Unknown expression symbols: " + ", ".join(sorted(unknown)))
return float(eval(compile(tree, "<designir-expression>", "eval"), {"__builtins__": {}}, values))
def parameter_values(payload: dict[str, Any]) -> dict[str, float]:
values = {
name: float(parameter["value"])
for name, parameter in payload["parameters"].items()
}
pending = dict(payload.get("expressions", {}))
while pending:
progressed = False
for name, expression in list(pending.items()):
try:
values[name] = _safe_expression(str(expression), values)
except DesignIRError as exc:
if "Unknown expression symbols" in str(exc):
continue
raise
del pending[name]
progressed = True
if not progressed:
raise DesignIRError(
"Unresolved or cyclic expressions: " + ", ".join(sorted(pending))
)
return values
def resolve_value(value: Any, values: dict[str, float], label: str) -> float:
if isinstance(value, (int, float)):
return float(value)
if isinstance(value, dict) and set(value) == {"parameter"}:
name = str(value["parameter"])
if name not in values:
raise DesignIRError(f"{label} references unknown parameter {name}")
return values[name]
if isinstance(value, dict) and set(value) == {"expression"}:
name = str(value["expression"])
if name not in values:
raise DesignIRError(f"{label} references unknown expression {name}")
return values[name]
raise DesignIRError(f"{label} must be a number, parameter, or expression")
def _axis_is_z(feature: dict[str, Any]) -> None:
if feature.get("axis", "primary_axis") not in {"z", "primary_axis"}:
raise DesignIRError("The initial compiler supports only the primary Z axis")
def build_shape(payload: dict[str, Any]) -> Part:
validate_designir(payload)
if payload["reconstruction_status"] != "ready":
missing = ", ".join(payload.get("missing_capabilities", [])) or "unspecified"
raise DesignIRError(
f"DesignIR is not independently rebuildable; missing capabilities: {missing}"
)
values = parameter_values(payload)
shape: Any = None
for feature in payload["features"]:
operation = feature["operation"]
_axis_is_z(feature)
if operation in {"extrude_circle", "add_cylinder"}:
radius = resolve_value(feature["radius"], values, f"{feature['id']}.radius")
height = resolve_value(feature["height"], values, f"{feature['id']}.height")
center = feature.get("center", [0, 0, 0])
_vector3(center, f"{feature['id']}.center")
tool = Pos(*map(float, center)) * Cylinder(
radius,
height,
align=(Align.CENTER, Align.CENTER, Align.MIN),
)
shape = tool if shape is None else shape + tool
elif operation == "extrude_rectangle":
width = resolve_value(feature["width"], values, f"{feature['id']}.width")
depth = resolve_value(feature["depth"], values, f"{feature['id']}.depth")
height = resolve_value(feature["height"], values, f"{feature['id']}.height")
center = feature.get("center", [0, 0, 0])
_vector3(center, f"{feature['id']}.center")
tool = Pos(*map(float, center)) * Box(
width,
depth,
height,
align=(Align.CENTER, Align.CENTER, Align.MIN),
)
shape = tool if shape is None else shape + tool
elif operation in {"through_hole", "polar_hole_pattern"}:
if shape is None:
raise DesignIRError(f"{feature['id']} requires a host solid")
diameter = resolve_value(
feature["diameter"], values, f"{feature['id']}.diameter"
)
bounds = shape.bounding_box()
cutter_height = float(bounds.size.Z) + 2.0
cutter_z = float(bounds.min.Z) - 1.0
centers: list[tuple[float, float]]
if operation == "through_hole":
center = feature.get("center", [0, 0, 0])
_vector3(center, f"{feature['id']}.center")
centers = [(float(center[0]), float(center[1]))]
else:
count = int(
round(resolve_value(feature["count"], values, f"{feature['id']}.count"))
)
pitch = resolve_value(
feature["pitch_diameter"],
values,
f"{feature['id']}.pitch_diameter",
)
if count < 1:
raise DesignIRError(f"{feature['id']}.count must be positive")
centers = [
(
math.cos(2.0 * math.pi * index / count) * pitch / 2.0,
math.sin(2.0 * math.pi * index / count) * pitch / 2.0,
)
for index in range(count)
]
for x, y in centers:
cutter = Pos(x, y, cutter_z) * Cylinder(
diameter / 2.0,
cutter_height,
align=(Align.CENTER, Align.CENTER, Align.MIN),
)
shape = shape - cutter
if shape is None:
raise DesignIRError("DesignIR produced no geometry")
result = Part(shape.wrapped)
if not list(result.solids()) or result.volume <= 0:
raise DesignIRError("DesignIR did not produce a valid solid")
return result
def build_simplecad_shape(payload: dict[str, Any]) -> tuple[Any, str]:
validate_designir(payload)
if payload["reconstruction_status"] != "ready":
missing = ", ".join(payload.get("missing_capabilities", [])) or "unspecified"
raise DesignIRError(
f"DesignIR is not independently rebuildable; missing capabilities: {missing}"
)
simplecad_src = Path(__file__).resolve().parents[2] / "SimpleCADAPI" / "src"
if str(simplecad_src) not in sys.path:
sys.path.insert(0, str(simplecad_src))
try:
import simplecadapi as scad
except ImportError as exc:
raise DesignIRError("SimpleCADAPI backend is unavailable") from exc
values = parameter_values(payload)
shape: Any = None
z_min: float | None = None
z_max: float | None = None
with scad.GraphSession() as session:
for feature in payload["features"]:
operation = feature["operation"]
_axis_is_z(feature)
if operation in {"extrude_circle", "add_cylinder"}:
radius = resolve_value(
feature["radius"], values, f"{feature['id']}.radius"
)
height = resolve_value(
feature["height"], values, f"{feature['id']}.height"
)
center = feature.get("center", [0, 0, 0])
_vector3(center, f"{feature['id']}.center")
point = tuple(map(float, center))
tool = scad.make_cylinder_rsolid(
radius=radius,
height=height,
bottom_face_center=point,
axis=(0.0, 0.0, 1.0),
)
shape = (
tool
if shape is None
else scad.union_rsolid([shape, tool], clean=True)
)
z_min = point[2] if z_min is None else min(z_min, point[2])
z_max = (
point[2] + height
if z_max is None
else max(z_max, point[2] + height)
)
elif operation == "extrude_rectangle":
width = resolve_value(
feature["width"], values, f"{feature['id']}.width"
)
depth = resolve_value(
feature["depth"], values, f"{feature['id']}.depth"
)
height = resolve_value(
feature["height"], values, f"{feature['id']}.height"
)
center = feature.get("center", [0, 0, 0])
_vector3(center, f"{feature['id']}.center")
point = tuple(map(float, center))
tool = scad.make_box_rsolid(
width=width,
height=depth,
depth=height,
bottom_face_center=point,
)
shape = (
tool
if shape is None
else scad.union_rsolid([shape, tool], clean=True)
)
z_min = point[2] if z_min is None else min(z_min, point[2])
z_max = (
point[2] + height
if z_max is None
else max(z_max, point[2] + height)
)
elif operation in {"through_hole", "polar_hole_pattern"}:
if shape is None or z_min is None or z_max is None:
raise DesignIRError(f"{feature['id']} requires a host solid")
diameter = resolve_value(
feature["diameter"], values, f"{feature['id']}.diameter"
)
if operation == "through_hole":
center = feature.get("center", [0, 0, 0])
_vector3(center, f"{feature['id']}.center")
centers = [(float(center[0]), float(center[1]))]
else:
count = int(
round(
resolve_value(
feature["count"], values, f"{feature['id']}.count"
)
)
)
pitch = resolve_value(
feature["pitch_diameter"],
values,
f"{feature['id']}.pitch_diameter",
)
if count < 1:
raise DesignIRError(
f"{feature['id']}.count must be positive"
)
centers = [
(
math.cos(2.0 * math.pi * index / count) * pitch / 2.0,
math.sin(2.0 * math.pi * index / count) * pitch / 2.0,
)
for index in range(count)
]
cutter_height = z_max - z_min + 2.0
cutters = [
scad.make_cylinder_rsolid(
radius=diameter / 2.0,
height=cutter_height,
bottom_face_center=(x, y, z_min - 1.0),
axis=(0.0, 0.0, 1.0),
)
for x, y in centers
]
shape = scad.cut_rsolid(
shape, cutters, skip_non_intersecting=False
)
print(
f"simplecad grounding {feature['id']}: volume={shape.get_volume():.6f}",
file=sys.stderr,
)
model_json = scad.export_model_json(session=session, indent=2)
if shape is None or shape.get_volume() <= 0:
raise DesignIRError("DesignIR did not produce a valid SimpleCAD solid")
return shape, model_json
def geometry_facts(shape: Any) -> dict[str, Any]:
bounds = shape.bounding_box()
center = shape.center(CenterOf.MASS)
return {
"solid_count": len(list(shape.solids())),
"face_count": len(list(shape.faces())),
"edge_count": len(list(shape.edges())),
"vertex_count": len(list(shape.vertices())),
"bounds_min_mm": [float(bounds.min.X), float(bounds.min.Y), float(bounds.min.Z)],
"bounds_max_mm": [float(bounds.max.X), float(bounds.max.Y), float(bounds.max.Z)],
"size_mm": [float(bounds.size.X), float(bounds.size.Y), float(bounds.size.Z)],
"volume_mm3": float(shape.volume),
"center_of_mass_mm": [float(center.X), float(center.Y), float(center.Z)],
}
def compile_designir(
payload: dict[str, Any], output: Path, backend: str | None = None
) -> dict[str, Any]:
selected_backend = backend or str(payload.get("backend_hint", "build123d"))
output = output.expanduser().resolve()
output.parent.mkdir(parents=True, exist_ok=True)
model_json_path: Path | None = None
if selected_backend == "build123d":
shape = build_shape(payload)
export_step(shape, output)
elif selected_backend == "simplecadapi":
shape, model_json = build_simplecad_shape(payload)
from simplecadapi import export_step as simplecad_export_step
simplecad_export_step(shapes=shape, filename=str(output))
model_json_path = output.with_suffix(".simplecad.model.json")
model_json_path.write_text(model_json, encoding="utf-8")
else:
raise DesignIRError(f"Unsupported DesignIR compiler backend: {selected_backend}")
rebuilt = import_step(output)
return {
"output": str(output),
"backend": selected_backend,
"model_json": str(model_json_path) if model_json_path else None,
"facts": geometry_facts(rebuilt),
}
def anti_cheat(payload: dict[str, Any], generator: Path | None = None) -> dict[str, Any]:
validate_designir(payload)
violations: list[str] = []
forbidden = find_forbidden(payload)
if forbidden:
violations.append(f"DesignIR teacher dependency at {forbidden}")
if generator:
text = generator.read_text(encoding="utf-8").lower()
for token in FORBIDDEN_TEXT + ("breptools", "stl", "trimesh"):
if token in text:
violations.append(f"Generator contains forbidden token {token}")
return {"valid": not violations, "violations": violations}
def isolated_rebuild(
designir_path: Path, output: Path, backend: str | None = None
) -> dict[str, Any]:
payload = read_json(designir_path)
guard = anti_cheat(payload)
if not guard["valid"]:
raise DesignIRError("; ".join(guard["violations"]))
with tempfile.TemporaryDirectory(prefix="designir-isolated-") as temporary:
isolated_root = Path(temporary)
isolated_designir = isolated_root / "model.designir.json"
write_json(isolated_designir, payload)
isolated_payload = read_json(isolated_designir)
result = compile_designir(
isolated_payload, isolated_root / "rebuilt.step", backend=backend
)
final_output = output.expanduser().resolve()
final_output.parent.mkdir(parents=True, exist_ok=True)
shutil.copy2(result["output"], final_output)
final_model_json: Path | None = None
if result.get("model_json"):
final_model_json = final_output.with_suffix(".simplecad.model.json")
shutil.copy2(str(result["model_json"]), final_model_json)
return {
"valid": True,
"teacher_step_available": False,
"designir_only": True,
"backend": result["backend"],
"output": str(final_output),
"model_json": str(final_model_json) if final_model_json else None,
"facts": geometry_facts(import_step(final_output)),
}
def _max_axis_error(first: list[float], second: list[float]) -> float:
return max(abs(a - b) for a, b in zip(first, second))
def _distance(first: list[float], second: list[float]) -> float:
return math.sqrt(sum((a - b) ** 2 for a, b in zip(first, second)))
def _symmetric_difference_volume(first: Part, second: Part) -> float:
try:
return float((first - second).volume + (second - first).volume)
except Exception as exc: # OpenCascade failures become an explicit metric state.
raise DesignIRError(f"Symmetric-difference boolean failed: {exc}") from exc
def acceptance_report(
teacher_path: Path,
rebuilt_path: Path,
payload: dict[str, Any],
) -> dict[str, Any]:
validate_designir(payload)
teacher = import_step(teacher_path.expanduser().resolve())
rebuilt = import_step(rebuilt_path.expanduser().resolve())
teacher_facts = geometry_facts(teacher)
rebuilt_facts = geometry_facts(rebuilt)
volume_base = max(teacher_facts["volume_mm3"], 1e-12)
symmetric_difference = _symmetric_difference_volume(teacher, rebuilt)
geometry = {
"teacher": teacher_facts,
"rebuilt": rebuilt_facts,
"relative_volume_error": abs(
rebuilt_facts["volume_mm3"] - teacher_facts["volume_mm3"]
)
/ volume_base,
"center_of_mass_error_mm": _distance(
rebuilt_facts["center_of_mass_mm"],
teacher_facts["center_of_mass_mm"],
),
"bounding_box_axis_error_mm": _max_axis_error(
rebuilt_facts["size_mm"], teacher_facts["size_mm"]
),
"symmetric_difference_volume_mm3": symmetric_difference,
"symmetric_difference_ratio": symmetric_difference / volume_base,
"topology_count_delta": {
key: rebuilt_facts[key] - teacher_facts[key]
for key in ("solid_count", "face_count", "edge_count", "vertex_count")
},
}
perturbations: list[dict[str, Any]] = []
with tempfile.TemporaryDirectory(prefix="designir-edits-") as temporary:
for index, test in enumerate(
payload["validation_contract"].get("perturbations", [])
):
modified = copy.deepcopy(payload)
name = test["parameter"]
original = float(modified["parameters"][name]["value"])
modified["parameters"][name]["value"] = original * float(test["scale"])
try:
output = Path(temporary) / f"perturbation-{index}.step"
result = compile_designir(modified, output)
perturbations.append(
{
"parameter": name,
"original_value": original,
"modified_value": modified["parameters"][name]["value"],
"expected_change": test.get("expected_change"),
"preserve": test.get("preserve", []),
"success": result["facts"]["solid_count"] > 0,
"facts": result["facts"],
}
)
except Exception as exc:
perturbations.append(
{"parameter": name, "success": False, "error": str(exc)}
)
success_rate = (
sum(1 for item in perturbations if item["success"]) / len(perturbations)
if perturbations
else 1.0
)
thresholds = payload["validation_contract"].get("thresholds", {})
threshold_checks = {
"relative_volume_error": geometry["relative_volume_error"]
<= float(thresholds.get("relative_volume_error_max", 0.01)),
"center_of_mass_error_mm": geometry["center_of_mass_error_mm"]
<= float(thresholds.get("center_of_mass_error_mm_max", 0.1)),
"bounding_box_axis_error_mm": geometry["bounding_box_axis_error_mm"]
<= float(thresholds.get("bounding_box_axis_error_mm_max", 0.1)),
"symmetric_difference_ratio": geometry["symmetric_difference_ratio"]
<= float(thresholds.get("symmetric_difference_ratio_max", 0.02)),
"parameter_perturbation_success_rate": success_rate
>= float(thresholds.get("parameter_perturbation_success_rate_min", 1.0)),
}
passed = all(threshold_checks.values())
return {
"schema_version": "1.0",
"report_kind": "independent_designir_acceptance",
"model_id": payload["model_id"],
"geometry": geometry,
"feature_semantics": {
"declared_feature_count": len(payload["features"]),
"declared_constraint_count": len(payload["constraints"]),
"unsupported_capabilities": payload.get("missing_capabilities", []),
},
"editability": {
"editable_parameter_count": len(
payload["edit_interface"]["editable_parameters"]
),
"perturbations": perturbations,
"parameter_perturbation_success_rate": success_rate,
},
"threshold_checks": threshold_checks,
"verdict": "pass" if passed else "fail",
"next_state": "replay_validated" if passed else "quarantine",
}
def promotion_check(reports: list[Path], policy_path: Path) -> dict[str, Any]:
policy = read_json(policy_path)
loaded = [read_json(path) for path in reports]
model_ids = {str(item.get("model_id")) for item in loaded}
passing = [item for item in loaded if item.get("next_state") == "replay_validated"]
minimum = int(policy["minimum_independent_cases"])
eligible = len(passing) >= minimum and len(model_ids) >= minimum
return {
"eligible": eligible,
"independent_case_count": len(model_ids),
"passing_case_count": len(passing),
"required_case_count": minimum,
"next_state": "promoted" if eligible else "candidate",
}
def build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(description=__doc__)
commands = parser.add_subparsers(dest="command", required=True)
validate = commands.add_parser("validate")
validate.add_argument("designir", type=Path)
guard = commands.add_parser("anti-cheat")
guard.add_argument("designir", type=Path)
guard.add_argument("--generator", type=Path)
compile_command = commands.add_parser("compile")
compile_command.add_argument("designir", type=Path)
compile_command.add_argument("--output", type=Path, required=True)
compile_command.add_argument(
"--backend", choices=("build123d", "simplecadapi")
)
isolated = commands.add_parser("isolated-rebuild")
isolated.add_argument("designir", type=Path)
isolated.add_argument("--output", type=Path, required=True)
isolated.add_argument("--backend", choices=("build123d", "simplecadapi"))
accept = commands.add_parser("accept")
accept.add_argument("--teacher", type=Path, required=True)
accept.add_argument("--designir", type=Path, required=True)
accept.add_argument("--rebuilt", type=Path, required=True)
accept.add_argument("--report", type=Path, required=True)
promote = commands.add_parser("promotion-check")
promote.add_argument("reports", type=Path, nargs="+")
promote.add_argument("--policy", type=Path, required=True)
return parser
def main(argv: list[str] | None = None) -> int:
args = build_parser().parse_args(argv)
try:
if args.command == "validate":
payload = validate_designir(read_json(args.designir))
result = {
"valid": True,
"model_id": payload["model_id"],
"supported_operations": sorted(SUPPORTED_OPERATIONS),
}
elif args.command == "anti-cheat":
result = anti_cheat(read_json(args.designir), args.generator)
elif args.command == "compile":
result = compile_designir(
read_json(args.designir), args.output, backend=args.backend
)
elif args.command == "isolated-rebuild":
result = isolated_rebuild(
args.designir, args.output, backend=args.backend
)
elif args.command == "accept":
result = acceptance_report(
args.teacher, args.rebuilt, read_json(args.designir)
)
write_json(args.report, result)
else:
result = promotion_check(args.reports, args.policy)
except (DesignIRError, OSError) as exc:
print(json.dumps({"valid": False, "error": str(exc)}, ensure_ascii=False, indent=2))
return 2
print(json.dumps(result, ensure_ascii=False, indent=2))
return 0 if result.get("valid", result.get("eligible", True)) is not False else 1
if __name__ == "__main__":
raise SystemExit(main())
@@ -0,0 +1,134 @@
---
name: cad-experience-builder
description: Batch-extract STEP files into private JSON, use the active large model to semantically distill transferable CAD methods from a sanitized batch, and deterministically verify and publish a backend-neutral experience library. Use when distilling STEP/STP corpora, reviewing candidate methods, inducing feature motifs and constraints, auditing instance-data leakage, or preparing promoted experience for cad-router and CAD backends.
---
# CAD Experience Builder
Induce methods, not copied answers. Keep source-part evidence outside the
published library and require repeated support before promoting any experience.
## Required workflow
1. Put source `.step` or `.stp` files in
`workspace/teacher/inbox/`. Only deterministic ingestion and Agent B may
access this vault.
2. Parse every new, content-unique STEP into visible private JSON under
`workspace/teacher/evidence/`:
```bash
../../scripts/cad-experience extract-folder
```
3. Prepare a sanitized semantic batch:
```bash
../../scripts/cad-experience prepare
```
4. Read `workspace/promotion/candidates/semantic-batch.json` completely. Act as the semantic
distiller: compare families, parameter roles, datum roles, canonical
reconstruction stages, feature cardinality classes, relations, prior
vocabulary, failure rules, and validation targets; then author
`workspace/promotion/candidates/experience-draft.json` using
`references/llm-review-contract.md`.
Propose methods, invariants, and family-level `reconstruction_grammar`
records, not raw co-occurrence pairs. A reconstruction grammar is a
canonical backend-neutral strategy inferred from repeated geometry; it is
not the source model's recovered feature history. Do not infer
manufacturing intent or failure causes that final B-Rep evidence cannot
support.
5. Publish only after the model draft exists:
```bash
../../scripts/cad-experience publish \
--draft ../../workspace/promotion/candidates/experience-draft.json
```
The publisher recomputes support from the private corpus, rejects numeric
instance answers, keeps weak proposals as candidates, and marks the library
router-consumable only after this verification.
6. Audit the public library. Treat any failure as a hard publishing failure:
```bash
python scripts/cad_experience.py audit \
../../workspace/promotion/promoted/library.json
```
7. Query only the published library for creation, modification, or rebuilding:
```bash
python scripts/cad_experience.py query \
../../workspace/promotion/promoted/library.json \
--family flanged_hub_adapter \
--features base_flange,hollow_sleeve,counterbore
```
8. Pass the query result, never the teacher vault, unrelated private evidence,
or a case JSON,
to `$cad-router` or a backend.
Direct `distill` without an LLM-authored draft is intentionally blocked. The
active Codex model supplies semantic judgment; no separate web API key is
required.
## STEP extraction boundary
- The V2 extractor reads analytic B-Rep surfaces, bounding boxes, topology
counts, axes, radii, and locations into a private evidence record.
- It derives scale-independent body classes, part families, rotational and
prismatic motifs, passage and pattern relationships, complexity roles, and
normalized ratio observations before sanitization.
- It also derives private exact cardinality evidence plus sanitized symbolic
parameter roles, datum roles, feature-count classes, and canonical
reconstruction stages. These are the evidence used to learn how to rebuild a
family without publishing any teacher dimensions.
- Exact source paths, hashes, dimensions, coordinates, and face references are
permitted in private case JSON and forbidden in the published library.
- Keep input/output as a human-visible staging area and induction corpus. Never
use it directly during creation, modification, or reconstruction.
- A semantic candidate is not a trusted design method until repeated,
independently sourced cases satisfy the promotion policy.
## Promotion rules
- One case is evidence, not experience.
- Publish generalized, dimension-free observations below threshold as visible
`candidate_experiences`, but never expose them to generation consumers.
- Require `min_support` independent cases and conditional `min_confidence`
within the proposal's semantic context before promotion.
- Promote feature compositions, semantic constraints, dimensionless
distributions, validation checks, and failure-repair methods.
- Reject absolute dimensions, coordinates, face references, source paths,
source hashes, and full parameter dictionaries from published experience.
- Keep generated examples out of the trusted corpus until independently
validated.
- Evaluate generalization on held-out cases that were not used for induction.
## Backend contract
The library is backend-neutral. A consumer receives:
- `feature_motif`: reusable compositions of feature types;
- `constraint`: invariant semantic relationships;
- `design_rule`: conditional modeling guidance;
- `dimensionless_distribution`: multi-case ratio ranges;
- `validation_rule`: checks independent of one part's coordinates;
- `failure_repair`: reusable detection and repair methods.
- `reconstruction_grammar`: symbolic parameters, datums, canonical feature
stages, optional/required feature roles, and validation roles for a family.
build123d, SimpleCADAPI, or another backend translates the same queried
experience into its own operations. Experience must never contain executable
teacher geometry.
## References
- Read `references/case-input-contract.md` before producing batch input.
- Read `references/experience-library-schema.md` before extending the output
schema or adding a consumer.
- Read `references/llm-review-contract.md` before authoring or revising a
semantic proposal draft.
@@ -0,0 +1,81 @@
# Case JSON input contract
Case JSON is private evidence used only during induction. It may contain exact
parameters, but the inducer never copies those fields into published experience.
Store source STEP files in `workspace/teacher/inbox` and generated private
evidence in `workspace/teacher/evidence`.
Do not pass either folder or a case JSON to cad-router or a CAD backend.
Creation, modification, and reconstruction may consume only the promoted public
experience library.
Supported fields:
```json
{
"provenance": {"source_sha256": "..."},
"design_ir": {
"part_family": "flanged_hub_adapter",
"features": [{"type": "base_flange"}, {"type": "hollow_sleeve"}],
"constraints": [{"id": "coaxial_stack", "type": "coaxial"}],
"reconstruction_evidence": {
"parameter_roles": ["overall_long_span", "coaxial_diameter_roles"],
"datum_roles": ["part_center", "primary_axis"],
"feature_roles": ["rotational_body", "coaxial_cylindrical_stack"],
"relation_roles": ["coaxial_stack"],
"canonical_stages": [
{
"id": "establish_reference_frame",
"operation": "define_datums",
"feature_roles": [],
"reference_roles": ["part_center", "primary_axis"]
}
],
"private_cardinality_evidence": {
"feature_role_count": 5,
"feature_role_count_class": "repeated"
},
"validation_roles": ["closed_solid", "coaxial_stack"]
},
"normalized_observations": [
{
"name": "sleeve_od_to_base_od",
"value": 0.35,
"numerator_role": "sleeve_outer_diameter",
"denominator_role": "base_outer_diameter"
}
]
},
"experience": {
"rules": [
{
"id": "bound_counterbore_to_host",
"scope": ["counterbore"],
"statement": "Terminate a counterbore at its host boundary.",
"repair": "Bind the cutter extent to the host feature."
}
],
"validation_targets": [
{
"id": "counterbore_host_boundary",
"check": "A counterbore does not cross an unrelated adjacent feature."
}
]
}
}
```
Rules and checks containing numeric literals are rejected because they are
likely instance answers. Ratios are accepted only through
`normalized_observations`, with an explicit numerator and denominator role.
Legacy distilled JSON can be used when it contains the fields above. Exact
`parameters`, `evidence`, centers, axes, and surface references are ignored.
V3 extractor output should also provide several independent semantic signals
per case: body-aspect class, topology-complexity role, analytic feature
networks, part family, relationship candidates, and role-named dimensionless
observations. It additionally provides symbolic reconstruction roles, canonical
reconstruction stages, and private exact cardinality evidence. Sanitization
converts exact counts into qualitative classes before LLM review. Do not encode
an absolute source dimension as a feature token.
@@ -0,0 +1,84 @@
# Published experience library
The published library uses schema version `2.0` and contains only promoted,
generalized experience:
```json
{
"schema_version": "2.0",
"library_kind": "generalized_cad_experience",
"induction_mode": "llm_proposals_with_deterministic_evidence_verification",
"policy": {
"instance_parameters_allowed": false,
"absolute_coordinates_allowed": false,
"llm_semantic_review_required": true,
"draft_evidence_verified": true,
"router_consumable": true,
"minimum_support": 20
},
"corpus_summary": {
"accepted_case_count": 1000,
"duplicate_case_count": 12
},
"experience_summary": {
"llm_proposal_count": 24,
"promoted_experience_count": 18,
"candidate_experience_count": 6,
"by_kind": [{"kind": "feature_motif", "count": 7}],
"by_scope": [{"scope": "flanged_hub_adapter", "count": 5}]
},
"experiences": [
{
"id": "motif.base_flange+hollow_sleeve",
"kind": "feature_motif",
"scope": ["flanged_hub_adapter"],
"when": {"features": ["base_flange", "hollow_sleeve"]},
"guidance": "Treat these feature roles as a reusable composition.",
"support": 243,
"confidence": 0.91
}
],
"candidate_experiences": [
{
"id": "motif.flanged_hub_adapter.base_flange+hollow_sleeve",
"kind": "feature_motif",
"scope": ["flanged_hub_adapter"],
"when": {"features": ["base_flange", "hollow_sleeve"]},
"support": 7,
"required_support": 20,
"remaining_support": 13,
"promotion_state": "candidate",
"consumer_policy": "visible_for_review_but_not_available_to_cad_router"
}
]
}
```
Allowed numeric values describe evidence strength or dimensionless
distributions: support counts, confidence, frequency, percentiles, and sample
counts. Absolute lengths, diameters, heights, radii, angles tied to a single
case, positions, and coordinates are forbidden.
Consumers query this library by requested family and feature vocabulary. They
must not receive the induction corpus, private case JSON, or
`candidate_experiences`. Only `experiences` is executable guidance.
## Reconstruction grammar
`reconstruction_grammar` is the executable bridge between generalized
experience and a backend-neutral design plan. It may contain only:
- symbolic `parameter_roles`;
- symbolic `datum_roles`;
- required or optional semantic feature and relation roles;
- canonical stages with tokenized operation, feature, and reference roles;
- qualitative cardinality classes such as `single`, `paired`, `repeated`, or
`dense`;
- validation roles.
It must not contain parameter values, exact member counts, absolute
coordinates, source identifiers, face references, or executable teacher
geometry. The publisher verifies that every promoted role and stage is present
in enough independent private cases. A consumer instantiates the grammar using
the user's dimensions, inferred dimensionless distributions, and backend
operations.
@@ -0,0 +1,97 @@
# LLM semantic review contract
Read the sanitized `work/review/semantic-batch.json`, not raw STEP geometry.
Maintain stable proposal identifiers when an existing candidate expresses the
same method.
Write `work/review/experience-draft.json` with this shape:
```json
{
"schema_version": "1.0",
"draft_kind": "llm_generalized_experience_proposals",
"proposals": [
{
"id": "constraint.part_family.shared_axis",
"kind": "constraint",
"scope": ["part_family"],
"evidence_query": {
"required_features": ["rotational_body", "central_passage"],
"required_relations": ["dominant_axis_alignment"],
"context_features": ["rotational_body", "central_passage"]
},
"guidance": "Preserve a shared axis across the participating feature roles.",
"semantic_rationale": "The relationship remains meaningful when individual dimensions change.",
"check": "Confirm that participating features retain their intended common axis."
}
]
}
```
Allowed kinds are `feature_motif`, `constraint`, `design_rule`,
`validation_rule`, `failure_repair`, `dimensionless_distribution`, and
`reconstruction_grammar`.
Distribution proposals also provide `observation_name`; the publisher computes
the distribution from private evidence and never accepts values from the draft.
A `reconstruction_grammar` proposal must additionally contain symbolic,
number-free reconstruction content:
```json
{
"id": "reconstruction.patterned_prismatic_part.primary",
"kind": "reconstruction_grammar",
"scope": ["patterned_prismatic_part"],
"evidence_query": {
"required_features": ["planar_dominant_body", "cylindrical_feature_network"],
"required_relations": [],
"context_features": ["planar_dominant_body"]
},
"guidance": "Build a datum-driven envelope before adding the passage network and repeated features.",
"semantic_rationale": "The staged method transfers across dimensional variants while preserving feature relationships.",
"reconstruction_grammar": {
"parameter_roles": ["overall_long_span", "overall_middle_span", "overall_short_span"],
"datum_roles": ["part_center", "primary_axis", "primary_transverse_plane"],
"feature_roles": ["planar_dominant_body", "cylindrical_feature_network"],
"relation_roles": [],
"canonical_stages": [
{
"id": "establish_reference_frame",
"operation": "define_datums",
"feature_roles": [],
"reference_roles": ["part_center", "primary_axis"]
},
{
"id": "construct_primary_envelope",
"operation": "extrude_profile",
"feature_roles": ["planar_dominant_body"],
"reference_roles": ["part_center", "primary_axis"]
}
],
"cardinality_classes": ["plane_dense", "cylinder_dense"],
"validation_roles": ["closed_solid", "surface_mix_consistency"]
}
}
```
Follow these rules:
- Require an evidence query composed only of semantic feature and relation
tokens present in the sanitized batch.
- Use scopes to prevent unrelated part families from supporting a proposal.
- Use `context_features` to define the conditional population used for
confidence, so a valid subtype method is not diluted by unrelated variants
in the same broad family.
- Explain why a proposal transfers across dimensions and variants.
- Avoid dimensions, coordinates, source identifiers, face references, and
numeric literals in prose.
- Do not infer build order or causal failure mechanisms from final geometry.
- Treat `canonical_stages` as a recommended reconstruction order, never as a
claim about the source CAD history.
- For reconstruction grammars, retain only roles and stages repeated by the
scoped evidence. Do not insert family knowledge that the sanitized batch
does not support.
- Prefer a small set of meaningful methods over every possible feature pair.
- Preserve earlier proposal IDs when evidence still supports the same meaning.
- Let the publisher calculate support and confidence; never claim them in the
draft.
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,767 @@
#!/usr/bin/env python3
"""Extract a private, backend-neutral case record from an analytic STEP model."""
from __future__ import annotations
import argparse
import hashlib
import json
import math
from collections import Counter, defaultdict
from pathlib import Path
from typing import Any
EXTRACTOR_VERSION = "3.0"
def _rounded(value: float) -> float:
return round(float(value), 6)
def _component(value: Any, name: str) -> float:
component = getattr(value, name)
return float(component() if callable(component) else component)
def _point(value: Any) -> list[float]:
return [_rounded(_component(value, name)) for name in ("X", "Y", "Z")]
def _direction(value: Any) -> list[float]:
return _point(value)
def _canonical_axis(direction: list[float]) -> str:
index = max(range(3), key=lambda item: abs(direction[item]))
return ("x", "y", "z")[index]
def _radial_offset(location: list[float], center: list[float], axis: str) -> float:
axes = {"x": (1, 2), "y": (0, 2), "z": (0, 1)}
first, second = axes[axis]
return math.hypot(
location[first] - center[first],
location[second] - center[second],
)
def _ratio(numerator: float, denominator: float) -> float:
return _rounded(numerator / denominator) if denominator > 1e-9 else 0.0
def _observation(
name: str, value: float, numerator_role: str, denominator_role: str
) -> dict[str, Any]:
return {
"name": name,
"value": _rounded(value),
"numerator_role": numerator_role,
"denominator_role": denominator_role,
}
def _cardinality_class(count: int) -> str:
if count <= 0:
return "absent"
if count == 1:
return "single"
if count == 2:
return "paired"
if count <= 6:
return "repeated"
return "dense"
def _surface_record(face: Any, index: int) -> dict[str, Any]:
from OCP.BRepAdaptor import BRepAdaptor_Surface
from OCP.GeomAbs import (
GeomAbs_Cone,
GeomAbs_Cylinder,
GeomAbs_Plane,
GeomAbs_Sphere,
GeomAbs_Torus,
)
adaptor = BRepAdaptor_Surface(face.wrapped)
kind = adaptor.GetType()
record: dict[str, Any] = {
"surface_id": f"face_{index}",
"area": _rounded(face.area),
"center": _point(face.center()),
}
if kind == GeomAbs_Plane:
plane = adaptor.Plane()
record.update(
{
"type": "plane",
"location": _point(plane.Location()),
"axis": _direction(plane.Axis().Direction()),
}
)
elif kind == GeomAbs_Cylinder:
cylinder = adaptor.Cylinder()
record.update(
{
"type": "cylinder",
"location": _point(cylinder.Location()),
"axis": _direction(cylinder.Axis().Direction()),
"radius": _rounded(cylinder.Radius()),
}
)
elif kind == GeomAbs_Cone:
cone = adaptor.Cone()
record.update(
{
"type": "cone",
"location": _point(cone.Location()),
"axis": _direction(cone.Axis().Direction()),
"reference_radius": _rounded(cone.RefRadius()),
"semi_angle_radians": _rounded(cone.SemiAngle()),
}
)
elif kind == GeomAbs_Sphere:
sphere = adaptor.Sphere()
record.update(
{
"type": "sphere",
"location": _point(sphere.Location()),
"radius": _rounded(sphere.Radius()),
}
)
elif kind == GeomAbs_Torus:
torus = adaptor.Torus()
record.update(
{
"type": "torus",
"location": _point(torus.Location()),
"axis": _direction(torus.Axis().Direction()),
"major_radius": _rounded(torus.MajorRadius()),
"minor_radius": _rounded(torus.MinorRadius()),
}
)
else:
record["type"] = "other"
return record
def _semantic_summary(
surfaces: list[dict[str, Any]], bbox_center: list[float], bbox_size: list[float]
) -> tuple[str, list[dict[str, Any]], list[dict[str, Any]], dict[str, Any]]:
cylinders = [item for item in surfaces if item["type"] == "cylinder"]
cones = [item for item in surfaces if item["type"] == "cone"]
axis_votes: Counter[str] = Counter()
for surface in cylinders + cones:
axis_votes[_canonical_axis(surface["axis"])] += max(surface["area"], 1.0)
dominant_axis = axis_votes.most_common(1)[0][0] if axis_votes else "z"
scale = max(bbox_size) if bbox_size else 1.0
sorted_spans = sorted((max(float(value), 1e-9) for value in bbox_size))
short_span, middle_span, long_span = sorted_spans
long_to_middle = _ratio(long_span, middle_span)
middle_to_short = _ratio(middle_span, short_span)
short_to_long = _ratio(short_span, long_span)
coaxial_tolerance = max(scale * 0.005, 1e-4)
parallel_cylinders: list[dict[str, Any]] = []
coaxial_cylinders: list[dict[str, Any]] = []
off_axis_cylinders: list[dict[str, Any]] = []
for surface in cylinders:
if _canonical_axis(surface["axis"]) != dominant_axis:
continue
enriched = dict(surface)
enriched["radial_offset"] = _rounded(
_radial_offset(surface["location"], bbox_center, dominant_axis)
)
parallel_cylinders.append(enriched)
if enriched["radial_offset"] <= coaxial_tolerance:
coaxial_cylinders.append(enriched)
else:
off_axis_cylinders.append(enriched)
distinct_coaxial_radii = {
round(surface["radius"], 4) for surface in coaxial_cylinders
}
repeated_groups: dict[float, list[dict[str, Any]]] = defaultdict(list)
for surface in off_axis_cylinders:
repeated_groups[round(surface["radius"], 4)].append(surface)
repeated_group_sizes = sorted(
(len(rows) for rows in repeated_groups.values()), reverse=True
)
largest_repeated_group = (
max(repeated_groups.values(), key=len) if repeated_groups else []
)
parallel_cones = [
item for item in cones if _canonical_axis(item["axis"]) == dominant_axis
]
has_rotational_stack = len(distinct_coaxial_radii) >= 2
has_repeated_axial_holes = bool(repeated_group_sizes and repeated_group_sizes[0] >= 3)
has_multi_axis_passages = any(
_canonical_axis(item["axis"]) != dominant_axis for item in cylinders
)
planes = [item for item in surfaces if item["type"] == "plane"]
toruses = [item for item in surfaces if item["type"] == "torus"]
spheres = [item for item in surfaces if item["type"] == "sphere"]
other_surfaces = [item for item in surfaces if item["type"] == "other"]
total_area = sum(max(float(item.get("area", 0.0)), 0.0) for item in surfaces)
planar_area = sum(max(float(item.get("area", 0.0)), 0.0) for item in planes)
cylindrical_area = sum(
max(float(item.get("area", 0.0)), 0.0) for item in cylinders
)
planar_fraction = _ratio(planar_area, total_area)
cylindrical_fraction = _ratio(cylindrical_area, total_area)
elongated = long_to_middle >= 3.0
plate_like = middle_to_short >= 4.0 and long_to_middle < 3.0
compact = long_to_middle < 2.0 and middle_to_short < 2.5
transverse_axes = {
"x": (1, 2),
"y": (0, 2),
"z": (0, 1),
}[dominant_axis]
transverse_span = max(bbox_size[index] for index in transverse_axes)
dominant_span = bbox_size[{"x": 0, "y": 1, "z": 2}[dominant_axis]]
largest_coaxial_radius = max(
(surface["radius"] for surface in coaxial_cylinders), default=0.0
)
smallest_coaxial_radius = min(
(surface["radius"] for surface in coaxial_cylinders), default=0.0
)
pattern_offsets = [
_radial_offset(surface["location"], bbox_center, dominant_axis)
for surface in largest_repeated_group
]
pattern_mean_offset = (
sum(pattern_offsets) / len(pattern_offsets) if pattern_offsets else 0.0
)
pattern_spread = (
max(pattern_offsets) - min(pattern_offsets) if pattern_offsets else 0.0
)
has_circular_pattern = bool(
len(pattern_offsets) >= 3
and pattern_mean_offset > coaxial_tolerance
and pattern_spread / pattern_mean_offset <= 0.08
)
# Multiple radii sharing a transverse center are a robust final-B-Rep
# signal for stepped bores/counterbores, without claiming operation order.
transverse_groups: dict[tuple[int, int, int], set[float]] = defaultdict(set)
axis_index = {"x": 0, "y": 1, "z": 2}[dominant_axis]
for surface in parallel_cylinders:
location = surface["location"]
key = (
int(round(location[transverse_axes[0]] / max(coaxial_tolerance, 1e-6))),
int(round(location[transverse_axes[1]] / max(coaxial_tolerance, 1e-6))),
axis_index,
)
transverse_groups[key].add(round(surface["radius"], 4))
has_stepped_cylindrical_passage = any(
len(radii) >= 2 for radii in transverse_groups.values()
)
features: list[dict[str, Any]] = []
if has_rotational_stack:
features.extend(
[
{"type": "rotational_body"},
{"type": "coaxial_cylindrical_stack"},
]
)
if len(distinct_coaxial_radii) >= 3:
features.append({"type": "central_passage_candidate"})
if parallel_cones:
features.append({"type": "conical_transition"})
if has_repeated_axial_holes:
features.append({"type": "repeated_axial_hole_pattern"})
if has_multi_axis_passages:
features.append({"type": "multi_axis_passage"})
features.append(
{
"type": (
"elongated_body"
if elongated
else "plate_like_body"
if plate_like
else "compact_body"
if compact
else "moderate_aspect_body"
)
}
)
if planar_fraction >= 0.55:
features.append({"type": "planar_dominant_body"})
if cylindrical_fraction >= 0.55:
features.append({"type": "cylindrical_dominant_body"})
if len(planes) >= 6:
features.append({"type": "multi_level_planar_profile"})
if len(cylinders) >= 2:
features.append({"type": "cylindrical_feature_network"})
if toruses:
features.append({"type": "toroidal_blend_or_groove"})
if spheres:
features.append({"type": "spherical_surface_feature"})
if other_surfaces:
features.append({"type": "freeform_surface_region"})
if has_circular_pattern:
features.append({"type": "circular_equal_radius_pattern"})
if has_stepped_cylindrical_passage:
features.append({"type": "stepped_cylindrical_passage"})
if has_rotational_stack:
features.append({"type": "stepped_rotational_profile"})
if elongated:
features.append({"type": "shaft_like_body"})
if len(distinct_coaxial_radii) >= 3 and elongated:
features.append({"type": "sleeve_or_hollow_shaft_candidate"})
if transverse_span > 0 and dominant_span / transverse_span < 0.8:
features.append({"type": "flange_like_rotational_body"})
if len(surfaces) >= 40:
features.append({"type": "high_topological_complexity"})
elif len(surfaces) >= 16:
features.append({"type": "medium_topological_complexity"})
else:
features.append({"type": "low_topological_complexity"})
constraints: list[dict[str, Any]] = []
if has_rotational_stack:
constraints.extend(
[
{"id": "dominant_axis_alignment", "type": "axis_alignment"},
{"id": "coaxial_stack", "type": "coaxial"},
]
)
if has_repeated_axial_holes:
constraints.append(
{"id": "repeated_radius_group", "type": "repeated_feature"}
)
if parallel_cones:
constraints.append(
{"id": "transition_axis_continuity", "type": "axis_alignment"}
)
if has_circular_pattern:
constraints.append(
{"id": "common_pattern_radius", "type": "radial_pattern"}
)
if has_multi_axis_passages:
constraints.append(
{"id": "multiple_axis_system", "type": "axis_network"}
)
if has_stepped_cylindrical_passage:
constraints.append(
{"id": "shared_passage_axis", "type": "coaxial"}
)
if has_rotational_stack and has_repeated_axial_holes:
family = "flanged_rotational_part"
elif has_rotational_stack and elongated and len(distinct_coaxial_radii) >= 3:
family = "hollow_or_stepped_shaft"
elif has_rotational_stack and elongated:
family = "shaft"
elif has_rotational_stack:
family = "rotational_part"
elif has_repeated_axial_holes and plate_like:
family = "patterned_plate"
elif has_repeated_axial_holes:
family = "patterned_prismatic_part"
elif has_multi_axis_passages and len(cylinders) >= 3:
family = "multi_axis_manifold"
elif plate_like and planes:
family = "plate_or_bracket"
elif planar_fraction >= 0.55:
family = "prismatic_block"
elif other_surfaces or toruses or spheres:
family = "hybrid_freeform_part"
else:
family = "general_mechanical_part"
observations = [
_observation(
"bbox_short_to_long",
short_to_long,
"short_bounding_span",
"long_bounding_span",
),
_observation(
"bbox_middle_to_long",
_ratio(middle_span, long_span),
"middle_bounding_span",
"long_bounding_span",
),
_observation(
"planar_area_fraction",
planar_fraction,
"planar_surface_area",
"total_surface_area",
),
_observation(
"cylindrical_area_fraction",
cylindrical_fraction,
"cylindrical_surface_area",
"total_surface_area",
),
]
if largest_coaxial_radius > 0:
observations.append(
_observation(
"largest_coaxial_radius_to_transverse_span",
_ratio(largest_coaxial_radius, transverse_span),
"largest_coaxial_radius",
"transverse_bounding_span",
)
)
if smallest_coaxial_radius > 0 and largest_coaxial_radius > 0:
observations.append(
_observation(
"smallest_to_largest_coaxial_radius",
_ratio(smallest_coaxial_radius, largest_coaxial_radius),
"smallest_coaxial_radius",
"largest_coaxial_radius",
)
)
if pattern_mean_offset > 0:
observations.append(
_observation(
"pattern_radius_to_transverse_span",
_ratio(pattern_mean_offset, transverse_span),
"pattern_radius",
"transverse_bounding_span",
)
)
if largest_repeated_group:
observations.append(
_observation(
"repeated_feature_radius_to_transverse_span",
_ratio(largest_repeated_group[0]["radius"], transverse_span),
"repeated_feature_radius",
"transverse_bounding_span",
)
)
summary = {
"dominant_axis": dominant_axis,
"coaxial_cylinder_surface_count": len(coaxial_cylinders),
"distinct_coaxial_radius_count": len(distinct_coaxial_radii),
"off_axis_parallel_cylinder_surface_count": len(off_axis_cylinders),
"largest_repeated_radius_group": repeated_group_sizes[0]
if repeated_group_sizes
else 0,
"parallel_cone_surface_count": len(parallel_cones),
"has_multi_axis_passages": has_multi_axis_passages,
"shape_class": (
"elongated" if elongated else "plate_like" if plate_like else "compact"
if compact
else "moderate_aspect"
),
"normalized_observations": observations,
}
return family, features, constraints, summary
def _reconstruction_evidence(
surfaces: list[dict[str, Any]],
features: list[dict[str, Any]],
constraints: list[dict[str, Any]],
semantic_summary: dict[str, Any],
) -> dict[str, Any]:
"""Describe a canonical reconstruction grammar without claiming CAD history."""
feature_roles = sorted(
{
str(item.get("type"))
for item in features
if isinstance(item, dict) and item.get("type")
}
)
relation_roles = sorted(
{
str(item.get("id") or item.get("type"))
for item in constraints
if isinstance(item, dict) and (item.get("id") or item.get("type"))
}
)
surface_counts = Counter(item.get("type", "other") for item in surfaces)
parameter_roles = [
"overall_long_span",
"overall_middle_span",
"overall_short_span",
"primary_axis_span",
"primary_transverse_span",
"secondary_transverse_span",
]
if "cylindrical_feature_network" in feature_roles:
parameter_roles.extend(
[
"passage_diameter_roles",
"passage_axis_offset_roles",
"passage_extent_roles",
]
)
if "coaxial_cylindrical_stack" in feature_roles:
parameter_roles.extend(
[
"coaxial_diameter_roles",
"axial_segment_length_roles",
"shoulder_position_roles",
]
)
if "repeated_axial_hole_pattern" in feature_roles:
parameter_roles.extend(
[
"pattern_member_diameter_role",
"pattern_radius_or_spacing_role",
"pattern_angular_or_linear_phase_role",
]
)
if "multi_level_planar_profile" in feature_roles:
parameter_roles.extend(
[
"planar_level_offset_roles",
"profile_width_roles",
"profile_length_roles",
]
)
if "conical_transition" in feature_roles:
parameter_roles.append("transition_slope_role")
stages: list[dict[str, Any]] = [
{
"id": "establish_reference_frame",
"operation": "define_datums",
"feature_roles": [],
"reference_roles": [
"part_center",
"primary_axis",
"primary_transverse_plane",
"secondary_transverse_plane",
],
},
{
"id": "construct_primary_envelope",
"operation": (
"revolve_profile"
if "rotational_body" in feature_roles
else "extrude_profile"
),
"feature_roles": [
role
for role in (
"rotational_body",
"planar_dominant_body",
"elongated_body",
"plate_like_body",
"compact_body",
"moderate_aspect_body",
)
if role in feature_roles
],
"reference_roles": ["part_center", "primary_axis"],
},
]
if "multi_level_planar_profile" in feature_roles:
stages.append(
{
"id": "establish_planar_levels",
"operation": "add_or_remove_profile_levels",
"feature_roles": ["multi_level_planar_profile"],
"reference_roles": ["primary_transverse_plane"],
}
)
if "coaxial_cylindrical_stack" in feature_roles:
stages.append(
{
"id": "construct_coaxial_stack",
"operation": "add_or_cut_coaxial_profiles",
"feature_roles": ["coaxial_cylindrical_stack"],
"reference_roles": ["primary_axis"],
}
)
if "cylindrical_feature_network" in feature_roles:
stages.append(
{
"id": "construct_passage_network",
"operation": "cut_semantic_passages",
"feature_roles": [
role
for role in (
"cylindrical_feature_network",
"central_passage_candidate",
"stepped_cylindrical_passage",
"multi_axis_passage",
)
if role in feature_roles
],
"reference_roles": ["primary_axis", "part_center"],
}
)
if "repeated_axial_hole_pattern" in feature_roles:
stages.append(
{
"id": "construct_repeated_feature_pattern",
"operation": "pattern_semantic_feature",
"feature_roles": [
role
for role in (
"repeated_axial_hole_pattern",
"circular_equal_radius_pattern",
)
if role in feature_roles
],
"reference_roles": ["primary_axis", "part_center"],
}
)
transition_roles = [
role
for role in (
"conical_transition",
"toroidal_blend_or_groove",
"spherical_surface_feature",
"freeform_surface_region",
)
if role in feature_roles
]
if transition_roles:
stages.append(
{
"id": "resolve_transitions_and_finishing",
"operation": "apply_transitions_or_blends",
"feature_roles": transition_roles,
"reference_roles": ["primary_axis"],
}
)
stages.append(
{
"id": "validate_reconstructed_shape",
"operation": "validate_geometry_and_relations",
"feature_roles": feature_roles,
"reference_roles": ["part_center", "primary_axis"],
}
)
return {
"interpretation": "canonical_reconstruction_plan_not_recovered_history",
"parameter_roles": sorted(set(parameter_roles)),
"datum_roles": [
"part_center",
"primary_axis",
"primary_transverse_plane",
"secondary_transverse_plane",
],
"feature_roles": feature_roles,
"relation_roles": relation_roles,
"canonical_stages": stages,
"private_cardinality_evidence": {
"surface_type_counts": dict(sorted(surface_counts.items())),
"surface_type_classes": {
kind: _cardinality_class(count)
for kind, count in sorted(surface_counts.items())
},
"feature_role_count": len(feature_roles),
"feature_role_count_class": _cardinality_class(len(feature_roles)),
"coaxial_radius_role_count": int(
semantic_summary.get("distinct_coaxial_radius_count", 0)
),
"coaxial_radius_role_count_class": _cardinality_class(
int(semantic_summary.get("distinct_coaxial_radius_count", 0))
),
"repeated_member_count": int(
semantic_summary.get("largest_repeated_radius_group", 0)
),
"repeated_member_count_class": _cardinality_class(
int(semantic_summary.get("largest_repeated_radius_group", 0))
),
},
"validation_roles": sorted(
{
"closed_solid",
"bounding_proportion_consistency",
"surface_mix_consistency",
*relation_roles,
}
),
}
def extract_step_case(source: Path) -> dict[str, Any]:
from build123d import import_step
resolved = source.expanduser().resolve()
source_bytes = resolved.read_bytes()
shape = import_step(str(resolved))
bbox = shape.bounding_box()
bbox_min = _point(bbox.min)
bbox_max = _point(bbox.max)
bbox_size = _point(bbox.size)
bbox_center = [
_rounded((lower + upper) / 2.0)
for lower, upper in zip(bbox_min, bbox_max)
]
surfaces = [
_surface_record(face, index) for index, face in enumerate(shape.faces())
]
surface_counts = Counter(surface["type"] for surface in surfaces)
family, features, constraints, semantic_summary = _semantic_summary(
surfaces, bbox_center, bbox_size
)
reconstruction_evidence = _reconstruction_evidence(
surfaces, features, constraints, semantic_summary
)
digest = hashlib.sha256(source_bytes).hexdigest()
normalized_observations = semantic_summary.pop("normalized_observations", [])
return {
"schema_version": "2.0",
"extractor_version": EXTRACTOR_VERSION,
"case_kind": "private_step_evidence",
"case_id": digest,
"provenance": {
"source_path": str(resolved),
"source_sha256": digest,
"source_format": "step",
},
"geometry_evidence": {
"valid": bool(shape.is_valid),
"solid_count": len(shape.solids()),
"face_count": len(shape.faces()),
"volume": _rounded(shape.volume),
"bounding_box": {
"min": bbox_min,
"max": bbox_max,
"size": bbox_size,
"center": bbox_center,
},
"analytic_surface_counts": dict(sorted(surface_counts.items())),
"analytic_surfaces": surfaces,
},
"design_ir": {
"part_family": family,
"features": features,
"constraints": constraints,
"normalized_observations": normalized_observations,
"semantic_summary": semantic_summary,
"reconstruction_evidence": reconstruction_evidence,
},
"experience": {
"rules": [],
"validation_targets": [],
"promotion_state": "evidence_only",
},
}
def write_json(path: Path, payload: dict[str, Any]) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(
json.dumps(payload, indent=2, ensure_ascii=False) + "\n", encoding="utf-8"
)
def main(argv: list[str] | None = None) -> int:
parser = argparse.ArgumentParser(
description="Extract a private case JSON from a STEP file."
)
parser.add_argument("source", type=Path)
parser.add_argument("--output", type=Path, required=True)
args = parser.parse_args(argv)
write_json(args.output, extract_step_case(args.source))
print(args.output)
return 0
if __name__ == "__main__":
raise SystemExit(main())
@@ -0,0 +1,114 @@
from __future__ import annotations
import copy
import importlib.util
import json
import tempfile
import unittest
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
SCRIPT = ROOT / "scripts" / "designir_pipeline.py"
SPEC = importlib.util.spec_from_file_location("designir_pipeline", SCRIPT)
MODULE = importlib.util.module_from_spec(SPEC)
assert SPEC.loader
SPEC.loader.exec_module(MODULE)
class DesignIRPipelineTests(unittest.TestCase):
def payload(self) -> dict:
return json.loads(
(ROOT / "examples" / "raised_hub_flange.designir.json").read_text()
)
def test_example_is_valid_and_compiles(self) -> None:
payload = MODULE.validate_designir(self.payload())
with tempfile.TemporaryDirectory() as temporary:
output = Path(temporary) / "flange.step"
result = MODULE.compile_designir(payload, output)
self.assertTrue(output.is_file())
self.assertEqual(1, result["facts"]["solid_count"])
self.assertGreater(result["facts"]["volume_mm3"], 30_000)
def test_teacher_dependency_is_rejected(self) -> None:
payload = self.payload()
payload["source_step"] = "teacher.step"
with self.assertRaisesRegex(MODULE.DesignIRError, "Teacher geometry"):
MODULE.validate_designir(payload)
def test_example_compiles_with_simplecadapi_and_exports_graph(self) -> None:
payload = self.payload()
with tempfile.TemporaryDirectory() as temporary:
output = Path(temporary) / "flange.step"
result = MODULE.compile_designir(
payload, output, backend="simplecadapi"
)
self.assertEqual("simplecadapi", result["backend"])
self.assertGreater(result["facts"]["volume_mm3"], 30_000)
self.assertTrue(Path(result["model_json"]).is_file())
def test_embedded_mesh_is_rejected(self) -> None:
payload = self.payload()
payload["features"][0]["mesh"] = {"vertices": [[0, 0, 0]]}
with self.assertRaisesRegex(MODULE.DesignIRError, "Teacher geometry"):
MODULE.validate_designir(payload)
def test_isolated_rebuild_has_no_teacher(self) -> None:
with tempfile.TemporaryDirectory() as temporary:
root = Path(temporary)
designir = root / "flange.designir.json"
designir.write_text(json.dumps(self.payload()), encoding="utf-8")
output = root / "rebuilt.step"
result = MODULE.isolated_rebuild(designir, output)
self.assertTrue(result["valid"])
self.assertFalse(result["teacher_step_available"])
self.assertTrue(output.is_file())
def test_acceptance_passes_identical_designir_rebuild(self) -> None:
payload = self.payload()
with tempfile.TemporaryDirectory() as temporary:
root = Path(temporary)
teacher = root / "teacher.step"
rebuilt = root / "rebuilt.step"
MODULE.compile_designir(payload, teacher)
MODULE.compile_designir(payload, rebuilt)
report = MODULE.acceptance_report(teacher, rebuilt, payload)
self.assertEqual("pass", report["verdict"])
self.assertEqual("replay_validated", report["next_state"])
self.assertEqual(
1.0, report["editability"]["parameter_perturbation_success_rate"]
)
def test_unsupported_vocabulary_is_honest_failure(self) -> None:
payload = self.payload()
payload["reconstruction_status"] = "unsupported_feature_vocabulary"
payload["missing_capabilities"] = ["multi_rail_sweep"]
with self.assertRaisesRegex(MODULE.DesignIRError, "multi_rail_sweep"):
MODULE.build_shape(payload)
def test_promotion_requires_independent_replay_validated_cases(self) -> None:
with tempfile.TemporaryDirectory() as temporary:
root = Path(temporary)
reports = []
for index in range(3):
report = root / f"case-{index}.json"
report.write_text(
json.dumps(
{
"model_id": f"case-{index}",
"next_state": "replay_validated",
}
),
encoding="utf-8",
)
reports.append(report)
result = MODULE.promotion_check(
reports, ROOT / "config" / "promotion-policy.json"
)
self.assertTrue(result["eligible"])
self.assertEqual("promoted", result["next_state"])
if __name__ == "__main__":
unittest.main()
@@ -0,0 +1,382 @@
from __future__ import annotations
import importlib.util
import json
import unittest
from pathlib import Path
SCRIPT = (
Path(__file__).parents[1]
/ "skills"
/ "cad-experience-builder"
/ "scripts"
/ "cad_experience.py"
)
SPEC = importlib.util.spec_from_file_location("cad_experience", SCRIPT)
MODULE = importlib.util.module_from_spec(SPEC)
assert SPEC.loader
SPEC.loader.exec_module(MODULE)
STEP_SCRIPT = (
Path(__file__).parents[1]
/ "skills"
/ "cad-experience-builder"
/ "scripts"
/ "step_to_case.py"
)
STEP_SPEC = importlib.util.spec_from_file_location("step_to_case", STEP_SCRIPT)
STEP_MODULE = importlib.util.module_from_spec(STEP_SPEC)
assert STEP_SPEC.loader
STEP_SPEC.loader.exec_module(STEP_MODULE)
def make_case(index: int) -> dict:
return {
"provenance": {"source_sha256": f"case_{index}"},
"design_ir": {
"part_family": "flanged_hub_adapter",
"features": [
{"type": "base_flange", "center": [91.0, 52.0]},
{"type": "hollow_sleeve", "surface_ids": ["face_1"]},
{"type": "counterbore"},
],
"constraints": [{"id": "coaxial_stack", "type": "coaxial"}],
"parameters": {
"base_outer_diameter": 91.0,
"counterbore_spacing": 52.0,
},
"normalized_observations": [
{
"name": "sleeve_od_to_base_od",
"value": 0.3 + index * 0.001,
"numerator_role": "sleeve_outer_diameter",
"denominator_role": "base_outer_diameter",
}
],
"reconstruction_evidence": {
"parameter_roles": [
"overall_long_span",
"overall_middle_span",
"overall_short_span",
],
"datum_roles": ["part_center", "primary_axis"],
"feature_roles": ["base_flange", "hollow_sleeve", "counterbore"],
"relation_roles": ["coaxial_stack"],
"canonical_stages": [
{
"id": "establish_reference_frame",
"operation": "define_datums",
"feature_roles": [],
"reference_roles": ["part_center", "primary_axis"],
},
{
"id": "construct_primary_envelope",
"operation": "revolve_profile",
"feature_roles": ["base_flange", "hollow_sleeve"],
"reference_roles": ["primary_axis"],
},
],
"private_cardinality_evidence": {
"surface_type_classes": {
"plane": "repeated",
"cylinder": "dense",
},
"feature_role_count_class": "repeated",
},
"validation_roles": ["closed_solid", "coaxial_stack"],
},
},
"experience": {
"rules": [
{
"id": "bound_counterbore_to_host",
"scope": ["flanged_hub_adapter"],
"statement": "Terminate a counterbore at its host boundary.",
"repair": "Bind the cutter extent to the host feature.",
}
],
"validation_targets": [
{
"id": "host_boundary",
"check": "A counterbore does not cross an unrelated adjacent feature.",
}
],
},
}
class CadExperienceTest(unittest.TestCase):
def reviewed_proposal(self) -> dict:
return {
"id": "constraint.flanged_hub_adapter.shared_axis",
"kind": "constraint",
"scope": ["flanged_hub_adapter"],
"evidence_query": {
"required_features": ["base_flange", "hollow_sleeve"],
"required_relations": ["coaxial_stack"],
},
"guidance": "Preserve a shared axis across participating feature roles.",
"semantic_rationale": "The relationship transfers across dimensional variants.",
}
def test_semantic_summary_derives_roles_without_copying_dimensions(self):
surfaces = [
{
"type": "cylinder",
"axis": [0.0, 1.0, 0.0],
"location": [0.0, 0.0, 0.0],
"radius": radius,
"area": 100.0,
}
for radius in (5.0, 10.0, 20.0)
]
surfaces.extend(
{
"type": "cylinder",
"axis": [0.0, 1.0, 0.0],
"location": [offset, 0.0, 0.0],
"radius": 2.0,
"area": 20.0,
}
for offset in (-15.0, 15.0, 16.0)
)
surfaces.append(
{
"type": "cone",
"axis": [0.0, 1.0, 0.0],
"location": [0.0, 0.0, 0.0],
"reference_radius": 10.0,
"semi_angle_radians": 0.5,
"area": 50.0,
}
)
family, features, constraints, summary = STEP_MODULE._semantic_summary(
surfaces, [0.0, 0.0, 0.0], [40.0, 20.0, 40.0]
)
self.assertEqual("flanged_rotational_part", family)
self.assertIn(
"coaxial_cylindrical_stack", {item["type"] for item in features}
)
self.assertIn("coaxial_stack", {item["id"] for item in constraints})
self.assertEqual("y", summary["dominant_axis"])
self.assertTrue(summary["normalized_observations"])
reconstruction = STEP_MODULE._reconstruction_evidence(
surfaces, features, constraints, summary
)
self.assertIn("overall_long_span", reconstruction["parameter_roles"])
self.assertTrue(reconstruction["canonical_stages"])
self.assertEqual(
"canonical_reconstruction_plan_not_recovered_history",
reconstruction["interpretation"],
)
def test_conditional_feature_context_does_not_dilute_valid_method(self):
proposal = self.reviewed_proposal()
proposal["evidence_query"]["context_features"] = [
"base_flange",
"hollow_sleeve",
]
target = [MODULE.normalize_case(make_case(index)) for index in range(20)]
unrelated = []
for index in range(80):
payload = make_case(index + 100)
payload["design_ir"]["features"] = [{"type": "base_flange"}]
payload["design_ir"]["constraints"] = []
unrelated.append(MODULE.normalize_case(payload))
library = MODULE.build_reviewed_library(
target + unrelated,
[MODULE.normalize_proposals({
"draft_kind": "llm_generalized_experience_proposals",
"proposals": [proposal],
})[0]],
20,
0.8,
{"duplicate_case_count": 0, "rejected_case_count": 0},
)
self.assertEqual(1, len(library["experiences"]))
def test_single_case_never_promotes(self):
case = MODULE.normalize_case(make_case(1))
library = MODULE.induce(
[case],
2,
0.8,
{"duplicate_case_count": 0, "rejected_case_count": 0},
)
self.assertEqual(library["status"], "collecting_evidence")
self.assertEqual(library["experiences"], [])
self.assertTrue(library["candidate_experiences"])
self.assertTrue(
all(
item["promotion_state"] == "candidate"
for item in library["candidate_experiences"]
)
)
self.assertTrue(
all(
item["consumer_policy"]
== "visible_for_review_but_not_available_to_cad_router"
for item in library["candidate_experiences"]
)
)
def test_batch_promotes_methods_without_instance_values(self):
cases = [MODULE.normalize_case(make_case(index)) for index in range(20)]
library = MODULE.induce(
cases,
20,
0.8,
{"duplicate_case_count": 0, "rejected_case_count": 0},
)
self.assertEqual(MODULE.audit_library(library), [])
rendered = json.dumps(library)
self.assertNotIn('"parameters"', rendered)
self.assertNotIn("91.0", rendered)
self.assertNotIn("52.0", rendered)
self.assertNotIn('"center"', rendered)
ids = {item["id"] for item in library["experiences"]}
self.assertIn("rule.bound_counterbore_to_host", ids)
self.assertIn("constraint.flanged_hub_adapter.coaxial_stack", ids)
self.assertIn("distribution.sleeve_od_to_base_od", ids)
def test_family_support_is_not_diluted_by_unrelated_families(self):
target = [MODULE.normalize_case(make_case(index)) for index in range(20)]
unrelated = []
for index in range(100):
payload = make_case(index + 1000)
payload["design_ir"]["part_family"] = "gear"
payload["design_ir"]["features"] = [
{"type": "gear_blank"},
{"type": "gear_teeth"},
]
unrelated.append(MODULE.normalize_case(payload))
library = MODULE.induce(
target + unrelated,
20,
0.8,
{"duplicate_case_count": 0, "rejected_case_count": 0},
)
ids = {item["id"] for item in library["experiences"]}
self.assertIn(
"motif.flanged_hub_adapter.base_flange+hollow_sleeve", ids
)
def test_query_returns_backend_neutral_context(self):
cases = [MODULE.normalize_case(make_case(index)) for index in range(20)]
library = MODULE.induce(
cases,
20,
0.8,
{"duplicate_case_count": 0, "rejected_case_count": 0},
)
result = MODULE.query_library(
library,
"flanged_hub_adapter",
{"base_flange", "hollow_sleeve", "counterbore"},
)
self.assertTrue(result["experiences"])
self.assertFalse(result["policy"]["contains_instance_parameters"])
def test_llm_proposal_is_candidate_until_evidence_threshold(self):
proposal = self.reviewed_proposal()
one_case = MODULE.normalize_case(make_case(1))
candidate_library = MODULE.build_reviewed_library(
[one_case],
[proposal],
20,
0.8,
{"duplicate_case_count": 0, "rejected_case_count": 0},
)
self.assertEqual([], candidate_library["experiences"])
self.assertEqual(1, len(candidate_library["candidate_experiences"]))
self.assertTrue(candidate_library["policy"]["router_consumable"])
cases = [MODULE.normalize_case(make_case(index)) for index in range(20)]
promoted_library = MODULE.build_reviewed_library(
cases,
[proposal],
20,
0.8,
{"duplicate_case_count": 0, "rejected_case_count": 0},
)
self.assertEqual(1, len(promoted_library["experiences"]))
self.assertEqual([], promoted_library["candidate_experiences"])
def test_numeric_instance_answer_is_rejected_from_llm_draft(self):
proposal = self.reviewed_proposal()
proposal["guidance"] = "Always use a diameter of 91 millimeters."
with self.assertRaisesRegex(ValueError, "instance numbers"):
MODULE.normalize_proposals(
{
"draft_kind": "llm_generalized_experience_proposals",
"proposals": [proposal],
}
)
def test_reconstruction_grammar_promotes_without_instance_values(self):
proposal = {
"id": "reconstruction.flanged_hub_adapter.primary",
"kind": "reconstruction_grammar",
"scope": ["flanged_hub_adapter"],
"evidence_query": {
"required_features": ["base_flange", "hollow_sleeve"],
"required_relations": ["coaxial_stack"],
"context_features": ["base_flange", "hollow_sleeve"],
},
"guidance": "Establish shared datums before composing the primary envelope.",
"semantic_rationale": "The symbolic stages transfer across dimensional variants.",
"reconstruction_grammar": {
"parameter_roles": [
"overall_long_span",
"overall_middle_span",
"overall_short_span",
],
"datum_roles": ["part_center", "primary_axis"],
"feature_roles": ["base_flange", "hollow_sleeve"],
"relation_roles": ["coaxial_stack"],
"canonical_stages": [
{
"id": "establish_reference_frame",
"operation": "define_datums",
"feature_roles": [],
"reference_roles": ["part_center", "primary_axis"],
},
{
"id": "construct_primary_envelope",
"operation": "revolve_profile",
"feature_roles": ["base_flange", "hollow_sleeve"],
"reference_roles": ["primary_axis"],
},
],
"cardinality_classes": ["plane_repeated", "cylinder_dense"],
"validation_roles": ["closed_solid", "coaxial_stack"],
},
}
normalized = MODULE.normalize_proposals(
{
"draft_kind": "llm_generalized_experience_proposals",
"proposals": [proposal],
}
)
cases = [MODULE.normalize_case(make_case(index)) for index in range(20)]
library = MODULE.build_reviewed_library(
cases,
normalized,
20,
0.8,
{"duplicate_case_count": 0, "rejected_case_count": 0},
)
self.assertEqual([], MODULE.audit_library(library))
self.assertEqual("reconstruction_grammar", library["experiences"][0]["kind"])
rendered = json.dumps(library)
self.assertNotIn("91.0", rendered)
self.assertNotIn('"parameters"', rendered)
def test_direct_distill_without_model_draft_is_blocked(self):
with self.assertRaisesRegex(SystemExit, "statistical distillation is disabled"):
MODULE.main(["distill"])
if __name__ == "__main__":
unittest.main()
+53
View File
@@ -0,0 +1,53 @@
# Distillation Workspace
This directory is the runtime boundary for uploaded STEP reconstruction and
experience distillation.
## Where to put target STEP files
Place STEP or STP files to be reconstructed in:
```text
teacher/inbox/
```
Use unique, stable filenames. Batch inputs are intentionally ignored by Git so
large private corpora do not inflate the repository.
## Where each stage is stored
| Stage | Directory | Contents |
| --- | --- | --- |
| Source input | `teacher/inbox/` | Original target STEP/STP files. |
| Extracted evidence | `teacher/evidence/` | Private geometry facts extracted from each source. |
| Agent A input | `reconstruction/inbox/` | Sanitized evidence packets and reconstruction requests. |
| Agent A result | `reconstruction/designir/` | Executable `*.designir.json` models. |
| Independent rebuild | `reconstruction/output/` | STEP rebuilt without teacher-file access; SimpleCAD jobs may also emit model JSON. |
| Agent B input | `acceptance/inbox/` | Acceptance manifests linking teacher, DesignIR, rebuild, and edit contract. |
| Acceptance result | `acceptance/reports/` | Geometry, topology, semantic, and parameter-perturbation reports. |
| Failed/unsupported | `quarantine/` | Unsupported vocabulary, invalid cases, and failure clusters. |
| Experience candidates | `promotion/candidates/` | Generalized methods proposed from validated cases. |
| Replay gate | `promotion/replay_validated/` | Methods repeated successfully across independent cases. |
| Final distilled result | `promotion/promoted/library.json` | Only deterministic, promoted experience consumed by CAD Router. |
## Data flow
```text
teacher/inbox/*.step
-> teacher/evidence/
-> reconstruction/inbox/
-> reconstruction/designir/
-> reconstruction/output/*.step
-> acceptance/inbox/
-> acceptance/reports/
-> promotion/candidates/
-> promotion/replay_validated/
-> promotion/promoted/library.json
```
Agent A cannot read `teacher/inbox/`. Agent B may read the teacher STEP and the
rebuilt result, but neither agent can directly publish experience. Promotion is
performed only by the deterministic policy gate.
The `.gitkeep` files preserve this layout in Git. Runtime STEP files, evidence,
rebuilds, and reports remain local by default.
@@ -0,0 +1 @@
@@ -0,0 +1 @@
@@ -0,0 +1 @@
@@ -0,0 +1 @@
@@ -0,0 +1,29 @@
{
"schema_version": "2.0",
"library_kind": "generalized_cad_experience",
"induction_mode": "replay_validated_designir_with_deterministic_publication",
"status": "empty",
"policy": {
"instance_parameters_allowed": false,
"absolute_coordinates_allowed": false,
"single_case_promotion_allowed": false,
"teacher_geometry_allowed": false,
"llm_semantic_review_required": true,
"draft_evidence_verified": true,
"router_consumable": true,
"minimum_support": 3
},
"corpus_summary": {
"accepted_case_count": 0,
"duplicate_case_count": 0,
"rejected_case_count": 0,
"family_count": 0
},
"experience_summary": {
"llm_proposal_count": 0,
"promoted_experience_count": 0,
"candidate_experience_count": 0
},
"experiences": [],
"candidate_experiences": []
}
@@ -0,0 +1 @@
@@ -0,0 +1,494 @@
{
"schema_version": "2.0",
"library_kind": "generalized_cad_experience",
"generated_at": "2026-07-23T08:16:40+00:00",
"induction_mode": "llm_proposals_with_deterministic_evidence_verification",
"status": "ready",
"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,
"minimum_support": 20,
"minimum_confidence": 0.8
},
"corpus_summary": {
"accepted_case_count": 2,
"duplicate_case_count": 0,
"rejected_case_count": 0,
"family_count": 11
},
"experience_summary": {
"llm_proposal_count": 4,
"promoted_experience_count": 0,
"candidate_experience_count": 4,
"by_kind": [],
"by_scope": [],
"candidate_by_kind": [
{
"kind": "constraint",
"count": 1
},
{
"kind": "feature_motif",
"count": 2
},
{
"kind": "validation_rule",
"count": 1
}
]
},
"experiences": [
{
"id": "constraint.rotational_part.shared_dominant_axis",
"kind": "constraint",
"scope": [
"rotational_part"
],
"when": {
"features": [
"coaxial_cylindrical_stack",
"rotational_body"
],
"relations": [
"dominant_axis_alignment"
]
},
"guidance": "Preserve the dominant axis as the organizing datum for rotational feature roles.",
"semantic_rationale": "The datum transfers because axis alignment is independent of the individual component dimensions.",
"support": 2,
"confidence": 1.0,
"check": "Confirm that rotational feature roles remain aligned to the same organizing datum.",
"promotion_state": "candidate",
"required_support": 20,
"remaining_support": 18,
"required_confidence": 0.8,
"consumer_policy": "visible_for_review_but_not_available_to_cad_router"
},
{
"id": "motif.rotational_part.coaxial_core_with_center_passage",
"kind": "feature_motif",
"scope": [
"rotational_part"
],
"when": {
"features": [
"circular_equal_radius_pattern"
],
"relations": [
"shared_passage_axis"
]
},
"guidance": "Treat the rotational body, coaxial cylindrical stack, and central passage as one shared-axis core.",
"semantic_rationale": "The relationship transfers because the participating roles describe axis organization rather than a particular size.",
"support": 2,
"confidence": 1.0,
"check": "Confirm that the rotational body, stacked cylindrical roles, and central passage retain their intended common axis.",
"promotion_state": "candidate",
"required_support": 20,
"remaining_support": 18,
"required_confidence": 0.8,
"consumer_policy": "visible_for_review_but_not_available_to_cad_router"
},
{
"id": "motif.rotational_part.patterned_axial_mounting",
"kind": "feature_motif",
"scope": [
"rotational_part"
],
"when": {
"features": [
"repeated_axial_hole_pattern",
"rotational_body"
],
"relations": []
},
"guidance": "Treat the participating feature roles as one coordinated composition rather than unrelated primitives.",
"semantic_rationale": "The composition recurs across independent dimensional variants in the same family.",
"support": 55,
"confidence": 1.0
},
{
"id": "motif.hollow_or_stepped_shaft.coaxial_cylindrical_stack+stepped_cylindrical_passage",
"kind": "feature_motif",
"scope": [
"hollow_or_stepped_shaft"
],
"when": {
"features": [
"coaxial_cylindrical_stack",
"stepped_cylindrical_passage"
],
"relations": []
},
"guidance": "Treat the participating feature roles as one coordinated composition rather than unrelated primitives.",
"semantic_rationale": "The composition recurs across independent dimensional variants in the same family.",
"support": 46,
"confidence": 1.0
},
{
"id": "motif.hollow_or_stepped_shaft.rotational_body+coaxial_cylindrical_stack",
"kind": "feature_motif",
"scope": [
"hollow_or_stepped_shaft"
],
"when": {
"features": [
"coaxial_cylindrical_stack",
"rotational_body"
],
"relations": []
},
"guidance": "Treat the participating feature roles as one coordinated composition rather than unrelated primitives.",
"semantic_rationale": "The composition recurs across independent dimensional variants in the same family.",
"support": 46,
"confidence": 1.0
},
{
"id": "motif.hollow_or_stepped_shaft.shaft_like_body+elongated_body",
"kind": "feature_motif",
"scope": [
"hollow_or_stepped_shaft"
],
"when": {
"features": [
"elongated_body",
"shaft_like_body"
],
"relations": []
},
"guidance": "Treat the participating feature roles as one coordinated composition rather than unrelated primitives.",
"semantic_rationale": "The composition recurs across independent dimensional variants in the same family.",
"support": 46,
"confidence": 1.0
},
{
"id": "motif.multi_axis_manifold.cylindrical_feature_network+multi_axis_passage",
"kind": "feature_motif",
"scope": [
"multi_axis_manifold"
],
"when": {
"features": [
"cylindrical_feature_network",
"multi_axis_passage"
],
"relations": []
},
"guidance": "Treat the participating feature roles as one coordinated composition rather than unrelated primitives.",
"semantic_rationale": "The composition recurs across independent dimensional variants in the same family.",
"support": 55,
"confidence": 1.0
},
{
"id": "motif.multi_axis_manifold.planar_dominant_body+multi_level_planar_profile",
"kind": "feature_motif",
"scope": [
"multi_axis_manifold"
],
"when": {
"features": [
"multi_level_planar_profile",
"planar_dominant_body"
],
"relations": []
},
"guidance": "Treat the participating feature roles as one coordinated composition rather than unrelated primitives.",
"semantic_rationale": "The composition recurs across independent dimensional variants in the same family.",
"support": 44,
"confidence": 1.0
},
{
"id": "motif.patterned_plate.cylindrical_feature_network+multi_axis_passage",
"kind": "feature_motif",
"scope": [
"patterned_plate"
],
"when": {
"features": [
"cylindrical_feature_network",
"multi_axis_passage"
],
"relations": []
},
"guidance": "Treat the participating feature roles as one coordinated composition rather than unrelated primitives.",
"semantic_rationale": "The composition recurs across independent dimensional variants in the same family.",
"support": 76,
"confidence": 1.0
},
{
"id": "motif.patterned_plate.planar_dominant_body+multi_level_planar_profile",
"kind": "feature_motif",
"scope": [
"patterned_plate"
],
"when": {
"features": [
"multi_level_planar_profile",
"planar_dominant_body"
],
"relations": []
},
"guidance": "Treat the participating feature roles as one coordinated composition rather than unrelated primitives.",
"semantic_rationale": "The composition recurs across independent dimensional variants in the same family.",
"support": 202,
"confidence": 1.0
},
{
"id": "motif.patterned_plate.repeated_axial_hole_pattern+circular_equal_radius_pattern",
"kind": "feature_motif",
"scope": [
"patterned_plate"
],
"when": {
"features": [
"circular_equal_radius_pattern",
"repeated_axial_hole_pattern"
],
"relations": []
},
"guidance": "Treat the participating feature roles as one coordinated composition rather than unrelated primitives.",
"semantic_rationale": "The composition recurs across independent dimensional variants in the same family.",
"support": 66,
"confidence": 1.0
},
{
"id": "motif.patterned_prismatic_part.cylindrical_feature_network+multi_axis_passage",
"kind": "feature_motif",
"scope": [
"patterned_prismatic_part"
],
"when": {
"features": [
"cylindrical_feature_network",
"multi_axis_passage"
],
"relations": []
},
"guidance": "Treat the participating feature roles as one coordinated composition rather than unrelated primitives.",
"semantic_rationale": "The composition recurs across independent dimensional variants in the same family.",
"support": 156,
"confidence": 1.0
},
{
"id": "motif.patterned_prismatic_part.planar_dominant_body+multi_level_planar_profile",
"kind": "feature_motif",
"scope": [
"patterned_prismatic_part"
],
"when": {
"features": [
"multi_level_planar_profile",
"planar_dominant_body"
],
"relations": []
},
"guidance": "Treat the participating feature roles as one coordinated composition rather than unrelated primitives.",
"semantic_rationale": "The composition recurs across independent dimensional variants in the same family.",
"support": 323,
"confidence": 1.0
},
{
"id": "motif.patterned_prismatic_part.repeated_axial_hole_pattern+circular_equal_radius_pattern",
"kind": "feature_motif",
"scope": [
"patterned_prismatic_part"
],
"when": {
"features": [
"circular_equal_radius_pattern",
"repeated_axial_hole_pattern"
],
"relations": []
},
"guidance": "Treat the participating feature roles as one coordinated composition rather than unrelated primitives.",
"semantic_rationale": "The composition recurs across independent dimensional variants in the same family.",
"support": 159,
"confidence": 1.0
},
{
"id": "motif.plate_or_bracket.planar_dominant_body+multi_level_planar_profile",
"kind": "feature_motif",
"scope": [
"plate_or_bracket"
],
"when": {
"features": [
"multi_level_planar_profile",
"planar_dominant_body"
],
"relations": []
},
"guidance": "Treat the participating feature roles as one coordinated composition rather than unrelated primitives.",
"semantic_rationale": "The composition recurs across independent dimensional variants in the same family.",
"support": 35,
"confidence": 1.0
},
{
"id": "motif.prismatic_block.planar_dominant_body+multi_level_planar_profile",
"kind": "feature_motif",
"scope": [
"prismatic_block"
],
"when": {
"features": [
"multi_level_planar_profile",
"planar_dominant_body"
],
"relations": []
},
"guidance": "Treat the participating feature roles as one coordinated composition rather than unrelated primitives.",
"semantic_rationale": "The composition recurs across independent dimensional variants in the same family.",
"support": 67,
"confidence": 1.0
},
{
"id": "motif.rotational_part.coaxial_cylindrical_stack+stepped_cylindrical_passage",
"kind": "feature_motif",
"scope": [
"rotational_part"
],
"when": {
"features": [
"coaxial_cylindrical_stack",
"stepped_cylindrical_passage"
],
"relations": []
},
"guidance": "Treat the participating feature roles as one coordinated composition rather than unrelated primitives.",
"semantic_rationale": "The composition recurs across independent dimensional variants in the same family.",
"support": 123,
"confidence": 1.0
},
{
"id": "motif.rotational_part.rotational_body+coaxial_cylindrical_stack",
"kind": "feature_motif",
"scope": [
"rotational_part"
],
"when": {
"features": [
"coaxial_cylindrical_stack",
"rotational_body"
],
"relations": []
},
"guidance": "Treat the participating feature roles as one coordinated composition rather than unrelated primitives.",
"semantic_rationale": "The composition recurs across independent dimensional variants in the same family.",
"support": 123,
"confidence": 1.0
},
{
"id": "motif.shaft.coaxial_cylindrical_stack+stepped_cylindrical_passage",
"kind": "feature_motif",
"scope": [
"shaft"
],
"when": {
"features": [
"coaxial_cylindrical_stack",
"stepped_cylindrical_passage"
],
"relations": []
},
"guidance": "Treat the participating feature roles as one coordinated composition rather than unrelated primitives.",
"semantic_rationale": "The composition recurs across independent dimensional variants in the same family.",
"support": 40,
"confidence": 1.0
},
{
"id": "motif.shaft.rotational_body+coaxial_cylindrical_stack",
"kind": "feature_motif",
"scope": [
"shaft"
],
"when": {
"features": [
"coaxial_cylindrical_stack",
"rotational_body"
],
"relations": []
},
"guidance": "Treat the participating feature roles as one coordinated composition rather than unrelated primitives.",
"semantic_rationale": "The composition recurs across independent dimensional variants in the same family.",
"support": 40,
"confidence": 1.0
},
{
"id": "motif.shaft.shaft_like_body+elongated_body",
"kind": "feature_motif",
"scope": [
"shaft"
],
"when": {
"features": [
"elongated_body",
"shaft_like_body"
],
"relations": []
},
"guidance": "Treat the participating feature roles as one coordinated composition rather than unrelated primitives.",
"semantic_rationale": "The composition recurs across independent dimensional variants in the same family.",
"support": 40,
"confidence": 1.0
},
{
"id": "reconstruction.flanged_rotational_part.primary",
"kind": "reconstruction_grammar",
"scope": [
"flanged_rotational_part"
],
"when": {
"features": [
"circular_equal_radius_pattern",
"coaxial_cylindrical_stack",
"cylindrical_feature_network",
"multi_level_planar_profile",
"repeated_axial_hole_pattern",
"rotational_body",
"stepped_cylindrical_passage"
],
"relations": [
"repeated_radius_group"
]
},
"guidance": "Represent repeated axial holes as a symmetric mounting pattern tied to the rotational body.",
"semantic_rationale": "The motif remains useful across variants because it captures repeated functional placement around a rotational form.",
"support": 2,
"confidence": 1.0,
"check": "Confirm that repeated axial holes remain grouped consistently around the rotational body.",
"promotion_state": "candidate",
"required_support": 20,
"remaining_support": 18,
"required_confidence": 0.8,
"consumer_policy": "visible_for_review_but_not_available_to_cad_router"
},
{
"id": "validation.rotational_part.axis_and_pattern_consistency",
"kind": "validation_rule",
"scope": [
"rotational_part"
],
"when": {
"features": [
"central_passage",
"repeated_axial_hole_pattern"
],
"relations": []
},
"guidance": "Validate both the shared rotational axis and the grouped repeated-hole relationship before handoff.",
"semantic_rationale": "These checks protect transferable intent without relying on instance geometry.",
"support": 2,
"confidence": 1.0,
"check": "Confirm shared-axis alignment and repeated-hole grouping before treating the model as ready.",
"promotion_state": "candidate",
"required_support": 20,
"remaining_support": 18,
"required_confidence": 0.8,
"consumer_policy": "visible_for_review_but_not_available_to_cad_router"
}
]
}
@@ -0,0 +1 @@
@@ -0,0 +1 @@
@@ -0,0 +1 @@
@@ -0,0 +1 @@
@@ -0,0 +1 @@