Skip to content

graflo.architecture.evolution.aspects

An aspect adds the types and relations every domain needs and none owns -- where a fact came from, how a fact changes over time -- grounded in a standard vocabulary, as a reviewable op list over the manifest. Each aspect plans on its own; :func:plan_lift composes the ones a caller names.

Four ship today:

  • provenance: SourceRecord and Agent with wasDerivedFrom and wasAttributedTo (PROV-O). Needs no spec.
  • state: <Type>State with a validity interval and <Type>Observation with a unit per row (PROV-O, SOSA, QUDT). Needs a :class:~graflo.architecture.evolution.aspects.spec.LiftSpec, because a planner can see structure but not meaning.
  • access: a reserved access_zones label on the types a spec names, each with a merge mode. Needs a spec; not lifted by default.
  • sync: a reserved sync_hash on every type a resource's sync block owns, written by bulk and sync runs. Needs no spec; not lifted by default.

The output is an op list, so the conversion is replayable, invertible and reviewable through the machinery every other manifest change already uses. A :class:~graflo.architecture.evolution.aspects.recipe.LiftRecipe records what was asked of a lift, so that it can be planned again (:func:plan_relift).

Modules:

Name Description
access

The access aspect: integer zone labels on declared types.

base

What every aspect shares: the refusal, and the entry that names it.

iris

The IRIs the aspects ground their types in.

join

A lift is a join: the manifest gains what an aspect's vocabulary has and it lacks.

provenance

The provenance aspect: where a fact came from, and who produced it (PROV-O).

recipe

What a lift was asked to do, recorded so that it can be done again.

spec

What a caller must declare for a lift, and what the lift works out itself.

state

The state aspect: how facts change over time, and what was measured (PROV-O, SOSA, QUDT).

sync

The sync aspect: a record's sync hash on the vertex types it owns.

Attributes

ACCESS = 'access' module-attribute

ASPECTS = {PROVENANCE: Aspect(name=PROVENANCE, description='SourceRecord and Agent with wasDerivedFrom and wasAttributedTo: where a fact came from and who produced it (PROV-O).', needs_spec=False, version=1), STATE: Aspect(name=STATE, description='<Type>State with a validity interval and <Type>Observation with a unit per row: how facts change and what was measured (PROV-O, SOSA, QUDT).', needs_spec=True, version=1), ACCESS: Aspect(name=ACCESS, description='A reserved access_zones label on declared types: the zone ids a writer stamps and a read guard checks.', needs_spec=True, version=1), SYNC: Aspect(name=SYNC, description="A reserved sync_hash on the types a resource's sync block owns: what lets a sync run skip an unchanged record.", needs_spec=False, version=1)} module-attribute

DEFAULT_ASPECTS = (PROVENANCE, STATE) module-attribute

PROVENANCE = 'provenance' module-attribute

STATE = 'state' module-attribute

SYNC = 'sync' module-attribute

__all__ = ['ACCESS', 'ASPECTS', 'DEFAULT_ASPECTS', 'PROVENANCE', 'STATE', 'SYNC', 'Aspect', 'EdgeGrounding', 'Grounding', 'LiftError', 'LiftRecipe', 'LiftSpec', 'StatePlan', 'build_lift_recipe', 'plan_access', 'plan_lift', 'plan_provenance', 'plan_relift', 'plan_state', 'plan_sync', 'recipe_drift', 'recorded_spec', 'resolve_aspects'] module-attribute

Classes

Aspect

Bases: ConfigBaseModel

One cross-cutting vocabulary a manifest can be lifted with.

An aspect is domain-free: it adds the types and relations that let a schema say something every domain needs to say -- where a fact came from, how a fact changes over time -- grounded in a standard vocabulary.

Attributes:

Name Type Description
name str

How the CLI and a caller address it (--aspect NAME).

description str

One line, for listings.

needs_spec bool

Whether planning it needs declarations only a :class:~graflo.architecture.evolution.aspects.spec.LiftSpec carries.

version int

Bumped when the vocabulary, or what the planner emits for a given spec, changes. A recorded lift names it, so a re-lift can tell which aspects moved since.

