Skip to content

symfonic.capabilities.memory.phases.phase_graph

phase_graph

What a phase is handed when it asks for the graph.

Its own module because it is a composition decision, not a QUICK-cadence one: every roster factory routes through it, and what it returns decides whether a phase's writes are deferred into the cycle's journal and whether they are fenced against a lost lease. Reading it should not mean reading four phase implementations first.

quick re-exports it, which is where every factory and test imports it from.

phase_graph

phase_graph(graph: Any) -> Any

The store the phases write through, from a store or a bare backend.

isinstance rather than duck-typing: both objects answer to query_nodes and their signatures differ (the store takes layer=, the backend takes a filter mapping), so a check that guessed from the shape would guess wrong exactly where it mattered.

The backend is wrapped in :class:~symfonic.capabilities.memory.journal. JournalledGraph and the store in :class:~symfonic.capabilities.memory. fencing.FencedGraph, which is why every factory routes through this one function. Ten of the roster's phase modules write to the graph directly rather than through the write coordinator, so anything applied phase by phase would be a rule each of them -- and each one written later -- has to remember. Applied here it is structural: a phase gets both by being handed its graph.

Both are inert outside a cycle. The journal defers mutations only while one is running on this task and the fence checks only while a lease is held, so single-process use, an ordinary turn and every unit test behave exactly as before.

A deployment that shares one backend between the phases and its memory layers should wrap it once at the composition root instead -- see :class:~symfonic.capabilities.memory.journal.JournalledGraph. Wrapping here covers what the phases reach; it cannot cover what ProceduralLayer writes through a store this function never sees.

Source code in src/symfonic/capabilities/memory/phases/phase_graph.py
def phase_graph(graph: Any) -> Any:
    """The store the phases write through, from a store or a bare backend.

    ``isinstance`` rather than duck-typing: both objects answer to
    ``query_nodes`` and their signatures differ (the store takes ``layer=``,
    the backend takes a filter mapping), so a check that guessed from the shape
    would guess wrong exactly where it mattered.

    The backend is wrapped in :class:`~symfonic.capabilities.memory.journal.\
    JournalledGraph` and the store in :class:`~symfonic.capabilities.memory.\
    fencing.FencedGraph`, which is why every factory routes through this one
    function. Ten of the roster's phase modules write to the graph directly
    rather than through the write coordinator, so anything applied phase by
    phase would be a rule each of them -- and each one written later -- has to
    remember. Applied here it is structural: a phase gets both by being handed
    its graph.

    Both are inert outside a cycle. The journal defers mutations only while one
    is running on this task and the fence checks only while a lease is held, so
    single-process use, an ordinary turn and every unit test behave exactly as
    before.

    A deployment that shares one backend between the phases and its memory
    layers should wrap it once at the composition root instead -- see
    :class:`~symfonic.capabilities.memory.journal.JournalledGraph`. Wrapping
    here covers what the phases reach; it cannot cover what
    ``ProceduralLayer`` writes through a store this function never sees.
    """
    if isinstance(graph, FencedGraph):
        # Idempotent, because the deep and nightly factories resolve the graph
        # once and hand the result to the factories they compose.
        return graph
    if isinstance(graph, GraphMemoryStore):
        return FencedGraph(graph)
    if isinstance(graph, JournalledGraph):
        # Already journalled at the composition root, where it also covers
        # the memory layers. Wrapping again would nest one journal inside
        # another.
        return FencedGraph(GraphMemoryStore(graph))
    return FencedGraph(GraphMemoryStore(JournalledGraph(graph)))