添加cadfs_to_cdsl代码,优化cad生成

This commit is contained in:
2026-09-03 10:40:51 +08:00
parent f3eb5dff54
commit c25b73c71e
34 changed files with 3586 additions and 147 deletions
@@ -0,0 +1,265 @@
"""Immutable feature-DAG planning contracts and deterministic scheduling."""
from __future__ import annotations
from hashlib import sha256
import json
from typing import Any
from pydantic import BaseModel, ConfigDict, Field, model_validator
class _StrictModel(BaseModel):
model_config = ConfigDict(extra="forbid", strict=True, str_strip_whitespace=True)
class FeatureNode(_StrictModel):
node_id: str = Field(pattern=r"^[a-z][a-z0-9_:-]{0,95}$")
priority: int = Field(ge=0, le=100_000)
intent: str = Field(min_length=1, max_length=360)
atomic_id: str = Field(pattern=r"^[a-z][a-z0-9_:-]{0,95}$")
depends_on: list[str] = Field(default_factory=list, max_length=64)
claim_ids: list[str] = Field(default_factory=list, max_length=128)
expected_change: str = Field(min_length=1, max_length=360)
@model_validator(mode="after")
def _unique_references(self) -> "FeatureNode":
if len(self.depends_on) != len(set(self.depends_on)):
raise ValueError("depends_on must not contain duplicates")
if len(self.claim_ids) != len(set(self.claim_ids)):
raise ValueError("claim_ids must not contain duplicates")
if self.node_id in self.depends_on:
raise ValueError("a feature node cannot depend on itself")
return self
class FeaturePlan(_StrictModel):
schema_version: str = Field(pattern=r"^cad\.v3\.2\.feature-plan\.v1$")
parent_plan_hash: str = Field(default="", pattern=r"^(|[a-f0-9]{64})$")
replaces_node_ids: list[str] = Field(default_factory=list, max_length=128)
nodes: list[FeatureNode] = Field(min_length=1, max_length=256)
final_claim_ids: list[str] = Field(default_factory=list, max_length=128)
@model_validator(mode="after")
def _unique_plan_fields(self) -> "FeaturePlan":
node_ids = [node.node_id for node in self.nodes]
priorities = [node.priority for node in self.nodes]
if len(node_ids) != len(set(node_ids)):
raise ValueError("node_id values must be unique")
if len(priorities) != len(set(priorities)):
raise ValueError("priority values must be unique")
if len(self.replaces_node_ids) != len(set(self.replaces_node_ids)):
raise ValueError("replaces_node_ids must not contain duplicates")
if len(self.final_claim_ids) != len(set(self.final_claim_ids)):
raise ValueError("final_claim_ids must not contain duplicates")
return self
def plan_hash(plan: FeaturePlan | dict[str, Any]) -> str:
payload = plan.model_dump(mode="json") if isinstance(plan, FeaturePlan) else plan
return sha256(json.dumps(payload, ensure_ascii=True, sort_keys=True, separators=(",", ":")).encode("utf-8")).hexdigest()
def node_hash(node: FeatureNode | dict[str, Any]) -> str:
payload = node.model_dump(mode="json") if isinstance(node, FeatureNode) else node
return sha256(json.dumps(payload, ensure_ascii=True, sort_keys=True, separators=(",", ":")).encode("utf-8")).hexdigest()
def contract_claims(contract: dict[str, Any]) -> dict[str, dict[str, Any]]:
result: dict[str, dict[str, Any]] = {}
for requirement in contract.get("requirements") or ():
if not isinstance(requirement, dict):
continue
for claim in requirement.get("acceptance_claims") or ():
if isinstance(claim, dict) and isinstance(claim.get("claim_id"), str):
result[claim["claim_id"]] = claim
return result
def validate_feature_plan(
plan: FeaturePlan,
contract: dict[str, Any],
atomic_ids: set[str] | frozenset[str] | tuple[str, ...],
*,
previous_plan: FeaturePlan | None = None,
completed_node_hashes: dict[str, str] | None = None,
required_replacements: set[str] | None = None,
) -> list[dict[str, str]]:
"""Return stable, tool-facing validation errors for a frozen plan."""
errors: list[dict[str, str]] = []
nodes = {node.node_id: node for node in plan.nodes}
known_atoms = set(atomic_ids)
claims = contract_claims(contract)
assigned: dict[str, str] = {}
for index, node in enumerate(plan.nodes):
prefix = f"/nodes/{index}"
if node.atomic_id not in known_atoms:
errors.append({"path": f"{prefix}/atomic_id", "message": "atomic_id is not supported by the runtime"})
for dependency in node.depends_on:
if dependency not in nodes:
errors.append({"path": f"{prefix}/depends_on", "message": f"unknown dependency '{dependency}'"})
for claim_id in node.claim_ids:
claim = claims.get(claim_id)
if claim is None:
errors.append({"path": f"{prefix}/claim_ids", "message": f"unknown frozen claim '{claim_id}'"})
continue
if claim.get("verification_mode") != "deterministic":
errors.append({"path": f"{prefix}/claim_ids", "message": f"visual claim '{claim_id}' belongs in final_claim_ids"})
owner = assigned.setdefault(claim_id, node.node_id)
if owner != node.node_id:
errors.append({"path": f"{prefix}/claim_ids", "message": f"claim '{claim_id}' is already owned by '{owner}'"})
final = set(plan.final_claim_ids)
for claim_id in plan.final_claim_ids:
claim = claims.get(claim_id)
if claim is None:
errors.append({"path": "/final_claim_ids", "message": f"unknown frozen claim '{claim_id}'"})
elif claim.get("verification_mode") == "deterministic":
errors.append({"path": "/final_claim_ids", "message": f"deterministic claim '{claim_id}' must belong to one node"})
for claim_id, claim in claims.items():
deterministic = claim.get("verification_mode") == "deterministic"
if deterministic and claim_id not in assigned:
errors.append({"path": "/nodes", "message": f"deterministic claim '{claim_id}' has no owner"})
if not deterministic and claim_id not in final:
errors.append({"path": "/final_claim_ids", "message": f"visual claim '{claim_id}' has no final-review owner"})
if deterministic and claim_id in final:
errors.append({"path": "/final_claim_ids", "message": f"deterministic claim '{claim_id}' has two owners"})
errors.extend(_cycle_errors(nodes))
if previous_plan is None:
if plan.parent_plan_hash:
errors.append({"path": "/parent_plan_hash", "message": "initial plan cannot have a parent_plan_hash"})
if plan.replaces_node_ids:
errors.append({"path": "/replaces_node_ids", "message": "initial plan cannot replace nodes"})
else:
errors.extend(_revision_errors(plan, previous_plan, completed_node_hashes or {}, required_replacements or set()))
return errors
def _cycle_errors(nodes: dict[str, FeatureNode]) -> list[dict[str, str]]:
visiting: set[str] = set()
visited: set[str] = set()
errors: list[dict[str, str]] = []
def walk(current: str) -> None:
if current in visiting:
errors.append({"path": "/nodes", "message": "feature dependencies contain a cycle"})
return
if current in visited:
return
visiting.add(current)
for dependency in nodes[current].depends_on:
if dependency in nodes:
walk(dependency)
visiting.remove(current)
visited.add(current)
for node_id in nodes:
walk(node_id)
return errors[:1]
def _revision_errors(plan: FeaturePlan, previous: FeaturePlan, completed: dict[str, str], required_replacements: set[str]) -> list[dict[str, str]]:
errors: list[dict[str, str]] = []
old_nodes = {node.node_id: node for node in previous.nodes}
next_nodes = {node.node_id: node for node in plan.nodes}
if plan.parent_plan_hash != plan_hash(previous):
errors.append({"path": "/parent_plan_hash", "message": "parent_plan_hash does not match the active plan"})
replaced = set(plan.replaces_node_ids)
if required_replacements and replaced != required_replacements:
errors.append({"path": "/replaces_node_ids", "message": "failed node and its unresolved downstream subgraph must be replaced together"})
for node_id, frozen_hash in completed.items():
node = next_nodes.get(node_id)
if node is None:
errors.append({"path": "/nodes", "message": f"completed node '{node_id}' was removed"})
elif node_hash(node) != frozen_hash:
errors.append({"path": "/nodes", "message": f"completed node '{node_id}' was modified"})
if node_id in replaced:
errors.append({"path": "/replaces_node_ids", "message": f"completed node '{node_id}' cannot be replaced"})
for node_id, old_node in old_nodes.items():
if node_id in replaced:
continue
current = next_nodes.get(node_id)
if current is None:
errors.append({"path": "/nodes", "message": f"unrelated node '{node_id}' was removed outside the replacement subgraph"})
elif node_hash(current) != node_hash(old_node):
errors.append({"path": "/nodes", "message": f"unrelated node '{node_id}' was modified outside the replacement subgraph"})
for node_id in replaced:
if node_id not in old_nodes:
errors.append({"path": "/replaces_node_ids", "message": f"unknown replaced node '{node_id}'"})
if node_id in next_nodes:
errors.append({"path": "/nodes", "message": f"replacement must use a new node_id, found '{node_id}'"})
return errors
class FeatureScheduler:
"""Derive a plan's runnable node from immutable ledger evidence."""
def __init__(self, plan: FeaturePlan, events: list[dict[str, Any]]) -> None:
self.plan = plan
self.events = events
self._nodes = {node.node_id: node for node in plan.nodes}
def statuses(self) -> dict[str, str]:
states: dict[str, str] = {node_id: "pending" for node_id in self._nodes}
expected_hashes = {node_id: node_hash(node) for node_id, node in self._nodes.items()}
for event in self.events:
node_id = str(event.get("node_id") or "")
if node_id not in states or event.get("node_hash") != expected_hashes[node_id]:
continue
if event.get("event") == "feature_node_verified":
states[node_id] = "done"
elif event.get("event") == "feature_node_invalidated" and states[node_id] != "done":
states[node_id] = "invalidated"
elif event.get("event") == "feature_node_failed" and states[node_id] != "done":
states[node_id] = "failed" if bool(event.get("terminal")) else "pending"
elif event.get("event") == "feature_node_scheduled" and states[node_id] == "pending":
states[node_id] = "running"
for node in self.plan.nodes:
if states[node.node_id] in {"done", "failed", "invalidated"}:
continue
dependency_states = [states.get(dependency, "blocked") for dependency in node.depends_on]
if any(value in {"failed", "invalidated", "blocked"} for value in dependency_states):
states[node.node_id] = "blocked"
elif all(value == "done" for value in dependency_states):
states[node.node_id] = "ready" if states[node.node_id] != "running" else "running"
return states
def next_ready(self) -> FeatureNode | None:
statuses = self.statuses()
ready = [node for node in self.plan.nodes if statuses[node.node_id] == "ready"]
return min(ready, key=lambda node: node.priority) if ready else None
def all_done(self) -> bool:
return all(value == "done" for value in self.statuses().values())
def feature_ids(self) -> dict[str, str]:
expected_hashes = {node_id: node_hash(node) for node_id, node in self._nodes.items()}
result: dict[str, str] = {}
for event in self.events:
node_id = str(event.get("node_id") or "")
feature_id = str(event.get("feature_id") or "")
if event.get("event") == "feature_node_verified" and node_id in expected_hashes and event.get("node_hash") == expected_hashes[node_id] and feature_id:
result[node_id] = feature_id
return result
def completed_node_hashes(self) -> dict[str, str]:
statuses = self.statuses()
return {
node_id: node_hash(self._nodes[node_id])
for node_id, status in statuses.items()
if status == "done"
}
def failure_count(self, node_id: str, failure_class: str) -> int:
expected = node_hash(self._nodes[node_id])
return sum(
1
for event in self.events
if event.get("event") == "feature_node_failed"
and event.get("node_id") == node_id
and event.get("node_hash") == expected
and event.get("failure_class") == failure_class
)
+51 -5
View File
@@ -12,6 +12,13 @@ class TaskPhase(StrEnum):
DRAFTING_REQUIREMENTS_DOCUMENT = "DRAFTING_REQUIREMENTS_DOCUMENT"
DRAFTING_COMPLETION_TARGET = "DRAFTING_COMPLETION_TARGET"
COMPILING_REQUIREMENTS = "COMPILING_REQUIREMENTS"
COMPILING_FEATURE_PLAN = "COMPILING_FEATURE_PLAN"
SCHEDULING_FEATURE = "SCHEDULING_FEATURE"
FEATURE_PENDING = "FEATURE_PENDING"
FEATURE_BUILDING = "FEATURE_BUILDING"
REPLANNING_FEATURE_SUBGRAPH = "REPLANNING_FEATURE_SUBGRAPH"
# Legacy v3.1 phases remain readable only so an interrupted process can
# fail cleanly during the protocol reset. New v3.2 tasks never enter them.
DRAFTING_MODELING_PLAN = "DRAFTING_MODELING_PLAN"
AWAITING_ACTION = "AWAITING_ACTION"
ACTION_PENDING = "ACTION_PENDING"
@@ -35,6 +42,15 @@ class PendingAction:
expected_change: str
contract_hash: str
idempotency_key: str
node_id: str = ""
plan_hash: str = ""
claim_ids: tuple[str, ...] = ()
depends_on_node_ids: tuple[str, ...] = ()
# The stored JSON key remains ``pending_action_json`` only in old artifacts.
# v3.2 code uses this alias to make the ownership boundary explicit.
PendingFeature = PendingAction
@dataclass(frozen=True, slots=True)
@@ -53,9 +69,16 @@ class TaskState:
requirements_document_path: str = ""
completion_target_path: str = ""
modeling_plan_path: str = ""
feature_plan_path: str = ""
feature_plan_hash: str = ""
feature_stage_id: str = ""
clarification_path: str = ""
requirements_contract_path: str = ""
@property
def pending_feature(self) -> PendingFeature | None:
return self.pending_action
@property
def working_head(self) -> str:
return f"{self.task_id}:{self.active_revision or 'root'}:v{self.version}"
@@ -67,7 +90,21 @@ _TRANSITIONS: dict[tuple[TaskPhase, str], TaskPhase] = {
(TaskPhase.DRAFTING_REQUIREMENTS_DOCUMENT, "image_observed"): TaskPhase.DRAFTING_REQUIREMENTS_DOCUMENT,
(TaskPhase.DRAFTING_REQUIREMENTS_DOCUMENT, "requirements_document_written"): TaskPhase.DRAFTING_COMPLETION_TARGET,
(TaskPhase.DRAFTING_COMPLETION_TARGET, "completion_target_written"): TaskPhase.COMPILING_REQUIREMENTS,
(TaskPhase.COMPILING_REQUIREMENTS, "requirements_compiled"): TaskPhase.DRAFTING_MODELING_PLAN,
(TaskPhase.COMPILING_REQUIREMENTS, "requirements_compiled"): TaskPhase.COMPILING_FEATURE_PLAN,
(TaskPhase.COMPILING_FEATURE_PLAN, "feature_plan_written"): TaskPhase.SCHEDULING_FEATURE,
# Retained for direct v3.1 handler callers only. The v3.2 workflow never
# exposes this event or accepts a Markdown plan from a model.
(TaskPhase.COMPILING_FEATURE_PLAN, "modeling_plan_written"): TaskPhase.AWAITING_ACTION,
(TaskPhase.REPLANNING_FEATURE_SUBGRAPH, "feature_plan_revised"): TaskPhase.SCHEDULING_FEATURE,
(TaskPhase.SCHEDULING_FEATURE, "feature_scheduled"): TaskPhase.FEATURE_PENDING,
(TaskPhase.SCHEDULING_FEATURE, "final_requested"): TaskPhase.FINAL_VALIDATION,
(TaskPhase.SCHEDULING_FEATURE, "feature_replan"): TaskPhase.REPLANNING_FEATURE_SUBGRAPH,
(TaskPhase.FEATURE_PENDING, "feature_started"): TaskPhase.FEATURE_BUILDING,
(TaskPhase.FEATURE_PENDING, "feature_retry"): TaskPhase.FEATURE_PENDING,
(TaskPhase.FEATURE_BUILDING, "feature_verified"): TaskPhase.SCHEDULING_FEATURE,
(TaskPhase.FEATURE_BUILDING, "feature_retry"): TaskPhase.FEATURE_PENDING,
(TaskPhase.FEATURE_PENDING, "feature_replan"): TaskPhase.REPLANNING_FEATURE_SUBGRAPH,
(TaskPhase.FEATURE_BUILDING, "feature_replan"): TaskPhase.REPLANNING_FEATURE_SUBGRAPH,
(TaskPhase.DRAFTING_MODELING_PLAN, "modeling_plan_written"): TaskPhase.AWAITING_ACTION,
(TaskPhase.DRAFTING_REQUIREMENTS_DOCUMENT, "waiting_for_user"): TaskPhase.WAITING_FOR_USER,
# User clarifications are durable task evidence. Resume on the same task
@@ -89,6 +126,7 @@ _TRANSITIONS: dict[tuple[TaskPhase, str], TaskPhase] = {
(TaskPhase.CANDIDATE_REVIEW, "candidate_accepted"): TaskPhase.AWAITING_ACTION,
(TaskPhase.CANDIDATE_REVIEW, "candidate_rejected"): TaskPhase.AWAITING_ACTION,
(TaskPhase.AWAITING_ACTION, "final_requested"): TaskPhase.FINAL_VALIDATION,
(TaskPhase.FINAL_VALIDATION, "feature_replan"): TaskPhase.REPLANNING_FEATURE_SUBGRAPH,
(TaskPhase.FINAL_VALIDATION, "final_accepted"): TaskPhase.COMPLETED,
(TaskPhase.FINAL_VALIDATION, "final_repair"): TaskPhase.AWAITING_ACTION,
}
@@ -111,6 +149,11 @@ _RETRY_RESUMABLE_PHASES = frozenset({
TaskPhase.DRAFTING_REQUIREMENTS_DOCUMENT,
TaskPhase.DRAFTING_COMPLETION_TARGET,
TaskPhase.COMPILING_REQUIREMENTS,
TaskPhase.COMPILING_FEATURE_PLAN,
TaskPhase.SCHEDULING_FEATURE,
TaskPhase.FEATURE_PENDING,
TaskPhase.FEATURE_BUILDING,
TaskPhase.REPLANNING_FEATURE_SUBGRAPH,
TaskPhase.DRAFTING_MODELING_PLAN,
TaskPhase.AWAITING_ACTION,
TaskPhase.ACTION_PENDING,
@@ -139,7 +182,7 @@ def retry_resume_event(state: TaskState) -> str | None:
return f"resume_{state.retry_from_phase.value.lower()}"
def transition(state: TaskState, event: str, *, pending_action: PendingAction | None | object = ..., active_revision: str | None = None, candidate_id: str | None = None, candidate_stage_id: str | None = None, repair_required: bool | None = None, error: ErrorCode | None = None, requirements_spec_path: str | None = None, requirements_document_path: str | None = None, completion_target_path: str | None = None, modeling_plan_path: str | None = None, clarification_path: str | None = None, requirements_contract_path: str | None = None) -> TaskState:
def transition(state: TaskState, event: str, *, pending_action: PendingAction | None | object = ..., active_revision: str | None = None, candidate_id: str | None = None, candidate_stage_id: str | None = None, feature_stage_id: str | None = None, repair_required: bool | None = None, error: ErrorCode | None = None, requirements_spec_path: str | None = None, requirements_document_path: str | None = None, completion_target_path: str | None = None, modeling_plan_path: str | None = None, feature_plan_path: str | None = None, feature_plan_hash: str | None = None, clarification_path: str | None = None, requirements_contract_path: str | None = None) -> TaskState:
"""Apply one legal transition and advance optimistic-concurrency version."""
target = _TRANSITIONS.get((state.phase, event))
if target is None:
@@ -147,7 +190,7 @@ def transition(state: TaskState, event: str, *, pending_action: PendingAction |
if state.phase == TaskPhase.WAITING_RETRY and event.startswith("resume_") and state.retry_from_phase != target:
raise ValueError("WAITING_RETRY resume event does not match its persisted source phase")
next_pending = state.pending_action if pending_action is ... else pending_action
if target in {TaskPhase.AWAITING_ACTION, TaskPhase.COMPLETED, TaskPhase.FAILED, TaskPhase.CANCELLED}:
if target in {TaskPhase.AWAITING_ACTION, TaskPhase.SCHEDULING_FEATURE, TaskPhase.REPLANNING_FEATURE_SUBGRAPH, TaskPhase.COMPLETED, TaskPhase.FAILED, TaskPhase.CANCELLED}:
next_pending = None
return replace(
state,
@@ -155,8 +198,8 @@ def transition(state: TaskState, event: str, *, pending_action: PendingAction |
version=state.version + 1,
active_revision=state.active_revision if active_revision is None else active_revision,
pending_action=next_pending,
candidate_id="" if target in {TaskPhase.AWAITING_ACTION, TaskPhase.COMPLETED, TaskPhase.FAILED, TaskPhase.CANCELLED} else state.candidate_id if candidate_id is None else candidate_id,
candidate_stage_id="" if target in {TaskPhase.AWAITING_ACTION, TaskPhase.COMPLETED, TaskPhase.FAILED, TaskPhase.CANCELLED} else state.candidate_stage_id if candidate_stage_id is None else candidate_stage_id,
candidate_id="" if target in {TaskPhase.AWAITING_ACTION, TaskPhase.SCHEDULING_FEATURE, TaskPhase.COMPLETED, TaskPhase.FAILED, TaskPhase.CANCELLED} else state.candidate_id if candidate_id is None else candidate_id,
candidate_stage_id="" if target in {TaskPhase.AWAITING_ACTION, TaskPhase.SCHEDULING_FEATURE, TaskPhase.COMPLETED, TaskPhase.FAILED, TaskPhase.CANCELLED} else state.candidate_stage_id if candidate_stage_id is None else candidate_stage_id,
repair_required=state.repair_required if repair_required is None else repair_required,
last_error=error,
retry_from_phase=state.phase if target == TaskPhase.WAITING_RETRY else None,
@@ -164,6 +207,9 @@ def transition(state: TaskState, event: str, *, pending_action: PendingAction |
requirements_document_path=state.requirements_document_path if requirements_document_path is None else requirements_document_path,
completion_target_path=state.completion_target_path if completion_target_path is None else completion_target_path,
modeling_plan_path=state.modeling_plan_path if modeling_plan_path is None else modeling_plan_path,
feature_plan_path=state.feature_plan_path if feature_plan_path is None else feature_plan_path,
feature_plan_hash=state.feature_plan_hash if feature_plan_hash is None else feature_plan_hash,
feature_stage_id="" if target in {TaskPhase.SCHEDULING_FEATURE, TaskPhase.FEATURE_PENDING, TaskPhase.REPLANNING_FEATURE_SUBGRAPH, TaskPhase.COMPLETED, TaskPhase.FAILED, TaskPhase.CANCELLED} else state.feature_stage_id if feature_stage_id is None else feature_stage_id,
clarification_path=state.clarification_path if clarification_path is None else clarification_path,
requirements_contract_path=state.requirements_contract_path if requirements_contract_path is None else requirements_contract_path,
)
@@ -647,6 +647,60 @@ def _coaxial_through_bore_group(expected: dict[str, Any], facts: dict[str, Any])
return _pass({"diameter_mm": expected["diameter_mm"], "axes": deviations, "tolerance_mm": tolerance}) if all(item["axis_dot"] >= 1 - 1e-6 and item["axis_distance_mm"] <= tolerance for item in deviations) else _fail({"diameter_mm": expected["diameter_mm"], "axes": deviations, "tolerance_mm": tolerance})
def _concentric_bore_to_outer_cylinder(expected: dict[str, Any], facts: dict[str, Any]) -> ClaimResult:
"""Verify one bore axis coincides with one external cylindrical shell.
This is distinct from ``coaxial_through_bore_group``, which compares two
or more inner bores. A centered flange bore has only one inner cylinder,
so treating its outer wall as a second bore makes the old verifier
permanently pending.
"""
if not _records(facts):
return _pending("topology is unavailable")
tolerance = float(expected["tolerance_mm"])
bore = _matching_cylinders({
"diameter_mm": expected["bore_diameter_mm"],
"tolerance_mm": tolerance,
}, facts)
outer_shells = _outer_cylinder_shells({
"diameter_mm": expected["outer_diameter_mm"],
"tolerance_mm": tolerance,
}, facts)
if not bore:
return _pending("the target bore has not been introduced at this checkpoint")
if not outer_shells:
return _pending("the target outer cylindrical surface has not been introduced at this checkpoint")
if len(bore) != 1 or len(outer_shells) != 1:
return _fail({
"expected_bore_count": 1,
"actual_bore_count": len(bore),
"expected_outer_count": 1,
"actual_outer_count": len(outer_shells),
"bore_diameter_mm": expected["bore_diameter_mm"],
"outer_diameter_mm": expected["outer_diameter_mm"],
})
bore_point = _cylinder_axis_point(bore[0])
bore_axis = _cylinder_axis_direction(bore[0])
outer_point = _cylinder_axis_point(outer_shells[0][0])
outer_axis = _cylinder_axis_direction(outer_shells[0][0])
if None in {bore_point, bore_axis, outer_point, outer_axis}:
return _pending("the bore or outer-cylinder axis is not measurable")
assert bore_point is not None and bore_axis is not None
assert outer_point is not None and outer_axis is not None
axis_dot = abs(sum(bore_axis[index] * outer_axis[index] for index in range(3)))
delta = tuple(bore_point[index] - outer_point[index] for index in range(3))
axial = sum(delta[index] * outer_axis[index] for index in range(3))
radial_distance = sqrt(sum((delta[index] - axial * outer_axis[index]) ** 2 for index in range(3)))
evidence = {
"bore_record_id": bore[0].get("record_id"),
"outer_record_ids": [record.get("record_id") for record in outer_shells[0]],
"axis_dot": axis_dot,
"axis_distance_mm": radial_distance,
"tolerance_mm": tolerance,
}
return _pass(evidence) if axis_dot >= 1 - 1e-6 and radial_distance <= tolerance else _fail(evidence)
def _orthogonal_intersecting_through_bores(expected: dict[str, Any], facts: dict[str, Any]) -> ClaimResult:
first = _matching_cylinders({"diameter_mm": expected["first_diameter_mm"], "tolerance_mm": expected["tolerance_mm"]}, facts)
second = _matching_cylinders({"diameter_mm": expected["second_diameter_mm"], "tolerance_mm": expected["tolerance_mm"]}, facts)
@@ -889,6 +943,7 @@ def default_registry() -> VerifierRegistry:
ClaimDefinition("circular_hole_pattern", _closed_object({"count": {"type": "integer", "minimum": 2}, "diameter_mm": positive, "pitch_radius_mm": positive, "concentric_bore_diameter_mm": positive, "tolerance_mm": tolerance}, ["count", "diameter_mm", "pitch_radius_mm", "tolerance_mm"]), ("topology",), _hole_pattern),
ClaimDefinition("collinear_through_bore_chain", bore_chain, ("topology",), _collinear_bore_chain),
ClaimDefinition("coaxial_through_bore_group", _closed_object({"diameter_mm": positive, "count": {"type": "integer", "minimum": 2, "maximum": 16}, "tolerance_mm": alignment_tolerance}, ["diameter_mm", "count", "tolerance_mm"]), ("topology",), _coaxial_through_bore_group),
ClaimDefinition("concentric_bore_to_outer_cylinder", _closed_object({"bore_diameter_mm": positive, "outer_diameter_mm": positive, "tolerance_mm": alignment_tolerance}, ["bore_diameter_mm", "outer_diameter_mm", "tolerance_mm"]), ("topology",), _concentric_bore_to_outer_cylinder),
ClaimDefinition("orthogonal_intersecting_through_bores", _closed_object({"first_diameter_mm": positive, "second_diameter_mm": positive, "first_axis": {"enum": ["x", "y", "z"]}, "second_axis": {"enum": ["x", "y", "z"]}, "tolerance_mm": alignment_tolerance}, ["first_diameter_mm", "second_diameter_mm", "first_axis", "second_axis", "tolerance_mm"]), ("topology",), _orthogonal_intersecting_through_bores),
ClaimDefinition("rectangular_corner_through_bore_pattern", _closed_object({"diameter_mm": positive, "count": {"const": 4}, "edge_offset_mm": positive, "tolerance_mm": tolerance}, ["diameter_mm", "count", "edge_offset_mm", "tolerance_mm"]), ("rebuild_report", "topology"), _rectangular_corner_bore_pattern),
ClaimDefinition("coaxial", _closed_object({"record_ids": {"type": "array", "items": {"type": "string", "minLength": 1}, "minItems": 2, "maxItems": 2, "uniqueItems": True}, "tolerance": alignment_tolerance}, ["record_ids", "tolerance"]), ("topology",), _coaxial),