Source code in graflo/architecture/evolution/aspects/base.py
class Aspect(ConfigBaseModel):
    """One cross-cutting vocabulary a manifest can be lifted with.

    An aspect is domain-free: it adds the types and relations that let a
    schema say something every domain needs to say -- where a fact came from,
    how a fact changes over time -- grounded in a standard vocabulary.

    Attributes:
        name: How the CLI and a caller address it (``--aspect NAME``).
        description: One line, for listings.
        needs_spec: Whether planning it needs declarations only a
            :class:`~graflo.architecture.evolution.aspects.spec.LiftSpec` carries.
        version: Bumped when the vocabulary, or what the planner emits for a
            given spec, changes. A recorded lift names it, so a re-lift can
            tell which aspects moved since.
    """

    name: str = PydanticField(..., min_length=1)
    description: str
    needs_spec: bool
    version: int = PydanticField(..., ge=1)

Attributes

description instance-attribute
name = PydanticField(..., min_length=1) class-attribute instance-attribute
needs_spec instance-attribute
version = PydanticField(..., ge=1) class-attribute instance-attribute

EdgeGrounding

Bases: Grounding

A grounding addressed at one edge triple.

Source code in graflo/architecture/evolution/aspects/spec.py
class EdgeGrounding(Grounding):
    """A grounding addressed at one edge triple."""

    source: str = PydanticField(..., description="Source vertex type name.")
    target: str = PydanticField(..., description="Target vertex type name.")
    relation: str | None = PydanticField(default=None, description="Relation name.")

Attributes

relation = PydanticField(default=None, description='Relation name.') class-attribute instance-attribute
source = PydanticField(..., description='Source vertex type name.') class-attribute instance-attribute
target = PydanticField(..., description='Target vertex type name.') class-attribute instance-attribute

Grounding

Bases: ConfigBaseModel

An external-vocabulary anchor a caller asserts for one element.

Source code in graflo/architecture/evolution/aspects/spec.py
class Grounding(ConfigBaseModel):
    """An external-vocabulary anchor a caller asserts for one element."""

    iri: str | None = PydanticField(
        default=None, description="IRI of the concept this element denotes."
    )
    exact_match: list[str] = PydanticField(
        default_factory=list, description="IRIs asserted equivalent to it."
    )
    synonyms: list[str] = PydanticField(
        default_factory=list, description="Alternative labels an agent may meet."
    )

    @model_validator(mode="after")
    def _validate_says_something(self) -> Grounding:
        if not self.iri and not self.exact_match and not self.synonyms:
            raise ValueError(
                "a grounding must carry at least one of iri, exact_match, synonyms"
            )
        return self

Attributes

exact_match = PydanticField(default_factory=list, description='IRIs asserted equivalent to it.') class-attribute instance-attribute
iri = PydanticField(default=None, description='IRI of the concept this element denotes.') class-attribute instance-attribute
synonyms = PydanticField(default_factory=list, description='Alternative labels an agent may meet.') class-attribute instance-attribute

LiftError

Bases: ValueError

The lift cannot be planned, and the message says which declaration is missing.

Raised rather than guessed. Every case here is one where the planner can see that something is wrong but not what the right answer is -- an ungrounded type, a name it would have to overwrite, a property it was told to move that is part of the key.

Source code in graflo/architecture/evolution/aspects/base.py
class LiftError(ValueError):
    """The lift cannot be planned, and the message says which declaration is missing.

    Raised rather than guessed. Every case here is one where the planner can see
    that something is wrong but not what the right answer is -- an ungrounded
    type, a name it would have to overwrite, a property it was told to move that
    is part of the key.
    """

LiftRecipe

Bases: ConfigBaseModel

The aspects a manifest was lifted with, their versions and the spec.

Source code in graflo/architecture/evolution/aspects/recipe.py
class LiftRecipe(ConfigBaseModel):
    """The aspects a manifest was lifted with, their versions and the spec."""

    kind: Literal["lift"] = "lift"
    base: str = PydanticField(
        ..., description="Content hash of the manifest the lift was planned on."
    )
    aspects: list[str] = PydanticField(
        ..., min_length=1, description="The aspects lifted, in the order named."
    )
    versions: dict[str, int] = PydanticField(
        ..., description="The version of each aspect at the time of the lift."
    )
    spec: dict[str, Any] = PydanticField(
        default_factory=dict,
        description="The lift spec, without its defaults (see `recorded_spec`).",
    )
    graflo_version: str | None = PydanticField(
        default=None, description="The graflo that planned the lift, when known."
    )

    def content_hash(self) -> str:
        """Content address of what the lift declared.

        ``graflo_version`` is left out: it says which tool planned the lift,
        not what was asked of it, and the aspect versions already say whether
        the vocabulary differs.
        """
        return suthing.stable_hash(
            {
                "kind": self.kind,
                "base": self.base,
                "aspects": list(self.aspects),
                "versions": dict(self.versions),
                "spec": self.spec,
            }
        )

    def lift_spec(self) -> LiftSpec:
        """The recorded spec as a :class:`LiftSpec`."""
        return LiftSpec.model_validate(self.spec)

