# CorpusFetch Method Slice (Zero-Prose Python Stdlib Reference)
# Canonical Residual: https://corpusfetch.com/c/where-must-an-instance-draw-the-line-between-speculative-and-committed-state-in-multi-agent-orchestration.md
# Remnant Warrant:    https://corpusfetch.com/c/where-must-an-instance-draw-the-line-between-speculative-and-committed-state-in-multi-agent-orchestration.warrant.json
# Markdown SHA-256:   f02c6a5d2014c6cc7dbbb53ff55dccd1c8f3af855a215ac0a8ead913e2d18c25
# Code AST SHA-256:   405f9f860cf327bc1c8b6661db5b94737451625bc30969fc0c5deb835440b3a1
# Verified Classes:   SpeculationScopeViolation
# Defensive Guards:   1 fail-closed invariants (zero external deps)

class SpeculationScopeViolation(RuntimeError):
    """Raised when a speculative branch would write to a channel observable
    by an agent outside its speculation scope before commit/discard."""
    pass


def assert_speculation_write_isolated(branch, scope, observable_channels):
    """
    branch:              identifier of the speculative branch
    scope:               set of agent ids participating in this speculation
    observable_channels: mapping channel -> set of reader agent ids that
                         may read it before the branch terminates
    """
    for channel in branch.write_channels:
        readers = observable_channels.get(channel, set())
        outsiders = readers - scope
        if outsiders:
            raise SpeculationScopeViolation(
                f"speculative branch {branch.id!r} writes channel {channel!r} "
                f"observable by out-of-scope agents {sorted(outsiders)!r}; "
                f"quarantine the channel or demote branch to non-speculative"
            )


def dispatch_speculative(branch, scope, observable_channels, run):
    # Gate BEFORE any execution: observability, not commitment, is the event.
    assert_speculation_write_isolated(branch, scope, observable_channels)
    return run(branch)
