Skip to content

graflo.architecture.contract.provenance

Manifest-level metadata, and the provenance stamping that writes part of it.

:class:~graflo.architecture.schema.provenance.Provenance itself lives at L2, beside the schema metadata that carries it. What lives here is the manifest's own metadata block -- Schema.metadata describes the schema, and a manifest is a larger object that may carry no schema at all, so it needs its own name, description, language version and content address -- plus the stamping helpers.

Stamping is a free function on purpose. It is something a commit point (the CLI, a server route) does to an artifact, not something the artifact does to itself. Keeping it outside the models is what stops provenance leaking into apply_evolution, which must stay pure: applying the same ops twice cannot be allowed to produce two artifacts that disagree about their own lineage.

Attributes

GSTL_VERSION = '1.0' module-attribute

SUPPORTED_GSTL_MAJORS = frozenset({1}) module-attribute

__all__ = ['GSTL_VERSION', 'SUPPORTED_GSTL_MAJORS', 'ManifestMetadata', 'Provenance', 'read_provenance', 'stamp_gstl', 'stamp_provenance'] module-attribute

Classes

ManifestMetadata

Bases: ConfigBaseModel

Metadata about a manifest as a whole.

Two kinds of thing live here, and both are properties of the manifest rather than of any one block. Provenance is a content address covering all three blocks together, so it cannot hang off the schema's metadata, which covers the schema alone. Name and description are the manifest's own identity: a manifest that carries only bindings has no schema to borrow a name from.

Every field here is excluded from the manifest's content hash -- :func:~graflo.architecture.evolution.hashing.manifest_hash covers the three blocks and nothing else. A content address that moved when a manifest was renamed could not recognise that two routes reach the same world model.

What does not belong here is presentation: display wording for a type is a property of the type (Vertex.description, Semantics), and display wording for a deployment is resolved outside the contract entirely.

Source code in graflo/architecture/contract/provenance.py
class ManifestMetadata(ConfigBaseModel):
    """Metadata about a manifest as a whole.

    Two kinds of thing live here, and both are properties of the manifest
    rather than of any one block. **Provenance** is a content address covering
    all three blocks together, so it cannot hang off the schema's metadata,
    which covers the schema alone. **Name and description** are the
    manifest's own identity: a manifest that carries only bindings has no
    schema to borrow a name from.

    Every field here is excluded from the manifest's content hash --
    :func:`~graflo.architecture.evolution.hashing.manifest_hash` covers the
    three blocks and nothing else. A content address that moved when a manifest
    was renamed could not recognise that two routes reach the same world model.

    What does **not** belong here is presentation: display wording for a type
    is a property of the type (``Vertex.description``, ``Semantics``), and
    display wording for a deployment is resolved outside the contract
    entirely.
    """

    name: str | None = PydanticField(
        default=None,
        description="Name of this manifest as a whole.",
    )
    gstl: str | None = PydanticField(
        default=None,
        pattern=r"^\d+\.\d+$",
        description=(
            "GSTL language version this manifest is written in, as MAJOR.MINOR. "
            "Absent means unstated."
        ),
    )
    description: str | None = PydanticField(
        default=None,
        description="Optional human-readable description of this manifest.",
    )
    provenance: Provenance | None = PydanticField(
        default=None,
        description="Content address and lineage of this manifest.",
    )

    @field_validator("gstl")
    @classmethod
    def _supported_major(cls, value: str | None) -> str | None:
        """Refuse a major version this graflo cannot read."""
        if value is None:
            return value
        major = int(value.split(".", 1)[0])
        if major not in SUPPORTED_GSTL_MAJORS:
            supported = ", ".join(str(m) for m in sorted(SUPPORTED_GSTL_MAJORS))
            raise ValueError(
                f"unsupported GSTL major version {major} (gstl={value!r}); "
                f"this graflo reads major {supported}"
            )
        return value

Attributes

description = PydanticField(default=None, description='Optional human-readable description of this manifest.') class-attribute instance-attribute
gstl = PydanticField(default=None, pattern='^\\d+\\.\\d+$', description='GSTL language version this manifest is written in, as MAJOR.MINOR. Absent means unstated.') class-attribute instance-attribute
name = PydanticField(default=None, description='Name of this manifest as a whole.') class-attribute instance-attribute
provenance = PydanticField(default=None, description='Content address and lineage of this manifest.') class-attribute instance-attribute

Provenance

Bases: ConfigBaseModel

Content address and lineage of a shipped schema or manifest.

Source code in graflo/architecture/schema/provenance.py
class Provenance(ConfigBaseModel):
    """Content address and lineage of a shipped schema or manifest."""

    content_hash: str | None = PydanticField(
        default=None,
        description=(
            "SHA-256 over this artifact's canonical payload, excluding this "
            "block. Two artifacts sharing a content_hash are the same world "
            "model, however they were reached."
        ),
    )
    canon: str | None = PydanticField(
        default=None,
        description=(
            "Canonicalization version that produced content_hash (e.g. "
            "'graflo/canon@2'). Hashes compare only within one canon."
        ),
    )
    parents: list[str] = PydanticField(
        default_factory=list,
        description=(
            "Parent commit ids in position order: empty for a root, one for an "
            "ordinary edit, two or more for a merge or merge3. Position is "
            "significant -- a merge's ops are materialized against the first "
            "parent, which is what keeps verified replay working unchanged."
        ),
    )
    commit: str | None = PydanticField(
        default=None,
        description="Id of the commit that produced this state.",
    )
    recipe: str | None = PydanticField(
        default=None,
        description=(
            "Content hash of the recorded recipe: a MergeRecipe on merge "
            "commits, a LiftRecipe on lift commits. The recipe rides with the "
            "commit; this is the pointer a re-merge or a re-lift follows to "
            "find it."
        ),
    )

    @property
    def is_multi_parent(self) -> bool:
        """Whether this state was produced by combining two or more lineages.

        True after a merge and after a merge3 alike -- both record two or more
        parents. The commit's `kind` is what distinguishes them.
        """
        return len(self.parents) > 1

