fix: stage CAD repair validation gates
This commit is contained in:
parent
cbdddb4eb4
commit
1f1e878b75
@ -321,16 +321,19 @@ def build_and_authoritatively_validate(ai3d, build_dir: pathlib.Path, source_nam
|
||||
|
||||
|
||||
def round_blocker_count(row: dict) -> int:
|
||||
"""Count end-to-end blockers for monotonic repair acceptance.
|
||||
"""Count blockers at the currently authoritative validation stage.
|
||||
|
||||
Compiler issues alone are insufficient: an edit can remove a loose-bounds
|
||||
warning while creating more exact B-rep clashes. A candidate therefore
|
||||
has to improve the complete compile/manufacturing/kernel/visual chain.
|
||||
B-rep and visual diagnostics generated from a graph that still fails its
|
||||
structural compiler are useful artifacts, but they are not stable repair
|
||||
targets: moving a part to address one such clash can multiply topology
|
||||
errors. Until compilation passes, compare compiler blockers only. After it
|
||||
passes, require monotonic improvement across the later gates as before.
|
||||
"""
|
||||
compiled = row.get("compiled") or {}
|
||||
total = len(compiled.get("issues") or [])
|
||||
if compiled.get("ok"):
|
||||
total += len((row.get("manufacturing_plan") or {}).get("issues") or [])
|
||||
if not compiled.get("ok"):
|
||||
return total
|
||||
total += len((row.get("manufacturing_plan") or {}).get("issues") or [])
|
||||
kernel_feedback = row.get("cad_kernel_feedback") or []
|
||||
total += len(kernel_feedback)
|
||||
if row.get("cad_kernel_error") and not kernel_feedback:
|
||||
@ -361,6 +364,36 @@ def has_minimum_axis_packing_dead_end(feedback: list[dict]) -> bool:
|
||||
)
|
||||
|
||||
|
||||
def qualified_occupied_scope_adjudication(dimension_ledger: dict, axis: str) -> dict | None:
|
||||
"""Return an already-qualified overall-arrow fact for one axis.
|
||||
|
||||
Dimension qualification is independent from graph generation. When it has
|
||||
accepted an arrow explicitly scoped as ``overall``, a later collision is
|
||||
evidence that the graph is wrong, not that the drawing became ambiguous.
|
||||
"""
|
||||
fact = (dimension_ledger or {}).get(axis) or {}
|
||||
if fact.get("ledger_scope") != "occupied_overall":
|
||||
return None
|
||||
source_ids = sorted(set(fact.get("source_image_ids") or []))
|
||||
if not source_ids:
|
||||
return None
|
||||
return {
|
||||
"decision": "occupied_overall_dimension",
|
||||
"axis": axis,
|
||||
"confidence": round(float(fact.get("confidence") or 1.0), 4),
|
||||
"evidence_image_ids": source_ids,
|
||||
"evidence": (
|
||||
f"尺寸资格门已将{axis.upper()}方向箭头验收为成品整体外形:"
|
||||
f"{fact.get('text') or fact.get('value_mm')}"
|
||||
)[:1000],
|
||||
"requires_user_question": False,
|
||||
"question_focus": None,
|
||||
"single_user_question": None,
|
||||
"question_options": [],
|
||||
"source": "qualified_overall_dimension_ledger",
|
||||
}
|
||||
|
||||
|
||||
def normalize_focused_source_adjudication(
|
||||
value, *, image_ids: set[str], required_question_focus: str | None = None,
|
||||
) -> dict:
|
||||
@ -1456,9 +1489,18 @@ def main():
|
||||
+ evidence_context
|
||||
)
|
||||
if graph:
|
||||
compile_gate_instruction = ""
|
||||
if accepted_compile_issue_count not in (None, 0):
|
||||
compile_gate_instruction = (
|
||||
f"当前结构编译器仍有{accepted_compile_issue_count}项阻塞;本轮只能修改"
|
||||
"编译器反馈中明确列出的稳定ID和路径,不得为了诊断草图的"
|
||||
"B-rep碰撞或外观问题改动其他零件、总成、尺寸或定位。"
|
||||
"一次只修一个反馈优先级的问题族,修复后必须保留其他已通过约束。"
|
||||
)
|
||||
prompt = (
|
||||
"你在修正一份已存在的参数化家具结构图谱。必须回看本轮所有原图,并只返回局部JSON修改,不能返回整份图谱。"
|
||||
"每条操作为add/replace/remove和JSON Pointer路径;修复引用、参数、实例位置、接触面或遗漏部件。"
|
||||
+ compile_gate_instruction
|
||||
+ "每条操作为add/replace/remove和JSON Pointer路径;修复引用、参数、实例位置、接触面或遗漏部件。"
|
||||
"不能通过放大公差、删除有原图证据的部件/连接或改低置信度来逃避问题。"
|
||||
"一次最多24条patch,优先级固定为:原图整体尺寸冲突、总成局部边界错误、五金安装宿主/安装面错误、实体穿插、已定义却无实例的部件、总成归属/总成关系错误、几何未真实接触、其他外观差异。"
|
||||
"若零件observed_quantity大于实例数,必须为每个真实物理件补instances,不能只保留零件定义。"
|
||||
@ -1677,6 +1719,15 @@ def main():
|
||||
if not row.get("patch_error") and isinstance(row.get("compiled"), dict):
|
||||
candidate_blocker_count = round_blocker_count(row)
|
||||
row["end_to_end_blocker_count"] = candidate_blocker_count
|
||||
if row["compiled"].get("ok") is not True:
|
||||
# Keep diagnostic kernel/render artifacts in the report,
|
||||
# but do not feed their secondary failures into the next
|
||||
# graph repair while the structural contract is invalid.
|
||||
feedback = enrich_hardware_feedback_with_verified_placements(
|
||||
graph, row["compiled"], list(row["compiled"].get("issues") or []),
|
||||
compile_candidate=compile_candidate_graph,
|
||||
)
|
||||
row["repair_feedback_stage"] = "compiler_only"
|
||||
if (
|
||||
round_base_graph
|
||||
and accepted_total_blocker_count is not None
|
||||
@ -1723,6 +1774,7 @@ def main():
|
||||
}
|
||||
if (
|
||||
row.get("patch_error")
|
||||
and round_base_compile_issue_count == 0
|
||||
and has_minimum_axis_packing_dead_end(round_base_feedback)
|
||||
and not (resolved_scope_axes & dead_end_axes)
|
||||
):
|
||||
@ -1750,8 +1802,12 @@ def main():
|
||||
"case": case, "phase": "ai_focused_source_adjudication",
|
||||
"dead_end_count": len(dead_end_feedback),
|
||||
}, ensure_ascii=False), flush=True)
|
||||
qualified_overall = (
|
||||
qualified_occupied_scope_adjudication(dimension_ledger, dead_end_axis)
|
||||
if required_question_focus == "overall_dimension_scope" else None
|
||||
)
|
||||
try:
|
||||
adjudication_response = engine._request_model_json({
|
||||
adjudication_response = qualified_overall or engine._request_model_json({
|
||||
"model": args.model,
|
||||
"messages": [{"role": "user", "content": images + [{
|
||||
"type": "text", "text": (
|
||||
@ -1794,6 +1850,31 @@ def main():
|
||||
else:
|
||||
report["status"] = "focused_source_adjudication_ready_for_graph_repair"
|
||||
report["blocking"]["next_owner"] = "graph_repair_with_independent_scope_adjudication"
|
||||
scope_fact = normalize_scope_facts({
|
||||
"id": f"qualified-scope-{focused['axis']}",
|
||||
"source_type": "independent_source_adjudication",
|
||||
"axis": focused["axis"],
|
||||
"ledger_scope": (
|
||||
"occupied_overall"
|
||||
if focused["decision"] == "occupied_overall_dimension" else "reference"
|
||||
),
|
||||
"confidence": focused["confidence"],
|
||||
"source_image_ids": focused["evidence_image_ids"],
|
||||
"text": focused["evidence"],
|
||||
}, image_ids=image_ids, dimension_ledger=dimension_ledger)[0]
|
||||
scope_facts = [
|
||||
item for item in scope_facts if item["axis"] != scope_fact["axis"]
|
||||
] + [scope_fact]
|
||||
graph, bound_user_evidence, bound_scope_adjudications, binding_audit = bind_scope_facts(
|
||||
graph, scope_facts,
|
||||
)
|
||||
report["focused_scope_binding_audit"] = binding_audit
|
||||
feedback = [{
|
||||
"code": "qualified_overall_scope_preserved",
|
||||
"axis": focused["axis"],
|
||||
"detail": "整体尺寸已由原图资格门确认为成品外包围,禁止再询问或改为柜体参考尺寸。",
|
||||
"correction": "保持该整体尺寸,仅修正门板/部件厚度、内嵌关系、局部位置或有原图依据的避让加工。",
|
||||
}] + dead_end_feedback
|
||||
except Exception as exc:
|
||||
report["focused_source_adjudication_error"] = f"{type(exc).__name__}: {exc}"
|
||||
focused = deterministic_dead_end_question(
|
||||
@ -1808,6 +1889,8 @@ def main():
|
||||
(case_dir / "report.json").write_text(
|
||||
json.dumps(report, ensure_ascii=False, indent=2), encoding="utf-8"
|
||||
)
|
||||
if report["status"] == "focused_source_adjudication_ready_for_graph_repair":
|
||||
continue
|
||||
break
|
||||
if (
|
||||
row.get("patch_error")
|
||||
|
||||
@ -133,6 +133,13 @@ def build_dimension_ledger(labels: list[dict], *, source_image_id: str) -> dict:
|
||||
"source_image_ids": [source_image_id],
|
||||
"text": str(row.get("text") or "")[:120],
|
||||
"location": str(row.get("location") or "")[:200],
|
||||
# This ledger accepts only labels whose independent reader has
|
||||
# already classified the arrow as ``scope=overall``. Persist that
|
||||
# semantic contract so a later CAD collision cannot turn an
|
||||
# authoritative finished-product dimension into a carcass/opening
|
||||
# dimension merely to make an invalid graph fit.
|
||||
"ledger_scope": "occupied_overall",
|
||||
"scope_source": "qualified_overall_arrow",
|
||||
})
|
||||
ledger = {"conflicts": [], "rejected": rejected}
|
||||
for axis, rows in candidates.items():
|
||||
|
||||
@ -17,6 +17,7 @@ from probe_structure_graph import (
|
||||
diagnostic_mounting_clearance_cuts, has_minimum_axis_packing_dead_end,
|
||||
enrich_hardware_feedback_with_verified_placements,
|
||||
normalize_focused_source_adjudication, normalize_scope_facts,
|
||||
qualified_occupied_scope_adjudication,
|
||||
normalize_visual_review,
|
||||
propose_reference_scope_projection, refresh_compiled_kernel_feedback,
|
||||
round_blocker_count, select_resume_checkpoint, source_for_role,
|
||||
@ -439,7 +440,7 @@ class ManufacturingOracleTest(unittest.TestCase):
|
||||
self.assertEqual({"x": 12.0, "y": 20.0, "z": 30.0}, feedback[0]["overlap_size_mm"])
|
||||
self.assertIn("/instances/left-door/placement", feedback[0]["legal_patch_roots"])
|
||||
|
||||
def test_repair_score_counts_kernel_regression_after_compile_improvement(self):
|
||||
def test_repair_score_uses_compiler_gate_before_kernel_and_visual(self):
|
||||
previous = {
|
||||
"compiled": {"ok": False, "issues": [{}, {}, {}, {}, {}, {}, {}]},
|
||||
"cad_kernel_feedback": [{}, {}, {}],
|
||||
@ -448,9 +449,9 @@ class ManufacturingOracleTest(unittest.TestCase):
|
||||
"compiled": {"ok": False, "issues": [{}, {}, {}, {}, {}]},
|
||||
"cad_kernel_feedback": [{}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}],
|
||||
}
|
||||
self.assertEqual(10, round_blocker_count(previous))
|
||||
self.assertEqual(20, round_blocker_count(candidate))
|
||||
self.assertGreater(round_blocker_count(candidate), round_blocker_count(previous))
|
||||
self.assertEqual(7, round_blocker_count(previous))
|
||||
self.assertEqual(5, round_blocker_count(candidate))
|
||||
self.assertLess(round_blocker_count(candidate), round_blocker_count(previous))
|
||||
|
||||
compile_pass_with_hidden_review_failure = {
|
||||
"compiled": {"ok": True, "issues": []},
|
||||
@ -459,6 +460,19 @@ class ManufacturingOracleTest(unittest.TestCase):
|
||||
}
|
||||
self.assertEqual(1, round_blocker_count(compile_pass_with_hidden_review_failure))
|
||||
|
||||
def test_qualified_overall_arrow_cannot_be_reopened_by_cad_collision(self):
|
||||
decision = qualified_occupied_scope_adjudication({
|
||||
"y": {
|
||||
"value_mm": 400, "confidence": 0.97,
|
||||
"source_image_ids": ["image4"], "text": "400",
|
||||
"ledger_scope": "occupied_overall",
|
||||
},
|
||||
}, "y")
|
||||
self.assertEqual("occupied_overall_dimension", decision["decision"])
|
||||
self.assertFalse(decision["requires_user_question"])
|
||||
self.assertEqual("qualified_overall_dimension_ledger", decision["source"])
|
||||
self.assertIsNone(qualified_occupied_scope_adjudication({"y": {"value_mm": 400}}, "y"))
|
||||
|
||||
def test_compile_lower_bound_compares_with_end_to_end_score(self):
|
||||
self.assertTrue(compile_lower_bound_can_improve(4, 10))
|
||||
self.assertFalse(compile_lower_bound_can_improve(10, 10))
|
||||
|
||||
@ -598,6 +598,8 @@ class StructureGraphTest(unittest.TestCase):
|
||||
{"axis": "z", "value": 1100, "unit": "mm", "scope": "overall", "confidence": 0.72},
|
||||
], source_image_id="dimension")
|
||||
self.assertEqual(800, ledger["x"]["value_mm"])
|
||||
self.assertEqual("occupied_overall", ledger["x"]["ledger_scope"])
|
||||
self.assertEqual("qualified_overall_arrow", ledger["x"]["scope_source"])
|
||||
self.assertNotIn("y", ledger)
|
||||
self.assertNotIn("z", ledger)
|
||||
conflicted = build_dimension_ledger([
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user