Attributes

aspects = PydanticField(..., min_length=1, description='The aspects lifted, in the order named.') class-attribute instance-attribute
base = PydanticField(..., description='Content hash of the manifest the lift was planned on.') class-attribute instance-attribute
graflo_version = PydanticField(default=None, description='The graflo that planned the lift, when known.') class-attribute instance-attribute
kind = 'lift' class-attribute instance-attribute
spec = PydanticField(default_factory=dict, description='The lift spec, without its defaults (see `recorded_spec`).') class-attribute instance-attribute
versions = PydanticField(..., description='The version of each aspect at the time of the lift.') class-attribute instance-attribute

Methods:

content_hash()

Content address of what the lift declared.

graflo_version is left out: it says which tool planned the lift, not what was asked of it, and the aspect versions already say whether the vocabulary differs.

Source code in graflo/architecture/evolution/aspects/recipe.py
def content_hash(self) -> str:
    """Content address of what the lift declared.

    ``graflo_version`` is left out: it says which tool planned the lift,
    not what was asked of it, and the aspect versions already say whether
    the vocabulary differs.
    """
    return suthing.stable_hash(
        {
            "kind": self.kind,
            "base": self.base,
            "aspects": list(self.aspects),
            "versions": dict(self.versions),
            "spec": self.spec,
        }
    )
lift_spec()

The recorded spec as a :class:LiftSpec.

Source code in graflo/architecture/evolution/aspects/recipe.py
def lift_spec(self) -> LiftSpec:
    """The recorded spec as a :class:`LiftSpec`."""
    return LiftSpec.model_validate(self.spec)

LiftSpec

Bases: ConfigBaseModel

The semantic input to :func:~graflo.architecture.evolution.aspects.plan_lift.

Source code in graflo/architecture/evolution/aspects/spec.py
class LiftSpec(ConfigBaseModel):
    """The semantic input to :func:`~graflo.architecture.evolution.aspects.plan_lift`."""

    grounding: dict[str, Grounding] = PydanticField(
        default_factory=dict,
        description="Per-vertex grounding: ``{type_name: Grounding}``.",
    )
    edge_grounding: list[EdgeGrounding] = PydanticField(
        default_factory=list,
        description="Per-edge grounding, addressed by triple.",
    )
    identity: dict[str, list[str]] = PydanticField(
        default_factory=dict,
        description=(
            "Explicit identity for types that would otherwise fall back to "
            "``identity_from_all_properties``. Required only for those."
        ),
    )
    stateful: dict[str, list[str]] = PydanticField(
        default_factory=dict,
        description=(
            "Per-type mutable properties: ``{type_name: [property, ...]}``. Each "
            "named type gains a ``<Type>State`` carrying those properties over a "
            "validity interval."
        ),
    )
    observed: list[str] = PydanticField(
        default_factory=list,
        description=(
            "Types that gain a ``<Type>Observation`` scaffold for measurements "
            "taken of them at a time."
        ),
    )
    measured: dict[str, str] = PydanticField(
        default_factory=dict,
        description=(
            "Units for existing properties, as ``{'Type.property': ucum_token}``. "
            "UCUM has no currency, so currency uses ISO-4217 (``USD``)."
        ),
    )
    provenance: bool = PydanticField(
        default=True,
        description=(
            "Lift the ``provenance`` aspect as well: ``SourceRecord`` and ``Agent`` "
            "with the edges that make 'where did this fact come from' answerable. "
            "``false`` leaves it out, as does not naming the aspect."
        ),
    )
    provenance_for: list[str] = PydanticField(
        default_factory=list,
        description=(
            "Existing types whose facts are read from a source record: each gains "
            "a ``wasDerivedFrom`` edge to ``SourceRecord``. The state and "
            "observation scaffolds get one regardless."
        ),
    )
    mentions_for: list[str] = PydanticField(
        default_factory=list,
        description=(
            "Existing types a source record can name: ``SourceRecord`` gains a "
            "``mentions`` edge to each."
        ),
    )
    access_for: dict[str, FieldMerge] = PydanticField(
        default_factory=dict,
        description=(
            "Vertex types that gain ``access_zones``, each with the merge mode of "
            "its zones on upsert: ``replace`` or ``union``."
        ),
    )
    access_edges: dict[str, FieldMerge] = PydanticField(
        default_factory=dict,
        description=(
            "Edge relations that gain ``access_zones``, each with the merge mode "
            "of its zones on upsert: ``replace`` or ``union``."
        ),
    )
    retire: Literal["move", "keep"] = PydanticField(
        default="move",
        description=(
            "What happens to a property named in ``stateful``. ``move`` removes it "
            "from the entity, since a fact that changes over "
            "time does not belong on the thing it is about. ``keep`` leaves it as "
            "a denormalized current value beside the history."
        ),
    )

    @model_validator(mode="after")
    def _validate_measured_addresses(self) -> LiftSpec:
        bad = sorted(key for key in self.measured if key.count(".") != 1)
        if bad:
            raise ValueError(f"measured keys must read 'Type.property', got {bad}")
        return self

    def measured_for(self, vertex: str) -> dict[str, str]:
        """``{property: unit}`` for one type."""
        return {
            key.split(".", 1)[1]: unit
            for key, unit in self.measured.items()
            if key.split(".", 1)[0] == vertex
        }

