integrate ganjihong engine refactor

This commit is contained in:
2026-09-09 18:41:56 +08:00
parent cf9cd0366a
commit 21a5185926
116 changed files with 3596 additions and 12259 deletions
@@ -1,41 +1,17 @@
"""#2 draft 假接受陷阱:extrudeParams.draft 必须被显式拒绝,而不是静默忽略。
"""Extrude draft contracts distinguish executable and unsupported extents.
中文说明
--------
这个文件在测试什么(issue #2「draft 被 schema 接受但 runtime 未执行」的回归测试):
1. 背景:三方合同错位——
- 机器契约 cdsl_schema.json(extrudeParams,约 101 行)允许
"draft": {"type": "object"},且是空 object(无任何子字段约束),
即"文档格式假接受";
- 人读契约 profile_schema.json 的 extrude_add_blind /
extrude_add_two_sided / extrude_cut_blind 均**未**声明 draft
为 optional_params;
- runtime(build123d_adapter.extrude 仅调 Solid.extrude,无锥形
拉伸)也完全没有 draft 实现,遇到 draft 就静默忽略。
- 隐患:importer(translator.py 已解析 SolidWorks 的
draft_angle_rad / reverse_draft_angle_rad)一旦把拔模角写进
CDSL params,runtime 会静默产出**无拔模角的直壁实体**——
注塑件/压铸件丢失脱模斜度,脱模卡死、分型面配合错误,且全程
无警告(与 #1 y_dir / #3 revolve.reverse 同族的静默错误)。
CADFS lowering now maps a single-sided blind draft to
``Build123dGeometryAdapter.extrude_taper``. The CDSL machine schema uses
``angle_deg`` plus ``pull_direction`` for that executable contract.
2. 修复策略:因为 build123d 内核没有锥形拉伸能力、且人读契约未声明
draft,正确的合同是"显式拒绝"而不是"实现拔模"——
capabilities.py 对携带 draft 的 extrude 特征报 unsupported_draft
blocker(与 unsupported_extent 同模式)。schema 字段保留(文档格式
契约,importer 未来可能产出),能力层明确划界。
3. 本测试套件把"draft 必须显式拒绝"固定下来:
- 主契约:带 draft 的 extrude 特征 → analyze 报 unsupported_draft
blocker,runtime_eligible=False;
- 回归护栏:不带 draft 的 extrude 特征 → 仍 runtime_eligible;
- 文档格式契约:带 draft 的文档仍能通过 cdsl_schema.json(拒绝
发生在能力层,不是 schema 层);
- 覆盖:extrude_add_blind / extrude_add_two_sided / extrude_cut_blind
三种原子都报同一 blocker(同一检查全类生效);
- 端到端:rebuild_cdsl(strict)带 draft → 抛 ValueError(大声失败),
analyze_document → runtime_eligible=False 且不 built(批量重建
不被静默污染)。
A two-sided or non-blind draft has no defined neutral-plane semantics in
the current CDSL runtime. It must remain in the input document but stop
with ``unsupported_draft_extent`` before execution. Non-canonical draft
fields are protocol errors and are rejected by the machine schema.
4. sys.path 说明:把 backend/engine 加入搜索路径,是为了直接 import
cdsl_engine 包做端到端测试(与 test_engine_revolve_reverse.py 风格
@@ -97,15 +73,16 @@ def _rectangle(minimum: list[float], maximum: list[float]) -> dict:
def _extrude_cdsl(*, atomic_id: str = "extrude_add_blind", with_draft: bool = True) -> dict:
"""构造最小 extrude 文档。
- with_draft=True 时 params 携带 draft 对象(任意非空 object 即可,
因为 cdsl_schema.json 对 draft 没有子字段约束);
- with_draft=True 时 params 携带可执行的单侧盲向 draft 对象;
- with_draft=False 时完全不写 draft 字段(回归护栏用)。
- atomic_id 可切换 extrude_add_blind / extrude_add_two_sided /
extrude_cut_blind 三种原子,验证同一 blocker 检查对全类生效。
"""
params: dict = {"distance_mm": 10.0}
if atomic_id == "extrude_add_two_sided":
params["reverse_distance_mm"] = 10.0
if with_draft:
params["draft"] = {"angle_deg": 5.0, "direction": "toward_sketch"}
params["draft"] = {"angle_deg": 5.0, "pull_direction": True}
return {
"schema": "cad.cdsl.llm.v1", "schema_version": "1.1.0", "kind": "part",
"part_id": "draft-contract", "meta": {"unit": "mm"},
@@ -116,7 +93,7 @@ def _extrude_cdsl(*, atomic_id: str = "extrude_add_blind", with_draft: bool = Tr
}]},
"features": [{
"id": "base_add", "atomic_id": atomic_id, "depends_on": [], "sketch_id": "base",
"params": params,
"params": params, "execution_status": "supported",
}],
}
@@ -134,20 +111,13 @@ def _validate_against_cdsl_schema(doc: dict) -> None:
class ExtrudeDraftContractTests(unittest.TestCase):
"""draft 假接受陷阱「文档格式-能力边界-运行时」三方合同的回归测试。"""
def test_draft_extrude_is_explicitly_blocked(self) -> None:
"""主契约:带 draft 的 extrude 特征必须被显式拒绝,而不是静默通过。
修复前(当前):capabilities 对 extrude 只检查 end_condition,
draft 字段完全无人过问 → analyze 报 runtime_eligible=True,
rebuild 静默产出直壁实体 → 本测试红灯。
修复后:capabilities 报 unsupported_draft blocker →
runtime_eligible=False → 绿灯。
"""
def test_blind_draft_extrude_is_runtime_eligible(self) -> None:
"""A canonical single-sided blind draft reaches the taper executor."""
analysis = analyze_cdsl(_extrude_cdsl(with_draft=True))
self.assertFalse(analysis.runtime_eligible)
self.assertTrue(analysis.runtime_eligible)
result = next(item for item in analysis.feature_results if item.feature_id == "base_add")
self.assertIn("unsupported_draft", [blocker.code for blocker in result.blockers])
self.assertNotIn("unsupported_draft_extent", [blocker.code for blocker in result.blockers])
def test_draft_free_extrude_stays_eligible(self) -> None:
"""回归护栏:不带 draft 的 extrude 特征仍必须 runtime_eligible。
@@ -160,52 +130,38 @@ class ExtrudeDraftContractTests(unittest.TestCase):
self.assertTrue(analysis.runtime_eligible)
def test_draft_passes_machine_schema(self) -> None:
"""文档格式契约:带 draft 的文档仍能通过 cdsl_schema.json 校验。
cdsl_schema.json(extrudeParams)保留 draft 字段,拒绝发生在
能力层(capabilities),不是 schema 层。这条测试锁死"schema 允许
+ 能力拒绝"的分层职责,防止未来把 schema 改过头(删掉字段后
importer 未来产出 draft 会直接被 schema 打回,失去可诊断性)。
"""
"""Canonical draft input is a valid CDSL document for each extrude form."""
_validate_against_cdsl_schema(_extrude_cdsl(atomic_id="extrude_add_blind", with_draft=True))
_validate_against_cdsl_schema(_extrude_cdsl(atomic_id="extrude_add_two_sided", with_draft=True))
_validate_against_cdsl_schema(_extrude_cdsl(atomic_id="extrude_cut_blind", with_draft=True))
def test_draft_blocks_every_extrude_atomic(self) -> None:
"""覆盖:三种 extrude 原子都报同一个 unsupported_draft blocker。
def test_two_sided_draft_is_explicitly_blocked(self) -> None:
analysis = analyze_cdsl(_extrude_cdsl(atomic_id="extrude_add_two_sided", with_draft=True))
draft 检查挂在 _SKETCH_ATOM_PREFIXES(extrude_/revolve_)公共入口,
必须对 extrude_add_blind / extrude_add_two_sided / extrude_cut_blind
同时生效,而不是只修了某一个。
"""
for atomic_id in ("extrude_add_blind", "extrude_add_two_sided", "extrude_cut_blind"):
with self.subTest(atomic_id=atomic_id):
analysis = analyze_cdsl(_extrude_cdsl(atomic_id=atomic_id, with_draft=True))
result = next(item for item in analysis.feature_results if item.feature_id == "base_add")
self.assertIn("unsupported_draft", [blocker.code for blocker in result.blockers])
self.assertFalse(analysis.runtime_eligible)
result = next(item for item in analysis.feature_results if item.feature_id == "base_add")
self.assertIn("unsupported_draft_extent", [blocker.code for blocker in result.blockers])
def test_draft_rebuild_fails_loudly_not_silently(self) -> None:
"""端到端:draft 文档的重建必须大声失败,而不是静默产出直壁实体。
def test_blind_draft_rebuilds_and_batch_reports_an_artifact(self) -> None:
"""The supported branch builds a STEP artifact through both entry points."""
with tempfile.TemporaryDirectory() as directory:
out_step = Path(directory) / "part.step"
rebuild_cdsl(_extrude_cdsl(with_draft=True), out_step)
self.assertTrue(out_step.exists())
修复前:rebuild_cdsl(strict)对 draft 视而不见 → 正常返回实体,
volume > 0,但几何是**没有拔模角的直壁**——静默错误。
修复后:rebuild_cdsl(strict)因 runtime_eligible=False 抛
ValueError(feature is not runtime eligible: unsupported_draft);
analyze_document 报告 runtime_eligible=False 且不 built——
批量重建不会被静默污染。
"""
# 1) strict 重建直接抛错(大声失败)。
with self.assertRaisesRegex(ValueError, "unsupported_draft"):
with tempfile.TemporaryDirectory() as directory:
rebuild_cdsl(_extrude_cdsl(with_draft=True), Path(directory) / "part.step")
# 2) 批量层 analyze_document 报告不可执行且不产出 STEP。
cdsl = _extrude_cdsl(with_draft=True)
with tempfile.TemporaryDirectory() as directory:
out_step = Path(directory) / "part.step"
report = _analyze_inline(cdsl, out_step)
self.assertFalse(report["runtime_eligible"])
self.assertFalse(report.get("built", False))
self.assertTrue(out_step.exists())
self.assertTrue(report["runtime_eligible"])
self.assertTrue(report["built"])
def test_noncanonical_draft_is_rejected_by_machine_schema(self) -> None:
cdsl = _extrude_cdsl(with_draft=True)
cdsl["features"][0]["params"]["draft"] = {"angle_deg": 5.0, "direction": "toward_sketch"}
with self.assertRaises(jsonschema.ValidationError):
_validate_against_cdsl_schema(cdsl)
def _analyze_inline(cdsl: dict, out_step: Path) -> dict: