Skip to content

symfonic.agent.cutover.criteria

criteria

Cutover criteria: the evidence a switch needs before it may be flipped.

Every capability here is non-shadowable in the T2.3.7 sense — the legacy engine's paths reach adopter tools, adopter plugins, and contributed prompt sections, none of which can be replayed under effect suppression. So the evidence road is the non-shadow one, and its three criteria are the ones symfonic.services.shadow.cutover already enforces: a parity suite, a characterization record, and a live cutover.

The ledgers below are not claims; they are citations. Each entry points at a file in the build's own evidence tree, and a capability whose live cutover has not happened yet carries None there rather than an optimistic string — which is exactly why invocation.stream stayed on legacy for as long as it did. It no longer does: TA8.2 filed the parity report and the call site, so invocation.stream joined invocation.run on the kernel. TA8.30 filed the third, invocation.stream_typed, citing ST2's parity rather than re-deriving it -- so all three switches in :data:DEFAULT_CRITERIA are complete and all three route to the kernel.

Say precisely what that means, because the word "flipped" collects meanings it has not earned. It means the route is derived from filed evidence and comes out KERNEL. It does not mean the legacy body is unreachable — dispatch still falls back out of the migrated envelope, and on a release line that predates the text-delta chunk contract stream() takes legacy on purpose (see invocation.stream's entry below). Unreachability is a separate measurement and RET-6's, not this ledger's.

There are two ledgers, and the split is the same one routes.py makes:

  • :data:DEFAULT_CRITERIA is what gates a flip, so it holds exactly the names dispatch reads. A record here is an argument the switchboard accepts.
  • :data:COVERAGE_EVIDENCE is what was built and verified for the nine capabilities dispatch does not route through. Same citations, no flip to gate, and a separate type so the distinction cannot be lost by assignment.

One ledger held all eleven, and the switchboard's constructor refuses a register by name -- so CutoverSwitchboard(criteria=DEFAULT_CRITERIA), the most obvious thing a caller can write, raised on models. The exported default has to be constructible with, or it is not a default.

CoverageEvidence dataclass

CoverageEvidence(kind: CoverageKind, parity_suite: str | None = None, characterization: str | None = None, live_cutover: str | None = None, dispatch_proof: str | None = None, opaque_dependencies: tuple[str, ...] = ())

What was built and verified for a capability nothing routes through.

Deliberately without complete. On a switch, complete is the predicate that flips it; a register has no flip, so the same word here would answer a question nobody can act on -- and that reading is how "nine of eleven complete" came to be heard as "nine of eleven serving turns".

What it answers instead is :attr:verified: the evidence this kind of register owes has been filed. A true claim, and a different one.

citations property

citations: tuple[str, ...]

Every citation actually filed, for a checker to resolve.

required property

required: tuple[str, ...]

The citations this kind of register owes.

unfiled property

unfiled: tuple[str, ...]

The owed citations still absent.

verified property

verified: bool

Every owed citation filed. Says nothing about what serves a turn.

CoverageKind

Bases: StrEnum

How a migrated capability reaches real turns — and so what proves it.

One requirement list for all nine registers was wrong in a way that matters to the instrument: it demanded a live_cutover citation from every one, and only three can have one. memory and prompting are composed and dispatched on every hydrating turn since #14 and #21 — through the envelope, on the evidence a bundle carries — and reported verified = False because no switch flipped to make that happen. The ledger built to stop understating progress was understating exactly the two capabilities that had travelled furthest.

So the requirement is a function of the kind:

  • :attr:CALL_SITE — the migrated implementation is the only one left on the engine's call path. Proof is that call site, in src/.
  • :attr:ENVELOPE — composed and dispatched through admission, on bundle evidence rather than a route. Proof is the integration test that shows the segment reaching the model; there is no call site to name because the legacy body is still reachable when the envelope refuses.
  • :attr:BUILT — implemented and verified, and nothing dispatches it. Parity and characterization are the whole claim, and asking for more would file an optimistic citation to satisfy a field.

SwitchCriteria dataclass

SwitchCriteria(parity_suite: str | None = None, characterization: str | None = None, live_cutover: str | None = None, opaque_dependencies: tuple[str, ...] = (), preconditions: tuple[str, ...] = ())

One capability's three non-shadow criteria, each a citation or None.

missing property

missing: tuple[str, ...]

The criteria still absent, in the order the recorder names them.

as_criteria

as_criteria() -> dict[str, Any]

The mapping CutoverCriteriaRecorder.record_non_shadow expects.

Source code in src/symfonic/agent/cutover/criteria.py
def as_criteria(self) -> dict[str, Any]:
    """The mapping ``CutoverCriteriaRecorder.record_non_shadow`` expects."""
    return {name: getattr(self, name) for name in NON_SHADOW_CRITERIA}

default_criteria

default_criteria() -> dict[str, SwitchCriteria]

A mutable copy of the flip ledger, one per switchboard instance.

Source code in src/symfonic/agent/cutover/criteria.py
def default_criteria() -> dict[str, SwitchCriteria]:
    """A mutable copy of the flip ledger, one per switchboard instance."""
    return {
        capability: DEFAULT_CRITERIA.get(capability, SwitchCriteria())
        for capability in CAPABILITY_SWITCHES
    }