Attributes

access_edges = PydanticField(default_factory=dict, description='Edge relations that gain ``access_zones``, each with the merge mode of its zones on upsert: ``replace`` or ``union``.') class-attribute instance-attribute
access_for = PydanticField(default_factory=dict, description='Vertex types that gain ``access_zones``, each with the merge mode of its zones on upsert: ``replace`` or ``union``.') class-attribute instance-attribute
edge_grounding = PydanticField(default_factory=list, description='Per-edge grounding, addressed by triple.') class-attribute instance-attribute
grounding = PydanticField(default_factory=dict, description='Per-vertex grounding: ``{type_name: Grounding}``.') class-attribute instance-attribute
identity = PydanticField(default_factory=dict, description='Explicit identity for types that would otherwise fall back to ``identity_from_all_properties``. Required only for those.') class-attribute instance-attribute
measured = PydanticField(default_factory=dict, description="Units for existing properties, as ``{'Type.property': ucum_token}``. UCUM has no currency, so currency uses ISO-4217 (``USD``).") class-attribute instance-attribute
mentions_for = PydanticField(default_factory=list, description='Existing types a source record can name: ``SourceRecord`` gains a ``mentions`` edge to each.') class-attribute instance-attribute
observed = PydanticField(default_factory=list, description='Types that gain a ``<Type>Observation`` scaffold for measurements taken of them at a time.') class-attribute instance-attribute
provenance = PydanticField(default=True, description="Lift the ``provenance`` aspect as well: ``SourceRecord`` and ``Agent`` with the edges that make 'where did this fact come from' answerable. ``false`` leaves it out, as does not naming the aspect.") class-attribute instance-attribute
provenance_for = PydanticField(default_factory=list, description='Existing types whose facts are read from a source record: each gains a ``wasDerivedFrom`` edge to ``SourceRecord``. The state and observation scaffolds get one regardless.') class-attribute instance-attribute
retire = PydanticField(default='move', description='What happens to a property named in ``stateful``. ``move`` removes it from the entity, since a fact that changes over time does not belong on the thing it is about. ``keep`` leaves it as a denormalized current value beside the history.') class-attribute instance-attribute
stateful = PydanticField(default_factory=dict, description='Per-type mutable properties: ``{type_name: [property, ...]}``. Each named type gains a ``<Type>State`` carrying those properties over a validity interval.') class-attribute instance-attribute

Methods:

measured_for(vertex)

{property: unit} for one type.

Source code in graflo/architecture/evolution/aspects/spec.py
def measured_for(self, vertex: str) -> dict[str, str]:
    """``{property: unit}`` for one type."""
    return {
        key.split(".", 1)[1]: unit
        for key, unit in self.measured.items()
        if key.split(".", 1)[0] == vertex
    }

StatePlan

Bases: ConfigBaseModel

What the state aspect planned, in the two halves a composed lift needs apart.

Attributes:

Name Type Description
ops list[ManifestOp]

The additive ops, in application order: grounding first, then structure, then the scaffolding.

scaffolded list[str]

Names of the <Type>State and <Type>Observation types the ops add, for an aspect that attaches to them.

retire list[ManifestOp]

The destructive half, to be applied last.

