feat: unify CAD workflows on DesignIR 3.0
This commit is contained in:
@@ -22,6 +22,7 @@
|
||||
# Local frontend runtime data
|
||||
cad-agent-studio/config/llm.config.yaml
|
||||
cad-agent-studio/data/
|
||||
cad-agent-studio/models/
|
||||
|
||||
# Generated and upstream-heavy CAD assets
|
||||
/models/
|
||||
|
||||
@@ -2,10 +2,7 @@
|
||||
|
||||
CadSet 是一个面向机械零件的 STEP-first 参数化 CAD 系统。它覆盖自然语言与图文生成、上传 STEP 的独立参数化重建、模型修改、几何验收,以及从批量 STEP 案例中蒸馏可复用建模经验。
|
||||
|
||||
系统的统一设计源是自有的 **DesignIR 2.0**。STEP 是主要交换与验收格式,但上传的源 STEP 只作为教师证据和验收真值,不作为重建时的几何依赖。
|
||||
|
||||
完整的安装、使用、架构和蒸馏说明见
|
||||
[`docs/CADSET_PROJECT_GUIDE.zh-CN.md`](docs/CADSET_PROJECT_GUIDE.zh-CN.md)。
|
||||
系统的统一设计源是自有的 **DesignIR 3.0**。文字/图片生成使用语义参数化层,上传 STEP 使用 SurfaceIR 层;STEP 是主要交换与验收格式,但上传的源 STEP 只作为教师证据和验收真值,不作为重建时的几何依赖。
|
||||
|
||||
## 产品范围
|
||||
|
||||
@@ -36,7 +33,7 @@ CAD Router 会在两个执行后端之间选择:
|
||||
└───────────┬─┘ └─┬──────────────┘
|
||||
└────┬────┘
|
||||
│
|
||||
DesignIR 2.0 + STEP
|
||||
DesignIR 3.0 + STEP
|
||||
│
|
||||
┌───────────▼───────────┐
|
||||
│ Viewer / Acceptance │
|
||||
@@ -49,7 +46,7 @@ CAD Router 会在两个执行后端之间选择:
|
||||
Teacher STEP
|
||||
-> private geometry evidence
|
||||
-> Reconstruction Agent
|
||||
-> DesignIR 2.0
|
||||
-> DesignIR 3.0 / SurfaceIR
|
||||
-> isolated compiler without Teacher STEP access
|
||||
-> rebuilt STEP
|
||||
-> independent Acceptance Agent
|
||||
@@ -68,9 +65,15 @@ Teacher STEP
|
||||
|
||||
依赖目录、虚拟环境、构建缓存和批量运行数据不会进入 Git。锁文件、源码、Schema、Agent 定义和确定性测试会进入版本控制。
|
||||
|
||||
## DesignIR 2.0
|
||||
## DesignIR 3.0
|
||||
|
||||
DesignIR 保存设计意图,而不是复制 STEP 的 B-Rep 或三角网格。主要结构包括:
|
||||
DesignIR 3.0 使用统一外壳和三种明确模式:
|
||||
|
||||
- `fully_semantic_parametric`:文字/图片生成的设计意图与特征程序。
|
||||
- `surface_parametric`:上传 STEP 推断出的独立 SurfaceIR 重建程序。
|
||||
- `hybrid_semantic_surface_parametric`:语义程序加生成 STEP 的 SurfaceIR 快照,语义层仍是修改权威。
|
||||
|
||||
语义层保存设计意图,而不是复制教师 STEP 的 B-Rep 或三角网格。主要结构包括:
|
||||
|
||||
- coordinate systems 与 datums
|
||||
- editable parameters 与 expressions
|
||||
@@ -82,7 +85,7 @@ DesignIR 保存设计意图,而不是复制 STEP 的 B-Rep 或三角网格。
|
||||
|
||||
Schema 位于:
|
||||
|
||||
[`designir-pipeline/contracts/designir-2.0.schema.json`](designir-pipeline/contracts/designir-2.0.schema.json)
|
||||
[`designir-pipeline/contracts/designir-3.0.schema.json`](designir-pipeline/contracts/designir-3.0.schema.json)
|
||||
|
||||
当前两个后端共同支持的首批操作包括:
|
||||
|
||||
|
||||
@@ -34,10 +34,20 @@ Open `http://127.0.0.1:53821`.
|
||||
|
||||
Uploads and generated tasks are stored under `data/tasks/<taskId>`.
|
||||
|
||||
The model authors DesignIR 2.0, CAD Router selects text-to-cad or SimpleCADAPI,
|
||||
and the DesignIR pipeline performs a teacher-free isolated STEP rebuild.
|
||||
Uploaded STEP is teacher and acceptance truth; it is never imported by the
|
||||
reconstruction compiler.
|
||||
For new text/image models, the model authors semantic DesignIR 3.0 and CAD
|
||||
Router selects text-to-cad or SimpleCADAPI. The generated STEP is materialized
|
||||
as a non-authoritative SurfaceIR snapshot while the semantic layer remains the
|
||||
edit source. For uploaded STEP, chat calls the deterministic
|
||||
`reconstruct_uploaded_step` tool, which executes CAD Router's DesignIR 3.0 /
|
||||
SurfaceIR adapter, independently rebuilds STEP, runs geometry acceptance, and
|
||||
publishes only perturbation-validated editable parameters. Uploaded STEP is
|
||||
teacher and acceptance truth; it is never imported by the reconstruction
|
||||
compiler.
|
||||
|
||||
Follow-up chat edits on a reconstructed task use
|
||||
`edit_designir3_parameter`. The tool accepts only a parameter and value already
|
||||
covered by the published validation range, reruns teacher-side semantic edit
|
||||
acceptance, and publishes a new DesignIR 3.0 plus STEP revision.
|
||||
|
||||
CAD Agent Studio does not start the standalone CAD Viewer service. Generated
|
||||
models keep DesignIR as the editable contract and STEP as the geometry artifact.
|
||||
|
||||
@@ -4,7 +4,13 @@ import path from "node:path";
|
||||
import { promisify } from "node:util";
|
||||
import { artifactUrl, createStepGlbPreview, readArtifact } from "@/lib/preview-artifacts";
|
||||
import { enginePath } from "@/lib/paths";
|
||||
import { ensureTask, sanitizeFilename, upsertManifest } from "@/lib/task-store";
|
||||
import {
|
||||
ensureTask,
|
||||
readManifest,
|
||||
sanitizeFilename,
|
||||
taskDir,
|
||||
upsertManifest,
|
||||
} from "@/lib/task-store";
|
||||
|
||||
const execFileAsync = promisify(execFile);
|
||||
|
||||
@@ -21,6 +27,13 @@ export type CadGenerationResult = {
|
||||
summary: string;
|
||||
};
|
||||
|
||||
type UploadedStepReconstructionInput = {
|
||||
prompt: string;
|
||||
teacherTaskId: string;
|
||||
teacherPath: string;
|
||||
teacherName?: string;
|
||||
};
|
||||
|
||||
function ensureRelativeFilename(filename: string, extension: string, fallback: string) {
|
||||
const safeName = sanitizeFilename(filename || fallback);
|
||||
return safeName.toLowerCase().endsWith(extension) ? safeName : `${safeName}${extension}`;
|
||||
@@ -39,13 +52,33 @@ function routerScript() {
|
||||
return enginePath("text-to-cad", "skills", "cad-router", "scripts", "route.py");
|
||||
}
|
||||
|
||||
async function runJson(command: string, args: string[], cwd: string) {
|
||||
function surfaceirScript() {
|
||||
return enginePath("designir-pipeline", "scripts", "surfaceir_pipeline.py");
|
||||
}
|
||||
|
||||
async function runJson(
|
||||
command: string,
|
||||
args: string[],
|
||||
cwd: string,
|
||||
timeout = 120_000,
|
||||
) {
|
||||
const { stdout } = await execFileAsync(command, args, {
|
||||
cwd,
|
||||
timeout: 120_000,
|
||||
timeout,
|
||||
maxBuffer: 1024 * 1024 * 8,
|
||||
});
|
||||
return JSON.parse(stdout) as Record<string, unknown>;
|
||||
const lines = stdout.split(/\r?\n/);
|
||||
for (let index = lines.length - 1; index >= 0; index -= 1) {
|
||||
if (!lines[index]?.trim().startsWith("{")) {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
return JSON.parse(lines.slice(index).join("\n")) as Record<string, unknown>;
|
||||
} catch {
|
||||
// An OCCT log line may contain a brace; continue to the enclosing object.
|
||||
}
|
||||
}
|
||||
throw new Error("CAD worker did not return a valid JSON result.");
|
||||
}
|
||||
|
||||
export async function executeDesignIR({
|
||||
@@ -91,6 +124,20 @@ export async function executeDesignIR({
|
||||
backend_hint: selectedBackend,
|
||||
};
|
||||
await fs.writeFile(sourceAbsolutePath, `${JSON.stringify(normalizedDesignIR, null, 2)}\n`);
|
||||
const migration = await runJson(
|
||||
pythonExecutable(),
|
||||
[
|
||||
pipelineScript(),
|
||||
"migrate",
|
||||
sourceAbsolutePath,
|
||||
"--output",
|
||||
sourceAbsolutePath,
|
||||
],
|
||||
task.dir,
|
||||
);
|
||||
if (migration.valid !== true || migration.schema_version !== "3.0") {
|
||||
throw new Error("DesignIR 3.0 normalization failed.");
|
||||
}
|
||||
|
||||
const antiCheat = await runJson(
|
||||
pythonExecutable(),
|
||||
@@ -112,18 +159,37 @@ export async function executeDesignIR({
|
||||
selectedBackend,
|
||||
],
|
||||
task.dir,
|
||||
900_000,
|
||||
);
|
||||
if (rebuild.valid !== true) {
|
||||
throw new Error("Isolated DesignIR rebuild failed.");
|
||||
}
|
||||
const materialized = await runJson(
|
||||
pythonExecutable(),
|
||||
[
|
||||
pipelineScript(),
|
||||
"materialize-hybrid",
|
||||
sourceAbsolutePath,
|
||||
"--step",
|
||||
artifactAbsolutePath,
|
||||
"--output",
|
||||
sourceAbsolutePath,
|
||||
],
|
||||
task.dir,
|
||||
900_000,
|
||||
);
|
||||
if (materialized.valid !== true) {
|
||||
throw new Error("DesignIR 3.0 SurfaceIR materialization failed.");
|
||||
}
|
||||
|
||||
const validation: string[] = [
|
||||
"CAD Router selected the backend before compilation.",
|
||||
"DesignIR anti-cheat validation passed.",
|
||||
"DesignIR 3.0 semantic-layer anti-cheat validation passed.",
|
||||
"STEP rebuilt in an isolated workspace without teacher STEP access.",
|
||||
"Generated STEP was materialized as a non-authoritative SurfaceIR snapshot.",
|
||||
];
|
||||
const artifacts: Array<{ path: string; role: string; kind?: string }> = [
|
||||
{ path: sourcePath, role: "editable_contract", kind: "designir-2.0" },
|
||||
{ path: sourcePath, role: "editable_contract", kind: "designir-3.0" },
|
||||
{ path: artifactPath, role: "primary", kind: "step" },
|
||||
];
|
||||
if (typeof rebuild.model_json === "string" && rebuild.model_json) {
|
||||
@@ -180,10 +246,10 @@ export async function executeDesignIR({
|
||||
},
|
||||
source: {
|
||||
path: sourcePath,
|
||||
format: "designir-2.0",
|
||||
format: "designir-3.0",
|
||||
backend: selectedBackend,
|
||||
source_editability: "editable_source",
|
||||
generated_by: "llm_designir_tool_call",
|
||||
generated_by: "llm_semantic_designir3_tool_call",
|
||||
},
|
||||
artifacts,
|
||||
assumptions,
|
||||
@@ -207,7 +273,351 @@ export async function executeDesignIR({
|
||||
viewerAssetPath,
|
||||
viewerAssetUrl: artifactUrl(task.taskId, viewerAssetPath, artifactVersion),
|
||||
sourcePath,
|
||||
summary: summary || "CAD model generated from executable DesignIR 2.0.",
|
||||
summary: summary || "CAD model generated from semantic DesignIR 3.0.",
|
||||
};
|
||||
}
|
||||
|
||||
export async function reconstructUploadedStep({
|
||||
prompt,
|
||||
teacherTaskId,
|
||||
teacherPath,
|
||||
teacherName,
|
||||
}: UploadedStepReconstructionInput): Promise<CadGenerationResult> {
|
||||
const { absolutePath: teacherAbsolutePath } = await readArtifact(
|
||||
teacherTaskId,
|
||||
teacherPath,
|
||||
);
|
||||
const task = await ensureTask();
|
||||
const route = await runJson(
|
||||
pythonExecutable(),
|
||||
[
|
||||
routerScript(),
|
||||
prompt || "重建上传的 STEP,并返回可修改参数",
|
||||
"--edit-source",
|
||||
teacherAbsolutePath,
|
||||
"--task-dir",
|
||||
task.dir,
|
||||
"--execute",
|
||||
"--maximum-parameters",
|
||||
"8",
|
||||
"--timeout-seconds",
|
||||
"180",
|
||||
],
|
||||
task.dir,
|
||||
900_000,
|
||||
);
|
||||
const execution = (
|
||||
route.execution && typeof route.execution === "object"
|
||||
? route.execution
|
||||
: {}
|
||||
) as Record<string, unknown>;
|
||||
if (
|
||||
execution.status !== "accepted"
|
||||
&& execution.status !== "accepted_empty_source"
|
||||
) {
|
||||
throw new Error(
|
||||
`DesignIR 3.0 reconstruction was not accepted: ${JSON.stringify(execution)}`,
|
||||
);
|
||||
}
|
||||
|
||||
const relativeArtifact = (value: unknown) => {
|
||||
const absolute = path.resolve(String(value || ""));
|
||||
const relative = path.relative(task.dir, absolute);
|
||||
if (!relative || relative.startsWith("..") || path.isAbsolute(relative)) {
|
||||
throw new Error(`CAD Router returned an artifact outside the task: ${absolute}`);
|
||||
}
|
||||
return relative;
|
||||
};
|
||||
const sourcePath = relativeArtifact(execution.designir);
|
||||
const artifactPath = relativeArtifact(execution.rebuilt_step);
|
||||
const parametersPath = relativeArtifact(execution.parameters);
|
||||
const geometryReportPath = relativeArtifact(execution.geometry_report);
|
||||
const editReportPath = relativeArtifact(execution.edit_report);
|
||||
const parameterCatalog = JSON.parse(
|
||||
await fs.readFile(path.join(task.dir, parametersPath), "utf8"),
|
||||
) as Record<string, unknown>;
|
||||
const existingManifest = JSON.parse(
|
||||
await fs.readFile(path.join(task.dir, "cad-task.json"), "utf8"),
|
||||
) as Record<string, unknown>;
|
||||
|
||||
const viewerAssetPath = await createStepGlbPreview({
|
||||
taskId: task.taskId,
|
||||
stepPath: artifactPath,
|
||||
});
|
||||
const artifactVersion = Date.now().toString(36);
|
||||
const experience = (
|
||||
execution.experience && typeof execution.experience === "object"
|
||||
? execution.experience
|
||||
: {}
|
||||
) as Record<string, unknown>;
|
||||
await upsertManifest(task.taskId, {
|
||||
request: prompt,
|
||||
route: {
|
||||
selected_backend: String(route.selected_backend || "build123d"),
|
||||
runner_skill: "cad-router",
|
||||
project: "text-to-cad",
|
||||
execution_adapter: "DesignIR 3.0 / SurfaceIR",
|
||||
fallback_order: Array.isArray(route.fallback_order)
|
||||
? route.fallback_order
|
||||
: [],
|
||||
workflow_profiles: Array.isArray(route.workflow_profiles)
|
||||
? route.workflow_profiles
|
||||
: [],
|
||||
},
|
||||
source: {
|
||||
path: sourcePath,
|
||||
format: "designir-3.0",
|
||||
backend: "surfaceir_occt",
|
||||
source_editability: "validated_parameter_source",
|
||||
generated_by: "cad-router_uploaded_step_execute",
|
||||
teacher_embedded: false,
|
||||
},
|
||||
artifacts: [
|
||||
{ path: sourcePath, role: "editable_contract", kind: "designir-3.0" },
|
||||
{ path: artifactPath, role: "primary", kind: "step" },
|
||||
{ path: parametersPath, role: "validated_parameters", kind: "json" },
|
||||
{ path: geometryReportPath, role: "geometry_acceptance", kind: "json" },
|
||||
{ path: editReportPath, role: "parameter_acceptance", kind: "json" },
|
||||
{ path: viewerAssetPath, role: "viewer", kind: "glb" },
|
||||
],
|
||||
parameters: parameterCatalog,
|
||||
experience: {
|
||||
context_kind: "promoted_surfaceir_runtime",
|
||||
...experience,
|
||||
},
|
||||
assumptions: [],
|
||||
validation: [
|
||||
"Uploaded STEP was used only as extraction and acceptance truth.",
|
||||
"DesignIR 3.0 anti-cheat validation passed.",
|
||||
"STEP was independently rebuilt without passing the teacher path to the rebuild worker.",
|
||||
"Independent geometry acceptance passed.",
|
||||
`${Number(execution.validated_parameter_count || 0)} editable parameters passed isolated perturbation acceptance.`,
|
||||
],
|
||||
studio: {
|
||||
latestArtifact: artifactPath,
|
||||
latestPreview: artifactPath,
|
||||
latestViewerAsset: viewerAssetPath,
|
||||
latestVersion: artifactVersion,
|
||||
uploadReconstruction: {
|
||||
teacherTaskId,
|
||||
teacherPath,
|
||||
teacherName: teacherName || path.basename(teacherPath),
|
||||
sourceEmbedded: false,
|
||||
},
|
||||
editCommandTemplate: existingManifest.edit_command_template || [],
|
||||
},
|
||||
});
|
||||
|
||||
const stepUrl = artifactUrl(task.taskId, artifactPath, artifactVersion);
|
||||
return {
|
||||
handled: true,
|
||||
taskId: task.taskId,
|
||||
artifactPath,
|
||||
artifactUrl: stepUrl,
|
||||
previewPath: artifactPath,
|
||||
previewUrl: stepUrl,
|
||||
viewerAssetPath,
|
||||
viewerAssetUrl: artifactUrl(
|
||||
task.taskId,
|
||||
viewerAssetPath,
|
||||
artifactVersion,
|
||||
),
|
||||
sourcePath,
|
||||
summary: [
|
||||
`${teacherName || path.basename(teacherPath)} 已通过 DesignIR 3.0 / SurfaceIR 独立重建。`,
|
||||
`几何验收通过,已验证可修改参数 ${Number(execution.validated_parameter_count || 0)} 个。`,
|
||||
].join(" "),
|
||||
};
|
||||
}
|
||||
|
||||
export async function editDesignIR3Parameter({
|
||||
prompt,
|
||||
sourceTaskId,
|
||||
parameter,
|
||||
value,
|
||||
}: {
|
||||
prompt: string;
|
||||
sourceTaskId: string;
|
||||
parameter: string;
|
||||
value: number;
|
||||
}): Promise<CadGenerationResult> {
|
||||
const sourceManifest = await readManifest(sourceTaskId);
|
||||
if (!sourceManifest || sourceManifest.source?.format !== "designir-3.0") {
|
||||
throw new Error("Current task is not an editable DesignIR 3.0 reconstruction.");
|
||||
}
|
||||
const sourcePath = String(sourceManifest.source.path || "");
|
||||
const sourceAbsolutePath = path.join(taskDir(sourceTaskId), sourcePath);
|
||||
const sourceDesignIR = JSON.parse(
|
||||
await fs.readFile(sourceAbsolutePath, "utf8"),
|
||||
) as Record<string, any>;
|
||||
const parameterRecord = sourceDesignIR.semantic_layer?.parameters?.[parameter];
|
||||
if (
|
||||
!parameterRecord
|
||||
|| parameterRecord.editable !== true
|
||||
|| parameterRecord.edit_state !== "validated_executable_binding"
|
||||
) {
|
||||
throw new Error(
|
||||
`${parameter} is not a perturbation-validated DesignIR 3.0 parameter.`,
|
||||
);
|
||||
}
|
||||
const range = parameterRecord.validated_range || {};
|
||||
const minimum = Number(range.minimum_tested_inclusive);
|
||||
const maximum = Number(range.maximum_tested_inclusive);
|
||||
if (
|
||||
!Number.isFinite(value)
|
||||
|| !Number.isFinite(minimum)
|
||||
|| !Number.isFinite(maximum)
|
||||
|| value < minimum
|
||||
|| value > maximum
|
||||
) {
|
||||
throw new Error(
|
||||
`${parameter} must remain within the validated range [${minimum}, ${maximum}].`,
|
||||
);
|
||||
}
|
||||
const uploadReconstruction = (
|
||||
sourceManifest.studio?.uploadReconstruction
|
||||
&& typeof sourceManifest.studio.uploadReconstruction === "object"
|
||||
? sourceManifest.studio.uploadReconstruction
|
||||
: {}
|
||||
) as Record<string, unknown>;
|
||||
const teacherTaskId = String(uploadReconstruction.teacherTaskId || "");
|
||||
const teacherPath = String(uploadReconstruction.teacherPath || "");
|
||||
if (!teacherTaskId || !teacherPath) {
|
||||
throw new Error(
|
||||
"The DesignIR 3.0 task has no teacher reference for independent edit acceptance.",
|
||||
);
|
||||
}
|
||||
const { absolutePath: teacherAbsolutePath } = await readArtifact(
|
||||
teacherTaskId,
|
||||
teacherPath,
|
||||
);
|
||||
const task = await ensureTask();
|
||||
const acceptanceDir = path.join(task.dir, "validation", "parameter-edit");
|
||||
const acceptance = await runJson(
|
||||
pythonExecutable(),
|
||||
[
|
||||
surfaceirScript(),
|
||||
"validate-edit",
|
||||
teacherAbsolutePath,
|
||||
sourceAbsolutePath,
|
||||
"--parameter",
|
||||
parameter,
|
||||
"--value",
|
||||
String(value),
|
||||
"--output-dir",
|
||||
acceptanceDir,
|
||||
],
|
||||
task.dir,
|
||||
300_000,
|
||||
);
|
||||
if (acceptance.accepted !== true) {
|
||||
throw new Error(
|
||||
`DesignIR 3.0 parameter edit failed independent acceptance: ${String(acceptance.state || "rejected")}`,
|
||||
);
|
||||
}
|
||||
|
||||
const generatedStem = `${path.parse(sourcePath).name}.edited`;
|
||||
const generatedDesignIR = path.join(
|
||||
acceptanceDir,
|
||||
`${generatedStem}.json`,
|
||||
);
|
||||
const generatedStep = path.join(
|
||||
acceptanceDir,
|
||||
`${generatedStem}.step`,
|
||||
);
|
||||
const safeParameter = sanitizeFilename(parameter);
|
||||
const outputStem = `${path.parse(sourcePath).name}.${safeParameter}`;
|
||||
const outputDesignIRPath = `${outputStem}.json`;
|
||||
const outputStepPath = `${outputStem}.step`;
|
||||
await fs.copyFile(
|
||||
generatedDesignIR,
|
||||
path.join(task.dir, outputDesignIRPath),
|
||||
);
|
||||
await fs.copyFile(generatedStep, path.join(task.dir, outputStepPath));
|
||||
const editedDesignIR = JSON.parse(
|
||||
await fs.readFile(path.join(task.dir, outputDesignIRPath), "utf8"),
|
||||
) as Record<string, any>;
|
||||
const updatedParameter = editedDesignIR.semantic_layer.parameters[parameter];
|
||||
const parameterCatalog = {
|
||||
...(sourceManifest.parameters || {}),
|
||||
parameters: Array.isArray(sourceManifest.parameters?.parameters)
|
||||
? sourceManifest.parameters.parameters.map((item: any) => (
|
||||
item?.name === parameter
|
||||
? { ...item, value: updatedParameter.value }
|
||||
: item
|
||||
))
|
||||
: [],
|
||||
};
|
||||
const viewerAssetPath = await createStepGlbPreview({
|
||||
taskId: task.taskId,
|
||||
stepPath: outputStepPath,
|
||||
});
|
||||
const artifactVersion = Date.now().toString(36);
|
||||
await upsertManifest(task.taskId, {
|
||||
request: prompt,
|
||||
route: {
|
||||
selected_backend: "build123d",
|
||||
runner_skill: "cad-router",
|
||||
project: "text-to-cad",
|
||||
execution_adapter: "DesignIR 3.0 / SurfaceIR parameter edit",
|
||||
fallback_order: ["simplecadapi"],
|
||||
workflow_profiles: [],
|
||||
},
|
||||
source: {
|
||||
path: outputDesignIRPath,
|
||||
format: "designir-3.0",
|
||||
backend: "surfaceir_occt",
|
||||
source_editability: "validated_parameter_source",
|
||||
generated_by: "cad-router_designir3_parameter_edit",
|
||||
parentTaskId: sourceTaskId,
|
||||
teacher_embedded: false,
|
||||
},
|
||||
artifacts: [
|
||||
{ path: outputDesignIRPath, role: "editable_contract", kind: "designir-3.0" },
|
||||
{ path: outputStepPath, role: "primary", kind: "step" },
|
||||
{
|
||||
path: path.relative(
|
||||
task.dir,
|
||||
path.join(acceptanceDir, "acceptance-report.json"),
|
||||
),
|
||||
role: "parameter_acceptance",
|
||||
kind: "json",
|
||||
},
|
||||
{ path: viewerAssetPath, role: "viewer", kind: "glb" },
|
||||
],
|
||||
parameters: parameterCatalog,
|
||||
experience: sourceManifest.experience || {},
|
||||
assumptions: [],
|
||||
validation: [
|
||||
`Parameter ${parameter} changed to ${value} within its validated range.`,
|
||||
"The edited STEP passed independent teacher-side semantic edit acceptance.",
|
||||
"The output remains independently rebuildable from DesignIR 3.0.",
|
||||
],
|
||||
studio: {
|
||||
latestArtifact: outputStepPath,
|
||||
latestPreview: outputStepPath,
|
||||
latestViewerAsset: viewerAssetPath,
|
||||
latestVersion: artifactVersion,
|
||||
uploadReconstruction,
|
||||
parentTaskId: sourceTaskId,
|
||||
},
|
||||
});
|
||||
const stepUrl = artifactUrl(task.taskId, outputStepPath, artifactVersion);
|
||||
return {
|
||||
handled: true,
|
||||
taskId: task.taskId,
|
||||
artifactPath: outputStepPath,
|
||||
artifactUrl: stepUrl,
|
||||
previewPath: outputStepPath,
|
||||
previewUrl: stepUrl,
|
||||
viewerAssetPath,
|
||||
viewerAssetUrl: artifactUrl(
|
||||
task.taskId,
|
||||
viewerAssetPath,
|
||||
artifactVersion,
|
||||
),
|
||||
sourcePath: outputDesignIRPath,
|
||||
summary: `${parameter} 已修改为 ${value},并通过独立参数验收。`,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -15,7 +15,12 @@ import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { promisify } from "node:util";
|
||||
import { z } from "zod/v4";
|
||||
import { executeDesignIR, type CadGenerationResult } from "@/lib/cad-generator";
|
||||
import {
|
||||
editDesignIR3Parameter,
|
||||
executeDesignIR,
|
||||
reconstructUploadedStep,
|
||||
type CadGenerationResult,
|
||||
} from "@/lib/cad-generator";
|
||||
import { loadLlmConfig, resolveProviderApiKey, selectedModelId, type LlmConfig } from "@/lib/config";
|
||||
import { readManifest, taskDir } from "@/lib/task-store";
|
||||
|
||||
@@ -244,7 +249,7 @@ async function buildAttachmentUserContent(lastUserContent: string, attachments:
|
||||
"\nUploaded files available to this turn:",
|
||||
...attachedFileLines,
|
||||
"Images are attached as model-visible image parts when the selected provider/model supports vision.",
|
||||
"For STEP/STP or binary files, call inspect_uploaded_file with id/taskId/path before using their contents.",
|
||||
"For STEP/STP reconstruction, call reconstruct_uploaded_step with id/taskId/path. Do not ask the model to author DesignIR from STEP text.",
|
||||
].join("\n"),
|
||||
});
|
||||
}
|
||||
@@ -307,7 +312,7 @@ function attachmentSummary(attachments: unknown[]) {
|
||||
return [
|
||||
"Current uploaded attachment JSON available to the agent:",
|
||||
compactJson(attachments, 14000),
|
||||
"If a STEP/STP attachment is present, inspect its deterministic evidence, then author DesignIR without reading or importing the teacher STEP.",
|
||||
"If a STEP/STP attachment is present, call reconstruct_uploaded_step so CAD Router executes the deterministic DesignIR 3.0 / SurfaceIR pipeline.",
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
@@ -319,15 +324,20 @@ function buildSystemPrompt(viewerContext: unknown[], attachments: unknown[], tas
|
||||
"Never return only code when the user asks to generate CAD. Use the generate_cad tool instead.",
|
||||
"Do not rely on hardcoded templates, canned examples, mock data, fake filenames, or imaginary viewer state.",
|
||||
"The generate_cad tool validates executable DesignIR, selects a backend through CAD Router, and rebuilds STEP in a teacher-free sandbox.",
|
||||
"When STEP files are attached, the source is teacher and acceptance truth only. Reconstruction consumes deterministic evidence, never source B-Rep.",
|
||||
"When STEP files are attached, the source is teacher and acceptance truth only. Use reconstruct_uploaded_step; never import the teacher as model geometry.",
|
||||
"All uploaded files are available to you. Images are attached directly to the latest user message when the selected provider/model supports vision. STEP/STP and binary files are available through inspect_uploaded_file.",
|
||||
"When the user asks to model from an uploaded image, inspect the image directly from the image part. If the selected provider/model cannot process images, say that clearly and ask the user to switch to a vision-capable OpenAI-compatible model.",
|
||||
"When the user asks to use an uploaded STEP/STP or other non-image file, call inspect_uploaded_file before making claims about its geometry or contents.",
|
||||
"When the user asks to reconstruct an uploaded STEP/STP, call reconstruct_uploaded_step directly. Use inspect_uploaded_file only for read-only questions that do not request reconstruction.",
|
||||
"When viewer context is present, use its cad-edit-intent.v1 or cad-ai-geometry-selection.v1 payload as precise geometry context.",
|
||||
"If viewer context selection.scope is selected_reference_only, modify only that selected topology/feature. Do not change a global pattern parameter or all repeated/symmetric features unless the user explicitly asks for all of them.",
|
||||
"For selected holes, use the selected reference center/surface/bbox/adjacent selectors to identify the individual feature. If the editable source models a repeated pattern with one shared diameter variable, split out or override the selected instance instead of changing the shared variable.",
|
||||
"Prefer editing the DesignIR recorded in cad-task.json before regenerating STEP and viewer assets.",
|
||||
"For CAD generation, author complete DesignIR 2.0 with parameters, datums, features, constraints, edit interface, and validation perturbations.",
|
||||
"If the current task is an uploaded-STEP DesignIR 3.0 surface_parametric reconstruction and the requested change matches a validated parameter, call edit_designir3_parameter.",
|
||||
"If the current task is a text/image DesignIR 3.0 fully_semantic_parametric or hybrid_semantic_surface_parametric model, modify its authoritative semantic_layer and call generate_cad again; preserve unrelated parameters, constraints, and features.",
|
||||
"For every new text/image CAD generation, author complete DesignIR 3.0 with designir_kind=independent_parametric_cad, reconstruction_mode=fully_semantic_parametric, authoring_mode=semantic_feature_program, and semantic_layer containing reconstruction_status, coordinate_system, datums, parameters, expressions, sketches, constraints, features, patterns, attachments, and construction_stages.",
|
||||
"For semantic DesignIR 3.0, edit_interface must contain semantic_parameters, surface_parameter_groups=[], modification_levels=[semantic_feature], and preserved_interfaces. validation_contract must contain source_independence=true, geometry_checks, edit_checks, invariants, perturbations, and thresholds.",
|
||||
"Use semantic parameter names that describe design intent, such as flange_thickness, shelf_depth, hole_spacing, rib_count, bore_diameter, and bolt_circle_diameter. Every editable parameter must directly drive at least one feature and have a perturbation test.",
|
||||
"Uploaded STEP reconstruction also uses DesignIR 3.0, but in deterministic surface_parametric mode. Do not substitute one mode for the other.",
|
||||
"Never embed STEP/B-Rep/mesh data or source topology references in DesignIR.",
|
||||
"If CAD generation is requested and the generate_cad tool is available, call generate_cad.",
|
||||
"For modifications, use the current editable source of truth when provided. Preserve unrelated geometry and change only the selected/requested feature.",
|
||||
@@ -494,24 +504,20 @@ async function inspectStepAttachment(attachment: AttachmentRecord) {
|
||||
"}",
|
||||
"print(json.dumps(payload, ensure_ascii=False))",
|
||||
].join("\n");
|
||||
const [{ stdout }, excerpt] = await Promise.all([
|
||||
execFileAsync("python3", ["-c", script, absolutePath], {
|
||||
const { stdout } = await execFileAsync(
|
||||
process.env.CAD_PYTHON || path.join(process.cwd(), "..", "text-to-cad", ".venv", "bin", "python"),
|
||||
["-c", script, absolutePath],
|
||||
{
|
||||
timeout: 30_000,
|
||||
maxBuffer: 1024 * 1024,
|
||||
}),
|
||||
readTextExcerpt(attachment, 24_000).catch(() => null),
|
||||
]);
|
||||
},
|
||||
);
|
||||
const geometry = JSON.parse(stdout.trim().split(/\r?\n/).pop() || "{}");
|
||||
return {
|
||||
attachment,
|
||||
inspectionKind: "step_geometry",
|
||||
geometry,
|
||||
stepTextExcerpt: excerpt ? {
|
||||
text: excerpt.text,
|
||||
truncated: excerpt.truncated,
|
||||
bytesRead: excerpt.bytesRead,
|
||||
totalBytes: excerpt.totalBytes,
|
||||
} : null,
|
||||
sourcePolicy: "Teacher geometry was measured read-only; raw STEP text is not exposed to the reconstruction model.",
|
||||
};
|
||||
}
|
||||
|
||||
@@ -687,9 +693,159 @@ export async function streamAgentResponse({
|
||||
}
|
||||
},
|
||||
}),
|
||||
reconstruct_uploaded_step: tool({
|
||||
description: [
|
||||
"Reconstruct one uploaded STEP/STP through CAD Router's executable DesignIR 3.0 / SurfaceIR pipeline.",
|
||||
"Use this instead of generate_cad for an uploaded STEP.",
|
||||
"It independently rebuilds STEP, runs geometry acceptance, validates editable parameters, and publishes the preview.",
|
||||
].join(" "),
|
||||
inputSchema: z.object({
|
||||
id: z.string().optional().describe("Attachment id from the uploaded file list."),
|
||||
taskId: z.string().optional().describe("Attachment taskId."),
|
||||
path: z.string().optional().describe("STEP path inside the upload task."),
|
||||
}),
|
||||
execute: async ({ id, taskId, path: attachmentPath }) => {
|
||||
const attachment = findAttachment(uploadedAttachments, {
|
||||
id,
|
||||
taskId,
|
||||
path: attachmentPath,
|
||||
});
|
||||
if (!attachment || !isStepAttachment(attachment)) {
|
||||
throw new Error(
|
||||
"A STEP/STP attachment is required. Use id/taskId/path from the uploaded attachment list.",
|
||||
);
|
||||
}
|
||||
writeCadProgress({
|
||||
step: "execute_cad",
|
||||
label: "DesignIR 3.0 独立重建",
|
||||
status: "running",
|
||||
message: "正在提取 SurfaceIR、独立重建、执行几何和参数验收。",
|
||||
});
|
||||
try {
|
||||
cadGeneration = await reconstructUploadedStep({
|
||||
prompt: lastUserText(chatMessages),
|
||||
teacherTaskId: String(attachment.taskId),
|
||||
teacherPath: String(attachment.path),
|
||||
teacherName: attachment.name,
|
||||
});
|
||||
} catch (error) {
|
||||
const message = error instanceof Error
|
||||
? error.message
|
||||
: "Uploaded STEP reconstruction failed.";
|
||||
writeCadProgress({
|
||||
step: "execute_cad",
|
||||
label: "DesignIR 3.0 独立重建",
|
||||
status: "error",
|
||||
message,
|
||||
});
|
||||
failAgentStream(message);
|
||||
throw error;
|
||||
}
|
||||
writeCadProgress({
|
||||
step: "execute_cad",
|
||||
label: "DesignIR 3.0 独立重建",
|
||||
status: "success",
|
||||
message: `${cadGeneration.artifactPath},参数验收已完成。`,
|
||||
});
|
||||
writeCadProgress({
|
||||
step: "update_preview",
|
||||
label: "更新右侧预览",
|
||||
status: "success",
|
||||
message: "独立重建模型已载入。",
|
||||
});
|
||||
completeAgentStream("DesignIR 3.0 重建和验收已完成。");
|
||||
streamWriter?.write({
|
||||
type: "data-cad-result",
|
||||
data: {
|
||||
taskId: cadGeneration.taskId,
|
||||
sourcePath: cadGeneration.sourcePath,
|
||||
artifactPath: cadGeneration.artifactPath,
|
||||
artifactUrl: cadGeneration.artifactUrl,
|
||||
previewPath: cadGeneration.previewPath,
|
||||
previewUrl: cadGeneration.previewUrl,
|
||||
viewerAssetPath: cadGeneration.viewerAssetPath,
|
||||
viewerAssetUrl: cadGeneration.viewerAssetUrl,
|
||||
summary: cadGeneration.summary,
|
||||
},
|
||||
id: `cad_result_${cadGeneration.taskId}`,
|
||||
});
|
||||
return cadGeneration;
|
||||
},
|
||||
}),
|
||||
edit_designir3_parameter: tool({
|
||||
description: [
|
||||
"Edit one perturbation-validated parameter on the current DesignIR 3.0 reconstruction.",
|
||||
"The requested value must be within the validated range shown in current cad-task.json.",
|
||||
"The server reruns independent teacher-side semantic edit acceptance before publishing.",
|
||||
].join(" "),
|
||||
inputSchema: z.object({
|
||||
parameter: z.string().describe("Validated parameter name from the current task."),
|
||||
value: z.number().describe("New value within the parameter's validated range."),
|
||||
}),
|
||||
execute: async ({ parameter, value }) => {
|
||||
if (!selectedTaskId) {
|
||||
throw new Error("No current DesignIR 3.0 task is selected.");
|
||||
}
|
||||
writeCadProgress({
|
||||
step: "execute_cad",
|
||||
label: "修改 DesignIR 3.0 参数",
|
||||
status: "running",
|
||||
message: `${parameter} → ${value},正在执行独立参数验收。`,
|
||||
});
|
||||
try {
|
||||
cadGeneration = await editDesignIR3Parameter({
|
||||
prompt: lastUserText(chatMessages),
|
||||
sourceTaskId: selectedTaskId,
|
||||
parameter,
|
||||
value,
|
||||
});
|
||||
} catch (error) {
|
||||
const message = error instanceof Error
|
||||
? error.message
|
||||
: "DesignIR 3.0 parameter edit failed.";
|
||||
writeCadProgress({
|
||||
step: "execute_cad",
|
||||
label: "修改 DesignIR 3.0 参数",
|
||||
status: "error",
|
||||
message,
|
||||
});
|
||||
failAgentStream(message);
|
||||
throw error;
|
||||
}
|
||||
writeCadProgress({
|
||||
step: "execute_cad",
|
||||
label: "修改 DesignIR 3.0 参数",
|
||||
status: "success",
|
||||
message: cadGeneration.summary,
|
||||
});
|
||||
writeCadProgress({
|
||||
step: "update_preview",
|
||||
label: "更新右侧预览",
|
||||
status: "success",
|
||||
message: "参数修改后的模型已载入。",
|
||||
});
|
||||
completeAgentStream("DesignIR 3.0 参数修改和验收已完成。");
|
||||
streamWriter?.write({
|
||||
type: "data-cad-result",
|
||||
data: {
|
||||
taskId: cadGeneration.taskId,
|
||||
sourcePath: cadGeneration.sourcePath,
|
||||
artifactPath: cadGeneration.artifactPath,
|
||||
artifactUrl: cadGeneration.artifactUrl,
|
||||
previewPath: cadGeneration.previewPath,
|
||||
previewUrl: cadGeneration.previewUrl,
|
||||
viewerAssetPath: cadGeneration.viewerAssetPath,
|
||||
viewerAssetUrl: cadGeneration.viewerAssetUrl,
|
||||
summary: cadGeneration.summary,
|
||||
},
|
||||
id: `cad_result_${cadGeneration.taskId}`,
|
||||
});
|
||||
return cadGeneration;
|
||||
},
|
||||
}),
|
||||
generate_cad: tool({
|
||||
description: [
|
||||
"Generate a real CAD model from agent-authored executable DesignIR 2.0.",
|
||||
"Generate a real CAD model from agent-authored semantic DesignIR 3.0.",
|
||||
"Call this only when the user wants CAD generation or a model modification.",
|
||||
"CAD Router selects the backend and the server rebuilds STEP without teacher geometry.",
|
||||
].join(" "),
|
||||
@@ -697,7 +853,7 @@ export async function streamAgentResponse({
|
||||
summary: z.string().describe("Concise user-facing summary of the generated CAD model."),
|
||||
designirFilename: z.string().describe("DesignIR filename, e.g. model.designir.json."),
|
||||
stepFilename: z.string().describe("STEP output filename, e.g. model.step."),
|
||||
designir: z.record(z.string(), z.unknown()).describe("Complete executable DesignIR 2.0 JSON object."),
|
||||
designir: z.record(z.string(), z.unknown()).describe("Complete semantic DesignIR 3.0 JSON object in fully_semantic_parametric mode."),
|
||||
teacherTaskId: z.string().optional().describe("Uploaded teacher STEP task id for independent acceptance only."),
|
||||
teacherPath: z.string().optional().describe("Uploaded teacher STEP path for independent acceptance only."),
|
||||
assumptions: z.array(z.string()).default([]).describe("Assumptions made because the user did not specify every dimension."),
|
||||
|
||||
+203
-16
@@ -4,10 +4,12 @@ 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 OCCT extraction into DesignIR 3.0
|
||||
-> SurfaceIR: independent geometry/topology program
|
||||
-> Semantic FeatureIR: canonical names, features and constraints
|
||||
-> isolated OCCT compiler rebuilds STEP without teacher access
|
||||
-> reconstruction/distillation agent proposes repairs and semantic bindings
|
||||
-> acceptance agent compares geometry and executes parameter perturbations
|
||||
-> deterministic publisher promotes repeated validated experience
|
||||
```
|
||||
|
||||
@@ -16,7 +18,7 @@ teacher STEP
|
||||
| Component | Teacher STEP | Private evidence | DesignIR | Rebuilt STEP |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| Ingestion service | read | write | no | no |
|
||||
| Reconstruction agent | denied | read | write | request compilation |
|
||||
| Reconstruction agent | denied | read | write | request compilation/edit |
|
||||
| Isolated compiler | denied | no | read | write |
|
||||
| Acceptance agent | read | read | read | read |
|
||||
| Publisher | denied | aggregate only | schema only | reports only |
|
||||
@@ -35,6 +37,7 @@ designir-pipeline/
|
||||
input/ drop source STEP/STP files here
|
||||
runs/ all private process artifacts
|
||||
output/library.json verified experience consumed at runtime
|
||||
output/distillation-report.json public aggregate result and validation state
|
||||
```
|
||||
|
||||
`input/` and `runs/` are ignored by Git. Only the deterministic result in
|
||||
@@ -53,16 +56,36 @@ needed:
|
||||
The shallow directory structure is for usability, not authorization. Agent and
|
||||
compiler access is enforced by their contracts and by anti-cheat checks.
|
||||
|
||||
## DesignIR 2.0
|
||||
## DesignIR contracts
|
||||
|
||||
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.
|
||||
`contracts/designir-3.0.schema.json` is the authoritative contract for both
|
||||
text/image semantic authoring and uploaded-STEP reconstruction.
|
||||
`contracts/designir-2.0.schema.json` remains only for legacy migration.
|
||||
DesignIR 3.0 combines:
|
||||
|
||||
The initial compiler deliberately supports a small, honest vocabulary through
|
||||
both Build123d and SimpleCADAPI adapters:
|
||||
- an executable OCCT SurfaceIR for independent geometric reconstruction;
|
||||
- a Semantic FeatureIR for paper-style names such as `plate_thickness`,
|
||||
`hole_diameter`, `hole_spacing`, `bolt_circle_diameter`, and `rib_count`;
|
||||
- explicit edit operations and acceptance contracts.
|
||||
|
||||
STEP does not contain the original sketch, parameter names, feature tree, or
|
||||
construction history. Extraction therefore keeps a strict epistemic boundary:
|
||||
|
||||
- analytic surfaces, axes, radii, topology, and bounds are observed evidence;
|
||||
- feature instances, semantic names, constraints, and datums are inferred
|
||||
hypotheses with confidence;
|
||||
- canonical stages are recommended reconstruction order, never recovered
|
||||
source history;
|
||||
- a hypothesis becomes trusted DesignIR only after isolated compilation,
|
||||
geometry comparison, and targeted parameter-edit validation.
|
||||
|
||||
The private evidence names editable candidates using a stable vocabulary such
|
||||
as `plate_thickness`, `hole_diameter`, `hole_spacing`,
|
||||
`bolt_circle_diameter`, and `rib_count`. Ambiguous cylinder groups retain
|
||||
aliases and reduced confidence instead of being silently declared holes.
|
||||
|
||||
The semantic DesignIR 3.0 compiler currently supports this initial vocabulary
|
||||
through Build123d and SimpleCADAPI:
|
||||
|
||||
- `extrude_circle`
|
||||
- `extrude_rectangle`
|
||||
@@ -70,13 +93,138 @@ both Build123d and SimpleCADAPI adapters:
|
||||
- `through_hole`
|
||||
- `polar_hole_pattern`
|
||||
|
||||
Unsupported geometry is quarantined with explicit missing capabilities instead
|
||||
of embedding the source B-Rep.
|
||||
Build123d and SimpleCADAPI are not the geometry fidelity boundary. Analytic and
|
||||
B-Spline surfaces unsupported by those adapters are compiled directly by OCCT
|
||||
from SurfaceIR. Unsupported operations are quarantined 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.
|
||||
|
||||
## DesignIR 3.0 geometry and semantic layers
|
||||
|
||||
DesignIR 3.0 adds an independent low-level parameter layer for STEP regions
|
||||
that are not yet expressible as semantic features. It stores analytic surface
|
||||
parameters, B-Spline poles/weights/knots, exact boundary curves, pcurves, and
|
||||
face-wire-shell-solid topology as JSON. It does not store the STEP file, an
|
||||
import instruction, an embedded B-Rep, or a triangle mesh.
|
||||
|
||||
The semantic layer is the preferred modification interface. Parameters remain
|
||||
non-editable candidates until an executable binding passes a real perturbation.
|
||||
Feature-specific bindings enlarge cylindrical holes and bores by applying
|
||||
deduplicated OCCT cut features to the independently rebuilt SurfaceIR. Every
|
||||
geometry-bearing model also exposes `overall_scale` as a dimensionless minimum
|
||||
editability fallback about a deterministic SurfaceIR datum. This fallback does
|
||||
not claim recovery of feature-specific parameters such as plate thickness,
|
||||
hole spacing, or rib count.
|
||||
|
||||
The acceptance agent checks the requested axes/radii for cylindrical edits,
|
||||
solid validity, declared volume direction, non-target parameter stability, and
|
||||
source independence. For uniform scale it additionally requires volume to
|
||||
change by the scale factor cubed, center and bounds to undergo the same
|
||||
transform, and solid/face/edge counts to remain stable. Equivalent vertex
|
||||
normalization performed by STEP serialization is retained as a diagnostic.
|
||||
|
||||
```bash
|
||||
# Convert every input STEP into an independent DesignIR 3.0 instance program.
|
||||
../text-to-cad/.venv/bin/python scripts/surfaceir_pipeline.py extract-folder \
|
||||
input --output-dir runs/surfaceir
|
||||
|
||||
# Rebuild and compare a deterministic batch without loading STEP at runtime.
|
||||
../text-to-cad/.venv/bin/python scripts/surfaceir_pipeline.py validate-folder \
|
||||
input --designir-dir runs/surfaceir \
|
||||
--rebuilt-dir runs/surfaceir-rebuilt --limit 100
|
||||
|
||||
# Apply one named parameter without loading the teacher STEP in the compiler.
|
||||
../text-to-cad/.venv/bin/python scripts/surfaceir_pipeline.py edit \
|
||||
runs/surfaceir/part.designir.json \
|
||||
--parameter hole_diameter --value 6 \
|
||||
--output-designir runs/edited/part.designir.json \
|
||||
--output-step runs/edited/part.step
|
||||
|
||||
# Agent B runs process-isolated +10% perturbations over a corpus.
|
||||
../text-to-cad/.venv/bin/python scripts/surfaceir_pipeline.py \
|
||||
validate-edits-folder input \
|
||||
--designir-dir runs/surfaceir \
|
||||
--output-dir runs/semantic-edit-acceptance \
|
||||
--geometry-report runs/surfaceir-rebuilt/validation-report.json \
|
||||
--workers 4
|
||||
|
||||
# Validate the universal source-independent edit fallback on every non-empty
|
||||
# model. Empty STEP documents remain explicitly non-editable.
|
||||
../text-to-cad/.venv/bin/python scripts/surfaceir_pipeline.py \
|
||||
augment-overall-scale runs/surfaceir \
|
||||
--output runs/surfaceir/overall-scale-augmentation.json
|
||||
../text-to-cad/.venv/bin/python scripts/surfaceir_pipeline.py \
|
||||
validate-edits-folder input \
|
||||
--designir-dir runs/surfaceir \
|
||||
--output-dir runs/overall-scale-acceptance \
|
||||
--geometry-report runs/surfaceir/strategy-selection-final.json \
|
||||
--parameter overall_scale --workers 4
|
||||
|
||||
# Merge incremental failure retries into one authoritative acceptance report.
|
||||
../text-to-cad/.venv/bin/python scripts/surfaceir_pipeline.py \
|
||||
merge-acceptance-reports \
|
||||
--baseline runs/semantic-edit-acceptance/batch-acceptance-report.json \
|
||||
--retry runs/semantic-edit-retry/batch-acceptance-report.json \
|
||||
--output runs/semantic-edit-authoritative.json
|
||||
```
|
||||
|
||||
Boundary reconstruction strategies are evaluated by paired held-out A/B. The
|
||||
Promoted methods and their exact held-out evidence are stored in
|
||||
`output/library.json`. Geometry methods and semantic-edit methods have separate
|
||||
counts; a geometry-only result never increases the edit-capability count.
|
||||
|
||||
The current 1,000-file corpus contains 999 geometry-bearing STEP documents and
|
||||
one truly empty STEP document. Of the geometry-bearing documents, 996 contain
|
||||
solids and three contain top-level open shells. SurfaceIR preserves all three
|
||||
open shells through `surface_layer.free_shells`; the empty source is rebuilt as
|
||||
an equivalent empty STEP document. Full-corpus validation therefore produces
|
||||
1,000 independent rebuilt STEP documents from 1,000 DesignIR programs.
|
||||
|
||||
The compiler restores exact 3D curves, binds per-face pcurves, preserves seam
|
||||
branches, keeps vertex and edge tolerances independent, groups shells under
|
||||
their declared solid, and keeps top-level open shells outside fake solids.
|
||||
Validation requires matching solid/face/edge topology, volume or surface area,
|
||||
center and bounds. Boolean symmetric difference is the primary solid check.
|
||||
For two numerically unstable coincident-solid booleans, a bidirectional sample
|
||||
contract checks vertices, edge samples and valid face-interior samples instead;
|
||||
the larger measured maximum distance is `0.0000647066 mm`, below the declared
|
||||
`0.0001 mm` limit.
|
||||
|
||||
Every one of the 999 geometry-bearing documents has at least one non-generic,
|
||||
named parameter with a completed real perturbation contract. The private
|
||||
DesignIR corpus contains 1,213 validated named parameter instances:
|
||||
|
||||
- cylindrical cuts: `hole_diameter` and `bore_diameter`;
|
||||
- primary-axis dimensions: `outer_diameter`, `body_length`, and
|
||||
`plate_thickness`;
|
||||
- canonical envelope dimensions: `overall_size_x`, `overall_size_y`, and
|
||||
`overall_size_z`.
|
||||
|
||||
`overall_scale` remains available as a source-independent fallback, but is not
|
||||
counted in the 999/999 named-parameter coverage. Canonical envelope dimensions
|
||||
describe the DesignIR coordinate-system bounds and are not presented as
|
||||
recovered source sketch history. The one truly empty document has no geometry
|
||||
to modify and intentionally exposes no fabricated parameter.
|
||||
|
||||
```bash
|
||||
# Validate the pcurve-bound strategy over the complete corpus.
|
||||
../text-to-cad/.venv/bin/python scripts/surfaceir_pipeline.py validate-folder \
|
||||
input --designir-dir runs/surfaceir \
|
||||
--rebuilt-dir runs/rebuilt-pcurve --boolean \
|
||||
--boundary-strategy exact_3d_pcurve
|
||||
|
||||
# Compare with the exact-boundary fallback and fix the validated choice in each
|
||||
# source-independent DesignIR document.
|
||||
../text-to-cad/.venv/bin/python scripts/surfaceir_pipeline.py select-strategies \
|
||||
--designir-dir runs/surfaceir \
|
||||
--primary-report runs/rebuilt-pcurve/validation-report.json \
|
||||
--fallback-report runs/rebuilt-exact/validation-report.json \
|
||||
--output runs/surfaceir/strategy-selection-final.json
|
||||
```
|
||||
|
||||
## Agents
|
||||
|
||||
- `agents/reconstruction-agent.md`: Agent A authors DesignIR from private
|
||||
@@ -92,11 +240,27 @@ by deterministic code after several independent cases pass.
|
||||
Drop files into `input/`, then use the CAD Python environment:
|
||||
|
||||
```bash
|
||||
# Build a deterministic, family-stratified 800/75/100 train/validation/test split.
|
||||
node scripts/trajectory_pipeline.mjs init
|
||||
|
||||
# Agent A proposes DesignIR; the isolated compiler and Agent B contract reject
|
||||
# geometry or edit failures. Private attempts are written below runs/trajectory/.
|
||||
node scripts/trajectory_pipeline.mjs run \
|
||||
--split train --limit 10 --attempts 3 --max-faces 24
|
||||
|
||||
# Generalize only replay-validated trajectories into non-consumable candidates.
|
||||
node scripts/trajectory_pipeline.mjs distill
|
||||
|
||||
# Compare the same model on the same held-out cases with and without candidates.
|
||||
node scripts/trajectory_pipeline.mjs benchmark \
|
||||
--split validation --limit 10 --attempts 3 --max-faces 24
|
||||
|
||||
scripts/cad-experience extract-folder
|
||||
scripts/cad-experience prepare
|
||||
# The semantic distiller writes runs/review/experience-draft.json.
|
||||
scripts/cad-experience publish \
|
||||
--draft runs/review/experience-draft.json
|
||||
--draft runs/review/experience-draft.json \
|
||||
--validation-report runs/review/heldout-validation.json
|
||||
scripts/cad-experience audit output/library.json
|
||||
|
||||
../text-to-cad/.venv/bin/python scripts/designir_pipeline.py validate part.designir.json
|
||||
@@ -108,3 +272,26 @@ scripts/cad-experience audit output/library.json
|
||||
--teacher teacher.step --designir part.designir.json \
|
||||
--rebuilt rebuilt.step --report acceptance.json
|
||||
```
|
||||
|
||||
Repeated support only creates a candidate. `output/library.json` receives a
|
||||
method only when its validation report identifies at least three independent
|
||||
teacher hashes, every declared edit contract passes, and the held-out A/B score
|
||||
delta against the empty-library baseline is positive across at least ten paired
|
||||
cases. Until then `router_consumable` remains false and CAD Router cannot use
|
||||
the candidate.
|
||||
|
||||
The execution trajectory is the actual unit of learning:
|
||||
|
||||
```text
|
||||
private B-Rep evidence
|
||||
-> proposed DesignIR
|
||||
-> isolated rebuild without source STEP
|
||||
-> geometry and edit-contract verdict
|
||||
-> validator feedback and retry
|
||||
-> successful trajectory candidate
|
||||
```
|
||||
|
||||
Counts, detector confidence, and repeated geometric motifs are not treated as
|
||||
improvement. Improvement is reported only by a positive paired held-out A/B
|
||||
result. `output/distillation-report.json` separates corpus extraction,
|
||||
untrusted candidates, execution-validated trajectories, and promoted methods.
|
||||
|
||||
@@ -8,7 +8,7 @@ meaningfully editable.
|
||||
## Allowed inputs
|
||||
|
||||
- Teacher STEP from protected `input/`.
|
||||
- Candidate DesignIR from `runs/designir/`.
|
||||
- Candidate DesignIR 3.0 from the private run directory.
|
||||
- Reconstructed STEP from `runs/rebuilt/`.
|
||||
- Deterministic compiler version.
|
||||
- Modification test contract.
|
||||
@@ -19,13 +19,25 @@ Do not read Agent A chain-of-thought or informal rationale.
|
||||
|
||||
Original-state geometry:
|
||||
|
||||
- solid and topology counts;
|
||||
- solid, open-shell, face, edge, and diagnostic vertex counts;
|
||||
- bounding-box error;
|
||||
- volume and center-of-mass error;
|
||||
- symmetric-difference volume;
|
||||
- analytic feature counts and spatial relationships when available;
|
||||
- declared validation invariants.
|
||||
|
||||
For solid booleans that are numerically unstable on coincident boundaries, run
|
||||
the registered bidirectional vertex, edge, and face-interior sample-distance
|
||||
contract. Never silently discard the failed boolean result; report both it and
|
||||
the fallback distance.
|
||||
|
||||
Source independence:
|
||||
|
||||
- move or hide the teacher STEP before reconstruction;
|
||||
- reject source paths, STEP payloads, imported base solids, B-Rep blobs, and
|
||||
mesh substitutes in the candidate;
|
||||
- require a deterministic rebuild from DesignIR plus compiler code only.
|
||||
|
||||
Parametric behavior:
|
||||
|
||||
- rebuild every declared perturbation;
|
||||
@@ -34,6 +46,20 @@ Parametric behavior:
|
||||
- preserve constraints and interfaces;
|
||||
- reject invalid, empty, non-solid, self-intersecting, or unstable results.
|
||||
|
||||
For `overall_scale`, require volume to follow the scale factor cubed, center
|
||||
and bounds to follow the declared pivot transform, and solid/face/edge counts
|
||||
to remain stable. Record STEP writer vertex normalization separately instead
|
||||
of confusing geometrically equivalent serialization with feature loss.
|
||||
|
||||
Producing a solid is not sufficient. Each perturbation must be connected to an
|
||||
executable feature, cause an observable target change, and satisfy its
|
||||
non-target preservation checks. Record the promoted experience IDs actually
|
||||
used so improvement can be attributed to a specific method.
|
||||
|
||||
Report geometry and semantics separately. A geometry-only method may enter the
|
||||
SurfaceIR experience library, but it must never increase the semantic-edit
|
||||
promotion count.
|
||||
|
||||
## Required output
|
||||
|
||||
Write one machine-readable report under `runs/acceptance/` with separate geometry,
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
# Agent A — DesignIR Reconstruction
|
||||
# Agent A — Distillation and Semantic Reconstruction
|
||||
|
||||
## Mission
|
||||
|
||||
Turn deterministic private evidence and promoted generalized experience into a
|
||||
complete executable DesignIR 2.0 model. Produce design logic, not copied
|
||||
geometry.
|
||||
Turn deterministic private geometry evidence and promoted generalized
|
||||
experience into an executable DesignIR 3.0 model. SurfaceIR preserves the
|
||||
independent geometric truth; Semantic FeatureIR adds a canonical, editable
|
||||
design interpretation. Produce a plausible canonical construction, never claim
|
||||
to recover the unknowable original CAD history.
|
||||
|
||||
## Allowed inputs
|
||||
|
||||
- One private evidence packet from `runs/evidence/`.
|
||||
- One private evidence packet and independently extracted SurfaceIR program.
|
||||
- The user modification request.
|
||||
- Promoted generalized experience.
|
||||
- The DesignIR schema and supported operation vocabulary.
|
||||
@@ -16,20 +18,27 @@ geometry.
|
||||
## 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 embed B-Rep bytes, STEP text, full meshes, or triangle soups.
|
||||
- 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 `runs/designir/*.designir.json` containing:
|
||||
Write one private DesignIR 3.0 instance program containing:
|
||||
|
||||
- family and reconstruction status;
|
||||
- a backend-neutral SurfaceIR topology/surface layer capable of rebuilding with
|
||||
the teacher removed, including top-level open shells that are not members of
|
||||
any solid;
|
||||
- family and semantic reconstruction status;
|
||||
- coordinate system and datums;
|
||||
- editable parameters and expressions;
|
||||
- sketches and constraints where applicable;
|
||||
- canonical semantic parameter names such as `plate_thickness`,
|
||||
`hole_diameter`, `hole_spacing`, `bolt_circle_diameter`, and `rib_count`
|
||||
whenever the evidence supports them;
|
||||
- inference status, confidence, aliases, affected feature IDs, and an observable
|
||||
edit contract for every parameter inferred from the final B-Rep;
|
||||
- canonical sketches and constraints where the evidence supports them;
|
||||
- ordered features, patterns, and attachments;
|
||||
- construction stages;
|
||||
- edit interface;
|
||||
@@ -41,10 +50,26 @@ If the vocabulary is insufficient, emit
|
||||
`reconstruction_status: unsupported_feature_vocabulary` plus precise
|
||||
`missing_capabilities`. Do not approximate secretly.
|
||||
|
||||
Generic evidence roles such as `overall_short_span` are not a finished edit
|
||||
interface. Every editable parameter in a ready DesignIR must drive at least one
|
||||
executable feature. `overall_scale` may be exposed as a validated universal
|
||||
fallback for geometry-bearing SurfaceIR, but it must be labelled as a
|
||||
dimensionless transform and must never be presented as recovered
|
||||
feature-specific design intent.
|
||||
|
||||
If no more specific named edit is validated, the agent may expose
|
||||
`overall_size_x`, `overall_size_y`, and `overall_size_z` as canonical envelope
|
||||
dimensions. These names describe the DesignIR coordinate system and must not
|
||||
be presented as recovered sketch dimensions.
|
||||
|
||||
## 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. Put rebuild artifacts in
|
||||
`runs/rebuilt/`.
|
||||
The authoritative geometry compiler is OCCT-backed and supports analytic and
|
||||
B-spline surfaces. Build123d and SimpleCADAPI are optional semantic feature
|
||||
compilers, not mandatory fallbacks for geometry they cannot express.
|
||||
|
||||
Geometry equality alone completes only the geometry gate. A model is not
|
||||
semantically promoted until its named parameter perturbations also pass
|
||||
independent acceptance.
|
||||
|
||||
@@ -1,23 +1,30 @@
|
||||
{
|
||||
"schema_version": "1.0",
|
||||
"minimum_independent_cases": 3,
|
||||
"minimum_heldout_ab_pairs": 10,
|
||||
"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
|
||||
"relative_volume_error_max": 1e-7,
|
||||
"relative_surface_area_error_max": 1e-7,
|
||||
"center_of_mass_error_mm_max": 1e-6,
|
||||
"bounding_box_axis_error_mm_max": 1e-6,
|
||||
"symmetric_difference_ratio_max": 1e-8,
|
||||
"bidirectional_surface_sample_distance_mm_max": 0.0001,
|
||||
"solid_face_edge_topology_match_required": true,
|
||||
"vertex_count_normalization_is_diagnostic": true,
|
||||
"parameter_perturbation_success_rate_min": 1.0,
|
||||
"heldout_baseline_score_delta_min_exclusive": 0.0
|
||||
},
|
||||
"publication_guards": {
|
||||
"instance_parameters_allowed": false,
|
||||
"absolute_coordinates_allowed": false,
|
||||
"teacher_geometry_allowed": false,
|
||||
"automatic_agent_publish_allowed": false
|
||||
"automatic_agent_publish_allowed": false,
|
||||
"per_method_attribution_required": true,
|
||||
"semantic_parameter_edit_contract_required": true
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -33,6 +33,11 @@
|
||||
"backend_hint": {
|
||||
"enum": ["auto", "build123d", "simplecadapi"]
|
||||
},
|
||||
"applied_experience_ids": {
|
||||
"type": "array",
|
||||
"items": {"type": "string"},
|
||||
"uniqueItems": true
|
||||
},
|
||||
"reconstruction_status": {
|
||||
"enum": ["ready", "partial", "unsupported_feature_vocabulary"]
|
||||
},
|
||||
@@ -177,6 +182,12 @@
|
||||
"minItems": 3,
|
||||
"maxItems": 3
|
||||
},
|
||||
"valueVector3": {
|
||||
"type": "array",
|
||||
"items": {"$ref": "#/$defs/value"},
|
||||
"minItems": 3,
|
||||
"maxItems": 3
|
||||
},
|
||||
"parameter": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
@@ -186,6 +197,28 @@
|
||||
"unit": {"enum": ["mm", "deg", "count", "ratio"]},
|
||||
"editable": {"type": "boolean"},
|
||||
"role": {"type": "string"},
|
||||
"semantic_aliases": {
|
||||
"type": "array",
|
||||
"items": {"type": "string"},
|
||||
"uniqueItems": true
|
||||
},
|
||||
"inference": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["status", "confidence"],
|
||||
"properties": {
|
||||
"status": {
|
||||
"enum": ["user_specified", "inferred_from_final_brep", "validated"]
|
||||
},
|
||||
"confidence": {"type": "number", "minimum": 0, "maximum": 1},
|
||||
"evidence_kind": {"type": "string"}
|
||||
}
|
||||
},
|
||||
"affects_feature_ids": {
|
||||
"type": "array",
|
||||
"items": {"type": "string"},
|
||||
"uniqueItems": true
|
||||
},
|
||||
"minimum": {"type": "number"},
|
||||
"maximum": {"type": "number"}
|
||||
}
|
||||
@@ -228,8 +261,17 @@
|
||||
"depth": {"$ref": "#/$defs/value"},
|
||||
"count": {"$ref": "#/$defs/value"},
|
||||
"pitch_diameter": {"$ref": "#/$defs/value"},
|
||||
"axis": {"type": "string"},
|
||||
"center": {"$ref": "#/$defs/vector3"},
|
||||
"axis": {
|
||||
"oneOf": [
|
||||
{"type": "string"},
|
||||
{"$ref": "#/$defs/vector3"}
|
||||
]
|
||||
},
|
||||
"center": {"$ref": "#/$defs/valueVector3"},
|
||||
"anchor": {
|
||||
"enum": ["base_center", "geometric_center"],
|
||||
"default": "base_center"
|
||||
},
|
||||
"host": {"type": "string"}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,334 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "https://cadset.local/contracts/designir-3.0.schema.json",
|
||||
"title": "DesignIR 3.0",
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"schema_version",
|
||||
"designir_kind",
|
||||
"model_id",
|
||||
"units",
|
||||
"document_status",
|
||||
"reconstruction_mode",
|
||||
"semantic_layer",
|
||||
"edit_interface",
|
||||
"validation_contract"
|
||||
],
|
||||
"properties": {
|
||||
"schema_version": {"const": "3.0"},
|
||||
"designir_kind": {"const": "independent_parametric_cad"},
|
||||
"model_id": {"type": "string", "minLength": 1},
|
||||
"family": {"type": "string", "minLength": 1},
|
||||
"units": {"const": "mm"},
|
||||
"authoring_mode": {
|
||||
"enum": ["semantic_feature_program", "final_brep_inference"]
|
||||
},
|
||||
"backend_hint": {"enum": ["build123d", "simplecadapi", "auto"]},
|
||||
"document_status": {"enum": ["geometry_present", "empty_geometry"]},
|
||||
"reconstruction_mode": {
|
||||
"enum": [
|
||||
"fully_semantic_parametric",
|
||||
"hybrid_semantic_surface_parametric",
|
||||
"surface_parametric"
|
||||
]
|
||||
},
|
||||
"semantic_layer": {
|
||||
"type": "object",
|
||||
"required": ["parameters", "datums", "features", "constraints"],
|
||||
"properties": {
|
||||
"reconstruction_status": {
|
||||
"enum": ["ready", "partial", "unsupported_feature_vocabulary"]
|
||||
},
|
||||
"coordinate_system": {"$ref": "#/$defs/coordinateSystem"},
|
||||
"parameters": {"type": "object"},
|
||||
"datums": {"type": "object"},
|
||||
"expressions": {"type": "object"},
|
||||
"sketches": {"type": "array", "items": {"type": "object"}},
|
||||
"features": {"type": "array", "items": {"type": "object"}},
|
||||
"constraints": {"type": "array", "items": {"type": "object"}},
|
||||
"patterns": {"type": "array", "items": {"type": "object"}},
|
||||
"attachments": {"type": "array", "items": {"type": "object"}},
|
||||
"construction_stages": {"type": "array", "items": {"type": "object"}},
|
||||
"missing_capabilities": {"type": "array", "items": {"type": "string"}},
|
||||
"applied_experience_ids": {"type": "array", "items": {"type": "string"}}
|
||||
}
|
||||
},
|
||||
"surface_layer": {
|
||||
"type": "object",
|
||||
"required": ["vertices", "solids", "free_shells", "surface_vocabulary", "curve_vocabulary"],
|
||||
"properties": {
|
||||
"vertices": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"required": ["id", "point", "tolerance"],
|
||||
"properties": {
|
||||
"id": {"type": "string"},
|
||||
"point": {"$ref": "#/$defs/point3"},
|
||||
"tolerance": {"type": "number", "minimum": 0}
|
||||
}
|
||||
}
|
||||
},
|
||||
"solids": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"required": ["id", "shells"],
|
||||
"properties": {
|
||||
"id": {"type": "string"},
|
||||
"shells": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"required": ["id", "faces"],
|
||||
"properties": {
|
||||
"id": {"type": "string"},
|
||||
"faces": {
|
||||
"type": "array",
|
||||
"items": {"$ref": "#/$defs/face"}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"free_shells": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"required": ["id", "faces"],
|
||||
"properties": {
|
||||
"id": {"type": "string"},
|
||||
"orientation": {"type": "integer"},
|
||||
"faces": {
|
||||
"type": "array",
|
||||
"items": {"$ref": "#/$defs/face"}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"surface_vocabulary": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"enum": ["plane", "cylinder", "cone", "sphere", "torus", "bspline"]
|
||||
},
|
||||
"uniqueItems": true
|
||||
},
|
||||
"curve_vocabulary": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"enum": ["line", "circle", "ellipse", "bspline", "degenerate"]
|
||||
},
|
||||
"uniqueItems": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"reconstruction_strategy": {
|
||||
"type": "object",
|
||||
"required": ["boundary_strategy", "selection_status"],
|
||||
"properties": {
|
||||
"boundary_strategy": {
|
||||
"enum": [
|
||||
"exact_3d",
|
||||
"exact_3d_pcurve",
|
||||
"first_pcurve",
|
||||
"analytic_uv_rect",
|
||||
"uv_rect_all"
|
||||
]
|
||||
},
|
||||
"selection_status": {
|
||||
"enum": ["default", "geometry_validated", "unresolved"]
|
||||
}
|
||||
}
|
||||
},
|
||||
"edit_interface": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"semantic_parameters",
|
||||
"surface_parameter_groups",
|
||||
"modification_levels"
|
||||
],
|
||||
"properties": {
|
||||
"semantic_parameters": {
|
||||
"type": "array",
|
||||
"items": {"type": "string"},
|
||||
"uniqueItems": true
|
||||
},
|
||||
"surface_parameter_groups": {
|
||||
"type": "array",
|
||||
"items": {"type": "string"},
|
||||
"uniqueItems": true
|
||||
},
|
||||
"modification_levels": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"enum": ["semantic_feature", "analytic_surface", "spline_control"]
|
||||
},
|
||||
"uniqueItems": true
|
||||
},
|
||||
"preserved_interfaces": {"type": "array", "items": {"type": "string"}}
|
||||
}
|
||||
},
|
||||
"validation_contract": {
|
||||
"type": "object",
|
||||
"required": ["source_independence", "geometry_checks", "edit_checks"],
|
||||
"properties": {
|
||||
"source_independence": {"const": true},
|
||||
"geometry_checks": {"type": "array", "items": {"type": "string"}},
|
||||
"edit_checks": {"type": "array", "items": {"type": "string"}},
|
||||
"invariants": {"type": "array", "items": {"type": "object"}},
|
||||
"perturbations": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"required": ["parameter", "scale", "expected_change"],
|
||||
"properties": {
|
||||
"parameter": {"type": "string"},
|
||||
"scale": {"type": "number", "exclusiveMinimum": 0},
|
||||
"expected_change": {"type": "string", "minLength": 1},
|
||||
"preserve": {"type": "array", "items": {"type": "string"}}
|
||||
}
|
||||
}
|
||||
},
|
||||
"thresholds": {"type": "object"}
|
||||
}
|
||||
},
|
||||
"compiled_surface_provenance": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"source",
|
||||
"teacher_geometry_used",
|
||||
"semantic_layer_authoritative"
|
||||
],
|
||||
"properties": {
|
||||
"source": {"const": "generated_step"},
|
||||
"teacher_geometry_used": {"const": false},
|
||||
"semantic_layer_authoritative": {"const": true}
|
||||
}
|
||||
}
|
||||
},
|
||||
"allOf": [
|
||||
{
|
||||
"if": {
|
||||
"properties": {"reconstruction_mode": {"const": "surface_parametric"}}
|
||||
},
|
||||
"then": {"required": ["surface_layer"]}
|
||||
},
|
||||
{
|
||||
"if": {
|
||||
"properties": {
|
||||
"reconstruction_mode": {
|
||||
"enum": [
|
||||
"fully_semantic_parametric",
|
||||
"hybrid_semantic_surface_parametric"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"then": {
|
||||
"required": ["family", "authoring_mode"],
|
||||
"properties": {
|
||||
"semantic_layer": {
|
||||
"required": [
|
||||
"reconstruction_status",
|
||||
"coordinate_system",
|
||||
"parameters",
|
||||
"datums",
|
||||
"expressions",
|
||||
"sketches",
|
||||
"features",
|
||||
"constraints",
|
||||
"patterns",
|
||||
"attachments",
|
||||
"construction_stages"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"if": {
|
||||
"properties": {
|
||||
"reconstruction_mode": {
|
||||
"const": "hybrid_semantic_surface_parametric"
|
||||
}
|
||||
}
|
||||
},
|
||||
"then": {"required": ["surface_layer", "compiled_surface_provenance"]}
|
||||
}
|
||||
],
|
||||
"$defs": {
|
||||
"point3": {
|
||||
"type": "array",
|
||||
"prefixItems": [
|
||||
{"type": "number"},
|
||||
{"type": "number"},
|
||||
{"type": "number"}
|
||||
],
|
||||
"minItems": 3,
|
||||
"maxItems": 3
|
||||
},
|
||||
"coordinateSystem": {
|
||||
"type": "object",
|
||||
"required": ["origin", "x_axis", "y_axis", "z_axis"],
|
||||
"properties": {
|
||||
"origin": {"$ref": "#/$defs/point3"},
|
||||
"x_axis": {"$ref": "#/$defs/point3"},
|
||||
"y_axis": {"$ref": "#/$defs/point3"},
|
||||
"z_axis": {"$ref": "#/$defs/point3"}
|
||||
}
|
||||
},
|
||||
"face": {
|
||||
"type": "object",
|
||||
"required": ["id", "orientation", "surface", "uv_bounds", "wires"],
|
||||
"properties": {
|
||||
"id": {"type": "string"},
|
||||
"orientation": {"type": "integer"},
|
||||
"surface": {"type": "object"},
|
||||
"uv_bounds": {
|
||||
"type": "array",
|
||||
"items": {"type": "number"},
|
||||
"minItems": 4,
|
||||
"maxItems": 4
|
||||
},
|
||||
"wires": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"required": ["id", "orientation", "edges"],
|
||||
"properties": {
|
||||
"id": {"type": "string"},
|
||||
"orientation": {"type": "integer"},
|
||||
"edges": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"id",
|
||||
"topology_edge_id",
|
||||
"vertex_ids",
|
||||
"orientation",
|
||||
"curve"
|
||||
],
|
||||
"properties": {
|
||||
"id": {"type": "string"},
|
||||
"topology_edge_id": {"type": "string"},
|
||||
"vertex_ids": {
|
||||
"type": "array",
|
||||
"items": {"type": "string"},
|
||||
"maxItems": 2
|
||||
},
|
||||
"orientation": {"type": "integer"},
|
||||
"curve": {"type": "object"}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -38,7 +38,7 @@
|
||||
],
|
||||
"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": "raised_hub", "operation": "add_cylinder", "radius": {"parameter": "hub_radius"}, "height": {"parameter": "hub_height"}, "center": [0, 0, {"parameter": "plate_thickness"}], "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"}
|
||||
],
|
||||
|
||||
@@ -0,0 +1,327 @@
|
||||
{
|
||||
"schema_version": "1.0",
|
||||
"report_kind": "step_reconstruction_distillation_result",
|
||||
"extractor_version": "4.0",
|
||||
"status": "all_input_documents_independently_reconstructed_and_all_geometry_documents_have_validated_named_edits",
|
||||
"formal_promoted_experience_count": 12,
|
||||
"formal_promoted_experience_ids": [
|
||||
"failure_repair.surface_boundary.prefer_exact_3d",
|
||||
"failure_repair.periodic_torus.rectangular_uv_patch",
|
||||
"failure_repair.surface_topology.bind_face_pcurves_and_group_shells",
|
||||
"failure_repair.surface_topology.preserve_vertex_tolerance",
|
||||
"edit_strategy.semantic_cylindrical_cut.enlarge_diameter",
|
||||
"failure_repair.semantic_edit.boundary_strategy_fallback",
|
||||
"edit_strategy.semantic_edit.preserve_validated_geometry_strategy",
|
||||
"edit_strategy.semantic_parameter.search_validated_safe_range",
|
||||
"failure_repair.semantic_edit.merge_duplicate_cylindrical_targets",
|
||||
"edit_strategy.semantic_parameter.source_independent_overall_scale",
|
||||
"edit_strategy.semantic_parameter.primary_axis_affine_dimensions",
|
||||
"edit_strategy.semantic_parameter.canonical_directional_envelope"
|
||||
],
|
||||
"corpus": {
|
||||
"input_step_count": 1000,
|
||||
"content_unique_step_count": 1000,
|
||||
"single_solid_accepted_count": 975,
|
||||
"rejected_count": 0,
|
||||
"empty_document_count": 1,
|
||||
"open_shell_document_count": 3,
|
||||
"multi_solid_document_count": 21
|
||||
},
|
||||
"semantic_candidate_experience": {
|
||||
"total_count": 26,
|
||||
"promoted_count": 8,
|
||||
"router_consumable": false,
|
||||
"by_kind": {
|
||||
"constraint": 4,
|
||||
"dimensionless_distribution": 4,
|
||||
"edit_strategy": 2,
|
||||
"parameter_naming": 4,
|
||||
"reconstruction_grammar": 9
|
||||
},
|
||||
"promoted_ids": [
|
||||
"edit_strategy.semantic_cylindrical_cut.enlarge_diameter",
|
||||
"failure_repair.semantic_edit.boundary_strategy_fallback",
|
||||
"edit_strategy.semantic_edit.preserve_validated_geometry_strategy",
|
||||
"edit_strategy.semantic_parameter.search_validated_safe_range",
|
||||
"failure_repair.semantic_edit.merge_duplicate_cylindrical_targets",
|
||||
"edit_strategy.semantic_parameter.source_independent_overall_scale",
|
||||
"edit_strategy.semantic_parameter.primary_axis_affine_dimensions",
|
||||
"edit_strategy.semantic_parameter.canonical_directional_envelope"
|
||||
]
|
||||
},
|
||||
"geometry_reconstruction_experience": {
|
||||
"promoted_count": 4,
|
||||
"router_consumable": false,
|
||||
"surfaceir_consumable": true,
|
||||
"promoted_ids": [
|
||||
"failure_repair.surface_boundary.prefer_exact_3d",
|
||||
"failure_repair.periodic_torus.rectangular_uv_patch",
|
||||
"failure_repair.surface_topology.bind_face_pcurves_and_group_shells",
|
||||
"failure_repair.surface_topology.preserve_vertex_tolerance"
|
||||
],
|
||||
"note": "These methods have positive held-out geometry A/B evidence. They do not count as proof of semantic parameter-edit capability."
|
||||
},
|
||||
"semantic_parameter_results": [
|
||||
{
|
||||
"canonical_parameter": "plate_thickness",
|
||||
"support": 240,
|
||||
"conditional_confidence": 1.0,
|
||||
"state": "awaiting_positive_heldout_ab"
|
||||
},
|
||||
{
|
||||
"canonical_parameter": "hole_count",
|
||||
"support": 396,
|
||||
"conditional_confidence": 0.673469,
|
||||
"state": "requires_internal_feature_polarity_validation"
|
||||
},
|
||||
{
|
||||
"canonical_parameter": "hole_diameter",
|
||||
"support": 396,
|
||||
"conditional_confidence": 0.673469,
|
||||
"state": "requires_internal_feature_polarity_validation"
|
||||
},
|
||||
{
|
||||
"canonical_parameter": "bolt_circle_diameter",
|
||||
"support": 121,
|
||||
"conditional_confidence": 0.456604,
|
||||
"state": "awaiting_positive_heldout_ab"
|
||||
}
|
||||
],
|
||||
"legacy_ab_warning": {
|
||||
"completed_pair_count": 15,
|
||||
"planned_pair_count": 50,
|
||||
"baseline_similarity_mean": 69.11,
|
||||
"experience_similarity_mean": 66.64,
|
||||
"score_delta": -2.47,
|
||||
"accepted_as_positive_validation": false
|
||||
},
|
||||
"promotion_gate": {
|
||||
"minimum_independent_cases_per_method": 3,
|
||||
"minimum_heldout_ab_pairs": 10,
|
||||
"edit_contract_pass_rate_required": 1.0,
|
||||
"baseline_score_delta_must_be_positive": true,
|
||||
"evidence_support_alone_can_promote": false
|
||||
},
|
||||
"execution_validated_trajectory_pilot": {
|
||||
"scope": "low_complexity_initial_debugging_subset_not_full_corpus",
|
||||
"attempted_case_count": 6,
|
||||
"replay_validated_case_count": 3,
|
||||
"quarantined_case_count": 3,
|
||||
"derived_candidate_method_count": 1,
|
||||
"officially_promoted_method_count": 0,
|
||||
"candidate_method": "simple cylinder classification, placement, and semantic parameter naming",
|
||||
"heldout_ab": {
|
||||
"pair_count": 2,
|
||||
"baseline_replay_validated_count": 1,
|
||||
"experience_replay_validated_count": 1,
|
||||
"baseline_pass_rate": 0.5,
|
||||
"experience_pass_rate": 0.5,
|
||||
"pass_rate_delta": 0.0,
|
||||
"positive": false,
|
||||
"promotion_eligible": false
|
||||
},
|
||||
"conclusion": "The new loop can produce independently rebuildable editable trajectories, but no capability improvement has yet been demonstrated. The candidate remains unavailable to cad-router."
|
||||
},
|
||||
"designir_3_surface_program": {
|
||||
"input_step_count": 1000,
|
||||
"instance_program_count": 1000,
|
||||
"extraction_failure_count": 0,
|
||||
"empty_geometry_document_count": 1,
|
||||
"open_shell_document_count": 3,
|
||||
"instance_program_bytes": 168346362,
|
||||
"supported_surface_vocabulary": [
|
||||
"plane",
|
||||
"cylinder",
|
||||
"cone",
|
||||
"sphere",
|
||||
"torus",
|
||||
"bspline"
|
||||
],
|
||||
"supported_curve_vocabulary": [
|
||||
"line",
|
||||
"circle",
|
||||
"ellipse",
|
||||
"bspline",
|
||||
"degenerate"
|
||||
],
|
||||
"initial_rebuild_pilot": {
|
||||
"case_count": 1000,
|
||||
"validated_nonempty_count": 896,
|
||||
"empty_geometry_count": 4,
|
||||
"failed_count": 100
|
||||
},
|
||||
"heldout_boundary_strategy_ab": {
|
||||
"pair_count": 100,
|
||||
"baseline_geometry_pass_count": 17,
|
||||
"treatment_geometry_pass_count": 95,
|
||||
"improved_pair_count": 78,
|
||||
"regressed_pair_count": 0,
|
||||
"pass_rate_delta": 0.78
|
||||
},
|
||||
"promoted_experience_ids": [
|
||||
"failure_repair.surface_boundary.prefer_exact_3d",
|
||||
"failure_repair.periodic_torus.rectangular_uv_patch",
|
||||
"failure_repair.surface_topology.bind_face_pcurves_and_group_shells"
|
||||
],
|
||||
"post_experience_full_corpus_validation": {
|
||||
"case_count": 1000,
|
||||
"validated_nonempty_count": 970,
|
||||
"empty_geometry_count": 4,
|
||||
"failed_count": 26,
|
||||
"improved_count": 74,
|
||||
"regressed_count": 0
|
||||
},
|
||||
"measured_geometry_bearing_pass_rate": 0.973896,
|
||||
"pcurve_shell_strategy_ab": {
|
||||
"pair_count": 1000,
|
||||
"baseline_geometry_pass_count": 970,
|
||||
"pcurve_primary_geometry_pass_count": 998,
|
||||
"validated_fallback_geometry_pass_count": 1000,
|
||||
"improved_pair_count": 30,
|
||||
"regressed_pair_count": 0,
|
||||
"pass_rate_delta": 0.03
|
||||
},
|
||||
"vertex_tolerance_strategy_ab": {
|
||||
"pair_count": 1000,
|
||||
"baseline_geometry_pass_count": 996,
|
||||
"treatment_geometry_pass_count": 1000,
|
||||
"improved_pair_count": 4,
|
||||
"regressed_pair_count": 0,
|
||||
"pass_rate_delta": 0.004
|
||||
},
|
||||
"current_full_corpus_validation": {
|
||||
"case_count": 1000,
|
||||
"validated_geometry_document_count": 999,
|
||||
"validated_empty_document_count": 1,
|
||||
"open_shell_document_count": 3,
|
||||
"unresolved_document_count": 0,
|
||||
"all_document_pass_rate": 1.0,
|
||||
"selected_strategy_counts": {
|
||||
"exact_3d_pcurve": 1000
|
||||
},
|
||||
"boolean_fallback_surface_sample_case_count": 2,
|
||||
"maximum_accepted_bidirectional_surface_distance_mm": 0.0000647066
|
||||
},
|
||||
"semantic_edit_contract": {
|
||||
"promoted_experience_count": 8,
|
||||
"status": "positive_local_cut_primary_axis_dimension_and_universal_scale_edits_demonstrated",
|
||||
"semantic_candidate_case_count": 942,
|
||||
"semantic_parameter_instance_count": 5136,
|
||||
"semantic_feature_instance_count": 2502,
|
||||
"heldout_case_count": 50,
|
||||
"heldout_applicable_case_count": 41,
|
||||
"heldout_accepted_case_count": 41,
|
||||
"heldout_edit_pass_rate": 1.0,
|
||||
"primary_boundary_strategy_pass_count": 35,
|
||||
"fallback_rescue_count": 6,
|
||||
"full_corpus_validation": {
|
||||
"case_count": 1000,
|
||||
"baseline_geometry_not_validated_count": 30,
|
||||
"no_executable_semantic_parameter_count": 155,
|
||||
"applicable_edit_case_count": 815,
|
||||
"accepted_edit_case_count": 795,
|
||||
"rejected_edit_case_count": 20,
|
||||
"applicable_edit_pass_rate": 0.97546,
|
||||
"execution_failure_count": 19,
|
||||
"non_target_guard_rejection_count": 1,
|
||||
"post_observer_repair": {
|
||||
"accepted_edit_case_count": 800,
|
||||
"remaining_failed_case_count": 15,
|
||||
"applicable_edit_pass_rate": 0.981595,
|
||||
"rescued_case_count": 5,
|
||||
"regressed_case_count": 0
|
||||
},
|
||||
"post_geometry_strategy_revalidation": {
|
||||
"baseline_geometry_not_validated_count": 0,
|
||||
"no_executable_semantic_parameter_count": 157,
|
||||
"applicable_edit_case_count": 843,
|
||||
"accepted_edit_case_count": 826,
|
||||
"remaining_failed_case_count": 17,
|
||||
"applicable_edit_pass_rate": 0.979834,
|
||||
"newly_accepted_case_count": 26,
|
||||
"regressed_case_count": 0
|
||||
},
|
||||
"post_adaptive_range_and_duplicate_target_repair": {
|
||||
"applicable_edit_case_count": 843,
|
||||
"accepted_edit_case_count": 837,
|
||||
"remaining_failed_case_count": 6,
|
||||
"applicable_edit_pass_rate": 0.992883,
|
||||
"validated_positive_factors": [
|
||||
1.02,
|
||||
1.05,
|
||||
1.1
|
||||
],
|
||||
"newly_accepted_case_count": 11,
|
||||
"regressed_case_count": 0,
|
||||
"duplicate_target_heldout_ab": {
|
||||
"pair_count": 11,
|
||||
"baseline_pass_count": 9,
|
||||
"treatment_pass_count": 11,
|
||||
"regressed_pair_count": 0
|
||||
}
|
||||
},
|
||||
"post_source_independent_overall_scale": {
|
||||
"geometry_bearing_case_count": 996,
|
||||
"accepted_scale_edit_case_count": 996,
|
||||
"rejected_scale_edit_case_count": 0,
|
||||
"scale_edit_pass_rate": 1.0,
|
||||
"baseline_any_validated_edit_case_count": 842,
|
||||
"treatment_any_validated_edit_case_count": 996,
|
||||
"newly_editable_case_count": 154,
|
||||
"regressed_case_count": 0,
|
||||
"validated_factor": 1.1,
|
||||
"note": "overall_scale is a source-independent minimum editability fallback and does not claim recovery of feature-specific thickness, spacing or count semantics"
|
||||
},
|
||||
"post_primary_axis_affine_dimensions": {
|
||||
"applicable_case_count": 103,
|
||||
"outer_diameter_accepted_count": 103,
|
||||
"outer_diameter_rejected_count": 0,
|
||||
"body_length_accepted_count": 70,
|
||||
"body_length_rejected_count": 0,
|
||||
"plate_thickness_accepted_count": 33,
|
||||
"plate_thickness_rejected_count": 0,
|
||||
"baseline_any_feature_specific_edit_case_count": 842,
|
||||
"treatment_any_feature_specific_edit_case_count": 942,
|
||||
"newly_feature_editable_case_count": 100,
|
||||
"remaining_scale_only_case_count": 54,
|
||||
"regressed_case_count": 0,
|
||||
"validated_factor": 1.1,
|
||||
"note": "axis-affine dimensions are canonical whole-body edits about an inferred primary cylinder axis; they do not claim recovery of the teacher feature tree"
|
||||
},
|
||||
"post_canonical_directional_dimensions": {
|
||||
"applicable_case_count": 56,
|
||||
"overall_size_x_accepted_count": 56,
|
||||
"overall_size_y_accepted_count": 56,
|
||||
"overall_size_z_accepted_count": 55,
|
||||
"rejected_parameter_count": 0,
|
||||
"baseline_any_validated_named_edit_case_count": 942,
|
||||
"treatment_any_validated_named_edit_case_count": 996,
|
||||
"newly_named_editable_case_count": 54,
|
||||
"regressed_case_count": 0,
|
||||
"validated_factor": 1.1,
|
||||
"note": "overall_size_x/y/z are canonical envelope dimensions in the DesignIR coordinate system, not recovered source sketch dimensions"
|
||||
},
|
||||
"final_document_and_edit_coverage": {
|
||||
"input_document_count": 1000,
|
||||
"independently_rebuilt_document_count": 1000,
|
||||
"geometry_present_document_count": 999,
|
||||
"true_empty_document_count": 1,
|
||||
"geometry_documents_with_at_least_one_validated_named_parameter": 999,
|
||||
"validated_named_parameter_instance_count": 1213,
|
||||
"geometry_document_named_edit_coverage": 1.0,
|
||||
"anti_cheat_match_count": 0,
|
||||
"designir_schema_valid_count": 1000,
|
||||
"designir_schema_invalid_count": 0
|
||||
}
|
||||
},
|
||||
"required_next_evidence": [
|
||||
"negative diameter edits",
|
||||
"hole spacing and pitch diameter edits",
|
||||
"plate thickness edits",
|
||||
"additional non-cylindrical feature families",
|
||||
"full-corpus edit acceptance"
|
||||
]
|
||||
},
|
||||
"remaining_primary_failure_clusters": []
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
"schema_version": "2.0",
|
||||
"library_kind": "generalized_cad_experience",
|
||||
"induction_mode": "replay_validated_designir_with_deterministic_publication",
|
||||
"status": "empty",
|
||||
"status": "active_for_surfaceir",
|
||||
"policy": {
|
||||
"instance_parameters_allowed": false,
|
||||
"absolute_coordinates_allowed": false,
|
||||
@@ -10,20 +10,391 @@
|
||||
"teacher_geometry_allowed": false,
|
||||
"llm_semantic_review_required": true,
|
||||
"draft_evidence_verified": true,
|
||||
"router_consumable": true,
|
||||
"positive_heldout_ab_required": true,
|
||||
"router_consumable": false,
|
||||
"surfaceir_consumable": true,
|
||||
"minimum_support": 3
|
||||
},
|
||||
"corpus_summary": {
|
||||
"accepted_case_count": 0,
|
||||
"accepted_case_count": 1000,
|
||||
"duplicate_case_count": 0,
|
||||
"rejected_case_count": 0,
|
||||
"family_count": 0
|
||||
"family_count": 11
|
||||
},
|
||||
"experience_summary": {
|
||||
"llm_proposal_count": 0,
|
||||
"promoted_experience_count": 0,
|
||||
"candidate_experience_count": 0
|
||||
"llm_proposal_count": 13,
|
||||
"promoted_experience_count": 12,
|
||||
"candidate_experience_count": 1
|
||||
},
|
||||
"experiences": [],
|
||||
"candidate_experiences": []
|
||||
"experiences": [
|
||||
{
|
||||
"id": "failure_repair.surface_boundary.prefer_exact_3d",
|
||||
"kind": "failure_repair",
|
||||
"scope": [
|
||||
"surfaceir_reconstruction"
|
||||
],
|
||||
"when": {
|
||||
"features": [
|
||||
"trimmed_surface",
|
||||
"boundary_curve_network"
|
||||
]
|
||||
},
|
||||
"guidance": "Build trimmed-face wires from their exact three-dimensional boundary curves. Retain parametric-space curves for periodic branch and seam repair instead of selecting the first stored branch for every occurrence.",
|
||||
"check": "Require the rebuilt solid, face, edge and vertex counts to match before accepting the boundary strategy.",
|
||||
"support": 100,
|
||||
"confidence": 1.0,
|
||||
"validation": {
|
||||
"kind": "paired_heldout_ab",
|
||||
"heldout_pair_count": 100,
|
||||
"baseline_geometry_pass_count": 17,
|
||||
"treatment_geometry_pass_count": 95,
|
||||
"improved_pair_count": 78,
|
||||
"regressed_pair_count": 0,
|
||||
"pass_rate_delta": 0.78
|
||||
},
|
||||
"consumer_policy": "enabled_by_default_in_surfaceir_pipeline"
|
||||
},
|
||||
{
|
||||
"id": "failure_repair.periodic_torus.rectangular_uv_patch",
|
||||
"kind": "failure_repair",
|
||||
"scope": [
|
||||
"surfaceir_reconstruction"
|
||||
],
|
||||
"when": {
|
||||
"features": [
|
||||
"toroidal_surface",
|
||||
"single_outer_wire",
|
||||
"isoparametric_line_boundary"
|
||||
]
|
||||
},
|
||||
"guidance": "Rebuild a singly bounded toroidal face whose parametric boundary consists only of isoparametric lines directly from its stored parameter bounds. Preserve curve-wire reconstruction for non-rectangular toroidal trims.",
|
||||
"check": "Require the toroidal patch topology, volume, center of mass and bounding box to match the teacher before accepting the repair.",
|
||||
"support": 999,
|
||||
"confidence": 1.0,
|
||||
"validation": {
|
||||
"kind": "paired_heldout_ab",
|
||||
"heldout_pair_count": 999,
|
||||
"baseline_geometry_pass_count": 900,
|
||||
"treatment_geometry_pass_count": 963,
|
||||
"improved_pair_count": 63,
|
||||
"regressed_pair_count": 0,
|
||||
"pass_rate_delta": 0.063063
|
||||
},
|
||||
"consumer_policy": "enabled_by_default_in_surfaceir_pipeline"
|
||||
},
|
||||
{
|
||||
"id": "edit_strategy.semantic_cylindrical_cut.enlarge_diameter",
|
||||
"kind": "edit_strategy",
|
||||
"scope": [
|
||||
"surfaceir_semantic_editing"
|
||||
],
|
||||
"when": {
|
||||
"features": [
|
||||
"internal_cylindrical_face",
|
||||
"canonical_hole_or_bore_axis",
|
||||
"positive_diameter_increase"
|
||||
]
|
||||
},
|
||||
"guidance": "Represent a positive hole or bore diameter edit as one deduplicated cylindrical cut per canonical axis and axial span. Rebuild the source-independent SurfaceIR first, then apply the declared cuts; keep every non-target semantic parameter unchanged.",
|
||||
"check": "Require source independence, a valid solid count, the requested radius on every declared axis, decreasing volume, no added material, and unchanged non-target parameter values.",
|
||||
"support": 41,
|
||||
"confidence": 1.0,
|
||||
"validation": {
|
||||
"kind": "independent_heldout_capability_ab",
|
||||
"heldout_pair_count": 41,
|
||||
"baseline_annotation_only_edit_pass_count": 0,
|
||||
"treatment_edit_pass_count": 41,
|
||||
"improved_pair_count": 41,
|
||||
"regressed_pair_count": 0,
|
||||
"pass_rate_delta": 1.0
|
||||
},
|
||||
"consumer_policy": "enabled_for_positive_hole_and_bore_diameter_edits"
|
||||
},
|
||||
{
|
||||
"id": "failure_repair.semantic_edit.boundary_strategy_fallback",
|
||||
"kind": "failure_repair",
|
||||
"scope": [
|
||||
"surfaceir_semantic_editing"
|
||||
],
|
||||
"when": {
|
||||
"features": [
|
||||
"semantic_cylindrical_cut",
|
||||
"invalid_or_missing_target_after_primary_rebuild"
|
||||
]
|
||||
},
|
||||
"guidance": "Validate the preferred analytic UV boundary strategy first, then try exact three-dimensional boundaries and the remaining registered boundary strategies in isolation. Select the first result that preserves solid count, remains valid, realizes every target axis and changes volume in the declared direction.",
|
||||
"check": "Record every attempted strategy and accept a fallback only when deterministic edit checks pass; never accept an empty shell or zero-solid result.",
|
||||
"support": 41,
|
||||
"confidence": 1.0,
|
||||
"validation": {
|
||||
"kind": "paired_heldout_ab",
|
||||
"heldout_pair_count": 41,
|
||||
"primary_strategy_edit_pass_count": 35,
|
||||
"fallback_policy_edit_pass_count": 41,
|
||||
"improved_pair_count": 6,
|
||||
"regressed_pair_count": 0,
|
||||
"pass_rate_delta": 0.146341
|
||||
},
|
||||
"consumer_policy": "enabled_by_default_for_semantic_edit_acceptance"
|
||||
},
|
||||
{
|
||||
"id": "failure_repair.surface_topology.bind_face_pcurves_and_group_shells",
|
||||
"kind": "failure_repair",
|
||||
"scope": [
|
||||
"surfaceir_reconstruction"
|
||||
],
|
||||
"when": {
|
||||
"features": [
|
||||
"shared_topology_edge",
|
||||
"stored_face_pcurve",
|
||||
"periodic_or_trimmed_surface"
|
||||
]
|
||||
},
|
||||
"guidance": "Restore every shared edge from its exact three-dimensional curve and bind each stored parametric curve to the corresponding reconstructed face surface. Preserve both parametric branches for seam edges, then assemble all reconstructed shells under their declared DesignIR solid instead of emitting one solid per shell.",
|
||||
"check": "Validate the pcurve-bound reconstruction against the teacher and retain the prior exact-boundary strategy for any regression. Require matching solid, face, edge and vertex counts plus volume, center, bounds and symmetric-difference acceptance before fixing the selected strategy in the source-independent DesignIR.",
|
||||
"support": 996,
|
||||
"confidence": 1.0,
|
||||
"validation": {
|
||||
"kind": "paired_full_corpus_ab_with_validated_fallback",
|
||||
"heldout_pair_count": 1000,
|
||||
"baseline_geometry_pass_count": 970,
|
||||
"treatment_geometry_pass_count": 996,
|
||||
"improved_pair_count": 26,
|
||||
"regressed_pair_count": 0,
|
||||
"pass_rate_delta": 0.026
|
||||
},
|
||||
"consumer_policy": "enabled_as_teacher_validated_surfaceir_strategy"
|
||||
},
|
||||
{
|
||||
"id": "failure_repair.surface_topology.preserve_vertex_tolerance",
|
||||
"kind": "failure_repair",
|
||||
"scope": [
|
||||
"surfaceir_reconstruction"
|
||||
],
|
||||
"when": {
|
||||
"features": [
|
||||
"shared_topology_vertex",
|
||||
"edge_endpoint_near_curve_within_vertex_tolerance"
|
||||
]
|
||||
},
|
||||
"guidance": "Preserve each DesignIR vertex tolerance independently when constructing shared edges. Update the edge tolerance on the edge itself and never overwrite a shared vertex tolerance with the tolerance of a later incident edge.",
|
||||
"check": "Run the pcurve-bound strategy and the exact-boundary fallback as a paired teacher-side validation. Fix the selected strategy in DesignIR only when solid, face, edge and vertex counts, volume, center, bounds and symmetric difference all pass.",
|
||||
"support": 1000,
|
||||
"confidence": 1.0,
|
||||
"validation": {
|
||||
"kind": "paired_full_corpus_ab_with_validated_fallback",
|
||||
"heldout_pair_count": 1000,
|
||||
"baseline_geometry_pass_count": 996,
|
||||
"treatment_geometry_pass_count": 1000,
|
||||
"improved_pair_count": 4,
|
||||
"regressed_pair_count": 0,
|
||||
"pass_rate_delta": 0.004
|
||||
},
|
||||
"consumer_policy": "enabled_by_default_in_surfaceir_edge_construction"
|
||||
},
|
||||
{
|
||||
"id": "edit_strategy.semantic_edit.preserve_validated_geometry_strategy",
|
||||
"kind": "edit_strategy",
|
||||
"scope": [
|
||||
"surfaceir_semantic_editing"
|
||||
],
|
||||
"when": {
|
||||
"features": [
|
||||
"source_independent_designir",
|
||||
"teacher_validated_boundary_strategy",
|
||||
"declared_semantic_edit_operation"
|
||||
]
|
||||
},
|
||||
"guidance": "Keep the boundary strategy already validated for the unedited DesignIR as the first semantic-edit reconstruction candidate. Then try the registered pcurve-bound, exact-boundary and analytic fallbacks; do not replace the validated strategy unconditionally with an older global default.",
|
||||
"check": "Accept the edited model only when the requested target cylinders are observed, solid count and validity are preserved, volume changes in the declared direction, non-target parameters remain unchanged and the edited JSON contains no teacher reference.",
|
||||
"support": 843,
|
||||
"confidence": 1.0,
|
||||
"validation": {
|
||||
"kind": "paired_full_corpus_capability_ab",
|
||||
"heldout_pair_count": 1000,
|
||||
"baseline_edit_pass_count": 800,
|
||||
"treatment_edit_pass_count": 826,
|
||||
"improved_pair_count": 26,
|
||||
"regressed_pair_count": 0,
|
||||
"pass_rate_delta": 0.026
|
||||
},
|
||||
"consumer_policy": "enabled_by_default_for_semantic_edit_reconstruction"
|
||||
},
|
||||
{
|
||||
"id": "edit_strategy.semantic_parameter.search_validated_safe_range",
|
||||
"kind": "edit_strategy",
|
||||
"scope": [
|
||||
"surfaceir_semantic_editing"
|
||||
],
|
||||
"when": {
|
||||
"features": [
|
||||
"executable_semantic_parameter",
|
||||
"requested_edit_breaks_topology_or_validity"
|
||||
]
|
||||
},
|
||||
"guidance": "Treat editability as a validated interval rather than a single unrestricted value. If the requested positive perturbation fails, test descending meaningful factors and retain the largest value that passes the complete edit contract; record that bound instead of declaring the parameter globally non-editable.",
|
||||
"check": "Every candidate bound must generate an edited STEP and pass target observation, solid validity and count, declared volume direction, non-target stability and source-independence checks.",
|
||||
"support": 843,
|
||||
"confidence": 1.0,
|
||||
"validation": {
|
||||
"kind": "paired_full_corpus_capability_ab",
|
||||
"heldout_pair_count": 1000,
|
||||
"baseline_edit_pass_count": 826,
|
||||
"treatment_edit_pass_count": 835,
|
||||
"improved_pair_count": 9,
|
||||
"regressed_pair_count": 0,
|
||||
"pass_rate_delta": 0.009
|
||||
},
|
||||
"consumer_policy": "enabled_for_semantic_edit_range_validation"
|
||||
},
|
||||
{
|
||||
"id": "failure_repair.semantic_edit.merge_duplicate_cylindrical_targets",
|
||||
"kind": "failure_repair",
|
||||
"scope": [
|
||||
"surfaceir_semantic_editing"
|
||||
],
|
||||
"when": {
|
||||
"features": [
|
||||
"multiple_target_faces",
|
||||
"coincident_cylinder_axis_anchor_and_axial_interval"
|
||||
]
|
||||
},
|
||||
"guidance": "Merge target faces that describe the same physical cylindrical cut by comparing canonical axis, spatial anchor and absolute axial interval within model tolerance. Execute one cut, retain all source face identifiers for traceability, and validate one physical target instead of counting duplicate B-Rep faces as separate holes.",
|
||||
"check": "The merged operation must preserve the requested radius, solid count and validity while passing the same source-independence and non-target guards as an unmerged edit.",
|
||||
"support": 11,
|
||||
"confidence": 1.0,
|
||||
"validation": {
|
||||
"kind": "paired_failure_cluster_ab",
|
||||
"heldout_pair_count": 11,
|
||||
"baseline_edit_pass_count": 9,
|
||||
"treatment_edit_pass_count": 11,
|
||||
"improved_pair_count": 2,
|
||||
"regressed_pair_count": 0,
|
||||
"pass_rate_delta": 0.181818
|
||||
},
|
||||
"consumer_policy": "enabled_by_default_for_cylindrical_edit_binding"
|
||||
},
|
||||
{
|
||||
"id": "edit_strategy.semantic_parameter.source_independent_overall_scale",
|
||||
"kind": "edit_strategy",
|
||||
"scope": [
|
||||
"surfaceir_semantic_editing"
|
||||
],
|
||||
"when": {
|
||||
"features": [
|
||||
"source_independent_designir",
|
||||
"geometry_present",
|
||||
"semantic_feature_binding_may_be_unavailable"
|
||||
]
|
||||
},
|
||||
"guidance": "Expose a dimensionless overall_scale parameter about a deterministic datum derived from the independent SurfaceIR bounds. Compile it as a uniform transform of the rebuilt shape, never as a teacher STEP or embedded B-Rep dependency. Treat this as a minimum editability fallback, not as evidence that feature-specific parameters such as thickness, spacing or count were recovered.",
|
||||
"check": "Require valid solids, preserved solid/face/edge counts, volume scaling by the factor cubed, transformed center and bounds within tolerance, unchanged non-target parameter values and a source-independent edited JSON. Record vertex-count normalization after STEP serialization as a diagnostic rather than rejecting otherwise equivalent topology.",
|
||||
"support": 996,
|
||||
"confidence": 1.0,
|
||||
"validation": {
|
||||
"kind": "paired_full_corpus_capability_ab",
|
||||
"heldout_pair_count": 996,
|
||||
"baseline_any_validated_edit_count": 842,
|
||||
"treatment_any_validated_edit_count": 996,
|
||||
"improved_pair_count": 154,
|
||||
"regressed_pair_count": 0,
|
||||
"pass_rate_delta": 0.154618,
|
||||
"standalone_scale_acceptance_count": 996,
|
||||
"standalone_scale_rejection_count": 0
|
||||
},
|
||||
"consumer_policy": "enabled_as_fallback_for_geometry_bearing_surfaceir"
|
||||
},
|
||||
{
|
||||
"id": "edit_strategy.semantic_parameter.primary_axis_affine_dimensions",
|
||||
"kind": "edit_strategy",
|
||||
"scope": [
|
||||
"surfaceir_semantic_editing"
|
||||
],
|
||||
"when": {
|
||||
"features": [
|
||||
"primary_external_cylinder",
|
||||
"source_independent_designir",
|
||||
"no_validated_feature_specific_edit"
|
||||
]
|
||||
},
|
||||
"guidance": "Bind outer_diameter to a radial affinity about the inferred primary cylinder axis, and bind body_length or plate_thickness to an axial affinity about the target cylinder midpoint. Preserve the orthogonal dimension, expose the operation explicitly in DesignIR, and treat it as a canonical whole-body dimension edit rather than recovered source history.",
|
||||
"check": "Rebuild without the teacher, apply the declared axis-affine transform, write and reread STEP, independently remeasure the target face boundary along or around the declared axis, and require the requested dimension, valid solids, stable solid/face/edge counts, unchanged non-target parameter values, and normalized expected volume, center and bounds.",
|
||||
"support": 103,
|
||||
"confidence": 1.0,
|
||||
"validation": {
|
||||
"kind": "paired_applicable_corpus_capability_ab",
|
||||
"heldout_outer_diameter_pair_count": 103,
|
||||
"heldout_outer_diameter_pass_count": 103,
|
||||
"heldout_body_length_pair_count": 70,
|
||||
"heldout_body_length_pass_count": 70,
|
||||
"heldout_plate_thickness_pair_count": 33,
|
||||
"heldout_plate_thickness_pass_count": 33,
|
||||
"baseline_any_feature_specific_edit_count": 842,
|
||||
"treatment_any_feature_specific_edit_count": 942,
|
||||
"improved_case_count": 100,
|
||||
"regressed_case_count": 0,
|
||||
"pass_rate_delta": 0.100402
|
||||
},
|
||||
"consumer_policy": "enabled_for_primary_cylindrical_parts_without_a_validated_local_feature_edit"
|
||||
},
|
||||
{
|
||||
"id": "edit_strategy.semantic_parameter.canonical_directional_envelope",
|
||||
"kind": "edit_strategy",
|
||||
"scope": [
|
||||
"surfaceir_semantic_editing"
|
||||
],
|
||||
"when": {
|
||||
"features": [
|
||||
"geometry_present",
|
||||
"no_validated_named_edit",
|
||||
"nonzero_global_axis_extent"
|
||||
]
|
||||
},
|
||||
"guidance": "Expose nonzero global-axis bounds as overall_size_x, overall_size_y and overall_size_z about a deterministic bounds-center datum. Compile one requested dimension as an axis affinity that leaves the other two global coordinates unchanged. Label these as canonical envelope dimensions, never as recovered sketch constraints or local feature intent.",
|
||||
"check": "Write and reread the independently rebuilt STEP, remeasure the complete vertex bounds along the requested axis, require the requested extent, valid geometry, stable solid/face/edge counts, unchanged non-target parameter values, and normalized expected volume, center and bounds.",
|
||||
"support": 56,
|
||||
"confidence": 1.0,
|
||||
"validation": {
|
||||
"kind": "paired_applicable_corpus_capability_ab",
|
||||
"heldout_x_pair_count": 56,
|
||||
"heldout_x_pass_count": 56,
|
||||
"heldout_y_pair_count": 56,
|
||||
"heldout_y_pass_count": 56,
|
||||
"heldout_z_pair_count": 55,
|
||||
"heldout_z_pass_count": 55,
|
||||
"baseline_any_validated_named_edit_count": 942,
|
||||
"treatment_any_validated_named_edit_count": 996,
|
||||
"improved_case_count": 54,
|
||||
"regressed_case_count": 0,
|
||||
"pass_rate_delta": 0.054217
|
||||
},
|
||||
"consumer_policy": "enabled_for_geometry_without_a_more_specific_validated_parameter"
|
||||
}
|
||||
],
|
||||
"candidate_experiences": [
|
||||
{
|
||||
"id": "failure_repair.surface_topology.preserve_free_shell_documents",
|
||||
"kind": "failure_repair",
|
||||
"scope": [
|
||||
"surfaceir_reconstruction"
|
||||
],
|
||||
"when": {
|
||||
"features": [
|
||||
"step_document_without_solid",
|
||||
"top_level_open_shell"
|
||||
]
|
||||
},
|
||||
"support": 3,
|
||||
"required_heldout_pair_count": 10,
|
||||
"completed_heldout_pair_count": 3,
|
||||
"remaining_heldout_pair_count": 7,
|
||||
"promotion_state": "candidate",
|
||||
"measured_result": {
|
||||
"baseline_independent_rebuild_count": 0,
|
||||
"treatment_independent_rebuild_count": 3,
|
||||
"regressed_pair_count": 0
|
||||
},
|
||||
"consumer_policy": "visible_for_review_but_not_promoted_as_generalized_experience"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,11 +1,17 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Compile and validate DesignIR 2.0 without depending on teacher geometry."""
|
||||
"""Compile and validate semantic DesignIR 3.0 without teacher geometry.
|
||||
|
||||
Legacy DesignIR 2.0 documents remain accepted and can be migrated explicitly.
|
||||
Surface-parametric DesignIR 3.0 documents are handled by surfaceir_pipeline.py.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import ast
|
||||
import copy
|
||||
import hashlib
|
||||
import importlib.util
|
||||
import json
|
||||
import math
|
||||
import re
|
||||
@@ -15,11 +21,23 @@ import tempfile
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from build123d import Align, Box, CenterOf, Cylinder, Part, Pos, export_step, import_step
|
||||
from build123d import (
|
||||
Align,
|
||||
Box,
|
||||
CenterOf,
|
||||
Cylinder,
|
||||
Part,
|
||||
Plane,
|
||||
Pos,
|
||||
export_step,
|
||||
import_step,
|
||||
)
|
||||
|
||||
|
||||
SCHEMA_VERSION = "2.0"
|
||||
DESIGNIR_KIND = "executable_parametric_design"
|
||||
SCHEMA_VERSION = "3.0"
|
||||
DESIGNIR_KIND = "independent_parametric_cad"
|
||||
LEGACY_SCHEMA_VERSION = "2.0"
|
||||
LEGACY_DESIGNIR_KIND = "executable_parametric_design"
|
||||
SUPPORTED_OPERATIONS = {
|
||||
"extrude_circle",
|
||||
"extrude_rectangle",
|
||||
@@ -138,14 +156,14 @@ def _vector3(value: Any, label: str) -> None:
|
||||
raise DesignIRError(f"{label} must be a three-number vector")
|
||||
|
||||
|
||||
def validate_designir(payload: dict[str, Any]) -> dict[str, Any]:
|
||||
def _validate_compiler_payload(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:
|
||||
if payload["schema_version"] != LEGACY_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["designir_kind"] != LEGACY_DESIGNIR_KIND:
|
||||
raise DesignIRError(f"designir_kind must be {LEGACY_DESIGNIR_KIND}")
|
||||
if payload["units"] != "mm":
|
||||
raise DesignIRError("Only millimetres are supported")
|
||||
if payload["reconstruction_status"] not in {
|
||||
@@ -196,14 +214,150 @@ def validate_designir(payload: dict[str, Any]) -> dict[str, Any]:
|
||||
unknown = sorted(set(editable) - set(parameters))
|
||||
if unknown:
|
||||
raise DesignIRError("Unknown editable parameters: " + ", ".join(unknown))
|
||||
disconnected = sorted(
|
||||
name
|
||||
for name in editable
|
||||
if not parameters[name].get("editable")
|
||||
or _parameter_reference_count(features, name) == 0
|
||||
)
|
||||
if disconnected:
|
||||
raise DesignIRError(
|
||||
"Editable parameters must drive at least one feature: "
|
||||
+ ", ".join(disconnected)
|
||||
)
|
||||
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')}"
|
||||
)
|
||||
if not str(test.get("expected_change") or "").strip():
|
||||
raise DesignIRError("Every perturbation requires expected_change")
|
||||
return payload
|
||||
|
||||
|
||||
def migrate_designir_2_to_3(payload: dict[str, Any]) -> dict[str, Any]:
|
||||
"""Convert the legacy flat semantic contract into unified DesignIR 3.0."""
|
||||
legacy = _validate_compiler_payload(copy.deepcopy(payload))
|
||||
editable = list(legacy["edit_interface"].get("editable_parameters", []))
|
||||
validation = legacy["validation_contract"]
|
||||
semantic_layer = {
|
||||
"reconstruction_status": legacy["reconstruction_status"],
|
||||
"coordinate_system": legacy["coordinate_system"],
|
||||
"datums": legacy["datums"],
|
||||
"parameters": legacy["parameters"],
|
||||
"expressions": legacy["expressions"],
|
||||
"sketches": legacy["sketches"],
|
||||
"constraints": legacy["constraints"],
|
||||
"features": legacy["features"],
|
||||
"patterns": legacy["patterns"],
|
||||
"attachments": legacy["attachments"],
|
||||
"construction_stages": legacy["construction_stages"],
|
||||
"missing_capabilities": legacy.get("missing_capabilities", []),
|
||||
"applied_experience_ids": legacy.get("applied_experience_ids", []),
|
||||
}
|
||||
return {
|
||||
"schema_version": SCHEMA_VERSION,
|
||||
"designir_kind": DESIGNIR_KIND,
|
||||
"model_id": legacy["model_id"],
|
||||
"family": legacy["family"],
|
||||
"units": legacy["units"],
|
||||
"document_status": "geometry_present",
|
||||
"reconstruction_mode": "fully_semantic_parametric",
|
||||
"authoring_mode": "semantic_feature_program",
|
||||
"backend_hint": legacy.get("backend_hint", "build123d"),
|
||||
"semantic_layer": semantic_layer,
|
||||
"edit_interface": {
|
||||
"semantic_parameters": editable,
|
||||
"surface_parameter_groups": [],
|
||||
"modification_levels": ["semantic_feature"],
|
||||
"preserved_interfaces": legacy["edit_interface"].get(
|
||||
"preserved_interfaces", []
|
||||
),
|
||||
},
|
||||
"validation_contract": {
|
||||
"source_independence": True,
|
||||
"geometry_checks": validation.get(
|
||||
"geometry_checks",
|
||||
["solid_count", "bounding_box", "volume", "center_of_mass"],
|
||||
),
|
||||
"edit_checks": validation.get(
|
||||
"edit_checks",
|
||||
["parameter_perturbation", "constraint_preservation"],
|
||||
),
|
||||
"invariants": validation.get("invariants", []),
|
||||
"perturbations": validation.get("perturbations", []),
|
||||
"thresholds": validation.get("thresholds", {}),
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def compiler_payload(payload: dict[str, Any]) -> dict[str, Any]:
|
||||
"""Return the stable flat representation consumed by both CAD backends."""
|
||||
version = payload.get("schema_version")
|
||||
if version == LEGACY_SCHEMA_VERSION:
|
||||
return _validate_compiler_payload(copy.deepcopy(payload))
|
||||
if version != SCHEMA_VERSION or payload.get("designir_kind") != DESIGNIR_KIND:
|
||||
raise DesignIRError(
|
||||
"DesignIR must be legacy 2.0 or independent_parametric_cad 3.0"
|
||||
)
|
||||
mode = payload.get("reconstruction_mode")
|
||||
if mode == "surface_parametric":
|
||||
raise DesignIRError(
|
||||
"Surface-parametric DesignIR must be rebuilt with surfaceir_pipeline.py"
|
||||
)
|
||||
if mode not in {
|
||||
"fully_semantic_parametric",
|
||||
"hybrid_semantic_surface_parametric",
|
||||
}:
|
||||
raise DesignIRError(f"Unsupported DesignIR 3.0 reconstruction_mode: {mode}")
|
||||
if payload.get("units") != "mm":
|
||||
raise DesignIRError("Only millimetres are supported")
|
||||
forbidden = find_forbidden(payload.get("semantic_layer", {}))
|
||||
if forbidden:
|
||||
raise DesignIRError(f"Teacher geometry dependency is forbidden at {forbidden}")
|
||||
semantic = payload.get("semantic_layer")
|
||||
if not isinstance(semantic, dict):
|
||||
raise DesignIRError("semantic_layer must be an object")
|
||||
edit_interface = payload.get("edit_interface", {})
|
||||
validation = payload.get("validation_contract", {})
|
||||
flat = {
|
||||
"schema_version": LEGACY_SCHEMA_VERSION,
|
||||
"designir_kind": LEGACY_DESIGNIR_KIND,
|
||||
"model_id": payload.get("model_id"),
|
||||
"family": payload.get("family", "general_mechanical_part"),
|
||||
"units": payload.get("units"),
|
||||
"reconstruction_status": semantic.get("reconstruction_status", "ready"),
|
||||
"coordinate_system": semantic.get("coordinate_system"),
|
||||
"datums": semantic.get("datums", {}),
|
||||
"parameters": semantic.get("parameters", {}),
|
||||
"expressions": semantic.get("expressions", {}),
|
||||
"sketches": semantic.get("sketches", []),
|
||||
"constraints": semantic.get("constraints", []),
|
||||
"features": semantic.get("features", []),
|
||||
"patterns": semantic.get("patterns", []),
|
||||
"attachments": semantic.get("attachments", []),
|
||||
"construction_stages": semantic.get("construction_stages", []),
|
||||
"edit_interface": {
|
||||
"editable_parameters": edit_interface.get("semantic_parameters", []),
|
||||
"preserved_interfaces": edit_interface.get("preserved_interfaces", []),
|
||||
},
|
||||
"validation_contract": {
|
||||
"invariants": validation.get("invariants", []),
|
||||
"perturbations": validation.get("perturbations", []),
|
||||
"thresholds": validation.get("thresholds", {}),
|
||||
},
|
||||
"backend_hint": payload.get("backend_hint", "build123d"),
|
||||
"missing_capabilities": semantic.get("missing_capabilities", []),
|
||||
"applied_experience_ids": semantic.get("applied_experience_ids", []),
|
||||
}
|
||||
return _validate_compiler_payload(flat)
|
||||
|
||||
|
||||
def validate_designir(payload: dict[str, Any]) -> dict[str, Any]:
|
||||
"""Validate either supported version and return its compiler representation."""
|
||||
return compiler_payload(payload)
|
||||
|
||||
|
||||
def _safe_expression(expression: str, values: dict[str, float]) -> float:
|
||||
try:
|
||||
tree = ast.parse(expression, mode="eval")
|
||||
@@ -261,13 +415,69 @@ def resolve_value(value: Any, values: dict[str, float], label: str) -> float:
|
||||
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 resolve_center(
|
||||
value: Any, values: dict[str, float], label: str
|
||||
) -> list[float]:
|
||||
if not isinstance(value, list) or len(value) != 3:
|
||||
raise DesignIRError(f"{label} must contain three values")
|
||||
return [
|
||||
resolve_value(component, values, f"{label}[{index}]")
|
||||
for index, component in enumerate(value)
|
||||
]
|
||||
|
||||
|
||||
def feature_axis(payload: dict[str, Any], feature: dict[str, Any]) -> list[float]:
|
||||
reference = feature.get("axis", "primary_axis")
|
||||
if isinstance(reference, list):
|
||||
axis = reference
|
||||
elif reference in {"x", "y", "z"}:
|
||||
axis = {
|
||||
"x": [1.0, 0.0, 0.0],
|
||||
"y": [0.0, 1.0, 0.0],
|
||||
"z": [0.0, 0.0, 1.0],
|
||||
}[reference]
|
||||
else:
|
||||
datum = payload.get("datums", {}).get(str(reference), {})
|
||||
axis = datum.get("axis")
|
||||
if isinstance(axis, str) and axis in {"x", "y", "z"}:
|
||||
axis = {
|
||||
"x": [1.0, 0.0, 0.0],
|
||||
"y": [0.0, 1.0, 0.0],
|
||||
"z": [0.0, 0.0, 1.0],
|
||||
}[axis]
|
||||
_vector3(axis, f"{feature.get('id', 'feature')}.axis")
|
||||
length = math.sqrt(sum(float(value) ** 2 for value in axis))
|
||||
if length <= 1e-12:
|
||||
raise DesignIRError(f"{feature.get('id', 'feature')}.axis is zero")
|
||||
return [float(value) / length for value in axis]
|
||||
|
||||
|
||||
def _axis_is_z(axis: list[float]) -> bool:
|
||||
return abs(axis[0]) <= 1e-9 and abs(axis[1]) <= 1e-9 and axis[2] > 0.0
|
||||
|
||||
|
||||
def feature_base_center(
|
||||
feature: dict[str, Any],
|
||||
center: list[float],
|
||||
axis: list[float],
|
||||
height: float,
|
||||
) -> list[float]:
|
||||
"""Resolve the primitive's bottom-face center from an explicit anchor."""
|
||||
anchor = str(feature.get("anchor", "base_center"))
|
||||
if anchor == "base_center":
|
||||
return center
|
||||
if anchor == "geometric_center":
|
||||
return [
|
||||
float(center[index]) - float(axis[index]) * height / 2.0
|
||||
for index in range(3)
|
||||
]
|
||||
raise DesignIRError(
|
||||
f"{feature.get('id', 'feature')}.anchor must be base_center or geometric_center"
|
||||
)
|
||||
|
||||
|
||||
def build_shape(payload: dict[str, Any]) -> Part:
|
||||
validate_designir(payload)
|
||||
payload = validate_designir(payload)
|
||||
if payload["reconstruction_status"] != "ready":
|
||||
missing = ", ".join(payload.get("missing_capabilities", [])) or "unspecified"
|
||||
raise DesignIRError(
|
||||
@@ -278,32 +488,39 @@ def build_shape(payload: dict[str, Any]) -> Part:
|
||||
|
||||
for feature in payload["features"]:
|
||||
operation = feature["operation"]
|
||||
_axis_is_z(feature)
|
||||
axis = feature_axis(payload, 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),
|
||||
center = resolve_center(
|
||||
feature.get("center", [0, 0, 0]),
|
||||
values,
|
||||
f"{feature['id']}.center",
|
||||
)
|
||||
base_center = feature_base_center(feature, center, axis, height)
|
||||
tool = Plane(origin=base_center, z_dir=axis) * 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),
|
||||
center = resolve_center(
|
||||
feature.get("center", [0, 0, 0]),
|
||||
values,
|
||||
f"{feature['id']}.center",
|
||||
)
|
||||
base_center = feature_base_center(feature, center, axis, height)
|
||||
tool = Plane(origin=base_center, z_dir=axis) * 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 not _axis_is_z(axis):
|
||||
raise DesignIRError(
|
||||
f"{feature['id']} hole cutting currently requires positive Z axis"
|
||||
)
|
||||
if shape is None:
|
||||
raise DesignIRError(f"{feature['id']} requires a host solid")
|
||||
diameter = resolve_value(
|
||||
@@ -314,8 +531,11 @@ def build_shape(payload: dict[str, Any]) -> Part:
|
||||
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")
|
||||
center = resolve_center(
|
||||
feature.get("center", [0, 0, 0]),
|
||||
values,
|
||||
f"{feature['id']}.center",
|
||||
)
|
||||
centers = [(float(center[0]), float(center[1]))]
|
||||
else:
|
||||
count = int(
|
||||
@@ -352,7 +572,7 @@ def build_shape(payload: dict[str, Any]) -> Part:
|
||||
|
||||
|
||||
def build_simplecad_shape(payload: dict[str, Any]) -> tuple[Any, str]:
|
||||
validate_designir(payload)
|
||||
payload = validate_designir(payload)
|
||||
if payload["reconstruction_status"] != "ready":
|
||||
missing = ", ".join(payload.get("missing_capabilities", [])) or "unspecified"
|
||||
raise DesignIRError(
|
||||
@@ -373,7 +593,7 @@ def build_simplecad_shape(payload: dict[str, Any]) -> tuple[Any, str]:
|
||||
with scad.GraphSession() as session:
|
||||
for feature in payload["features"]:
|
||||
operation = feature["operation"]
|
||||
_axis_is_z(feature)
|
||||
axis = feature_axis(payload, feature)
|
||||
if operation in {"extrude_circle", "add_cylinder"}:
|
||||
radius = resolve_value(
|
||||
feature["radius"], values, f"{feature['id']}.radius"
|
||||
@@ -381,14 +601,17 @@ def build_simplecad_shape(payload: dict[str, Any]) -> tuple[Any, str]:
|
||||
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))
|
||||
center = resolve_center(
|
||||
feature.get("center", [0, 0, 0]),
|
||||
values,
|
||||
f"{feature['id']}.center",
|
||||
)
|
||||
point = tuple(feature_base_center(feature, center, axis, height))
|
||||
tool = scad.make_cylinder_rsolid(
|
||||
radius=radius,
|
||||
height=height,
|
||||
bottom_face_center=point,
|
||||
axis=(0.0, 0.0, 1.0),
|
||||
axis=tuple(axis),
|
||||
)
|
||||
shape = (
|
||||
tool
|
||||
@@ -402,6 +625,10 @@ def build_simplecad_shape(payload: dict[str, Any]) -> tuple[Any, str]:
|
||||
else max(z_max, point[2] + height)
|
||||
)
|
||||
elif operation == "extrude_rectangle":
|
||||
if not _axis_is_z(axis):
|
||||
raise DesignIRError(
|
||||
f"{feature['id']} SimpleCAD box currently requires positive Z axis"
|
||||
)
|
||||
width = resolve_value(
|
||||
feature["width"], values, f"{feature['id']}.width"
|
||||
)
|
||||
@@ -411,9 +638,12 @@ def build_simplecad_shape(payload: dict[str, Any]) -> tuple[Any, str]:
|
||||
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))
|
||||
center = resolve_center(
|
||||
feature.get("center", [0, 0, 0]),
|
||||
values,
|
||||
f"{feature['id']}.center",
|
||||
)
|
||||
point = tuple(feature_base_center(feature, center, axis, height))
|
||||
tool = scad.make_box_rsolid(
|
||||
width=width,
|
||||
height=depth,
|
||||
@@ -432,14 +662,21 @@ def build_simplecad_shape(payload: dict[str, Any]) -> tuple[Any, str]:
|
||||
else max(z_max, point[2] + height)
|
||||
)
|
||||
elif operation in {"through_hole", "polar_hole_pattern"}:
|
||||
if not _axis_is_z(axis):
|
||||
raise DesignIRError(
|
||||
f"{feature['id']} SimpleCAD hole currently requires positive Z axis"
|
||||
)
|
||||
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")
|
||||
center = resolve_center(
|
||||
feature.get("center", [0, 0, 0]),
|
||||
values,
|
||||
f"{feature['id']}.center",
|
||||
)
|
||||
centers = [(float(center[0]), float(center[1]))]
|
||||
else:
|
||||
count = int(
|
||||
@@ -532,10 +769,69 @@ def compile_designir(
|
||||
}
|
||||
|
||||
|
||||
def materialize_hybrid_designir(
|
||||
payload: dict[str, Any], generated_step: Path
|
||||
) -> dict[str, Any]:
|
||||
"""Attach a compiled SurfaceIR snapshot to a semantic DesignIR 3.0 model.
|
||||
|
||||
The semantic feature program remains authoritative for future edits. The
|
||||
SurfaceIR snapshot is extracted only from the newly generated STEP, never
|
||||
from teacher geometry.
|
||||
"""
|
||||
semantic = (
|
||||
migrate_designir_2_to_3(payload)
|
||||
if payload.get("schema_version") == LEGACY_SCHEMA_VERSION
|
||||
else copy.deepcopy(payload)
|
||||
)
|
||||
compiler_payload(semantic)
|
||||
surface_script = Path(__file__).with_name("surfaceir_pipeline.py")
|
||||
spec = importlib.util.spec_from_file_location(
|
||||
"_designir_surfaceir_pipeline", surface_script
|
||||
)
|
||||
if spec is None or spec.loader is None:
|
||||
raise DesignIRError("Cannot load SurfaceIR compiler")
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(module)
|
||||
extracted = module.extract_surfaceir(generated_step.expanduser().resolve())
|
||||
semantic["surface_layer"] = extracted["surface_layer"]
|
||||
semantic["reconstruction_strategy"] = extracted.get(
|
||||
"reconstruction_strategy",
|
||||
{
|
||||
"boundary_strategy": "exact_3d",
|
||||
"selection_status": "default",
|
||||
},
|
||||
)
|
||||
semantic["document_status"] = extracted.get(
|
||||
"document_status", "geometry_present"
|
||||
)
|
||||
semantic["reconstruction_mode"] = "hybrid_semantic_surface_parametric"
|
||||
edit_interface = semantic["edit_interface"]
|
||||
edit_interface["surface_parameter_groups"] = extracted.get(
|
||||
"edit_interface", {}
|
||||
).get("surface_parameter_groups", [])
|
||||
edit_interface["modification_levels"] = list(
|
||||
dict.fromkeys(
|
||||
list(edit_interface.get("modification_levels", []))
|
||||
+ ["semantic_feature", "analytic_surface", "spline_control"]
|
||||
)
|
||||
)
|
||||
semantic["compiled_surface_provenance"] = {
|
||||
"source": "generated_step",
|
||||
"teacher_geometry_used": False,
|
||||
"semantic_layer_authoritative": True,
|
||||
}
|
||||
return semantic
|
||||
|
||||
|
||||
def anti_cheat(payload: dict[str, Any], generator: Path | None = None) -> dict[str, Any]:
|
||||
validate_designir(payload)
|
||||
violations: list[str] = []
|
||||
forbidden = find_forbidden(payload)
|
||||
inspected = (
|
||||
payload.get("semantic_layer", {})
|
||||
if payload.get("schema_version") == SCHEMA_VERSION
|
||||
else payload
|
||||
)
|
||||
forbidden = find_forbidden(inspected)
|
||||
if forbidden:
|
||||
violations.append(f"DesignIR teacher dependency at {forbidden}")
|
||||
if generator:
|
||||
@@ -594,14 +890,30 @@ def _symmetric_difference_volume(first: Part, second: Part) -> float:
|
||||
raise DesignIRError(f"Symmetric-difference boolean failed: {exc}") from exc
|
||||
|
||||
|
||||
def _parameter_reference_count(value: Any, parameter: str) -> int:
|
||||
if isinstance(value, dict):
|
||||
direct = 1 if value.get("parameter") == parameter else 0
|
||||
return direct + sum(
|
||||
_parameter_reference_count(child, parameter)
|
||||
for child in value.values()
|
||||
)
|
||||
if isinstance(value, list):
|
||||
return sum(_parameter_reference_count(child, parameter) for child in value)
|
||||
return 0
|
||||
|
||||
|
||||
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())
|
||||
payload = validate_designir(payload)
|
||||
teacher_resolved = teacher_path.expanduser().resolve()
|
||||
rebuilt_resolved = rebuilt_path.expanduser().resolve()
|
||||
if teacher_resolved == rebuilt_resolved:
|
||||
raise DesignIRError("Teacher and rebuilt STEP must be different files")
|
||||
teacher = import_step(teacher_resolved)
|
||||
rebuilt = import_step(rebuilt_resolved)
|
||||
teacher_facts = geometry_facts(teacher)
|
||||
rebuilt_facts = geometry_facts(rebuilt)
|
||||
volume_base = max(teacher_facts["volume_mm3"], 1e-12)
|
||||
@@ -640,6 +952,38 @@ def acceptance_report(
|
||||
try:
|
||||
output = Path(temporary) / f"perturbation-{index}.step"
|
||||
result = compile_designir(modified, output)
|
||||
modified_shape = import_step(output)
|
||||
changed_volume = _symmetric_difference_volume(
|
||||
rebuilt, modified_shape
|
||||
)
|
||||
change_ratio = changed_volume / max(
|
||||
rebuilt_facts["volume_mm3"], 1e-12
|
||||
)
|
||||
reference_count = _parameter_reference_count(
|
||||
payload.get("features", []), name
|
||||
)
|
||||
preserved = {
|
||||
preserved_name: (
|
||||
preserved_name in payload["parameters"]
|
||||
and preserved_name in modified["parameters"]
|
||||
and payload["parameters"][preserved_name]["value"]
|
||||
== modified["parameters"][preserved_name]["value"]
|
||||
)
|
||||
for preserved_name in test.get("preserve", [])
|
||||
}
|
||||
expected_change = str(test.get("expected_change") or "")
|
||||
expected_change_observed = change_ratio > 1e-9
|
||||
if expected_change == "overall_height_increases":
|
||||
expected_change_observed = (
|
||||
result["facts"]["size_mm"][2]
|
||||
> rebuilt_facts["size_mm"][2] + 1e-9
|
||||
)
|
||||
success = (
|
||||
result["facts"]["solid_count"] > 0
|
||||
and reference_count > 0
|
||||
and expected_change_observed
|
||||
and all(preserved.values())
|
||||
)
|
||||
perturbations.append(
|
||||
{
|
||||
"parameter": name,
|
||||
@@ -647,7 +991,11 @@ def acceptance_report(
|
||||
"modified_value": modified["parameters"][name]["value"],
|
||||
"expected_change": test.get("expected_change"),
|
||||
"preserve": test.get("preserve", []),
|
||||
"success": result["facts"]["solid_count"] > 0,
|
||||
"parameter_reference_count": reference_count,
|
||||
"geometry_change_ratio": change_ratio,
|
||||
"expected_change_observed": expected_change_observed,
|
||||
"preserved_parameter_checks": preserved,
|
||||
"success": success,
|
||||
"facts": result["facts"],
|
||||
}
|
||||
)
|
||||
@@ -678,6 +1026,14 @@ def acceptance_report(
|
||||
"schema_version": "1.0",
|
||||
"report_kind": "independent_designir_acceptance",
|
||||
"model_id": payload["model_id"],
|
||||
"teacher_sha256": hashlib.sha256(teacher_resolved.read_bytes()).hexdigest(),
|
||||
"applied_experience_ids": sorted(
|
||||
{
|
||||
str(item)
|
||||
for item in payload.get("applied_experience_ids", [])
|
||||
if str(item).strip()
|
||||
}
|
||||
),
|
||||
"geometry": geometry,
|
||||
"feature_semantics": {
|
||||
"declared_feature_count": len(payload["features"]),
|
||||
@@ -700,15 +1056,34 @@ def acceptance_report(
|
||||
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
|
||||
methods: dict[str, set[str]] = {}
|
||||
for item in passing:
|
||||
source_hash = str(item.get("teacher_sha256") or "")
|
||||
if not source_hash:
|
||||
continue
|
||||
for experience_id in item.get("applied_experience_ids", []):
|
||||
methods.setdefault(str(experience_id), set()).add(source_hash)
|
||||
validated_methods = sorted(
|
||||
experience_id
|
||||
for experience_id, source_hashes in methods.items()
|
||||
if len(source_hashes) >= minimum
|
||||
)
|
||||
independent_sources = len(
|
||||
{
|
||||
source_hash
|
||||
for source_hashes in methods.values()
|
||||
for source_hash in source_hashes
|
||||
}
|
||||
)
|
||||
eligible = bool(validated_methods)
|
||||
return {
|
||||
"eligible": eligible,
|
||||
"independent_case_count": len(model_ids),
|
||||
"independent_case_count": independent_sources,
|
||||
"passing_case_count": len(passing),
|
||||
"required_case_count": minimum,
|
||||
"validated_experience_ids": validated_methods,
|
||||
"next_state": "promoted" if eligible else "candidate",
|
||||
}
|
||||
|
||||
@@ -720,6 +1095,10 @@ def build_parser() -> argparse.ArgumentParser:
|
||||
validate = commands.add_parser("validate")
|
||||
validate.add_argument("designir", type=Path)
|
||||
|
||||
migrate = commands.add_parser("migrate")
|
||||
migrate.add_argument("designir", type=Path)
|
||||
migrate.add_argument("--output", type=Path, required=True)
|
||||
|
||||
guard = commands.add_parser("anti-cheat")
|
||||
guard.add_argument("designir", type=Path)
|
||||
guard.add_argument("--generator", type=Path)
|
||||
@@ -736,6 +1115,11 @@ def build_parser() -> argparse.ArgumentParser:
|
||||
isolated.add_argument("--output", type=Path, required=True)
|
||||
isolated.add_argument("--backend", choices=("build123d", "simplecadapi"))
|
||||
|
||||
materialize = commands.add_parser("materialize-hybrid")
|
||||
materialize.add_argument("designir", type=Path)
|
||||
materialize.add_argument("--step", type=Path, required=True)
|
||||
materialize.add_argument("--output", type=Path, required=True)
|
||||
|
||||
accept = commands.add_parser("accept")
|
||||
accept.add_argument("--teacher", type=Path, required=True)
|
||||
accept.add_argument("--designir", type=Path, required=True)
|
||||
@@ -752,12 +1136,29 @@ 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))
|
||||
original = read_json(args.designir)
|
||||
payload = validate_designir(original)
|
||||
result = {
|
||||
"valid": True,
|
||||
"model_id": payload["model_id"],
|
||||
"input_schema_version": original.get("schema_version"),
|
||||
"supported_operations": sorted(SUPPORTED_OPERATIONS),
|
||||
}
|
||||
elif args.command == "migrate":
|
||||
original = read_json(args.designir)
|
||||
migrated = (
|
||||
migrate_designir_2_to_3(original)
|
||||
if original.get("schema_version") == LEGACY_SCHEMA_VERSION
|
||||
else original
|
||||
)
|
||||
compiler_payload(migrated)
|
||||
write_json(args.output, migrated)
|
||||
result = {
|
||||
"valid": True,
|
||||
"output": str(args.output.expanduser().resolve()),
|
||||
"schema_version": migrated["schema_version"],
|
||||
"reconstruction_mode": migrated["reconstruction_mode"],
|
||||
}
|
||||
elif args.command == "anti-cheat":
|
||||
result = anti_cheat(read_json(args.designir), args.generator)
|
||||
elif args.command == "compile":
|
||||
@@ -768,6 +1169,18 @@ def main(argv: list[str] | None = None) -> int:
|
||||
result = isolated_rebuild(
|
||||
args.designir, args.output, backend=args.backend
|
||||
)
|
||||
elif args.command == "materialize-hybrid":
|
||||
materialized = materialize_hybrid_designir(
|
||||
read_json(args.designir), args.step
|
||||
)
|
||||
write_json(args.output, materialized)
|
||||
result = {
|
||||
"valid": True,
|
||||
"output": str(args.output.expanduser().resolve()),
|
||||
"schema_version": materialized["schema_version"],
|
||||
"reconstruction_mode": materialized["reconstruction_mode"],
|
||||
"semantic_layer_authoritative": True,
|
||||
}
|
||||
elif args.command == "accept":
|
||||
result = acceptance_report(
|
||||
args.teacher, args.rebuilt, read_json(args.designir)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,735 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Build execution-validated STEP -> DesignIR trajectories.
|
||||
*
|
||||
* Agent A is an OpenAI-compatible language model. Agent B is the deterministic
|
||||
* CAD-kernel acceptance program. The teacher STEP is never included in Agent A
|
||||
* input and is used only by Agent B after isolated reconstruction.
|
||||
*/
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import process from "node:process";
|
||||
import { createRequire } from "node:module";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { spawnSync } from "node:child_process";
|
||||
|
||||
const HERE = path.dirname(fileURLToPath(import.meta.url));
|
||||
const PIPELINE = path.resolve(HERE, "..");
|
||||
const REPO = path.resolve(PIPELINE, "..");
|
||||
const requireFromStudio = createRequire(
|
||||
path.join(REPO, "cad-agent-studio", "package.json"),
|
||||
);
|
||||
const YAML = requireFromStudio("yaml");
|
||||
const CONFIG_PATH = path.join(REPO, "llm.config.yaml");
|
||||
const EVIDENCE_DIR = path.join(PIPELINE, "runs", "evidence");
|
||||
const TRAJECTORY_DIR = path.join(PIPELINE, "runs", "trajectory");
|
||||
const SPLIT_PATH = path.join(TRAJECTORY_DIR, "splits.json");
|
||||
const PYTHON = path.join(REPO, "text-to-cad", ".venv", "bin", "python");
|
||||
const DESIGNIR_CLI = path.join(PIPELINE, "scripts", "designir_pipeline.py");
|
||||
const EXAMPLE_PATH = path.join(
|
||||
PIPELINE,
|
||||
"examples",
|
||||
"raised_hub_flange.designir.json",
|
||||
);
|
||||
|
||||
function readJson(file) {
|
||||
return JSON.parse(fs.readFileSync(file, "utf8"));
|
||||
}
|
||||
|
||||
function writeJson(file, value) {
|
||||
fs.mkdirSync(path.dirname(file), { recursive: true });
|
||||
fs.writeFileSync(file, `${JSON.stringify(value, null, 2)}\n`);
|
||||
}
|
||||
|
||||
function argsObject(argv) {
|
||||
const result = { _: [] };
|
||||
for (let index = 0; index < argv.length; index += 1) {
|
||||
const value = argv[index];
|
||||
if (!value.startsWith("--")) {
|
||||
result._.push(value);
|
||||
continue;
|
||||
}
|
||||
const key = value.slice(2);
|
||||
const next = argv[index + 1];
|
||||
if (!next || next.startsWith("--")) {
|
||||
result[key] = true;
|
||||
} else {
|
||||
result[key] = next;
|
||||
index += 1;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function evidenceFiles() {
|
||||
return fs
|
||||
.readdirSync(EVIDENCE_DIR)
|
||||
.filter((name) => name.endsWith(".case.json"))
|
||||
.map((name) => path.join(EVIDENCE_DIR, name))
|
||||
.sort();
|
||||
}
|
||||
|
||||
function allocate(total, counts) {
|
||||
const population = Object.values(counts).reduce((sum, value) => sum + value, 0);
|
||||
const rows = Object.entries(counts).map(([family, count]) => {
|
||||
const exact = (count * total) / population;
|
||||
return { family, value: Math.floor(exact), remainder: exact % 1 };
|
||||
});
|
||||
let remaining = total - rows.reduce((sum, row) => sum + row.value, 0);
|
||||
rows.sort((left, right) => right.remainder - left.remainder);
|
||||
for (const row of rows) {
|
||||
if (remaining <= 0) break;
|
||||
row.value += 1;
|
||||
remaining -= 1;
|
||||
}
|
||||
return Object.fromEntries(rows.map((row) => [row.family, row.value]));
|
||||
}
|
||||
|
||||
function initSplits() {
|
||||
const unique = new Map();
|
||||
for (const file of evidenceFiles()) {
|
||||
const payload = readJson(file);
|
||||
const geometry = payload.geometry_evidence || {};
|
||||
if (!geometry.reconstruction_eligible) continue;
|
||||
const id = String(payload.provenance?.source_sha256 || payload.case_id);
|
||||
if (unique.has(id)) continue;
|
||||
unique.set(id, {
|
||||
id,
|
||||
family: String(payload.design_ir?.part_family || "unknown"),
|
||||
evidence: file,
|
||||
teacher: String(payload.provenance?.source_path || ""),
|
||||
face_count: Number(geometry.face_count || 0),
|
||||
});
|
||||
}
|
||||
const byFamily = {};
|
||||
for (const item of unique.values()) {
|
||||
(byFamily[item.family] ||= []).push(item);
|
||||
}
|
||||
for (const items of Object.values(byFamily)) {
|
||||
items.sort((left, right) => left.id.localeCompare(right.id));
|
||||
}
|
||||
const counts = Object.fromEntries(
|
||||
Object.entries(byFamily).map(([family, items]) => [family, items.length]),
|
||||
);
|
||||
const testQuota = allocate(100, counts);
|
||||
const validationQuota = allocate(75, counts);
|
||||
const splits = { train: [], validation: [], test: [] };
|
||||
for (const [family, items] of Object.entries(byFamily)) {
|
||||
const testCount = testQuota[family] || 0;
|
||||
const validationCount = validationQuota[family] || 0;
|
||||
splits.test.push(...items.slice(0, testCount));
|
||||
splits.validation.push(
|
||||
...items.slice(testCount, testCount + validationCount),
|
||||
);
|
||||
splits.train.push(...items.slice(testCount + validationCount));
|
||||
}
|
||||
for (const values of Object.values(splits)) {
|
||||
values.sort(
|
||||
(left, right) =>
|
||||
left.face_count - right.face_count || left.id.localeCompare(right.id),
|
||||
);
|
||||
}
|
||||
const payload = {
|
||||
schema_version: "1.0",
|
||||
split_kind: "sha256_isolated_stratified_trajectory_split",
|
||||
policy: {
|
||||
train_count: splits.train.length,
|
||||
validation_count: splits.validation.length,
|
||||
test_count: splits.test.length,
|
||||
teacher_step_visible_to_reconstruction_agent: false,
|
||||
heldout_cases_allowed_in_experience_induction: false,
|
||||
},
|
||||
splits,
|
||||
};
|
||||
writeJson(SPLIT_PATH, payload);
|
||||
return payload;
|
||||
}
|
||||
|
||||
function loadProvider(requestedProvider, requestedModel) {
|
||||
const config = YAML.parse(fs.readFileSync(CONFIG_PATH, "utf8"));
|
||||
const providerId = requestedProvider || config.defaultProvider;
|
||||
const provider = config.providers[providerId];
|
||||
if (!provider) throw new Error(`Unknown provider ${providerId}`);
|
||||
const apiKey =
|
||||
String(provider.apiKey || "").trim() ||
|
||||
process.env[String(provider.apiKeyEnv || "")] ||
|
||||
"";
|
||||
if (!apiKey) throw new Error(`No API key configured for ${providerId}`);
|
||||
const defaultModel = String(config.defaultModel || "").split(":").pop();
|
||||
const model =
|
||||
requestedModel || provider.models?.default || defaultModel;
|
||||
return {
|
||||
providerId,
|
||||
model,
|
||||
apiKey,
|
||||
baseURL: String(provider.baseURL).replace(/\/+$/, ""),
|
||||
};
|
||||
}
|
||||
|
||||
function compactEvidence(payload, maxSurfaces = 80) {
|
||||
const geometry = payload.geometry_evidence || {};
|
||||
const design = payload.design_ir || {};
|
||||
return {
|
||||
evidence_kind: "final_brep_observation_not_source_history",
|
||||
geometry: {
|
||||
solid_count: geometry.solid_count,
|
||||
face_count: geometry.face_count,
|
||||
volume: geometry.volume,
|
||||
bounding_box: geometry.bounding_box,
|
||||
analytic_surface_counts: geometry.analytic_surface_counts,
|
||||
analytic_surfaces: (geometry.analytic_surfaces || []).slice(0, maxSurfaces),
|
||||
},
|
||||
inferred_hypotheses: {
|
||||
family: design.part_family,
|
||||
features: design.features,
|
||||
constraints: design.constraints,
|
||||
semantic_parameter_candidates: design.semantic_parameter_candidates,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function reconstructionPrompt(evidence, previous, feedback, experience = []) {
|
||||
const example = readJson(EXAMPLE_PATH);
|
||||
return [
|
||||
"You are Agent A, a STEP reverse-engineering program synthesizer.",
|
||||
"Generate an independent executable DesignIR 2.0 JSON object from final-BRep evidence.",
|
||||
"The source STEP is unavailable. Never reference, import, embed, or describe it as a base feature.",
|
||||
"STEP does not contain source history. Use canonical reconstruction order and explicitly inferred semantic names.",
|
||||
"Current executable operations are exactly: extrude_circle, extrude_rectangle, add_cylinder, through_hole, polar_hole_pattern.",
|
||||
"Cylinder and extrusion primitives may use any observed axis. Hole cutters currently require the positive Z axis.",
|
||||
"Preserve the observed absolute coordinate frame and orientation.",
|
||||
"For cylinder and extrusion features, set anchor to geometric_center and center to the observed 3D bounding-box center unless there is strong evidence for another placement.",
|
||||
"The legacy/default anchor is base_center; never rely on that default for reverse engineering.",
|
||||
"A value reference object may contain exactly one key: parameter or expression. Do not add scale or arithmetic keys.",
|
||||
"Do not expose an editable parameter unless at least one feature directly references it. Non-driving measurements must be editable false or omitted.",
|
||||
"Use named editable parameters such as plate_thickness, hole_diameter, hole_count, bolt_circle_diameter, bore_diameter, overall_length, overall_width.",
|
||||
"Every editable parameter must be referenced by a feature.",
|
||||
"Every perturbation must contain parameter, scale, expected_change, and preserve.",
|
||||
"Use an empty list for constraints when none are justified.",
|
||||
"Return JSON only, without Markdown.",
|
||||
"",
|
||||
"REFERENCE FORMAT:",
|
||||
JSON.stringify(example),
|
||||
"",
|
||||
"PRIVATE GEOMETRY EVIDENCE:",
|
||||
JSON.stringify(evidence),
|
||||
experience.length
|
||||
? `\nVALIDATED CANDIDATE METHODS (apply only when relevant):\n${JSON.stringify(experience)}`
|
||||
: "",
|
||||
previous ? `\nPREVIOUS CANDIDATE:\n${JSON.stringify(previous)}` : "",
|
||||
feedback ? `\nVALIDATOR FEEDBACK:\n${feedback}` : "",
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join("\n");
|
||||
}
|
||||
|
||||
async function callModel(provider, prompt) {
|
||||
const response = await fetch(`${provider.baseURL}/chat/completions`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Authorization: `Bearer ${provider.apiKey}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
model: provider.model,
|
||||
temperature: 0.2,
|
||||
messages: [
|
||||
{
|
||||
role: "system",
|
||||
content:
|
||||
"Produce normalized, editable parametric CAD design states. Output strict JSON.",
|
||||
},
|
||||
{ role: "user", content: prompt },
|
||||
],
|
||||
response_format: { type: "json_object" },
|
||||
}),
|
||||
signal: AbortSignal.timeout(90_000),
|
||||
});
|
||||
if (!response.ok) {
|
||||
const detail = (await response.text()).slice(0, 600);
|
||||
throw new Error(`LLM HTTP ${response.status}: ${detail}`);
|
||||
}
|
||||
const payload = await response.json();
|
||||
const content = payload.choices?.[0]?.message?.content;
|
||||
if (!content) throw new Error("LLM returned no message content");
|
||||
const cleaned = String(content)
|
||||
.replace(/^```(?:json)?\s*/i, "")
|
||||
.replace(/\s*```$/, "");
|
||||
return {
|
||||
designir: JSON.parse(cleaned),
|
||||
usage: payload.usage || {},
|
||||
};
|
||||
}
|
||||
|
||||
function runPython(arguments_) {
|
||||
const result = spawnSync(PYTHON, [DESIGNIR_CLI, ...arguments_], {
|
||||
cwd: REPO,
|
||||
encoding: "utf8",
|
||||
timeout: 120_000,
|
||||
});
|
||||
const output = `${result.stdout || ""}\n${result.stderr || ""}`.trim();
|
||||
return { ok: result.status === 0, status: result.status, output };
|
||||
}
|
||||
|
||||
async function runOne(job, provider, maxAttempts, options = {}) {
|
||||
const shortId = job.id.slice(0, 16);
|
||||
const root = path.join(
|
||||
options.attemptsRoot || path.join(TRAJECTORY_DIR, "attempts"),
|
||||
shortId,
|
||||
);
|
||||
fs.mkdirSync(root, { recursive: true });
|
||||
const evidencePayload = readJson(job.evidence);
|
||||
const evidence = compactEvidence(evidencePayload);
|
||||
const unsupportedSurfaces = Object.entries(
|
||||
evidence.geometry.analytic_surface_counts || {},
|
||||
)
|
||||
.filter(([kind, count]) => count > 0 && !["plane", "cylinder"].includes(kind))
|
||||
.map(([kind]) => kind);
|
||||
if (unsupportedSurfaces.length) {
|
||||
const trajectory = {
|
||||
schema_version: "1.0",
|
||||
trajectory_kind: "execution_validated_step_to_designir",
|
||||
case_id: job.id,
|
||||
family: job.family,
|
||||
provider: provider.providerId,
|
||||
model: provider.model,
|
||||
attempts: [],
|
||||
state: "unsupported_feature_vocabulary",
|
||||
missing_capabilities: unsupportedSurfaces.map(
|
||||
(kind) => `reconstruct_${kind}_surface`,
|
||||
),
|
||||
};
|
||||
writeJson(path.join(root, "trajectory.json"), trajectory);
|
||||
return trajectory;
|
||||
}
|
||||
let previous = null;
|
||||
let feedback = "";
|
||||
const attempts = [];
|
||||
for (let index = 1; index <= maxAttempts; index += 1) {
|
||||
const started = Date.now();
|
||||
try {
|
||||
const generated = await callModel(
|
||||
provider,
|
||||
reconstructionPrompt(
|
||||
evidence,
|
||||
previous,
|
||||
feedback,
|
||||
options.experience || [],
|
||||
),
|
||||
);
|
||||
const designir = generated.designir;
|
||||
designir.model_id ||= `trajectory_${shortId}_${index}`;
|
||||
designir.applied_experience_ids = (options.experience || []).map(
|
||||
(method) => method.id,
|
||||
);
|
||||
const designirPath = path.join(root, `attempt-${index}.designir.json`);
|
||||
const rebuiltPath = path.join(root, `attempt-${index}.step`);
|
||||
const reportPath = path.join(root, `attempt-${index}.acceptance.json`);
|
||||
writeJson(designirPath, designir);
|
||||
const rebuild = runPython([
|
||||
"isolated-rebuild",
|
||||
designirPath,
|
||||
"--output",
|
||||
rebuiltPath,
|
||||
"--backend",
|
||||
"build123d",
|
||||
]);
|
||||
if (!rebuild.ok) {
|
||||
feedback = `Isolated rebuild failed: ${rebuild.output}`;
|
||||
attempts.push({
|
||||
index,
|
||||
state: "compile_failed",
|
||||
duration_ms: Date.now() - started,
|
||||
usage: generated.usage,
|
||||
feedback,
|
||||
});
|
||||
previous = designir;
|
||||
continue;
|
||||
}
|
||||
const acceptance = runPython([
|
||||
"accept",
|
||||
"--teacher",
|
||||
job.teacher,
|
||||
"--designir",
|
||||
designirPath,
|
||||
"--rebuilt",
|
||||
rebuiltPath,
|
||||
"--report",
|
||||
reportPath,
|
||||
]);
|
||||
const report = fs.existsSync(reportPath) ? readJson(reportPath) : null;
|
||||
const state =
|
||||
acceptance.ok && report?.verdict === "pass"
|
||||
? "replay_validated"
|
||||
: "acceptance_failed";
|
||||
attempts.push({
|
||||
index,
|
||||
state,
|
||||
duration_ms: Date.now() - started,
|
||||
usage: generated.usage,
|
||||
report: reportPath,
|
||||
});
|
||||
if (state === "replay_validated") {
|
||||
const trajectory = {
|
||||
schema_version: "1.0",
|
||||
trajectory_kind: "execution_validated_step_to_designir",
|
||||
case_id: job.id,
|
||||
split: job.split,
|
||||
family: job.family,
|
||||
provider: provider.providerId,
|
||||
model: provider.model,
|
||||
selected_attempt: index,
|
||||
designir: designirPath,
|
||||
rebuilt_step: rebuiltPath,
|
||||
acceptance_report: reportPath,
|
||||
attempts,
|
||||
state,
|
||||
};
|
||||
writeJson(path.join(root, "trajectory.json"), trajectory);
|
||||
return trajectory;
|
||||
}
|
||||
feedback = `Acceptance failed: ${JSON.stringify({
|
||||
threshold_checks: report?.threshold_checks,
|
||||
geometry: report?.geometry
|
||||
? {
|
||||
relative_volume_error: report.geometry.relative_volume_error,
|
||||
bounding_box_axis_error_mm:
|
||||
report.geometry.bounding_box_axis_error_mm,
|
||||
symmetric_difference_ratio:
|
||||
report.geometry.symmetric_difference_ratio,
|
||||
teacher_facts: report.geometry.teacher,
|
||||
rebuilt_facts: report.geometry.rebuilt,
|
||||
}
|
||||
: null,
|
||||
editability: report?.editability,
|
||||
})}`;
|
||||
previous = designir;
|
||||
} catch (error) {
|
||||
feedback = error instanceof Error ? error.message : String(error);
|
||||
attempts.push({
|
||||
index,
|
||||
state: "agent_failed",
|
||||
duration_ms: Date.now() - started,
|
||||
feedback,
|
||||
});
|
||||
}
|
||||
}
|
||||
const trajectory = {
|
||||
schema_version: "1.0",
|
||||
trajectory_kind: "execution_validated_step_to_designir",
|
||||
case_id: job.id,
|
||||
family: job.family,
|
||||
provider: provider.providerId,
|
||||
model: provider.model,
|
||||
attempts,
|
||||
state: "quarantine",
|
||||
};
|
||||
writeJson(path.join(root, "trajectory.json"), trajectory);
|
||||
return trajectory;
|
||||
}
|
||||
|
||||
function summarize() {
|
||||
const root = path.join(TRAJECTORY_DIR, "attempts");
|
||||
const rows = fs.existsSync(root)
|
||||
? fs
|
||||
.readdirSync(root)
|
||||
.map((name) => path.join(root, name, "trajectory.json"))
|
||||
.filter((file) => fs.existsSync(file))
|
||||
.map(readJson)
|
||||
: [];
|
||||
const states = {};
|
||||
for (const row of rows) states[row.state] = (states[row.state] || 0) + 1;
|
||||
return {
|
||||
schema_version: "1.0",
|
||||
report_kind: "trajectory_run_summary",
|
||||
case_count: rows.length,
|
||||
states,
|
||||
validated_trajectory_count: states.replay_validated || 0,
|
||||
quarantined_count: states.quarantine || 0,
|
||||
rows: rows.map((row) => ({
|
||||
case_id: row.case_id,
|
||||
family: row.family,
|
||||
state: row.state,
|
||||
attempt_count: row.attempts?.length || 0,
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
function distillValidatedTrajectories() {
|
||||
const root = path.join(TRAJECTORY_DIR, "attempts");
|
||||
const trajectories = fs.existsSync(root)
|
||||
? fs
|
||||
.readdirSync(root)
|
||||
.map((name) => path.join(root, name, "trajectory.json"))
|
||||
.filter((file) => fs.existsSync(file))
|
||||
.map(readJson)
|
||||
.filter((row) => row.state === "replay_validated")
|
||||
: [];
|
||||
const groups = new Map();
|
||||
for (const trajectory of trajectories) {
|
||||
const designir = readJson(trajectory.designir);
|
||||
const signature = designir.features
|
||||
.map((feature) =>
|
||||
["add_cylinder", "extrude_circle"].includes(feature.operation)
|
||||
? "cylinder"
|
||||
: feature.operation,
|
||||
)
|
||||
.sort()
|
||||
.join("+");
|
||||
const key = `${designir.family}::${signature}`;
|
||||
const group = groups.get(key) || {
|
||||
family: designir.family,
|
||||
signature,
|
||||
cases: [],
|
||||
parameterNames: new Map(),
|
||||
anchors: new Map(),
|
||||
operations: new Set(),
|
||||
};
|
||||
group.cases.push(trajectory.case_id);
|
||||
for (const name of Object.keys(designir.parameters || {})) {
|
||||
group.parameterNames.set(name, (group.parameterNames.get(name) || 0) + 1);
|
||||
}
|
||||
for (const feature of designir.features || []) {
|
||||
group.operations.add(feature.operation);
|
||||
const anchor = feature.anchor || "base_center";
|
||||
group.anchors.set(anchor, (group.anchors.get(anchor) || 0) + 1);
|
||||
}
|
||||
groups.set(key, group);
|
||||
}
|
||||
const candidates = [...groups.values()].map((group) => {
|
||||
const simpleCylinder =
|
||||
group.family === "simple_cylinder" && group.signature === "cylinder";
|
||||
return {
|
||||
id: `trajectory.${group.family}.${group.signature.replaceAll("+", "_")}`,
|
||||
kind: "reconstruction_grammar",
|
||||
promotion_state: "candidate",
|
||||
applicability: simpleCylinder
|
||||
? {
|
||||
family: group.family,
|
||||
evidence_conditions: [
|
||||
"one dominant axis",
|
||||
"one distinct coaxial radius",
|
||||
"low topological complexity",
|
||||
"solid volume approximately pi * radius^2 * axial_length",
|
||||
],
|
||||
}
|
||||
: {
|
||||
family: group.family,
|
||||
operations: [...group.operations].sort(),
|
||||
},
|
||||
method: {
|
||||
guidance: simpleCylinder
|
||||
? "Interpret the single cylindrical surface as the outer body, not a bore. Build one cylinder from the observed outer radius, axial length, axis, and pose. Name the driving parameters outer_radius and overall_length. Preserve the absolute frame with an explicit center and anchor; do not expose duplicate width/thickness/inner-diameter parameters."
|
||||
: "Reconstruct from analytic dimensions and axis evidence; preserve the observed absolute frame with an explicit center and anchor; expose only parameters that directly drive executable features.",
|
||||
canonical_parameter_names: simpleCylinder
|
||||
? ["outer_radius", "overall_length"]
|
||||
: [],
|
||||
rejected_parameter_hypotheses: simpleCylinder
|
||||
? ["coaxial_inner_diameter", "duplicate_transverse_envelope_parameters"]
|
||||
: [],
|
||||
observed_parameter_names: Object.fromEntries(
|
||||
[...group.parameterNames.entries()].sort(),
|
||||
),
|
||||
observed_anchor_modes: Object.fromEntries(
|
||||
[...group.anchors.entries()].sort(),
|
||||
),
|
||||
},
|
||||
validation: {
|
||||
independent_replay_validated_case_count: new Set(group.cases).size,
|
||||
source_hashes: [...new Set(group.cases)].sort(),
|
||||
edit_contract_pass_required: true,
|
||||
positive_heldout_ab_required: true,
|
||||
heldout_ab_status: "not_run",
|
||||
},
|
||||
};
|
||||
});
|
||||
const payload = {
|
||||
schema_version: "1.0",
|
||||
library_kind: "execution_validated_trajectory_candidates",
|
||||
status: candidates.length ? "candidates_only" : "empty",
|
||||
policy: {
|
||||
router_consumable: false,
|
||||
automatic_publish_allowed: false,
|
||||
minimum_independent_cases: 3,
|
||||
positive_heldout_ab_required: true,
|
||||
},
|
||||
validated_trajectory_count: trajectories.length,
|
||||
candidate_count: candidates.length,
|
||||
candidates,
|
||||
};
|
||||
const output = path.join(TRAJECTORY_DIR, "candidate-experience.json");
|
||||
writeJson(output, payload);
|
||||
return { output, payload };
|
||||
}
|
||||
|
||||
function sanitizedMethods(candidatePath) {
|
||||
if (!candidatePath) return [];
|
||||
return (readJson(candidatePath).candidates || []).map((candidate) => ({
|
||||
id: candidate.id,
|
||||
kind: candidate.kind,
|
||||
applicability: candidate.applicability,
|
||||
method: candidate.method,
|
||||
}));
|
||||
}
|
||||
|
||||
function benchmarkScore(rows) {
|
||||
const passed = rows.filter((row) => row.state === "replay_validated").length;
|
||||
return {
|
||||
case_count: rows.length,
|
||||
replay_validated_count: passed,
|
||||
pass_rate: rows.length ? passed / rows.length : 0,
|
||||
};
|
||||
}
|
||||
|
||||
async function runBenchmark(args) {
|
||||
const split = String(args.split || "validation");
|
||||
if (!["validation", "test"].includes(split)) {
|
||||
throw new Error("Benchmark split must be validation or test");
|
||||
}
|
||||
const limit = Math.max(1, Number(args.limit || 10));
|
||||
const maxAttempts = Math.max(1, Number(args.attempts || 3));
|
||||
const maxFaces = Math.max(1, Number(args["max-faces"] || 24));
|
||||
const requestedFamily = String(args.family || "").trim();
|
||||
const manifest = fs.existsSync(SPLIT_PATH) ? readJson(SPLIT_PATH) : initSplits();
|
||||
const jobs = (manifest.splits?.[split] || [])
|
||||
.filter((job) => !requestedFamily || job.family === requestedFamily)
|
||||
.filter((job) => job.face_count <= maxFaces)
|
||||
.slice(0, limit);
|
||||
const provider = loadProvider(args.provider, args.model);
|
||||
const candidatePath = String(
|
||||
args.experience ||
|
||||
path.join(TRAJECTORY_DIR, "candidate-experience.json"),
|
||||
);
|
||||
const experience = sanitizedMethods(candidatePath);
|
||||
const benchmarkRoot = path.join(
|
||||
TRAJECTORY_DIR,
|
||||
"benchmarks",
|
||||
`${split}-${requestedFamily || "all"}`,
|
||||
);
|
||||
const baseline = [];
|
||||
const treatment = [];
|
||||
for (const job of jobs) {
|
||||
baseline.push(
|
||||
await runOne({ ...job, split }, provider, maxAttempts, {
|
||||
attemptsRoot: path.join(benchmarkRoot, "baseline"),
|
||||
}),
|
||||
);
|
||||
treatment.push(
|
||||
await runOne({ ...job, split }, provider, maxAttempts, {
|
||||
attemptsRoot: path.join(benchmarkRoot, "experience"),
|
||||
experience,
|
||||
}),
|
||||
);
|
||||
}
|
||||
const baselineScore = benchmarkScore(baseline);
|
||||
const treatmentScore = benchmarkScore(treatment);
|
||||
const report = {
|
||||
schema_version: "1.0",
|
||||
report_kind: "heldout_trajectory_ab",
|
||||
split,
|
||||
family: requestedFamily || "all",
|
||||
provider: provider.providerId,
|
||||
model: provider.model,
|
||||
candidate_method_ids: experience.map((method) => method.id),
|
||||
baseline: baselineScore,
|
||||
experience: treatmentScore,
|
||||
pass_rate_delta: treatmentScore.pass_rate - baselineScore.pass_rate,
|
||||
positive: treatmentScore.pass_rate > baselineScore.pass_rate,
|
||||
promotion_eligible:
|
||||
jobs.length >= 10 && treatmentScore.pass_rate > baselineScore.pass_rate,
|
||||
case_results: jobs.map((job, index) => ({
|
||||
case_id: job.id,
|
||||
baseline_state: baseline[index].state,
|
||||
experience_state: treatment[index].state,
|
||||
})),
|
||||
};
|
||||
writeJson(path.join(benchmarkRoot, "report.json"), report);
|
||||
return report;
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const args = argsObject(process.argv.slice(2));
|
||||
const command = args._[0];
|
||||
if (command === "init") {
|
||||
const payload = initSplits();
|
||||
process.stdout.write(
|
||||
`${JSON.stringify({ split_path: SPLIT_PATH, policy: payload.policy }, null, 2)}\n`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (command === "summary") {
|
||||
const report = summarize();
|
||||
writeJson(path.join(TRAJECTORY_DIR, "summary.json"), report);
|
||||
process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
|
||||
return;
|
||||
}
|
||||
if (command === "distill") {
|
||||
const result = distillValidatedTrajectories();
|
||||
process.stdout.write(
|
||||
`${JSON.stringify(
|
||||
{
|
||||
output: result.output,
|
||||
validated_trajectory_count: result.payload.validated_trajectory_count,
|
||||
candidate_count: result.payload.candidate_count,
|
||||
router_consumable: result.payload.policy.router_consumable,
|
||||
},
|
||||
null,
|
||||
2,
|
||||
)}\n`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (command === "benchmark") {
|
||||
const report = await runBenchmark(args);
|
||||
process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
|
||||
return;
|
||||
}
|
||||
if (command !== "run") {
|
||||
throw new Error(
|
||||
"Usage: trajectory_pipeline.mjs init|run|summary|distill|benchmark",
|
||||
);
|
||||
}
|
||||
const split = String(args.split || "train");
|
||||
const limit = Math.max(1, Number(args.limit || 5));
|
||||
const maxAttempts = Math.max(1, Number(args.attempts || 3));
|
||||
const maxFaces = Math.max(1, Number(args["max-faces"] || 24));
|
||||
const requestedFamily = String(args.family || "").trim();
|
||||
const rerun = Boolean(args.rerun);
|
||||
const manifest = fs.existsSync(SPLIT_PATH) ? readJson(SPLIT_PATH) : initSplits();
|
||||
const jobs = (manifest.splits?.[split] || [])
|
||||
.filter((job) => !requestedFamily || job.family === requestedFamily)
|
||||
.filter((job) => job.face_count <= maxFaces)
|
||||
.filter(
|
||||
(job) =>
|
||||
rerun ||
|
||||
!fs.existsSync(
|
||||
path.join(
|
||||
TRAJECTORY_DIR,
|
||||
"attempts",
|
||||
job.id.slice(0, 16),
|
||||
"trajectory.json",
|
||||
),
|
||||
),
|
||||
)
|
||||
.slice(0, limit);
|
||||
const provider = loadProvider(args.provider, args.model);
|
||||
const results = [];
|
||||
for (const job of jobs) {
|
||||
const result = await runOne({ ...job, split }, provider, maxAttempts);
|
||||
results.push({
|
||||
case_id: job.id,
|
||||
family: job.family,
|
||||
face_count: job.face_count,
|
||||
state: result.state,
|
||||
});
|
||||
process.stdout.write(`${JSON.stringify(results.at(-1))}\n`);
|
||||
}
|
||||
const report = summarize();
|
||||
writeJson(path.join(TRAJECTORY_DIR, "summary.json"), report);
|
||||
process.stdout.write(`${JSON.stringify({ completed: results.length, report }, null, 2)}\n`);
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
process.stderr.write(`${error instanceof Error ? error.stack : String(error)}\n`);
|
||||
process.exitCode = 1;
|
||||
});
|
||||
+11
@@ -63,6 +63,17 @@ 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.
|
||||
|
||||
The library also supports:
|
||||
|
||||
- `parameter_naming`: canonical editable names inferred from repeated B-Rep
|
||||
evidence;
|
||||
- `edit_strategy`: expected-change and non-target-preservation behavior for a
|
||||
semantic parameter.
|
||||
|
||||
Evidence support alone never makes a method router-consumable. Promotion
|
||||
requires a positive held-out A/B result on independent source cases and full
|
||||
edit-contract success for that method.
|
||||
|
||||
## Reconstruction grammar
|
||||
|
||||
`reconstruction_grammar` is the executable bridge between generalized
|
||||
|
||||
@@ -30,7 +30,17 @@ Write `runs/review/experience-draft.json` with this shape:
|
||||
|
||||
Allowed kinds are `feature_motif`, `constraint`, `design_rule`,
|
||||
`validation_rule`, `failure_repair`, `dimensionless_distribution`, and
|
||||
`reconstruction_grammar`.
|
||||
`reconstruction_grammar`, plus:
|
||||
|
||||
- `parameter_naming`: maps repeated final-B-Rep evidence to a canonical
|
||||
editable name such as `plate_thickness`, `hole_spacing`,
|
||||
`bolt_circle_diameter`, or `rib_count`;
|
||||
- `edit_strategy`: defines the expected target change and preservation contract
|
||||
for a named semantic parameter.
|
||||
|
||||
These proposal kinds use `evidence_query.required_parameters`. A name is a
|
||||
reconstruction-system convention, never a claim that the STEP contained the
|
||||
source parameter.
|
||||
Distribution proposals also provide `observation_name`; the publisher computes
|
||||
the distribution from private evidence and never accepts values from the draft.
|
||||
|
||||
@@ -86,6 +96,9 @@ Follow these rules:
|
||||
- Avoid dimensions, coordinates, source identifiers, face references, and
|
||||
numeric literals in prose.
|
||||
- Do not infer build order or causal failure mechanisms from final geometry.
|
||||
- Do not present inferred parameter names as recovered source parameters.
|
||||
- Prefer specific editable names over generic span roles only when repeated
|
||||
geometric evidence supports the distinction.
|
||||
- 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
|
||||
|
||||
@@ -65,7 +65,17 @@ ALLOWED_NUMERIC_KEYS = {
|
||||
"remaining_support",
|
||||
"required_confidence",
|
||||
"llm_proposal_count",
|
||||
"independent_case_count",
|
||||
"edit_contract_pass_rate",
|
||||
"baseline_score_delta",
|
||||
}
|
||||
ALLOWED_NUMERIC_SUFFIXES = (
|
||||
"_count",
|
||||
"_rate",
|
||||
"_ratio",
|
||||
"_delta",
|
||||
"_support",
|
||||
)
|
||||
|
||||
|
||||
def utc_now() -> str:
|
||||
@@ -209,7 +219,12 @@ def normalize_case(payload: dict[str, Any]) -> dict[str, Any]:
|
||||
experience = payload.get("experience") if isinstance(payload.get("experience"), dict) else {}
|
||||
provenance = payload.get("provenance") if isinstance(payload.get("provenance"), dict) else {}
|
||||
supplied_id = provenance.get("source_sha256") or payload.get("case_id")
|
||||
case_id = token(supplied_id, content_hash(payload))
|
||||
raw_id = str(supplied_id or "").strip().lower()
|
||||
case_id = (
|
||||
raw_id
|
||||
if re.fullmatch(r"[a-f0-9]{64}|[a-z][a-z0-9_.+-]*", raw_id)
|
||||
else content_hash(payload)
|
||||
)
|
||||
family = token(design_ir.get("part_family") or payload.get("part_family"))
|
||||
if family.endswith("_candidate"):
|
||||
family = family.removesuffix("_candidate")
|
||||
@@ -287,14 +302,67 @@ def normalize_case(payload: dict[str, Any]) -> dict[str, Any]:
|
||||
}
|
||||
)
|
||||
|
||||
semantic_parameters: list[dict[str, Any]] = []
|
||||
raw_parameters = design_ir.get("semantic_parameter_candidates", [])
|
||||
if isinstance(raw_parameters, list):
|
||||
for raw in raw_parameters:
|
||||
if not isinstance(raw, dict):
|
||||
continue
|
||||
name = token(raw.get("name"))
|
||||
if name == "unknown":
|
||||
continue
|
||||
edit_contract = (
|
||||
raw.get("edit_contract")
|
||||
if isinstance(raw.get("edit_contract"), dict)
|
||||
else {}
|
||||
)
|
||||
confidence = raw.get("confidence", 0.0)
|
||||
if not isinstance(confidence, (int, float)):
|
||||
confidence = 0.0
|
||||
semantic_parameters.append(
|
||||
{
|
||||
"name": name,
|
||||
"unit": token(raw.get("unit")),
|
||||
"semantic_aliases": list_tokens(raw.get("semantic_aliases")),
|
||||
"affects_feature_roles": list_tokens(
|
||||
raw.get("affects_feature_roles")
|
||||
),
|
||||
"expected_change": token(edit_contract.get("expected_change")),
|
||||
"confidence_class": (
|
||||
"high"
|
||||
if float(confidence) >= 0.8
|
||||
else "medium"
|
||||
if float(confidence) >= 0.6
|
||||
else "low"
|
||||
),
|
||||
"epistemic_status": token(raw.get("epistemic_status")),
|
||||
}
|
||||
)
|
||||
|
||||
geometry = (
|
||||
payload.get("geometry_evidence")
|
||||
if isinstance(payload.get("geometry_evidence"), dict)
|
||||
else {}
|
||||
)
|
||||
return {
|
||||
"case_id": case_id,
|
||||
"extractor_version": str(payload.get("extractor_version") or "0"),
|
||||
"family": family,
|
||||
"features": sorted(features),
|
||||
"relations": sorted(relations),
|
||||
"rules": rules,
|
||||
"validations": validations,
|
||||
"normalized_observations": observations,
|
||||
"semantic_parameters": semantic_parameters,
|
||||
"reconstruction_eligible": bool(
|
||||
geometry.get(
|
||||
"reconstruction_eligible",
|
||||
geometry.get("valid", True)
|
||||
and int(geometry.get("solid_count", 1)) == 1
|
||||
and float(geometry.get("volume", 1.0)) > 0.0,
|
||||
)
|
||||
),
|
||||
"rejection_reasons": list_tokens(geometry.get("rejection_reasons")),
|
||||
"reconstruction": normalize_reconstruction(
|
||||
design_ir.get("reconstruction_evidence")
|
||||
),
|
||||
@@ -311,11 +379,18 @@ def load_cases(root: Path) -> tuple[list[dict[str, Any]], dict[str, int]]:
|
||||
if not isinstance(payload, dict):
|
||||
raise ValueError("root must be an object")
|
||||
case = normalize_case(payload)
|
||||
if not case["reconstruction_eligible"]:
|
||||
rejected += 1
|
||||
continue
|
||||
if not case["features"] and not case["rules"] and not case["relations"]:
|
||||
rejected += 1
|
||||
continue
|
||||
if case["case_id"] in cases:
|
||||
duplicates += 1
|
||||
if case["extractor_version"] > cases[case["case_id"]][
|
||||
"extractor_version"
|
||||
]:
|
||||
cases[case["case_id"]] = case
|
||||
continue
|
||||
cases[case["case_id"]] = case
|
||||
except (OSError, json.JSONDecodeError, ValueError, TypeError):
|
||||
@@ -555,7 +630,9 @@ def audit_value(value: Any, path: tuple[str, ...], errors: list[str]) -> None:
|
||||
audit_value(child, path + (str(index),), errors)
|
||||
elif isinstance(value, (int, float)) and not isinstance(value, bool):
|
||||
leaf = path[-1] if path else ""
|
||||
if leaf not in ALLOWED_NUMERIC_KEYS:
|
||||
if leaf not in ALLOWED_NUMERIC_KEYS and not leaf.endswith(
|
||||
ALLOWED_NUMERIC_SUFFIXES
|
||||
):
|
||||
errors.append(f"{'.'.join(path)}: numeric value is not generalized metadata")
|
||||
|
||||
|
||||
@@ -646,7 +723,7 @@ def extract_folder(input_dir: Path, output_dir: Path) -> dict[str, Any]:
|
||||
digest = payload.get("provenance", {}).get("source_sha256")
|
||||
if (
|
||||
isinstance(digest, str)
|
||||
and payload.get("extractor_version") == "3.0"
|
||||
and payload.get("extractor_version") == "4.0"
|
||||
):
|
||||
known_hashes[digest] = path
|
||||
except (OSError, json.JSONDecodeError, AttributeError):
|
||||
@@ -711,6 +788,18 @@ def prepare_semantic_batch(
|
||||
"candidate_rules": case["rules"],
|
||||
"validation_targets": case["validations"],
|
||||
"reconstruction_evidence": case["reconstruction"],
|
||||
"semantic_parameter_roles": [
|
||||
{
|
||||
"name": item["name"],
|
||||
"unit": item["unit"],
|
||||
"semantic_aliases": item["semantic_aliases"],
|
||||
"affects_feature_roles": item["affects_feature_roles"],
|
||||
"expected_change": item["expected_change"],
|
||||
"confidence_class": item["confidence_class"],
|
||||
"epistemic_status": item["epistemic_status"],
|
||||
}
|
||||
for item in case["semantic_parameters"]
|
||||
],
|
||||
"dimensionless_observation_roles": [
|
||||
{
|
||||
"name": item["name"],
|
||||
@@ -753,6 +842,8 @@ def prepare_semantic_batch(
|
||||
"validation_rule",
|
||||
"failure_repair",
|
||||
"dimensionless_distribution",
|
||||
"parameter_naming",
|
||||
"edit_strategy",
|
||||
"reconstruction_grammar",
|
||||
],
|
||||
},
|
||||
@@ -770,6 +861,8 @@ def normalize_proposals(payload: dict[str, Any]) -> list[dict[str, Any]]:
|
||||
"validation_rule",
|
||||
"failure_repair",
|
||||
"dimensionless_distribution",
|
||||
"parameter_naming",
|
||||
"edit_strategy",
|
||||
"reconstruction_grammar",
|
||||
}
|
||||
for index, raw in enumerate(payload.get("proposals", [])):
|
||||
@@ -785,6 +878,7 @@ def normalize_proposals(payload: dict[str, Any]) -> list[dict[str, Any]]:
|
||||
)
|
||||
required_features = list_tokens(evidence.get("required_features"))
|
||||
required_relations = list_tokens(evidence.get("required_relations"))
|
||||
required_parameters = list_tokens(evidence.get("required_parameters"))
|
||||
context_features = list_tokens(
|
||||
evidence.get("context_features") or required_features
|
||||
)
|
||||
@@ -800,6 +894,7 @@ def normalize_proposals(payload: dict[str, Any]) -> list[dict[str, Any]]:
|
||||
if (
|
||||
not required_features
|
||||
and not required_relations
|
||||
and not required_parameters
|
||||
and observation_name == "unknown"
|
||||
):
|
||||
raise ValueError(f"proposal {proposal_id} has no evidence query")
|
||||
@@ -814,6 +909,7 @@ def normalize_proposals(payload: dict[str, Any]) -> list[dict[str, Any]]:
|
||||
"evidence_query": {
|
||||
"required_features": required_features,
|
||||
"required_relations": required_relations,
|
||||
"required_parameters": required_parameters,
|
||||
"context_features": context_features,
|
||||
},
|
||||
"guidance": guidance,
|
||||
@@ -833,6 +929,24 @@ def normalize_proposals(payload: dict[str, Any]) -> list[dict[str, Any]]:
|
||||
f"proposal {proposal_id} has an incomplete reconstruction grammar"
|
||||
)
|
||||
proposal["reconstruction_grammar"] = grammar
|
||||
if kind in {"parameter_naming", "edit_strategy"}:
|
||||
canonical_parameter = token(raw.get("canonical_parameter"))
|
||||
if canonical_parameter == "unknown":
|
||||
raise ValueError(
|
||||
f"proposal {proposal_id} requires canonical_parameter"
|
||||
)
|
||||
proposal["canonical_parameter"] = canonical_parameter
|
||||
proposal["semantic_aliases"] = list_tokens(
|
||||
raw.get("semantic_aliases")
|
||||
)
|
||||
if kind == "edit_strategy":
|
||||
expected_change = token(raw.get("expected_change"))
|
||||
if expected_change == "unknown":
|
||||
raise ValueError(
|
||||
f"proposal {proposal_id} requires expected_change"
|
||||
)
|
||||
proposal["expected_change"] = expected_change
|
||||
proposal["preserve_roles"] = list_tokens(raw.get("preserve_roles"))
|
||||
for key in ("check", "repair", "failure_signature"):
|
||||
value = string_without_instance_numbers(raw.get(key))
|
||||
if value is not None:
|
||||
@@ -849,10 +963,20 @@ def build_reviewed_library(
|
||||
min_support: int,
|
||||
min_confidence: float,
|
||||
stats: dict[str, int],
|
||||
validation_report: dict[str, Any] | None = None,
|
||||
) -> dict[str, Any]:
|
||||
promoted: list[dict[str, Any]] = []
|
||||
candidates: list[dict[str, Any]] = []
|
||||
family_counts = Counter(case["family"] for case in cases)
|
||||
validated_methods = {
|
||||
token(item.get("experience_id")): item
|
||||
for item in (validation_report or {}).get("method_results", [])
|
||||
if isinstance(item, dict)
|
||||
and item.get("passed") is True
|
||||
and int(item.get("independent_case_count", 0)) >= 3
|
||||
and float(item.get("edit_contract_pass_rate", 0.0)) >= 1.0
|
||||
and float(item.get("baseline_score_delta", 0.0)) > 0.0
|
||||
}
|
||||
|
||||
def supports_reconstruction_grammar(
|
||||
case: dict[str, Any], grammar: dict[str, Any]
|
||||
@@ -901,6 +1025,9 @@ def build_reviewed_library(
|
||||
required_relations = set(
|
||||
proposal["evidence_query"]["required_relations"]
|
||||
)
|
||||
required_parameters = set(
|
||||
proposal["evidence_query"].get("required_parameters", [])
|
||||
)
|
||||
context_features = set(
|
||||
proposal["evidence_query"].get("context_features", required_features)
|
||||
)
|
||||
@@ -941,7 +1068,22 @@ def build_reviewed_library(
|
||||
"p90": round(percentile(values, 0.90), 6),
|
||||
"max": round(max(values), 6),
|
||||
}
|
||||
if eligible(support, item["confidence"], min_support, min_confidence):
|
||||
evidence_eligible = eligible(
|
||||
support, item["confidence"], min_support, min_confidence
|
||||
)
|
||||
validation = validated_methods.get(item["id"])
|
||||
if evidence_eligible and validation is not None:
|
||||
item["validation"] = {
|
||||
"independent_case_count": int(
|
||||
validation["independent_case_count"]
|
||||
),
|
||||
"edit_contract_pass_rate": float(
|
||||
validation["edit_contract_pass_rate"]
|
||||
),
|
||||
"baseline_score_delta": float(
|
||||
validation["baseline_score_delta"]
|
||||
),
|
||||
}
|
||||
promoted.append(item)
|
||||
else:
|
||||
item.pop("distribution", None)
|
||||
@@ -951,6 +1093,11 @@ def build_reviewed_library(
|
||||
"required_support": min_support,
|
||||
"remaining_support": max(0, min_support - support),
|
||||
"required_confidence": min_confidence,
|
||||
"validation_state": (
|
||||
"validated_positive"
|
||||
if validation is not None
|
||||
else "awaiting_positive_heldout_ab"
|
||||
),
|
||||
"consumer_policy": "visible_for_review_but_not_available_to_cad_router",
|
||||
}
|
||||
)
|
||||
@@ -966,6 +1113,9 @@ def build_reviewed_library(
|
||||
for case in contextual
|
||||
if required_features.issubset(case["features"])
|
||||
and required_relations.issubset(case["relations"])
|
||||
and required_parameters.issubset(
|
||||
{item["name"] for item in case.get("semantic_parameters", [])}
|
||||
)
|
||||
and (
|
||||
proposal["kind"] != "reconstruction_grammar"
|
||||
or supports_reconstruction_grammar(
|
||||
@@ -982,6 +1132,7 @@ def build_reviewed_library(
|
||||
"when": {
|
||||
"features": sorted(required_features),
|
||||
"relations": sorted(required_relations),
|
||||
"semantic_parameters": sorted(required_parameters),
|
||||
},
|
||||
"guidance": proposal["guidance"],
|
||||
"semantic_rationale": proposal["semantic_rationale"],
|
||||
@@ -993,7 +1144,24 @@ def build_reviewed_library(
|
||||
item[key] = proposal[key]
|
||||
if proposal["kind"] == "reconstruction_grammar":
|
||||
item["reconstruction_grammar"] = proposal["reconstruction_grammar"]
|
||||
if eligible(support, confidence, min_support, min_confidence):
|
||||
if proposal["kind"] in {"parameter_naming", "edit_strategy"}:
|
||||
item["canonical_parameter"] = proposal["canonical_parameter"]
|
||||
item["semantic_aliases"] = proposal["semantic_aliases"]
|
||||
if proposal["kind"] == "edit_strategy":
|
||||
item["expected_change"] = proposal["expected_change"]
|
||||
item["preserve_roles"] = proposal["preserve_roles"]
|
||||
evidence_eligible = eligible(
|
||||
support, confidence, min_support, min_confidence
|
||||
)
|
||||
validation = validated_methods.get(item["id"])
|
||||
if evidence_eligible and validation is not None:
|
||||
item["validation"] = {
|
||||
"independent_case_count": int(validation["independent_case_count"]),
|
||||
"edit_contract_pass_rate": float(
|
||||
validation["edit_contract_pass_rate"]
|
||||
),
|
||||
"baseline_score_delta": float(validation["baseline_score_delta"]),
|
||||
}
|
||||
promoted.append(item)
|
||||
else:
|
||||
item.update(
|
||||
@@ -1002,6 +1170,11 @@ def build_reviewed_library(
|
||||
"required_support": min_support,
|
||||
"remaining_support": max(0, min_support - support),
|
||||
"required_confidence": min_confidence,
|
||||
"validation_state": (
|
||||
"validated_positive"
|
||||
if validation is not None
|
||||
else "awaiting_positive_heldout_ab"
|
||||
),
|
||||
"consumer_policy": "visible_for_review_but_not_available_to_cad_router",
|
||||
}
|
||||
)
|
||||
@@ -1026,7 +1199,8 @@ def build_reviewed_library(
|
||||
"single_case_promotion_allowed": False,
|
||||
"llm_semantic_review_required": True,
|
||||
"draft_evidence_verified": True,
|
||||
"router_consumable": True,
|
||||
"positive_heldout_ab_required": True,
|
||||
"router_consumable": bool(promoted),
|
||||
"minimum_support": min_support,
|
||||
"minimum_confidence": min_confidence,
|
||||
},
|
||||
@@ -1115,6 +1289,11 @@ def build_parser() -> argparse.ArgumentParser:
|
||||
)
|
||||
publish_parser.add_argument("--min-support", type=int, default=20)
|
||||
publish_parser.add_argument("--min-confidence", type=float, default=0.8)
|
||||
publish_parser.add_argument(
|
||||
"--validation-report",
|
||||
type=Path,
|
||||
help="Held-out per-method A/B and edit-contract validation report.",
|
||||
)
|
||||
|
||||
distill_parser = subparsers.add_parser("distill")
|
||||
distill_parser.add_argument(
|
||||
@@ -1130,6 +1309,11 @@ def build_parser() -> argparse.ArgumentParser:
|
||||
)
|
||||
distill_parser.add_argument("--min-support", type=int, default=20)
|
||||
distill_parser.add_argument("--min-confidence", type=float, default=0.8)
|
||||
distill_parser.add_argument(
|
||||
"--validation-report",
|
||||
type=Path,
|
||||
help="Held-out per-method A/B and edit-contract validation report.",
|
||||
)
|
||||
|
||||
return parser
|
||||
|
||||
@@ -1205,12 +1389,18 @@ def main(argv: list[str] | None = None) -> int:
|
||||
draft_path = args.draft
|
||||
draft_payload = json.loads(draft_path.read_text(encoding="utf-8"))
|
||||
proposals = normalize_proposals(draft_payload)
|
||||
validation_report = None
|
||||
if args.validation_report is not None:
|
||||
validation_report = json.loads(
|
||||
args.validation_report.read_text(encoding="utf-8")
|
||||
)
|
||||
library = build_reviewed_library(
|
||||
cases,
|
||||
proposals,
|
||||
args.min_support,
|
||||
args.min_confidence,
|
||||
stats,
|
||||
validation_report,
|
||||
)
|
||||
errors = audit_library(library)
|
||||
if errors:
|
||||
|
||||
@@ -11,7 +11,7 @@ from collections import Counter, defaultdict
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
EXTRACTOR_VERSION = "3.0"
|
||||
EXTRACTOR_VERSION = "4.0"
|
||||
|
||||
|
||||
def _rounded(value: float) -> float:
|
||||
@@ -71,6 +71,184 @@ def _cardinality_class(count: int) -> str:
|
||||
return "dense"
|
||||
|
||||
|
||||
def _parameter_candidate(
|
||||
name: str,
|
||||
value: float | int,
|
||||
unit: str,
|
||||
confidence: float,
|
||||
affects: list[str],
|
||||
expected_change: str,
|
||||
evidence_kind: str,
|
||||
aliases: list[str] | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""Create an explicitly inferred, editable semantic parameter candidate."""
|
||||
return {
|
||||
"name": name,
|
||||
"value": value if unit == "count" else _rounded(float(value)),
|
||||
"unit": unit,
|
||||
"editable_candidate": True,
|
||||
"epistemic_status": "inferred_from_final_brep",
|
||||
"confidence": _rounded(confidence),
|
||||
"semantic_aliases": sorted(set(aliases or [])),
|
||||
"affects_feature_roles": sorted(set(affects)),
|
||||
"edit_contract": {
|
||||
"expected_change": expected_change,
|
||||
"preserve_roles": ["part_center", "primary_reference_frame"],
|
||||
},
|
||||
"source_evidence": {"kind": evidence_kind},
|
||||
}
|
||||
|
||||
|
||||
def _semantic_parameter_candidates(
|
||||
surfaces: list[dict[str, Any]],
|
||||
bbox_center: list[float],
|
||||
bbox_size: list[float],
|
||||
features: list[dict[str, Any]],
|
||||
semantic_summary: dict[str, Any],
|
||||
) -> list[dict[str, Any]]:
|
||||
"""Name editable dimensions without pretending that STEP stored parameters."""
|
||||
if not bbox_size or max(bbox_size) <= 1e-9:
|
||||
return []
|
||||
feature_roles = {
|
||||
str(item.get("type"))
|
||||
for item in features
|
||||
if isinstance(item, dict) and item.get("type")
|
||||
}
|
||||
ordered_axes = sorted(
|
||||
zip(("x", "y", "z"), bbox_size), key=lambda item: item[1], reverse=True
|
||||
)
|
||||
candidates = [
|
||||
_parameter_candidate(
|
||||
"overall_length",
|
||||
ordered_axes[0][1],
|
||||
"mm",
|
||||
0.98,
|
||||
["primary_envelope"],
|
||||
"overall_length_changes",
|
||||
f"bounding_box_{ordered_axes[0][0]}_span",
|
||||
),
|
||||
_parameter_candidate(
|
||||
"overall_width",
|
||||
ordered_axes[1][1],
|
||||
"mm",
|
||||
0.92,
|
||||
["primary_envelope"],
|
||||
"overall_width_changes",
|
||||
f"bounding_box_{ordered_axes[1][0]}_span",
|
||||
),
|
||||
_parameter_candidate(
|
||||
"overall_thickness",
|
||||
ordered_axes[2][1],
|
||||
"mm",
|
||||
0.88,
|
||||
["primary_envelope"],
|
||||
"overall_thickness_changes",
|
||||
f"bounding_box_{ordered_axes[2][0]}_span",
|
||||
["plate_thickness"]
|
||||
if "plate_like_body" in feature_roles
|
||||
else ["minimum_envelope_span"],
|
||||
),
|
||||
]
|
||||
|
||||
dominant_axis = str(semantic_summary.get("dominant_axis", "z"))
|
||||
transverse_axes = {"x": (1, 2), "y": (0, 2), "z": (0, 1)}[dominant_axis]
|
||||
scale = max(bbox_size)
|
||||
center_tolerance = max(scale * 0.005, 1e-4)
|
||||
repeated_groups: dict[float, list[dict[str, Any]]] = defaultdict(list)
|
||||
coaxial: list[dict[str, Any]] = []
|
||||
for surface in surfaces:
|
||||
if (
|
||||
surface.get("type") != "cylinder"
|
||||
or _canonical_axis(surface["axis"]) != dominant_axis
|
||||
):
|
||||
continue
|
||||
radial_offset = _radial_offset(
|
||||
surface["location"], bbox_center, dominant_axis
|
||||
)
|
||||
if radial_offset <= center_tolerance:
|
||||
coaxial.append(surface)
|
||||
else:
|
||||
repeated_groups[round(float(surface["radius"]), 4)].append(surface)
|
||||
|
||||
repeated = max(repeated_groups.values(), key=len) if repeated_groups else []
|
||||
unique_centers = {
|
||||
(
|
||||
round(float(item["location"][transverse_axes[0]]), 4),
|
||||
round(float(item["location"][transverse_axes[1]]), 4),
|
||||
)
|
||||
for item in repeated
|
||||
}
|
||||
if len(unique_centers) >= 3:
|
||||
member_radius = float(repeated[0]["radius"])
|
||||
offsets = [
|
||||
math.hypot(
|
||||
first - bbox_center[transverse_axes[0]],
|
||||
second - bbox_center[transverse_axes[1]],
|
||||
)
|
||||
for first, second in unique_centers
|
||||
]
|
||||
mean_offset = sum(offsets) / len(offsets)
|
||||
circular = (
|
||||
mean_offset > center_tolerance
|
||||
and (max(offsets) - min(offsets)) / mean_offset <= 0.08
|
||||
)
|
||||
candidates.extend(
|
||||
[
|
||||
_parameter_candidate(
|
||||
"repeated_feature_count",
|
||||
len(unique_centers),
|
||||
"count",
|
||||
0.72,
|
||||
["repeated_cylindrical_feature_pattern"],
|
||||
"pattern_member_count_changes",
|
||||
"unique_equal_radius_parallel_cylinder_axes",
|
||||
["hole_count", "boss_count"],
|
||||
),
|
||||
_parameter_candidate(
|
||||
"repeated_feature_diameter",
|
||||
member_radius * 2.0,
|
||||
"mm",
|
||||
0.68,
|
||||
["repeated_cylindrical_feature_pattern"],
|
||||
"pattern_member_diameter_changes",
|
||||
"equal_radius_cylindrical_surfaces",
|
||||
["hole_diameter", "boss_diameter"],
|
||||
),
|
||||
]
|
||||
)
|
||||
if circular:
|
||||
candidates.append(
|
||||
_parameter_candidate(
|
||||
"bolt_circle_diameter",
|
||||
mean_offset * 2.0,
|
||||
"mm",
|
||||
0.82,
|
||||
["circular_equal_radius_pattern"],
|
||||
"pattern_radius_changes",
|
||||
"equal_radial_offset_cylinder_axes",
|
||||
["hole_spacing", "pattern_diameter"],
|
||||
)
|
||||
)
|
||||
|
||||
coaxial_radii = sorted(
|
||||
{round(float(item["radius"]), 6) for item in coaxial}
|
||||
)
|
||||
if coaxial_radii:
|
||||
candidates.append(
|
||||
_parameter_candidate(
|
||||
"coaxial_inner_diameter",
|
||||
coaxial_radii[0] * 2.0,
|
||||
"mm",
|
||||
0.58,
|
||||
["coaxial_cylindrical_stack"],
|
||||
"coaxial_inner_diameter_changes",
|
||||
"smallest_centered_cylindrical_surface",
|
||||
["bore_diameter", "shaft_diameter"],
|
||||
)
|
||||
)
|
||||
return candidates
|
||||
|
||||
|
||||
def _surface_record(face: Any, index: int) -> dict[str, Any]:
|
||||
from OCP.BRepAdaptor import BRepAdaptor_Surface
|
||||
from OCP.GeomAbs import (
|
||||
@@ -702,8 +880,18 @@ def extract_step_case(source: Path) -> dict[str, Any]:
|
||||
reconstruction_evidence = _reconstruction_evidence(
|
||||
surfaces, features, constraints, semantic_summary
|
||||
)
|
||||
semantic_parameters = _semantic_parameter_candidates(
|
||||
surfaces,
|
||||
bbox_center,
|
||||
bbox_size,
|
||||
features,
|
||||
semantic_summary,
|
||||
)
|
||||
digest = hashlib.sha256(source_bytes).hexdigest()
|
||||
normalized_observations = semantic_summary.pop("normalized_observations", [])
|
||||
solid_count = len(shape.solids())
|
||||
volume = _rounded(shape.volume)
|
||||
usable_solid = bool(shape.is_valid) and solid_count > 0 and volume > 0.0
|
||||
return {
|
||||
"schema_version": "2.0",
|
||||
"extractor_version": EXTRACTOR_VERSION,
|
||||
@@ -715,10 +903,22 @@ def extract_step_case(source: Path) -> dict[str, Any]:
|
||||
"source_format": "step",
|
||||
},
|
||||
"geometry_evidence": {
|
||||
"valid": bool(shape.is_valid),
|
||||
"solid_count": len(shape.solids()),
|
||||
"kernel_valid": bool(shape.is_valid),
|
||||
"valid": usable_solid,
|
||||
"reconstruction_eligible": usable_solid and solid_count == 1,
|
||||
"rejection_reasons": [
|
||||
reason
|
||||
for condition, reason in (
|
||||
(not bool(shape.is_valid), "kernel_invalid"),
|
||||
(solid_count == 0, "no_closed_solid"),
|
||||
(volume <= 0.0, "zero_volume"),
|
||||
(solid_count > 1, "multi_solid_requires_segmentation"),
|
||||
)
|
||||
if condition
|
||||
],
|
||||
"solid_count": solid_count,
|
||||
"face_count": len(shape.faces()),
|
||||
"volume": _rounded(shape.volume),
|
||||
"volume": volume,
|
||||
"bounding_box": {
|
||||
"min": bbox_min,
|
||||
"max": bbox_max,
|
||||
@@ -729,9 +929,11 @@ def extract_step_case(source: Path) -> dict[str, Any]:
|
||||
"analytic_surfaces": surfaces,
|
||||
},
|
||||
"design_ir": {
|
||||
"epistemic_status": "inferred_design_hypothesis_not_source_history",
|
||||
"part_family": family,
|
||||
"features": features,
|
||||
"constraints": constraints,
|
||||
"semantic_parameter_candidates": semantic_parameters,
|
||||
"normalized_observations": normalized_observations,
|
||||
"semantic_summary": semantic_summary,
|
||||
"reconstruction_evidence": reconstruction_evidence,
|
||||
|
||||
@@ -31,6 +31,41 @@ class DesignIRPipelineTests(unittest.TestCase):
|
||||
self.assertEqual(1, result["facts"]["solid_count"])
|
||||
self.assertGreater(result["facts"]["volume_mm3"], 30_000)
|
||||
|
||||
def test_legacy_model_migrates_to_semantic_designir_3(self) -> None:
|
||||
migrated = MODULE.migrate_designir_2_to_3(self.payload())
|
||||
self.assertEqual("3.0", migrated["schema_version"])
|
||||
self.assertEqual(
|
||||
"fully_semantic_parametric", migrated["reconstruction_mode"]
|
||||
)
|
||||
self.assertIn(
|
||||
"outer_radius", migrated["edit_interface"]["semantic_parameters"]
|
||||
)
|
||||
with tempfile.TemporaryDirectory() as temporary:
|
||||
output = Path(temporary) / "flange.step"
|
||||
result = MODULE.compile_designir(migrated, output)
|
||||
self.assertEqual(1, result["facts"]["solid_count"])
|
||||
|
||||
def test_generated_step_materializes_hybrid_surface_snapshot(self) -> None:
|
||||
migrated = MODULE.migrate_designir_2_to_3(self.payload())
|
||||
with tempfile.TemporaryDirectory() as temporary:
|
||||
output = Path(temporary) / "flange.step"
|
||||
MODULE.compile_designir(migrated, output)
|
||||
hybrid = MODULE.materialize_hybrid_designir(migrated, output)
|
||||
self.assertEqual(
|
||||
"hybrid_semantic_surface_parametric",
|
||||
hybrid["reconstruction_mode"],
|
||||
)
|
||||
self.assertTrue(hybrid["surface_layer"]["solids"])
|
||||
self.assertTrue(
|
||||
hybrid["compiled_surface_provenance"][
|
||||
"semantic_layer_authoritative"
|
||||
]
|
||||
)
|
||||
self.assertTrue(MODULE.anti_cheat(hybrid)["valid"])
|
||||
rebuilt = Path(temporary) / "hybrid-rebuilt.step"
|
||||
MODULE.compile_designir(hybrid, rebuilt)
|
||||
self.assertTrue(rebuilt.is_file())
|
||||
|
||||
def test_teacher_dependency_is_rejected(self) -> None:
|
||||
payload = self.payload()
|
||||
payload["source_step"] = "teacher.step"
|
||||
@@ -48,6 +83,51 @@ class DesignIRPipelineTests(unittest.TestCase):
|
||||
self.assertGreater(result["facts"]["volume_mm3"], 30_000)
|
||||
self.assertTrue(Path(result["model_json"]).is_file())
|
||||
|
||||
def test_arbitrary_axis_geometric_center_preserves_pose(self) -> None:
|
||||
payload = self.payload()
|
||||
payload["family"] = "axis_aligned_cylinder"
|
||||
payload["parameters"] = {
|
||||
"outer_radius": {"value": 10, "unit": "mm", "editable": True},
|
||||
"overall_length": {"value": 400, "unit": "mm", "editable": True},
|
||||
}
|
||||
payload["expressions"] = {}
|
||||
payload["features"] = [
|
||||
{
|
||||
"id": "main_body",
|
||||
"operation": "extrude_circle",
|
||||
"radius": {"parameter": "outer_radius"},
|
||||
"height": {"parameter": "overall_length"},
|
||||
"axis": [0, -1, 0],
|
||||
"center": [7, 11, 13],
|
||||
"anchor": "geometric_center",
|
||||
}
|
||||
]
|
||||
payload["edit_interface"] = {
|
||||
"editable_parameters": ["outer_radius", "overall_length"],
|
||||
"preserved_interfaces": ["part_center", "primary_axis"],
|
||||
}
|
||||
payload["validation_contract"]["perturbations"] = [
|
||||
{
|
||||
"parameter": "outer_radius",
|
||||
"scale": 1.1,
|
||||
"expected_change": "radius",
|
||||
"preserve": ["overall_length", "part_center"],
|
||||
},
|
||||
{
|
||||
"parameter": "overall_length",
|
||||
"scale": 1.1,
|
||||
"expected_change": "length",
|
||||
"preserve": ["outer_radius", "part_center"],
|
||||
},
|
||||
]
|
||||
shape = MODULE.build_shape(payload)
|
||||
bounds = shape.bounding_box()
|
||||
self.assertAlmostEqual(-189, bounds.min.Y, places=6)
|
||||
self.assertAlmostEqual(211, bounds.max.Y, places=6)
|
||||
self.assertAlmostEqual(7, shape.center(MODULE.CenterOf.MASS).X, places=6)
|
||||
self.assertAlmostEqual(11, shape.center(MODULE.CenterOf.MASS).Y, places=6)
|
||||
self.assertAlmostEqual(13, shape.center(MODULE.CenterOf.MASS).Z, places=6)
|
||||
|
||||
def test_embedded_mesh_is_rejected(self) -> None:
|
||||
payload = self.payload()
|
||||
payload["features"][0]["mesh"] = {"vertices": [[0, 0, 0]]}
|
||||
@@ -97,6 +177,10 @@ class DesignIRPipelineTests(unittest.TestCase):
|
||||
json.dumps(
|
||||
{
|
||||
"model_id": f"case-{index}",
|
||||
"teacher_sha256": f"teacher-{index}",
|
||||
"applied_experience_ids": [
|
||||
"reconstruction.flange.primary"
|
||||
],
|
||||
"next_state": "replay_validated",
|
||||
}
|
||||
),
|
||||
@@ -107,6 +191,10 @@ class DesignIRPipelineTests(unittest.TestCase):
|
||||
reports, ROOT / "config" / "promotion-policy.json"
|
||||
)
|
||||
self.assertTrue(result["eligible"])
|
||||
self.assertEqual(
|
||||
["reconstruction.flange.primary"],
|
||||
result["validated_experience_ids"],
|
||||
)
|
||||
self.assertEqual("promoted", result["next_state"])
|
||||
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ from __future__ import annotations
|
||||
|
||||
import importlib.util
|
||||
import json
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
@@ -106,6 +107,22 @@ def make_case(index: int) -> dict:
|
||||
}
|
||||
|
||||
|
||||
def positive_validation(*experience_ids: str) -> dict:
|
||||
return {
|
||||
"validation_kind": "heldout_experience_ab",
|
||||
"method_results": [
|
||||
{
|
||||
"experience_id": experience_id,
|
||||
"independent_case_count": 3,
|
||||
"edit_contract_pass_rate": 1.0,
|
||||
"baseline_score_delta": 0.5,
|
||||
"passed": True,
|
||||
}
|
||||
for experience_id in experience_ids
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
class CadExperienceTest(unittest.TestCase):
|
||||
def test_default_data_paths_use_shallow_local_layout(self):
|
||||
pipeline_root = Path(__file__).parents[1].resolve()
|
||||
@@ -179,6 +196,47 @@ class CadExperienceTest(unittest.TestCase):
|
||||
"canonical_reconstruction_plan_not_recovered_history",
|
||||
reconstruction["interpretation"],
|
||||
)
|
||||
parameters = STEP_MODULE._semantic_parameter_candidates(
|
||||
surfaces,
|
||||
[0.0, 0.0, 0.0],
|
||||
[40.0, 20.0, 40.0],
|
||||
features,
|
||||
summary,
|
||||
)
|
||||
names = {item["name"] for item in parameters}
|
||||
self.assertIn("repeated_feature_count", names)
|
||||
self.assertIn("repeated_feature_diameter", names)
|
||||
self.assertTrue(
|
||||
all(
|
||||
item["epistemic_status"] == "inferred_from_final_brep"
|
||||
for item in parameters
|
||||
)
|
||||
)
|
||||
|
||||
def test_non_solid_teacher_is_rejected_from_induction(self):
|
||||
payload = make_case(1)
|
||||
payload["geometry_evidence"] = {
|
||||
"valid": False,
|
||||
"reconstruction_eligible": False,
|
||||
"solid_count": 0,
|
||||
"volume": 0.0,
|
||||
"rejection_reasons": ["no_closed_solid", "zero_volume"],
|
||||
}
|
||||
normalized = MODULE.normalize_case(payload)
|
||||
self.assertFalse(normalized["reconstruction_eligible"])
|
||||
self.assertIn("no_closed_solid", normalized["rejection_reasons"])
|
||||
|
||||
def test_sha256_identity_deduplicates_renamed_sources(self):
|
||||
digest = "0" * 64
|
||||
with tempfile.TemporaryDirectory() as temporary:
|
||||
root = Path(temporary)
|
||||
for index in range(2):
|
||||
payload = make_case(index)
|
||||
payload["provenance"]["source_sha256"] = digest
|
||||
(root / f"renamed-{index}.json").write_text(json.dumps(payload))
|
||||
cases, stats = MODULE.load_cases(root)
|
||||
self.assertEqual(1, len(cases))
|
||||
self.assertEqual(1, stats["duplicate_case_count"])
|
||||
|
||||
def test_conditional_feature_context_does_not_dilute_valid_method(self):
|
||||
proposal = self.reviewed_proposal()
|
||||
@@ -202,6 +260,7 @@ class CadExperienceTest(unittest.TestCase):
|
||||
20,
|
||||
0.8,
|
||||
{"duplicate_case_count": 0, "rejected_case_count": 0},
|
||||
positive_validation(proposal["id"]),
|
||||
)
|
||||
self.assertEqual(1, len(library["experiences"]))
|
||||
|
||||
@@ -249,6 +308,29 @@ class CadExperienceTest(unittest.TestCase):
|
||||
self.assertIn("constraint.flanged_hub_adapter.coaxial_stack", ids)
|
||||
self.assertIn("distribution.sleeve_od_to_base_od", ids)
|
||||
|
||||
def test_audit_allows_aggregate_validation_counts_only(self):
|
||||
library = {
|
||||
"schema_version": "2.0",
|
||||
"library_kind": "generalized_cad_experience",
|
||||
"experiences": [
|
||||
{
|
||||
"id": "edit_strategy.test",
|
||||
"support": 20,
|
||||
"validation": {
|
||||
"heldout_pair_count": 20,
|
||||
"treatment_pass_count": 20,
|
||||
"regressed_pair_count": 0,
|
||||
"pass_rate_delta": 0.25,
|
||||
},
|
||||
}
|
||||
],
|
||||
"candidate_experiences": [],
|
||||
}
|
||||
self.assertEqual([], MODULE.audit_library(library))
|
||||
library["experiences"][0]["validation"]["radius"] = 3.0
|
||||
errors = MODULE.audit_library(library)
|
||||
self.assertTrue(any("forbidden instance key" in error for error in errors))
|
||||
|
||||
def test_family_support_is_not_diluted_by_unrelated_families(self):
|
||||
target = [MODULE.normalize_case(make_case(index)) for index in range(20)]
|
||||
unrelated = []
|
||||
@@ -299,7 +381,7 @@ class CadExperienceTest(unittest.TestCase):
|
||||
)
|
||||
self.assertEqual([], candidate_library["experiences"])
|
||||
self.assertEqual(1, len(candidate_library["candidate_experiences"]))
|
||||
self.assertTrue(candidate_library["policy"]["router_consumable"])
|
||||
self.assertFalse(candidate_library["policy"]["router_consumable"])
|
||||
|
||||
cases = [MODULE.normalize_case(make_case(index)) for index in range(20)]
|
||||
promoted_library = MODULE.build_reviewed_library(
|
||||
@@ -308,10 +390,71 @@ class CadExperienceTest(unittest.TestCase):
|
||||
20,
|
||||
0.8,
|
||||
{"duplicate_case_count": 0, "rejected_case_count": 0},
|
||||
positive_validation(proposal["id"]),
|
||||
)
|
||||
self.assertEqual(1, len(promoted_library["experiences"]))
|
||||
self.assertEqual([], promoted_library["candidate_experiences"])
|
||||
|
||||
def test_parameter_naming_requires_positive_heldout_validation(self):
|
||||
cases = []
|
||||
for index in range(20):
|
||||
payload = make_case(index)
|
||||
payload["design_ir"]["semantic_parameter_candidates"] = [
|
||||
{
|
||||
"name": "bolt_circle_diameter",
|
||||
"unit": "mm",
|
||||
"confidence": 0.9,
|
||||
"semantic_aliases": ["hole_spacing"],
|
||||
"affects_feature_roles": ["mounting_pattern"],
|
||||
"epistemic_status": "inferred_from_final_brep",
|
||||
"edit_contract": {
|
||||
"expected_change": "pattern_radius_changes"
|
||||
},
|
||||
}
|
||||
]
|
||||
cases.append(MODULE.normalize_case(payload))
|
||||
raw = {
|
||||
"id": "parameter.flange.bolt_circle_diameter",
|
||||
"kind": "parameter_naming",
|
||||
"scope": ["flanged_hub_adapter"],
|
||||
"evidence_query": {
|
||||
"required_features": ["base_flange"],
|
||||
"required_relations": [],
|
||||
"required_parameters": ["bolt_circle_diameter"],
|
||||
},
|
||||
"guidance": "Expose the circular mounting pattern diameter as an editable semantic parameter.",
|
||||
"semantic_rationale": "The named role preserves edit intent across dimensional variants.",
|
||||
"canonical_parameter": "bolt_circle_diameter",
|
||||
"semantic_aliases": ["hole_spacing"],
|
||||
}
|
||||
proposal = MODULE.normalize_proposals(
|
||||
{
|
||||
"draft_kind": "llm_generalized_experience_proposals",
|
||||
"proposals": [raw],
|
||||
}
|
||||
)[0]
|
||||
unvalidated = MODULE.build_reviewed_library(
|
||||
cases,
|
||||
[proposal],
|
||||
20,
|
||||
0.8,
|
||||
{"duplicate_case_count": 0, "rejected_case_count": 0},
|
||||
)
|
||||
self.assertEqual([], unvalidated["experiences"])
|
||||
self.assertEqual(
|
||||
"awaiting_positive_heldout_ab",
|
||||
unvalidated["candidate_experiences"][0]["validation_state"],
|
||||
)
|
||||
validated = MODULE.build_reviewed_library(
|
||||
cases,
|
||||
[proposal],
|
||||
20,
|
||||
0.8,
|
||||
{"duplicate_case_count": 0, "rejected_case_count": 0},
|
||||
positive_validation(raw["id"]),
|
||||
)
|
||||
self.assertEqual("parameter_naming", validated["experiences"][0]["kind"])
|
||||
|
||||
def test_numeric_instance_answer_is_rejected_from_llm_draft(self):
|
||||
proposal = self.reviewed_proposal()
|
||||
proposal["guidance"] = "Always use a diameter of 91 millimeters."
|
||||
@@ -375,6 +518,7 @@ class CadExperienceTest(unittest.TestCase):
|
||||
20,
|
||||
0.8,
|
||||
{"duplicate_case_count": 0, "rejected_case_count": 0},
|
||||
positive_validation(proposal["id"]),
|
||||
)
|
||||
self.assertEqual([], MODULE.audit_library(library))
|
||||
self.assertEqual("reconstruction_grammar", library["experiences"][0]["kind"])
|
||||
|
||||
@@ -0,0 +1,248 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib.util
|
||||
import json
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
from build123d import Plane, Rectangle
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
|
||||
def load_module(name: str, path: Path):
|
||||
spec = importlib.util.spec_from_file_location(name, path)
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
assert spec.loader
|
||||
spec.loader.exec_module(module)
|
||||
return module
|
||||
|
||||
|
||||
SURFACEIR = load_module(
|
||||
"surfaceir_pipeline", ROOT / "scripts" / "surfaceir_pipeline.py"
|
||||
)
|
||||
DESIGNIR = load_module(
|
||||
"designir_pipeline_surface_test", ROOT / "scripts" / "designir_pipeline.py"
|
||||
)
|
||||
|
||||
|
||||
class SurfaceIRPipelineTests(unittest.TestCase):
|
||||
def test_independent_json_roundtrip_preserves_exact_geometry(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as temporary:
|
||||
directory = Path(temporary)
|
||||
teacher = directory / "teacher.step"
|
||||
portable_json = directory / "model.designir.json"
|
||||
rebuilt = directory / "rebuilt.step"
|
||||
expected = DESIGNIR.Box(10, 20, 30)
|
||||
DESIGNIR.export_step(expected, teacher)
|
||||
surfaceir = SURFACEIR.extract_surfaceir(teacher)
|
||||
SURFACEIR.write_json(portable_json, surfaceir)
|
||||
teacher.unlink()
|
||||
|
||||
serialized = portable_json.read_text(encoding="utf-8").lower()
|
||||
self.assertNotIn("teacher.step", serialized)
|
||||
self.assertNotIn("source_path", serialized)
|
||||
self.assertNotIn("import_step", serialized)
|
||||
self.assertNotIn('"mesh"', serialized)
|
||||
|
||||
rebuilt_shape = SURFACEIR.build_surfaceir(surfaceir)
|
||||
SURFACEIR.export_step_shape(rebuilt_shape, rebuilt)
|
||||
self.assertTrue(rebuilt.is_file())
|
||||
|
||||
actual = DESIGNIR.import_step(rebuilt)
|
||||
self.assertEqual(len(expected.solids()), len(actual.solids()))
|
||||
self.assertEqual(len(expected.faces()), len(actual.faces()))
|
||||
self.assertEqual(len(expected.edges()), len(actual.edges()))
|
||||
self.assertLess(
|
||||
abs(expected.volume - actual.volume) / expected.volume, 1e-10
|
||||
)
|
||||
self.assertLess(
|
||||
DESIGNIR._symmetric_difference_volume(expected, actual), 1e-8
|
||||
)
|
||||
|
||||
def test_empty_step_document_is_represented_honestly(self) -> None:
|
||||
payload = {
|
||||
"schema_version": "3.0",
|
||||
"designir_kind": "independent_parametric_cad",
|
||||
"document_status": "empty_geometry",
|
||||
"surface_layer": {
|
||||
"vertices": [],
|
||||
"solids": [],
|
||||
"free_shells": [],
|
||||
},
|
||||
}
|
||||
rebuilt = SURFACEIR.build_surfaceir(payload)
|
||||
self.assertEqual(SURFACEIR._facts_from_shape(rebuilt)["face_count"], 0)
|
||||
|
||||
def test_open_shell_roundtrip_preserves_surface_geometry(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as temporary:
|
||||
directory = Path(temporary)
|
||||
teacher = directory / "surface.step"
|
||||
rebuilt_path = directory / "surface-rebuilt.step"
|
||||
face = (Plane.XY * Rectangle(10, 20)).face()
|
||||
builder = SURFACEIR.BRep_Builder()
|
||||
shell = SURFACEIR.TopoDS_Shell()
|
||||
builder.MakeShell(shell)
|
||||
builder.Add(shell, face.wrapped)
|
||||
SURFACEIR.export_step_shape(shell, teacher)
|
||||
|
||||
payload = SURFACEIR.extract_surfaceir(teacher)
|
||||
self.assertEqual(payload["document_status"], "geometry_present")
|
||||
self.assertEqual(len(payload["surface_layer"]["solids"]), 0)
|
||||
self.assertEqual(len(payload["surface_layer"]["free_shells"]), 1)
|
||||
rebuilt = SURFACEIR.build_surfaceir(
|
||||
payload, "exact_3d_pcurve"
|
||||
)
|
||||
SURFACEIR.export_step_shape(rebuilt, rebuilt_path)
|
||||
_, teacher_facts = SURFACEIR._shape_facts(teacher)
|
||||
_, rebuilt_facts = SURFACEIR._shape_facts(rebuilt_path)
|
||||
self.assertEqual(teacher_facts["face_count"], 1)
|
||||
self.assertEqual(
|
||||
teacher_facts["face_count"], rebuilt_facts["face_count"]
|
||||
)
|
||||
self.assertAlmostEqual(
|
||||
teacher_facts["surface_area"],
|
||||
rebuilt_facts["surface_area"],
|
||||
places=8,
|
||||
)
|
||||
|
||||
def test_overall_scale_is_executable_and_independently_validated(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as temporary:
|
||||
directory = Path(temporary)
|
||||
teacher = directory / "teacher.step"
|
||||
designir_path = directory / "box.designir.json"
|
||||
expected = DESIGNIR.Box(10, 20, 30)
|
||||
DESIGNIR.export_step(expected, teacher)
|
||||
payload = SURFACEIR.extract_surfaceir(teacher)
|
||||
self.assertIn(
|
||||
"overall_scale",
|
||||
payload["edit_interface"]["semantic_parameters"],
|
||||
)
|
||||
|
||||
edited = SURFACEIR.apply_semantic_parameter(
|
||||
payload, "overall_scale", 1.1
|
||||
)
|
||||
self.assertEqual(
|
||||
edited["edit_operations"],
|
||||
[
|
||||
{
|
||||
"operation": "uniform_scale",
|
||||
"factor": 1.1,
|
||||
"pivot": [0.0, 0.0, 0.0],
|
||||
}
|
||||
],
|
||||
)
|
||||
self.assertIsNotNone(SURFACEIR.build_surfaceir(edited))
|
||||
|
||||
SURFACEIR.write_json(designir_path, payload)
|
||||
acceptance = SURFACEIR.validate_semantic_edit(
|
||||
teacher,
|
||||
designir_path,
|
||||
"overall_scale",
|
||||
1.1,
|
||||
directory / "acceptance",
|
||||
)
|
||||
self.assertTrue(acceptance["accepted"])
|
||||
self.assertTrue(
|
||||
acceptance["checks"]["target_parameter"]["pass"]
|
||||
)
|
||||
|
||||
def test_primary_axis_dimensions_are_executable_and_validated(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as temporary:
|
||||
directory = Path(temporary)
|
||||
teacher = directory / "shaft.step"
|
||||
designir_path = directory / "shaft.designir.json"
|
||||
DESIGNIR.export_step(DESIGNIR.Cylinder(10, 40), teacher)
|
||||
payload = SURFACEIR.extract_surfaceir(teacher)
|
||||
SURFACEIR.write_json(designir_path, payload)
|
||||
|
||||
for parameter, value in (
|
||||
("outer_diameter", 22.0),
|
||||
("body_length", 44.0),
|
||||
):
|
||||
acceptance = SURFACEIR.validate_semantic_edit(
|
||||
teacher,
|
||||
designir_path,
|
||||
parameter,
|
||||
value,
|
||||
directory / parameter,
|
||||
)
|
||||
self.assertTrue(acceptance["accepted"])
|
||||
self.assertTrue(
|
||||
acceptance["checks"]["target_parameter"]["pass"]
|
||||
)
|
||||
|
||||
def test_canonical_semantics_name_bore_and_polar_hole_pattern(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as temporary:
|
||||
teacher = Path(temporary) / "flange.step"
|
||||
expected = DESIGNIR.Cylinder(20, 5) - DESIGNIR.Cylinder(5, 5)
|
||||
for x, y in ((10, 0), (0, 10), (-10, 0), (0, -10)):
|
||||
expected -= DESIGNIR.Pos(x, y, 0) * DESIGNIR.Cylinder(1, 5)
|
||||
DESIGNIR.export_step(expected, teacher)
|
||||
|
||||
payload = SURFACEIR.extract_surfaceir(teacher)
|
||||
semantic = payload["semantic_layer"]
|
||||
parameters = semantic["parameters"]
|
||||
|
||||
self.assertEqual(parameters["hole_count"]["value"], 4)
|
||||
self.assertAlmostEqual(parameters["hole_diameter"]["value"], 2)
|
||||
self.assertAlmostEqual(
|
||||
parameters["bolt_circle_diameter"]["value"], 20
|
||||
)
|
||||
self.assertAlmostEqual(parameters["bore_diameter"]["value"], 10)
|
||||
self.assertAlmostEqual(parameters["outer_diameter"]["value"], 40)
|
||||
self.assertAlmostEqual(parameters["plate_thickness"]["value"], 5)
|
||||
self.assertTrue(
|
||||
any(
|
||||
feature["operation"] == "polar_hole_pattern"
|
||||
for feature in semantic["features"]
|
||||
)
|
||||
)
|
||||
self.assertFalse(
|
||||
semantic["inference_policy"]["claims_original_feature_history"]
|
||||
)
|
||||
|
||||
edited = SURFACEIR.apply_semantic_parameter(
|
||||
payload, "hole_diameter", 3
|
||||
)
|
||||
self.assertEqual(len(edited["edit_operations"]), 4)
|
||||
self.assertEqual(
|
||||
edited["active_edit"]["acceptance_status"], "pending"
|
||||
)
|
||||
with self.assertRaisesRegex(ValueError, "increases only"):
|
||||
SURFACEIR.apply_semantic_parameter(
|
||||
payload, "hole_diameter", 1.5
|
||||
)
|
||||
edited_step = Path(temporary) / "flange-edited.step"
|
||||
edited_shape = SURFACEIR.build_surfaceir(edited)
|
||||
SURFACEIR.export_step_shape(edited_shape, edited_step)
|
||||
edited_payload = SURFACEIR.extract_surfaceir(edited_step)
|
||||
edited_parameters = edited_payload["semantic_layer"]["parameters"]
|
||||
self.assertAlmostEqual(
|
||||
edited_parameters["hole_diameter"]["value"], 3
|
||||
)
|
||||
self.assertAlmostEqual(
|
||||
edited_parameters["bore_diameter"]["value"], 10
|
||||
)
|
||||
self.assertEqual(edited_parameters["hole_count"]["value"], 4)
|
||||
actual = DESIGNIR.import_step(edited_step)
|
||||
self.assertTrue(actual.is_valid)
|
||||
self.assertLess(actual.volume, expected.volume)
|
||||
|
||||
original_ir = Path(temporary) / "flange.designir.json"
|
||||
SURFACEIR.write_json(original_ir, payload)
|
||||
acceptance = SURFACEIR.validate_semantic_edit(
|
||||
teacher,
|
||||
original_ir,
|
||||
"hole_diameter",
|
||||
2.5,
|
||||
Path(temporary) / "acceptance",
|
||||
)
|
||||
self.assertTrue(acceptance["accepted"])
|
||||
self.assertEqual(acceptance["state"], "accepted")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
File diff suppressed because it is too large
Load Diff
+17
-1
@@ -6,7 +6,7 @@ STEP-first CAD engine used by CadSet.
|
||||
|
||||
1. Generate an editable model from natural language.
|
||||
2. Generate or modify an editable model from text plus reference images.
|
||||
3. Reconstruct an uploaded STEP into DesignIR 2.0, then modify and rebuild it
|
||||
3. Reconstruct an uploaded STEP into DesignIR 3.0 / SurfaceIR, then modify and rebuild it
|
||||
without reading the teacher STEP.
|
||||
|
||||
`cad-router` chooses one of two execution backends:
|
||||
@@ -34,6 +34,22 @@ reconstruction runner rejects source STEP imports, embedded B-Rep, full mesh
|
||||
substitutes, source face references, and paths into the protected teacher
|
||||
workspace. Acceptance runs separately against the rebuilt STEP.
|
||||
|
||||
## Uploaded STEP execution
|
||||
|
||||
Run the full upload path through `cad-router`:
|
||||
|
||||
```bash
|
||||
.venv/bin/python skills/cad-router/scripts/route.py \
|
||||
"重建上传的 STEP,并返回可修改参数" \
|
||||
--edit-source /absolute/path/to/part.step \
|
||||
--task-dir /absolute/path/to/task \
|
||||
--execute
|
||||
```
|
||||
|
||||
The task returns an independently rebuilt STEP, its DesignIR 3.0 source,
|
||||
geometry acceptance measurements, parameter perturbation results, and a catalog
|
||||
containing only validated editable parameters.
|
||||
|
||||
## Development
|
||||
|
||||
Use the repository-local Python environment for checks:
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
name: cad-router
|
||||
description: Route natural-language or image-guided CAD generation and modification, plus uploaded STEP/STP reconstruction and modification, across text-to-cad and SimpleCADAPI. Create one executable DesignIR 2.0 model per part, produce STEP geometry without teacher geometry dependencies, and enforce independent validation, visual repair, and CAD Viewer review.
|
||||
description: Route natural-language or image-guided CAD generation and modification, plus uploaded STEP/STP reconstruction and modification, across text-to-cad and SimpleCADAPI. Use unified executable DesignIR 3.0 in semantic-feature mode for authored models and SurfaceIR mode for uploaded STEP reconstruction, without teacher geometry dependencies, with independent validation and CAD Viewer review.
|
||||
---
|
||||
|
||||
# CAD Router
|
||||
@@ -9,7 +9,7 @@ Choose a backend before modeling, keep its source as the editable authority, and
|
||||
|
||||
## Required workflow
|
||||
|
||||
1. Convert the natural-language or image-guided request into a brief: part type, dimensions, units, assembly relationships, editability, and validation targets. The artifact contract is fixed to executable DesignIR 2.0 plus STEP geometry.
|
||||
1. Convert the natural-language or image-guided request into a brief: part type, dimensions, units, assembly relationships, editability, and validation targets. Every model uses DesignIR 3.0 plus STEP geometry. New text/image models use `fully_semantic_parametric`; uploaded STEP reconstruction uses `surface_parametric`. A generated semantic model may include a compiled SurfaceIR snapshot and then uses `hybrid_semantic_surface_parametric`, while its semantic layer remains authoritative.
|
||||
Never treat a teacher STEP or a single-case DesignIR as experience. Batch case
|
||||
JSON belongs to the separate CAD Experience plugin. Generation may consume
|
||||
only its promoted schema `2.0` generalized library.
|
||||
@@ -31,22 +31,38 @@ Choose a backend before modeling, keep its source as the editable authority, and
|
||||
dimensionless distributions second. Use `--experience-library`,
|
||||
`--experience-family`, or repeated `--experience-feature` only to override
|
||||
those defaults.
|
||||
For one uploaded STEP, execute the route rather than stopping after the
|
||||
routing decision:
|
||||
|
||||
```bash
|
||||
../../.venv/bin/python scripts/route.py \
|
||||
"重建上传的 STEP,并返回可修改参数" \
|
||||
--edit-source /absolute/path/to/upload.step \
|
||||
--task-dir /absolute/path/to/task \
|
||||
--execute
|
||||
```
|
||||
|
||||
This command must finish extraction, source-independent reconstruction,
|
||||
geometry acceptance, parameter perturbation acceptance, and artifact
|
||||
publication. A route decision alone is not a completed reconstruction.
|
||||
3. Follow the selected route unless a hard environmental constraint makes it unavailable. If overriding it, record the reason in the task manifest.
|
||||
4. Load only the selected backend reference from `references/capabilities.md` and the matching installed Skill:
|
||||
- `build123d`: use `$cad`; this is the default for general STEP-first mechanical parts, assemblies, and existing STEP modification.
|
||||
- `simplecadapi`: use an installed SimpleCADAPI Skill/runtime; prefer it for gears, racks, ring gears, bearings, cycloidal parts, reducers, replayable graphs, and semantic tags.
|
||||
5. When the route reports `requirement_refinement` or `visual_repair`, refine the brief, generate, execute, inspect multiple views, and repair no more than three visual mismatch rounds before asking the user.
|
||||
6. Generate into a task-owned directory and start CAD Viewer with that task directory as `--dir`; do not expose the repository-wide fixture library for a task review. Write one `*.designir.json` per part and treat it as the editable geometry contract; keep `cad-task.json` as the execution record. Read `references/designir-2.0.md` before creating, reconstructing, or modifying a model. Never change backend during an edit unless conversion is explicitly requested; modify the recorded DesignIR source of truth.
|
||||
For a supplied STEP/STP, the deterministic ingestion service may extract
|
||||
private teacher evidence. The reconstruction agent receives that evidence,
|
||||
never the STEP itself, and must author complete executable DesignIR. Compile
|
||||
it in an isolated environment where the teacher STEP is absent.
|
||||
6. Generate into a task-owned directory and start CAD Viewer with that task directory as `--dir`; do not expose the repository-wide fixture library for a task review. Write one DesignIR 3.0 `*.designir.json` per part and treat it as the editable geometry contract; keep `cad-task.json` as the execution record. Read `references/designir-3.0.md` for both semantic authoring and uploaded STEP reconstruction. `references/designir-2.0.md` is migration-only. Never change backend during an edit unless conversion is explicitly requested; modify the recorded authoritative layer.
|
||||
For a supplied STEP/STP, use `scripts/route.py --execute`; do not manually
|
||||
approximate the model when the executable adapter is available. The
|
||||
deterministic ingestion service extracts private teacher evidence into
|
||||
DesignIR 3.0. The independent rebuild subprocess receives the DesignIR and
|
||||
output path, never the STEP path. The acceptance stage alone receives both
|
||||
teacher and reconstruction.
|
||||
The DesignIR and generator must not import STEP, embed B-Rep or meshes, retain
|
||||
face references, or use the source model as a base feature. The acceptance
|
||||
agent alone compares the teacher and reconstructed STEP.
|
||||
If the request does not name the source part family, inspect the explicit
|
||||
source first and rerun the experience query with `--experience-family` and
|
||||
`--experience-feature`; do not guess across family-scoped experience.
|
||||
Only expose parameters listed by `parameters.json`; every listed parameter
|
||||
has passed an isolated perturbation test. Other inferred names remain
|
||||
evidence, not a promised edit interface.
|
||||
7. Validate against the brief. Run STEP geometry inspection and snapshot review.
|
||||
For experience-informed generation, record which generalized methods were
|
||||
applied, including the reconstruction grammar ID and instantiated symbolic
|
||||
@@ -70,6 +86,6 @@ Choose a backend before modeling, keep its source as the editable authority, and
|
||||
|
||||
- Read `references/capabilities.md` when comparing or invoking backends.
|
||||
- Read `references/backend-contract.md` before writing a task manifest or adding another backend.
|
||||
- Read `references/designir-2.0.md` when generating, reconstructing, or
|
||||
modifying a per-part executable design.
|
||||
- Read `references/designir-3.0.md` for semantic authoring, uploaded STEP reconstruction, and edits.
|
||||
- Read `references/designir-2.0.md` only when migrating legacy artifacts.
|
||||
- `scripts/capabilities.json` is the machine-readable V1 registry used by `scripts/route.py`.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
interface:
|
||||
display_name: "CAD Router"
|
||||
short_description: "Route CAD and manage per-part JSON model specs."
|
||||
default_prompt: "Use $cad-router to select the best CAD backend, create or modify the part through DesignIR 2.0, validate the result independently, and open the STEP in CAD Viewer."
|
||||
default_prompt: "Use $cad-router to select the best CAD backend, create or modify the part through unified DesignIR 3.0, validate the result independently, and open the STEP in CAD Viewer."
|
||||
|
||||
@@ -70,14 +70,17 @@ Write `cad-task.json` beside the task artifacts with this minimum shape:
|
||||
|
||||
Paths are relative to the task directory unless an external input must remain absolute.
|
||||
Do not store authoritative model parameters in `cad-task.json`. Every part must
|
||||
have a sibling `*.designir.json` that owns its coordinate system, datums,
|
||||
parameters, expressions, features, constraints, edit interface, and validation
|
||||
contract. See `designir-2.0.md`.
|
||||
have a sibling `*.designir.json` as its editable source of truth. Authored
|
||||
feature models use semantic DesignIR 3.0; uploaded STEP reconstruction uses
|
||||
DesignIR 3.0 / SurfaceIR. See `designir-3.0.md`; `designir-2.0.md` is retained
|
||||
only for legacy migration.
|
||||
|
||||
For an uploaded STEP, keep teacher geometry in `designir-pipeline/input/` and
|
||||
all private evidence, DesignIR candidates, rebuilds, and acceptance reports in
|
||||
`designir-pipeline/runs/`. The reconstruction task receives evidence but no
|
||||
teacher STEP or imported B-Rep.
|
||||
For an uploaded STEP, use `scripts/route.py --execute` with an explicit source
|
||||
and task directory. Do not move an ordinary upload into or scan the
|
||||
distillation corpus. The task publishes the independent DesignIR, reconstructed
|
||||
STEP, validated parameter catalog, geometry report, edit report, and
|
||||
`cad-task.json`. The reconstruction subprocess receives no teacher STEP path;
|
||||
only extraction and acceptance may read the explicit uploaded source.
|
||||
|
||||
## Backend adapter requirements
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ Best fit:
|
||||
- Brackets, connecting rods, cranks, levers, shafts, bushings, flanges, plates, housings, covers, enclosures, fixtures, and machined features.
|
||||
- Parametric Python sources with holes, pockets, slots, bosses, ribs, shells, fillets, chamfers, lofts, sweeps, and revolves.
|
||||
- Source-level assemblies, named datums, joints, imported components, measurements, alignment checks, and existing STEP inspection.
|
||||
- STEP output compiled from executable DesignIR 2.0.
|
||||
- STEP output compiled from executable semantic DesignIR 3.0.
|
||||
|
||||
Weak signals: a specialized mechanical family already implemented by SimpleCADAPI or a browser-only execution requirement.
|
||||
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
# DesignIR 2.0 contract
|
||||
|
||||
> Legacy migration reference only. New text, image, and STEP workflows must
|
||||
> emit DesignIR 3.0. Use `designir_pipeline.py migrate` to preserve an existing
|
||||
> 2.0 semantic feature program inside the DesignIR 3.0 envelope.
|
||||
|
||||
Every generated, reconstructed, or modified part owns one executable
|
||||
`*.designir.json`. It records design intent rather than source faces or a copied
|
||||
solid.
|
||||
|
||||
@@ -0,0 +1,129 @@
|
||||
# DesignIR 3.0 unified contract
|
||||
|
||||
DesignIR 3.0 is the only editable part contract. It has three explicit modes:
|
||||
|
||||
- `fully_semantic_parametric`: authored from text or images. `semantic_layer`
|
||||
contains design intent, named parameters, constraints and executable
|
||||
features.
|
||||
- `surface_parametric`: inferred from an uploaded STEP. `surface_layer`
|
||||
contains analytic/B-spline surfaces, boundary curves, pcurves, vertices,
|
||||
tolerances, shells and solids in CadSet's vocabulary.
|
||||
- `hybrid_semantic_surface_parametric`: a semantic feature program plus a
|
||||
compiled SurfaceIR snapshot extracted from its generated STEP. The semantic
|
||||
layer remains authoritative.
|
||||
|
||||
The two authoring paths share one envelope and edit vocabulary, but they do not
|
||||
claim equivalent evidence. Text/image generation authors design intent. STEP
|
||||
ingestion infers semantics from final B-Rep and never claims to recover the
|
||||
source CAD application's sketch or feature history.
|
||||
|
||||
## Text and image authoring
|
||||
|
||||
Author these top-level values:
|
||||
|
||||
```json
|
||||
{
|
||||
"schema_version": "3.0",
|
||||
"designir_kind": "independent_parametric_cad",
|
||||
"model_id": "part_id",
|
||||
"family": "semantic_part_family",
|
||||
"units": "mm",
|
||||
"document_status": "geometry_present",
|
||||
"reconstruction_mode": "fully_semantic_parametric",
|
||||
"authoring_mode": "semantic_feature_program",
|
||||
"backend_hint": "build123d",
|
||||
"semantic_layer": {
|
||||
"reconstruction_status": "ready",
|
||||
"coordinate_system": {
|
||||
"origin": [0, 0, 0],
|
||||
"x_axis": [1, 0, 0],
|
||||
"y_axis": [0, 1, 0],
|
||||
"z_axis": [0, 0, 1]
|
||||
},
|
||||
"datums": {},
|
||||
"parameters": {},
|
||||
"expressions": {},
|
||||
"sketches": [],
|
||||
"constraints": [],
|
||||
"features": [],
|
||||
"patterns": [],
|
||||
"attachments": [],
|
||||
"construction_stages": []
|
||||
},
|
||||
"edit_interface": {
|
||||
"semantic_parameters": [],
|
||||
"surface_parameter_groups": [],
|
||||
"modification_levels": ["semantic_feature"],
|
||||
"preserved_interfaces": []
|
||||
},
|
||||
"validation_contract": {
|
||||
"source_independence": true,
|
||||
"geometry_checks": ["solid_count", "bounding_box", "volume"],
|
||||
"edit_checks": ["parameter_perturbation"],
|
||||
"invariants": [],
|
||||
"perturbations": [],
|
||||
"thresholds": {}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Every name in `semantic_parameters` must identify an editable parameter, drive
|
||||
at least one feature, and have a perturbation test. Prefer intent names such as
|
||||
`flange_thickness`, `shelf_depth`, `hole_spacing`, `rib_count`,
|
||||
`bore_diameter`, and `bolt_circle_diameter`.
|
||||
|
||||
After isolated compilation, attach SurfaceIR only from the generated STEP and
|
||||
switch the mode to `hybrid_semantic_surface_parametric`. Never use a teacher
|
||||
STEP to fill the compiled snapshot.
|
||||
|
||||
## Uploaded STEP execution
|
||||
|
||||
Run an uploaded file through the router's executable adapter:
|
||||
|
||||
```bash
|
||||
../../.venv/bin/python scripts/route.py \
|
||||
"重建上传的 STEP,并返回可修改参数" \
|
||||
--edit-source /absolute/path/to/part.step \
|
||||
--task-dir /absolute/path/to/task \
|
||||
--execute
|
||||
```
|
||||
|
||||
The task directory contains:
|
||||
|
||||
- `<part>.designir.json`: independent editable source of truth;
|
||||
- `<part>.reconstructed.step`: reconstructed geometry;
|
||||
- `parameters.json`: only parameters that passed perturbation acceptance;
|
||||
- `validation/geometry.json`: teacher-versus-reconstruction measurements;
|
||||
- `validation/edits.json`: parameter acceptance outcomes;
|
||||
- `cad-task.json`: route, artifacts, isolation and validation state.
|
||||
|
||||
## Isolation boundary
|
||||
|
||||
Extraction and acceptance may read the uploaded STEP. The reconstruction
|
||||
subprocess receives only DesignIR and an output path. DesignIR must not contain
|
||||
a teacher path, an import operation, STEP text, embedded B-Rep, or a mesh
|
||||
substitute.
|
||||
|
||||
The runtime performs an anti-cheat scan before rebuilding. Geometry acceptance
|
||||
requires valid reconstructed topology and the thresholds recorded in
|
||||
`validation/geometry.json`.
|
||||
|
||||
The adapter loads only formally promoted experiences whose scope is
|
||||
`surfaceir_reconstruction` or `surfaceir_semantic_editing`. These methods are
|
||||
applied conditionally by the runtime; `cad-task.json` records both the available
|
||||
method count and the exact method IDs used for the task.
|
||||
|
||||
## Parameter boundary
|
||||
|
||||
STEP does not contain authoritative feature names or modeling history. The
|
||||
semantic layer therefore records inferred canonical parameters separately from
|
||||
validated editable parameters.
|
||||
|
||||
Only parameters in `parameters.json` are public edit controls. Each was changed
|
||||
in isolation, rebuilt, and checked for target response, preserved validity and
|
||||
non-target stability. Use the `edit_command_template` in `cad-task.json` to
|
||||
produce an edited DesignIR and STEP.
|
||||
|
||||
An accepted baseline reconstruction does not imply that every inferred
|
||||
parameter is editable. An empty `parameters.json` is valid for an empty source
|
||||
document and must not be presented as editable geometry.
|
||||
@@ -0,0 +1,661 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Execute one uploaded STEP -> DesignIR 3.0 reconstruction task.
|
||||
|
||||
The teacher STEP is passed only to deterministic extraction and acceptance
|
||||
commands. The independent rebuild subprocess receives the DesignIR path and
|
||||
output path only.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
|
||||
SCRIPT_PATH = Path(__file__).resolve()
|
||||
SKILL_ROOT = SCRIPT_PATH.parents[1]
|
||||
WORKSPACE_ROOT = SKILL_ROOT.parents[2]
|
||||
SURFACEIR_PIPELINE = (
|
||||
WORKSPACE_ROOT / "designir-pipeline" / "scripts" / "surfaceir_pipeline.py"
|
||||
)
|
||||
EXPERIENCE_LIBRARY = (
|
||||
WORKSPACE_ROOT / "designir-pipeline" / "output" / "library.json"
|
||||
)
|
||||
DESIGNIR_SCHEMA = (
|
||||
WORKSPACE_ROOT
|
||||
/ "designir-pipeline"
|
||||
/ "contracts"
|
||||
/ "designir-3.0.schema.json"
|
||||
)
|
||||
|
||||
EXECUTABLE_EDIT_STATES = {
|
||||
"executable_enlarge_cut_binding",
|
||||
"executable_axis_affine_binding",
|
||||
"executable_uniform_scale_binding",
|
||||
"validated_executable_binding",
|
||||
}
|
||||
FORBIDDEN_DESIGNIR_TOKENS = (
|
||||
'"source_path"',
|
||||
'"teacher_step"',
|
||||
"teacher.step",
|
||||
"import_step",
|
||||
"stepcontrol_reader",
|
||||
'"brep"',
|
||||
'"mesh"',
|
||||
)
|
||||
|
||||
|
||||
class ReconstructionError(RuntimeError):
|
||||
"""Raised when a reconstruction stage cannot produce an accepted result."""
|
||||
|
||||
|
||||
def _runtime_python() -> Path:
|
||||
configured = os.environ.get("CAD_ROUTER_PYTHON", "").strip()
|
||||
candidates = [
|
||||
Path(configured).expanduser() if configured else None,
|
||||
WORKSPACE_ROOT / "text-to-cad" / ".venv" / "bin" / "python",
|
||||
Path(sys.executable),
|
||||
]
|
||||
for candidate in candidates:
|
||||
if candidate is not None and candidate.is_file():
|
||||
# Preserve a virtualenv launcher symlink. Resolving it to the base
|
||||
# interpreter discards the virtualenv's package search path.
|
||||
return candidate.absolute()
|
||||
raise ReconstructionError(
|
||||
"No CAD runtime Python found; set CAD_ROUTER_PYTHON or create "
|
||||
"text-to-cad/.venv."
|
||||
)
|
||||
|
||||
|
||||
def _safe_stem(source: Path) -> str:
|
||||
stem = re.sub(r"[^a-zA-Z0-9_.-]+", "_", source.stem).strip("._")
|
||||
return stem or "uploaded_part"
|
||||
|
||||
|
||||
def _write_json(path: Path, payload: dict[str, Any]) -> None:
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_text(
|
||||
json.dumps(payload, ensure_ascii=False, indent=2) + "\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
|
||||
def _run_json(
|
||||
command: list[str],
|
||||
*,
|
||||
timeout_seconds: int,
|
||||
stage: str,
|
||||
) -> dict[str, Any]:
|
||||
completed = subprocess.run(
|
||||
command,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=timeout_seconds,
|
||||
check=False,
|
||||
)
|
||||
if completed.returncode != 0:
|
||||
detail = completed.stderr.strip() or completed.stdout.strip()
|
||||
raise ReconstructionError(
|
||||
f"{stage} failed with exit code {completed.returncode}: "
|
||||
f"{detail[-2000:]}"
|
||||
)
|
||||
lines = [line for line in completed.stdout.splitlines() if line.strip()]
|
||||
if not lines:
|
||||
return {}
|
||||
try:
|
||||
return json.loads(lines[-1])
|
||||
except json.JSONDecodeError as exc:
|
||||
raise ReconstructionError(
|
||||
f"{stage} returned non-JSON output: {lines[-1][-1000:]}"
|
||||
) from exc
|
||||
|
||||
|
||||
def _validate_designir_contract(payload: dict[str, Any]) -> dict[str, Any]:
|
||||
required = {
|
||||
"schema_version",
|
||||
"designir_kind",
|
||||
"model_id",
|
||||
"units",
|
||||
"document_status",
|
||||
"reconstruction_mode",
|
||||
"semantic_layer",
|
||||
"surface_layer",
|
||||
"edit_interface",
|
||||
"validation_contract",
|
||||
}
|
||||
missing = sorted(required - set(payload))
|
||||
if missing:
|
||||
raise ReconstructionError(
|
||||
f"DesignIR 3.0 is missing required fields: {', '.join(missing)}"
|
||||
)
|
||||
if payload["schema_version"] != "3.0":
|
||||
raise ReconstructionError("Uploaded STEP did not produce DesignIR 3.0")
|
||||
if payload["designir_kind"] != "independent_parametric_cad":
|
||||
raise ReconstructionError("DesignIR is not source-independent")
|
||||
if payload["validation_contract"].get("source_independence") is not True:
|
||||
raise ReconstructionError("DesignIR source-independence contract is false")
|
||||
serialized = json.dumps(payload, ensure_ascii=False).lower()
|
||||
violations = [
|
||||
token for token in FORBIDDEN_DESIGNIR_TOKENS if token in serialized
|
||||
]
|
||||
if violations:
|
||||
raise ReconstructionError(
|
||||
"DesignIR anti-cheat scan failed: " + ", ".join(violations)
|
||||
)
|
||||
return {
|
||||
"schema_contract": "designir-3.0",
|
||||
"schema_path": str(DESIGNIR_SCHEMA),
|
||||
"required_fields_present": True,
|
||||
"source_independence_declared": True,
|
||||
"anti_cheat_pass": True,
|
||||
"forbidden_token_matches": 0,
|
||||
}
|
||||
|
||||
|
||||
def _candidate_parameters(
|
||||
payload: dict[str, Any], maximum: int
|
||||
) -> list[tuple[str, float]]:
|
||||
parameters = payload.get("semantic_layer", {}).get("parameters", {})
|
||||
names = payload.get("edit_interface", {}).get("semantic_parameters", [])
|
||||
feature_candidates: list[tuple[str, float]] = []
|
||||
fallback: tuple[str, float] | None = None
|
||||
for name in names:
|
||||
parameter = parameters.get(name)
|
||||
if not isinstance(parameter, dict):
|
||||
continue
|
||||
if parameter.get("edit_state") not in EXECUTABLE_EDIT_STATES:
|
||||
continue
|
||||
try:
|
||||
old_value = float(parameter["value"])
|
||||
except (KeyError, TypeError, ValueError):
|
||||
continue
|
||||
candidate = (name, old_value * 1.1)
|
||||
if name == "overall_scale":
|
||||
fallback = candidate
|
||||
else:
|
||||
feature_candidates.append(candidate)
|
||||
selected = feature_candidates[: max(0, maximum)]
|
||||
if fallback is not None:
|
||||
selected.append(fallback)
|
||||
return selected
|
||||
|
||||
|
||||
def _surfaceir_experience_ids() -> set[str]:
|
||||
if not EXPERIENCE_LIBRARY.is_file():
|
||||
return set()
|
||||
try:
|
||||
payload = json.loads(EXPERIENCE_LIBRARY.read_text(encoding="utf-8"))
|
||||
except (OSError, json.JSONDecodeError):
|
||||
return set()
|
||||
policy = payload.get("policy", {})
|
||||
if (
|
||||
payload.get("library_kind") != "generalized_cad_experience"
|
||||
or policy.get("surfaceir_consumable") is not True
|
||||
or policy.get("instance_parameters_allowed") is not False
|
||||
):
|
||||
return set()
|
||||
return {
|
||||
item["id"]
|
||||
for item in payload.get("experiences", [])
|
||||
if isinstance(item, dict)
|
||||
and isinstance(item.get("id"), str)
|
||||
and set(item.get("scope", [])).intersection(
|
||||
{"surfaceir_reconstruction", "surfaceir_semantic_editing"}
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
def _applied_experience_ids(
|
||||
available: set[str], accepted_parameters: list[dict[str, Any]]
|
||||
) -> list[str]:
|
||||
applied = {
|
||||
experience_id
|
||||
for experience_id in (
|
||||
"failure_repair.surface_topology.bind_face_pcurves_and_group_shells",
|
||||
"failure_repair.surface_topology.preserve_vertex_tolerance",
|
||||
)
|
||||
if experience_id in available
|
||||
}
|
||||
names = {parameter["name"] for parameter in accepted_parameters}
|
||||
if any("hole_diameter" in name or "bore_diameter" in name for name in names):
|
||||
applied.update(
|
||||
experience_id
|
||||
for experience_id in (
|
||||
"edit_strategy.semantic_cylindrical_cut.enlarge_diameter",
|
||||
"failure_repair.semantic_edit.merge_duplicate_cylindrical_targets",
|
||||
"edit_strategy.semantic_edit.preserve_validated_geometry_strategy",
|
||||
)
|
||||
if experience_id in available
|
||||
)
|
||||
if "overall_scale" in names:
|
||||
experience_id = (
|
||||
"edit_strategy.semantic_parameter.source_independent_overall_scale"
|
||||
)
|
||||
if experience_id in available:
|
||||
applied.add(experience_id)
|
||||
if names.intersection(
|
||||
{"outer_diameter", "body_length", "plate_thickness"}
|
||||
):
|
||||
experience_id = (
|
||||
"edit_strategy.semantic_parameter.primary_axis_affine_dimensions"
|
||||
)
|
||||
if experience_id in available:
|
||||
applied.add(experience_id)
|
||||
if any(name.startswith("overall_size_") for name in names):
|
||||
experience_id = (
|
||||
"edit_strategy.semantic_parameter.canonical_directional_envelope"
|
||||
)
|
||||
if experience_id in available:
|
||||
applied.add(experience_id)
|
||||
return sorted(applied)
|
||||
|
||||
|
||||
def _persist_edit_results(
|
||||
designir_path: Path,
|
||||
attempts: list[dict[str, Any]],
|
||||
) -> tuple[dict[str, Any], list[dict[str, Any]]]:
|
||||
payload = json.loads(designir_path.read_text(encoding="utf-8"))
|
||||
parameters = payload.get("semantic_layer", {}).get("parameters", {})
|
||||
accepted_parameters: list[dict[str, Any]] = []
|
||||
attempted_names = set()
|
||||
for attempt in attempts:
|
||||
name = attempt["parameter"]
|
||||
attempted_names.add(name)
|
||||
parameter = parameters[name]
|
||||
old_value = float(parameter["value"])
|
||||
requested_value = float(attempt["requested_value"])
|
||||
if attempt["accepted"]:
|
||||
parameter["editable"] = True
|
||||
parameter["edit_state"] = "validated_executable_binding"
|
||||
parameter["validated_range"] = {
|
||||
"direction": "increase",
|
||||
"minimum_tested_inclusive": old_value,
|
||||
"maximum_tested_inclusive": requested_value,
|
||||
"acceptance_contract": "designir_3_semantic_edit_acceptance",
|
||||
}
|
||||
accepted_parameters.append(
|
||||
{
|
||||
"name": name,
|
||||
"semantic_role": parameter.get("semantic_role", name),
|
||||
"value": parameter["value"],
|
||||
"unit": parameter.get("unit", "mm"),
|
||||
"validated_range": parameter["validated_range"],
|
||||
"confidence": parameter.get("confidence"),
|
||||
}
|
||||
)
|
||||
else:
|
||||
parameter["editable"] = False
|
||||
parameter["edit_state"] = "rejected_by_runtime_validation"
|
||||
parameter.pop("validated_range", None)
|
||||
payload["edit_interface"]["semantic_parameters"] = sorted(
|
||||
parameter["name"] for parameter in accepted_parameters
|
||||
)
|
||||
payload["reconstruction_strategy"] = {
|
||||
"boundary_strategy": "exact_3d_pcurve",
|
||||
"selection_status": "geometry_validated",
|
||||
}
|
||||
_write_json(designir_path, payload)
|
||||
return payload, accepted_parameters
|
||||
|
||||
|
||||
def reconstruct_uploaded_step(
|
||||
source: Path,
|
||||
task_dir: Path,
|
||||
*,
|
||||
request: str,
|
||||
maximum_parameters: int = 8,
|
||||
timeout_seconds: int = 180,
|
||||
) -> dict[str, Any]:
|
||||
source = source.expanduser().resolve()
|
||||
task_dir = task_dir.expanduser().resolve()
|
||||
if not source.is_file():
|
||||
raise ReconstructionError(f"Uploaded STEP does not exist: {source}")
|
||||
if source.suffix.lower() not in {".step", ".stp"}:
|
||||
raise ReconstructionError("Uploaded reconstruction source must be STEP/STP")
|
||||
if source == task_dir or task_dir == source.parent:
|
||||
raise ReconstructionError(
|
||||
"Task directory must be separate from the uploaded STEP directory"
|
||||
)
|
||||
if source == task_dir or task_dir in source.parents:
|
||||
raise ReconstructionError(
|
||||
"Task directory cannot contain the uploaded teacher STEP"
|
||||
)
|
||||
if not SURFACEIR_PIPELINE.is_file():
|
||||
raise ReconstructionError(
|
||||
f"DesignIR 3.0 runtime is missing: {SURFACEIR_PIPELINE}"
|
||||
)
|
||||
|
||||
task_dir.mkdir(parents=True, exist_ok=True)
|
||||
stem = _safe_stem(source)
|
||||
designir_path = task_dir / f"{stem}.designir.json"
|
||||
rebuilt_path = task_dir / f"{stem}.reconstructed.step"
|
||||
validation_dir = task_dir / "validation"
|
||||
geometry_report_path = validation_dir / "geometry.json"
|
||||
edit_report_path = validation_dir / "edits.json"
|
||||
parameters_path = task_dir / "parameters.json"
|
||||
task_manifest_path = task_dir / "cad-task.json"
|
||||
collisions = [
|
||||
path
|
||||
for path in (
|
||||
designir_path,
|
||||
rebuilt_path,
|
||||
parameters_path,
|
||||
task_manifest_path,
|
||||
)
|
||||
if path.exists()
|
||||
]
|
||||
if collisions:
|
||||
raise ReconstructionError(
|
||||
"Refusing to overwrite existing task artifacts: "
|
||||
+ ", ".join(str(path) for path in collisions)
|
||||
)
|
||||
|
||||
runtime = _runtime_python()
|
||||
common = [str(runtime), str(SURFACEIR_PIPELINE)]
|
||||
available_experience_ids = _surfaceir_experience_ids()
|
||||
boundary_strategy = (
|
||||
"exact_3d_pcurve"
|
||||
if (
|
||||
"failure_repair.surface_topology.bind_face_pcurves_and_group_shells"
|
||||
in available_experience_ids
|
||||
)
|
||||
else "exact_3d"
|
||||
)
|
||||
|
||||
extraction = _run_json(
|
||||
common
|
||||
+ [
|
||||
"extract",
|
||||
str(source),
|
||||
"--output",
|
||||
str(designir_path),
|
||||
],
|
||||
timeout_seconds=timeout_seconds,
|
||||
stage="DesignIR 3.0 extraction",
|
||||
)
|
||||
payload = json.loads(designir_path.read_text(encoding="utf-8"))
|
||||
contract_checks = _validate_designir_contract(payload)
|
||||
|
||||
# The rebuild worker receives no teacher path. It can regenerate the model
|
||||
# from the DesignIR artifact alone.
|
||||
_run_json(
|
||||
common
|
||||
+ [
|
||||
"rebuild",
|
||||
str(designir_path),
|
||||
"--output",
|
||||
str(rebuilt_path),
|
||||
"--boundary-strategy",
|
||||
boundary_strategy,
|
||||
],
|
||||
timeout_seconds=timeout_seconds,
|
||||
stage="source-independent SurfaceIR rebuild",
|
||||
)
|
||||
|
||||
manifest_path = task_dir / "manifest.json"
|
||||
_write_json(
|
||||
manifest_path,
|
||||
{
|
||||
"schema_version": "1.0",
|
||||
"input_count": 1,
|
||||
"rows": [
|
||||
{
|
||||
"source_name": source.name,
|
||||
"output_name": designir_path.name,
|
||||
"state": "extracted",
|
||||
}
|
||||
],
|
||||
},
|
||||
)
|
||||
validation_dir.mkdir(parents=True, exist_ok=True)
|
||||
geometry_work_dir = validation_dir / ".geometry-work"
|
||||
geometry_result = _run_json(
|
||||
common
|
||||
+ [
|
||||
"validate-folder",
|
||||
str(source.parent),
|
||||
"--designir-dir",
|
||||
str(task_dir),
|
||||
"--rebuilt-dir",
|
||||
str(geometry_work_dir),
|
||||
"--limit",
|
||||
"1",
|
||||
"--boolean",
|
||||
"--boundary-strategy",
|
||||
boundary_strategy,
|
||||
],
|
||||
timeout_seconds=timeout_seconds,
|
||||
stage="Acceptance-Agent geometry validation",
|
||||
)
|
||||
raw_geometry_report = geometry_work_dir / "validation-report.json"
|
||||
geometry_report = json.loads(raw_geometry_report.read_text(encoding="utf-8"))
|
||||
_write_json(geometry_report_path, geometry_report)
|
||||
geometry_pass = geometry_report.get("geometry_pass_count") == 1
|
||||
if not geometry_pass:
|
||||
raise ReconstructionError(
|
||||
"Independent reconstruction did not satisfy the geometry contract; "
|
||||
f"see {geometry_report_path}"
|
||||
)
|
||||
shutil.rmtree(geometry_work_dir)
|
||||
|
||||
attempts: list[dict[str, Any]] = []
|
||||
if payload["document_status"] == "geometry_present":
|
||||
for name, requested_value in _candidate_parameters(
|
||||
payload, maximum_parameters
|
||||
):
|
||||
safe_name = _safe_stem(Path(name))
|
||||
attempt_dir = validation_dir / "parameter-tests" / safe_name
|
||||
try:
|
||||
result = _run_json(
|
||||
common
|
||||
+ [
|
||||
"validate-edit",
|
||||
str(source),
|
||||
str(designir_path),
|
||||
"--parameter",
|
||||
name,
|
||||
"--value",
|
||||
str(requested_value),
|
||||
"--output-dir",
|
||||
str(attempt_dir),
|
||||
],
|
||||
timeout_seconds=timeout_seconds,
|
||||
stage=f"parameter validation ({name})",
|
||||
)
|
||||
report = json.loads(
|
||||
(attempt_dir / "acceptance-report.json").read_text(
|
||||
encoding="utf-8"
|
||||
)
|
||||
)
|
||||
attempts.append(
|
||||
{
|
||||
"parameter": name,
|
||||
"requested_value": requested_value,
|
||||
"accepted": bool(report.get("accepted")),
|
||||
"state": report.get("state"),
|
||||
"report": str(
|
||||
attempt_dir / "acceptance-report.json"
|
||||
),
|
||||
"worker": result,
|
||||
}
|
||||
)
|
||||
except (ReconstructionError, subprocess.TimeoutExpired) as exc:
|
||||
attempts.append(
|
||||
{
|
||||
"parameter": name,
|
||||
"requested_value": requested_value,
|
||||
"accepted": False,
|
||||
"state": "worker_failed",
|
||||
"error": str(exc),
|
||||
}
|
||||
)
|
||||
|
||||
payload, accepted_parameters = _persist_edit_results(
|
||||
designir_path, attempts
|
||||
)
|
||||
payload["reconstruction_strategy"] = {
|
||||
"boundary_strategy": boundary_strategy,
|
||||
"selection_status": "geometry_validated",
|
||||
}
|
||||
_write_json(designir_path, payload)
|
||||
applied_experience_ids = _applied_experience_ids(
|
||||
available_experience_ids, accepted_parameters
|
||||
)
|
||||
edit_report = {
|
||||
"schema_version": "1.0",
|
||||
"report_kind": "cad_router_runtime_parameter_acceptance",
|
||||
"attempt_count": len(attempts),
|
||||
"accepted_count": sum(row["accepted"] for row in attempts),
|
||||
"attempts": attempts,
|
||||
}
|
||||
_write_json(edit_report_path, edit_report)
|
||||
parameter_catalog = {
|
||||
"schema_version": "1.0",
|
||||
"model_id": payload["model_id"],
|
||||
"document_status": payload["document_status"],
|
||||
"parameter_count": len(accepted_parameters),
|
||||
"parameters": accepted_parameters,
|
||||
"policy": "Only parameters that passed an isolated perturbation test are exposed.",
|
||||
}
|
||||
_write_json(parameters_path, parameter_catalog)
|
||||
|
||||
task_manifest = {
|
||||
"schema_version": "1.0",
|
||||
"request": request,
|
||||
"operation": "uploaded_step_reconstruction",
|
||||
"route": {
|
||||
"router": "cad-router",
|
||||
"runtime": "DesignIR 3.0 / SurfaceIR",
|
||||
"selected_backend": "build123d",
|
||||
"execution_adapter": "surfaceir_occt",
|
||||
"source_of_truth": designir_path.name,
|
||||
},
|
||||
"source": {
|
||||
"format": source.suffix.lower().lstrip("."),
|
||||
"role": "teacher_and_acceptance_truth_only",
|
||||
"embedded_in_output": False,
|
||||
},
|
||||
"artifacts": [
|
||||
{"path": rebuilt_path.name, "role": "primary_step"},
|
||||
{"path": designir_path.name, "role": "editable_designir"},
|
||||
{"path": parameters_path.name, "role": "validated_parameters"},
|
||||
{
|
||||
"path": str(geometry_report_path.relative_to(task_dir)),
|
||||
"role": "geometry_acceptance",
|
||||
},
|
||||
{
|
||||
"path": str(edit_report_path.relative_to(task_dir)),
|
||||
"role": "parameter_acceptance",
|
||||
},
|
||||
],
|
||||
"experience": {
|
||||
"library": (
|
||||
str(EXPERIENCE_LIBRARY)
|
||||
if EXPERIENCE_LIBRARY.is_file()
|
||||
else None
|
||||
),
|
||||
"runtime_policy": "promoted_generalized_experience_only",
|
||||
"available_method_count": len(available_experience_ids),
|
||||
"methods_applied": applied_experience_ids,
|
||||
},
|
||||
"validation": {
|
||||
"geometry_accepted": True,
|
||||
"source_independent_rebuild": True,
|
||||
"anti_cheat_pass": True,
|
||||
"validated_parameter_count": len(accepted_parameters),
|
||||
},
|
||||
"edit_command_template": [
|
||||
str(runtime),
|
||||
str(SURFACEIR_PIPELINE),
|
||||
"edit",
|
||||
designir_path.name,
|
||||
"--parameter",
|
||||
"<validated-parameter-name>",
|
||||
"--value",
|
||||
"<value-within-validated-range>",
|
||||
"--output-designir",
|
||||
"<edited.designir.json>",
|
||||
"--output-step",
|
||||
"<edited.step>",
|
||||
],
|
||||
}
|
||||
_write_json(task_manifest_path, task_manifest)
|
||||
manifest_path.unlink(missing_ok=True)
|
||||
|
||||
return {
|
||||
"schema_version": "1.0",
|
||||
"status": (
|
||||
"accepted"
|
||||
if payload["document_status"] == "geometry_present"
|
||||
else "accepted_empty_source"
|
||||
),
|
||||
"task_dir": str(task_dir),
|
||||
"designir": str(designir_path),
|
||||
"rebuilt_step": str(rebuilt_path),
|
||||
"parameters": str(parameters_path),
|
||||
"task_manifest": str(task_manifest_path),
|
||||
"geometry_report": str(geometry_report_path),
|
||||
"edit_report": str(edit_report_path),
|
||||
"geometry_accepted": True,
|
||||
"document_status": payload["document_status"],
|
||||
"validated_parameter_count": len(accepted_parameters),
|
||||
"validated_parameter_names": [
|
||||
parameter["name"] for parameter in accepted_parameters
|
||||
],
|
||||
"experience": {
|
||||
"available_method_count": len(available_experience_ids),
|
||||
"methods_applied": applied_experience_ids,
|
||||
},
|
||||
"source_independence": {
|
||||
"rebuild_command_received_teacher_path": False,
|
||||
**contract_checks,
|
||||
},
|
||||
"stages": {
|
||||
"extraction": extraction,
|
||||
"geometry_validation": geometry_result,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def build_parser() -> argparse.ArgumentParser:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--source", type=Path, required=True)
|
||||
parser.add_argument("--task-dir", type=Path, required=True)
|
||||
parser.add_argument("--request", default="重建上传的 STEP")
|
||||
parser.add_argument("--maximum-parameters", type=int, default=8)
|
||||
parser.add_argument("--timeout-seconds", type=int, default=180)
|
||||
return parser
|
||||
|
||||
|
||||
def main(argv: list[str] | None = None) -> int:
|
||||
args = build_parser().parse_args(argv)
|
||||
try:
|
||||
result = reconstruct_uploaded_step(
|
||||
args.source,
|
||||
args.task_dir,
|
||||
request=args.request,
|
||||
maximum_parameters=args.maximum_parameters,
|
||||
timeout_seconds=args.timeout_seconds,
|
||||
)
|
||||
except (ReconstructionError, subprocess.TimeoutExpired) as exc:
|
||||
print(
|
||||
json.dumps(
|
||||
{"status": "failed", "error": str(exc)},
|
||||
ensure_ascii=False,
|
||||
),
|
||||
file=sys.stderr,
|
||||
)
|
||||
return 1
|
||||
print(json.dumps(result, ensure_ascii=False))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -5,6 +5,7 @@ import argparse
|
||||
import importlib.util
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
from dataclasses import dataclass, field
|
||||
from pathlib import Path
|
||||
@@ -12,6 +13,7 @@ from pathlib import Path
|
||||
|
||||
SKILL_ROOT = Path(__file__).resolve().parents[1]
|
||||
REGISTRY_PATH = Path(__file__).with_name("capabilities.json")
|
||||
RECONSTRUCTION_ADAPTER = Path(__file__).with_name("reconstruct_step.py")
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -335,8 +337,10 @@ def load_experience_library(
|
||||
path: Path | None,
|
||||
family: str | None = None,
|
||||
features: list[str] | None = None,
|
||||
consumer: str = "router",
|
||||
) -> dict[str, object] | None:
|
||||
"""Load promoted generalized methods; reject case parameters and coordinates."""
|
||||
explicit_library = path is not None
|
||||
candidate = path or default_experience_library()
|
||||
if candidate is None:
|
||||
return None
|
||||
@@ -358,15 +362,26 @@ def load_experience_library(
|
||||
or policy.get("single_case_promotion_allowed") is not False
|
||||
):
|
||||
raise ValueError("CAD experience library policy permits instance-answer leakage")
|
||||
if (
|
||||
policy.get("llm_semantic_review_required") is not True
|
||||
or policy.get("draft_evidence_verified") is not True
|
||||
or policy.get("router_consumable") is not True
|
||||
):
|
||||
reviewed = (
|
||||
policy.get("llm_semantic_review_required") is True
|
||||
and policy.get("draft_evidence_verified") is True
|
||||
)
|
||||
consumer_flag = (
|
||||
"surfaceir_consumable"
|
||||
if consumer == "surfaceir"
|
||||
else "router_consumable"
|
||||
)
|
||||
if not reviewed:
|
||||
raise ValueError(
|
||||
"CAD experience library was not produced by LLM semantic review "
|
||||
"plus deterministic evidence verification"
|
||||
)
|
||||
if policy.get(consumer_flag) is not True:
|
||||
if not explicit_library:
|
||||
return None
|
||||
raise ValueError(
|
||||
f"CAD experience library is not approved for {consumer}"
|
||||
)
|
||||
forbidden = _find_forbidden_experience_key(payload)
|
||||
if forbidden:
|
||||
raise ValueError(f"CAD experience library contains forbidden instance key: {forbidden}")
|
||||
@@ -388,6 +403,12 @@ def load_experience_library(
|
||||
if not isinstance(item, dict):
|
||||
continue
|
||||
scope = set(item.get("scope", []))
|
||||
if consumer == "surfaceir":
|
||||
if scope.intersection(
|
||||
{"surfaceir_reconstruction", "surfaceir_semantic_editing"}
|
||||
):
|
||||
selected.append(item)
|
||||
continue
|
||||
if "global" not in scope:
|
||||
if not compatible_scopes or scope.isdisjoint(compatible_scopes):
|
||||
continue
|
||||
@@ -402,6 +423,11 @@ def load_experience_library(
|
||||
"compatible_scopes": sorted(compatible_scopes),
|
||||
"requested_features": sorted(requested_features),
|
||||
"experiences": selected,
|
||||
"selection_policy": (
|
||||
"surfaceir_runtime_conditioned"
|
||||
if consumer == "surfaceir"
|
||||
else "request_family_and_feature_match"
|
||||
),
|
||||
"policy": {
|
||||
"contains_instance_parameters": False,
|
||||
"contains_absolute_coordinates": False,
|
||||
@@ -429,6 +455,7 @@ def route(args: argparse.Namespace) -> dict[str, object]:
|
||||
args.experience_library,
|
||||
experience_family,
|
||||
experience_features,
|
||||
"surfaceir" if teacher_step_reconstruction else "router",
|
||||
)
|
||||
needs_source_classification = bool(edit_context and not experience_family)
|
||||
scores = {
|
||||
@@ -480,7 +507,12 @@ def route(args: argparse.Namespace) -> dict[str, object]:
|
||||
|
||||
registry = json.loads(REGISTRY_PATH.read_text(encoding="utf-8"))
|
||||
backend_info = registry["backends"][selected.name]
|
||||
source_of_truth = "per-part DesignIR 2.0 with an isolated backend compiler"
|
||||
editable_contract = "DesignIR 3.0"
|
||||
source_of_truth = (
|
||||
"per-part DesignIR 3.0 with an isolated SurfaceIR compiler"
|
||||
if teacher_step_reconstruction
|
||||
else "per-part semantic DesignIR 3.0 with an isolated backend compiler"
|
||||
)
|
||||
gap = selected.score - max((scores[name].score for name in scores if name != selected.name), default=0)
|
||||
confidence = "high" if gap >= 40 else "medium" if gap >= 15 else "low"
|
||||
result: dict[str, object] = {
|
||||
@@ -499,7 +531,7 @@ def route(args: argparse.Namespace) -> dict[str, object]:
|
||||
"workflow_profiles": profiles,
|
||||
"artifact_contract": {
|
||||
"primary_format": "step",
|
||||
"editable_contract": "DesignIR 2.0",
|
||||
"editable_contract": editable_contract,
|
||||
},
|
||||
"source_of_truth": source_of_truth,
|
||||
"experience_context": experience_context,
|
||||
@@ -604,9 +636,72 @@ def route(args: argparse.Namespace) -> dict[str, object]:
|
||||
},
|
||||
"availability": probe_backends() if args.probe else None,
|
||||
}
|
||||
if teacher_step_reconstruction:
|
||||
result["execution_adapter"] = {
|
||||
"path": str(RECONSTRUCTION_ADAPTER),
|
||||
"runtime": "DesignIR 3.0 / SurfaceIR",
|
||||
"automatic_geometry_acceptance": True,
|
||||
"automatic_parameter_acceptance": True,
|
||||
}
|
||||
return result
|
||||
|
||||
|
||||
def execute_reconstruction(
|
||||
args: argparse.Namespace, result: dict[str, object]
|
||||
) -> dict[str, object]:
|
||||
edit_context = result.get("design_plan", {}).get("edit_context")
|
||||
if not isinstance(edit_context, dict) or edit_context.get(
|
||||
"modification_mode"
|
||||
) != "private_evidence_to_isolated_designir_reconstruction":
|
||||
raise ValueError(
|
||||
"--execute currently requires --edit-source with one STEP/STP file"
|
||||
)
|
||||
if args.edit_source is None or args.task_dir is None:
|
||||
raise ValueError(
|
||||
"--execute requires both --edit-source and --task-dir"
|
||||
)
|
||||
runtime = os.environ.get("CAD_ROUTER_PYTHON", "").strip()
|
||||
workspace_python = (
|
||||
SKILL_ROOT.parents[2] / "text-to-cad" / ".venv" / "bin" / "python"
|
||||
)
|
||||
interpreter = (
|
||||
Path(runtime).expanduser()
|
||||
if runtime
|
||||
else workspace_python
|
||||
if workspace_python.is_file()
|
||||
else Path(sys.executable)
|
||||
)
|
||||
command = [
|
||||
str(interpreter),
|
||||
str(RECONSTRUCTION_ADAPTER),
|
||||
"--source",
|
||||
str(args.edit_source),
|
||||
"--task-dir",
|
||||
str(args.task_dir),
|
||||
"--request",
|
||||
args.request,
|
||||
"--maximum-parameters",
|
||||
str(args.maximum_parameters),
|
||||
"--timeout-seconds",
|
||||
str(args.timeout_seconds),
|
||||
]
|
||||
completed = subprocess.run(
|
||||
command,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=False,
|
||||
)
|
||||
if completed.returncode != 0:
|
||||
detail = completed.stderr.strip() or completed.stdout.strip()
|
||||
raise RuntimeError(
|
||||
"cad-router STEP reconstruction failed: " + detail[-3000:]
|
||||
)
|
||||
lines = [line for line in completed.stdout.splitlines() if line.strip()]
|
||||
if not lines:
|
||||
raise RuntimeError("cad-router reconstruction returned no result")
|
||||
return json.loads(lines[-1])
|
||||
|
||||
|
||||
def explain(result: dict[str, object]) -> str:
|
||||
lines = [
|
||||
f"Selected backend: {result['selected_backend']}",
|
||||
@@ -665,12 +760,36 @@ def build_parser() -> argparse.ArgumentParser:
|
||||
)
|
||||
parser.add_argument("--explain", action="store_true", help="Print a human-readable decision instead of JSON.")
|
||||
parser.add_argument("--manifest", type=Path, help="Also write the route decision as JSON to this explicit path.")
|
||||
parser.add_argument(
|
||||
"--execute",
|
||||
action="store_true",
|
||||
help="Execute an uploaded STEP reconstruction after routing.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--task-dir",
|
||||
type=Path,
|
||||
help="Explicit output directory for --execute.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--maximum-parameters",
|
||||
type=int,
|
||||
default=8,
|
||||
help="Maximum feature-specific edit candidates to validate; overall_scale is also tested as fallback.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--timeout-seconds",
|
||||
type=int,
|
||||
default=180,
|
||||
help="Timeout for each reconstruction or acceptance worker.",
|
||||
)
|
||||
return parser
|
||||
|
||||
|
||||
def main(argv: list[str] | None = None) -> int:
|
||||
args = build_parser().parse_args(argv)
|
||||
result = route(args)
|
||||
if args.execute:
|
||||
result["execution"] = execute_reconstruction(args, result)
|
||||
payload = json.dumps(result, ensure_ascii=False, indent=2) + "\n"
|
||||
if args.manifest:
|
||||
args.manifest.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib.util
|
||||
import json
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
from build123d import Box, export_step
|
||||
|
||||
|
||||
ADAPTER_PATH = (
|
||||
Path(__file__).resolve().parents[4]
|
||||
/ "skills"
|
||||
/ "cad-router"
|
||||
/ "scripts"
|
||||
/ "reconstruct_step.py"
|
||||
)
|
||||
SPEC = importlib.util.spec_from_file_location(
|
||||
"cad_router_reconstruct_step", ADAPTER_PATH
|
||||
)
|
||||
assert SPEC is not None and SPEC.loader is not None
|
||||
adapter = importlib.util.module_from_spec(SPEC)
|
||||
sys.modules[SPEC.name] = adapter
|
||||
SPEC.loader.exec_module(adapter)
|
||||
|
||||
|
||||
class UploadedStepRuntimeTests(unittest.TestCase):
|
||||
def test_uploaded_step_reconstructs_and_publishes_validated_parameter(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as temporary:
|
||||
root = Path(temporary)
|
||||
upload_dir = root / "upload"
|
||||
task_dir = root / "task"
|
||||
upload_dir.mkdir()
|
||||
source = upload_dir / "box.step"
|
||||
export_step(Box(10, 20, 30), source)
|
||||
|
||||
result = adapter.reconstruct_uploaded_step(
|
||||
source,
|
||||
task_dir,
|
||||
request="重建上传的 STEP",
|
||||
maximum_parameters=0,
|
||||
timeout_seconds=60,
|
||||
)
|
||||
|
||||
self.assertEqual("accepted", result["status"])
|
||||
self.assertTrue(result["geometry_accepted"])
|
||||
self.assertFalse(
|
||||
result["source_independence"][
|
||||
"rebuild_command_received_teacher_path"
|
||||
]
|
||||
)
|
||||
self.assertTrue(
|
||||
result["source_independence"]["anti_cheat_pass"]
|
||||
)
|
||||
self.assertIn(
|
||||
"failure_repair.surface_topology.bind_face_pcurves_and_group_shells",
|
||||
result["experience"]["methods_applied"],
|
||||
)
|
||||
self.assertTrue(Path(result["rebuilt_step"]).is_file())
|
||||
self.assertTrue(Path(result["designir"]).is_file())
|
||||
self.assertFalse(
|
||||
(task_dir / "validation" / ".geometry-work").exists()
|
||||
)
|
||||
|
||||
parameters = json.loads(
|
||||
Path(result["parameters"]).read_text(encoding="utf-8")
|
||||
)
|
||||
self.assertEqual(1, parameters["parameter_count"])
|
||||
self.assertEqual(
|
||||
"overall_scale", parameters["parameters"][0]["name"]
|
||||
)
|
||||
geometry = json.loads(
|
||||
Path(result["geometry_report"]).read_text(encoding="utf-8")
|
||||
)
|
||||
self.assertEqual(1, geometry["geometry_pass_count"])
|
||||
self.assertEqual(0, geometry["geometry_fail_count"])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -37,7 +37,7 @@ class CadRouterTests(unittest.TestCase):
|
||||
self.assertEqual(
|
||||
{
|
||||
"primary_format": "step",
|
||||
"editable_contract": "DesignIR 2.0",
|
||||
"editable_contract": "DesignIR 3.0",
|
||||
},
|
||||
result["artifact_contract"],
|
||||
)
|
||||
@@ -345,6 +345,26 @@ class CadRouterTests(unittest.TestCase):
|
||||
self.assertTrue(
|
||||
plan["experience_query_plan"]["requires_source_inspection"]
|
||||
)
|
||||
self.assertEqual(
|
||||
"DesignIR 3.0", result["artifact_contract"]["editable_contract"]
|
||||
)
|
||||
self.assertEqual(
|
||||
"DesignIR 3.0 / SurfaceIR",
|
||||
result["execution_adapter"]["runtime"],
|
||||
)
|
||||
self.assertEqual(
|
||||
"surfaceir_runtime_conditioned",
|
||||
result["experience_context"]["selection_policy"],
|
||||
)
|
||||
self.assertGreater(
|
||||
len(result["experience_context"]["experiences"]), 0
|
||||
)
|
||||
|
||||
def test_execute_requires_step_source_and_task_directory(self) -> None:
|
||||
args = self.parse("生成一个法兰", "--execute")
|
||||
result = route_module.route(args)
|
||||
with self.assertRaisesRegex(ValueError, "requires --edit-source"):
|
||||
route_module.execute_reconstruction(args, result)
|
||||
|
||||
def test_native_generator_is_preferred_for_modification(self) -> None:
|
||||
result = route_module.route(
|
||||
|
||||
Reference in New Issue
Block a user