Skip to content

graflo.architecture.evolution.structure

Structure-plane evolution ops: introducing and retargeting graph entities.

The contract vocabulary could remove, merge, rename and project, but not create. Everything here closes that half of the loop so a change set can describe a growing graph without falling back to a binary compose.

apply_add_edges(manifest, op)

Add new logical edge relations between existing vertex types.

Source code in graflo/architecture/evolution/structure.py
def apply_add_edges(manifest: GraphManifest, op: AddEdgesOp) -> None:
    """Add new logical edge relations between existing vertex types."""
    schema = manifest.graph_schema
    if schema is None:
        raise ValueError("add_edges requires graph_schema")

    edge_config = schema.core_schema.edge_config
    known_vertices = schema.core_schema.vertex_config.vertex_set
    existing_ids = {edge.edge_id for edge in edge_config.edges}

    collisions = sorted(
        str((edge.source, edge.target, edge.relation))
        for edge in op.edges
        if (edge.source, edge.target, edge.relation) in existing_ids
    )
    if collisions:
        raise ValueError(f"add_edges: edges already exist: {collisions}")

    unknown = sorted(
        {
            name
            for edge in op.edges
            for name in (edge.source, edge.target)
            if name not in known_vertices
        }
    )
    if unknown:
        raise ValueError(
            f"add_edges: unknown endpoint vertex types: {unknown}; add them with "
            "AddVerticesOp first"
        )

    edge_config.edges = [
        *edge_config.edges,
        *(edge.model_copy(deep=True) for edge in op.edges),
    ]
    schema.finish_init()

apply_add_vertices(manifest, op)

Add new logical vertex types to the schema.

Source code in graflo/architecture/evolution/structure.py
def apply_add_vertices(manifest: GraphManifest, op: AddVerticesOp) -> None:
    """Add new logical vertex types to the schema."""
    schema = manifest.graph_schema
    if schema is None:
        raise ValueError("add_vertices requires graph_schema")

    vertex_config = schema.core_schema.vertex_config
    existing = vertex_config.vertex_set
    collisions = sorted(
        vertex.name for vertex in op.vertices if vertex.name in existing
    )
    if collisions:
        raise ValueError(
            f"add_vertices: vertices already exist: {collisions}; use "
            "RenameVerticesOp or MergeVerticesOp instead"
        )

    vertex_config.vertices = [
        *vertex_config.vertices,
        *(vertex.model_copy(deep=True) for vertex in op.vertices),
    ]
    schema.finish_init()

apply_retarget_edges(manifest, op)

Repoint edges at different endpoint vertex types, keeping everything else.

Mutates manifest in place: rewrites the edge's source / target, moves the matching db_profile physical specs onto the new EdgeId, and repoints pipeline edge steps that named the old triple.

Source code in graflo/architecture/evolution/structure.py
def apply_retarget_edges(manifest: GraphManifest, op: RetargetEdgesOp) -> None:
    """Repoint edges at different endpoint vertex types, keeping everything else.

    Mutates *manifest* in place: rewrites the edge's ``source`` / ``target``, moves the
    matching ``db_profile`` physical specs onto the new ``EdgeId``, and repoints
    pipeline edge steps that named the old triple.
    """
    schema = manifest.graph_schema
    if schema is None:
        raise ValueError("retarget_edges requires graph_schema")

    edge_config = schema.core_schema.edge_config
    by_edge_id = {edge.edge_id: edge for edge in edge_config.edges}
    known_vertices = schema.core_schema.vertex_config.vertex_set

    unknown_edges = sorted(
        str(entry.edge_id()) for entry in op.edges if entry.edge_id() not in by_edge_id
    )
    if unknown_edges:
        raise ValueError(f"retarget_edges: unknown edges: {unknown_edges}")

    unknown_vertices = sorted(
        {
            name
            for entry in op.edges
            for name in (entry.new_source, entry.new_target)
            if name is not None and name not in known_vertices
        }
    )
    if unknown_vertices:
        raise ValueError(
            f"retarget_edges: unknown endpoint vertex types: {unknown_vertices}"
        )

    collisions = sorted(
        str(entry.retargeted_edge_id())
        for entry in op.edges
        if entry.retargeted_edge_id() in by_edge_id
    )
    if collisions:
        raise ValueError(
            f"retarget_edges: retargeted edges collide with existing ones: "
            f"{collisions}; use MergeEdgesOp to combine them"
        )

    mapping: dict[EdgeId, tuple[str, str]] = {}
    for entry in op.edges:
        edge = by_edge_id[entry.edge_id()]
        new_source, new_target, _ = entry.retargeted_edge_id()
        mapping[entry.edge_id()] = (new_source, new_target)

        for spec in schema.db_profile.edge_specs:
            if (spec.source, spec.target, spec.relation) == entry.edge_id():
                spec.source = new_source
                spec.target = new_target

        edge.source = new_source
        edge.target = new_target
        logger.debug(
            "retarget_edges: %s -> %s", entry.edge_id(), entry.retargeted_edge_id()
        )

    # ``EdgeConfig._edges_map`` is a private index keyed on EdgeId and rebuilt by a
    # model validator. Mutating an edge's endpoints in place leaves it keyed on the
    # pre-retarget id, which the Schema-level spec check then reads as a dangling
    # reference. Reassigning the list re-runs the validator and rebuilds the index.
    edge_config.edges = list(edge_config.edges)

    schema.db_profile = _revalidated_profile(schema)
    schema.finish_init()

    if manifest.ingestion_model is not None:
        from .apply import _rebuild_ingestion_with_pipeline_rewrite
        from .rewrite import rewrite_edge_endpoints_in_pipeline

        _rebuild_ingestion_with_pipeline_rewrite(
            manifest,
            lambda pipeline: rewrite_edge_endpoints_in_pipeline(pipeline, mapping),
        )