Source code in graflo/architecture/evolution/aspects/state/plan.py
class StatePlan(ConfigBaseModel):
    """What the state aspect planned, in the two halves a composed lift needs apart.

    Attributes:
        ops: The additive ops, in application order: grounding first, then
            structure, then the scaffolding.
        scaffolded: Names of the ``<Type>State`` and ``<Type>Observation``
            types the ops add, for an aspect that attaches to them.
        retire: The destructive half, to be applied last.
    """

    ops: list[ManifestOp] = PydanticField(default_factory=list)
    scaffolded: list[str] = PydanticField(default_factory=list)
    retire: list[ManifestOp] = PydanticField(default_factory=list)

Attributes

ops = PydanticField(default_factory=list) class-attribute instance-attribute
retire = PydanticField(default_factory=list) class-attribute instance-attribute
scaffolded = PydanticField(default_factory=list) class-attribute instance-attribute

Functions:

build_lift_recipe(manifest, spec=None, *, aspects)

The recipe of lifting manifest with aspects under spec.

Raises:

Type Description
LiftError

an aspect name no aspect has.

Source code in graflo/architecture/evolution/aspects/recipe.py
def build_lift_recipe(
    manifest: GraphManifest,
    spec: LiftSpec | None = None,
    *,
    aspects: Iterable[str],
) -> LiftRecipe:
    """The recipe of lifting *manifest* with *aspects* under *spec*.

    Raises:
        LiftError: an aspect name no aspect has.
    """
    from . import resolve_aspects

    chosen = resolve_aspects(aspects)
    return LiftRecipe(
        base=manifest_hash(manifest),
        aspects=[aspect.name for aspect in chosen],
        versions={aspect.name: aspect.version for aspect in chosen},
        spec=recorded_spec(spec or LiftSpec()),
        graflo_version=_graflo_version(),
    )

plan_access(manifest, *, vertices, edges)

The ops that label vertices and edges with access_zones.

A type that already declares the field keeps it when it is the same field; one declared with another type or merge mode is a contradiction (:func:~graflo.architecture.evolution.aspects.join.join).

Parameters:

Name Type Description Default
manifest GraphManifest

the manifest to lift, already finish_init()-ed, with the types and relations it names.

required
vertices Mapping[str, FieldMerge]

vertex type -> merge mode of its zones.

required
edges Mapping[str, FieldMerge]

edge relation -> merge mode of its zones.

required

Raises:

Type Description
LiftError

nothing to label, a type or relation the manifest does not have, or a field that contradicts the aspect's.

Source code in graflo/architecture/evolution/aspects/access/plan.py
def plan_access(
    manifest: GraphManifest,
    *,
    vertices: Mapping[str, FieldMerge],
    edges: Mapping[str, FieldMerge],
) -> list[ManifestOp]:
    """The ops that label *vertices* and *edges* with ``access_zones``.

    A type that already declares the field keeps it when it is the same field;
    one declared with another type or merge mode is a contradiction
    (:func:`~graflo.architecture.evolution.aspects.join.join`).

    Args:
        manifest: the manifest to lift, already ``finish_init()``-ed, with the
            types and relations it names.
        vertices: vertex type -> merge mode of its zones.
        edges: edge relation -> merge mode of its zones.

    Raises:
        LiftError: nothing to label, a type or relation the manifest does not
            have, or a field that contradicts the aspect's.
    """
    if not vertices and not edges:
        raise LiftError(
            "the access aspect needs access_for or access_edges: it labels only "
            "the types a spec names"
        )
    schema = manifest.graph_schema
    if schema is None:
        raise LiftError("cannot lift a manifest with no schema block")
    return join(
        schema.core_schema,
        vertex_fields={
            name: [access_field(merge)] for name, merge in sorted(vertices.items())
        },
        relation_fields={
            relation: [access_field(merge)] for relation, merge in sorted(edges.items())
        },
    )

plan_lift(manifest, spec=None, *, aspects=DEFAULT_ASPECTS, authored=None)

The ops that lift manifest with the named aspects.

Parameters:

Name Type Description Default
manifest GraphManifest

the manifest to lift, already finish_init()-ed.

required
spec LiftSpec | None

the semantic declarations a planner cannot infer. Required by the state and access aspects; the provenance aspect reads only provenance_for, mentions_for and provenance from it, the access aspect only access_for and access_edges.

None
aspects Iterable[str]

which aspects to lift, by name. The state aspect's scaffolds are attached to SourceRecord when both are named.

DEFAULT_ASPECTS
authored dict[str, Any] | None

