添加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
@@ -7,6 +7,7 @@ import json
import secrets
from typing import Any
from app.cad_agent.domain.feature_plan import FeaturePlan, FeatureScheduler, node_hash
from app.cad_agent.application.llm_contracts import (
CandidateReview,
FinalReview,
@@ -55,6 +56,236 @@ class ActionCommandHandler:
available.append(atomic_id)
return tuple(available)
def schedule_next_feature(self, task_id: str) -> Accepted | Rejected:
"""Select the server-owned next ready node by fixed plan priority."""
state = self.repository.get_state(task_id)
if state is None or state.phase != TaskPhase.SCHEDULING_FEATURE:
return Rejected(WorkflowError(ErrorCode.AUTHOR_DECISION_REJECTED, "Feature scheduling is not expected in the current workflow state."))
plan = self._feature_plan(task_id, state)
if plan is None:
return Rejected(WorkflowError(ErrorCode.STORAGE_FAILURE, "The active feature plan is unavailable.", retryable=True))
scheduler = FeatureScheduler(plan, self.repository.ledger_events(task_id))
if scheduler.all_done():
claim_results = self._evaluate_claims(task_id, self._facts(task_id, state.active_revision))
deterministic_failures = [
item for item in claim_results
if item.get("deterministic") and item.get("status") != "pass"
]
if deterministic_failures:
next_state = transition(
state,
"feature_replan",
error=ErrorCode.CLAIM_VERIFICATION_FAILED,
)
result = {
"status": "replan_required",
"phase": next_state.phase.value,
"claim_results": claim_results,
}
if not self.repository.compare_and_swap(next_state, events=[{
"event": "feature_plan_completion_failed",
"plan_hash": state.feature_plan_hash,
"revision_id": state.active_revision,
"claim_results": claim_results,
"failed_claim_ids": [str(item.get("claim_id") or "") for item in deterministic_failures],
"message": "All feature nodes completed, but final deterministic validation failed.",
}]):
return Rejected(self._stale())
return Accepted(result)
next_state = transition(state, "final_requested")
result = {"status": "final_validation", "phase": next_state.phase.value, "claim_results": claim_results}
if not self.repository.compare_and_swap(next_state, events=[{
"event": "feature_plan_complete",
"plan_hash": state.feature_plan_hash,
"revision_id": state.active_revision,
"claim_results": claim_results,
}], invocation_id=None, invocation_result=None):
return Rejected(self._stale())
return Accepted(result)
node = scheduler.next_ready()
if node is None:
return Rejected(WorkflowError(ErrorCode.REQUIREMENTS_SPEC_INVALID, "Feature plan has no runnable node; revise its unresolved subgraph."))
contract = self.runtime.operation_contract(node.atomic_id)
requirement_ids = tuple(sorted(self._requirement_ids_for_claims(task_id, tuple(node.claim_ids))))
action_id = "feature_" + sha256(f"{state.feature_plan_hash}|{node.node_id}|{state.active_revision}".encode("utf-8")).hexdigest()[:16]
pending = PendingAction(
action_id=action_id,
working_head=state.working_head,
intent=node.intent,
requirement_ids=requirement_ids,
atomic_id=node.atomic_id,
expected_change=node.expected_change,
contract_hash=contract["contract_hash"],
idempotency_key=sha256(f"{task_id}|{action_id}|{state.working_head}".encode("utf-8")).hexdigest(),
node_id=node.node_id,
plan_hash=state.feature_plan_hash,
claim_ids=tuple(node.claim_ids),
depends_on_node_ids=tuple(node.depends_on),
)
next_state = transition(state, "feature_scheduled", pending_action=pending)
event = {
"event": "feature_node_scheduled", "node_id": node.node_id, "node_hash": node_hash(node),
"plan_hash": state.feature_plan_hash, "action_id": action_id, "atomic_id": node.atomic_id,
"depends_on": node.depends_on, "claim_ids": node.claim_ids, "priority": node.priority,
}
if not self.repository.compare_and_swap(next_state, events=[event]):
return Rejected(self._stale())
return Accepted({"status": "scheduled", "node_id": node.node_id, "atomic_id": node.atomic_id, "phase": next_state.phase.value})
def submit_feature_fragment(self, task_id: str, fragment: dict[str, Any], *, invocation_id: str) -> Accepted | Rejected:
state = self.repository.get_state(task_id)
action = state.pending_feature if state else None
if state is None or state.phase != TaskPhase.FEATURE_PENDING or action is None:
return Rejected(WorkflowError(ErrorCode.AUTHOR_DECISION_REJECTED, "A feature fragment requires one scheduled feature node."))
plan = self._feature_plan(task_id, state)
if plan is None or action.plan_hash != state.feature_plan_hash:
return Rejected(WorkflowError(ErrorCode.STALE_WORKING_HEAD, "The scheduled feature belongs to an obsolete plan."))
node = next((item for item in plan.nodes if item.node_id == action.node_id), None)
if node is None or node.atomic_id != action.atomic_id or tuple(node.claim_ids) != action.claim_ids:
return Rejected(WorkflowError(ErrorCode.STALE_WORKING_HEAD, "The scheduled feature no longer matches the active plan."))
contract = self.runtime.operation_contract(action.atomic_id)
if contract["contract_hash"] != action.contract_hash:
return Rejected(WorkflowError(ErrorCode.STALE_WORKING_HEAD, "The operation contract changed while the feature was scheduled."))
base = self.artifacts.read_active_cdsl(task_id, state.active_revision)
selectors = self.runtime.selector_tokens(self.artifacts.read_topology(task_id, state.active_revision))
references = self.runtime.reference_tokens(base)
selector_policy = contract.get("selector_policy") if isinstance(contract.get("selector_policy"), dict) else {}
selector_kind = str(selector_policy.get("token_kind") or "")
allowed_selectors = [
token for token, value in selectors.items()
if str((contract.get("fragment_shape") or {}).get("selector_tokens") or "forbidden") == "required"
and isinstance(value, dict) and value.get("kind") == selector_kind
]
errors = validate_fragment(contract, fragment, selector_tokens=allowed_selectors, reference_tokens=list(references), root_xy_datum=not bool(state.active_revision))
if errors:
return self._feature_failure(task_id, state, action, ErrorCode.AUTHOR_FORMAT_INVALID, "fragment_preflight", "CDSL fragment violates the scheduled operation schema.", invocation_id=invocation_id, field_errors=tuple(errors))
scheduler = FeatureScheduler(plan, self.repository.ledger_events(task_id))
feature_ids = scheduler.feature_ids()
direct_feature_ids = tuple(feature_ids.get(dependency, "") for dependency in action.depends_on_node_ids)
if any(not value for value in direct_feature_ids):
return self._feature_failure(task_id, state, action, ErrorCode.RUNTIME_PRECONDITION_FAILED, "dependency", "A direct dependency has no verified runtime feature.", invocation_id=invocation_id)
fragment_hash = canonical_hash(fragment)
key = self._key(task_id, "feature_fragment", state.working_head, {"node_id": action.node_id, "fragment": fragment})
invocation = self.repository.begin_invocation(task_id, invocation_id, key)
if invocation.status == "finished" and invocation.result is not None:
return Accepted(invocation.result)
try:
cdsl, audit = self.runtime.materialize_fragment(
base, fragment, contract, selectors, references,
require_through=self._claims_require_through(task_id, action.claim_ids),
depends_on_feature_ids=direct_feature_ids,
)
except Exception as error:
return self._feature_failure(task_id, state, action, self._runtime_error(error).code, "fragment_preflight", str(error), invocation_id=invocation_id, invocation=invocation)
stage_id = "feature_stage_" + sha256(key.encode("utf-8")).hexdigest()[:16]
try:
stage = self.artifacts.start_candidate_stage(task_id, key, {
"schema_version": "cad.v3.2.feature-input.v1", "stage_id": stage_id,
"node_id": action.node_id, "node_hash": node_hash(node), "plan_hash": state.feature_plan_hash,
"action_id": action.action_id, "fragment": fragment, "fragment_audit": audit,
})
except OSError as error:
return self._park_retry(state, ErrorCode.STORAGE_FAILURE, event="feature_stage_storage_failure", message=str(error), details={"node_id": action.node_id})
building = transition(state, "feature_started", feature_stage_id=stage.stage_id)
if not self.repository.compare_and_swap(building, events=[{
"event": "feature_node_building", "node_id": action.node_id, "node_hash": node_hash(node),
"plan_hash": state.feature_plan_hash, "stage_id": stage.stage_id, "fragment_hash": fragment_hash,
}]):
return Rejected(self._stale())
try:
rebuilt = self.runtime.build_checkpoint(cdsl, stage.output_dir, task_id, stage_id)
try:
preview = self.runtime.create_preview(stage.output_dir)
rebuilt["paths"]["glb"] = preview["path"]
except Exception as preview_error:
preview = {"preview_unavailable": str(preview_error)[:500]}
claim_results = self._evaluate_claims(task_id, rebuilt, claim_ids=set(action.claim_ids))
operation_results = self._operation_candidate_results(
action, contract, cdsl, rebuilt, parent_facts=self._facts(task_id, state.active_revision),
require_through=self._claims_require_through(task_id, action.claim_ids),
)
global_failures = [item for item in self._evaluate_claims(task_id, rebuilt) if item.get("claim_kind") in {"solid_count_equals", "single_connected_body"} and item.get("status") != "pass"]
blockers = [
# A claim is assigned to exactly one runtime-atomic node in
# the Feature Plan. Unlike unassigned future-work claims,
# an assigned claim may not remain pending when that node is
# published: doing so would turn a missing feature into a
# permanent, apparently successful checkpoint.
*[item for item in claim_results if item.get("status") != "pass"],
*[item for item in operation_results if item.get("status") != "pass"],
*global_failures,
]
if blockers:
self.artifacts.write_stage_json(task_id, stage.stage_id, "node-verification.json", {
"schema_version": "cad.v3.2.node-verification.v1", "node_id": action.node_id,
"claim_results": claim_results, "operation_verifier_results": operation_results, "blockers": blockers,
})
return self._feature_failure(task_id, building, action, ErrorCode.CLAIM_VERIFICATION_FAILED, "node_validation", "The feature did not satisfy its local deterministic acceptance.", invocation_id=invocation_id, invocation=invocation, details={"stage_id": stage.stage_id, "blockers": blockers})
verification = {
"schema_version": "cad.v3.2.node-verification.v1", "node_id": action.node_id,
"node_hash": node_hash(node), "plan_hash": state.feature_plan_hash,
"feature_id": audit["assigned_feature_ids"][0], "claim_results": claim_results,
"operation_verifier_results": operation_results, "health": rebuilt["health"], "preview": preview,
}
self.artifacts.write_stage_json(task_id, stage.stage_id, "node-verification.json", verification)
revision_id = self._next_revision(task_id)
paths = self.artifacts.publish_candidate(task_id, stage.stage_id, revision_id)
next_state = transition(building, "feature_verified", active_revision=revision_id)
result = {"status": "verified", "node_id": action.node_id, "revision_id": revision_id, "paths": paths, "claim_results": claim_results}
event = {
"event": "feature_node_verified", "node_id": action.node_id, "node_hash": node_hash(node),
"plan_hash": state.feature_plan_hash, "feature_id": audit["assigned_feature_ids"][0],
"atomic_id": action.atomic_id,
"revision_id": revision_id, "parent_revision": state.active_revision, "stage_id": stage.stage_id,
"claim_results": claim_results, "preview_unavailable": preview.get("preview_unavailable", ""),
}
if not self._commit_invocation(next_state, [event], invocation, result):
return Rejected(self._stale())
return Accepted(result)
except OSError as error:
return self._park_retry(building, ErrorCode.STORAGE_FAILURE, event="feature_build_storage_failure", message=str(error), details={"node_id": action.node_id, "stage_id": stage.stage_id})
except Exception as error:
return self._feature_failure(task_id, building, action, self._runtime_error(error).code, "engine_build", str(error), invocation_id=invocation_id, invocation=invocation, details={"stage_id": stage.stage_id})
def recover_feature_build(self, task_id: str) -> Accepted | Rejected:
"""Make a crashed build retryable without losing its scheduled node."""
state = self.repository.get_state(task_id)
action = state.pending_feature if state else None
if state is None or state.phase != TaskPhase.FEATURE_BUILDING or action is None:
return Rejected(WorkflowError(ErrorCode.AUTHOR_DECISION_REJECTED, "There is no recoverable feature build."))
next_state = transition(state, "feature_retry")
if not self.repository.compare_and_swap(next_state, events=[{
"event": "feature_build_recovered", "node_id": action.node_id, "plan_hash": action.plan_hash,
"stage_id": state.feature_stage_id, "message": "Interrupted node build returned to the same scheduled feature.",
}]):
return Rejected(self._stale())
return Accepted({"status": "retry", "node_id": action.node_id, "phase": next_state.phase.value})
def _feature_failure(self, task_id: str, state: TaskState, action: PendingAction, code: ErrorCode, failure_class: str, message: str, *, invocation_id: str, field_errors: tuple[dict[str, Any], ...] = (), invocation: Any = None, details: dict[str, Any] | None = None) -> Rejected:
plan = self._feature_plan(task_id, state)
node = next((item for item in (plan.nodes if plan else []) if item.node_id == action.node_id), None)
if node is None:
return Rejected(WorkflowError(ErrorCode.STORAGE_FAILURE, "The active feature node is unavailable.", retryable=True))
scheduler = FeatureScheduler(plan, self.repository.ledger_events(task_id))
attempt = scheduler.failure_count(action.node_id, failure_class) + 1
terminal = attempt >= 2
event = {
"event": "feature_node_failed", "node_id": action.node_id, "node_hash": node_hash(node),
"plan_hash": state.feature_plan_hash, "failure_class": failure_class, "attempt": attempt,
"terminal": terminal, "code": code.value, "normalized_error_code": code.value,
"atomic_id": action.atomic_id, "checkpoint_revision": state.active_revision,
"message": message[:1000], **(details or {}),
}
next_state = transition(state, "feature_replan" if terminal else "feature_retry", error=code)
result = {"status": "replan_required" if terminal else "retry", "node_id": action.node_id, "attempt": attempt, "failure_class": failure_class}
if invocation is not None:
committed = self._commit_invocation(next_state, [event], invocation, result)
else:
committed = self.repository.compare_and_swap(next_state, events=[event])
if not committed:
return Rejected(self._stale())
return Rejected(WorkflowError(code, message, field_errors=field_errors, details={**(details or {}), "node_id": action.node_id, "attempt": attempt, "replan_required": terminal}))
def propose_next_action(self, task_id: str, proposal: NextAction, *, invocation_id: str) -> Accepted | Rejected:
state = self.repository.get_state(task_id)
if state is None or state.phase != TaskPhase.AWAITING_ACTION or state.pending_action is not None:
@@ -388,6 +619,8 @@ class ActionCommandHandler:
def submit_cdsl_fragment(self, task_id: str, fragment: dict[str, Any], *, invocation_id: str) -> Accepted | Rejected:
state = self.repository.get_state(task_id)
if state is not None and state.phase == TaskPhase.FEATURE_PENDING:
return self.submit_feature_fragment(task_id, fragment, invocation_id=invocation_id)
action = state.pending_action if state else None
if state is None or state.phase != TaskPhase.ACTION_PENDING or action is None:
return Rejected(WorkflowError(ErrorCode.AUTHOR_DECISION_REJECTED, "A CDSL fragment requires one pending action."))
@@ -922,11 +1155,18 @@ class ActionCommandHandler:
"atomic_id": atomic_id,
"fragment_hash": accepted_fragment,
}) if accepted_fragment and atomic_id else ""
next_state = transition(state, "final_repair", repair_required=True, error=ErrorCode.CLAIM_VERIFICATION_FAILED if deterministic_fail else ErrorCode.CANDIDATE_REVIEW_REJECTED)
is_dag = bool(state.feature_plan_hash)
next_state = transition(
state,
"feature_replan" if is_dag else "final_repair",
repair_required=not is_dag,
error=ErrorCode.CLAIM_VERIFICATION_FAILED if deterministic_fail else ErrorCode.CANDIDATE_REVIEW_REJECTED,
)
result = {"status": "repair", "revision_id": state.active_revision}
if not self._commit_invocation(next_state, [{
"event": "final_review_repair",
"event": "final_visual_reviewed" if is_dag else "final_review_repair",
"revision_id": state.active_revision,
**({"plan_hash": state.feature_plan_hash} if is_dag else {}),
"claim_results": claim_results,
"review_claim_coverage": [item.model_dump(mode="json") for item in review.claim_coverage],
"visual_not_passed": [str(item.get("claim_id") or "") for item in visual_not_passed],
@@ -947,15 +1187,48 @@ class ActionCommandHandler:
return Accepted(result)
next_state = transition(state, "final_accepted")
result = {"status": "completed", "revision_id": state.active_revision}
if not self._commit_invocation(next_state, [{"event": "completed", "revision_id": state.active_revision, "final_review_path": final_review_path, "completion_result_path": "completion-result.md", "claim_results": claim_results}], invocation, result):
if not self._commit_invocation(next_state, [{"event": "final_visual_reviewed", "revision_id": state.active_revision, "final_review_path": final_review_path, "completion_result_path": "completion-result.md", "claim_results": claim_results}], invocation, result):
return Rejected(self._stale())
return Accepted(result)
def _evaluate_claims(self, task_id: str, facts: dict[str, Any]) -> list[dict[str, Any]]:
def _evaluate_claims(self, task_id: str, facts: dict[str, Any], *, claim_ids: set[str] | None = None) -> list[dict[str, Any]]:
contract = self._requirements_contract(task_id) or {}
claims = [claim for requirement in contract.get("requirements") or () if isinstance(requirement, dict) for claim in requirement.get("acceptance_claims") or () if isinstance(claim, dict)]
claims = [
claim for requirement in contract.get("requirements") or () if isinstance(requirement, dict)
for claim in requirement.get("acceptance_claims") or ()
if isinstance(claim, dict) and (claim_ids is None or str(claim.get("claim_id") or "") in claim_ids)
]
return self.verifiers.evaluate(claims, facts)
def _feature_plan(self, task_id: str, state: TaskState | None = None) -> FeaturePlan | None:
state = state or self.repository.get_state(task_id)
if state is None or not state.feature_plan_path:
return None
raw = self.artifacts.read_json(task_id, state.feature_plan_path)
try:
return FeaturePlan.model_validate(raw)
except ValueError:
return None
def _requirement_ids_for_claims(self, task_id: str, claim_ids: tuple[str, ...]) -> set[str]:
wanted = set(claim_ids)
contract = self._requirements_contract(task_id) or {}
return {
str(requirement.get("requirement_id") or "")
for requirement in contract.get("requirements") or ()
if isinstance(requirement, dict)
and any(str(claim.get("claim_id") or "") in wanted for claim in requirement.get("acceptance_claims") or () if isinstance(claim, dict))
}
def _claims_require_through(self, task_id: str, claim_ids: tuple[str, ...]) -> bool:
wanted = set(claim_ids)
contract = self._requirements_contract(task_id) or {}
return any(
str(claim.get("claim_id") or "") in wanted and claim.get("claim_kind") == "through_cylindrical_bore"
for requirement in contract.get("requirements") or () if isinstance(requirement, dict)
for claim in requirement.get("acceptance_claims") or () if isinstance(claim, dict)
)
def claim_summary(self, task_id: str, state: TaskState) -> list[dict[str, Any]]:
"""Return bounded current claim facts for author context assembly."""
return [
@@ -1059,7 +1332,16 @@ class ActionCommandHandler:
continue
expected: dict[str, Any]
if claim_kind in {"cylindrical_bore", "through_cylindrical_bore"}:
diameter = params.get("diameter_mm")
# A counterbore can reuse an already-existing pilot bore. In
# that case the material change is the larger cylindrical
# recess, not an additional instance of the pilot diameter.
# Measuring the pilot would count the parent bore as a new
# feature and make a valid counterbore checkpoint fail.
diameter = (
params.get("counterbore_diameter_mm")
if action.atomic_id == "hole_counterbore" and claim_kind == "cylindrical_bore"
else params.get("diameter_mm")
)
positions = params.get("positions")
if not isinstance(diameter, (int, float)):
return [{"claim_id": f"operation_{action.action_id}_{claim_kind}", "claim_kind": claim_kind, "deterministic": True, "status": "unavailable", "evidence": {"reason": "operation has no measurable bore diameter"}}]
@@ -1152,7 +1434,7 @@ class ActionCommandHandler:
values = [
int(str(event.get("revision_id") or "").removeprefix("rev_"))
for event in self.repository.ledger_events(task_id)
if event.get("event") == "accepted"
if event.get("event") in {"accepted", "feature_node_verified"}
and str(event.get("revision_id") or "").startswith("rev_")
and str(event.get("revision_id") or "").removeprefix("rev_").isdigit()
]