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 be None)

  • source_scope: The scope_id where the value was found (or None)

  • source_type: The type that provided the value (or None)

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

  1. Use provenance for debugging: When unexpected values appear, use get_provenance() to trace their origin.

  2. Check for explicit vs inherited: None return from get_provenance() means the value is explicitly set, not inherited.

  3. Combine with value inspection: Use resolve_with_provenance() when you need both the value and its origin in a single call.

  4. UI feedback: Display provenance information in tooltips or status text to help users understand configuration inheritance.

  5. 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:

  1. Context Layer Stack: The config_context() manager tracks (scope_id, obj) tuples as contexts are pushed.

  2. 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.

  3. Live Provenance Cache: ObjectState maintains _live_provenance dict that stores (scope_id, source_type) tuples for each inherited field.

  4. Snapshot Integration: Provenance is included in StateSnapshot objects 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_provenance cache 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