the manifest as written, when available; see :func:~graflo.architecture.evolution.aspects.state.plan.plan_state.

None

Returns:

Type Description
list[ManifestOp]

Ops in application order: the additive ops of every aspect, then the

list[ManifestOp]

removals, so a reviewer sees everything that is added before anything

list[ManifestOp]

that is taken away.

Raises:

Type Description
LiftError

an unknown aspect, a missing declaration, or a name the lift would mint that is already taken.

Source code in graflo/architecture/evolution/aspects/__init__.py
def plan_lift(
    manifest: GraphManifest,
    spec: LiftSpec | None = None,
    *,
    aspects: Iterable[str] = DEFAULT_ASPECTS,
    authored: dict[str, Any] | None = None,
) -> list[ManifestOp]:
    """The ops that lift *manifest* with the named *aspects*.

    Args:
        manifest: the manifest to lift, already ``finish_init()``-ed.
        spec: the semantic declarations a planner cannot infer. Required by the
            ``state`` and ``access`` aspects; the ``provenance`` aspect reads
            only ``provenance_for``, ``mentions_for`` and ``provenance`` from it,
            the ``access`` aspect only ``access_for`` and ``access_edges``.
        aspects: which aspects to lift, by name. The ``state`` aspect's
            scaffolds are attached to ``SourceRecord`` when both are named.
        authored: the manifest as written, when available; see
            :func:`~graflo.architecture.evolution.aspects.state.plan.plan_state`.

    Returns:
        Ops in application order: the additive ops of every aspect, then the
        removals, so a reviewer sees everything that is added before anything
        that is taken away.

    Raises:
        LiftError: an unknown aspect, a missing declaration, or a name the lift
            would mint that is already taken.
    """
    # Deferred: apply sits beside this package, and only a composed lift needs it.
    from ..apply import apply_evolution

    chosen = {aspect.name for aspect in resolve_aspects(aspects)}
    spec = spec or LiftSpec()
    ops: list[ManifestOp] = []
    # Each aspect is planned against the manifest as the aspects before it left
    # it, so every join sees the types and fields it would otherwise only be
    # told about.
    lifted = manifest

    def _then(planned: list[ManifestOp]) -> None:
        nonlocal lifted
        ops.extend(planned)
        if planned:
            lifted = apply_evolution(lifted, planned, bump_version=False)

    state: StatePlan | None = None
    if STATE in chosen:
        state = plan_state(manifest, spec, authored=authored)
        _then(state.ops)

    if PROVENANCE in chosen and spec.provenance:
        schema = manifest.graph_schema
        existing = (
            set(schema.core_schema.vertex_config.vertex_set)
            if schema is not None
            else set()
        )
        for field, names in (
            ("provenance_for", spec.provenance_for),
            ("mentions_for", spec.mentions_for),
        ):
            unknown = sorted(set(names) - existing)
            if unknown:
                raise LiftError(
                    f"{field} names types the manifest does not have: {unknown}"
                )
        attach = [
            *sorted(spec.provenance_for),
            *(state.scaffolded if state is not None else []),
        ]
        _then(
            plan_provenance(lifted, attach=attach, mentions=sorted(spec.mentions_for))
        )

    if ACCESS in chosen:
        _then(plan_access(lifted, vertices=spec.access_for, edges=spec.access_edges))

    if SYNC in chosen:
        _then(plan_sync(lifted))

    if state is not None:
        ops += state.retire
    return ops

plan_provenance(manifest, *, attach=(), mentions=())

The ops that make lineage expressible in manifest.

SourceRecord -wasAttributedTo-> Agent is emitted even when there is nothing else to attach: it is what makes the schema able to say where a fact came from, which is the question the world-model profile asks.

The vocabulary is joined with what the manifest has (:func:~graflo.architecture.evolution.aspects.join.join): a SourceRecord or Agent it already declares keeps its own properties and gains the aspect's missing ones, so manifests that carry the vocabulary, such as source kits, compose and lift in any order.

Parameters:

Name Type Description Default
manifest GraphManifest

the manifest to lift, already finish_init()-ed.

required
attach Iterable[str]

vertex type names that gain a wasDerivedFrom edge to SourceRecord, in this order. The caller vouches that each one exists or is added by the same change set.

()
mentions Iterable[str]

vertex type names SourceRecord gains a mentions edge to, in this order, on the same terms.

()

Raises:

Type Description
LiftError

the manifest has no schema, or declares part of the vocabulary in a way that contradicts it.

