Repository navigation
Conversation
…ayer stack Attribute access and ObjectState both read resolve_with_provenance. The merged-config walk (resolve_field_inheritance) and the process-wide caches are deleted. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This was referenced Oct 12, 2026
Merged
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #29
Fixes #33
Based on
fix/34-31-context-stack-loud(PR #41); the diff shows only this work.What changed
One resolution (#29).
resolve_with_provenance(container_type, field_name)is the only function that walks scopes and MRO for a field. It returns aResolvedField(value, source_scope, source_type)named tuple. Lazy__getattribute__and_resolve_field_valuereturn its.value;ObjectStateunpacks all three as before.resolve_field_inheritance(the merged-config walk) is deleted, along with the "stage 2" merged-config lookup in__getattribute__.The layer stack is the value resolution reads.
context_layer_stacknow holds aContextLayerStackofContextLayer(scope_id, obj, masks_outer_layers):config_contextis a layer, with or without ascope_id(source scopeNonewhen it has none);"";mask_with_none=Truehides the layers outside it.Caches belong to the context (#33).
_mro_resolution_cacheand_extract_all_configs_cacheare deleted. Answers are kept inContextLayerStack.resolved_fields, keyed by(container_type, field_name); each layer keeps its own extracted configs (ContextLayer.configs). A new stack is made on everyconfig_contextentry, so answers are dropped with the stack.config_contextclears nothing.Loud global lookup.
get_base_global_configno longer returnsNoneonImportError; it raises. The same handler is removed from_normalize_typeand_is_global_type.Acceptance criteria
#29
test_attribute_read_and_provenance_agree_when_a_layer_has_no_scope_id: read2, resolver(2, None, WellFilterConfig). Fails on the base branch.build_context_stackcan produce, the attribute read equals the valueObjectStatereportstest_attribute_read_equals_what_object_state_reports(8 arrangements: global alone, ancestor supplying the same type, ancestor supplying a base type only, two ancestors, a global config among the ancestors, the object's own value, saved global differing from live, nothing supplying a value) compares reads insidebuild_context_stackwithObjectState.resolve_saved_object, values and provenance...._for_a_global_configcovers the masked global arrangement.grep -n "__mro__" src/objectstate/dual_axis_resolver.py: the layer walk is_resolve_in_layersonly (called byresolve_with_provenance/resolve_in_stack);declared_defaultwalks the MRO for class defaults, with no scopes.#33
config_contextcontains no cache-clearing callsgrep -n "clear_.*cache" src/objectstate/context_manager.py src/objectstate/dual_axis_resolver.pyreturns nothing.test_answers_survive_another_thread_entering_and_leaving_a_context: the second resolve returns the identicalResolvedFieldobject.id(...)grep -n "id(" src/objectstate/dual_axis_resolver.py src/objectstate/context_manager.pyreturns nothing.test_answers_are_dropped_with_the_context_they_were_computed_fromshows the stack is collected after exit.Paper results
Behaviour changes for callers (pyqt-reactive, OpenHCS)
resolve_field_inheritance,clear_mro_resolution_cache,invalidate_mro_cache_for_field,clear_extract_all_configs_cache,set_current_temp_globaland the_has_concrete_field_overrideexport are removed.resolve_with_provenanceandResolvedFieldare exported fromobjectstate.get_context_layer_stack()/context_layer_stack.get()return aContextLayerStack(.layersofContextLayer), not a tuple of(scope_id, obj). The stack now includes unscoped layers and the root global layer.resolve_with_provenanceraisesNoActiveContextErrorwith no active context (it returned(None, None, None)).ObjectState). A layer object holding two instances of one config type now raises on a read, as it already did forObjectState.ObjectStateresolved values now include inherited declared defaults where a read returned them (it reportedNonebefore). Provenance tuples are unchanged.build_context_stackpassesuse_liveto every layer. Before,use_live=Falsemerged the saved global over the live one, so reads could see live values where the saved one wasNone. It no longer enters the global config a second time; when called inside an active context it no longer pushes the global config on top of it.clear_current_temp_global()also empties the layer stack.extract_all_configsreads stored field values (no lazy resolution of the object being read) and is uncached._resolve_field_valueno longer skips the same-type lookup when the instance value is notNone.ObjectStatewithout a scope id now has its own object as a layer, so its fields inherit from sibling configs of the same object (see the OpenHCS test below).mask_with_nonehides all outer layers for resolution.Files outside the named set
object_state_registry.py: removed the three references to the deleted cache functions (8 lines).__init__.py: exports. Docs:docs/provenance.rst,src/objectstate/docs/context_system.rst.Performance
Benchmark: 8 lazy field reads (same type, via MRO, three layers) on CPython 3.12, best of 5, lowest of three alternating runs on a loaded machine (run-to-run noise is about 20%).
The only added cache is
_declared_config_fields, keyed by the dataclass type it describes.Tests
ruff check src/ --select E9,F63,F7,F82passes;scripts/check_ruff_added_lines.pyagainst the base branch reports no diagnostics (local ruff 0.16.9; CI pins 0.16.0).tests/unitwith this branch asexternal/ObjectState(first commit of this PR, same resolver code): 11565 passed, 62 failed. The 52 failures outside the MCP-stdio and ack-route tests were rerun together on the base branch and on this branch: 5 fail on both, 46 pass on both, and one fails only here:tests/unit/pyqt_gui/test_source_bindings_editor.py::test_inline_source_bindings_dropdown_edit_flashes_only_changed_cell. It buildsObjectState(PipelineConfig())with no scope id. On the base branch the state's own object was not a layer forObjectState(no scope id), sostep_source_bindings_config.source_filtersstayed()from the global config while an attribute read returned the pipeline'ssource_bindings_config.source_filtersthrough the MRO. Now both return the pipeline value, so the edit also flashes the inheriting step field. This is the Attribute reads and displayed provenance can resolve the same field to different values #29 divergence; the OpenHCS test needs the second path added.The 10 MCP-stdio and ack-route failures were not rerun on the base branch.
🤖 Generated with Claude Code