Provenance Validation¶
Provenance metadata records where a piece of context came from -- its origin URL, author, creation date, and version. Provena validates this metadata on every logged entry and assigns a verdict that tells you whether the declared lineage is complete. Completeness is not the same as truthfulness: see VALID means present, not verified below.
ProvenanceMetadata¶
The ProvenanceMetadata dataclass carries origin information for a context
input:
from provena import ProvenanceMetadata
from datetime import datetime, timezone
provenance = ProvenanceMetadata(
source_url="https://docs.example.com/api/v2",
author="Platform Team",
created_at=datetime(2025, 3, 15, tzinfo=timezone.utc),
version="2.1.0",
extra={"department": "engineering", "review_status": "approved"},
)
| Field | Type | Description |
|---|---|---|
source_url |
str or None |
URL where the content was retrieved |
author |
str or None |
Author or creator of the content |
created_at |
datetime or None |
When the content was originally published |
version |
str or None |
Version identifier |
extra |
dict[str, Any] |
Arbitrary additional metadata |
Validation Verdicts¶
Every logged entry receives one of three provenance verdicts:
| Verdict | Meaning |
|---|---|
| VALID | All required fields are present and non-empty |
| MISSING | No ProvenanceMetadata was attached at all |
| INCOMPLETE | Metadata was attached but one or more required fields are absent |
from provena import ContextTrail, ProvenanceMetadata
from datetime import datetime, timezone
trail = ContextTrail(backend="memory")
# MISSING -- no provenance provided
trail.log(content="Some text", source="retriever")
# INCOMPLETE -- source_url present but created_at is missing
trail.log(
content="Some text",
source="retriever",
provenance=ProvenanceMetadata(source_url="https://example.com"),
)
# VALID -- both default required fields present
trail.log(
content="Some text",
source="retriever",
provenance=ProvenanceMetadata(
source_url="https://example.com",
created_at=datetime(2025, 6, 1, tzinfo=timezone.utc),
),
)
summary = trail.summary()
print(summary["provenance"])
# {'MISSING': 1, 'INCOMPLETE': 1, 'VALID': 1}
VALID means present, not verified
A VALID verdict only confirms that the required fields are present and
non-empty. It doesn't fetch source_url or check created_at against
when the entry was actually logged, so a plausible but fabricated value
passes just as well as a real one. This is deliberate: Provena is an
audit trail, and it records what was declared, not whether the
declaration is true.
For stricter checks, add a rule to the policy engine's
provenance_check() hook, for example a domain allowlist or a
timestamp-recency check. The hook runs at log time with full access to
the record, but a BLOCK-level DENY fires after the record is
persisted and the chain has advanced, so the declaration still ends up
in the trail.
There is no fourth verdict for present but uncorroborated, because from
the validator's position every VALID entry is uncorroborated. Read a
VALID count as "the callers declared an origin this often", never as
"the origins were confirmed this often".
Default Required Fields¶
By default, Provena requires two fields for a VALID verdict:
source_url-- where the content was retrievedcreated_at-- when the content was created or published
If either is None or an empty string, the verdict is INCOMPLETE.
Custom Required Fields¶
Override the defaults by passing required_fields to ContextTrail:
trail = ContextTrail(
backend="memory",
required_fields=["source_url", "author", "version"],
)
# INCOMPLETE -- author and version are missing
record = trail.log(
content="API response payload",
source="tool:api",
provenance=ProvenanceMetadata(
source_url="https://api.example.com/v2/data",
),
)
# VALID -- all three custom fields present
record = trail.log(
content="API response payload",
source="tool:api",
provenance=ProvenanceMetadata(
source_url="https://api.example.com/v2/data",
author="data-service",
version="2.4.1",
),
)
Attaching Provenance to trail.log()¶
Pass a ProvenanceMetadata instance to the provenance parameter:
from provena import ContextTrail, ProvenanceMetadata
from datetime import datetime, timezone
trail = ContextTrail(backend="memory")
record = trail.log(
content="Kubernetes 1.30 deprecates PodSecurityPolicy.",
source="retriever",
source_name="changelog_db",
provenance=ProvenanceMetadata(
source_url="https://kubernetes.io/blog/2024/k8s-1.30",
author="Kubernetes Release Team",
created_at=datetime(2024, 4, 17, tzinfo=timezone.utc),
version="1.30",
),
)
print(record.provenance_result.status) # "VALID"
Auto-extraction from LangChain Documents¶
When using @trail.track() with objects that have a .metadata dictionary,
Provena automatically builds ProvenanceMetadata from recognized keys:
class Document:
def __init__(self, page_content: str, metadata: dict):
self.page_content = page_content
self.metadata = metadata
@trail.track(source="retriever")
def retrieve(query: str) -> Document:
return Document(
page_content="Horizontal pod autoscaling adjusts replica count.",
metadata={
"source": "https://k8s.io/docs/hpa",
"author": "SIG Autoscaling",
},
)
doc = retrieve("autoscaling")
The following .metadata keys are mapped automatically:
| Document metadata key | ProvenanceMetadata field |
|---|---|
source or source_url |
source_url |
author |
author |
version |
version |
LlamaIndex nodes
LlamaIndex nodes follow the same pattern. The metadata dict keys
source, file_path, and author are extracted automatically when
using the ProvenaPostprocessor integration.
Auto-extraction from LlamaIndex Nodes¶
The ProvenaPostprocessor extracts provenance from LlamaIndex node metadata:
from provena import ContextTrail
from provena.integrations.llamaindex import ProvenaPostprocessor
trail = ContextTrail(backend="memory")
postprocessor = ProvenaPostprocessor(trail=trail)
# Nodes with metadata like {"source": "...", "author": "..."}
# will have ProvenanceMetadata created automatically.
| Node metadata key | ProvenanceMetadata field |
|---|---|
source or file_path |
source_url |
author |
author |
Checking Provenance in trail.summary()¶
The summary() method provides an aggregate breakdown of provenance verdicts
across all records:
summary = trail.summary()
print(summary["provenance"])
# Example output:
# {'VALID': 42, 'INCOMPLETE': 3, 'MISSING': 7}
Use this to monitor governance health over time. A high MISSING count indicates that context sources are not providing origin metadata.
Serialization¶
ProvenanceMetadata supports round-trip serialization:
from provena import ProvenanceMetadata
from datetime import datetime, timezone
original = ProvenanceMetadata(
source_url="https://example.com/doc",
author="Engineering",
created_at=datetime(2025, 1, 15, tzinfo=timezone.utc),
version="3.0",
extra={"classification": "internal"},
)
# Serialize to dict
data = original.to_dict()
print(data)
# {
# 'source_url': 'https://example.com/doc',
# 'author': 'Engineering',
# 'created_at': '2025-01-15T00:00:00+00:00',
# 'version': '3.0',
# 'extra': {'classification': 'internal'}
# }
# Deserialize back
restored = ProvenanceMetadata.from_dict(data)
assert restored.source_url == original.source_url
assert restored.created_at == original.created_at
Storage format
Provena stores provenance as JSON in the audit database. The to_dict()
method omits fields that are None, keeping storage compact. The
from_dict() class method handles ISO-format datetime strings
automatically.