# CorpusFetch Method Slice (Zero-Prose Python Stdlib Reference)
# Canonical Residual: https://corpusfetch.com/c/which-invariants-prevent-silent-task-resurrection-across-a2a-v1-0-agent-card-re-resolution.md
# Remnant Warrant:    https://corpusfetch.com/c/which-invariants-prevent-silent-task-resurrection-across-a2a-v1-0-agent-card-re-resolution.warrant.json
# Markdown SHA-256:   a1b91661beba88bf79f399a108071f006c73ff89802049521cdcd3a6a3a34368
# Code AST SHA-256:   30b2c7737a0f1423a22a6dd34b4d0daa41c1b9a3c115199177935661b5d4f7ba
# Verified Classes:   TaskResurrectionError, CardSnapshot, TaskHandle
# Defensive Guards:   2 fail-closed invariants (zero external deps)

import hashlib
import json
from dataclasses import dataclass
from typing import Optional


class TaskResurrectionError(RuntimeError):
    """Raised when a task handle is used across an incompatible card revision
    or when the observed task state regresses illegally."""
    pass


LEGAL_EDGES = {
    "submitted": {"working", "canceled", "failed"},
    "working": {"completed", "failed", "canceled", "input-required"},
    "input-required": {"working", "canceled", "failed"},
    "completed": set(),
    "failed": set(),
    "canceled": set(),
}


@dataclass
class CardSnapshot:
    url: str
    capabilities: dict
    skill_ids: frozenset

    def revision_hash(self) -> str:
        payload = json.dumps(
            {
                "url": self.url,
                "capabilities": self.capabilities,
                "skill_ids": sorted(self.skill_ids),
            },
            sort_keys=True,
        ).encode()
        return hashlib.sha256(payload).hexdigest()


@dataclass
class TaskHandle:
    task_id: str
    card_revision: str
    last_state: str


def continue_task(
    handle: TaskHandle,
    observed_state: str,
    current_card: CardSnapshot,
) -> TaskHandle:
    current_rev = current_card.revision_hash()
    if current_rev != handle.card_revision:
        raise TaskResurrectionError(
            f"card revision changed for task {handle.task_id}: "
            f"{handle.card_revision[:12]} -> {current_rev[:12]}; "
            f"task is orphaned and must be canceled or abandoned"
        )
    if observed_state not in LEGAL_EDGES.get(handle.last_state, set()):
        raise TaskResurrectionError(
            f"illegal state regression for task {handle.task_id}: "
            f"{handle.last_state} -> {observed_state}; "
            f"suspected task-id reuse across revisions"
        )
    return TaskHandle(
        task_id=handle.task_id,
        card_revision=current_rev,
        last_state=observed_state,
    )
