"""Requirements drafting/review handlers; Markdown is rendered, never parsed.""" from __future__ import annotations from copy import deepcopy from hashlib import sha256 import json from typing import Any from app.cad_agent.application.llm_contracts import ( RequirementsDraftBatch, RequirementsPatchBatch, RequirementsReview, requirements_draft_schema, requirements_patch_schema, requirements_review_schema, ) from app.cad_agent.application.normalization import NormalizationError, resolve_normalization from app.cad_agent.application.results import Accepted, Rejected, Waiting from app.cad_agent.domain.claim_matching import contains_expected from app.cad_agent.domain.errors import ErrorCode, WorkflowError from app.cad_agent.domain.state import TaskPhase, TaskState, transition from app.cad_agent.domain.verifier_registry import VerifierRegistry from app.cad_agent.ports import ArtifactStore, TaskRepository class RequirementsCommandHandler: def __init__(self, repository: TaskRepository, artifacts: ArtifactStore, registry: VerifierRegistry) -> None: self.repository = repository self.artifacts = artifacts self.registry = registry # This intentionally has no production configuration path. The live # evaluator registers its fixture oracle per task before authoring. self._evaluation_contract_oracles: dict[str, list[dict[str, Any]]] = {} self._evaluation_capability_gaps: dict[str, list[dict[str, str]]] = {} def register_evaluation_contract_oracle( self, task_id: str, required_claims: list[dict[str, Any]], *, validation_capability_gaps: list[dict[str, Any]] | None = None, ) -> None: """Register an external contract oracle for one live-evaluation task. The source requirement remains evidence for normal production work; this guarded fixture data exists only to keep an evaluation from accepting a reviewer false positive before CAD execution starts. """ normalized: list[dict[str, Any]] = [] for item in required_claims: claim_kind = item.get("claim_kind") if isinstance(item, dict) else None expected = item.get("expected") if isinstance(item, dict) else None if ( not isinstance(claim_kind, str) or claim_kind not in self.registry.claim_kinds or not isinstance(expected, dict) ): raise ValueError("Evaluation contract oracle claims require claim_kind and expected object.") # The frozen contract is production-valid and complete. Fixture # expectations are intentionally partial so they can compare # provider-chosen optional fields such as tolerances. normalized.append({"claim_kind": claim_kind, "expected": deepcopy(expected)}) if not normalized: raise ValueError("Evaluation contract oracle requires at least one claim.") self._evaluation_contract_oracles[task_id] = normalized gaps: list[dict[str, str]] = [] for gap in validation_capability_gaps or (): gap_id = gap.get("id") if isinstance(gap, dict) else None description = gap.get("description") if isinstance(gap, dict) else None if not isinstance(gap_id, str) or not gap_id or not isinstance(description, str) or not description: raise ValueError("Evaluation capability gaps require id and description.") gaps.append({"id": gap_id, "description": description}) self._evaluation_capability_gaps[task_id] = gaps def evaluation_review_context(self, task_id: str) -> dict[str, Any] | None: """Expose fixture-only known verifier gaps to the live reviewer.""" gaps = self._evaluation_capability_gaps.get(task_id) if gaps is None: return None return {"evaluation_only": True, "known_validation_capability_gaps": deepcopy(gaps)} def draft_schema(self, task_id: str) -> dict[str, Any]: return requirements_draft_schema(self.registry.expected_one_of_schema(), list(self.artifacts.read_source_index(task_id))) def patch_schema(self, task_id: str) -> dict[str, Any]: state = self.repository.get_state(task_id) draft = self._draft(state) ids = [str(item.get("draft_id") or "") for item in draft.get("items") or () if isinstance(item, dict) and item.get("draft_id")] return requirements_patch_schema(self.registry.expected_one_of_schema(), list(self.artifacts.read_source_index(task_id)), ids) def review_schema(self, task_id: str) -> dict[str, Any]: state = self.repository.get_state(task_id) draft = self._draft(state) draft_ids = [str(item.get("draft_id") or "") for item in draft.get("items") or () if isinstance(item, dict) and item.get("draft_id")] source_ids = list(self.artifacts.read_source_index(task_id)) return requirements_review_schema(source_ids, draft_ids) def submit_draft(self, task_id: str, batch: RequirementsDraftBatch, *, invocation_id: str) -> Accepted | Rejected: replay = self._replay(task_id, invocation_id) if replay is not None: return replay state = self.repository.get_state(task_id) if state is None: return Rejected(WorkflowError(ErrorCode.AUTHOR_DECISION_REJECTED, "Task does not exist.")) if state.phase != TaskPhase.DRAFTING_REQUIREMENTS: return Rejected(WorkflowError(ErrorCode.AUTHOR_DECISION_REJECTED, "Requirements cannot be drafted in the current workflow phase.")) draft = self._draft(state) source_index = self.artifacts.read_source_index(task_id) current = [deepcopy(item) for item in draft.get("items") or () if isinstance(item, dict)] errors = self._validate_items(batch.items, source_index, current) if errors: return Rejected(WorkflowError(ErrorCode.AUTHOR_FORMAT_INVALID, "Requirements draft batch failed canonical validation.", field_errors=tuple(errors))) invocation = self.repository.begin_invocation( task_id, invocation_id, self._key(task_id, "requirements_draft", state.working_head, batch.model_dump(mode="json")), ) if invocation.status == "finished" and invocation.result is not None: return self._restore(invocation.result) start = len(current) + 1 for position, item in enumerate(batch.items, start): current.append({"draft_id": f"draft_{position:03d}", **item.model_dump(mode="json")}) payload = {"schema_version": "cad.requirements-draft.v1", "revision": int(draft.get("revision") or 0) + 1, "items": current} try: artifact_path = self.artifacts.write_requirements_draft(task_id, payload, invocation_id=invocation_id) except OSError as error: return self._park_for_storage_retry( state, event="requirements_draft_storage_failure", message=str(error), ) next_state = transition(state, "draft_updated", requirements_draft_path=artifact_path) result = Accepted({"draft_revision": payload["revision"], "draft_ids": [item["draft_id"] for item in current[-len(batch.items):]]}) if not self._commit(next_state, [{"event": "requirements_draft_updated", "invocation_id": invocation_id, "draft_revision": payload["revision"], "draft_path": artifact_path, "item_count": len(current)}], invocation, result): return Rejected(self._stale()) return result def patch_draft(self, task_id: str, batch: RequirementsPatchBatch, *, invocation_id: str) -> Accepted | Rejected: replay = self._replay(task_id, invocation_id) if replay is not None: return replay state = self.repository.get_state(task_id) if state is None or state.phase != TaskPhase.DRAFTING_REQUIREMENTS: return Rejected(WorkflowError(ErrorCode.AUTHOR_DECISION_REJECTED, "Requirements patches are not allowed in the current workflow phase.")) draft = self._draft(state) items = [deepcopy(item) for item in draft.get("items") or () if isinstance(item, dict)] by_id = {str(item.get("draft_id") or ""): item for item in items} source_index = self.artifacts.read_source_index(task_id) errors: list[dict[str, str]] = [] seen: set[str] = set() for index, patch in enumerate(batch.patches): if patch.target_draft_id in seen: errors.append({"path": f"/patches/{index}/target_draft_id", "message": "A draft item may be patched only once per batch."}) seen.add(patch.target_draft_id) if patch.target_draft_id not in by_id: errors.append({"path": f"/patches/{index}/target_draft_id", "message": "Unknown current draft ID."}) if patch.item: errors.extend(self._validate_items([patch.item], source_index, [item for item in items if item.get("draft_id") != patch.target_draft_id], prefix=f"/patches/{index}/item")) if errors: return Rejected(WorkflowError(ErrorCode.AUTHOR_FORMAT_INVALID, "Requirements patch batch failed canonical validation.", field_errors=tuple(errors))) invocation = self.repository.begin_invocation( task_id, invocation_id, self._key(task_id, "requirements_patch", state.working_head, batch.model_dump(mode="json")), ) if invocation.status == "finished" and invocation.result is not None: return self._restore(invocation.result) remaining: list[dict[str, Any]] = [] patches = {patch.target_draft_id: patch for patch in batch.patches} for item in items: patch = patches.get(str(item["draft_id"])) if patch is None: remaining.append(item) elif patch.op == "replace" and patch.item is not None: remaining.append({"draft_id": item["draft_id"], **patch.item.model_dump(mode="json")}) payload = {"schema_version": "cad.requirements-draft.v1", "revision": int(draft.get("revision") or 0) + 1, "items": remaining} try: artifact_path = self.artifacts.write_requirements_draft(task_id, payload, invocation_id=invocation_id) except OSError as error: return self._park_for_storage_retry( state, event="requirements_patch_storage_failure", message=str(error), ) # A successful patch creates a new draft revision. The prior review # describes the old revision and must not keep the author trapped in # patch-only mode; it will be replaced after explicit re-finalization. next_state = transition( state, "draft_updated", requirements_draft_path=artifact_path, requirements_review_path="", ) result = Accepted({"draft_revision": payload["revision"], "item_count": len(remaining)}) if not self._commit(next_state, [{"event": "requirements_draft_patched", "invocation_id": invocation_id, "draft_revision": payload["revision"], "draft_path": artifact_path, "item_count": len(remaining)}], invocation, result): return Rejected(self._stale()) return result def finalize_draft(self, task_id: str, *, invocation_id: str) -> Accepted | Rejected: replay = self._replay(task_id, invocation_id) if replay is not None: return replay state = self.repository.get_state(task_id) if state is None or state.phase != TaskPhase.DRAFTING_REQUIREMENTS: return Rejected(WorkflowError(ErrorCode.AUTHOR_DECISION_REJECTED, "Requirements cannot be finalized in the current workflow phase.")) draft = self._draft(state) items = [item for item in draft.get("items") or () if isinstance(item, dict)] source_index = self.artifacts.read_source_index(task_id) covered = {str(source) for item in items for source in item.get("source_ids") or ()} missing = sorted(set(source_index) - covered) if not items or missing: return Rejected(WorkflowError(ErrorCode.AUTHOR_DECISION_REJECTED, "REQUIREMENTS_COVERAGE_INCOMPLETE", details={"missing_source_ids": missing})) invocation = self.repository.begin_invocation(task_id, invocation_id, self._key(task_id, "requirements_finalize", state.working_head, {})) if invocation.status == "finished" and invocation.result is not None: return self._restore(invocation.result) next_state = transition(state, "requirements_finalized") result = Accepted({"phase": next_state.phase.value, "draft_revision": draft.get("revision", 0)}) if not self._commit(next_state, [{"event": "requirements_review_requested", "invocation_id": invocation_id, "draft_revision": draft.get("revision", 0)}], invocation, result): return Rejected(self._stale()) return result def record_review(self, task_id: str, review: RequirementsReview, *, invocation_id: str) -> Accepted | Rejected | Waiting: replay = self._replay(task_id, invocation_id) if replay is not None: return replay state = self.repository.get_state(task_id) if state is None or state.phase != TaskPhase.REVIEWING_REQUIREMENTS: return Rejected(WorkflowError(ErrorCode.AUTHOR_DECISION_REJECTED, "Requirements review is not expected in the current workflow phase.")) draft = self._draft(state) items = [item for item in draft.get("items") or () if isinstance(item, dict)] source_ids = set(self.artifacts.read_source_index(task_id)) draft_ids = {str(item.get("draft_id") or "") for item in items} errors = self._validate_review(review, source_ids, items, draft_ids) if errors: return Rejected(WorkflowError(ErrorCode.AUTHOR_FORMAT_INVALID, "Requirements review failed canonical validation.", field_errors=tuple(errors))) invocation = self.repository.begin_invocation( task_id, invocation_id, self._key(task_id, "requirements_review", state.working_head, review.model_dump(mode="json")), ) if invocation.status == "finished" and invocation.result is not None: return self._restore(invocation.result) coverage = self._derive_coverage(source_ids, items) normalizations: list[dict[str, Any]] = [] normalization_errors: list[dict[str, str]] = [] for index, finding in enumerate(review.findings): if finding.finding_type != "derivable_conflict" or finding.normalization is None: continue try: normalizations.append(resolve_normalization( task_id=task_id, draft_id=finding.draft_id, source_ids=list(finding.source_ids), description=finding.description, value=finding.normalization.model_dump(mode="json"), )) except NormalizationError as error: normalization_errors.append({"path": f"/findings/{index}/normalization", "message": str(error)}) if normalization_errors: return Rejected(WorkflowError( ErrorCode.AUTHOR_FORMAT_INVALID, "Requirements review supplied an invalid deterministic normalization.", field_errors=tuple(normalization_errors), )) warnings = [ finding.description for finding in review.findings if finding.finding_type == "verification_gap" ] author_findings = [ finding for finding in review.findings if finding.finding_type in {"missing_source_semantics", "claim_mismatch"} or ( finding.finding_type == "verification_gap" and not self._draft_has_visual_claim(items, finding.draft_id) ) ] ambiguous = [finding for finding in review.findings if finding.finding_type == "ambiguous_conflict"] decision = "waiting_for_user" if ambiguous else "revise" if author_findings else "freeze" review_payload = { "schema_version": "cad.requirements-review.v2", "findings": [finding.model_dump(mode="json") for finding in review.findings], "coverage": coverage, "decision": decision, "verification_warnings": warnings, "applied_normalizations": normalizations, } try: review_path = self.artifacts.write_requirements_review(task_id, review_payload, invocation_id=invocation_id) except OSError as error: return self._park_for_storage_retry( state, event="requirements_review_storage_failure", message=str(error), ) if ambiguous: next_state = transition(state, "waiting_for_user", error=ErrorCode.WAITING_FOR_USER, requirements_review_path=review_path) questions = [str(finding.question) for finding in ambiguous if finding.question] message = "Requirements contain an ambiguity that cannot be resolved deterministically." ambiguous_findings = [finding.model_dump(mode="json") for finding in ambiguous] result = Waiting(WorkflowError( ErrorCode.WAITING_FOR_USER, message, details={"questions": questions, "findings": ambiguous_findings}, )) if not self._commit(next_state, [{ "event": "requirements_waiting_for_user", "invocation_id": invocation_id, "review_path": review_path, "message": message, "questions": questions, "findings": ambiguous_findings, }], invocation, result): return Rejected(self._stale()) return result if author_findings: next_state = transition(state, "requirements_revise", requirements_review_path=review_path) result = Accepted({"phase": next_state.phase.value, "review": review_payload}) if not self._commit(next_state, [{"event": "requirements_review_revise", "invocation_id": invocation_id, "review_path": review_path}], invocation, result): return Rejected(self._stale()) return result if normalizations: normalization_digest = sha256( json.dumps(normalizations, ensure_ascii=True, sort_keys=True, separators=(",", ":")).encode("utf-8") ).hexdigest()[:12] try: normalization_path = self.artifacts.write_json_once( task_id, f"documents/requirements-normalizations-{normalization_digest}.json", { "schema_version": "cad.requirements-normalizations.v1", "task_id": task_id, "items": normalizations, }, ) except OSError as error: return self._park_for_storage_retry( state, event="requirements_normalization_storage_failure", message=str(error), ) else: normalization_path = "" contract = self._freeze_contract( task_id, items, verification_warnings=warnings, applied_normalizations=normalizations, ) missing_claims = self._missing_evaluation_oracle_claims(task_id, contract) if missing_claims: oracle_payload = self._evaluation_oracle_failure( task_id, draft_revision=int(draft.get("revision") or 0), reviewer_review_path=review_path, draft_ids=sorted(draft_ids), missing_claims=missing_claims, ) oracle_digest = sha256( json.dumps(oracle_payload, ensure_ascii=True, sort_keys=True, separators=(",", ":")).encode("utf-8") ).hexdigest()[:12] try: oracle_path = self.artifacts.write_json_once( task_id, f"documents/evaluation-contract-oracle-{oracle_digest}.json", oracle_payload, ) except OSError as error: return self._park_for_storage_retry( state, event="evaluation_contract_oracle_storage_failure", message=str(error), ) next_state = transition(state, "requirements_revise", requirements_review_path=oracle_path) result = Accepted({ "phase": next_state.phase.value, "review": oracle_payload, "reviewer_review_path": review_path, "evaluation_oracle": {"missing_claims": missing_claims}, }) if not self._commit(next_state, [{ "event": "requirements_evaluation_oracle_revise", "invocation_id": invocation_id, "review_path": review_path, "oracle_path": oracle_path, "missing_claims": missing_claims, }], invocation, result): return Rejected(self._stale()) return result try: contract_path = self.artifacts.write_requirements_contract(task_id, contract, invocation_id=invocation_id) except OSError as error: return self._park_for_storage_retry( state, event="requirements_contract_storage_failure", message=str(error), ) next_state = transition( state, "requirements_approved", requirements_review_path=review_path, requirements_contract_path=contract_path, ) result = Accepted({ "phase": next_state.phase.value, "contract_hash": contract["contract_hash"], "verification_warnings": warnings, "applied_normalizations": normalizations, }) events = [] if normalizations: events.append({ "event": "requirements_normalized", "normalization_path": normalization_path, "applied_normalizations": normalizations, }) events.append({"event": "requirements_contract_frozen", "invocation_id": invocation_id, "contract_hash": contract["contract_hash"], "contract_path": contract_path, "review_path": review_path, "requirement_count": len(contract["requirements"]), "verification_warnings": warnings, "applied_normalizations": normalizations}) if not self._commit(next_state, events, invocation, result): return Rejected(self._stale()) return result def ensure_rendered_contract_views(self, task_id: str, state: TaskState) -> None: """Rebuild read-only contract views from the committed immutable path. The SQLite transition is authoritative. Rendering convenience files after its commit must never reclassify a frozen contract as an internal authoring failure, so this idempotent operation is replayed by the workflow until every view exists. """ if not state.requirements_contract_path: return contract = self.artifacts.read_requirements_contract(task_id, state.requirements_contract_path) if not isinstance(contract, dict): raise RuntimeError("Committed requirements contract is unavailable") self.artifacts.write_requirements_contract(task_id, contract) self.artifacts.write_json_once(task_id, "requirements-index.json", { "schema_version": "cad.requirements-index.v1", "requirements": [ { "requirement_id": item["requirement_id"], "claim_ids": [claim["claim_id"] for claim in item["acceptance_claims"]], } for item in contract.get("requirements") or () if isinstance(item, dict) ], }) self.artifacts.write_text_once(task_id, "requirements.md", self._requirements_markdown(contract)) self.artifacts.write_text_once(task_id, "completion.md", self._completion_markdown(contract)) def _replay(self, task_id: str, invocation_id: str) -> Accepted | Waiting | None: invocation = self.repository.get_invocation(task_id, invocation_id) if invocation is None or invocation.status != "finished" or invocation.result is None: return None return self._restore(invocation.result) def _finish(self, invocation_id: str, result: Accepted | Waiting) -> None: if isinstance(result, Waiting): payload = {"result_type": "waiting", "error": result.error.payload()} else: payload = {"result_type": "accepted", "payload": result.payload} self.repository.finish_invocation(invocation_id, payload) def _commit(self, state: TaskState, events: list[dict[str, Any]], invocation: Any, result: Accepted | Waiting) -> bool: if isinstance(result, Waiting): payload = {"result_type": "waiting", "error": result.error.payload()} else: payload = {"result_type": "accepted", "payload": result.payload} return self.repository.compare_and_swap( state, events=events, invocation_id=invocation.invocation_id, invocation_result=payload, ) @staticmethod def _restore(payload: dict[str, Any]) -> Accepted | Waiting: if payload.get("result_type") == "waiting": error = payload.get("error") if isinstance(payload.get("error"), dict) else {} return Waiting(WorkflowError( ErrorCode(str(error.get("code") or ErrorCode.WAITING_FOR_USER.value)), str(error.get("message") or "Requirements need a user decision."), tuple(error.get("field_errors") or ()), bool(error.get("retryable")), dict(error.get("details") or {}), )) value = payload.get("payload") if isinstance(payload.get("payload"), dict) else payload return Accepted(value) def _draft(self, state: TaskState | None) -> dict[str, Any]: return self.artifacts.read_requirements_draft(state.task_id, state.requirements_draft_path) if state is not None else {"schema_version": "cad.requirements-draft.v1", "revision": 0, "items": []} @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() def _validate_items(self, inputs: list[Any], source_index: dict[str, str], existing: list[dict[str, Any]], *, prefix: str = "/items") -> list[dict[str, str]]: errors: list[dict[str, str]] = [] seen = {(tuple(item.get("source_ids") or ()), str(item.get("statement") or "").casefold()) for item in existing} for index, item in enumerate(inputs): path = f"{prefix}/{index}" if not set(item.source_ids).issubset(source_index): errors.append({"path": f"{path}/source_ids", "message": "source_ids must come from the current source index."}) signature = (tuple(item.source_ids), item.statement.casefold()) if signature in seen: errors.append({"path": f"{path}/statement", "message": "Duplicate requirement item."}) seen.add(signature) for claim_index, claim in enumerate(item.acceptance_claims): try: claim_errors = self.registry.validate_expected(claim.claim_kind, claim.expected) except ValueError: errors.append({"path": f"{path}/acceptance_claims/{claim_index}/claim_kind", "message": "VERIFIER_UNAVAILABLE"}) continue errors.extend({"path": f"{path}/acceptance_claims/{claim_index}/expected{error['path']}", "message": error["message"]} for error in claim_errors) return errors @staticmethod def _validate_review( review: RequirementsReview, source_ids: set[str], items: list[dict[str, Any]], draft_ids: set[str], ) -> list[dict[str, str]]: errors: list[dict[str, str]] = [] by_draft = {str(item.get("draft_id") or ""): item for item in items} for index, finding in enumerate(review.findings): if finding.draft_id not in draft_ids: errors.append({"path": f"/findings/{index}/draft_id", "message": "Finding must reference a current draft item."}) continue if not set(finding.source_ids).issubset(source_ids): errors.append({"path": f"/findings/{index}/source_ids", "message": "Finding source_ids must come from the current source index."}) continue cited = set(by_draft[finding.draft_id].get("source_ids") or ()) if not set(finding.source_ids).issubset(cited): errors.append({"path": f"/findings/{index}/source_ids", "message": "Finding source_ids must be cited by its draft item."}) return errors @staticmethod def _derive_coverage(source_ids: set[str], items: list[dict[str, Any]]) -> list[dict[str, Any]]: coverage = {source_id: [] for source_id in sorted(source_ids)} for position, item in enumerate(items, 1): requirement_id = f"req_{position:03d}" for source_id in item.get("source_ids") or (): if source_id in coverage: coverage[source_id].append(requirement_id) return [{"source_id": source_id, "requirement_ids": requirement_ids} for source_id, requirement_ids in coverage.items()] @staticmethod def _draft_has_visual_claim(items: list[dict[str, Any]], draft_id: str) -> bool: return any( claim.get("claim_kind") == "visual" for item in items if item.get("draft_id") == draft_id for claim in item.get("acceptance_claims") or () if isinstance(claim, dict) ) def _freeze_contract( self, task_id: str, items: list[dict[str, Any]], *, verification_warnings: list[str], applied_normalizations: list[dict[str, Any]], ) -> dict[str, Any]: requirements: list[dict[str, Any]] = [] claim_position = 1 for position, item in enumerate(items, 1): claims: list[dict[str, Any]] = [] for claim in item.get("acceptance_claims") or (): deterministic = self.registry.definition(str(claim["claim_kind"])).deterministic claims.append({"claim_id": f"claim_{claim_position:03d}", "claim_kind": claim["claim_kind"], "expected": claim["expected"], "verification_mode": "deterministic" if deterministic else "visual"}) claim_position += 1 requirements.append({"requirement_id": f"req_{position:03d}", "draft_id": item["draft_id"], "source_ids": item["source_ids"], "statement": item["statement"], "assumptions": item["assumptions"], "acceptance_claims": claims}) payload = { "schema_version": "cad.requirements-contract.v2", "task_id": task_id, "requirements": requirements, "verification_warnings": list(verification_warnings), "applied_normalizations": deepcopy(applied_normalizations), } payload["contract_hash"] = sha256(json.dumps(payload, ensure_ascii=True, sort_keys=True, separators=(",", ":")).encode("utf-8")).hexdigest() return payload def _missing_evaluation_oracle_claims(self, task_id: str, contract: dict[str, Any]) -> list[dict[str, Any]]: required = self._evaluation_contract_oracles.get(task_id) if not required: return [] actual = [ {"claim_kind": str(claim.get("claim_kind") or ""), "expected": claim.get("expected")} for requirement in contract.get("requirements") or () if isinstance(requirement, dict) for claim in requirement.get("acceptance_claims") or () if isinstance(claim, dict) and isinstance(claim.get("expected"), dict) ] return [ deepcopy(claim) for claim in required if not any( actual_claim["claim_kind"] == claim["claim_kind"] and contains_expected(actual_claim["expected"], claim["expected"]) for actual_claim in actual ) ] @staticmethod def _evaluation_oracle_failure( task_id: str, *, draft_revision: int, reviewer_review_path: str, draft_ids: list[str], missing_claims: list[dict[str, Any]], ) -> dict[str, Any]: return { "schema_version": "cad.requirements-evaluation-oracle.v1", "task_id": task_id, "evaluation_only": True, "decision": "revise", "draft_revision": draft_revision, "reviewer_decision": "freeze", "reviewer_review_path": reviewer_review_path, "findings": [ { "draft_id": draft_id, "source_ids": [], "finding_type": "missing_source_semantics", "description": "The external live-evaluation contract oracle found missing executable acceptance claims.", } for draft_id in draft_ids ], "missing_claims": missing_claims, } @staticmethod def _requirements_markdown(contract: dict[str, Any]) -> str: rows = ["# Requirements", ""] for item in contract["requirements"]: rows.append(f"- {item['requirement_id']}: {item['statement']}") for assumption in item["assumptions"]: rows.append(f" - Assumption: {assumption}") return "\n".join(rows) + "\n" @staticmethod def _completion_markdown(contract: dict[str, Any]) -> str: return "# Completion\n\n" + "\n".join(f"- [ ] {item['requirement_id']}: {item['statement']}" for item in contract["requirements"]) + "\n" @staticmethod def _stale() -> WorkflowError: return WorkflowError(ErrorCode.STALE_WORKING_HEAD, "Task state changed before this command could commit.") def _park_for_storage_retry(self, state: TaskState, *, event: str, message: str) -> Rejected: waiting = transition(state, "waiting_retry", error=ErrorCode.STORAGE_FAILURE) if not self.repository.compare_and_swap(waiting, events=[{ "event": event, "code": ErrorCode.STORAGE_FAILURE.value, "message": message[:1000], }]): return Rejected(self._stale()) return Rejected(WorkflowError( ErrorCode.STORAGE_FAILURE, "Requirements artifact storage is temporarily unavailable; the task can be resumed.", retryable=True, ))