Source code in graflo/architecture/evolution/aspects/provenance/plan.py
def plan_provenance(
    manifest: GraphManifest,
    *,
    attach: Iterable[str] = (),
    mentions: Iterable[str] = (),
) -> list[ManifestOp]:
    """The ops that make lineage expressible in *manifest*.

    ``SourceRecord -wasAttributedTo-> Agent`` is emitted even when there is
    nothing else to attach: it is what makes the schema *able* to say where a
    fact came from, which is the question the world-model profile asks.

    The vocabulary is joined with what the manifest has
    (:func:`~graflo.architecture.evolution.aspects.join.join`): a
    ``SourceRecord`` or ``Agent`` it already declares keeps its own properties
    and gains the aspect's missing ones, so manifests that carry the
    vocabulary, such as source kits, compose and lift in any order.

    Args:
        manifest: the manifest to lift, already ``finish_init()``-ed.
        attach: vertex type names that gain a ``wasDerivedFrom`` edge to
            ``SourceRecord``, in this order. The caller vouches that each one
            exists or is added by the same change set.
        mentions: vertex type names ``SourceRecord`` gains a ``mentions`` edge to,
            in this order, on the same terms.

    Raises:
        LiftError: the manifest has no schema, or declares part of the
            vocabulary in a way that contradicts it.
    """
    schema = manifest.graph_schema
    if schema is None:
        raise LiftError(
            "cannot lift a manifest with no schema block: there are no types to "
            "attach provenance to"
        )
    return join(
        schema.core_schema,
        vertices=[vocab.source_record_vertex(), vocab.agent_vertex()],
        edges=[
            *(vocab.derived_from_edge(name) for name in attach),
            *(vocab.mentions_edge(name) for name in mentions),
            vocab.attributed_to_edge(),
        ],
    )

plan_relift(recipe, manifest, *, authored=None)

The ops that lift manifest again as recipe records.

manifest need not be the one the recipe was planned on: a re-lift is for a manifest that has moved on, or for an aspect whose version has (see :func:recipe_drift). A manifest that already carries everything plans no ops.

Raises:

Type Description
LiftError

as :func:~graflo.architecture.evolution.aspects.plan_lift.

Source code in graflo/architecture/evolution/aspects/recipe.py
def plan_relift(
    recipe: LiftRecipe,
    manifest: GraphManifest,
    *,
    authored: dict[str, Any] | None = None,
) -> list[ManifestOp]:
    """The ops that lift *manifest* again as *recipe* records.

    *manifest* need not be the one the recipe was planned on: a re-lift is
    for a manifest that has moved on, or for an aspect whose version has
    (see :func:`recipe_drift`). A manifest that already carries everything
    plans no ops.

    Raises:
        LiftError: as :func:`~graflo.architecture.evolution.aspects.plan_lift`.
    """
    from . import plan_lift

    return plan_lift(
        manifest, recipe.lift_spec(), aspects=recipe.aspects, authored=authored
    )

plan_state(manifest, spec, *, authored=None)

The ops that lift manifest into one that tracks state and measurements.

Parameters:

Name Type Description Default
manifest GraphManifest

the manifest to lift, already finish_init()-ed.

required
spec LiftSpec

the semantic declarations the planner cannot infer.

required
authored dict[str, Any] | None

the manifest as written, when available. Only an authored document distinguishes "this vertex declared no identity" from "this vertex declared every property as its identity", so without it the planner cannot detect the fallback and does not try.

None

Returns:

Type Description
StatePlan

The additive ops, the scaffolded type names, and the removals kept

StatePlan

apart so they can run last. Ordering matters -- a property is moved

StatePlan

onto its State before it is removed from the entity.

Raises:

Type Description
LiftError

a declaration the planner needs is missing, or a name it would mint is already taken.

