优化流程

This commit is contained in:
2026-09-02 13:51:35 +08:00
parent 97a03c290b
commit f3eb5dff54
71 changed files with 5290 additions and 515 deletions
+1
View File
@@ -29,6 +29,7 @@ class ErrorCode(StrEnum):
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"
@@ -131,7 +131,7 @@ def validate_operation_contract(contract: dict[str, Any]) -> None:
raise OperationContractError("Operation candidate verifiers are duplicated")
def fragment_schema(contract: dict[str, Any], *, selector_tokens: list[str], reference_tokens: list[str] | None = None) -> dict[str, Any]:
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"]
@@ -174,15 +174,15 @@ def fragment_schema(contract: dict[str, Any], *, selector_tokens: list[str], ref
properties: dict[str, Any] = {"feature": feature}
required = ["feature"]
if shape["sketch"] == "required":
properties["sketch"] = _sketch_schema()
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) -> list[dict[str, str]]:
schema = fragment_schema(contract, selector_tokens=selector_tokens, reference_tokens=reference_tokens)
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))
@@ -193,10 +193,23 @@ def _point(size: int) -> dict[str, Any]:
return {"type": "array", "items": {"type": "number"}, "minItems": size, "maxItems": size}
def _sketch_schema() -> dict[str, Any]:
def _sketch_schema(*, root_xy_datum: bool = False) -> dict[str, Any]:
point2 = _point(2)
point3 = _point(3)
workplane = {"type": "object", "properties": {"origin_mm": point3, "x_dir": deepcopy(point3), "normal": deepcopy(point3)}, "required": ["origin_mm", "x_dir", "normal"], "additionalProperties": False}
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},
+27 -7
View File
@@ -9,7 +9,10 @@ from .errors import ErrorCode, WorkflowError
class TaskPhase(StrEnum):
DRAFTING_REQUIREMENTS = "DRAFTING_REQUIREMENTS"
DRAFTING_REQUIREMENTS_DOCUMENT = "DRAFTING_REQUIREMENTS_DOCUMENT"
DRAFTING_COMPLETION_TARGET = "DRAFTING_COMPLETION_TARGET"
COMPILING_REQUIREMENTS = "COMPILING_REQUIREMENTS"
DRAFTING_MODELING_PLAN = "DRAFTING_MODELING_PLAN"
AWAITING_ACTION = "AWAITING_ACTION"
ACTION_PENDING = "ACTION_PENDING"
CANDIDATE_BUILDING = "CANDIDATE_BUILDING"
@@ -47,6 +50,9 @@ class TaskState:
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 = ""
clarification_path: str = ""
requirements_contract_path: str = ""
@@ -58,13 +64,16 @@ class TaskState:
# 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, "image_observed"): TaskPhase.DRAFTING_REQUIREMENTS,
(TaskPhase.DRAFTING_REQUIREMENTS, "requirements_approved"): TaskPhase.AWAITING_ACTION,
(TaskPhase.DRAFTING_REQUIREMENTS, "waiting_for_user"): TaskPhase.WAITING_FOR_USER,
(TaskPhase.DRAFTING_REQUIREMENTS_DOCUMENT, "image_observed"): TaskPhase.DRAFTING_REQUIREMENTS_DOCUMENT,
(TaskPhase.DRAFTING_REQUIREMENTS_DOCUMENT, "requirements_document_written"): TaskPhase.DRAFTING_COMPLETION_TARGET,
(TaskPhase.DRAFTING_COMPLETION_TARGET, "completion_target_written"): TaskPhase.COMPILING_REQUIREMENTS,
(TaskPhase.COMPILING_REQUIREMENTS, "requirements_compiled"): TaskPhase.DRAFTING_MODELING_PLAN,
(TaskPhase.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,
(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,
@@ -83,6 +92,11 @@ _TRANSITIONS: dict[tuple[TaskPhase, str], TaskPhase] = {
(TaskPhase.FINAL_VALIDATION, "final_accepted"): TaskPhase.COMPLETED,
(TaskPhase.FINAL_VALIDATION, "final_repair"): TaskPhase.AWAITING_ACTION,
}
_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
@@ -94,7 +108,10 @@ _TRANSITIONS.update({
if phase not in {TaskPhase.COMPLETED, TaskPhase.FAILED, TaskPhase.CANCELLED}
})
_RETRY_RESUMABLE_PHASES = frozenset({
TaskPhase.DRAFTING_REQUIREMENTS,
TaskPhase.DRAFTING_REQUIREMENTS_DOCUMENT,
TaskPhase.DRAFTING_COMPLETION_TARGET,
TaskPhase.COMPILING_REQUIREMENTS,
TaskPhase.DRAFTING_MODELING_PLAN,
TaskPhase.AWAITING_ACTION,
TaskPhase.ACTION_PENDING,
TaskPhase.CANDIDATE_BUILDING,
@@ -122,7 +139,7 @@ def retry_resume_event(state: TaskState) -> str | None:
return f"resume_{state.retry_from_phase.value.lower()}"
def transition(state: TaskState, event: str, *, pending_action: PendingAction | None | object = ..., active_revision: str | None = None, candidate_id: str | None = None, candidate_stage_id: str | None = None, repair_required: bool | None = None, error: ErrorCode | None = None, requirements_spec_path: str | None = None, clarification_path: str | None = None, requirements_contract_path: str | None = None) -> TaskState:
def transition(state: TaskState, event: str, *, pending_action: PendingAction | None | object = ..., active_revision: str | None = None, candidate_id: str | None = None, candidate_stage_id: str | None = None, repair_required: bool | None = None, error: ErrorCode | None = None, requirements_spec_path: str | None = None, requirements_document_path: str | None = None, completion_target_path: str | None = None, modeling_plan_path: str | None = None, clarification_path: str | None = None, requirements_contract_path: str | None = None) -> TaskState:
"""Apply one legal transition and advance optimistic-concurrency version."""
target = _TRANSITIONS.get((state.phase, event))
if target is None:
@@ -144,6 +161,9 @@ def transition(state: TaskState, event: str, *, pending_action: PendingAction |
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,
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,
)
@@ -3,6 +3,7 @@
from __future__ import annotations
from dataclasses import dataclass
from copy import deepcopy
from math import atan2, isclose, pi, sqrt
from typing import Any, Callable
@@ -12,6 +13,7 @@ from jsonschema.exceptions import SchemaError
ClaimResult = dict[str, Any]
ClaimEvaluator = Callable[[dict[str, Any], dict[str, Any]], ClaimResult]
_DEFAULT_TOLERANCE_MM = 0.1
def _closed_object(properties: dict[str, Any], required: list[str]) -> dict[str, Any]:
@@ -49,7 +51,13 @@ def _cylinder_axis_span(record: dict[str, Any]) -> float | None:
def _cylinder_axis_interval(record: dict[str, Any]) -> tuple[float, float] | None:
"""Return the inclusive axial interval of a cylindrical face's bounds."""
"""Return the inclusive axial interval of a cylindrical face's bounds.
B-rep face orientation is not a physical property of a cylindrical shell.
In particular, a two-sided extrusion can return its two half-walls with
opposite axis directions. Use one canonical direction for an undirected
cylinder axis so those halves share the same coordinate interval.
"""
geometry = record.get("geometry") if isinstance(record.get("geometry"), dict) else {}
bbox = geometry.get("bbox_mm")
direction = _cylinder_axis_direction(record)
@@ -60,6 +68,9 @@ def _cylinder_axis_interval(record: dict[str, Any]) -> tuple[float, float] | Non
maximum = tuple(float(value) for value in bbox[3:])
except (TypeError, ValueError):
return None
dominant_index = max(range(3), key=lambda index: abs(direction[index]))
if direction[dominant_index] < 0:
direction = tuple(-component for component in direction)
projections = [
sum(direction[index] * point[index] for index in range(3))
for point in (
@@ -418,7 +429,7 @@ def _outer_cylindrical_surface(expected: dict[str, Any], facts: dict[str, Any])
max(interval[1] for interval in shell_intervals if interval is not None)
- min(interval[0] for interval in shell_intervals if interval is not None)
)
tolerance = float(expected["tolerance_mm"])
tolerance = float(expected.get("tolerance_mm", _DEFAULT_TOLERANCE_MM))
evidence.update({"axial_spans_mm": actual_spans, "tolerance_mm": tolerance})
return _pass(evidence) if all(abs(span - float(axial_span)) <= tolerance for span in actual_spans) else _fail({"diameter_mm": expected["diameter_mm"], "expected_axial_span_mm": axial_span, "actual_axial_spans_mm": actual_spans, "tolerance_mm": tolerance})
@@ -790,11 +801,13 @@ class VerifierRegistry:
except KeyError as error:
raise ValueError(f"VERIFIER_UNAVAILABLE: {claim_kind}") from error
def expected_one_of_schema(self) -> dict[str, Any]:
def expected_one_of_schema(self, *, exclude_claim_kinds: set[str] | frozenset[str] | tuple[str, ...] = ()) -> dict[str, Any]:
excluded = set(exclude_claim_kinds)
return {
"oneOf": [
_closed_object({"claim_kind": {"const": item.claim_kind}, "expected": item.expected_schema}, ["claim_kind", "expected"])
for item in self._definitions.values()
if item.claim_kind not in excluded
]
}
@@ -806,6 +819,19 @@ class VerifierRegistry:
for error in sorted(validator.iter_errors(expected), key=lambda item: (list(item.absolute_path), item.message))
]
def normalize_expected(self, claim_kind: str, expected: dict[str, Any]) -> dict[str, Any]:
"""Apply protocol defaults before a compiled contract is frozen.
These defaults describe verifier mechanics, never user geometry. The
outer-cylinder verifier can match a diameter without a tolerance, but
measuring its optional axial span needs one. Persist the default so
the resulting contract is complete and independently reproducible.
"""
normalized = deepcopy(expected)
if claim_kind == "outer_cylindrical_surface" and "axial_span_mm" in normalized:
normalized.setdefault("tolerance_mm", _DEFAULT_TOLERANCE_MM)
return normalized
def evaluate(self, claim_kind: str, expected: dict[str, Any], facts: dict[str, Any]) -> ClaimResult:
errors = self.validate_expected(claim_kind, expected)
if errors: