Skip to content

symfonic.services.shadow.classification

classification

The effect-port classification table and its exhaustiveness check.

This module is the registry T2.3.7's shadow harness is governed by. It answers exactly two questions — "is this port classified?" and "what does shadow mode do with it?" — and refuses to answer either by guessing.

EffectPortClassification dataclass

EffectPortClassification(ports: tuple[EffectPort, ...] = ())

An immutable table of port_id -> disposition.

Immutable because a classification that could be widened at runtime would let a shadow run mint its own permission halfway through, which is exactly what the fail-closed rule is protecting against.

assert_classifies

assert_classifies(port_ids: Iterable[str]) -> None

Every named port has a row. Complements :meth:assert_exhaustive.

Family coverage proves the taxonomy is complete; it says nothing about whether the ports the framework actually declares are in the table. A port the runtime crosses but the table has never heard of is not "fail-closed" — it is simply never observed, because nothing routes it through the gateway. This is the check that names them.

Source code in src/symfonic/services/shadow/classification.py
def assert_classifies(self, port_ids: Iterable[str]) -> None:
    """Every named port has a row. Complements :meth:`assert_exhaustive`.

    Family coverage proves the *taxonomy* is complete; it says nothing
    about whether the ports the framework actually declares are in the
    table. A port the runtime crosses but the table has never heard of is
    not "fail-closed" — it is simply never observed, because nothing routes
    it through the gateway. This is the check that names them.
    """
    unknown = sorted({port_id for port_id in port_ids if not self.knows(port_id)})
    if unknown:
        raise IncompleteClassificationError(
            "declared framework effect ports with no classification row: "
            + ", ".join(unknown)
            + "; shadow evidence is withheld until each is classified"
        )

assert_exhaustive

assert_exhaustive() -> None

Every declared family has at least one classified port.

Source code in src/symfonic/services/shadow/classification.py
def assert_exhaustive(self) -> None:
    """Every declared family has at least one classified port."""
    missing = sorted(
        family.value for family in EffectFamily if family not in self.families_covered()
    )
    if missing:
        raise IncompleteClassificationError(
            "effect families with no classified port: "
            + ", ".join(missing)
            + "; shadow evidence is withheld until every declared family is "
            "classified"
        )

classify

classify(port_id: str) -> EffectPort

The classification row, or a fail-closed refusal. Never a default.

Source code in src/symfonic/services/shadow/classification.py
def classify(self, port_id: str) -> EffectPort:
    """The classification row, or a fail-closed refusal. Never a default."""
    try:
        return self._by_id[port_id]
    except KeyError:
        raise UnclassifiedEffectError(
            f"port {port_id!r} is not in the effect-port classification; a "
            "shadow run cannot suppress an effect it has never been told "
            "about, so the run is aborted rather than trusted"
        ) from None

extended_with

extended_with(ports: Iterable[EffectPort]) -> EffectPortClassification

A new table with extra rows. Never mutates the receiver.

Source code in src/symfonic/services/shadow/classification.py
def extended_with(self, ports: Iterable[EffectPort]) -> EffectPortClassification:
    """A new table with extra rows. Never mutates the receiver."""
    return EffectPortClassification(ports=(*self.ports, *tuple(ports)))