Attributes

canon = PydanticField(default=None, description="Canonicalization version that produced content_hash (e.g. 'graflo/canon@2'). Hashes compare only within one canon.") class-attribute instance-attribute
commit = PydanticField(default=None, description='Id of the commit that produced this state.') class-attribute instance-attribute
content_hash = PydanticField(default=None, description="SHA-256 over this artifact's canonical payload, excluding this block. Two artifacts sharing a content_hash are the same world model, however they were reached.") class-attribute instance-attribute
is_multi_parent property

Whether this state was produced by combining two or more lineages.

True after a merge and after a merge3 alike -- both record two or more parents. The commit's kind is what distinguishes them.

parents = PydanticField(default_factory=list, description="Parent commit ids in position order: empty for a root, one for an ordinary edit, two or more for a merge or merge3. Position is significant -- a merge's ops are materialized against the first parent, which is what keeps verified replay working unchanged.") class-attribute instance-attribute
recipe = PydanticField(default=None, description='Content hash of the recorded recipe: a MergeRecipe on merge commits, a LiftRecipe on lift commits. The recipe rides with the commit; this is the pointer a re-merge or a re-lift follows to find it.') class-attribute instance-attribute

Functions:

read_provenance(target)

The provenance block on target, or None when it carries none.

Source code in graflo/architecture/contract/provenance.py
def read_provenance(target: Any) -> Provenance | None:
    """The provenance block on *target*, or ``None`` when it carries none."""
    metadata = getattr(target, "metadata", None)
    return getattr(metadata, "provenance", None) if metadata is not None else None

stamp_gstl(target)

Write :data:GSTL_VERSION into target's metadata.gstl, in place.

target is a GraphManifest; metadata is created if absent. Writers call this on what they emit, so their output states the language it is in.

Source code in graflo/architecture/contract/provenance.py
def stamp_gstl(target: Any) -> None:
    """Write :data:`GSTL_VERSION` into *target*'s ``metadata.gstl``, in place.

    *target* is a ``GraphManifest``; ``metadata`` is created if absent. Writers
    call this on what they emit, so their output states the language it is in.
    """
    metadata = getattr(target, "metadata", None)
    if metadata is None:
        metadata = ManifestMetadata()
        object.__setattr__(target, "metadata", metadata)
    object.__setattr__(metadata, "gstl", GSTL_VERSION)

stamp_provenance(target, *, content_hash, canon, commit=None, parents=None, recipe=None)

Write a :class:Provenance block onto target, in place.

Accepts a GraphManifest (creating metadata if absent) or anything else carrying a metadata block that holds provenance, such as Schema. A manifest's metadata also gets gstl (:func:stamp_gstl).

Parameters:

Name Type Description Default
target Any

The artifact to stamp.

required
content_hash str

Content address of target, excluding provenance.

required
canon str

Canonicalization version that produced content_hash.

required
commit str | None

Id of the commit producing this state.

None
parents list[str] | None

Parent commit ids, in position order.

None
recipe str | None

Content hash of the recorded recipe, on merge and lift commits only.

None

Returns:

Type Description
Provenance

The provenance block that was written.

Raises:

Type Description
TypeError

target has no metadata block to stamp.

Source code in graflo/architecture/contract/provenance.py
def stamp_provenance(
    target: Any,
    *,
    content_hash: str,
    canon: str,
    commit: str | None = None,
    parents: list[str] | None = None,
    recipe: str | None = None,
) -> Provenance:
    """Write a :class:`Provenance` block onto *target*, in place.

    Accepts a ``GraphManifest`` (creating ``metadata`` if absent) or anything
    else carrying a metadata block that holds provenance, such as ``Schema``.
    A manifest's metadata also gets ``gstl`` (:func:`stamp_gstl`).

    Args:
        target: The artifact to stamp.
        content_hash: Content address of *target*, excluding provenance.
        canon: Canonicalization version that produced *content_hash*.
        commit: Id of the commit producing this state.
        parents: Parent commit ids, in position order.
        recipe: Content hash of the recorded recipe, on merge and lift
            commits only.

    Returns:
        The provenance block that was written.

    Raises:
        TypeError: *target* has no metadata block to stamp.
    """
    fields = getattr(type(target), "model_fields", None)
    if not isinstance(fields, dict) or "metadata" not in fields:
        raise TypeError(
            f"{type(target).__name__} has no metadata block to stamp provenance on"
        )

    provenance = Provenance(
        content_hash=content_hash,
        canon=canon,
        commit=commit,
        parents=list(parents or []),
        recipe=recipe,
    )

    metadata = getattr(target, "metadata", None)
    if metadata is None:
        metadata = ManifestMetadata()
        object.__setattr__(target, "metadata", metadata)
    object.__setattr__(metadata, "provenance", provenance)
    if isinstance(metadata, ManifestMetadata):
        object.__setattr__(metadata, "gstl", GSTL_VERSION)
    return provenance