"""Action selection, candidate, review, and completion command handlers.""" from __future__ import annotations from hashlib import sha256 import json import secrets from typing import Any from app.cad_agent.application.llm_contracts import ( CandidateReview, FinalReview, GeometryConclusion, NextAction, RollbackCheckpoint, ) from app.cad_agent.application.results import Accepted, Rejected from app.cad_agent.domain.errors import ErrorCode, WorkflowError from app.cad_agent.domain.operation_contract import canonical_hash, validate_fragment from app.cad_agent.domain.state import PendingAction, TaskPhase, TaskState, reject_stale_head, transition from app.cad_agent.ports import ArtifactStore, CadRuntime, TaskRepository, VerifierExecutor class ActionCommandHandler: def __init__(self, repository: TaskRepository, artifacts: ArtifactStore, runtime: CadRuntime, verifiers: VerifierExecutor) -> None: self.repository = repository self.artifacts = artifacts self.runtime = runtime self.verifiers = verifiers def available_atomic_ids(self, task_id: str, state: TaskState | None = None) -> tuple[str, ...]: """Return operations whose mandatory snapshot inputs exist now.""" state = state or self.repository.get_state(task_id) topology = self.artifacts.read_topology(task_id, state.active_revision) if state is not None else None selector_tokens = self.runtime.selector_tokens(topology) active_cdsl = self.artifacts.read_active_cdsl(task_id, state.active_revision) if state is not None else None reference_tokens = self.runtime.reference_tokens(active_cdsl) has_active_solid = isinstance(active_cdsl, dict) and bool(active_cdsl.get("features")) available: list[str] = [] for atomic_id in self.runtime.supported_atomic_ids(): contract = self.runtime.operation_contract(atomic_id) shape = contract.get("fragment_shape") if isinstance(contract.get("fragment_shape"), dict) else {} if str(shape.get("selector_tokens") or "forbidden") == "required": policy = contract.get("selector_policy") if isinstance(contract.get("selector_policy"), dict) else {} required_kind = str(policy.get("token_kind") or "") if not any(token.get("kind") == required_kind for token in selector_tokens.values() if isinstance(token, dict)): continue reference_policy = contract.get("reference_policy") if isinstance(contract.get("reference_policy"), dict) else {} if reference_policy.get("mode") == "snapshot_bound": minimum = int(reference_policy.get("min_items") or 1) if len(reference_tokens) < minimum: continue if "requires_active_solid" in (contract.get("semantic_preflight") or ()) and not has_active_solid: continue available.append(atomic_id) return tuple(available) 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: return Rejected(WorkflowError(ErrorCode.AUTHOR_DECISION_REJECTED, "A next action is not allowed in the current workflow state.")) if not self.repair_action_ready(task_id, state): return Rejected(WorkflowError(ErrorCode.AUTHOR_DECISION_REJECTED, "Record a current geometry conclusion before selecting a repair action.")) stale = reject_stale_head(state, proposal.working_head) if stale: return Rejected(stale) contract = self._requirements_contract(task_id, state) if not isinstance(contract, dict): return Rejected(WorkflowError(ErrorCode.AUTHOR_DECISION_REJECTED, "Requirements contract is not frozen.")) requirement_ids = {str(item.get("requirement_id") or "") for item in contract.get("requirements") or () if isinstance(item, dict)} if not set(proposal.requirement_ids).issubset(requirement_ids): return Rejected(WorkflowError(ErrorCode.AUTHOR_FORMAT_INVALID, "Action references a requirement outside the frozen contract.")) if proposal.atomic_id not in self.runtime.supported_atomic_ids(): return Rejected(WorkflowError(ErrorCode.RUNTIME_CONTRACT_INVALID, "Action selects an operation absent from the verified runtime registry.")) if proposal.atomic_id not in self.available_atomic_ids(task_id, state): return Rejected(WorkflowError( ErrorCode.AUTHOR_DECISION_REJECTED, "Action requires selector or feature-reference facts unavailable from the current geometry snapshot.", )) operation = self.runtime.operation_contract(proposal.atomic_id) key = self._key(task_id, "next_action", state.working_head, proposal.model_dump(mode="json")) invocation = self.repository.begin_invocation(task_id, invocation_id, key) if invocation.status == "finished" and invocation.result is not None: return Accepted(invocation.result) action = PendingAction( action_id=f"act_{state.version + 1:03d}", working_head=state.working_head, intent=proposal.intent, requirement_ids=tuple(proposal.requirement_ids), atomic_id=proposal.atomic_id, expected_change=proposal.expected_change, contract_hash=str(operation["contract_hash"]), idempotency_key=key, ) next_state = transition(state, "action_proposed", pending_action=action) payload = {"action_id": action.action_id, "working_head": action.working_head, "atomic_id": action.atomic_id, "contract_hash": action.contract_hash} if not self._commit_invocation(next_state, [{"event": "proposed", **payload, "intent": action.intent, "requirement_ids": list(action.requirement_ids), "expected_change": action.expected_change}], invocation, payload): return Rejected(self._stale()) return Accepted(payload) def diagnostic_evidence_refs(self, task_id: str, state: TaskState | None = None) -> tuple[str, ...]: """Return the server-generated evidence references for a repair turn. These are opaque names for current facts and committed audit evidence, never model-provided paths or feature IDs. The command handler checks them again so a schema from an earlier turn cannot authorize a write. """ state = state or self.repository.get_state(task_id) if state is None: return () refs = ["evidence_current_state"] if state.active_revision: refs.extend(["evidence_current_model", "evidence_current_claims"]) for event in self.repository.ledger_events(task_id)[-16:]: if event.get("event") in { "candidate_rejected", "candidate_build_failed", "candidate_recovery_failed", "candidate_recovered_rejected", "candidate_operation_skipped", "final_review_repair", } or (event.get("event") == "accepted" and event.get("repair_required")): sequence = event.get("sequence") if isinstance(sequence, int): refs.append(f"evidence_ledger_{sequence}") return tuple(dict.fromkeys(refs)) def repair_diagnostics(self, task_id: str, state: TaskState | None = None) -> list[dict[str, Any]]: """Return bounded, server-measured evidence from failed candidates. A repair turn runs against the last accepted checkpoint, while its useful facts often live in an immutable rejected candidate stage. An opaque ledger reference alone forces the author to rediscover those facts or repeat a failed construction. This projection deliberately exposes only compact build/verification outcomes, never the rejected fragment or a server-selected next operation. """ state = state or self.repository.get_state(task_id) if state is None or not state.repair_required: return [] relevant = { "candidate_rejected", "candidate_build_failed", "candidate_recovery_failed", "candidate_recovered_rejected", "candidate_operation_skipped", "runtime_precondition_rejected", "final_review_repair", "accepted", } diagnostics: list[dict[str, Any]] = [] for event in reversed(self.repository.ledger_events(task_id)): if event.get("event") not in relevant: continue sequence = event.get("sequence") item: dict[str, Any] = { "event": str(event.get("event") or ""), "evidence_ref": f"evidence_ledger_{sequence}" if isinstance(sequence, int) else "evidence_current_state", } for field in ("candidate_id", "action_id"): value = event.get(field) if isinstance(value, str) and value: item[field] = value message = event.get("message") if isinstance(message, str) and message: item["message"] = message[:500] for field in ("issues", "failed_checklist_items"): values = event.get(field) if isinstance(values, list): item[field] = [str(value)[:500] for value in values[:8] if isinstance(value, str)] failures = event.get("operation_failures") if isinstance(failures, list): item["operation_failures"] = [ {key: str(value)[:500] for key, value in failure.items() if key in {"feature_id", "message"}} for failure in failures[:8] if isinstance(failure, dict) ] fragment_hash = event.get("fragment_hash") if isinstance(fragment_hash, str) and fragment_hash: item["fragment_hash"] = fragment_hash stage_id = event.get("stage_id") candidate = None if isinstance(stage_id, str) and stage_id: try: candidate = self.artifacts.read_stage_json(task_id, stage_id, "candidate.json") except (OSError, ValueError, json.JSONDecodeError): # The ledger remains authoritative if an abandoned stage # is unavailable; diagnostic projection must not turn an # existing repair into a new service failure. candidate = None if isinstance(candidate, dict): atomic_id = candidate.get("actual_atomic_id") if isinstance(atomic_id, str) and atomic_id: item["atomic_id"] = atomic_id health = candidate.get("health") if isinstance(candidate.get("health"), dict) else {} bbox = health.get("bbox_mm") if isinstance(health.get("bbox_mm"), dict) else {} measured = { key: health[key] for key in ("solid_count", "feature_count", "volume_mm3") if isinstance(health.get(key), (int, float)) } if isinstance(bbox.get("dimensions"), list): measured["bbox_dimensions_mm"] = bbox["dimensions"][:3] if measured: item["measured"] = measured claim_results = candidate.get("claim_results") if isinstance(claim_results, list): failures: list[dict[str, Any]] = [] for claim in claim_results: if not isinstance(claim, dict) or claim.get("status") not in {"fail", "unavailable"}: continue evidence = claim.get("evidence") if isinstance(claim.get("evidence"), dict) else {} failures.append({ "claim_id": str(claim.get("claim_id") or ""), "claim_kind": str(claim.get("claim_kind") or ""), "status": str(claim.get("status") or ""), "evidence": evidence, }) if failures: item["failed_claims"] = failures[:8] operation_results = candidate.get("operation_verifier_results") if isinstance(operation_results, list): operation_blockers: list[dict[str, Any]] = [] for result in operation_results: if not isinstance(result, dict) or result.get("status") == "pass": continue evidence = result.get("evidence") if isinstance(result.get("evidence"), dict) else {} operation_blockers.append({ "claim_id": str(result.get("claim_id") or ""), "claim_kind": str(result.get("claim_kind") or ""), "status": str(result.get("status") or ""), "evidence": evidence, }) if operation_blockers: # An operation may be rejected because it produced no # net change. That result is often pending relative # to the full requirements contract, but it is still # a definite blocker for this specific action. item["operation_blockers"] = operation_blockers[:8] review = None if isinstance(stage_id, str) and stage_id: try: review = self.artifacts.read_stage_json(task_id, stage_id, "candidate-review.json") except (OSError, ValueError, json.JSONDecodeError): review = None if isinstance(review, dict): evidence = review.get("evidence") issues = review.get("issues") if isinstance(evidence, list): item["review_evidence"] = [str(value)[:500] for value in evidence[:8] if isinstance(value, str)] if isinstance(issues, list): item["review_issues"] = [str(value)[:500] for value in issues[:8] if isinstance(value, str)] diagnostics.append(item) if len(diagnostics) == 3: break return diagnostics def checkpoint_tokens(self, task_id: str, state: TaskState | None = None) -> dict[str, str]: """Map opaque rollback tokens to the current linear checkpoint lineage.""" state = state or self.repository.get_state(task_id) if state is None: return {} accepted = [ event for event in self.repository.ledger_events(task_id) if event.get("event") == "accepted" and isinstance(event.get("revision_id"), str) ] by_revision = {str(event["revision_id"]): event for event in accepted} if state.active_revision and state.active_revision not in by_revision: return {} lineage: list[str] = [] cursor = state.active_revision while cursor: event = by_revision.get(cursor) if event is None or cursor in lineage: return {} lineage.append(cursor) parent = event.get("parent_revision") if isinstance(parent, str): cursor = parent continue # v3 events written before parent_revision existed were linear. position = accepted.index(event) cursor = str(accepted[position - 1]["revision_id"]) if position else "" lineage.reverse() return {"checkpoint_root": "", **{f"checkpoint_{revision}": revision for revision in lineage}} def rollback_available(self, task_id: str, state: TaskState | None = None) -> bool: state = state or self.repository.get_state(task_id) if state is None or state.phase != TaskPhase.AWAITING_ACTION or state.pending_action is not None: return False has_earlier_checkpoint = any( revision != state.active_revision for revision in self.checkpoint_tokens(task_id, state).values() ) return has_earlier_checkpoint and any( event.get("event") == "geometry_conclusion" and event.get("decision") == "rollback" and event.get("working_head") == state.working_head for event in reversed(self.repository.ledger_events(task_id)) ) def repair_action_ready(self, task_id: str, state: TaskState | None = None) -> bool: """Require a structured diagnosis before repairing failed geometry. A rollback is itself a resolved repair decision. For a new feature, the author must instead record a return-to-action-selection conclusion after the latest failed candidate. The ordering matters: a diagnosis used to select a prior repair action cannot authorize retries after that new action fails on the same checkpoint. """ state = state or self.repository.get_state(task_id) if state is None or not state.repair_required: return True repair_triggers = { "candidate_rejected", "candidate_build_failed", "candidate_recovery_failed", "candidate_recovered_rejected", "final_review_repair", } for event in reversed(self.repository.ledger_events(task_id)): event_name = event.get("event") if event_name == "rollback": return True if event_name == "geometry_conclusion" and event.get("decision") == "return_to_action_selection": return True if event_name in repair_triggers: return False return False def record_geometry_conclusion(self, task_id: str, conclusion: GeometryConclusion, *, invocation_id: str) -> Accepted | Rejected: state = self.repository.get_state(task_id) if state is None or state.phase not in {TaskPhase.ACTION_PENDING, TaskPhase.AWAITING_ACTION}: return Rejected(WorkflowError(ErrorCode.AUTHOR_DECISION_REJECTED, "Geometry diagnosis is only available while repairing or selecting an action.")) stale = reject_stale_head(state, conclusion.working_head) if stale: return Rejected(stale) available = set(self.diagnostic_evidence_refs(task_id, state)) if not set(conclusion.evidence_refs).issubset(available): return Rejected(WorkflowError(ErrorCode.AUTHOR_FORMAT_INVALID, "Geometry conclusion references evidence outside the current server snapshot.")) if conclusion.decision == "rollback" and state.phase != TaskPhase.AWAITING_ACTION: return Rejected(WorkflowError(ErrorCode.AUTHOR_DECISION_REJECTED, "Return to action selection before requesting a checkpoint rollback.")) key = self._key(task_id, "geometry_conclusion", state.working_head, conclusion.model_dump(mode="json")) invocation = self.repository.begin_invocation(task_id, invocation_id, key) if invocation.status == "finished" and invocation.result is not None: return Accepted(invocation.result) if conclusion.decision == "return_to_action_selection" and state.phase == TaskPhase.ACTION_PENDING: next_state = transition(state, "diagnosis_return_to_action_selection", repair_required=True) else: next_state = transition(state, "diagnosis_recorded", repair_required=True) event = { "event": "geometry_conclusion", "working_head_before": state.working_head, "working_head": next_state.working_head, "evidence_refs": list(conclusion.evidence_refs), "root_cause": conclusion.root_cause, "decision": conclusion.decision, "corrective_intent": conclusion.corrective_intent or "", } result = {"decision": conclusion.decision, "phase": next_state.phase.value, "working_head": next_state.working_head} if not self._commit_invocation(next_state, [event], invocation, result): return Rejected(self._stale()) return Accepted(result) def rollback_checkpoint(self, task_id: str, rollback: RollbackCheckpoint, *, 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: return Rejected(WorkflowError(ErrorCode.AUTHOR_DECISION_REJECTED, "Checkpoint rollback requires an idle action-selection state.")) stale = reject_stale_head(state, rollback.working_head) if stale: return Rejected(stale) if not self.rollback_available(task_id, state): return Rejected(WorkflowError(ErrorCode.AUTHOR_DECISION_REJECTED, "Record a current geometry conclusion with decision=rollback before rolling back.")) target = self.checkpoint_tokens(task_id, state).get(rollback.checkpoint_token) if target is None: return Rejected(WorkflowError(ErrorCode.AUTHOR_FORMAT_INVALID, "Rollback checkpoint token is not in the active lineage.")) if target == state.active_revision: return Rejected(WorkflowError(ErrorCode.AUTHOR_DECISION_REJECTED, "Rollback must select an earlier checkpoint.")) key = self._key(task_id, "rollback", state.working_head, rollback.model_dump(mode="json")) invocation = self.repository.begin_invocation(task_id, invocation_id, key) if invocation.status == "finished" and invocation.result is not None: return Accepted(invocation.result) next_state = transition(state, "rollback", active_revision=target, repair_required=True) event = { "event": "rollback", "working_head_before": state.working_head, "working_head": next_state.working_head, "checkpoint_token": rollback.checkpoint_token, "rollback_from_revision": state.active_revision, "rollback_to_revision": target, "reason": rollback.reason, } result = {"status": "rolled_back", "active_revision": target, "working_head": next_state.working_head} if not self._commit_invocation(next_state, [event], invocation, result): return Rejected(self._stale()) return Accepted(result) def submit_cdsl_fragment(self, task_id: str, fragment: dict[str, Any], *, invocation_id: str) -> Accepted | Rejected: state = self.repository.get_state(task_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.")) contract = self.runtime.operation_contract(action.atomic_id) if contract["contract_hash"] != action.contract_hash: return Rejected(WorkflowError(ErrorCode.STALE_WORKING_HEAD, "The pending operation contract changed; read the current contract again.")) base = self.artifacts.read_active_cdsl(task_id, state.active_revision) topology = self.artifacts.read_topology(task_id, state.active_revision) selectors = self.runtime.selector_tokens(topology) references = self.runtime.reference_tokens(base) selector_policy = contract.get("selector_policy") if isinstance(contract.get("selector_policy"), dict) else {} expected_selector_kind = str(selector_policy.get("token_kind") or "") allowed_selector_tokens = ( [token for token, value in selectors.items() if isinstance(value, dict) and value.get("kind") == expected_selector_kind] if str((contract.get("fragment_shape") or {}).get("selector_tokens") or "forbidden") == "required" else [] ) errors = validate_fragment( contract, fragment, selector_tokens=allowed_selector_tokens, reference_tokens=list(references), root_xy_datum=not bool(state.active_revision), ) if errors: return Rejected(WorkflowError(ErrorCode.AUTHOR_FORMAT_INVALID, "CDSL fragment violates the active operation schema.", field_errors=tuple(errors))) fragment_hash = canonical_hash(fragment) exact_fingerprint = canonical_hash({ "active_revision": state.active_revision, "atomic_id": action.atomic_id, "fragment_hash": fragment_hash, }) prior_exact = next(( event for event in reversed(self.repository.ledger_events(task_id)) if event.get("failure_exact_fingerprint") == exact_fingerprint ), None) if prior_exact is not None: return Rejected(WorkflowError( ErrorCode.RUNTIME_PRECONDITION_FAILED, "This exact CDSL fragment was already proven to fail at the current checkpoint; choose a different fragment or operation path.", details={ "duplicate_fragment": True, "active_revision": state.active_revision, "atomic_id": action.atomic_id, "fragment_hash": fragment_hash, "failure_exact_fingerprint": exact_fingerprint, "normalized_error_code": str(prior_exact.get("normalized_error_code") or ErrorCode.RUNTIME_PRECONDITION_FAILED.value), }, )) key = self._key(task_id, "fragment", action.working_head, {"action_id": action.action_id, "fragment": fragment}) try: require_through = self._action_requires_through(task_id, action.requirement_ids) cdsl, audit = self.runtime.materialize_fragment(base, fragment, contract, selectors, references, require_through=require_through) except Exception as error: runtime_error = self._runtime_error(error) return Rejected(WorkflowError( runtime_error.code, runtime_error.message, field_errors=runtime_error.field_errors, retryable=runtime_error.retryable, details={ **runtime_error.details, "active_revision": state.active_revision, "atomic_id": action.atomic_id, "fragment_hash": fragment_hash, "failure_exact_fingerprint": exact_fingerprint, "normalized_error_code": runtime_error.code.value, }, )) invocation = self.repository.begin_invocation(task_id, invocation_id, key) if invocation.status == "finished" and invocation.result is not None: return Accepted(invocation.result) candidate_id = "candidate_" + sha256(key.encode("utf-8")).hexdigest()[:16] try: stage = self.artifacts.start_candidate_stage(task_id, key, {"schema_version": "cad.v3.candidate-input.v1", "idempotency_key": key, "candidate_id": candidate_id, "action_id": action.action_id, "working_head": action.working_head, "contract_hash": action.contract_hash, "fragment": fragment, "fragment_audit": audit}) except OSError as error: return self._park_for_storage_retry( state, event="candidate_stage_storage_failure", message=str(error), ) building = transition(state, "candidate_started", candidate_id=candidate_id, candidate_stage_id=stage.stage_id) if not self.repository.compare_and_swap(building, events=[{"event": "candidate_building", "candidate_id": candidate_id, "stage_id": stage.stage_id, "action_id": action.action_id, "fragment_hash": audit["fragment_hash"]}]): return Rejected(self._stale()) try: rebuilt, operation_failures = self.runtime.rebuild_best_effort(cdsl, stage.output_dir, task_id, candidate_id) attempted_feature_ids = {str(value) for value in audit.get("assigned_feature_ids") or () if isinstance(value, str)} executed_feature_ids = {str(value) for value in rebuilt.get("executed_feature_ids") or () if isinstance(value, str)} if attempted_feature_ids and not attempted_feature_ids.intersection(executed_feature_ids): next_state = transition(building, "candidate_rejected", candidate_id="", candidate_stage_id="", repair_required=True, error=ErrorCode.CANDIDATE_BUILD_FAILED) result = {"candidate_id": candidate_id, "status": "skipped", "code": ErrorCode.CANDIDATE_BUILD_FAILED.value, "operation_failures": operation_failures} if not self._commit_invocation(next_state, [{ "event": "candidate_operation_skipped", "candidate_id": candidate_id, "stage_id": stage.stage_id, "action_id": action.action_id, "working_head": action.working_head, "checkpoint_revision": state.active_revision, "atomic_id": action.atomic_id, "fragment_hash": fragment_hash, "operation_failures": operation_failures, "message": "The submitted feature did not execute; earlier executable features were retained.", }], invocation, result): return Rejected(self._stale()) return Rejected(WorkflowError( ErrorCode.CANDIDATE_BUILD_FAILED, "The submitted feature could not execute; the previous executable checkpoint was retained.", details={"operation_failures": operation_failures}, )) try: claim_results = self._evaluate_claims(task_id, rebuilt) except Exception as error: failed_state = transition(building, "failed", error=ErrorCode.REQUIREMENTS_SPEC_INVALID) result = {"candidate_id": candidate_id, "status": "failed", "code": ErrorCode.REQUIREMENTS_SPEC_INVALID.value} if not self._commit_invocation(failed_state, [{ "event": "requirements_contract_execution_failed", "candidate_id": candidate_id, "stage_id": stage.stage_id, "action_id": action.action_id, "message": str(error)[:1000], }], invocation, result): return Rejected(self._stale()) return Rejected(WorkflowError( ErrorCode.REQUIREMENTS_SPEC_INVALID, "The frozen requirements contract could not be evaluated; no CAD repair was attempted.", details={"diagnostic": str(error)[:1000]}, )) operation_results = self._operation_candidate_results( action, contract, cdsl, rebuilt, parent_facts=self._facts(task_id, state.active_revision), require_through=require_through, ) blockers = [ *self._candidate_blockers(task_id, action.requirement_ids, claim_results), *[item for item in operation_results if item.get("status") != "pass"], ] candidate = {"schema_version": "cad.v3.candidate.v1", "candidate_id": candidate_id, "stage_id": stage.stage_id, "action_id": action.action_id, "working_head": action.working_head, "actual_atomic_id": action.atomic_id, "fragment_hash": audit["fragment_hash"], "selector_snapshot_id": audit["selector_snapshot_id"], "claim_results": claim_results, "operation_verifier_results": operation_results, "blockers": blockers, "operation_failures": operation_failures, "executed_feature_ids": rebuilt.get("executed_feature_ids") or [], "health": rebuilt["health"], "render_manifest": rebuilt.get("render_manifest") or {}, "paths": rebuilt["paths"]} self.artifacts.write_stage_json(task_id, stage.stage_id, "candidate.json", candidate) review = transition(building, "candidate_built", candidate_id=candidate_id, candidate_stage_id=stage.stage_id) result = {"candidate_id": candidate_id, "stage_id": stage.stage_id, "status": "awaiting_review", "claim_results": claim_results, "operation_failures": operation_failures} if not self._commit_invocation(review, [{"event": "candidate_built", "candidate_id": candidate_id, "action_id": action.action_id, "claim_results": claim_results, "operation_failures": operation_failures}], invocation, result): return Rejected(self._stale()) return Accepted(result) except OSError as error: return self._park_retry( building, ErrorCode.STORAGE_FAILURE, event="candidate_build_storage_failure", message=str(error), details={"candidate_id": candidate_id, "stage_id": stage.stage_id}, ) except Exception as error: if "RENDER_SERVICE_UNAVAILABLE" in str(error): return self._park_retry( building, ErrorCode.RENDER_SERVICE_UNAVAILABLE, event="candidate_render_failure", message=str(error), details={"candidate_id": candidate_id, "stage_id": stage.stage_id}, ) if "RUNTIME_EXECUTION_FAILURE" in str(error): return self._park_retry( building, ErrorCode.RUNTIME_EXECUTION_FAILURE, event="candidate_runtime_execution_failure", message=str(error), details={"candidate_id": candidate_id, "stage_id": stage.stage_id, "checkpoint_revision": state.active_revision}, ) failed_state = transition(building, "candidate_rejected", candidate_id="", candidate_stage_id="", repair_required=True, error=ErrorCode.CANDIDATE_BUILD_FAILED) result = {"candidate_id": candidate_id, "status": "failed", "code": ErrorCode.CANDIDATE_BUILD_FAILED.value} if not self._commit_invocation(failed_state, [{ "event": "candidate_build_failed", "candidate_id": candidate_id, "stage_id": stage.stage_id, "action_id": action.action_id, "working_head": action.working_head, "checkpoint_revision": state.active_revision, "atomic_id": action.atomic_id, "fragment_hash": fragment_hash, "normalized_error_code": ErrorCode.CANDIDATE_BUILD_FAILED.value, "failure_exact_fingerprint": exact_fingerprint, "message": str(error)[:1000], }], invocation, result): return Rejected(self._stale()) return Rejected(WorkflowError(ErrorCode.CANDIDATE_BUILD_FAILED, "Candidate build failed; the checkpoint remains unchanged.", details={"diagnostic": str(error)[:1000]})) def recover_candidate_build(self, task_id: str) -> Accepted | Rejected: """Resume one persisted candidate build without inventing another action. A complete staging directory is converted to the next state directly. If a process stopped mid-build, the same stage/idempotency key is used and an already-written CDSL document is rebuilt in place. """ state = self.repository.get_state(task_id) action = state.pending_action if state else None if state is None or state.phase != TaskPhase.CANDIDATE_BUILDING or action is None or not state.candidate_stage_id: return Rejected(WorkflowError(ErrorCode.AUTHOR_DECISION_REJECTED, "There is no recoverable candidate build.")) try: candidate = self.artifacts.read_stage_json(task_id, state.candidate_stage_id, "candidate.json") if isinstance(candidate, dict): return self._advance_recovered_candidate(task_id, state, candidate) source = self.artifacts.read_stage_json(task_id, state.candidate_stage_id, "input.json") if not isinstance(source, dict) or source.get("candidate_id") != state.candidate_id: return self._park_for_storage_retry( state, event="candidate_recovery_storage_failure", message="Candidate recovery input is unavailable.", ) report = self.artifacts.read_stage_json(task_id, state.candidate_stage_id, "rebuild-report.json") topology = self.artifacts.read_stage_json(task_id, state.candidate_stage_id, "model.topology.json") cdsl = self.artifacts.read_stage_json(task_id, state.candidate_stage_id, "model.cdsl.json") if isinstance(report, dict) and isinstance(topology, dict) and isinstance(cdsl, dict): rebuilt = { "health": report.get("health") or {}, "topology": topology, "report": report, "render_manifest": report.get("render_manifest") or {}, "paths": {"cdsl": "model.cdsl.json", "step": "model.step", "glb": "model.glb", "topology": "model.topology.json", "report": "rebuild-report.json", "render_manifest": "renders/render-manifest.json"}, } else: base = self.artifacts.read_active_cdsl(task_id, state.active_revision) fragment = source.get("fragment") contract = self.runtime.operation_contract(action.atomic_id) if not isinstance(fragment, dict) or contract.get("contract_hash") != action.contract_hash: raise RuntimeError("RUNTIME_PRECONDITION_FAILED: candidate recovery contract is stale") selectors = self.runtime.selector_tokens(self.artifacts.read_topology(task_id, state.active_revision)) references = self.runtime.reference_tokens(base) cdsl, _audit = self.runtime.materialize_fragment(base, fragment, contract, selectors, references, require_through=self._action_requires_through(task_id, action.requirement_ids)) rebuilt, operation_failures = self.runtime.rebuild_best_effort(cdsl, self.artifacts.stage_output_dir(task_id, state.candidate_stage_id), task_id, state.candidate_id) if "operation_failures" not in locals(): operation_failures = [] claim_results = self._evaluate_claims(task_id, rebuilt) audit = source.get("fragment_audit") if isinstance(source.get("fragment_audit"), dict) else {} contract = self.runtime.operation_contract(action.atomic_id) operation_results = self._operation_candidate_results( action, contract, cdsl, rebuilt, parent_facts=self._facts(task_id, state.active_revision), require_through=self._action_requires_through(task_id, action.requirement_ids), ) candidate = { "schema_version": "cad.v3.candidate.v1", "candidate_id": state.candidate_id, "stage_id": state.candidate_stage_id, "action_id": action.action_id, "working_head": action.working_head, "actual_atomic_id": action.atomic_id, "fragment_hash": audit.get("fragment_hash") or canonical_hash(source.get("fragment") or {}), "selector_snapshot_id": audit.get("selector_snapshot_id") or "", "claim_results": claim_results, "operation_verifier_results": operation_results, "operation_failures": operation_failures, "blockers": [ *self._candidate_blockers(task_id, action.requirement_ids, claim_results), *[item for item in operation_results if item.get("status") != "pass"], ], "health": rebuilt["health"], "render_manifest": rebuilt.get("render_manifest") or {}, "paths": rebuilt["paths"], } self.artifacts.write_stage_json(task_id, state.candidate_stage_id, "candidate.json", candidate) return self._advance_recovered_candidate(task_id, state, candidate) except OSError as error: return self._park_for_storage_retry(state, event="candidate_recovery_storage_failure", message=str(error)) except Exception as error: if "RENDER_SERVICE_UNAVAILABLE" in str(error): return self._park_retry( state, ErrorCode.RENDER_SERVICE_UNAVAILABLE, event="candidate_recovery_render_failure", message=str(error), details={"candidate_id": state.candidate_id, "stage_id": state.candidate_stage_id}, ) if "RUNTIME_EXECUTION_FAILURE" in str(error): return self._park_retry( state, ErrorCode.RUNTIME_EXECUTION_FAILURE, event="candidate_recovery_runtime_execution_failure", message=str(error), details={"candidate_id": state.candidate_id, "stage_id": state.candidate_stage_id, "checkpoint_revision": state.active_revision}, ) failed = transition(state, "candidate_rejected", candidate_id="", candidate_stage_id="", repair_required=True, error=ErrorCode.CANDIDATE_BUILD_FAILED) self.repository.compare_and_swap(failed, events=[{ "event": "candidate_recovery_failed", "candidate_id": state.candidate_id, "stage_id": state.candidate_stage_id, "action_id": action.action_id, "working_head": action.working_head, "message": str(error)[:1000], }]) return Accepted({"candidate_id": state.candidate_id, "status": "failed", "code": ErrorCode.CANDIDATE_BUILD_FAILED.value, "diagnostic": str(error)[:1000]}) def record_candidate_review(self, task_id: str, review: CandidateReview, *, invocation_id: str) -> Accepted | Rejected: state = self.repository.get_state(task_id) action = state.pending_action if state else None if state is None or state.phase != TaskPhase.CANDIDATE_REVIEW or action is None: return Rejected(WorkflowError(ErrorCode.AUTHOR_DECISION_REJECTED, "Candidate review is not expected in the current workflow phase.")) if review.candidate_id != state.candidate_id or review.working_head != action.working_head: return Rejected(WorkflowError(ErrorCode.STALE_WORKING_HEAD, "Candidate review references a stale candidate or head.")) candidate = self.artifacts.read_stage_json(task_id, state.candidate_stage_id, "candidate.json") published = None if isinstance(candidate, dict) else self.artifacts.find_published_candidate(task_id, state.candidate_stage_id) published_revision = published[0] if published else "" if published: candidate = published[1] if not isinstance(candidate, dict): return Rejected(WorkflowError(ErrorCode.STORAGE_FAILURE, "Candidate review evidence is unavailable.", retryable=True)) actual = candidate.get("claim_results") if isinstance(candidate.get("claim_results"), list) else [] expected_ids = {str(item.get("claim_id") or "") for item in actual if isinstance(item, dict)} submitted = {item.claim_id: item.status for item in review.claim_coverage} submitted_ids = set(submitted) if submitted_ids != expected_ids or len(submitted_ids) != len(review.claim_coverage): return Rejected(WorkflowError(ErrorCode.AUTHOR_FORMAT_INVALID, "Candidate review must cover exactly the current claim set.")) mismatched_deterministic = [ str(item.get("claim_id") or "") for item in actual if isinstance(item, dict) and item.get("deterministic") and submitted.get(str(item.get("claim_id") or "")) != item.get("status") ] if mismatched_deterministic: return Rejected(WorkflowError( ErrorCode.AUTHOR_FORMAT_INVALID, "Candidate review must report the server's deterministic claim status exactly.", details={"claim_ids": mismatched_deterministic}, )) deterministic_fail = [item for item in actual if isinstance(item, dict) and item.get("deterministic") and item.get("status") in {"fail", "unavailable"}] key = self._key(task_id, "candidate_review", action.working_head, review.model_dump(mode="json")) invocation = self.repository.begin_invocation(task_id, invocation_id, key) if invocation.status == "finished" and invocation.result is not None: return Accepted(invocation.result) review_payload = review.model_dump(mode="json") try: if published_revision: self.artifacts.write_json_once(task_id, f"revisions/{published_revision}/candidate-review.json", review_payload) else: self.artifacts.write_stage_json(task_id, state.candidate_stage_id, "candidate-review.json", review_payload) except OSError as error: return self._park_for_storage_retry( state, event="candidate_review_storage_failure", message=str(error), ) operation_failures = candidate.get("operation_failures") if isinstance(candidate.get("operation_failures"), list) else [] candidate_blockers = candidate.get("blockers") if isinstance(candidate.get("blockers"), list) else [] needs_repair = review.verdict != "accept" or bool(deterministic_fail) or bool(operation_failures) or bool(candidate_blockers) revision_id = self._next_revision(task_id) if published_revision and published_revision != revision_id: return Rejected(WorkflowError(ErrorCode.STORAGE_FAILURE, "Published candidate revision does not match the current checkpoint lineage.", retryable=True)) try: paths = self.artifacts.publish_candidate(task_id, state.candidate_stage_id, revision_id) if not published_revision else {} except OSError as error: return self._park_for_storage_retry( state, event="candidate_publish_storage_failure", message=str(error), ) next_state = transition(state, "candidate_accepted", active_revision=revision_id, repair_required=needs_repair, error=ErrorCode.CLAIM_VERIFICATION_FAILED if needs_repair else None) event = {"event": "accepted", "action_id": action.action_id, "working_head_before": action.working_head, "parent_revision": state.active_revision, "revision_id": revision_id, "actual_atomic_id": action.atomic_id, "fragment_hash": candidate.get("fragment_hash"), "selector_snapshot_id": candidate.get("selector_snapshot_id"), "candidate_id": state.candidate_id, "review_path": f"revisions/{revision_id}/candidate-review.json", "coverage": actual, "issues": list(review.issues), "operation_failures": operation_failures, "repair_required": needs_repair} result = {"candidate_id": state.candidate_id, "revision_id": revision_id, "paths": paths, "status": "accepted_with_issues" if needs_repair else "accepted"} if not self._commit_invocation(next_state, [event], invocation, result): return Rejected(self._stale()) return Accepted(result) def recover_candidate_review(self, task_id: str) -> Accepted | Rejected | None: state = self.repository.get_state(task_id) if state is None or state.phase != TaskPhase.CANDIDATE_REVIEW or not state.candidate_stage_id: return None raw = self.artifacts.read_stage_json(task_id, state.candidate_stage_id, "candidate-review.json") if raw is None: published = self.artifacts.find_published_candidate(task_id, state.candidate_stage_id) raw = self.artifacts.read_json(task_id, f"revisions/{published[0]}/candidate-review.json") if published else None if raw is None: return None try: review = CandidateReview.model_validate(raw) except ValueError as error: return Rejected(WorkflowError(ErrorCode.STORAGE_FAILURE, "Persisted candidate review is invalid.", details={"diagnostic": str(error)[:1000]})) return self.record_candidate_review(task_id, review, invocation_id=f"{task_id}_recover_candidate_{secrets.token_hex(8)}") def complete_task(self, task_id: str, *, invocation_id: str) -> Accepted | Rejected: state = self.repository.get_state(task_id) if state is None: return Rejected(WorkflowError(ErrorCode.AUTHOR_DECISION_REJECTED, "Task completion is not available for an unknown task.")) # Completion idempotency is revision-scoped. A duplicate request after # final validation or publication replays the existing result and can # never start a second final rebuild/review. key = self._key(task_id, "complete", state.active_revision, {}) if state.phase == TaskPhase.FINAL_VALIDATION and state.active_revision: invocation = self.repository.begin_invocation(task_id, invocation_id, key) if invocation.status == "finished" and invocation.result is not None: return Accepted(invocation.result) return Rejected(WorkflowError(ErrorCode.AUTHOR_DECISION_REJECTED, "Final validation is already in progress for this revision.")) if state.phase != TaskPhase.AWAITING_ACTION or state.pending_action is not None or state.repair_required: return Rejected(WorkflowError(ErrorCode.AUTHOR_DECISION_REJECTED, "Task completion is not available until there is no pending action or repair.")) claim_results = self._evaluate_claims(task_id, self._facts(task_id, state.active_revision)) deterministic = [item for item in claim_results if item.get("deterministic")] if not state.active_revision or any(item.get("status") != "pass" for item in deterministic): return Rejected(WorkflowError(ErrorCode.CLAIM_VERIFICATION_FAILED, "All deterministic claims must pass before final review.", details={"claim_results": claim_results})) invocation = self.repository.begin_invocation(task_id, invocation_id, key) if invocation.status == "finished" and invocation.result is not None: return Accepted(invocation.result) next_state = transition(state, "final_requested") result = {"revision_id": state.active_revision, "working_head": next_state.working_head, "claim_results": claim_results} if not self._commit_invocation(next_state, [{"event": "final_validation_requested", "revision_id": state.active_revision, "claim_results": claim_results}], invocation, result): return Rejected(self._stale()) return Accepted(result) def recover_final_review(self, task_id: str) -> Accepted | Rejected | None: state = self.repository.get_state(task_id) if state is None or state.phase != TaskPhase.FINAL_VALIDATION or not state.active_revision: return None raw = self.artifacts.read_json(task_id, self._final_review_path(state.active_revision)) if raw is None: return None try: review = FinalReview.model_validate(raw) except ValueError as error: return Rejected(WorkflowError(ErrorCode.STORAGE_FAILURE, "Persisted final review is invalid.", details={"diagnostic": str(error)[:1000]})) return self.record_final_review(task_id, review, invocation_id=f"{task_id}_recover_final_{secrets.token_hex(8)}") def _advance_recovered_candidate(self, task_id: str, state: TaskState, candidate: dict[str, Any]) -> Accepted | Rejected: action = state.pending_action if action is None or candidate.get("candidate_id") != state.candidate_id or candidate.get("action_id") != action.action_id: return Rejected(WorkflowError(ErrorCode.STORAGE_FAILURE, "Candidate recovery evidence does not match the pending action.", retryable=True)) claim_results = candidate.get("claim_results") if isinstance(candidate.get("claim_results"), list) else [] operation_results = candidate.get("operation_verifier_results") if isinstance(candidate.get("operation_verifier_results"), list) else [] blockers = [ *self._candidate_blockers(task_id, action.requirement_ids, claim_results), *[item for item in operation_results if isinstance(item, dict) and item.get("status") != "pass"], ] key = str((self.artifacts.read_stage_json(task_id, state.candidate_stage_id, "input.json") or {}).get("idempotency_key") or "") invocation = self.repository.begin_invocation(task_id, f"{task_id}_recover_build_{secrets.token_hex(8)}", key) if key else None if invocation is not None and invocation.status == "finished" and invocation.result is not None: return Accepted(invocation.result) review = transition(state, "candidate_built", candidate_id=state.candidate_id, candidate_stage_id=state.candidate_stage_id) result = {"candidate_id": state.candidate_id, "stage_id": state.candidate_stage_id, "status": "awaiting_review", "claim_results": claim_results} committed = self._commit_invocation(review, [{"event": "candidate_recovered", "candidate_id": state.candidate_id, "action_id": action.action_id, "claim_results": claim_results}], invocation, result) if invocation is not None else self.repository.compare_and_swap(review, events=[{"event": "candidate_recovered", "candidate_id": state.candidate_id, "action_id": action.action_id, "claim_results": claim_results}]) if not committed: return Rejected(self._stale()) return Accepted(result) def finalize_best_effort(self, task_id: str, *, reason: ErrorCode, invocation_id: str) -> Accepted | Rejected: """Publish the last executable checkpoint when further repair is bounded. This is deliberately separate from strict final validation. It does not claim that unmet acceptance targets passed; it makes the usable model and its measured gaps durable instead of converting a planning dead-end into a failed task with no deliverable. """ state = self.repository.get_state(task_id) if state is None or not state.active_revision: return Rejected(WorkflowError(ErrorCode.AUTHOR_DECISION_REJECTED, "Best-effort completion requires an executable checkpoint.")) if state.phase == TaskPhase.COMPLETED: return Accepted({"status": "completed", "revision_id": state.active_revision}) key = self._key(task_id, "best_effort_complete", state.active_revision, {"reason": reason.value}) invocation = self.repository.begin_invocation(task_id, invocation_id, key) if invocation.status == "finished" and invocation.result is not None: return Accepted(invocation.result) claim_results = self._evaluate_claims(task_id, self._facts(task_id, state.active_revision)) issues = [ f"{item.get('claim_kind')}: {item.get('status')}" for item in claim_results if item.get("status") != "pass" ] next_state = transition(state, "best_effort_completed", error=ErrorCode.BEST_EFFORT_COMPLETED, repair_required=False) result = {"status": "completed_with_warnings", "revision_id": state.active_revision, "claim_results": claim_results, "issues": issues} if not self._commit_invocation(next_state, [{ "event": "completed_best_effort", "revision_id": state.active_revision, "termination_code": reason.value, "message": "Further CAD repair was bounded; the last executable checkpoint was published.", "issues": issues, "claim_results": claim_results, "completion_result_path": "completion-result.md", }], invocation, result): return Rejected(self._stale()) return Accepted(result) def record_final_review(self, task_id: str, review: FinalReview, *, invocation_id: str) -> Accepted | Rejected: state = self.repository.get_state(task_id) if state is None or state.phase != TaskPhase.FINAL_VALIDATION: return Rejected(WorkflowError(ErrorCode.AUTHOR_DECISION_REJECTED, "Final review is not expected in the current workflow phase.")) stale = reject_stale_head(state, review.working_head) if stale: return Rejected(stale) claim_results = self._evaluate_claims(task_id, self._facts(task_id, state.active_revision)) expected_ids = {str(item.get("claim_id") or "") for item in claim_results} submitted = {item.claim_id: item.status for item in review.claim_coverage} submitted_ids = set(submitted) if submitted_ids != expected_ids or len(submitted_ids) != len(review.claim_coverage): return Rejected(WorkflowError(ErrorCode.AUTHOR_FORMAT_INVALID, "Final review must cover exactly the final claim set.")) mismatched_deterministic = [ str(item.get("claim_id") or "") for item in claim_results if item.get("deterministic") and submitted.get(str(item.get("claim_id") or "")) != item.get("status") ] if mismatched_deterministic: return Rejected(WorkflowError( ErrorCode.AUTHOR_FORMAT_INVALID, "Final review must report the server's deterministic claim status exactly.", details={"claim_ids": mismatched_deterministic}, )) deterministic_fail = [item for item in claim_results if item.get("deterministic") and item.get("status") != "pass"] review_coverage = submitted visual_not_passed = [ item for item in claim_results if not item.get("deterministic") and review_coverage.get(str(item.get("claim_id") or "")) != "pass" ] key = self._key(task_id, "final_review", state.working_head, review.model_dump(mode="json")) invocation = self.repository.begin_invocation(task_id, invocation_id, key) if invocation.status == "finished" and invocation.result is not None: return Accepted(invocation.result) final_review_path = self._final_review_path(state.active_revision) try: self.artifacts.write_json_once(task_id, final_review_path, review.model_dump(mode="json")) except OSError as error: return self._park_for_storage_retry( state, event="final_review_storage_failure", message=str(error), ) if review.verdict != "pass" or deterministic_fail or visual_not_passed: accepted_fragment = next(( event.get("fragment_hash") for event in reversed(self.repository.ledger_events(task_id)) if event.get("event") == "accepted" and event.get("revision_id") == state.active_revision ), "") atomic_id = next(( str(event.get("actual_atomic_id") or "") for event in reversed(self.repository.ledger_events(task_id)) if event.get("event") == "accepted" and event.get("revision_id") == state.active_revision ), "") exact_fingerprint = canonical_hash({ "active_revision": state.active_revision, "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) result = {"status": "repair", "revision_id": state.active_revision} if not self._commit_invocation(next_state, [{ "event": "final_review_repair", "revision_id": state.active_revision, "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], "issues": list(review.issues), "evidence": list(review.evidence), "failed_checklist_items": [ str(requirement.get("statement") or "") for requirement in (self._requirements_contract(task_id) or {}).get("requirements") or () if isinstance(requirement, dict) and any(str(claim.get("claim_id") or "") in {str(item.get("claim_id") or "") for item in deterministic_fail + visual_not_passed} for claim in requirement.get("acceptance_claims") or () if isinstance(claim, dict)) ], "atomic_id": atomic_id, "fragment_hash": accepted_fragment, "failure_exact_fingerprint": exact_fingerprint, "normalized_error_code": ErrorCode.CLAIM_VERIFICATION_FAILED.value if deterministic_fail else ErrorCode.CANDIDATE_REVIEW_REJECTED.value, }], invocation, result): return Rejected(self._stale()) 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): return Rejected(self._stale()) return Accepted(result) def _evaluate_claims(self, task_id: str, facts: dict[str, Any]) -> 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)] return self.verifiers.evaluate(claims, facts) def claim_summary(self, task_id: str, state: TaskState) -> list[dict[str, Any]]: """Return bounded current claim facts for author context assembly.""" return [ { "claim_id": str(item.get("claim_id") or ""), "claim_kind": str(item.get("claim_kind") or ""), "deterministic": bool(item.get("deterministic")), "status": str(item.get("status") or "pending"), "evidence": item.get("evidence") if isinstance(item.get("evidence"), dict) else {}, } for item in self._evaluate_claims(task_id, self._facts(task_id, state.active_revision)) if isinstance(item, dict) ] def model_summary(self, task_id: str, state: TaskState) -> dict[str, Any]: """Expose measurements, not raw topology, in recurring author turns.""" if not state.active_revision: return {"revision_id": "", "available": False} facts = self._facts(task_id, state.active_revision) health = facts.get("health") if isinstance(facts.get("health"), dict) else {} topology = facts.get("topology") if isinstance(facts.get("topology"), dict) else {} counts: dict[str, int] = {} inner_cylindrical_bores: list[dict[str, Any]] = [] for record in topology.get("records") or (): if isinstance(record, dict) and isinstance(record.get("kind"), str): kind = record["kind"] counts[kind] = counts.get(kind, 0) + 1 geometry = record.get("geometry") if isinstance(record.get("geometry"), dict) else {} radius = geometry.get("radius_mm") axis_origin = geometry.get("axis_origin_mm") if ( geometry.get("surface_type") == "cylinder" and geometry.get("cylinder_role") == "inner" and isinstance(radius, (int, float)) and isinstance(axis_origin, list) and len(axis_origin) == 3 and all(isinstance(value, (int, float)) for value in axis_origin) ): inner_cylindrical_bores.append({ "diameter_mm": round(float(radius) * 2, 6), "axis_origin_mm": [round(float(value), 6) for value in axis_origin], "through": bool(geometry.get("through")), }) bbox = health.get("bbox_mm") if isinstance(health.get("bbox_mm"), dict) else {} return { "revision_id": state.active_revision, "available": bool(health or topology), "solid_count": health.get("solid_count"), "bbox_dimensions_mm": bbox.get("dimensions"), "topology_snapshot_id": str(topology.get("snapshot_id") or ""), "topology_counts": counts, # These are compact measured facts, not author-controlled CDSL. # They make duplicate or misplaced hole repairs observable without # injecting the full topology snapshot into every author turn. "inner_cylindrical_bores": inner_cylindrical_bores[:16], } def _facts(self, task_id: str, revision_id: str) -> dict[str, Any]: if not revision_id: return {} report = self.artifacts.read_json(task_id, f"revisions/{revision_id}/rebuild-report.json") or {} return {"health": report.get("health") or {}, "topology": self.artifacts.read_topology(task_id, revision_id) or {}, "report": report} def _action_requires_through(self, task_id: str, requirement_ids: tuple[str, ...]) -> bool: contract = self._requirements_contract(task_id) or {} return any(claim.get("claim_kind") == "through_cylindrical_bore" for requirement in contract.get("requirements") or () if isinstance(requirement, dict) and requirement.get("requirement_id") in requirement_ids for claim in requirement.get("acceptance_claims") or () if isinstance(claim, dict)) def _candidate_blockers(self, task_id: str, action_requirements: tuple[str, ...], results: list[dict[str, Any]]) -> list[dict[str, Any]]: # Global connected-body failure always rejects; action-linked claims # must not already be false, while future-work claims may remain pending. return [item for item in results if item.get("status") in {"fail", "unavailable"} and (item.get("claim_kind") in {"solid_count_equals", "single_connected_body"} or item.get("claim_id", "") in self._claim_ids_for_requirements(task_id, action_requirements))] def _operation_candidate_results( self, action: PendingAction, contract: dict[str, Any], cdsl: dict[str, Any], rebuilt: dict[str, Any], *, parent_facts: dict[str, Any], require_through: bool, ) -> list[dict[str, Any]]: """Run the verified operation-level acceptance checks after rebuild. Requirement claims prove the user contract. These checks separately prove that an operation declared in the runtime registry actually made the kind of change it advertises, even when a user did not include a matching claim. A blind-hole action does not require the through-bore verifier unless its linked requirement explicitly requests through topology. """ features = cdsl.get("features") if isinstance(cdsl.get("features"), list) else [] feature = next( (item for item in reversed(features) if isinstance(item, dict) and item.get("atomic_id") == action.atomic_id), {}, ) params = feature.get("params") if isinstance(feature, dict) and isinstance(feature.get("params"), dict) else {} claims: list[dict[str, Any]] = [] for claim_kind in contract.get("candidate_verifiers") or (): if claim_kind == "through_cylindrical_bore" and not require_through: continue expected: dict[str, Any] if claim_kind in {"cylindrical_bore", "through_cylindrical_bore"}: diameter = 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"}}] increment = len(positions) if isinstance(positions, list) else 1 prior_count = self._existing_bore_count(claim_kind, float(diameter), parent_facts) expected = { "diameter_mm": float(diameter), # Candidate topology represents the full model, not only # the feature just submitted. Therefore the operation # proof must compare against the parent checkpoint plus # this action's declared number of positions. "count": prior_count + increment, "tolerance_mm": 0.01, } elif claim_kind in {"single_connected_body", "volume_decreased"}: expected = {} else: return [{"claim_id": f"operation_{action.action_id}_{claim_kind}", "claim_kind": str(claim_kind), "deterministic": True, "status": "unavailable", "evidence": {"reason": "operation verifier has no runtime expected-value binding"}}] claims.append({"claim_id": f"operation_{action.action_id}_{claim_kind}", "claim_kind": claim_kind, "expected": expected}) facts = { "health": rebuilt.get("health") or {}, "parent_health": parent_facts.get("health") or {}, "topology": rebuilt.get("topology") or {}, "report": rebuilt.get("report") or {}, } results = self.verifiers.evaluate(claims, facts) for result, claim in zip(results, claims, strict=True): expected = claim.get("expected") if isinstance(claim.get("expected"), dict) else {} if claim.get("claim_kind") not in {"cylindrical_bore", "through_cylindrical_bore"}: continue evidence = result.get("evidence") if isinstance(result.get("evidence"), dict) else {} evidence = dict(evidence) requested_total = expected.get("count") if isinstance(requested_total, int): positions = params.get("positions") increment = len(positions) if isinstance(positions, list) else 1 evidence.update({ "parent_matching_count": requested_total - increment, "expected_increment": increment, "expected_total_count": requested_total, }) result["evidence"] = evidence return results def _existing_bore_count(self, claim_kind: str, diameter_mm: float, parent_facts: dict[str, Any]) -> int: """Measure same-diameter bores in the parent with registry semantics. The registry owns topology coalescing, including periodic faces from a single analytic circle. Asking it for one instance provides an exact count for populated parent geometry without duplicating B-rep logic in the workflow layer. """ topology = parent_facts.get("topology") if isinstance(parent_facts.get("topology"), dict) else {} if not isinstance(topology.get("records"), list) or not topology["records"]: return 0 result = self.verifiers.evaluate([{ "claim_id": "operation_parent_bore_count", "claim_kind": claim_kind, "expected": {"diameter_mm": diameter_mm, "count": 1, "tolerance_mm": 0.01}, }], parent_facts)[0] evidence = result.get("evidence") if isinstance(result.get("evidence"), dict) else {} actual_count = evidence.get("actual_count") if isinstance(actual_count, int): return actual_count matched = evidence.get("matched_cylindrical_faces") if isinstance(matched, list): return len(matched) through = evidence.get("through_bores") if isinstance(through, list): return len(through) return 0 def _claim_ids_for_requirements(self, task_id: str, requirement_ids: tuple[str, ...]) -> set[str]: contract = self._requirements_contract(task_id) or {} return { str(claim.get("claim_id") or "") for requirement in contract.get("requirements") or () if isinstance(requirement, dict) and requirement.get("requirement_id") in requirement_ids for claim in requirement.get("acceptance_claims") or () if isinstance(claim, dict) } def _next_revision(self, task_id: str) -> str: """Allocate a monotonically increasing immutable revision ID. Rollback changes the active lineage but never reuses an artifact directory. The ledger is the authority for already allocated IDs, including a directory published just before an interrupted CAS. """ values = [ int(str(event.get("revision_id") or "").removeprefix("rev_")) for event in self.repository.ledger_events(task_id) if event.get("event") == "accepted" and str(event.get("revision_id") or "").startswith("rev_") and str(event.get("revision_id") or "").removeprefix("rev_").isdigit() ] return f"rev_{max(values, default=0) + 1:03d}" def _requirements_contract(self, task_id: str, state: TaskState | None = None) -> dict[str, Any] | None: state = state or self.repository.get_state(task_id) if state is None or not state.requirements_contract_path: return None return self.artifacts.read_requirements_contract(task_id, state.requirements_contract_path) @staticmethod def _final_review_path(revision_id: str) -> str: # Revisions are manifest-sealed when a candidate becomes a checkpoint. # Final review evidence therefore has its own immutable namespace. return f"reviews/final/{revision_id}/final-review.json" def _commit_invocation(self, state: TaskState, events: list[dict[str, Any]], invocation: Any, result: dict[str, Any]) -> bool: """Commit state/outbox and its idempotent result in one SQLite transaction.""" return self.repository.compare_and_swap( state, events=events, invocation_id=invocation.invocation_id, invocation_result=result, ) @staticmethod def _key(task_id: str, kind: str, head: str, value: dict[str, Any]) -> str: encoded = json.dumps(value, ensure_ascii=True, sort_keys=True, separators=(",", ":")) return sha256(f"{task_id}|{kind}|{head}|{encoded}".encode("utf-8")).hexdigest() @staticmethod def _failure_class_fingerprint(active_revision: str, atomic_id: str, code: ErrorCode) -> str: return sha256(f"{active_revision}|{atomic_id}|{code.value}".encode("utf-8")).hexdigest() def _failure_class_events(self, task_id: str, fingerprint: str) -> list[dict[str, Any]]: return [ event for event in self.repository.ledger_events(task_id) if event.get("failure_class_fingerprint") == fingerprint ] @staticmethod def _stale() -> WorkflowError: return WorkflowError(ErrorCode.STALE_WORKING_HEAD, "Task state changed before this command could commit.") @staticmethod def _runtime_error(error: Exception) -> WorkflowError: message = str(error) code = ( ErrorCode.RUNTIME_PRECONDITION_FAILED if "RUNTIME_PRECONDITION_FAILED" in message else ErrorCode.RUNTIME_CONTRACT_INVALID if "RUNTIME_CONTRACT_INVALID" in message or "CDSL schema violation" in message else ErrorCode.RUNTIME_EXECUTION_FAILURE if "RUNTIME_EXECUTION_FAILURE" in message else ErrorCode.AUTHOR_FORMAT_INVALID ) return WorkflowError(code, message[:1000]) def _park_for_storage_retry(self, state: TaskState, *, event: str, message: str) -> Rejected: """Persist a recoverable artifact failure without changing CAD evidence.""" return self._park_retry(state, ErrorCode.STORAGE_FAILURE, event=event, message=message) def _park_retry( self, state: TaskState, code: ErrorCode, *, event: str, message: str, details: dict[str, Any] | None = None, ) -> Rejected: waiting = transition(state, "waiting_retry", error=code) payload = { "event": event, "code": code.value, "message": message[:1000], **(details or {}), } if not self.repository.compare_and_swap(waiting, events=[{ **payload, }]): return Rejected(self._stale()) return Rejected(WorkflowError( code, "Candidate render evidence is temporarily unavailable; the checkpoint is preserved." if code == ErrorCode.RENDER_SERVICE_UNAVAILABLE else "The CAD runtime failed after validation; retry will resume from the preserved checkpoint." if code == ErrorCode.RUNTIME_EXECUTION_FAILURE else "Candidate artifact storage is temporarily unavailable; the checkpoint is preserved.", retryable=True, ))