添加cadfs_to_cdsl代码,优化cad生成
This commit is contained in:
@@ -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
|
||||
)
|
||||
@@ -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),
|
||||
|
||||
Reference in New Issue
Block a user