Provenance Tracking
Provenance tracking allows you to determine which scope provided a resolved value for inherited fields. This is particularly useful for debugging configuration inheritance and understanding where values originate in the dual-axis resolution system.
Overview
When a field value is inherited from a parent context (global, pipeline, step, etc.), the provenance system tracks:
source_scope_id: The scope identifier where the value was found (e.g.,
"plate_123::step_0")source_type: The type of the object that provided the value
This enables you to trace the origin of any inherited field value through the configuration hierarchy.
Key Concepts
Provenance Information
For each inherited field, provenance is stored as a tuple:
(source_scope_id, source_type)
source_scope_id: String identifier of the scope (e.g.,""for global,"plate_123"for a plate,"plate_123::step_0"for a step)source_type: The class type that provided the value
When a field has an explicit value (not inherited), it has no provenance - the value comes from the current object itself.
Live vs Saved Provenance
The provenance system maintains separate tracking for:
Live provenance: Tracks where values come from in the current context (with all edits applied)
Saved provenance: Captured at the last save point, representing the committed state
API Reference
ObjectState.get_provenance()
Get the source scope_id and type for an inherited field value:
from objectstate import ObjectStateRegistry
# Get the ObjectState for your object
state = ObjectStateRegistry.get_by_scope("plate_123::step_0")
# Get provenance for a field
provenance = state.get_provenance("well_filter")
if provenance:
source_scope, source_type = provenance
print(f"well_filter comes from scope: {source_scope}")
print(f"well_filter comes from type: {source_type.__name__}")
else:
print("well_filter is explicitly set on this object")
Returns:
* Tuple[str, type] if the value is inherited
* None if the value is explicitly set on the current object
Note: Returns provenance even when the resolved value is None (signature default). A “concrete None” just means the class default is None and nothing overrode it.
resolve_with_provenance()
Resolve a field value AND find its provenance source in ONE walk:
from objectstate.dual_axis_resolver import resolve_with_provenance
# Resolve value and get provenance simultaneously
value, source_scope, source_type = resolve_with_provenance(
container_type=PathPlanningConfig,
field_name="well_filter"
)
print(f"Value: {value}")
print(f"From scope: {source_scope}")
print(f"From type: {source_type.__name__}")
Parameters:
container_type: The type containing the field (e.g.,LazyPathPlanningConfig)field_name: Name of the field to find provenance for (e.g.,"well_filter")
Returns:
value: The resolved field value (may beNone)source_scope: The scope_id where the value was found (orNone)source_type: The type that provided the value (orNone)
Performance: Single walk instead of separate resolve + provenance calls.
get_field_provenance()
Convenience wrapper that returns only the scope and type (not the value):
from objectstate.dual_axis_resolver import get_field_provenance
source_scope, source_type = get_field_provenance(
container_type=PathPlanningConfig,
field_name="well_filter"
)
print(f"Field comes from: {source_scope} ({source_type.__name__})")
Note: Calls resolve_with_provenance() internally and returns scope + type. Use resolve_with_provenance() directly when you also need the value.
Context Layer Stack
The provenance system uses the context layer stack for tracking:
from objectstate.context_manager import get_context_layer_stack
# Get the current layer stack for provenance tracking
layers = get_context_layer_stack()
# Returns: [(scope_id, obj), ...] tuples
# Used by get_field_provenance() to determine which scope provided a resolved value
The context layer stack tracks (scope_id, obj) tuples for provenance tracking, parallel to the merged config. It preserves hierarchy for inheritance source lookup.
Usage Examples
Debugging Inherited Values
Use provenance to understand where a value comes from:
from objectstate import ObjectStateRegistry
state = ObjectStateRegistry.get_by_scope("plate_123::step_0")
# Check where various fields come from
for field_name in ["well_filter", "output_dir", "num_workers"]:
provenance = state.get_provenance(field_name)
if provenance:
source_scope, source_type = provenance
print(f"{field_name}: inherited from {source_scope} ({source_type.__name__})")
else:
print(f"{field_name}: explicitly set on this object")
Example output:
well_filter: inherited from (GlobalPipelineConfig)
output_dir: inherited from plate_123 (PlateConfig)
num_workers: explicitly set on this object
Tracing Value Origins
Trace the full inheritance chain for a field:
from objectstate.dual_axis_resolver import resolve_with_provenance
def trace_field_origin(container_type, field_name):
"""Trace where a field value comes from."""
value, source_scope, source_type = resolve_with_provenance(
container_type, field_name
)
print(f"\nField: {field_name}")
print(f" Value: {value}")
print(f" Source scope: {source_scope or '<none>'}")
print(f" Source type: {source_type.__name__ if source_type else '<none>'}")
if source_scope == "":
print(" → Value comes from global configuration")
elif source_scope:
print(f" → Value inherited from scope: {source_scope}")
else:
print(" → Value is a class default (no override)")
# Use it
trace_field_origin(PathPlanningConfig, "well_filter")
trace_field_origin(PathPlanningConfig, "output_dir_suffix")
UI Integration
Provenance information can be displayed in UI elements to help users understand where values come from:
from objectstate import ObjectStateRegistry
state = ObjectStateRegistry.get_by_scope("plate_123::step_0")
def get_field_display_info(field_name):
"""Get display information for a field including provenance."""
value = getattr(state.object_instance, field_name, None)
provenance = state.get_provenance(field_name)
if provenance:
source_scope, source_type = provenance
origin = f"Inherited from {source_type.__name__}"
if source_scope:
origin += f" ({source_scope})"
else:
origin = "Explicitly set"
return {
"value": value,
"origin": origin
}
# Display in UI
info = get_field_display_info("well_filter")
print(f"well_filter: {info['value']} [{info['origin']}]")
Provenance in Snapshots
Provenance is captured in state snapshots for history tracking:
from objectstate import ObjectStateRegistry
# Provenance is stored in the snapshot
state = ObjectStateRegistry.get_by_scope("plate_123::step_0")
# The _live_provenance dict tracks provenance for all inherited fields
# Format: {field_name: (scope_id, source_type)}
for field_name, (scope_id, source_type) in state._live_provenance.items():
print(f"{field_name}: from {scope_id} ({source_type.__name__})")
When recording snapshots for undo/redo, the provenance information is preserved, allowing you to trace the origin of values at any point in history.
Best Practices
Use provenance for debugging: When unexpected values appear, use
get_provenance()to trace their origin.Check for explicit vs inherited:
Nonereturn fromget_provenance()means the value is explicitly set, not inherited.Combine with value inspection: Use
resolve_with_provenance()when you need both the value and its origin in a single call.UI feedback: Display provenance information in tooltips or status text to help users understand configuration inheritance.
History analysis: Use provenance in snapshots to understand how values evolved over time.
Implementation Details
How Provenance Works
The provenance system works through these mechanisms:
Context Layer Stack: The
config_context()manager tracks(scope_id, obj)tuples as contexts are pushed.Dual-Axis Resolution with Provenance: When resolving inherited fields,
resolve_with_provenance()walks both the MRO and context hierarchy, returning the first concrete value found along with its source information.Live Provenance Cache:
ObjectStatemaintains_live_provenancedict that stores(scope_id, source_type)tuples for each inherited field.Snapshot Integration: Provenance is included in
StateSnapshotobjects for history tracking.
Performance Considerations
Provenance tracking adds minimal overhead - it’s computed during the same resolution walk that finds values.
Use
resolve_with_provenance()for combined value+provenance lookup instead of separate calls.The
_live_provenancecache is invalidated when contexts change, ensuring accurate tracking.
See Also
Architecture - Dual-axis resolution system
State Management - ObjectState and state snapshots
context_system - Context management and stacking