From 685955b1ab6d46a9b6582635289681e7dc054d2a Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Mon, 12 Oct 2026 03:12:59 -0400 Subject: [PATCH 1/6] OS3: receipt for the ObjectState 3.0.0 caller update Co-Authored-By: Claude Opus 5.5 --- .../consolidation-20261009/OS3-callers.md | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 docs/refactor/consolidation-20261009/OS3-callers.md diff --git a/docs/refactor/consolidation-20261009/OS3-callers.md b/docs/refactor/consolidation-20261009/OS3-callers.md new file mode 100644 index 000000000..c69b3215e --- /dev/null +++ b/docs/refactor/consolidation-20261009/OS3-callers.md @@ -0,0 +1,37 @@ +# OS3: callers of ObjectState 3.0.0 + +Base: `origin/refactor/U6-gui-integration` (PR #1219), not `main`. +Task list: the 3.0.0 caller ledger (`OS-3.0-caller-ledger.md`, kept with the +integration notes) and the release PR OpenHCSDev/ObjectState#50, head `abe763c`. + +## What is wrong + +ObjectState 3.0.0 removes public names and makes one resolver answer every +read. Its callers still use the removed names: + +| Caller | Sites that break | Where | +|---|---|---| +| formbridge 0.1.1 | 3 | `strategies/preview_formatting.py` walks `GROUP_ABBREVIATIONS_REGISTRY`; version bound | +| pyqt-reactive 0.7.0 | 10 | `services/widget_service.py` calls `build_context_stack(ancestor_objects=...)`; bound; CI candidates; 7 test lines | +| pycodify | 0 | | +| OpenHCS | 10 | version bound; 9 test lines | + +The ledger's "no hits" entries were re-checked with one grep over all four +callers for every removed name: no site beyond the ledger. + +## The rule for config reads + +A config value has three reads. Each site says which one it needs. + +- Raw: only what was set on this object (`state.parameters[path]`, saved `.py`). +- Resolved: the effective value after inheritance (`state.get_resolved_value(path)`, + or attribute access on a lazy object inside its context). +- Widget preview: the resolved value together with its source + (`state.get_provenance(path)`), while the field's own value stays unset. + +Both state reads come from the one resolution `resolve_with_provenance`, which +returns `ResolvedField(value, source_scope, source_type)`. + +## Done / next + +See the end of this file. From 543c56eb8f6632aca4664657e2f44d0fab11b57f Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Mon, 12 Oct 2026 03:14:09 -0400 Subject: [PATCH 2/6] WIP OS3: ObjectState 3 pins, submodules and test call sites Co-Authored-By: Claude Opus 5.5 --- external/ObjectState | 2 +- external/formbridge | 2 +- external/pyqt-reactive | 2 +- pyproject.toml | 6 ++--- ...est_parameter_form_manager_cross_window.py | 1 - .../pyqt_gui/test_source_bindings_editor.py | 27 +++++++++++++++---- tests/unit/pyqt_gui/test_ui_agent_bridge.py | 3 --- tests/unit/test_time_travel_navigation.py | 3 --- 8 files changed, 28 insertions(+), 18 deletions(-) diff --git a/external/ObjectState b/external/ObjectState index 1fcdf1297..abe763cbf 160000 --- a/external/ObjectState +++ b/external/ObjectState @@ -1 +1 @@ -Subproject commit 1fcdf1297f98217eba76f2e23423fcb0e488dc4a +Subproject commit abe763cbf58cbfbaa85649b1ef394a12778daca5 diff --git a/external/formbridge b/external/formbridge index d39cb6ee7..1e73e6c9f 160000 --- a/external/formbridge +++ b/external/formbridge @@ -1 +1 @@ -Subproject commit d39cb6ee7dedbc22d4f6f502cd109092b2d30ac0 +Subproject commit 1e73e6c9fbab365616516928731c86a8d435e2d3 diff --git a/external/pyqt-reactive b/external/pyqt-reactive index 4c35bb5ac..1fa382e8d 160000 --- a/external/pyqt-reactive +++ b/external/pyqt-reactive @@ -1 +1 @@ -Subproject commit 4c35bb5acc9eaf1a10be2b73720cc9a51145fb06 +Subproject commit 1fa382e8d335aedb0fa78479842708d1762e8631 diff --git a/pyproject.toml b/pyproject.toml index bdaf69303..dc7c7e969 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -101,13 +101,13 @@ dependencies = [ # External dependencies (PyPI versions for production builds) "zmqruntime>=0.6.2,<0.7", "pycodify>=0.2.0,<0.3", - "objectstate>=2.1.1,<3", + "objectstate>=3.0,<4", "python-introspect>=0.2.8,<0.3", "metaclass-registry>=0.3.0,<0.4", "arraybridge>=0.4.1,<0.5", "polystore>=0.6.1,<0.7", - "formbridge>=0.1.1,<0.2", - "pyqt-reactive>=0.7,<0.8", + "formbridge>=0.1.2,<0.2", + "pyqt-reactive>=0.7.1,<0.8", ] [project.optional-dependencies] diff --git a/tests/unit/pyqt_gui/test_parameter_form_manager_cross_window.py b/tests/unit/pyqt_gui/test_parameter_form_manager_cross_window.py index 7b85cac57..75f6581f4 100644 --- a/tests/unit/pyqt_gui/test_parameter_form_manager_cross_window.py +++ b/tests/unit/pyqt_gui/test_parameter_form_manager_cross_window.py @@ -175,7 +175,6 @@ def record_invalidation(cls, **kwargs): classmethod(record_invalidation), ) ObjectStateRegistry._deferred_invalidations.clear() - ObjectStateRegistry._deferred_invalidation_depth = 0 with ObjectStateRegistry.defer_live_invalidations(): ObjectStateRegistry.invalidate_by_type_and_scope("", str, "name") diff --git a/tests/unit/pyqt_gui/test_source_bindings_editor.py b/tests/unit/pyqt_gui/test_source_bindings_editor.py index 8e448761c..9986b287d 100644 --- a/tests/unit/pyqt_gui/test_source_bindings_editor.py +++ b/tests/unit/pyqt_gui/test_source_bindings_editor.py @@ -3127,7 +3127,7 @@ def test_inline_source_bindings_edit_flashes_changed_collection() -> None: manager.deleteLater() -def test_inline_source_bindings_dropdown_edit_flashes_only_changed_cell() -> None: +def test_inline_source_bindings_dropdown_edit_flashes_changed_cell_and_inheriting_cell() -> None: QtApplicationHarness.app() state = ObjectState(PipelineConfig()) manager = ParameterFormManager( @@ -3173,16 +3173,33 @@ def test_inline_source_bindings_dropdown_edit_flashes_only_changed_cell() -> Non ) QApplication.processEvents() - expected_path = "source_bindings_config.source_filters[0].match_type" - assert [entry[0] for entry in registered] == [expected_path] - assert queued == [expected_path] - assert state.parameters["source_bindings_config.source_filters"] == ( + # The edited cell, and the same cell of the step section: that section + # sets no filters of its own and shows the ones it inherits from + # source_bindings_config of the same PipelineConfig, so the value it + # resolves to changes with the edit while its own value stays unset. + edited_clauses = ( SourceFilterClause( SourceFileName, TextContains, "DNA", ), ) + expected_paths = [ + "source_bindings_config.source_filters[0].match_type", + "step_source_bindings_config.source_filters[0].match_type", + ] + assert [entry[0] for entry in registered] == expected_paths + assert queued == expected_paths + assert state.parameters["source_bindings_config.source_filters"] == edited_clauses + assert state.parameters["step_source_bindings_config.source_filters"] is None + assert ( + state.get_resolved_value("step_source_bindings_config.source_filters") + == edited_clauses + ) + assert state.get_provenance("step_source_bindings_config.source_filters") == ( + state.scope_id, + SourceBindingsConfig, + ) queued.clear() registered.clear() combo = widget.table("source_filters").cellWidget( diff --git a/tests/unit/pyqt_gui/test_ui_agent_bridge.py b/tests/unit/pyqt_gui/test_ui_agent_bridge.py index a930690b1..04f65f2f4 100644 --- a/tests/unit/pyqt_gui/test_ui_agent_bridge.py +++ b/tests/unit/pyqt_gui/test_ui_agent_bridge.py @@ -3735,9 +3735,6 @@ def reset_object_state_registry(): ObjectStateRegistry._current_timeline = "main" ObjectStateRegistry._current_head = None ObjectStateRegistry._in_time_travel = False - ObjectStateRegistry._atomic_depth = 0 - ObjectStateRegistry._atomic_label = None - ObjectStateRegistry._atomic_triggering_scope = None ObjectStateRegistry._token = 0 yield BridgeSessions.close() diff --git a/tests/unit/test_time_travel_navigation.py b/tests/unit/test_time_travel_navigation.py index d213f0105..cc225cd84 100644 --- a/tests/unit/test_time_travel_navigation.py +++ b/tests/unit/test_time_travel_navigation.py @@ -26,9 +26,6 @@ def _reset_registry() -> None: ObjectStateRegistry._current_timeline = "main" ObjectStateRegistry._current_head = None ObjectStateRegistry._in_time_travel = False - ObjectStateRegistry._atomic_depth = 0 - ObjectStateRegistry._atomic_label = None - ObjectStateRegistry._atomic_triggering_scope = None def test_time_travel_navigation_filters_unrelated_dirty_scopes() -> None: From 861329e616e08fcb0a025ff7ba974a8c218ec526 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Mon, 12 Oct 2026 03:34:32 -0400 Subject: [PATCH 3/6] WIP OS3: receipt with sites, flagged-test verdict and resolved-value probes Co-Authored-By: Claude Opus 5.5 --- .../consolidation-20261009/OS3-callers.md | 122 ++++++++++++++++++ 1 file changed, 122 insertions(+) diff --git a/docs/refactor/consolidation-20261009/OS3-callers.md b/docs/refactor/consolidation-20261009/OS3-callers.md index c69b3215e..a2b111274 100644 --- a/docs/refactor/consolidation-20261009/OS3-callers.md +++ b/docs/refactor/consolidation-20261009/OS3-callers.md @@ -32,6 +32,128 @@ A config value has three reads. Each site says which one it needs. Both state reads come from the one resolution `resolve_with_provenance`, which returns `ResolvedField(value, source_scope, source_type)`. +## Target + +Every caller reads through the one owner of each fact, and the three +repositories release against ObjectState 3.0.0 together. + +### formbridge 0.1.2 (OpenHCSDev/formbridge#9, `1e73e6c`) + +- `strategies/preview_formatting.py`: the module's own walk of + `GROUP_ABBREVIATIONS_REGISTRY` is deleted. `FormattingConfig.container_abbr_func` + defaults to `objectstate.lazy_factory.get_group_abbreviation`. + Certificate **derived**: ObjectState's function does the lazy-to-base step, + the MRO walk and the class-name fallback that the deleted copy did. +- `objectstate>=3.0,<4`. New `requirements-ci.txt` (pyqt-reactive's convention) + installs ObjectState at `abe763c` in CI until 3.0.0 is published. + +### pyqt-reactive 0.7.1 (OpenHCSDev/PyQT-reactive#28, `1fa382e`) + +0.7.1 and not 0.8.0: no public name is removed. + +- `services/widget_service.py` (preview read). The removed + `build_context_stack(ancestor_objects=...)` call is not translated; it is + deleted. The site rebuilt a context stack by hand, asked for placeholder + text, then read the attribute again for the value: a second resolver beside + the state. `WidgetService.show_resolved_value_as_placeholder` shows + `state.get_resolved_value(path)`; the two refresh paths in + `ParameterOpsService`, which each repeated resolve-and-format, call it too. + Certificate **derived** (the state's resolved read). One visible change: the + value-update path now uses the form's `placeholder_prefix`; it used the + default prefix. +- `widgets/shared/clickable_help_components.py` (source read). The ledger + marked this "review: the same-window branch already handles + `(None, source_type)`". It did not: the target path was computed first from + `ObjectStateRegistry.get_by_scope(None)`, which is the global state. The + source state is now the widget's own state when the source scope is its own + scope. `get_provenance` is read once per action; the tuple-shape re-check and + the blanket `except` are deleted (certificate **contract**: the return type + is `tuple[str | None, type] | None`). +- `widgets/shared/abstract_manager_widget.py`: unused `LiveContextResolver` + instance deleted (certificate **unused**). +- Bounds `objectstate>=3.0,<4`, `formbridge>=0.1.2,<0.2`; `requirements-ci.txt` + names both branch commits. +- Tests: `@abbreviation` instead of writes to the removed registry; removed + `_atomic_*` assignments deleted; new `tests/test_provenance_navigation.py`. + +The supply map (`--focus abbreviation`, `--focus WidgetService`) reported no +second provider, copy or forwarder for either class. It sees classes, not +functions, so the three placeholder copies were found by reading. + +### OpenHCS + +- `pyproject.toml`: `objectstate>=3.0,<4`, `formbridge>=0.1.2,<0.2`, + `pyqt-reactive>=0.7.1,<0.8`. Submodules at `abe763c`, `1e73e6c`, `1fa382e`. +- `tests/unit/test_time_travel_navigation.py`, + `tests/unit/pyqt_gui/test_ui_agent_bridge.py`, + `tests/unit/pyqt_gui/test_parameter_form_manager_cross_window.py`: assignments + to the removed block-state attributes deleted. +- No production file in `openhcs/`, `benchmark/` or `scripts/` changes. + +### The flagged test + +`test_inline_source_bindings_dropdown_edit_flashes_only_changed_cell` is now +`..._flashes_changed_cell_and_inheriting_cell` and expects two paths. +The 3.0.0 author's claim holds. Observed with `.claude` probes on this branch: + +| Field | Raw | Resolved before the edit | Resolved after | Source | +|---|---|---|---|---| +| `source_bindings_config.source_filters` | the clause | `TextEquals` clause | `TextContains` clause | set here | +| `step_source_bindings_config.source_filters` | `None` | `TextEquals` clause | `TextContains` clause | `SourceBindingsConfig` of the same object | + +`StepSourceBindingsConfig` inherits `SourceBindingsConfig`, and the step section +sets no filters of its own, so the cell it previews changes value with the +edit. A flash marks a field whose resolved value changed, so both cells flash. +The result is the same with `scope_id="/plate"` and with none, and an attribute +read on the object gives the same value. The old expectation held only because +a state without a scope id did not see its own object as a layer. The test now +also asserts the step section's raw value stays `None`, its resolved value is +the edited clause, and its source is `SourceBindingsConfig`. + +## New-case experiments + +Resolved values, ObjectState 2.1.1 (U6 base) against 3.0.0 (this branch): + +- Default `PipelineConfig` (with and without scope id), `GlobalPipelineConfig` + and a `FunctionStep` state: 534 paths, no value differs. +- For every field name held by several configs of `PipelineConfig`, set it on + one config and list which other fields change: with a scope id the two + versions agree on all 30 probes. Without a scope id, 3.0.0 now agrees with + the scoped result; 2.1.1 missed 8 sibling inheritances + (`streaming_defaults.{enabled,host,listen_host,persistent}` to the napari and + fiji streaming configs, `path_planning_config.output_dir_suffix` to + `step_materialization_config`, `napari_display_config.colormap`, + `fiji_display_config.{auto_contrast,lut}` to their streaming configs). +- Production code builds a state without a scope id in two places, neither on + a pipeline path: `image_browser.NoDataset.config_state` (one viewer config, + nothing loaded) and the synthetic plate generator window. Compiled pipelines + use scoped states. + +## Tests + +PLACEHOLDER_TESTS + +## Done when + +The three PRs are open, each suite and lint passes, guardrails and smoke end +`rc=0`, and parity is 30/30. + +Release order: ObjectState 3.0.0, formbridge 0.1.2, pyqt-reactive 0.7.1 +(empty its `requirements-ci.txt` first, as formbridge's), then OpenHCS with the +submodules moved to the release tags. + +## Out of scope, found + +- `formbridge.forms.parameter_form_service.ParameterFormService.get_placeholder_text` + has no caller after the pyqt-reactive change (formbridge; public method, left). +- `f"{manager.field_id}.{name}" if manager.field_id else name` is written out + in 7 pyqt-reactive modules; the form manager should own the field's path. +- `tests/unit/test_artifact_publication_journey.py` (8), + `test_named_checkpoint_publication.py` (2) and + `test_bioformats_microscope_handler.py` (1) fail in the 108-file selection + below on the U6 base with ObjectState 2.1.1 as well; all but the last pass + when their files run alone. + ## Done / next See the end of this file. From 23f4c83296b2ee638b05aff5dcec14a82019e071 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Mon, 12 Oct 2026 04:05:10 -0400 Subject: [PATCH 4/6] OS3: receipt with observed checks Co-Authored-By: Claude Opus 5.5 --- .../consolidation-20261009/OS3-callers.md | 26 +++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/docs/refactor/consolidation-20261009/OS3-callers.md b/docs/refactor/consolidation-20261009/OS3-callers.md index a2b111274..d82cf885e 100644 --- a/docs/refactor/consolidation-20261009/OS3-callers.md +++ b/docs/refactor/consolidation-20261009/OS3-callers.md @@ -131,7 +131,23 @@ Resolved values, ObjectState 2.1.1 (U6 base) against 3.0.0 (this branch): ## Tests -PLACEHOLDER_TESTS +Observed locally, CPython 3.12, the three submodules at the commits above. + +| Check | Result | +|---|---| +| formbridge `pytest tests -n 2` | 107 passed | +| formbridge `ruff check src tests` | clean | +| pyqt-reactive `pytest tests -n 2` | 593 passed, 1 failed (`test_published_runtime_is_not_overridden_by_a_source_candidate`: compares the installed `zmqruntime` with its bound; fails the same way at 0.7.0) | +| pyqt-reactive `ruff check src/ --select E9,F63,F7,F822,F823`, `check_ruff_added_lines.py` | clean | +| OpenHCS `tests/unit/pyqt_gui -n 2` | 899 passed | +| OpenHCS 108 unit files that name ObjectState, lazy configs or `PipelineConfig`, with `test_validate_local_release_floors.py` and `agent/test_mcp_tool_contract.py` | 2388 passed, 11 failed, 3 skipped; the same 11 fail on the U6 base with ObjectState 2.1.1 (listed under "Out of scope") | +| `test_compilation_session.py` (ledger item 9) | passed in that run | +| `guardrails_local.sh` | `guardrails rc=0` (at `543c56eb8`) | +| `SMOKE_SKIP_GUI=1 smoke_local.sh` | `smoke rc=0`, 5 passed (at `543c56eb8`) | +| official30 parity, run 1 | 28/30 equivalent, not a valid run: `cp_tutorial_pixel_based_classification` was compiled by an execution server of another worktree (`agent-a05d025a19446fcd4`, `cannot import name 'LazyAnalysisConsolidationConfig'` from that worktree's file) and `cp_tutorial_translocation_final` was skipped after it | +| official30 parity, run 2 | PARITY2 | + +MCP tool contract: `test_mcp_tool_contract.py` passes with no schema file changed. ## Done when @@ -156,4 +172,10 @@ submodules moved to the release tags. ## Done / next -See the end of this file. +Done: the three library and application changes, the three PRs, every check +in the table above. + +Next: a clean parity run. Run 2 is queued on `official30.lock` +(`.claude/parity.sh` in the worktree; result in `.claude/logs/parity.done` and +`parity.txt`). Then move the submodules to the release tags once the libraries +are published. From 3dc46bdaf3b96dce51a72b34584fd45986e747e4 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Mon, 12 Oct 2026 04:23:40 -0400 Subject: [PATCH 5/6] OS3: receipt with both parity runs Co-Authored-By: Claude Opus 5.5 --- docs/refactor/consolidation-20261009/OS3-callers.md | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/docs/refactor/consolidation-20261009/OS3-callers.md b/docs/refactor/consolidation-20261009/OS3-callers.md index d82cf885e..9f7625844 100644 --- a/docs/refactor/consolidation-20261009/OS3-callers.md +++ b/docs/refactor/consolidation-20261009/OS3-callers.md @@ -145,7 +145,7 @@ Observed locally, CPython 3.12, the three submodules at the commits above. | `guardrails_local.sh` | `guardrails rc=0` (at `543c56eb8`) | | `SMOKE_SKIP_GUI=1 smoke_local.sh` | `smoke rc=0`, 5 passed (at `543c56eb8`) | | official30 parity, run 1 | 28/30 equivalent, not a valid run: `cp_tutorial_pixel_based_classification` was compiled by an execution server of another worktree (`agent-a05d025a19446fcd4`, `cannot import name 'LazyAnalysisConsolidationConfig'` from that worktree's file) and `cp_tutorial_translocation_final` was skipped after it | -| official30 parity, run 2 | PARITY2 | +| official30 parity, run 2 | 29/30 equivalent, not a clean run either: `ExampleYeastColonies` ended with `OpenHCS ZMQ execution failed: Execution was cancelled` (nothing in this branch cancels; other agents' runs were active on the machine). It was equivalent in run 1, and the two cases that failed in run 1 are equivalent here, so every case was equivalent in one of the two runs and no value mismatch was seen in either | MCP tool contract: `test_mcp_tool_contract.py` passes with no schema file changed. @@ -175,7 +175,6 @@ submodules moved to the release tags. Done: the three library and application changes, the three PRs, every check in the table above. -Next: a clean parity run. Run 2 is queued on `official30.lock` -(`.claude/parity.sh` in the worktree; result in `.claude/logs/parity.done` and -`parity.txt`). Then move the submodules to the release tags once the libraries -are published. +Next: one clean 30/30 parity run on a quiet machine (`.claude/parity.sh` in the +worktree, under `official30.lock`). Then move the submodules to the release +tags once the libraries are published. From d9898db191dcbb34ec4b09c68c82ce95f29c6c51 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Mon, 12 Oct 2026 04:29:47 -0400 Subject: [PATCH 6/6] OS3: pin released objectstate 3.0.0, formbridge 0.1.2, pyqt-reactive 0.7.1 Co-Authored-By: Claude Opus 5.5 --- external/ObjectState | 2 +- external/formbridge | 2 +- external/pyqt-reactive | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/external/ObjectState b/external/ObjectState index abe763cbf..cf597be09 160000 --- a/external/ObjectState +++ b/external/ObjectState @@ -1 +1 @@ -Subproject commit abe763cbf58cbfbaa85649b1ef394a12778daca5 +Subproject commit cf597be09d392490be076384d27a0112480a872d diff --git a/external/formbridge b/external/formbridge index 1e73e6c9f..d7afc3583 160000 --- a/external/formbridge +++ b/external/formbridge @@ -1 +1 @@ -Subproject commit 1e73e6c9fbab365616516928731c86a8d435e2d3 +Subproject commit d7afc3583fa63707b717cb06cc6a9908032ea623 diff --git a/external/pyqt-reactive b/external/pyqt-reactive index 1fa382e8d..a408a22b2 160000 --- a/external/pyqt-reactive +++ b/external/pyqt-reactive @@ -1 +1 @@ -Subproject commit 1fa382e8d335aedb0fa78479842708d1762e8631 +Subproject commit a408a22b2b344835f772d9254a88c0370bfd1aa8