Source code in graflo/architecture/evolution/aspects/state/plan.py
def plan_state(
    manifest: GraphManifest,
    spec: LiftSpec,
    *,
    authored: dict[str, Any] | None = None,
) -> StatePlan:
    """The ops that lift *manifest* into one that tracks state and measurements.

    Args:
        manifest: the manifest to lift, already ``finish_init()``-ed.
        spec: the semantic declarations the planner cannot infer.
        authored: the manifest as written, when available. Only an authored
            document distinguishes "this vertex declared no identity" from
            "this vertex declared every property as its identity", so without
            it the planner cannot detect the fallback and does not try.

    Returns:
        The additive ops, the scaffolded type names, and the removals kept
        apart so they can run last. Ordering matters -- a property is moved
        onto its ``State`` before it is removed from the entity.

    Raises:
        LiftError: a declaration the planner needs is missing, or a name it
            would mint is already taken.
    """
    schema = manifest.graph_schema
    if schema is None:
        raise LiftError(
            "cannot lift a manifest with no schema block: there are no types to "
            "ground and nothing to attach state to"
        )

    core = schema.core_schema
    vertex_config = core.vertex_config
    existing = set(vertex_config.vertex_set)
    by_name = {vertex.name: vertex for vertex in vertex_config.vertices}

    unknown = sorted(
        (set(spec.grounding) | set(spec.stateful) | set(spec.observed)) - existing
    )
    if unknown:
        raise LiftError(f"spec names types the manifest does not have: {unknown}")

    ops: list[ManifestOp] = []
    ops += _identity_ops(spec, vertex_config, authored)
    ops += _grounding_ops(spec, core)
    ops += _directionality_ops(core)
    ops += _unit_ops(spec, by_name)
    scaffold_ops, scaffolded = _scaffold_ops(spec, by_name, core)
    ops += scaffold_ops
    return StatePlan(ops=ops, scaffolded=scaffolded, retire=_retire_ops(spec, by_name))

plan_sync(manifest)

The ops that give every type a resource owns a sync_hash.

Raises:

Type Description
LiftError

no schema, or no resource declares a sync block.

Source code in graflo/architecture/evolution/aspects/sync/plan.py
def plan_sync(manifest: GraphManifest) -> list[ManifestOp]:
    """The ops that give every type a resource owns a ``sync_hash``.

    Raises:
        LiftError: no schema, or no resource declares a ``sync`` block.
    """
    if manifest.graph_schema is None:
        raise LiftError("cannot lift a manifest with no schema block")
    model = manifest.ingestion_model
    owned = sorted(
        {r.sync.owns for r in (model.resources if model else []) if r.sync is not None}
    )
    if not owned:
        raise LiftError(
            "the sync aspect labels the types a resource's sync block owns; "
            "no resource declares one"
        )
    return join(
        manifest.graph_schema.core_schema,
        vertex_fields={name: [sync_field()] for name in owned},
    )

recipe_drift(recipe)

The aspects whose version moved since recipe: {name: (recorded, now)}.

Raises:

Type Description
LiftError

an aspect the recipe names that no longer exists.

Source code in graflo/architecture/evolution/aspects/recipe.py
def recipe_drift(recipe: LiftRecipe) -> dict[str, tuple[int, int]]:
    """The aspects whose version moved since *recipe*: ``{name: (recorded, now)}``.

    Raises:
        LiftError: an aspect the recipe names that no longer exists.
    """
    from . import resolve_aspects

    current = {
        aspect.name: aspect.version for aspect in resolve_aspects(recipe.versions)
    }
    return {
        name: (recorded, current[name])
        for name, recorded in recipe.versions.items()
        if current[name] != recorded
    }

recorded_spec(spec)

spec as a recipe records it: no defaults, set-like lists sorted.

Without defaults, a field added to :class:LiftSpec later does not move the hash of a recipe recorded before it. The lists the planners read as sets are sorted, so two declarations that plan the same lift are the same recipe.

Source code in graflo/architecture/evolution/aspects/recipe.py
def recorded_spec(spec: LiftSpec) -> dict[str, Any]:
    """*spec* as a recipe records it: no defaults, set-like lists sorted.

    Without defaults, a field added to :class:`LiftSpec` later does not move
    the hash of a recipe recorded before it. The lists the planners read as
    sets are sorted, so two declarations that plan the same lift are the same
    recipe.
    """
    recorded = spec.to_dict(skip_defaults=True)
    for name in _UNORDERED_SPEC_LISTS:
        if name in recorded:
            recorded[name] = sorted(recorded[name])
    return recorded

resolve_aspects(names)

The aspects called names, in the order given.

Raises:

Type Description
LiftError

a name no aspect has.

Source code in graflo/architecture/evolution/aspects/__init__.py
def resolve_aspects(names: Iterable[str]) -> list[Aspect]:
    """The aspects called *names*, in the order given.

    Raises:
        LiftError: a name no aspect has.
    """
    chosen = list(names)
    unknown = sorted(set(chosen) - set(ASPECTS))
    if unknown:
        raise LiftError(f"no aspect named {unknown}; the aspects are {sorted(ASPECTS)}")
    return [ASPECTS[name] for name in chosen]