diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml
new file mode 100644
index 000000000..70b9b2459
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/bug_report.yml
@@ -0,0 +1,62 @@
+name: Bug report
+description: Report a reproducible OpenHCS failure
+title: "[Bug]: "
+labels:
+ - bug
+body:
+ - type: markdown
+ attributes:
+ value: |
+ Thanks for helping improve OpenHCS. Do not include patient data, credentials, private plate paths, or proprietary images.
+ - type: input
+ id: version
+ attributes:
+ label: OpenHCS version
+ description: Copy the version from About, `python -m openhcs --version`, or package metadata.
+ validations:
+ required: true
+ - type: dropdown
+ id: install
+ attributes:
+ label: Installation route
+ options:
+ - Windows installer
+ - macOS installer
+ - PyPI
+ - Source checkout
+ - Other
+ validations:
+ required: true
+ - type: textarea
+ id: environment
+ attributes:
+ label: Environment
+ description: Operating system, Python version if applicable, CPU/GPU, viewer, and MCP client/surface if involved.
+ validations:
+ required: true
+ - type: textarea
+ id: reproduce
+ attributes:
+ label: Minimal reproduction
+ description: Numbered steps, with a synthetic or redacted example when data is involved.
+ validations:
+ required: true
+ - type: textarea
+ id: expected
+ attributes:
+ label: Expected and actual behavior
+ validations:
+ required: true
+ - type: textarea
+ id: logs
+ attributes:
+ label: Logs or screenshots
+ description: Paste the smallest relevant excerpt and redact sensitive paths or data.
+ render: shell
+ - type: checkboxes
+ id: sensitive
+ attributes:
+ label: Data safety
+ options:
+ - label: I removed credentials, patient data, private paths, and proprietary image content.
+ required: true
diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml
new file mode 100644
index 000000000..e5395baf0
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/config.yml
@@ -0,0 +1,8 @@
+blank_issues_enabled: true
+contact_links:
+ - name: Security report
+ url: mailto:tristan.simas@mail.mcgill.ca
+ about: Report vulnerabilities privately rather than opening an issue.
+ - name: Documentation
+ url: https://openhcs.readthedocs.io/
+ about: Check installation, architecture, and MCP client guidance.
diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md
new file mode 100644
index 000000000..f6f8594ec
--- /dev/null
+++ b/.github/PULL_REQUEST_TEMPLATE.md
@@ -0,0 +1,16 @@
+## What changed
+
+Describe the user-visible outcome and the declaration or typed owner changed.
+
+## Evidence
+
+- [ ] Added or updated a focused regression
+- [ ] Ran the relevant local tests
+- [ ] Preserved declaration ownership; no mirrored registry or metadata source
+- [ ] Documented platform, GPU, viewer, or installer journeys not run locally
+
+Commands and results:
+
+```text
+
+```
diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml
index e5d117188..d1b6c2165 100644
--- a/.github/workflows/integration-tests.yml
+++ b/.github/workflows/integration-tests.yml
@@ -533,6 +533,8 @@ jobs:
run: git config --global core.longpaths true
- uses: actions/checkout@v4
+ with:
+ submodules: ${{ env.OPENHCS_CI_DEP_SOURCE == 'submodules' && 'recursive' || 'false' }}
- name: Set up Python 3.12
uses: actions/setup-python@v5
@@ -557,6 +559,9 @@ jobs:
python -m pip install build packaging pytest
python -m pytest -q -c tests/installer/pytest.ini tests/installer
+ - name: Build installer source wheelhouse
+ run: python -m scripts.build_installer_source_wheelhouse --output dist --dependency-source "${{ env.OPENHCS_CI_DEP_SOURCE }}"
+
- name: Parse Windows PowerShell installer sources
if: matrix.platform == 'windows'
shell: pwsh
@@ -574,8 +579,7 @@ jobs:
shell: pwsh
timeout-minutes: 30
run: |
- python -m build --wheel
- $releaseVersion = python -c "from pathlib import Path; from packaging.utils import parse_wheel_filename; print(parse_wheel_filename(next(Path('dist').glob('*.whl')).name)[1])"
+ $releaseVersion = python -c "from pathlib import Path; from packaging.utils import parse_wheel_filename; versions=[parse_wheel_filename(path.name)[1] for path in Path('dist').glob('*.whl') if str(parse_wheel_filename(path.name)[0])=='openhcs']; assert len(versions)==1, versions; print(versions[0])"
$releaseVersion = $releaseVersion.Trim()
$stage = Join-Path $env:RUNNER_TEMP "OpenHCS-Windows-Smoke"
$installRoot = Join-Path $env:RUNNER_TEMP "OpenHCS Installed"
@@ -828,8 +832,7 @@ jobs:
- name: Execute and verify macOS installer
if: matrix.platform == 'macos'
run: |
- python -m build --wheel
- release_version=$(python -c "from pathlib import Path; from packaging.utils import parse_wheel_filename; print(parse_wheel_filename(next(Path('dist').glob('*.whl')).name)[1])")
+ release_version=$(python -c "from pathlib import Path; from packaging.utils import parse_wheel_filename; versions=[parse_wheel_filename(path.name)[1] for path in Path('dist').glob('*.whl') if str(parse_wheel_filename(path.name)[0])=='openhcs']; assert len(versions)==1, versions; print(versions[0])")
staged_contract="$RUNNER_TEMP/installer_contract.json"
installer_app="$RUNNER_TEMP/OpenHCS Release Installer.app"
smoke_home="$RUNNER_TEMP/openhcs-installer-home"
diff --git a/.gitignore b/.gitignore
index a7daeb9df..0739b57d6 100644
--- a/.gitignore
+++ b/.gitignore
@@ -180,6 +180,10 @@ benchmark/results/napari_streaming_validator/
/*.md
!/README.md
!/CHANGELOG.md
+!/CITATION.cff
+!/CODE_OF_CONDUCT.md
+!/CONTRIBUTING.md
+!/SECURITY.md
# Chunkhound
.chunkhound/
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 5a0549935..3dad8bfe9 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,6 +7,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
+### Changed
+
+- Removed the dataset-specific BBBC021 and BBBC038 microscope choices. BBBC
+ datasets now use their declared source bindings, while benchmark dataset IDs
+ remain available as provenance.
+- Renamed Napari component placement from ``slice`` to ``layer`` so display
+ configuration now distinguishes separate viewer layers from genuine image
+ slices; Fiji slice placement is unchanged.
+
### Added
#### Auto-Add Output Plate Feature
@@ -41,6 +50,91 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Added `requests>=2.31.0` dependency for HTTP communication
- Comprehensive test suite for LLM service and chat panel
+## [0.7.11] - 2026-08-01
+
+### Fixed
+
+- Windows multiprocessing now preserves the installed interpreter used to
+ launch OpenHCS instead of resolving to an unrelated Python executable.
+- Desktop monitoring presents the canonical brand and exposes its typed
+ settings without clipped information text.
+
+## [0.7.10] - 2026-08-01
+
+### Fixed
+
+- Execution progress registration now routes through the control authority so
+ UI and agent projections observe the same lifecycle.
+
+## [0.7.9] - 2026-08-01
+
+### Changed
+
+- Configuration layouts now reflow responsively while preserving readable
+ field and help surfaces.
+
+## [0.7.8] - 2026-07-31
+
+### Fixed
+
+- Release builds resolve versions for independently published dependencies
+ from their authoritative package declarations.
+
+## [0.7.7] - 2026-07-31
+
+### Changed
+
+- Native desktop updates and their recovery/presentation lifecycle were
+ hardened across supported platforms.
+
+## [0.7.6] - 2026-07-31
+
+### Fixed
+
+- Dock widgets correctly return to the managed workspace and branding refresh
+ no longer disturbs the native layout.
+
+## [0.7.5] - 2026-07-31
+
+### Changed
+
+- Desktop startup, docked workspace presentation, and CI runner path handling
+ were polished after the initial gallery release.
+
+## [0.7.4] - 2026-07-31
+
+### Added
+
+- A verified workflow media gallery and capture tooling that rejects fabricated
+ UI state.
+- Unit/core CI gates, typed viewer controls, agent-visible viewer authority,
+ UI recovery receipts, and hardened bridge endpoint identity.
+
+### Changed
+
+- The main workspace uses native dock and control theming, preserves geometry,
+ and suppresses consoles for background GUI processes.
+- ObjectState 1.1.1 provides transactional typed configuration and edit-history
+ recovery.
+
+## [0.7.3] - 2026-07-30
+
+### Fixed
+
+- The official logo family is used consistently across release and desktop
+ surfaces.
+
+## [0.7.2] - 2026-07-30
+
+### Added
+
+- Canonical brand assets for the GUI, launcher, installer, website, and package.
+
+### Changed
+
+- Windows GUI launchers suppress the console and the public site describes the
+ current beta without stale patch-level launch claims.
+
## [0.7.1] - 2026-07-30
### Added
@@ -221,4 +315,14 @@ See git history for changes in versions 0.3.14 and earlier.
[0.4.0]: https://github.com/trissim/openhcs/compare/v0.3.15...v0.4.0
[0.3.15]: https://github.com/trissim/openhcs/releases/tag/v0.3.15
+[0.7.11]: https://github.com/OpenHCSDev/OpenHCS/compare/v0.7.10...v0.7.11
+[0.7.10]: https://github.com/OpenHCSDev/OpenHCS/compare/v0.7.9...v0.7.10
+[0.7.9]: https://github.com/OpenHCSDev/OpenHCS/compare/v0.7.8...v0.7.9
+[0.7.8]: https://github.com/OpenHCSDev/OpenHCS/compare/v0.7.7...v0.7.8
+[0.7.7]: https://github.com/OpenHCSDev/OpenHCS/compare/v0.7.6...v0.7.7
+[0.7.6]: https://github.com/OpenHCSDev/OpenHCS/compare/v0.7.5...v0.7.6
+[0.7.5]: https://github.com/OpenHCSDev/OpenHCS/compare/v0.7.4...v0.7.5
+[0.7.4]: https://github.com/OpenHCSDev/OpenHCS/compare/v0.7.3...v0.7.4
+[0.7.3]: https://github.com/OpenHCSDev/OpenHCS/compare/v0.7.2...v0.7.3
+[0.7.2]: https://github.com/OpenHCSDev/OpenHCS/compare/v0.7.1...v0.7.2
[0.7.0]: https://github.com/OpenHCSDev/OpenHCS/compare/v0.6.17...v0.7.0
diff --git a/CITATION.cff b/CITATION.cff
new file mode 100644
index 000000000..268253638
--- /dev/null
+++ b/CITATION.cff
@@ -0,0 +1,17 @@
+cff-version: 1.2.0
+message: "If you use OpenHCS, cite the exact release and archive identifier used in your analysis."
+title: "OpenHCS: typed high-content microscopy workflows"
+type: software
+authors:
+ - family-names: Simas
+ given-names: Tristan
+ email: tristan.simas@mail.mcgill.ca
+repository-code: "https://github.com/OpenHCSDev/OpenHCS"
+url: "https://openhcs.readthedocs.io/"
+license: MIT
+keywords:
+ - high-content screening
+ - bioimage analysis
+ - microscopy
+ - image processing
+ - Model Context Protocol
diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md
new file mode 100644
index 000000000..a75bae7af
--- /dev/null
+++ b/CODE_OF_CONDUCT.md
@@ -0,0 +1,23 @@
+# Code of Conduct
+
+OpenHCS is committed to a welcoming, harassment-free community for scientists,
+software contributors, and users of every background and level of experience.
+
+Be respectful and specific. Critique ideas and evidence rather than people;
+assume good faith while accepting correction; protect private research data;
+and make space for people who are new to bioimage analysis or open-source
+development. Harassment, intimidation, discriminatory language, sexualized
+attention, doxxing, and sustained disruption are not acceptable.
+
+Maintainers may edit or remove contributions and temporarily or permanently
+restrict participation when conduct is unsafe or persistently disruptive.
+Enforcement decisions should be proportionate, documented privately, and avoid
+unnecessary disclosure of the reporter's identity.
+
+Report conduct concerns privately to
+[tristan.simas@mail.mcgill.ca](mailto:tristan.simas@mail.mcgill.ca). Reports
+will be reviewed promptly and handled as confidentially as practical. This
+policy applies in project repositories, documentation, community forums, and
+project-related private communications.
+
+This policy is informed by the Contributor Covenant, version 2.1.
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 000000000..36d94a523
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1,69 @@
+# Contributing to OpenHCS
+
+OpenHCS welcomes reproducible bug reports, microscope fixtures, processing
+functions, documentation, and focused code changes.
+
+## Report an issue
+
+Search the issue tracker first, then include the OpenHCS version, installation
+route, operating system, Python version, minimal steps, expected behavior, and
+relevant logs. For microscopy failures, describe the instrument, axes, naming
+pattern, and expected output without uploading sensitive image data.
+
+Security vulnerabilities should be reported privately as described in
+[SECURITY.md](SECURITY.md).
+
+## Development checkout
+
+OpenHCS uses source submodules for its independently published libraries:
+
+```bash
+git clone --depth 1 --recurse-submodules --shallow-submodules \
+ https://github.com/OpenHCSDev/OpenHCS.git
+cd OpenHCS
+python -m venv .venv
+source .venv/bin/activate
+python -m pip install --upgrade pip
+python -m pip install -e "external/python-introspect"
+python -m pip install -e "external/metaclass-registry"
+python -m pip install -e "external/ObjectState"
+python -m pip install -e "external/arraybridge"
+python -m pip install -e "external/pycodify"
+python -m pip install -e "external/PolyStore"
+python -m pip install -e "external/pyqt-reactive"
+python -m pip install -e "external/zmqruntime"
+python -m pip install -e ".[dev,dev-gui,gui,mcp]"
+```
+
+See `docs/development_setup.md` for optional GPU, viewer, Bio-Formats, OMERO,
+and CellProfiler compatibility dependencies.
+
+## Architecture expectations
+
+Read `docs/source/architecture/quick_start.rst`, then
+`system_overview.rst`, `nominal_ownership.rst`, and
+`abstraction_lattices.rst` before a structural change.
+
+- Change the declaration or typed owner of a behavior.
+- Derive schemas, UI projections, registries, and runtime plans from that owner.
+- Do not add mirrored metadata tables, parallel kind registries, sidecars, or
+ compatibility shims when the owning contract can express the rule.
+- Keep compilation deterministic. Workers consume frozen compiled plans rather
+ than reinterpreting mutable authoring objects.
+- Put reusable behavior in the library that owns it; update the OpenHCS
+ submodule pointer only after that library change is independently tested.
+
+## Tests and pull requests
+
+Run the narrowest relevant tests while developing, then the CPU unit gate:
+
+```bash
+OPENHCS_CPU_ONLY=1 python -m pytest tests/unit
+python -m ruff check openhcs tests
+```
+
+GUI tests can use `QT_QPA_PLATFORM=offscreen`. Add a regression that fails for
+the reported behavior and exercises its public or nominal boundary. Keep pull
+requests focused, explain the evidence boundary, and note any platform or GPU
+journey you could not run locally. All contributions must follow
+[CODE_OF_CONDUCT.md](CODE_OF_CONDUCT.md).
diff --git a/README.md b/README.md
index c0765a00e..a68da067a 100644
--- a/README.md
+++ b/README.md
@@ -4,14 +4,15 @@

-**Bioimage analysis platform for high-content screening**\
-**Compile-time validation · Bidirectional GUI↔Code · Multi-GPU · LLM pipeline generation · Extensible function registry**
+**Typed bioimage workflows for high-content screening**\
+**GUI ↔ Python · Compile-first execution · Multi-GPU · Local MCP agents · CellProfiler import**
[](https://pypi.org/project/openhcs/)
[](https://opensource.org/licenses/MIT)
[](https://www.python.org/downloads/)
[](https://github.com/OpenHCSDev/OpenHCS)
[](https://openhcs.readthedocs.io)
+[](https://github.com/OpenHCSDev/OpenHCS/actions/workflows/integration-tests.yml)
@@ -276,6 +277,21 @@ The GUI and execution services consume the same `list[FunctionStep]`,
[API orientation](https://openhcs.readthedocs.io/en/latest/api/) for the explicit
low-level execution call and progress lifecycle.
+### Local agent quick start
+
+The optional MCP server projects those same typed declarations and compiler
+contracts to local agent clients. Start with a bounded, compile-first request:
+
+> Inspect this Opera Phenix plate, infer its axes, and draft a nuclei
+> segmentation plus per-cell intensity pipeline. Compile it and explain every
+> validation result, but do not execute it.
+
+After reviewing the compiled plan, explicitly authorize a small run such as one
+well and one site. Configure read and write roots before connecting a client;
+OpenHCS does not provide a public hosted endpoint. See the
+[MCP client guide](https://openhcs.readthedocs.io/en/latest/user_guide/mcp_clients.html)
+for setup and trust boundaries.
+
📦 All installation options
@@ -399,17 +415,16 @@ A class-level registry tracks all active form managers. When a value changes in
## 🤝 Contributing
-```bash
-git clone --recurse-submodules https://github.com/OpenHCSDev/OpenHCS.git
-cd OpenHCS
-# Install the eight local packages as described in docs/development_setup.md,
-# then install OpenHCS itself:
-python -m pip install -e ".[dev,gui]"
-OPENHCS_CPU_ONLY=1 python -m pytest tests/unit
-```
+Start with [CONTRIBUTING.md](CONTRIBUTING.md). A shallow recursive clone is
+recommended when you do not need the repository's historical media revisions.
**Contribution areas**: microscope formats · processing functions · GPU backends · documentation
+Please use the [issue tracker](https://github.com/OpenHCSDev/OpenHCS/issues) for
+reproducible bugs and workflow requests. See [SECURITY.md](SECURITY.md) for
+private vulnerability reporting and [CITATION.cff](CITATION.cff) for citation
+metadata.
+
---
## 📄 License
diff --git a/SECURITY.md b/SECURITY.md
new file mode 100644
index 000000000..9e2ef6004
--- /dev/null
+++ b/SECURITY.md
@@ -0,0 +1,28 @@
+# Security Policy
+
+## Supported versions
+
+Security fixes are applied to the current `0.7.x` beta line. Older beta lines
+may be asked to upgrade before a fix can be evaluated.
+
+## Reporting a vulnerability
+
+Do not open a public issue for a suspected vulnerability. Email
+[tristan.simas@mail.mcgill.ca](mailto:tristan.simas@mail.mcgill.ca) with the
+affected version, impact, minimal reproduction, and any suggested mitigation.
+Please avoid sending real patient data, credentials, access tokens, or
+proprietary microscopy datasets. You should receive an acknowledgment within
+five business days.
+
+## MCP and desktop trust boundary
+
+The supported MCP server is local and uses explicit read and write roots. Its
+tools can inspect data, author pipelines, start local execution, and write
+outputs within the configured capabilities. Review a compiled plan before
+execution, grant the smallest useful roots, and apply the privacy and retention
+policy of the agent client and model provider you choose. OpenHCS does not run a
+public hosted MCP endpoint.
+
+The current Windows and macOS beta installers are not code-signed or notarized.
+Download them only from the official GitHub Release links and verify that the
+resolved repository owner is `OpenHCSDev`.
diff --git a/benchmark/contracts/dataset.py b/benchmark/contracts/dataset.py
index b1b4d71ee..8baf9a907 100644
--- a/benchmark/contracts/dataset.py
+++ b/benchmark/contracts/dataset.py
@@ -93,7 +93,7 @@ class DatasetSpec:
"""Archive format."""
microscope_type: str
- """Microscope handler type (e.g., 'bbbc021', 'bbbc038')"""
+ """Dataset source-family provenance; not necessarily a runtime handler key."""
validation_rule: DatasetValidationRule
"""How to validate extracted data."""
@@ -137,5 +137,6 @@ class AcquiredDataset:
id: str
path: Path
microscope_type: str
+ """Dataset source-family provenance inherited from the dataset declaration."""
image_count: int
metadata: dict
diff --git a/docs/announcements/image_sc_openhcs_0_6_beta.md b/docs/announcements/image_sc_openhcs_0_6_beta.md
deleted file mode 100644
index eaf12cfa1..000000000
--- a/docs/announcements/image_sc_openhcs_0_6_beta.md
+++ /dev/null
@@ -1,182 +0,0 @@
-# image.sc announcement: OpenHCS 0.6 beta
-
-Post only after every required item in the readiness checklist is checked
-against the public release being announced.
-
-## Proposed title
-
-OpenHCS 0.6 beta: open high-content screening workflows in a GUI, Python, Napari/Fiji, and MCP
-
-## Ready-to-paste post
-
-Hello image.sc community,
-
-I am releasing the OpenHCS 0.6 beta and would value feedback from people who
-build or maintain high-content microscopy workflows.
-
-OpenHCS is an open-source, Python-native platform for high-content screening.
-Its desktop GUI, generated Python, headless runtime, CellProfiler importer, and
-optional MCP server all use the same public `PipelineConfig` plus ordered
-`FunctionStep` declarations. Pipelines are compiled before execution so source
-matching, configuration, artifact routing, memory contracts, and worker
-requirements can fail early rather than after a large screen has started.
-
-The beta currently includes:
-
-- visual pipeline authoring with bidirectional GUI ↔ Python editing;
-- source bindings for plate fields such as well, site, channel, Z, and
- timepoint;
-- ImageXpress and Opera Phenix plate parsing, plus Bio-Formats and OMERO-backed
- source/storage integration; the separate OMERO web panel remains
- experimental;
-- supported CellProfiler `.cppipe` import into ordinary OpenHCS declarations;
-- live image and result streaming to Napari and Fiji/ImageJ;
-- CPU multiprocessing, optional CUDA backends, and persisted custom Python
- functions;
-- typed images, object labels, measurements, relationships, tables, grids,
- graphs, and metadata, with ROI and file materialization;
-- an optional local MCP server so compatible agents can inspect the same
- schemas, documentation, pipeline state, runtime progress, and viewer state.
-
-For CellProfiler interoperability, OpenHCS uses a source-backed corpus of 30
-example pipelines as end-to-end regression evidence. The current release
-passes that Official30 suite under the documented equivalence policies. This
-is not a claim that every historical CellProfiler module, plugin, version, and
-setting is supported: unfamiliar pipelines should still be imported and
-compiled to expose the exact compatibility boundary.
-
-### Try it
-
-For Windows or macOS, the landing page links directly to small graphical
-installers; no ZIP extraction or existing Python installation is required:
-
-https://openhcsdev.github.io/openhcs/
-
-For an existing Python 3.11–3.13 environment:
-
-```bash
-python -m pip install "openhcs[gui,viz,bioformats,mcp,cellprofiler-compat]"
-openhcs
-```
-
-The desktop installers are CPU-first and install CellProfiler compatibility,
-Napari, Fiji/PyImageJ, Bio-Formats, and local MCP support. Fiji's Java
-components are resolved on first use, and GPU packages are opt-in. Current
-installer assets are unsigned, so Windows SmartScreen or macOS Gatekeeper may
-ask you to confirm that you trust them.
-
-A five-minute interface demo is available here:
-
-https://openhcs.readthedocs.io/en/latest/_static/openhcs.mp4
-
-Documentation:
-
-https://openhcs.readthedocs.io/
-
-Source and issue tracker:
-
-https://github.com/OpenHCSDev/openhcs
-
-### Feedback I am looking for
-
-I would especially like to hear from people who have:
-
-- an ImageXpress or Opera Phenix plate with a nontrivial naming/layout pattern;
-- an existing CellProfiler pipeline they would like to import;
-- multi-site, multi-channel, Z-stack, or time-series assays;
-- workflows that produce measurements, ROIs, or CellProfiler Analyst outputs;
-- a need to move between visual editing, Python, interactive viewers, and
- automated execution.
-
-If you are willing to try it, please reply with the microscope format, image
-axes, biological goal, and expected outputs. I am also happy to help the first
-few labs migrate one representative pipeline and use that experience to
-improve onboarding.
-
-OpenHCS is MIT licensed. Thank you for any testing, criticism, or workflow
-examples you can share.
-
-## Release readiness checklist
-
-The release is a **go** only when every required item is verified against the
-same commit and public version.
-
-### 1. Source and release identity
-
-- [ ] `origin/main`, the annotated version tag, and the GitHub Release resolve
- to the same commit.
-- [ ] Package, Codex plugin, MCP bundle, and `server.json` metadata all project
- the announced version from `openhcs.__version__`.
-- [ ] The worktree contains no uncommitted production changes.
-- [ ] The PyPI project classifier is `Development Status :: 4 - Beta`.
-
-### 2. Automated acceptance
-
-- [ ] The complete GitHub Integration Tests workflow passes for the tagged
- commit.
-- [ ] Official30 headless CellProfiler parity passes in that workflow.
-- [ ] Python 3.11 and 3.13 boundary jobs pass on Linux, Windows, and macOS.
-- [ ] Wheel installation, desktop installer, backend/microscope, and OMERO jobs
- pass.
-- [ ] Documentation and Website workflows pass.
-- [ ] The persisted-custom-function spawn regression passes with two genuine
- worker processes.
-- [ ] The typed `run_plate` action is enabled during execution and dispatches
- Stop; cancellation returns the manager to an actionable state.
-
-### 3. Public artifacts
-
-- [ ] PyPI exposes the exact announced wheel and sdist, neither yanked.
-- [ ] The GitHub Release is public, non-draft, and marked latest.
-- [ ] `OpenHCS-Windows-Installer.exe` and
- `OpenHCS-macOS-Installer.dmg` are attached without ZIP wrapping.
-- [ ] Both landing-page “latest/download” installer URLs return HTTP 200 and
- resolve to the announced release.
-- [ ] The landing page displays the announced version and does not describe
- patch-level fixes as newly introduced platform features.
-- [ ] The MCP Registry lists `io.github.OpenHCSDev/openhcs` at the announced
- version, marks it latest, and pins
- `openhcs[gui,mcp,viz]==`.
-
-### 4. Installation and first-use journeys
-
-- [ ] A clean Windows installer run completes, creates a desktop launcher,
- launches OpenHCS, and registers detected supported MCP clients without
- overwriting unrelated entries.
-- [ ] A clean macOS DMG run completes the equivalent user-scoped installation
- and launch journey.
-- [ ] Installer progress, failure logs, startup splash output, updater, and
- unsigned-installer warnings are accurate.
-- [ ] A fresh PyPI environment can import the base package and launch the GUI
- with the documented desktop extras.
-- [ ] `openhcs-mcp-demo --json` completes its portable workflow, observes
- nonzero data in Napari, and cleans up its allocated endpoints.
-
-### 5. Scientific workflow journeys
-
-- [ ] Add a representative plate, inspect source inventory/bindings, author or
- import a pipeline, compile it, run it, and inspect materialized results.
-- [ ] A supported `.cppipe` imports through the GUI and exposes ordinary
- OpenHCS steps without a hidden CellProfiler runtime.
-- [ ] Measurement rows retain biological coordinates and aggregate output
- names do not imply site/channel coordinates that were aggregated away.
-- [ ] Reusing an output directory cannot contaminate the current summary with
- stale measurements from an older execution.
-- [ ] Napari and Fiji receive correctly indexed payloads through their tested
- viewer routes.
-- [ ] A persisted custom function runs with at least two spawned workers,
- materializes its declared outputs, cancels cleanly, and can be replaced
- and rerun without restarting the GUI.
-
-### 6. Announcement and support
-
-- [ ] Every feature claim above has a documentation, test, or live-release
- evidence boundary.
-- [ ] The screenshot preview and five-minute demo URLs are public.
-- [ ] Documentation, GitHub issues, PyPI, release, installer, and MCP Registry
- links are public and current.
-- [ ] The post states CPU-first/GPU opt-in and unsigned-installer boundaries.
-- [ ] The requested feedback is specific enough for a biologist to respond
- with microscope format, axes, assay goal, and expected outputs.
-- [ ] The maintainer is prepared to answer installation and first-workflow
- replies during the first several days after posting.
diff --git a/docs/announcements/image_sc_openhcs_beta.md b/docs/announcements/image_sc_openhcs_beta.md
new file mode 100644
index 000000000..87d065286
--- /dev/null
+++ b/docs/announcements/image_sc_openhcs_beta.md
@@ -0,0 +1,169 @@
+# image.sc announcement: OpenHCS beta
+
+Post only after every required item in the readiness checklist is verified
+against the same public release. This draft intentionally does not freeze a
+commit or name a not-yet-published patch release.
+
+## Proposed title
+
+OpenHCS beta: build and run typed HCS pipelines from the GUI, Python, or a local agent
+
+## Ready-to-paste post
+
+Hello image.sc community,
+
+I am preparing an OpenHCS beta announcement and would value feedback from
+people who build or maintain high-content microscopy workflows.
+
+OpenHCS is an open-source, Python-native platform for turning a microscopy
+plate into a compiled, reproducible analysis. You can author the same typed
+pipeline in the desktop GUI, generated Python, or a local MCP-capable agent;
+inspect it in Napari or Fiji; and import supported CellProfiler `.cppipe`
+workflows into ordinary OpenHCS declarations.
+
+The key design choice is compile before execute. Source matching,
+configuration, artifact routing, memory contracts, and worker requirements are
+validated across the selected wells, sites, channels, Z planes, and timepoints
+before expensive work starts. The GUI, Python API, executor, and MCP tools all
+consume the same `PipelineConfig` and ordered `FunctionStep` declarations.
+
+The beta includes:
+
+- ImageXpress and Opera Phenix plate parsing, plus Bio-Formats and OMERO-backed
+ source/storage integration;
+- visual pipeline authoring with bidirectional GUI to Python editing;
+- CPU multiprocessing and opt-in CUDA/OpenCL processing backends;
+- typed images, labels, measurements, relationships, tables, ROIs, and files;
+- live, process-isolated streaming to Napari and Fiji/ImageJ;
+- supported CellProfiler import with explicit compatibility reports; and
+- a local stdio MCP server that projects the typed registry, authoring schema,
+ compiler, runtime progress, results, and viewer state to compatible clients.
+
+For CellProfiler interoperability, the automated suite runs a source-backed
+corpus of 30 example pipelines under documented equivalence policies. Passing
+that Official30 corpus is a concrete regression boundary, not a claim that
+every historical CellProfiler plugin, version, or setting is supported. An
+unfamiliar pipeline should be imported and compiled to expose its exact
+compatibility boundary.
+
+### A bounded agent workflow
+
+After configuring explicit read and write roots, a useful first request is:
+
+> Inspect this Opera Phenix plate, infer its axes, and draft a nuclei
+> segmentation plus per-cell intensity pipeline. Compile it and explain every
+> validation result, but do not execute it.
+
+Review the compiled plan, then authorize a deliberately small run:
+
+> Run A01, site 1; stream the labels to Napari, save the measurements, and
+> summarize the cell counts.
+
+The supported MCP server runs locally; OpenHCS does not provide a public hosted
+endpoint. Tool inputs and outputs are also subject to the privacy and retention
+policy of the agent client and model provider you choose. Treat data roots,
+execution, and output writes as capabilities and grant only what the workflow
+needs.
+
+### Try it
+
+Windows and macOS installers, verified workflow media, and the PyPI command are
+on the landing page:
+
+https://openhcsdev.github.io/openhcs/
+
+For an existing Python 3.11 to 3.13 environment:
+
+```bash
+python -m pip install "openhcs[gui,viz,bioformats,mcp,cellprofiler-compat]"
+openhcs
+```
+
+The desktop installers are CPU-first. Fiji's Java components are resolved on
+first use and GPU packages are opt-in. Current beta installers are unsigned, so
+Windows SmartScreen or macOS Gatekeeper may ask you to confirm that you trust
+them.
+
+MCP client setup and trust boundaries:
+
+https://openhcs.readthedocs.io/en/latest/user_guide/mcp_clients.html
+
+Documentation and five-minute interface demo:
+
+https://openhcs.readthedocs.io/
+
+https://openhcs.readthedocs.io/en/latest/_static/openhcs.mp4
+
+Source and issue tracker:
+
+https://github.com/OpenHCSDev/OpenHCS
+
+### Feedback I am looking for
+
+I would especially like to hear from people with:
+
+- an ImageXpress or Opera Phenix plate with a nontrivial naming pattern;
+- a CellProfiler pipeline they would like to import;
+- multi-site, multi-channel, Z-stack, or time-series assays;
+- workflows producing measurements, ROIs, relationships, or Analyst outputs;
+- a need to move between visual editing, Python, viewers, and bounded agent
+ automation.
+
+If you try it, please share the microscope format, image axes, biological goal,
+and expected outputs. A synthetic or redacted representative is enough. I am
+also happy to help the first few labs migrate one representative pipeline and
+use that experience to improve onboarding.
+
+OpenHCS is MIT licensed. Thank you for testing, criticism, and workflow
+examples.
+
+## Release readiness checklist
+
+The announcement is a **go** only when every required item is verified against
+the same tagged commit and public version. Checks below are deliberately left
+open until the announcement release exists.
+
+### 1. Identity and automated acceptance
+
+- [ ] `main`, the annotated tag, PyPI, GitHub Release, plugin bundle, and MCP
+ Registry resolve to the same version and commit.
+- [ ] The worktree is clean and the package classifier remains Beta.
+- [ ] Integration Tests, Official30, documentation, and website workflows pass
+ for the tagged commit.
+- [ ] Python 3.11 and 3.13 boundaries, wheel installation, native installers,
+ and backend/microscope jobs pass.
+
+### 2. Public artifacts and clean journeys
+
+- [ ] Wheel, sdist, direct installer assets, landing-page links, and registry
+ metadata are public and resolve to the announced release.
+- [ ] A clean Windows installer launch, MCP demo, Napari nonzero-layer check,
+ and desktop shortcut check pass.
+- [ ] A clean macOS installer launch, MCP demo, Napari smoke, and desktop app
+ check pass.
+- [ ] A fresh PyPI environment imports and launches with the documented extras.
+- [ ] Unsigned/notarized installer warnings remain explicit on the landing page
+ and release notes.
+
+### 3. Scientific and agent journeys
+
+- [ ] Add a representative plate, inspect bindings, author or import a
+ pipeline, compile, run, and inspect materialized results.
+- [ ] Reusing an output directory replaces current-run outputs and cannot
+ contaminate measurement consolidation with stale execution data.
+- [ ] Editing function code updates the live step declaration and saved
+ pipeline; moved cached paths fall back to a valid directory.
+- [ ] Napari and Fiji receive correctly indexed payloads over platform-safe
+ transport.
+- [ ] The two prompts above work through a fresh stdio client: the first cannot
+ execute, and the second is bounded to A01/site 1.
+
+### 4. Support boundary
+
+- [ ] Every claim above links to documentation, a focused test, or a public CI
+ result.
+- [ ] Landing-page gallery media is captured from real UI state; no fabricated
+ or unverified session is presented as a demo.
+- [ ] Issue, security, contribution, conduct, and citation paths are public.
+- [ ] The maintainer can answer installation and first-workflow replies during
+ the first several days after posting.
diff --git a/external/PolyStore b/external/PolyStore
index a7db3f2c9..8b58fd1fa 160000
--- a/external/PolyStore
+++ b/external/PolyStore
@@ -1 +1 @@
-Subproject commit a7db3f2c9b8ee7b1f4b9c753a643cbaa7a591d3e
+Subproject commit 8b58fd1fa90aef6472883b6e98f7b7c5f547defe
diff --git a/external/pyqt-reactive b/external/pyqt-reactive
index d44672685..c3dab7316 160000
--- a/external/pyqt-reactive
+++ b/external/pyqt-reactive
@@ -1 +1 @@
-Subproject commit d44672685543e043b3b3246404820568c7cdd2e5
+Subproject commit c3dab731695d21570df685e7f481f5ce5ef0c1cd
diff --git a/external/zmqruntime b/external/zmqruntime
index bf5fa8a73..058f0723f 160000
--- a/external/zmqruntime
+++ b/external/zmqruntime
@@ -1 +1 @@
-Subproject commit bf5fa8a73cbaa8685d8f8ce1785101e1c3206fc9
+Subproject commit 058f0723f53b2566c1d66399b09e4075f789fe37
diff --git a/openhcs/constants/constants.py b/openhcs/constants/constants.py
index 30b401a74..76f4da447 100644
--- a/openhcs/constants/constants.py
+++ b/openhcs/constants/constants.py
@@ -22,8 +22,6 @@ class Microscope(Enum):
OPENHCS = "openhcsdata"
IMAGEXPRESS = "imagexpress"
OPERAPHENIX = "opera_phenix"
- BBBC021 = "bbbc021"
- BBBC038 = "bbbc038"
OMERO = "omero" # Added for OMERO virtual filesystem backend
BIOFORMATS = "bioformats"
SOURCE_BINDINGS = "source_bindings"
diff --git a/openhcs/core/config.py b/openhcs/core/config.py
index 56fd53162..1ad49df9f 100644
--- a/openhcs/core/config.py
+++ b/openhcs/core/config.py
@@ -180,10 +180,10 @@ class GlobalPipelineConfig(AnnotatedDataclassValidationMixin):
# (GlobalPipelineConfig → PipelineConfig by removing "Global" prefix)
class NapariDimensionMode(Enum):
- """How to handle different dimensions in napari visualization."""
+ """How component values are placed in Napari image layers."""
- SLICE = "slice" # Show as 2D slice (take middle slice)
- STACK = "stack" # Show as 3D stack/volume
+ LAYER = "layer" # Create a separate Napari layer for each value
+ STACK = "stack" # Stack values along an axis in one Napari layer
class NapariVariableSizeHandling(Enum):
@@ -226,37 +226,33 @@ class NapariDisplayConfig(
site_mode: NapariDimensionMode = field(
default=NapariDimensionMode.STACK,
metadata={
- "description": "Whether site values are stacked or reduced to a 2D slice."
+ "description": "Whether site values are stacked or use separate layers."
},
)
channel_mode: NapariDimensionMode = field(
default=NapariDimensionMode.STACK,
metadata={
- "description": (
- "Whether channel values are stacked or reduced to a 2D slice."
- )
+ "description": "Whether channel values are stacked or use separate layers."
},
)
z_index_mode: NapariDimensionMode = field(
default=NapariDimensionMode.STACK,
metadata={
- "description": (
- "Whether z-index values are stacked or reduced to a 2D slice."
- )
+ "description": "Whether z-index values are stacked or use separate layers."
},
)
timepoint_mode: NapariDimensionMode = field(
default=NapariDimensionMode.STACK,
metadata={
"description": (
- "Whether timepoint values are stacked or reduced to a 2D slice."
+ "Whether timepoint values are stacked or use separate layers."
)
},
)
well_mode: NapariDimensionMode = field(
default=NapariDimensionMode.STACK,
metadata={
- "description": "Whether well values are stacked or reduced to a 2D slice."
+ "description": "Whether well values are stacked or use separate layers."
},
)
@@ -342,13 +338,13 @@ class FijiDimensionMode(Enum):
ImageJ hyperstacks have 3 dimensions: Channels (C), Slices (Z), Frames (T).
Each OpenHCS component (site, channel, z_index, timepoint) can be mapped to one of these.
- - WINDOW: Create separate windows for each value (like Napari SLICE mode)
+ - WINDOW: Create separate windows for each value (like Napari LAYER mode)
- CHANNEL: Map to ImageJ Channel dimension (C)
- SLICE: Map to ImageJ Slice dimension (Z)
- FRAME: Map to ImageJ Frame dimension (T)
"""
- WINDOW = "window" # Separate windows (like Napari SLICE mode)
+ WINDOW = "window" # Separate windows (like Napari LAYER mode)
CHANNEL = "channel" # ImageJ Channel dimension (C)
SLICE = "slice" # ImageJ Slice dimension (Z)
FRAME = "frame" # ImageJ Frame dimension (T)
diff --git a/openhcs/core/runtime_equivalence.py b/openhcs/core/runtime_equivalence.py
index d80f2f3aa..a5d9a94ce 100644
--- a/openhcs/core/runtime_equivalence.py
+++ b/openhcs/core/runtime_equivalence.py
@@ -19,16 +19,16 @@
import openhcs.core.runtime_artifact_queries as runtime_artifact_queries
import openhcs.core.measurement_feature_queries as measurement_feature_queries
import openhcs.core.measurement_row_materialization as measurement_row_materialization
-import openhcs.core.equivalence.cells as equivalence_cells
-import openhcs.core.equivalence.keys as equivalence_keys
-import openhcs.core.equivalence.measurement_facts as measurement_facts
-import openhcs.core.equivalence.measurement_features as measurement_features
-import openhcs.core.equivalence.measurement_requirements as measurement_requirements
-import openhcs.core.equivalence.measurement_rows as equivalence_measurement_rows
-import openhcs.core.equivalence.object_label_measurements as object_label_measurements
-import openhcs.core.equivalence.policy as equivalence_policy
-import openhcs.core.equivalence.relationships as equivalence_relationships
-import openhcs.core.equivalence.tables as equivalence_tables
+from openhcs.core.equivalence import cells as equivalence_cells
+from openhcs.core.equivalence import keys as equivalence_keys
+from openhcs.core.equivalence import measurement_facts
+from openhcs.core.equivalence import measurement_features
+from openhcs.core.equivalence import measurement_requirements
+from openhcs.core.equivalence import measurement_rows as equivalence_measurement_rows
+from openhcs.core.equivalence import object_label_measurements
+from openhcs.core.equivalence import policy as equivalence_policy
+from openhcs.core.equivalence import relationships as equivalence_relationships
+from openhcs.core.equivalence import tables as equivalence_tables
import openhcs.core.runtime_measurements as runtime_measurements
from openhcs.core.artifacts import (
ArtifactType,
diff --git a/openhcs/microscopes/bbbc.py b/openhcs/microscopes/bbbc.py
deleted file mode 100644
index 204c300a7..000000000
--- a/openhcs/microscopes/bbbc.py
+++ /dev/null
@@ -1,618 +0,0 @@
-"""
-BBBC (Broad Bioimage Benchmark Collection) microscope implementations.
-
-This module provides handlers for BBBC datasets in different formats:
-- BBBC021: ImageXpress-like format with UUID, files in Week*/Week*_##### subdirectories
-- BBBC038: Simple hex ID filenames in stage1_train/{ImageId}/images/ subdirectories
-
-Each dataset gets its own handler following the established MicroscopeHandler pattern.
-"""
-
-import logging
-import os
-import re
-from pathlib import Path
-from typing import Any, ClassVar, Dict, List, Optional, Tuple, Union, Type
-
-from openhcs.constants.constants import Backend, Microscope
-from openhcs.core.components.parser_metaprogramming import (
- format_filename_component,
- require_filename_component,
-)
-from openhcs.microscopes.microscope_base import MicroscopeHandler
-from openhcs.microscopes.microscope_interfaces import (
- DiskImageFileListingMetadataHandler,
- FilenameParseResult,
- FilenameParser,
- MetadataHandler,
-)
-from openhcs.microscopes.tiff_metadata_mixin import TiffPixelSizeMixin
-from polystore.exceptions import MetadataNotFoundError
-from polystore.filemanager import FileManager
-from polystore.virtual_workspace import SourcePixelRef
-
-logger = logging.getLogger(__name__)
-
-
-class BBBCFilenameParser(FilenameParser):
- """Shared BBBC parser shell for regex-based filename families."""
-
- _pattern: ClassVar[re.Pattern[str]]
-
- def __init__(self, filemanager=None, pattern_format=None):
- super().__init__()
- self.filemanager = filemanager
- self.pattern_format = pattern_format
-
- @classmethod
- def can_parse(cls, filename: str) -> bool:
- """Return whether the filename matches this BBBC parser family."""
- basename = Path(str(filename)).name
- return cls._pattern.match(basename) is not None
-
-
-class BBBCHandlerBase(MicroscopeHandler):
- """Shared BBBC handler shell for parser/metadata wiring."""
-
- _parser_class: ClassVar[Type[BBBCFilenameParser]]
- _metadata_handler_class: ClassVar[Type[MetadataHandler]]
- _microscope_type: ClassVar[str]
-
- def __init__(self, filemanager: FileManager, pattern_format: Optional[str] = None):
- self.parser = self._parser_class(filemanager, pattern_format)
- self.metadata_handler = self._metadata_handler_class(filemanager)
- super().__init__(parser=self.parser, metadata_handler=self.metadata_handler)
-
- @property
- def microscope_type(self) -> str:
- return self._microscope_type
-
- @property
- def metadata_handler_class(self) -> Type[MetadataHandler]:
- return self._metadata_handler_class
-
- @property
- def compatible_backends(self) -> List[Backend]:
- return [Backend.DISK]
-
-
-# ============================================================================
-# BBBC021 Handler (ImageXpress-like with UUID, in Week subfolders)
-# ============================================================================
-
-class BBBC021FilenameParser(BBBCFilenameParser):
- """
- Parser for BBBC021 dataset filenames.
-
- Format: {Well}_s{Site}_w{Channel}{UUID}.tif
- Example: G10_s1_w1BEDC2073-A983-4B98-95E9-84466707A25D.tif
-
- Components:
- - Well: Alphanumeric plate coordinate (e.g., A01, G10, P24)
- - Site: Numeric site/field ID (e.g., 1, 2, 3)
- - Channel: Single digit channel ID (1=DAPI, 2=Tubulin, 4=Actin)
- - UUID: Hex identifier with dashes (ignored for parsing, but part of filename)
- - z_index: Not in filename, defaults to 1
- - timepoint: Not in filename, defaults to 1
-
- Note: Channel 3 is not used in BBBC021 (only 1, 2, 4).
- """
-
- # Pattern matches both original and virtual workspace filenames:
- # Original: G10_s1_w1{UUID}.tif
- # Virtual: G10_s1_w1_z001_t001.tif
- _pattern = re.compile(
- r'^.*?' # Optional prefix (non-greedy)
- r'([A-P][0-9]{2})' # Well: letter A-P + two digits
- r'_s(\d+|\{[^\}]*\})' # Site: _s + digits or placeholder
- r'_w(\d|\{[^\}]*\})' # Channel: _w + single digit or placeholder
- r'(?:_z(\d+|\{[^\}]*\}))?' # Optional z
- r'(?:_t(\d+|\{[^\}]*\}))?' # Optional timepoint
- r'([A-F0-9-]*)' # Optional UUID
- r'(\.\w+)$', # Extension
- re.IGNORECASE
- )
-
- def parse_filename(self, filename: str) -> Optional[FilenameParseResult]:
- """
- Parse BBBC021 filename into components.
-
- Args:
- filename: Filename to parse
-
- Returns:
- Dict with keys: well, site, channel, z_index, timepoint, extension
- Or None if parsing fails
- """
- basename = Path(str(filename)).name
- match = self._pattern.match(basename)
-
- if not match:
- logger.debug("Could not parse BBBC021 filename: %s", filename)
- return None
-
- well, site_str, channel_str, z_str, t_str, uuid, ext = match.groups()
-
- def parse_component(value: str | None) -> int | None:
- if not value or "{" in value:
- return None
- return int(value)
-
- return FilenameParseResult({
- 'well': well,
- 'site': parse_component(site_str),
- 'channel': parse_component(channel_str),
- 'z_index': parse_component(z_str),
- 'timepoint': parse_component(t_str),
- 'extension': ext,
- })
-
- def extract_component_coordinates(self, component_value: str) -> Tuple[str, str]:
- """
- Extract row/column from well identifier.
-
- Args:
- component_value: Well like 'A01', 'G10', etc.
-
- Returns:
- (row, column) tuple like ('A', '01'), ('G', '10')
- """
- if not component_value or len(component_value) < 2:
- raise ValueError(f"Invalid well format: {component_value}")
-
- row = component_value[0] # First character (letter)
- col = component_value[1:] # Remaining digits
-
- if not row.isalpha() or not col.isdigit():
- raise ValueError(f"Invalid BBBC021 well format: {component_value}. Expected format like 'A01', 'G10'")
-
- return (row, col)
-
- def construct_filename(
- self,
- extension: str = '.tif',
- site_padding: int = 1, # BBBC021 uses single digits for sites
- z_padding: int = 3,
- timepoint_padding: int = 3,
- **component_values
- ) -> str:
- """
- Construct BBBC021 filename from components for virtual workspace.
-
- Note: UUID is NOT reconstructed. Virtual workspace filenames include
- ALL components (z_index, timepoint) even if not in original filenames.
- This ensures consistent pattern discovery.
-
- Args:
- well: Well ID (e.g., 'A01', 'G10')
- site: Site number
- channel: Channel number
- z_index: Z-index (defaults to 1)
- timepoint: Timepoint (defaults to 1)
- extension: File extension
- **component_values: Other component values
-
- Returns:
- Filename: {Well}_s{Site}_w{Channel}_z{Z}_t{T}.tif
- """
- well = require_filename_component(component_values, 'well')
- site = require_filename_component(component_values, 'site')
- channel = require_filename_component(component_values, 'channel')
- z_index = require_filename_component(component_values, 'z_index')
- timepoint = require_filename_component(component_values, 'timepoint')
-
- # Build filename parts
- parts = [well]
-
- # Site
- parts.append(f"_s{format_filename_component(site, site_padding)}")
-
- # Channel (no padding)
- parts.append(f"_w{format_filename_component(channel)}")
-
- # Z-index (ALWAYS include for virtual workspace)
- parts.append(f"_z{format_filename_component(z_index, z_padding)}")
-
- # Timepoint (ALWAYS include for virtual workspace)
- parts.append(f"_t{format_filename_component(timepoint, timepoint_padding)}")
-
- return "".join(parts) + extension
-
-
-class BBBCSinglePlaneMetadataHandler(DiskImageFileListingMetadataHandler):
- """Shared metadata defaults for BBBC single-plane image collections."""
-
- def get_grid_dimensions(self, plate_path: Union[str, Path]) -> Tuple[int, int]:
- return (1, 1)
-
- def get_well_values(self, plate_path: Union[str, Path]) -> Optional[Dict[str, Optional[str]]]:
- return None
-
- def get_site_values(self, plate_path: Union[str, Path]) -> Optional[Dict[str, Optional[str]]]:
- return None
-
- def get_z_index_values(self, plate_path: Union[str, Path]) -> Optional[Dict[str, Optional[str]]]:
- return None
-
- def get_timepoint_values(self, plate_path: Union[str, Path]) -> Optional[Dict[str, Optional[str]]]:
- return None
-
-
-class BBBC021MetadataHandler(TiffPixelSizeMixin, BBBCSinglePlaneMetadataHandler):
- """
- Metadata handler for BBBC021 dataset.
-
- BBBC021 public mirror ships only TIFFs; we extract metadata from TIFF tags.
- """
-
- def __init__(self, filemanager: FileManager):
- super().__init__()
- self.filemanager = filemanager
-
- def find_metadata_file(self, plate_path: Union[str, Path]) -> Path:
- """
- BBBC021 ship we have contains no separate metadata files; rely solely on TIFFs.
- Ensure caller pointed at the expected plate directory.
- """
- plate_path = Path(plate_path)
- if plate_path.name != "Week1_22123":
- raise MetadataNotFoundError(
- f"BBBC021 plate must be the Week1_22123 directory, got '{plate_path.name}'"
- )
- return plate_path
-
- def get_pixel_size(self, plate_path: Union[str, Path]) -> float:
- return self._pixel_size_from_tiff(plate_path, self.filemanager)
-
- def get_channel_values(self, plate_path: Union[str, Path]) -> Optional[Dict[str, Optional[str]]]:
- # Derive channel names from TIFF tag (if present). May return {'1': 'DAPI'} etc.
- return self._channel_from_tiff(plate_path, self.filemanager)
-
-
-class BBBC021Handler(BBBCHandlerBase):
- """
- Microscope handler for BBBC021 dataset.
-
- BBBC021: Human MCF7 cells from compound profiling experiment.
- Format: ImageXpress-like with {Well}_s{Site}_w{Channel}{UUID}.tif pattern.
- Files are in Week#/Week#_#####/ subdirectories.
- """
-
- _microscope_type = Microscope.BBBC021.value
- _parser_class = BBBC021FilenameParser
- _metadata_handler_class = BBBC021MetadataHandler
-
- @classmethod
- def detect(cls, plate_folder: Path, filemanager: FileManager) -> bool:
- """
- Detect via metadata CSV first, else via filename parser match.
- """
- plate_folder = Path(plate_folder)
- # Filename signal only (no external metadata shipped)
- try:
- files = filemanager.list_files(plate_folder, Backend.DISK.value, recursive=True)
- parser = BBBC021FilenameParser()
- for f in files:
- name = Path(f).name
- if name.lower().endswith((".tif", ".tiff")) and parser.can_parse(name):
- return True
- except Exception:
- return False
- return False
-
- @property
- def root_dir(self) -> str:
- """
- BBBC021 virtual workspace is at plate root.
-
- Files are physically in Week#/Week#_##### subdirectories,
- but virtually flattened to plate root.
- """
- return "."
-
- def _build_virtual_mapping(self, plate_path: Path, filemanager: FileManager) -> Path:
- """
- Build virtual workspace mapping for BBBC021.
-
- Flattens Week#/Week#_##### subdirectory structure to plate root,
- and adds missing z_index and timepoint components to filenames.
-
- Args:
- plate_path: Path to plate directory
- filemanager: FileManager instance
-
- Returns:
- Path to plate root
- """
- plate_path = Path(plate_path)
-
- logger.info(f"🔄 BUILDING VIRTUAL MAPPING: BBBC021 folder flattening for {plate_path}")
-
- # Initialize mapping dict (PLATE-RELATIVE paths)
- workspace_mapping = {}
-
- # Recursively find all .tif files
- image_files = filemanager.list_image_files(plate_path, Backend.DISK.value, recursive=True)
-
- for file_path in image_files:
- # Get filename
- if isinstance(file_path, str):
- filename = os.path.basename(file_path)
- elif isinstance(file_path, Path):
- filename = file_path.name
- else:
- continue
-
- # Parse original filename
- metadata = self.parser.parse_filename(filename)
- if not metadata:
- logger.warning(f"Could not parse BBBC021 filename: {filename}")
- continue
-
- # Add default z_index and timepoint (missing from original filenames)
- if metadata['z_index'] is None:
- metadata['z_index'] = 1
- if metadata['timepoint'] is None:
- metadata['timepoint'] = 1
-
- # Reconstruct filename with all components (standardized)
- new_filename = self.parser.construct_filename(**metadata)
-
- # Build PLATE-RELATIVE virtual path (at plate root)
- virtual_relative = new_filename
-
- # Build PLATE-RELATIVE real path (in subfolder)
- real_relative = Path(file_path).relative_to(plate_path).as_posix()
-
- # Add to mapping
- workspace_mapping[virtual_relative] = SourcePixelRef(
- backend=Backend.DISK.value,
- backend_address=real_relative,
- )
- logger.debug(f" Mapped: {virtual_relative} → {real_relative}")
-
- logger.info(f"Built {len(workspace_mapping)} virtual path mappings for BBBC021")
-
- # Save virtual workspace mapping
- self.save_virtual_workspace_metadata(plate_path, workspace_mapping)
-
- return plate_path
-
-
-# ============================================================================
-# BBBC038 Handler (Kaggle Nuclei - Hex ID Format)
-# ============================================================================
-
-class BBBC038FilenameParser(BBBCFilenameParser):
- """
- Parser for BBBC038 dataset (Kaggle 2018 Data Science Bowl).
-
- Format: {HexID}.png
- Example: 0a7e06cd488667b8fe53a1521d88ab3f4e8d8a05b5663e89dc5df7b02ca93f38.png
-
- BBBC038 uses simple hex string identifiers as filenames.
- Each ImageId represents a unique image (treated as a unique "well").
-
- Organization: stage1_train/{ImageId}/images/{ImageId}.png
- Parser only sees the filename, not the full path structure.
- """
-
- # Pattern: hex string + .png extension
- _pattern = re.compile(r'^([a-f0-9]+)\.png$', re.IGNORECASE)
-
- def parse_filename(self, filename: str) -> Optional[FilenameParseResult]:
- """
- Parse BBBC038 filename into components.
-
- Args:
- filename: Filename to parse
-
- Returns:
- Dict with well=ImageId, site/channel/z all fixed at 1
- Or None if parsing fails
- """
- basename = Path(str(filename)).name
- match = self._pattern.match(basename)
-
- if not match:
- logger.debug("Could not parse BBBC038 filename: %s", filename)
- return None
-
- image_id = match.group(1)
-
- return FilenameParseResult({
- 'well': image_id, # ImageId is the well identifier
- 'site': 1, # Single image per ID
- 'channel': 1, # Single channel (nuclei stain)
- 'z_index': None, # No Z-stacks, will default to 1
- 'timepoint': None, # No timepoints, will default to 1
- 'extension': '.png',
- })
-
- def extract_component_coordinates(self, component_value: str) -> Tuple[str, str]:
- """
- Extract coordinates from ImageId.
-
- BBBC038 has no spatial grid layout - ImageIds are arbitrary identifiers.
- Split the hex string for display purposes only.
-
- Args:
- component_value: ImageId (hex string)
-
- Returns:
- (first_half, second_half) of the hex ID
- """
- if not component_value:
- raise ValueError("Invalid ImageId: empty")
-
- mid = len(component_value) // 2
- return (component_value[:mid], component_value[mid:])
-
- def construct_filename(
- self,
- extension: str = '.png',
- **component_values
- ) -> str:
- """
- Construct BBBC038 filename from components.
-
- Args:
- well: ImageId (hex string)
- extension: File extension
- **component_values: Other components (ignored)
-
- Returns:
- Filename string: {ImageId}.png
- """
- image_id = require_filename_component(component_values, 'well')
- return f"{image_id}{extension}"
-
-
-class BBBC038MetadataHandler(BBBCSinglePlaneMetadataHandler):
- """
- Metadata handler for BBBC038 (Kaggle nuclei dataset).
-
- Metadata comes from:
- - metadata.xlsx
- - stage1_train_labels.csv (run-length encoded masks)
- - stage1_solution.csv (evaluation metrics)
- """
-
- def __init__(self, filemanager: FileManager):
- super().__init__()
- self.filemanager = filemanager
-
- def find_metadata_file(self, plate_path: Union[str, Path]) -> Path:
- """Find metadata.xlsx or stage1_train_labels.csv."""
- plate_path = Path(plate_path)
-
- candidates = [
- plate_path / "metadata.xlsx",
- plate_path / "stage1_train_labels.csv",
- plate_path.parent / "metadata.xlsx",
- plate_path.parent / "stage1_train_labels.csv",
- ]
-
- for candidate in candidates:
- if candidate.exists():
- return candidate
-
- raise MetadataNotFoundError(
- f"BBBC038 metadata not found in {plate_path}. "
- "Download from https://data.broadinstitute.org/bbbc/BBBC038/"
- )
-
- def get_pixel_size(self, plate_path: Union[str, Path]) -> float:
- """BBBC038 pixel size varies across different imaging conditions."""
- return 1.0 # No standard pixel size (diverse sources)
-
- def get_channel_values(self, plate_path: Union[str, Path]) -> Optional[Dict[str, Optional[str]]]:
- """BBBC038 is single-channel (nuclei stain)."""
- return {"1": "Nuclei"}
-
-
-class BBBC038Handler(BBBCHandlerBase):
- """
- Microscope handler for BBBC038 dataset (Kaggle nuclei, PNG format).
-
- BBBC038: Nuclei from diverse organisms and imaging conditions.
- Format: {HexID}.png in stage1_train/{ImageId}/images/ subdirectories.
- """
-
- _microscope_type = Microscope.BBBC038.value
- _parser_class = BBBC038FilenameParser
- _metadata_handler_class = BBBC038MetadataHandler
-
- @classmethod
- def detect(cls, plate_folder: Path, filemanager: FileManager) -> bool:
- """
- Detect BBBC038 by presence of stage1_train folder with PNGs.
- """
- stage1 = Path(plate_folder) / "stage1_train"
- if not stage1.exists():
- return False
- try:
- files = filemanager.list_files(stage1, Backend.DISK.value, pattern="*.png", recursive=True)
- return len(files) > 0
- except Exception:
- return False
-
- @property
- def root_dir(self) -> str:
- """
- BBBC038 virtual workspace is at stage1_train directory.
-
- Images are in stage1_train/{ImageId}/images/ subdirectories.
- """
- return "stage1_train"
-
- def _build_virtual_mapping(self, plate_path: Path, filemanager: FileManager) -> Path:
- """
- Build virtual workspace mapping for BBBC038.
-
- Flattens stage1_train/{ImageId}/images/ structure.
- Since filenames are already unique (ImageId), just flatten to stage1_train/.
-
- Args:
- plate_path: Path to plate directory (contains stage1_train/)
- filemanager: FileManager instance
-
- Returns:
- Path to stage1_train directory
- """
- plate_path = Path(plate_path)
- stage1_path = plate_path / "stage1_train"
-
- if not stage1_path.exists():
- logger.warning(f"stage1_train directory not found in {plate_path}")
- return plate_path
-
- logger.info(f"🔄 BUILDING VIRTUAL MAPPING: BBBC038 folder flattening for {plate_path}")
-
- # Initialize mapping dict (PLATE-RELATIVE paths)
- workspace_mapping = {}
-
- # Find all .png files in images/ subdirectories
- image_files = filemanager.list_image_files(stage1_path, Backend.DISK.value, recursive=True)
-
- for file_path in image_files:
- # Only process files in images/ subdirectories (skip masks/)
- if '/images/' not in str(file_path):
- continue
-
- # Get filename
- if isinstance(file_path, str):
- filename = os.path.basename(file_path)
- elif isinstance(file_path, Path):
- filename = file_path.name
- else:
- continue
-
- # Parse filename
- metadata = self.parser.parse_filename(filename)
- if not metadata:
- logger.warning(f"Could not parse BBBC038 filename: {filename}")
- continue
-
- # Filename is already correct (ImageId.png)
- # Just flatten to stage1_train/ directory
-
- # Build PLATE-RELATIVE virtual path (in stage1_train/)
- virtual_relative = (Path("stage1_train") / filename).as_posix()
-
- # Build PLATE-RELATIVE real path (in stage1_train/{ImageId}/images/)
- real_relative = Path(file_path).relative_to(plate_path).as_posix()
-
- # Add to mapping
- workspace_mapping[virtual_relative] = SourcePixelRef(
- backend=Backend.DISK.value,
- backend_address=real_relative,
- )
- logger.debug(f" Mapped: {virtual_relative} → {real_relative}")
-
- logger.info(f"Built {len(workspace_mapping)} virtual path mappings for BBBC038")
-
- # Save virtual workspace mapping
- self.save_virtual_workspace_metadata(plate_path, workspace_mapping)
-
- return stage1_path
diff --git a/openhcs/runtime/napari_viewer_server.py b/openhcs/runtime/napari_viewer_server.py
index 3dc4c3078..66b00dbab 100644
--- a/openhcs/runtime/napari_viewer_server.py
+++ b/openhcs/runtime/napari_viewer_server.py
@@ -892,12 +892,12 @@ def routing_context(
context="Napari separate-layer routing",
)
if well_component in context.layout.components_for_mode(
- ViewerComponentMode.SLICE
+ ViewerComponentMode.LAYER
):
return context
component_modes = dict(context.layout.component_modes)
- component_modes[well_component] = ViewerComponentMode.SLICE.value
+ component_modes[well_component] = ViewerComponentMode.LAYER.value
return replace(
context,
layout=ViewerComponentLayout.from_parts(
@@ -1254,7 +1254,7 @@ def _build_nd_image_array(
class NapariLayerTitleAuthority:
- """Build visible layer titles from producer identity and real slice axes."""
+ """Build visible titles from producer identity and layer-routed components."""
@classmethod
def title(
@@ -1268,7 +1268,7 @@ def title(
) -> str:
parts = [StreamProducerDisplayNameAuthority.output_label(producer)]
for component in component_layout.components_for_mode(
- ViewerComponentMode.SLICE
+ ViewerComponentMode.LAYER
):
value = ViewerComponentCoordinateAuthority.required_value(
component_info,
diff --git a/scripts/build_installer_source_wheelhouse.py b/scripts/build_installer_source_wheelhouse.py
new file mode 100644
index 000000000..899177924
--- /dev/null
+++ b/scripts/build_installer_source_wheelhouse.py
@@ -0,0 +1,82 @@
+#!/usr/bin/env python3
+"""Build the candidate wheels consumed by installer source tests.
+
+Project metadata remains the package authority. Submodule mode discovers
+first-party dependency candidates from ``external/*/pyproject.toml`` instead
+of maintaining a second package list in the workflow.
+"""
+
+from __future__ import annotations
+
+import argparse
+from collections.abc import Callable, Sequence
+from pathlib import Path
+import subprocess
+import sys
+
+from scripts.validate_local_release_floors import discover_local_projects
+
+
+REPO_ROOT = Path(__file__).resolve().parents[1]
+BuildRunner = Callable[[Sequence[str]], None]
+
+
+def source_projects(
+ repo_root: Path,
+ dependency_source: str,
+) -> tuple[Path, ...]:
+ """Return source trees whose wheels belong in the installer wheelhouse."""
+
+ projects = [repo_root]
+ if dependency_source == "submodules":
+ projects.extend(
+ project.path.parent for project in discover_local_projects(repo_root)
+ )
+ return tuple(projects)
+
+
+def _run_build(command: Sequence[str]) -> None:
+ subprocess.run(command, check=True)
+
+
+def build_wheelhouse(
+ output_directory: Path,
+ dependency_source: str,
+ *,
+ repo_root: Path = REPO_ROOT,
+ runner: BuildRunner = _run_build,
+) -> tuple[Path, ...]:
+ """Build the root candidate and any metadata-discovered local dependencies."""
+
+ output_directory.mkdir(parents=True, exist_ok=True)
+ projects = source_projects(repo_root, dependency_source)
+ for project in projects:
+ runner(
+ (
+ sys.executable,
+ "-m",
+ "build",
+ "--wheel",
+ "--outdir",
+ str(output_directory),
+ str(project),
+ )
+ )
+ return projects
+
+
+def main(argv: Sequence[str] | None = None) -> int:
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument("--output", type=Path, required=True)
+ parser.add_argument(
+ "--dependency-source",
+ choices=("submodules", "pypi"),
+ required=True,
+ )
+ args = parser.parse_args(argv)
+ build_wheelhouse(args.output, args.dependency_source)
+ return 0
+
+
+if __name__ == "__main__":
+ raise SystemExit(main())
diff --git a/tests/installer/test_windows_simple_installer.py b/tests/installer/test_windows_simple_installer.py
index be304072c..fb4c82344 100644
--- a/tests/installer/test_windows_simple_installer.py
+++ b/tests/installer/test_windows_simple_installer.py
@@ -553,6 +553,21 @@ def test_windows_installer_ci_has_an_absolute_safety_ceiling() -> None:
assert "$installerProcess.ExitCode" in smoke_step
+def test_desktop_installer_source_ci_builds_declared_dependency_candidates() -> None:
+ workflow = INTEGRATION_WORKFLOW_PATH.read_text(encoding="utf-8")
+ desktop_job = workflow[
+ workflow.index(" desktop-installer-source-test:") : workflow.index(
+ " pypi-dependency-readiness:"
+ )
+ ]
+
+ assert "submodules: ${{ env.OPENHCS_CI_DEP_SOURCE" in desktop_job
+ assert "python -m scripts.build_installer_source_wheelhouse" in desktop_job
+ assert '--dependency-source "${{ env.OPENHCS_CI_DEP_SOURCE }}"' in desktop_job
+ assert '$env:UV_FIND_LINKS = (Resolve-Path "dist").Path' in desktop_job
+ assert 'export UV_FIND_LINKS="$GITHUB_WORKSPACE/dist"' in desktop_job
+
+
def test_windows_installer_ci_exercises_long_path_update_cleanup() -> None:
workflow = INTEGRATION_WORKFLOW_PATH.read_text(encoding="utf-8")
smoke_step = workflow[
diff --git a/tests/integration/test_cellprofiler_generated_pipeline.py b/tests/integration/test_cellprofiler_generated_pipeline.py
index 1be32f3d8..f8ccba4a7 100644
--- a/tests/integration/test_cellprofiler_generated_pipeline.py
+++ b/tests/integration/test_cellprofiler_generated_pipeline.py
@@ -477,7 +477,6 @@ def test_bbbc021_cppipe_executes_named_channel_bindings_through_zmq(
tmp_path,
cppipe_path=cppipe_path,
source_root=plate_path,
- microscope=Microscope.BBBC021,
)
nuclei_records = _runtime_records(
@@ -517,7 +516,6 @@ def test_bbbc021_canonical_illum_cppipe_materializes_declared_images_through_zmq
tmp_path,
cppipe_path=cppipe_path,
source_root=plate_path,
- microscope=Microscope.BBBC021,
)
image_names = {path.name for path in export.exports.image_outputs}
diff --git a/tests/unit/pyqt_gui/test_dual_editor_session.py b/tests/unit/pyqt_gui/test_dual_editor_session.py
new file mode 100644
index 000000000..4ec713284
--- /dev/null
+++ b/tests/unit/pyqt_gui/test_dual_editor_session.py
@@ -0,0 +1,49 @@
+"""Regression tests for dual-editor declaration synchronization."""
+
+from __future__ import annotations
+
+from types import SimpleNamespace
+
+from objectstate import ObjectState, ObjectStateRegistry
+
+from openhcs.core.steps.function_step import FunctionStep
+from openhcs.pyqt_gui.windows.dual_editor_session import (
+ DualEditorFunctionPatternController,
+ DualEditorSession,
+)
+
+
+def _original(image):
+ return image
+
+
+def _replacement(image):
+ return image
+
+
+def test_saved_function_pattern_updates_step_object_state() -> None:
+ """Saving edited code must update the step declaration used by the pipeline."""
+
+ ObjectStateRegistry.clear()
+ editing_step = FunctionStep(func=_original, name="edited step")
+ state = ObjectState(editing_step, scope_id="plate::functionstep_0")
+ ObjectStateRegistry.register(state, _skip_snapshot=True)
+ session = DualEditorSession(
+ editing_step=editing_step,
+ step_editor=SimpleNamespace(state=state),
+ func_editor=SimpleNamespace(current_pattern=_replacement),
+ )
+ changes: list[str] = []
+ controller = DualEditorFunctionPatternController(
+ session=session,
+ detect_changes=lambda: changes.append("changed"),
+ invalidate_artifact_plan=lambda: changes.append("invalidated"),
+ )
+
+ try:
+ controller.handle_change()
+
+ assert session.object_session().to_object(update_delegate=False).func is _replacement
+ assert changes == ["changed", "invalidated"]
+ finally:
+ ObjectStateRegistry.clear()
diff --git a/tests/unit/test_build_installer_source_wheelhouse.py b/tests/unit/test_build_installer_source_wheelhouse.py
new file mode 100644
index 000000000..1da3831ba
--- /dev/null
+++ b/tests/unit/test_build_installer_source_wheelhouse.py
@@ -0,0 +1,61 @@
+"""Tests for the installer source wheelhouse projection."""
+
+from pathlib import Path
+from types import SimpleNamespace
+
+from scripts import build_installer_source_wheelhouse as wheelhouse
+
+
+def test_pypi_mode_builds_only_the_openhcs_candidate(tmp_path: Path) -> None:
+ output_directory = tmp_path / "wheelhouse"
+ commands: list[tuple[str, ...]] = []
+
+ projects = wheelhouse.build_wheelhouse(
+ output_directory,
+ "pypi",
+ repo_root=tmp_path,
+ runner=lambda command: commands.append(tuple(command)),
+ )
+
+ assert projects == (tmp_path,)
+ assert output_directory.is_dir()
+ assert len(commands) == 1
+ assert commands[0][-1] == str(tmp_path)
+
+
+def test_submodule_mode_derives_dependency_candidates_from_project_metadata(
+ monkeypatch,
+ tmp_path: Path,
+) -> None:
+ output_directory = tmp_path / "wheelhouse"
+ dependency_paths = (
+ tmp_path / "external" / "PolyStore" / "pyproject.toml",
+ tmp_path / "external" / "zmqruntime" / "pyproject.toml",
+ )
+ monkeypatch.setattr(
+ wheelhouse,
+ "discover_local_projects",
+ lambda repo_root: tuple(
+ SimpleNamespace(path=path) for path in dependency_paths
+ ),
+ )
+ commands: list[tuple[str, ...]] = []
+
+ projects = wheelhouse.build_wheelhouse(
+ output_directory,
+ "submodules",
+ repo_root=tmp_path,
+ runner=lambda command: commands.append(tuple(command)),
+ )
+
+ assert projects == (
+ tmp_path,
+ dependency_paths[0].parent,
+ dependency_paths[1].parent,
+ )
+ assert [command[-1] for command in commands] == [
+ str(project) for project in projects
+ ]
+ assert all(
+ command[-3:-1] == ("--outdir", str(output_directory)) for command in commands
+ )
diff --git a/tests/unit/test_build_website.py b/tests/unit/test_build_website.py
index 8e2da5dc0..031ad2624 100644
--- a/tests/unit/test_build_website.py
+++ b/tests/unit/test_build_website.py
@@ -196,7 +196,10 @@ def test_shipping_copy_projects_current_release_and_keeps_boundaries_explicit(
assert "https://openhcs.readthedocs.io/en/latest/" in html
assert "https://openhcs.readthedocs.io/en/latest/api/" in html
assert ">Install local MCP" in html
- assert "https://github.com/OpenHCSDev/OpenHCS/releases" in html
+ assert "https://openhcs.readthedocs.io/en/latest/user_guide/mcp_clients.html" in html
+ assert "Compile and explain it" in html
+ assert "but do not execute" in html
+ assert "Run A01, site 1" in html
assert 'id="gallery"' in html
assert 'src="assets/gallery/' in html
assert "CellProfiler" in html and package_version in html
diff --git a/tests/unit/test_file_materialization_options.py b/tests/unit/test_file_materialization_options.py
index 754a8f047..35fb1dfd9 100644
--- a/tests/unit/test_file_materialization_options.py
+++ b/tests/unit/test_file_materialization_options.py
@@ -168,6 +168,25 @@ def test_materialization_spec_error_write_mode_refuses_existing_path(tmp_path) -
assert output_path.read_bytes() == b"existing"
+def test_default_write_mode_replaces_existing_memory_artifact() -> None:
+ """Re-running a pipeline replaces its prior in-memory output by default."""
+
+ _image_options, bundle_options = _option_types()
+ filemanager = FileManager({"memory": MemoryStorageBackend()})
+ spec = MaterializationSpec(bundle_options())
+
+ for contents in (b"first run", b"second run"):
+ materialize(
+ spec,
+ data={"report.txt": contents},
+ path="/analysis/ExportBundle.pkl",
+ filemanager=filemanager,
+ backends=("memory",),
+ )
+
+ assert filemanager.load("/analysis/report.txt", "memory") == b"second run"
+
+
@pytest.mark.parametrize(
"bundle",
(
diff --git a/tests/unit/test_microscope_handler_identity.py b/tests/unit/test_microscope_handler_identity.py
index aba4031df..e93807bd5 100644
--- a/tests/unit/test_microscope_handler_identity.py
+++ b/tests/unit/test_microscope_handler_identity.py
@@ -58,7 +58,7 @@ def test_typed_microscope_values_are_exact_registered_handler_keys() -> None:
if microscope is not Microscope.AUTO
}
- assert configured_types <= handler_types
+ assert configured_types == handler_types
assert {
handler_type._microscope_type
for handler_type in MicroscopeHandler.__registry__.values()
diff --git a/tests/unit/test_napari_streaming_handlers.py b/tests/unit/test_napari_streaming_handlers.py
index a380840ec..82e6704ef 100644
--- a/tests/unit/test_napari_streaming_handlers.py
+++ b/tests/unit/test_napari_streaming_handlers.py
@@ -3268,7 +3268,7 @@ def test_napari_layer_title_authority_uses_stream_display_name_policy():
artifact_kind="object_labels",
)
component_layout = ViewerComponentLayout(
- component_modes={"well": "slice", "channel": "stack"},
+ component_modes={"well": "layer", "channel": "stack"},
component_order=["well", "channel"],
)
@@ -3530,7 +3530,7 @@ def _stream(config: NapariDisplayConfig):
)
assert len(separate_server.component_groups) == 2
assert {
- route.layout.components_for_mode("slice")
+ route.layout.components_for_mode("layer")
for _, _, route in separate_server.display_pipeline.scheduled
} == {("well",)}
assert sorted(
diff --git a/tests/unit/test_path_cache.py b/tests/unit/test_path_cache.py
new file mode 100644
index 000000000..669254d69
--- /dev/null
+++ b/tests/unit/test_path_cache.py
@@ -0,0 +1,25 @@
+"""Regression tests for persisted UI path selection."""
+
+from __future__ import annotations
+
+import json
+
+from openhcs.core.path_cache import PathCacheKey, UnifiedPathCache
+
+
+def test_stale_pipeline_path_is_removed_and_fallback_is_used(tmp_path) -> None:
+ """A moved pipeline cannot strand the next file dialog on a dead path."""
+
+ cache_file = tmp_path / "path_cache.json"
+ moved_path = tmp_path / "moved-away"
+ fallback = tmp_path / "pipelines"
+ fallback.mkdir()
+ cache_file.write_text(
+ json.dumps({PathCacheKey.PIPELINE_FILES.value: str(moved_path)}),
+ encoding="utf-8",
+ )
+
+ cache = UnifiedPathCache(cache_file)
+
+ assert cache.get_initial_path(PathCacheKey.PIPELINE_FILES, fallback) == fallback
+ assert json.loads(cache_file.read_text(encoding="utf-8")) == {}
diff --git a/tests/unit/test_runtime_equivalence_package.py b/tests/unit/test_runtime_equivalence_package.py
index a2bb45abe..154d7d6fe 100644
--- a/tests/unit/test_runtime_equivalence_package.py
+++ b/tests/unit/test_runtime_equivalence_package.py
@@ -3,6 +3,8 @@
import ast
from collections import Counter
from pathlib import Path
+import subprocess
+import sys
from unittest.mock import patch
import numpy as np
@@ -43,6 +45,41 @@
PROJECT_ROOT = Path(__file__).parents[2]
+def test_runtime_equivalence_imports_owned_package_in_a_cold_process() -> None:
+ completed = subprocess.run(
+ [
+ sys.executable,
+ "-c",
+ (
+ "import sys; import openhcs.core; "
+ "assert 'openhcs.core.equivalence' not in sys.modules; "
+ "import openhcs.core.runtime_equivalence"
+ ),
+ ],
+ cwd=PROJECT_ROOT,
+ capture_output=True,
+ text=True,
+ check=False,
+ )
+
+ assert completed.returncode == 0, completed.stderr
+
+
+def test_runtime_equivalence_does_not_depend_on_deep_alias_import_side_effects() -> (
+ None
+):
+ source_path = PROJECT_ROOT / "openhcs/core/runtime_equivalence.py"
+ tree = ast.parse(source_path.read_text(encoding="utf-8"), filename=str(source_path))
+
+ assert not [
+ alias.name
+ for node in ast.walk(tree)
+ if isinstance(node, ast.Import)
+ for alias in node.names
+ if alias.name.startswith("openhcs.core.equivalence.")
+ ]
+
+
def test_runtime_equivalence_report_types_have_package_owner() -> None:
assert runtime_equivalence.RuntimeEquivalenceDifference is (
RuntimeEquivalenceDifference
diff --git a/tests/unit/test_source_binding_workspace.py b/tests/unit/test_source_binding_workspace.py
index 800dce234..5113f1ac1 100644
--- a/tests/unit/test_source_binding_workspace.py
+++ b/tests/unit/test_source_binding_workspace.py
@@ -391,7 +391,7 @@ def test_nonempty_source_bindings_override_physical_source_microscope(
tmp_path,
):
handler = create_microscope_handler(
- microscope_type=Microscope.BBBC021.value,
+ microscope_type=Microscope.IMAGEXPRESS.value,
plate_folder=tmp_path,
filemanager=_filemanager(),
source_bindings_config=SourceBindingsConfig(
diff --git a/tests/unit/test_streaming_service.py b/tests/unit/test_streaming_service.py
index ca10cea63..0623cb4d2 100644
--- a/tests/unit/test_streaming_service.py
+++ b/tests/unit/test_streaming_service.py
@@ -128,6 +128,14 @@ def test_streaming_config_component_modes_apply_display_defaults() -> None:
}
+def test_napari_dimension_modes_distinguish_layers_from_slices() -> None:
+ assert tuple(NapariDimensionMode) == (
+ NapariDimensionMode.LAYER,
+ NapariDimensionMode.STACK,
+ )
+ assert NapariDimensionMode.LAYER.value == "layer"
+
+
def test_stream_images_uses_resolved_config_backend_not_viewer_name(
monkeypatch,
) -> None:
diff --git a/website/index.html b/website/index.html
index 3728e6a8f..48d8927a3 100644
--- a/website/index.html
+++ b/website/index.html
@@ -475,7 +475,7 @@ Agent access to pipeline and runtime state.
@@ -487,7 +487,9 @@ Agent access to pipeline and runtime state.
- Load this plate, group by well, segment nuclei, and report intensity.
+ Inspect this Opera Phenix plate, infer its axes, and draft a nuclei
+ segmentation plus per-cell intensity pipeline. Compile and explain it,
+ but do not execute.
OPENHCS
@@ -498,8 +500,8 @@
Agent access to pipeline and runtime state.
↻ openhcs_create_config draft config
- Installer users: connect once, restart the agent, and ask
- Use OpenHCS to inspect this microscopy plate.
+ Then review the plan and explicitly authorize a bounded run
+ Run A01, site 1; stream labels and save measurements.