integrate ganjihong engine refactor
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
"""Pure domain objects and policies for protocol v3."""
|
||||
"""Pure domain objects and policies for the Authoring CDSL protocol."""
|
||||
|
||||
from .errors import ErrorCode, WorkflowError
|
||||
from .state import TaskPhase, TaskState, transition
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
"""Canonical partial matching for acceptance-claim expectations."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
|
||||
def contains_expected(actual: Any, required: Any) -> bool:
|
||||
"""Allow an actual claim to add provider-chosen optional fields only."""
|
||||
if isinstance(required, dict):
|
||||
return isinstance(actual, dict) and all(
|
||||
key in actual and contains_expected(actual[key], value)
|
||||
for key, value in required.items()
|
||||
)
|
||||
if isinstance(required, list):
|
||||
return isinstance(actual, list) and len(actual) == len(required) and all(
|
||||
contains_expected(actual_item, required_item)
|
||||
for actual_item, required_item in zip(actual, required, strict=True)
|
||||
)
|
||||
return actual == required
|
||||
@@ -1,4 +1,4 @@
|
||||
"""Typed, serializable failures used across the v3 workflow."""
|
||||
"""Typed, serializable failures used across the single-stage workflow."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -9,29 +9,25 @@ from typing import Any
|
||||
|
||||
class ErrorCode(StrEnum):
|
||||
CANCELLED = "CANCELLED"
|
||||
AUTHOR_FORMAT_INVALID = "AUTHOR_FORMAT_INVALID"
|
||||
AUTHOR_DECISION_REJECTED = "AUTHOR_DECISION_REJECTED"
|
||||
STALE_WORKING_HEAD = "STALE_WORKING_HEAD"
|
||||
FAILED_AUTHOR_FORMAT = "FAILED_AUTHOR_FORMAT"
|
||||
RUNTIME_PRECONDITION_FAILED = "RUNTIME_PRECONDITION_FAILED"
|
||||
RUNTIME_CONTRACT_INVALID = "RUNTIME_CONTRACT_INVALID"
|
||||
CANDIDATE_BUILD_FAILED = "CANDIDATE_BUILD_FAILED"
|
||||
CANDIDATE_REVIEW_REJECTED = "CANDIDATE_REVIEW_REJECTED"
|
||||
VERIFIER_UNAVAILABLE = "VERIFIER_UNAVAILABLE"
|
||||
CLAIM_VERIFICATION_FAILED = "CLAIM_VERIFICATION_FAILED"
|
||||
MODEL_STRUCTURED_OUTPUT_UNSUPPORTED = "MODEL_STRUCTURED_OUTPUT_UNSUPPORTED"
|
||||
MODEL_PROTOCOL_CHECK_PENDING = "MODEL_PROTOCOL_CHECK_PENDING"
|
||||
AUTHOR_TRANSPORT_UNAVAILABLE = "AUTHOR_TRANSPORT_UNAVAILABLE"
|
||||
REVIEW_SERVICE_UNAVAILABLE = "REVIEW_SERVICE_UNAVAILABLE"
|
||||
RENDER_SERVICE_UNAVAILABLE = "RENDER_SERVICE_UNAVAILABLE"
|
||||
STORAGE_FAILURE = "STORAGE_FAILURE"
|
||||
CALL_BUDGET_EXHAUSTED = "CALL_BUDGET_EXHAUSTED"
|
||||
REQUIREMENTS_SPEC_INVALID = "REQUIREMENTS_SPEC_INVALID"
|
||||
NO_PROGRESS_LIMIT = "NO_PROGRESS_LIMIT"
|
||||
RUNTIME_EXECUTION_FAILURE = "RUNTIME_EXECUTION_FAILURE"
|
||||
BEST_EFFORT_COMPLETED = "BEST_EFFORT_COMPLETED"
|
||||
FAILED_INTERNAL = "FAILED_INTERNAL"
|
||||
WAITING_FOR_USER = "WAITING_FOR_USER"
|
||||
AUTHOR_FORBIDDEN_FIELD = "AUTHOR_FORBIDDEN_FIELD"
|
||||
AUTHOR_SCHEMA_INVALID = "AUTHOR_SCHEMA_INVALID"
|
||||
AUTHOR_REFERENCE_INVALID = "AUTHOR_REFERENCE_INVALID"
|
||||
AUTHOR_CYCLE = "AUTHOR_CYCLE"
|
||||
OPERATION_UNSUPPORTED = "OPERATION_UNSUPPORTED"
|
||||
SELECTOR_NOT_FOUND = "SELECTOR_NOT_FOUND"
|
||||
SELECTOR_AMBIGUOUS = "SELECTOR_AMBIGUOUS"
|
||||
SELECTOR_DEPENDENCY_UNAVAILABLE = "SELECTOR_DEPENDENCY_UNAVAILABLE"
|
||||
SELECTOR_KIND_MISMATCH = "SELECTOR_KIND_MISMATCH"
|
||||
ENGINE_EXECUTION_FAILED = "ENGINE_EXECUTION_FAILED"
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
|
||||
@@ -1,265 +0,0 @@
|
||||
"""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
|
||||
)
|
||||
@@ -1,8 +1,7 @@
|
||||
"""Versioned runtime operation contracts and dynamic fragment schemas."""
|
||||
"""Versioned runtime operation contracts for the Authoring compiler."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from copy import deepcopy
|
||||
from hashlib import sha256
|
||||
import json
|
||||
from typing import Any
|
||||
@@ -15,25 +14,6 @@ class OperationContractError(ValueError):
|
||||
pass
|
||||
|
||||
|
||||
SEMANTIC_PREFLIGHT_NAMES = frozenset({
|
||||
"sketch_workplane",
|
||||
"profile_non_self_intersecting",
|
||||
"host_face_exists",
|
||||
"hole_positions_on_host_plane",
|
||||
"cut_exit_distance",
|
||||
"requires_active_solid",
|
||||
"revolve_axis_on_sketch",
|
||||
"reference_plane_nonzero_normal",
|
||||
"reference_axis_nonzero_direction",
|
||||
"selected_edges_exist",
|
||||
"source_features_exist",
|
||||
"mirror_plane_exists",
|
||||
"loft_profiles_exist",
|
||||
"loft_profiles_closed",
|
||||
"loft_profiles_single_region",
|
||||
})
|
||||
|
||||
|
||||
def canonical_hash(value: dict[str, Any]) -> str:
|
||||
return sha256(json.dumps(value, ensure_ascii=True, sort_keys=True, separators=(",", ":")).encode("utf-8")).hexdigest()
|
||||
|
||||
@@ -58,10 +38,16 @@ def _is_closed(schema: Any) -> bool:
|
||||
return True
|
||||
|
||||
|
||||
def is_authoring_schema_closed(schema: Any) -> bool:
|
||||
"""Whether an operation can safely be exposed in the Authoring whitelist."""
|
||||
return _is_closed(schema)
|
||||
|
||||
|
||||
def validate_operation_contract(contract: dict[str, Any]) -> None:
|
||||
required = {
|
||||
"atomic_id", "contract_version", "fragment_shape", "author_params_schema", "selector_policy",
|
||||
"server_injected_paths", "reference_policy", "semantic_preflight", "candidate_verifiers",
|
||||
"runtime_capability",
|
||||
}
|
||||
if not required.issubset(contract) or set(contract) - required - {"contract_hash", "registry_revision"}:
|
||||
raise OperationContractError("Operation contract has unknown or missing fields")
|
||||
@@ -111,113 +97,43 @@ def validate_operation_contract(contract: dict[str, Any]) -> None:
|
||||
if reference["mode"] == "snapshot_bound":
|
||||
if set(reference) != {"mode", "slot", "token_kind", "min_items", "max_items", "snapshot_bound"}:
|
||||
raise OperationContractError("Snapshot-bound reference policy is invalid")
|
||||
if not isinstance(reference["slot"], str) or not reference["slot"].startswith("params.") or reference["token_kind"] != "feature" or not isinstance(reference["min_items"], int) or not isinstance(reference["max_items"], int) or not 1 <= reference["min_items"] <= reference["max_items"] <= 64 or reference["snapshot_bound"] is not True:
|
||||
raise OperationContractError("Snapshot-bound reference policy has invalid bounds")
|
||||
reference_name = reference["slot"].removeprefix("params.")
|
||||
properties = params.get("properties") if isinstance(params.get("properties"), dict) else {}
|
||||
reference_schema = properties.get(reference_name)
|
||||
required_params = params.get("required") if isinstance(params.get("required"), list) else []
|
||||
slot = reference["slot"]
|
||||
if (
|
||||
not isinstance(reference_schema, dict)
|
||||
or reference_schema.get("type") != "array"
|
||||
or not isinstance(reference_schema.get("items"), dict)
|
||||
or reference_name not in required_params
|
||||
not isinstance(slot, str)
|
||||
or (slot != "feature.selectors" and not slot.startswith("params."))
|
||||
or reference["token_kind"] not in {"face", "edge", "plane", "axis", "body", "feature"}
|
||||
or not isinstance(reference["min_items"], int)
|
||||
or not isinstance(reference["max_items"], int)
|
||||
or not 0 <= reference["min_items"] <= reference["max_items"] <= 64
|
||||
or reference["snapshot_bound"] is not True
|
||||
):
|
||||
raise OperationContractError("Snapshot-bound reference slot must be a required author array")
|
||||
raise OperationContractError("Snapshot-bound reference policy has invalid bounds")
|
||||
if slot == "feature.selectors":
|
||||
if shape["selector_tokens"] != "required" or selector["token_kind"] != reference["token_kind"]:
|
||||
raise OperationContractError("Feature selector reference policy disagrees with selector policy")
|
||||
else:
|
||||
reference_name = slot.removeprefix("params.")
|
||||
properties = params.get("properties") if isinstance(params.get("properties"), dict) else {}
|
||||
reference_schema = properties.get(reference_name)
|
||||
if not isinstance(reference_schema, dict) or reference_schema.get("type") not in {"array", "string"}:
|
||||
raise OperationContractError("Snapshot-bound reference slot is absent from author params schema")
|
||||
if reference_schema.get("type") == "string" and reference["max_items"] > 1:
|
||||
raise OperationContractError("Scalar snapshot-bound reference must allow at most one item")
|
||||
if not isinstance(contract["semantic_preflight"], list) or not all(isinstance(item, str) and item for item in contract["semantic_preflight"]):
|
||||
raise OperationContractError("Operation semantic preflight is invalid")
|
||||
if len(set(contract["semantic_preflight"])) != len(contract["semantic_preflight"]) or set(contract["semantic_preflight"]) - SEMANTIC_PREFLIGHT_NAMES:
|
||||
raise OperationContractError("Operation semantic preflight names are unknown or duplicated")
|
||||
if len(set(contract["semantic_preflight"])) != len(contract["semantic_preflight"]):
|
||||
raise OperationContractError("Operation semantic preflight names are duplicated")
|
||||
if not isinstance(contract["candidate_verifiers"], list) or not all(isinstance(item, str) and item for item in contract["candidate_verifiers"]):
|
||||
raise OperationContractError("Operation candidate_verifiers are invalid")
|
||||
if len(set(contract["candidate_verifiers"])) != len(contract["candidate_verifiers"]):
|
||||
raise OperationContractError("Operation candidate verifiers are duplicated")
|
||||
|
||||
|
||||
def fragment_schema(contract: dict[str, Any], *, selector_tokens: list[str], reference_tokens: list[str] | None = None, root_xy_datum: bool = False) -> dict[str, Any]:
|
||||
"""Build the one-operation schema exposed for one pending action."""
|
||||
validate_operation_contract(contract)
|
||||
shape = contract["fragment_shape"]
|
||||
params = deepcopy(contract["author_params_schema"])
|
||||
reference = contract["reference_policy"]
|
||||
if reference["mode"] == "snapshot_bound":
|
||||
slot = str(reference["slot"]).removeprefix("params.")
|
||||
items = params.get("properties", {}).get(slot, {}).get("items") if isinstance(params.get("properties"), dict) else None
|
||||
if not isinstance(items, dict):
|
||||
raise OperationContractError("Snapshot-bound reference slot is absent from author params schema")
|
||||
items.clear()
|
||||
items.update({"enum": reference_tokens or []})
|
||||
feature_properties: dict[str, Any] = {
|
||||
"atomic_id": {"const": contract["atomic_id"]},
|
||||
"params": params,
|
||||
runtime_capability = contract["runtime_capability"]
|
||||
required_capability_flags = {
|
||||
"body_mutating", "requires_active_body", "replayable", "requires_selector", "open_profile_ok",
|
||||
}
|
||||
feature_required = ["atomic_id", "params"]
|
||||
if shape["selector_tokens"] == "required":
|
||||
policy = contract["selector_policy"]
|
||||
feature_properties["selector_tokens"] = {
|
||||
"type": "array", "items": {"enum": selector_tokens}, "minItems": policy["min_items"],
|
||||
"maxItems": policy["max_items"], "uniqueItems": True,
|
||||
"description": (
|
||||
"Required author input. Copy the opaque selector token returned by the current topology "
|
||||
"snapshot here; do not omit it and do not put a host face in params. The server resolves this "
|
||||
"token into the host selector after schema validation."
|
||||
),
|
||||
}
|
||||
feature_required.append("selector_tokens")
|
||||
feature = {
|
||||
"type": "object",
|
||||
"description": (
|
||||
"One atomic feature. selector_tokens, when present, is an author-supplied topology token array "
|
||||
"rather than a server-filled params field."
|
||||
),
|
||||
"properties": feature_properties,
|
||||
"required": feature_required,
|
||||
"additionalProperties": False,
|
||||
}
|
||||
properties: dict[str, Any] = {"feature": feature}
|
||||
required = ["feature"]
|
||||
if shape["sketch"] == "required":
|
||||
properties["sketch"] = _sketch_schema(root_xy_datum=root_xy_datum and contract["atomic_id"] in {"extrude_add_blind", "extrude_add_two_sided"})
|
||||
required.insert(0, "sketch")
|
||||
schema = {"$schema": "https://json-schema.org/draft/2020-12/schema", "type": "object", "properties": properties, "required": required, "additionalProperties": False}
|
||||
Draft202012Validator.check_schema(schema)
|
||||
return schema
|
||||
|
||||
|
||||
def validate_fragment(contract: dict[str, Any], fragment: Any, *, selector_tokens: list[str], reference_tokens: list[str] | None = None, root_xy_datum: bool = False) -> list[dict[str, str]]:
|
||||
schema = fragment_schema(contract, selector_tokens=selector_tokens, reference_tokens=reference_tokens, root_xy_datum=root_xy_datum)
|
||||
return [
|
||||
{"path": "/" + "/".join(str(part) for part in error.absolute_path), "message": error.message}
|
||||
for error in sorted(Draft202012Validator(schema).iter_errors(fragment), key=lambda item: (list(item.absolute_path), item.message))
|
||||
]
|
||||
|
||||
|
||||
def _point(size: int) -> dict[str, Any]:
|
||||
return {"type": "array", "items": {"type": "number"}, "minItems": size, "maxItems": size}
|
||||
|
||||
|
||||
def _sketch_schema(*, root_xy_datum: bool = False) -> dict[str, Any]:
|
||||
point2 = _point(2)
|
||||
point3 = _point(3)
|
||||
workplane = {
|
||||
"type": "object",
|
||||
"description": "origin_mm is the world position of sketch local (0,0); profile coordinates are local to this plane. normal is positive extrusion direction and x_dir is local +X in world coordinates.",
|
||||
"properties": {"origin_mm": point3, "x_dir": deepcopy(point3), "normal": deepcopy(point3)},
|
||||
"required": ["origin_mm", "x_dir", "normal"],
|
||||
"additionalProperties": False,
|
||||
}
|
||||
if root_xy_datum:
|
||||
workplane["description"] += " Root extrusion uses fixed world XY datum: origin X/Y are 0, normal is +Z, x_dir is +X. Only origin Z is task-defined."
|
||||
workplane["properties"] = {
|
||||
"origin_mm": {"type": "array", "prefixItems": [{"const": 0}, {"const": 0}, {"type": "number"}], "items": False, "minItems": 3, "maxItems": 3},
|
||||
"x_dir": {"const": [1, 0, 0]},
|
||||
"normal": {"const": [0, 0, 1]},
|
||||
}
|
||||
profile = {
|
||||
"oneOf": [
|
||||
{"type": "object", "properties": {"type": {"const": "circle"}, "center": deepcopy(point2), "radius_mm": {"type": "number", "exclusiveMinimum": 0}}, "required": ["type", "radius_mm"], "additionalProperties": False},
|
||||
{"type": "object", "properties": {"type": {"const": "polygon"}, "vertices": {"type": "array", "minItems": 3, "items": deepcopy(point2)}}, "required": ["type", "vertices"], "additionalProperties": False},
|
||||
{"type": "object", "properties": {"type": {"const": "analytic_contours"}, "contours": {"type": "array", "minItems": 1, "maxItems": 8, "items": {"type": "object", "properties": {"role": {"enum": ["outer", "inner"]}, "closed": {"const": True}, "segments": {"type": "array", "minItems": 1, "items": {"oneOf": [{"type": "object", "properties": {"type": {"const": "line"}, "start": deepcopy(point2), "end": deepcopy(point2)}, "required": ["type", "start", "end"], "additionalProperties": False}, {"type": "object", "properties": {"type": {"const": "circle"}, "center": deepcopy(point2), "radius_mm": {"type": "number", "exclusiveMinimum": 0}}, "required": ["type", "center", "radius_mm"], "additionalProperties": False}, {"type": "object", "properties": {"type": {"const": "arc"}, "start": deepcopy(point2), "end": deepcopy(point2), "center": deepcopy(point2), "radius_mm": {"type": "number", "exclusiveMinimum": 0}, "clockwise": {"type": "boolean"}}, "required": ["type", "start", "end", "center", "radius_mm"], "additionalProperties": False}]}}}, "required": ["role", "closed", "segments"], "additionalProperties": False}}}, "required": ["type", "contours"], "additionalProperties": False},
|
||||
]
|
||||
}
|
||||
return {"type": "object", "properties": {"workplane": workplane, "profile": profile}, "required": ["workplane", "profile"], "additionalProperties": False}
|
||||
if (
|
||||
not isinstance(runtime_capability, dict)
|
||||
or set(runtime_capability) != required_capability_flags
|
||||
or not all(isinstance(value, bool) for value in runtime_capability.values())
|
||||
):
|
||||
raise OperationContractError("Operation runtime capability is invalid")
|
||||
|
||||
@@ -1,56 +1,24 @@
|
||||
"""Finite workflow state machine. This module has no persistence imports."""
|
||||
"""Finite state for the single-stage Authoring CDSL protocol."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass, replace
|
||||
from enum import StrEnum
|
||||
|
||||
from .errors import ErrorCode, WorkflowError
|
||||
from .errors import ErrorCode
|
||||
|
||||
|
||||
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"
|
||||
CANDIDATE_BUILDING = "CANDIDATE_BUILDING"
|
||||
CANDIDATE_REVIEW = "CANDIDATE_REVIEW"
|
||||
FINAL_VALIDATION = "FINAL_VALIDATION"
|
||||
WAITING_RETRY = "WAITING_RETRY"
|
||||
ANALYZING_REQUEST = "ANALYZING_REQUEST"
|
||||
AUTHORING_CDSL = "AUTHORING_CDSL"
|
||||
COMPILING_CDSL = "COMPILING_CDSL"
|
||||
BUILDING = "BUILDING"
|
||||
REPAIRING = "REPAIRING"
|
||||
PUBLISHING_BEST_EFFORT = "PUBLISHING_BEST_EFFORT"
|
||||
WAITING_FOR_USER = "WAITING_FOR_USER"
|
||||
CANCELLED = "CANCELLED"
|
||||
COMPLETED = "COMPLETED"
|
||||
FAILED = "FAILED"
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class PendingAction:
|
||||
action_id: str
|
||||
working_head: str
|
||||
intent: str
|
||||
requirement_ids: tuple[str, ...]
|
||||
atomic_id: str
|
||||
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
|
||||
CANCELLED = "CANCELLED"
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
@@ -59,167 +27,84 @@ class TaskState:
|
||||
phase: TaskPhase
|
||||
version: int
|
||||
active_revision: str = ""
|
||||
pending_action: PendingAction | None = None
|
||||
candidate_id: str = ""
|
||||
candidate_stage_id: str = ""
|
||||
repair_required: bool = False
|
||||
repair_count: int = 0
|
||||
last_error: ErrorCode | None = None
|
||||
retry_from_phase: TaskPhase | None = None
|
||||
requirements_spec_path: str = ""
|
||||
requirements_document_path: str = ""
|
||||
completion_target_path: str = ""
|
||||
modeling_plan_path: str = ""
|
||||
feature_plan_path: str = ""
|
||||
feature_plan_hash: str = ""
|
||||
feature_stage_id: str = ""
|
||||
requirements_path: str = ""
|
||||
authoring_path: str = ""
|
||||
runtime_cdsl_path: str = ""
|
||||
compile_audit_path: str = ""
|
||||
diagnostics_path: str = ""
|
||||
completion_path: 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}"
|
||||
|
||||
|
||||
# Legal state transitions. Events are intentionally terse persistence-neutral
|
||||
# names used by command handlers and architecture tests.
|
||||
_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.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
|
||||
# so its frozen request remains authoritative
|
||||
# instead of turning a clarification into a new CAD request.
|
||||
(TaskPhase.WAITING_FOR_USER, "requirements_clarified"): TaskPhase.DRAFTING_REQUIREMENTS_DOCUMENT,
|
||||
(TaskPhase.AWAITING_ACTION, "action_proposed"): TaskPhase.ACTION_PENDING,
|
||||
(TaskPhase.AWAITING_ACTION, "diagnosis_recorded"): TaskPhase.AWAITING_ACTION,
|
||||
(TaskPhase.AWAITING_ACTION, "rollback"): TaskPhase.AWAITING_ACTION,
|
||||
(TaskPhase.ACTION_PENDING, "candidate_started"): TaskPhase.CANDIDATE_BUILDING,
|
||||
# Runtime semantic preflight happens before candidate staging. It must
|
||||
# leave the checkpoint untouched and return control to the author for a
|
||||
# fresh observation/action decision, never masquerade as a format error.
|
||||
(TaskPhase.ACTION_PENDING, "runtime_precondition_rejected"): TaskPhase.AWAITING_ACTION,
|
||||
(TaskPhase.ACTION_PENDING, "diagnosis_recorded"): TaskPhase.ACTION_PENDING,
|
||||
(TaskPhase.ACTION_PENDING, "diagnosis_return_to_action_selection"): TaskPhase.AWAITING_ACTION,
|
||||
(TaskPhase.CANDIDATE_BUILDING, "candidate_built"): TaskPhase.CANDIDATE_REVIEW,
|
||||
(TaskPhase.CANDIDATE_BUILDING, "candidate_rejected"): TaskPhase.ACTION_PENDING,
|
||||
(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,
|
||||
(TaskPhase.ANALYZING_REQUEST, "analysis_written"): TaskPhase.AUTHORING_CDSL,
|
||||
(TaskPhase.ANALYZING_REQUEST, "waiting_for_user"): TaskPhase.WAITING_FOR_USER,
|
||||
(TaskPhase.WAITING_FOR_USER, "clarification_received"): TaskPhase.ANALYZING_REQUEST,
|
||||
(TaskPhase.AUTHORING_CDSL, "authoring_written"): TaskPhase.COMPILING_CDSL,
|
||||
(TaskPhase.AUTHORING_CDSL, "repair_required"): TaskPhase.REPAIRING,
|
||||
(TaskPhase.COMPILING_CDSL, "compiled"): TaskPhase.BUILDING,
|
||||
(TaskPhase.COMPILING_CDSL, "repair_required"): TaskPhase.REPAIRING,
|
||||
(TaskPhase.BUILDING, "build_completed"): TaskPhase.PUBLISHING_BEST_EFFORT,
|
||||
(TaskPhase.BUILDING, "repair_required"): TaskPhase.REPAIRING,
|
||||
(TaskPhase.REPAIRING, "repair_started"): TaskPhase.AUTHORING_CDSL,
|
||||
(TaskPhase.PUBLISHING_BEST_EFFORT, "published"): TaskPhase.COMPLETED,
|
||||
(TaskPhase.PUBLISHING_BEST_EFFORT, "failed"): TaskPhase.FAILED,
|
||||
}
|
||||
_TRANSITIONS.update({
|
||||
(phase, "best_effort_completed"): TaskPhase.COMPLETED
|
||||
for phase in TaskPhase
|
||||
if phase not in {TaskPhase.COMPLETED, TaskPhase.FAILED, TaskPhase.CANCELLED}
|
||||
})
|
||||
_TRANSITIONS.update({
|
||||
(phase, "failed"): TaskPhase.FAILED
|
||||
for phase in TaskPhase
|
||||
if phase not in {TaskPhase.COMPLETED, TaskPhase.FAILED, TaskPhase.CANCELLED}
|
||||
})
|
||||
_TRANSITIONS.update({
|
||||
(phase, "cancelled"): TaskPhase.CANCELLED
|
||||
for phase in TaskPhase
|
||||
if phase not in {TaskPhase.COMPLETED, TaskPhase.FAILED, TaskPhase.CANCELLED}
|
||||
})
|
||||
_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,
|
||||
TaskPhase.CANDIDATE_BUILDING,
|
||||
TaskPhase.CANDIDATE_REVIEW,
|
||||
TaskPhase.FINAL_VALIDATION,
|
||||
})
|
||||
_TRANSITIONS.update({
|
||||
(TaskPhase.WAITING_RETRY, f"resume_{phase.value.lower()}"): phase
|
||||
for phase in _RETRY_RESUMABLE_PHASES
|
||||
})
|
||||
_TRANSITIONS.update({
|
||||
(phase, "waiting_retry"): TaskPhase.WAITING_RETRY
|
||||
for phase in TaskPhase
|
||||
if phase not in {TaskPhase.COMPLETED, TaskPhase.FAILED, TaskPhase.CANCELLED, TaskPhase.WAITING_RETRY}
|
||||
})
|
||||
for _phase in TaskPhase:
|
||||
if _phase not in {TaskPhase.COMPLETED, TaskPhase.FAILED, TaskPhase.CANCELLED}:
|
||||
_TRANSITIONS[(_phase, "failed")] = TaskPhase.FAILED
|
||||
_TRANSITIONS[(_phase, "cancelled")] = TaskPhase.CANCELLED
|
||||
if _phase in {
|
||||
TaskPhase.AUTHORING_CDSL, TaskPhase.COMPILING_CDSL,
|
||||
TaskPhase.BUILDING, TaskPhase.REPAIRING,
|
||||
}:
|
||||
_TRANSITIONS[(_phase, "publish_best_effort")] = TaskPhase.PUBLISHING_BEST_EFFORT
|
||||
|
||||
|
||||
def legal_transitions() -> dict[tuple[TaskPhase, str], TaskPhase]:
|
||||
"""Return a copy of the protocol transition table for architecture tests."""
|
||||
return dict(_TRANSITIONS)
|
||||
|
||||
|
||||
def retry_resume_event(state: TaskState) -> str | None:
|
||||
"""Return the only lossless resume event for a parked service failure."""
|
||||
if state.phase != TaskPhase.WAITING_RETRY or state.retry_from_phase not in _RETRY_RESUMABLE_PHASES:
|
||||
return 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, 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))
|
||||
def transition(
|
||||
state: TaskState,
|
||||
event: str,
|
||||
*,
|
||||
active_revision: str | None = None,
|
||||
repair_count: int | None = None,
|
||||
error: ErrorCode | None = None,
|
||||
requirements_path: str | None = None,
|
||||
authoring_path: str | None = None,
|
||||
runtime_cdsl_path: str | None = None,
|
||||
compile_audit_path: str | None = None,
|
||||
diagnostics_path: str | None = None,
|
||||
completion_path: str | None = None,
|
||||
clarification_path: str | None = None,
|
||||
) -> TaskState:
|
||||
target = state.retry_from_phase if state.phase == TaskPhase.FAILED and event == "resume" else _TRANSITIONS.get((state.phase, event))
|
||||
if target is None:
|
||||
raise ValueError(f"Illegal v3 transition: {state.phase.value} --{event}--> ?")
|
||||
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.SCHEDULING_FEATURE, TaskPhase.REPLANNING_FEATURE_SUBGRAPH, TaskPhase.COMPLETED, TaskPhase.FAILED, TaskPhase.CANCELLED}:
|
||||
next_pending = None
|
||||
raise ValueError(f"Illegal single-stage transition: {state.phase.value} --{event}--> ?")
|
||||
next_repairs = state.repair_count if repair_count is None else repair_count
|
||||
if not 0 <= next_repairs <= 2:
|
||||
raise ValueError("Single-stage repair count must be within [0, 2]")
|
||||
return replace(
|
||||
state,
|
||||
phase=target,
|
||||
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.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,
|
||||
repair_count=next_repairs,
|
||||
last_error=error,
|
||||
retry_from_phase=state.phase if target == TaskPhase.WAITING_RETRY else None,
|
||||
requirements_spec_path=state.requirements_spec_path if requirements_spec_path is None else requirements_spec_path,
|
||||
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,
|
||||
retry_from_phase=state.phase if target == TaskPhase.FAILED and error in {
|
||||
ErrorCode.AUTHOR_TRANSPORT_UNAVAILABLE, ErrorCode.STORAGE_FAILURE,
|
||||
ErrorCode.RUNTIME_EXECUTION_FAILURE,
|
||||
} else None,
|
||||
requirements_path=state.requirements_path if requirements_path is None else requirements_path,
|
||||
authoring_path=state.authoring_path if authoring_path is None else authoring_path,
|
||||
runtime_cdsl_path=state.runtime_cdsl_path if runtime_cdsl_path is None else runtime_cdsl_path,
|
||||
compile_audit_path=state.compile_audit_path if compile_audit_path is None else compile_audit_path,
|
||||
diagnostics_path=state.diagnostics_path if diagnostics_path is None else diagnostics_path,
|
||||
completion_path=state.completion_path if completion_path is None else completion_path,
|
||||
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,
|
||||
)
|
||||
|
||||
|
||||
def reject_stale_head(state: TaskState, supplied_head: str) -> WorkflowError | None:
|
||||
if supplied_head != state.working_head:
|
||||
return WorkflowError(
|
||||
ErrorCode.STALE_WORKING_HEAD,
|
||||
"The command was bound to an obsolete working head.",
|
||||
details={"expected_working_head": state.working_head, "supplied_working_head": supplied_head},
|
||||
)
|
||||
return None
|
||||
|
||||
Reference in New Issue
Block a user