diff --git a/docs/validation/physical-source-inspection-445-original-20261002.tar.gz b/docs/validation/physical-source-inspection-445-original-20261002.tar.gz new file mode 100644 index 0000000000..aaa04a250a Binary files /dev/null and b/docs/validation/physical-source-inspection-445-original-20261002.tar.gz differ diff --git a/docs/validation/physical-source-inspection-445-source-20261002.tar.gz b/docs/validation/physical-source-inspection-445-source-20261002.tar.gz new file mode 100644 index 0000000000..42e1613042 Binary files /dev/null and b/docs/validation/physical-source-inspection-445-source-20261002.tar.gz differ diff --git a/docs/validation/physical-source-inspection-445.rst b/docs/validation/physical-source-inspection-445.rst new file mode 100644 index 0000000000..f78b558b9b --- /dev/null +++ b/docs/validation/physical-source-inspection-445.rst @@ -0,0 +1,191 @@ +Physical-source inspection445 receiving receipt +=============================================== + +Status and ownership +-------------------- + +Singer owns the initial read-only BioFormats/inspection RCA and source fixtures +for issue445. Parent owns integration and eventual installed physical-source QA. +Working production checkpoint ``107512352`` is source-qualified and published +in draft446. Installed/native receiving acceptance remains open; issue445 is +addressed, not auto-closed. +This is independent of404's native graph receiving acceptance and440/441's +pending installed receiving acceptance. Planck's live scientific source and +viewer remain untouched. + +The existing persistent worktree has an ordinary new branch from main +``3df650bf7faf84f67dbad9acf9ee62a095ab1ecf``. Root394 was checked at +``919fee286f8003a00aafc023ad28767d9713842b``. Its active shared owners include +source bindings, metadata, projection, runtime payloads and materialisation. +No shared394 file is edited by this checkpoint. The source changes below are +limited to the existing public plate inventory and read-only inspection owners; +no Root worktree, environment, package, process or scientific output is changed. +Original untracked134/440 evidence remains intact. No cleanup is authorised. +Root was also checked at current ``a5134a0b85182078700ac06f27dffd3e5788302b``; +neither production path below appears in its shared-file changes. The exact +two-file scope was posted in394 comment5950317703. Qualification is against +main3df650bf7, not a claim about Root's subsequently changing runtime code. + +Retained installed witness +-------------------------- + +The full reproducer is +``/home/ts/wt/openhcs-issue-batch-20260929/carrier434-installed-20261002/C3-PHYSICAL-READ-BOUNDARY.rst``. +The author's exact requests are retained under +``/home/ts/wt/openhcs-issue-batch-20260929/rbpms-r0010-development-434-20261002/output``: +``CANDIDATE05-C3-READONLY-QA.rst``, +``CANDIDATE05-C3-READONLY-EVIDENCE.json`` and +``mcp-continuation03.stdin/stdout/timing``. + +Source metadata declares C1 AF647-T1, C2 AF488-T2 and C3 H3258-T3. +Ordinary and explicit BioFormats queries expose only the pipeline-bound C1; +an explicit physical-container sample also resolves C1. This is missing safe +original-channel QA access, not evidence of a channel swap. Only engineering +receipts, request schemas and metadata identities were inspected. No CZI, +scientific array values, ROI geometry or held-out/reference input was opened. + +Existing owner boundary +------------------------ + +``PlateInspectionService._image_inventory`` reads the persisted virtual-workspace +projection before ``PlateImageInventory.from_handler`` considers the metadata +owner's exact ``source_dataset``. A nonempty persisted C1 projection therefore +suppresses the fresh explicit BioFormats owner's physical C2/C3 candidates. +The query/sample/stream DTOs have no separate physical-plane selector. + +The required relation is not to widen the pipeline source set: a read-only +explicit acquisition handler must be able to inventory its own exact declared +planes independently of an already prepared workspace. The automatically selected +OpenHCS workspace owner must still expose the persisted C1 selection. Sampling +a returned exact virtual record must use its original ``SourcePixelRef``; +one container path shared by several planes must not silently select one plane. + +The original proposed implementation moved the existing candidate-to-inventory loop +into ``PlateImageInventory.from_source_dataset`` and reuse it from the existing +``from_handler`` path. Read-only ``_image_inventory`` first asks the selected +metadata owner for its exact dataset; if present, use that common constructor. +Otherwise retain the existing virtual-workspace derivation. The generic pipeline +``from_handler`` projection precedence stays unchanged. No BioFormats name/type +branch, new mode, channel roster, selector table, decoder, source receipt or +metadata mutation is necessary. The obsolete inline loop is replaced, not copied. + +Working source implementation +------------------------------ + +Production checkpoint: ``107512352d7af4d9d4f59f539b9440e9143f9144``. +The only changed production files are +``openhcs/core/plate_image_inventory.py`` and +``openhcs/agent/services/plate_inspection_service.py``. +The original full-file R0 rejected the first placement at ``07c216483``: +``PlateInspectionService`` god-class excess increased by8 lines. Its original +measurement remains archived. The actual correction puts the combined read-only +resolution algorithm on existing ``PlateImageInventory.from_read_only_handler``; +the service retains its small dispatch/error-reporting hook. The common +``from_source_dataset`` constructor replaces the old inline candidate loop, and +the generic pipeline/Image Browser ``from_handler`` reuses that constructor +without changing its persisted-projection precedence. Replaced code and the +service's unused projection import are deleted. No parallel source authority is +introduced. + +The existing typed request contracts suffice after repair: an explicit +acquisition handler can expose its exact physical planes independently of a +prepared pipeline selection. Query with the original handler declaration +(``microscope_type=bioformats`` for this retained source), select the returned +record's actual channel metadata and exact ``virtual_path``, then use that path +for sample/stream. An auto OpenHCS/workspace query still returns the persisted C1 +domain. A physical-container path identifying multiple acquisition planes is +rejected by the unchanged strict record resolver; it is not a channel selector. +There is no new string mode, request selector, channel-specific branch or receipt. + +The final five source cases pass. For both three- and four-channel declarations, +preparation persists C1 only; auto inspection/sample and the generic inventory +constructor retain C1. Explicit acquisition inspection returns every declared +plane, exact C3 bounded pixels match the engineering source, and the original +stream source projection plus ``ViewerStreamingSource.load_image`` load exact +C3 pixels with the declared0.5 micrometre calibration. No native viewer is +created. Ambiguous container samples return the original typed failure with no +sample. Persisted metadata remains byte-exact across the whole receiving journey. + +An independent ``DeclaredInspectionMetadata`` leaf adds one source-dataset hook +which calls cooperative ``super().source_dataset`` and supplies a new declared +metadata fact. That fact, all four channel addresses and the exact source refs +flow through unchanged generic inventory. No generic consumer, registry or +catalog entry is edited for this case. No genuinely independent new production +capability needs another mixin: existing metadata polymorphism supplies the hook, +and the existing nominal inventory owner supplies the common algorithm. + +Final source run:5 PASS,4.87s elapsed, peak240280KiB RSS, zero swap; same +1CPU/512MiB/no-swap/60s kernel limits. Eleven existing inventory, source-ref, +dual stream-record, DTO and path-policy controls passed in the earlier journey +run at the first production placement; those completed controls were not rerun +for ceremony. All tests use source-owned engineering fixtures and existing +read-only dependencies, not scientific input or a new environment. + +Three intervening test-harness failures remain retained: an assumed error word +instead of the existing ``matched multiple records`` message; an assumedNone +instead of the DTO's emptytuple failure sample; and an image record passed where +the stream owner requires the unified file record. Only test assertions/record +selection changed for these failures; product guards were not weakened. + +Original pinned R0 +------------------- + +The unchanged original detector at +``/home/ts/wt/openhcs-s1-original-ratchet-20261001`` +``3b03785f45df2ef5dc62ba6aed99294192ecbb01`` measured main +``3df650bf7faf84f67dbad9acf9ee62a095ab1ecf`` to production ``107512352`` +using ``--root openhcs``. Both actual changed production files are included; +no copied detector, omitted path, increased threshold or positive-delta waiver. +Final exit0,17.82s, peak87628KiB, zero swap. Positive deltas are empty; +the sole nonzero delta is ``PlateInspectionService`` god-class excess minus13. + +``physical-source-inspection-445-source-20261002.tar.gz`` contains byte-exact +``original.log``, ``repair.log``, ``final.log``, ``journey.log``, +``qualified.log``, ``owner-final.log``, ``r0.log`` and ``r0-final.log``. +The original smaller archive and loose originals remain retained. Original log +whitespace is not rewritten or represented as production diff cleanliness. +Final archive SHA256: +``ffc8001ed2d76c43626f6154f84b06fa54a4b1865f209ab4a00212d9b6ccef8a``. + +NRA and catalog review +----------------------- + +The current NRA skill and authoritative refactor-audit archive were read. +BOUND-8 applies where the richer acquisition candidate domain is hidden behind +the persisted pipeline projection. BOUND-2 prohibits bypassing the typed +``SourceCandidate``/``SourcePixelRef`` owners. MEMB-1/2 prohibit a separate +physical-channel catalog. IMPL-12/13 prohibit another candidate converter or +sampling mechanism. A future exact source dataset supplied by another metadata +declaration must work without a concrete generic-consumer branch. No independent +production capability requires ornamental multiple inheritance. + +Original bounded source checkpoint +------------------------------------ + +``tests/unit/agent/test_physical_source_inspection_445.py`` extends the existing +BioFormats manifest fixture to a tiny three-channel NPY-backed container. +It uses the real ``PlateInspectionService``, ``BioFormatsHandler``, durable +workspace preparation, inventory and storage sampler. A nominal +``PlateInspectionFileManagerFactory`` leaf supplies only the real disk backend; +there is no JVM, Fiji, optional backend preparation, native viewer or provider. + +Original main: two controls PASS, required post-preparation C3 access RED: +``observed_channels=[1], total_count=1``. Before preparation, both three- and +four-channel declarations expose and sample C3 exactly; no generic consumer +edit is needed for the fourth channel. The C1-only persisted source domain and +sample are checked, and metadata remains byte-exact even when the red assertion +fails. This is source-boundary proof, not live scientific acceptance. + +Kernel scope: MemoryMax512MiB, MemorySwapMax0, CPUQuota100%, one CPU affinity, +external60s timeout, plugin/provider-free. Elapsed4.60s, peak240972KiB RSS, +zero swap. Pytest's two plugin-disabled configuration warnings are retained. +The byte-exact first log is archived in +``physical-source-inspection-445-original-20261002.tar.gz`` and remains loose +under owned ``.qa445-physical-source-20261002/original.log``. + +Receiving acceptance still required +------------------------------------ + +The bounded source requirements above are qualified. Parent still owns the +installed public original-C1/C3 matched raw/composite and +nuisance QA journey. No installed, native or biological readiness is claimed. diff --git a/openhcs/agent/services/plate_inspection_service.py b/openhcs/agent/services/plate_inspection_service.py index df278685fb..895feb7582 100644 --- a/openhcs/agent/services/plate_inspection_service.py +++ b/openhcs/agent/services/plate_inspection_service.py @@ -64,10 +64,6 @@ PlateResultFilePreviewReader, PlateResultFileInventory, ) -from openhcs.core.source_workspace_projection import ( - VirtualWorkspaceSourceProjectionAuthority, -) - if TYPE_CHECKING: from openhcs.core.components.parser_metaprogramming import ( FilenameParseResult, @@ -2050,23 +2046,10 @@ def _image_inventory( warnings: list[AgentWarning], ) -> PlateImageInventory: try: - handler.register_source_backends(filemanager) - source_projection = ( - VirtualWorkspaceSourceProjectionAuthority.from_plate_metadata( - plate_path=plate_path, - metadata_handler=handler.metadata_handler, - filemanager=filemanager, - ).projection_if_available() - ) - if source_projection is not None: - handler.register_workspace_backends(plate_path, filemanager) - return PlateImageInventory.from_handler( + return PlateImageInventory.from_read_only_handler( plate_path=plate_path, handler=handler, filemanager=filemanager, - backend=handler.get_primary_backend(plate_path, filemanager), - source_projection=source_projection, - all_subdirs=True, ) except Exception as exc: warnings.append( diff --git a/openhcs/core/plate_image_inventory.py b/openhcs/core/plate_image_inventory.py index 28aa4b2fd8..cbc5e123c7 100644 --- a/openhcs/core/plate_image_inventory.py +++ b/openhcs/core/plate_image_inventory.py @@ -35,7 +35,11 @@ FilenameParser, MetadataHandler, ) - from openhcs.core.source_projection import SourceCandidate, SourceProjection + from openhcs.core.source_projection import ( + SourceCandidate, + SourcePlaneDataset, + SourceProjection, + ) from polystore.filemanager import FileManager from polystore.roi import ROI, ROIShape @@ -91,6 +95,43 @@ class PlateImageInventory: plate_path: Path records: tuple[PlateImageRecord, ...] + @classmethod + def from_read_only_handler( + cls, + *, + plate_path: Path, + handler: "MicroscopeHandler", + filemanager: "FileManager", + ) -> "PlateImageInventory": + """Inventory the selected metadata owner, not a different source domain. + + Acquisition owners expose their exact physical dataset. Workspace owners + retain the persisted projection. Neither path prepares or rewrites it. + """ + + handler.register_source_backends(filemanager) + source_dataset = handler.metadata_handler.source_dataset(plate_path) + if source_dataset is not None: + return cls.from_source_dataset( + plate_path=plate_path, + handler=handler, + filemanager=filemanager, + source_dataset=source_dataset, + ) + source_projection = cls._projection( + plate_path, handler.metadata_handler, filemanager + ) + if source_projection is not None: + handler.register_workspace_backends(plate_path, filemanager) + return cls.from_handler( + plate_path=plate_path, + handler=handler, + filemanager=filemanager, + backend=handler.get_primary_backend(plate_path, filemanager), + source_projection=source_projection, + all_subdirs=True, + ) + @classmethod def from_orchestrator( cls, @@ -135,21 +176,11 @@ def from_handler( ) -> "PlateImageInventory": source_dataset = handler.metadata_handler.source_dataset(plate_path) if source_projection is None and source_dataset is not None: - parser = handler.require_filename_parser( - "Exact source datasets require a filename parser for inventory projection." - ) - return cls( + return cls.from_source_dataset( plate_path=plate_path, - records=tuple( - cls._record_from_source_candidate( - plate_path=plate_path, - candidate=candidate, - handler=handler, - parser=parser, - filemanager=filemanager, - ) - for candidate in source_dataset.candidates - ), + handler=handler, + filemanager=filemanager, + source_dataset=source_dataset, ) image_files = ( tuple(sorted(source_projection.relative_virtual_paths())) @@ -177,6 +208,34 @@ def from_handler( ) return cls(plate_path=plate_path, records=records) + @classmethod + def from_source_dataset( + cls, + *, + plate_path: Path, + handler: "MicroscopeHandler", + filemanager: "FileManager", + source_dataset: "SourcePlaneDataset", + ) -> "PlateImageInventory": + """Expose the metadata owner's exact acquisition planes, without writes.""" + + parser = handler.require_filename_parser( + "Exact source datasets require a filename parser for inventory projection." + ) + return cls( + plate_path=plate_path, + records=tuple( + cls._record_from_source_candidate( + plate_path=plate_path, + candidate=candidate, + handler=handler, + parser=parser, + filemanager=filemanager, + ) + for candidate in source_dataset.candidates + ), + ) + @staticmethod def _record_from_source_candidate( *, diff --git a/tests/unit/agent/test_physical_source_inspection_445.py b/tests/unit/agent/test_physical_source_inspection_445.py new file mode 100644 index 0000000000..ff9c750253 --- /dev/null +++ b/tests/unit/agent/test_physical_source_inspection_445.py @@ -0,0 +1,248 @@ +"""Real inspection/inventory/source-ref boundary; no Java or scientific input.""" + +import json +from dataclasses import replace +from pathlib import Path + +import numpy as np +import pytest +from polystore.disk import DiskStorageBackend +from polystore.filemanager import FileManager + +from openhcs.agent.dto.plate import ( + PlateFileQueryRequest, + PlateImageSampleRequest, + PlatePathInspectionRequest, +) +from openhcs.agent.path_policy import AgentPathPolicy +from openhcs.agent.services.plate_inspection_service import ( + PlateInspectionFileManagerFactory, + PlateInspectionService, +) +from openhcs.constants.constants import Backend +from openhcs.core.plate_image_inventory import PlateFileKind, PlateImageInventory +from openhcs.core.source_bindings import ( + MetadataSelector, + NamedSourceBinding, + SourceBindingsConfig, + SourceSelector, +) +from openhcs.microscopes.bioformats import BioFormatsHandler +from openhcs.core.source_workspace_projection import VirtualWorkspaceSourceProjectionAuthority +from openhcs.core.source_metadata import SourceVoxelSpacing +from openhcs.microscopes.bioformats import BioFormatsMetadataHandler +from tests.unit.bioformats_fixture import write_bioformats_manifest_fixture + + +class DiskOnlyInspectionFileManagerFactory(PlateInspectionFileManagerFactory): + """Declare only the real storage capability used by the engineering fixture.""" + + def create(self) -> FileManager: + return FileManager({Backend.DISK.value: DiskStorageBackend()}) + + +class DeclaredInspectionMetadata(BioFormatsMetadataHandler): + """Independent acquisition metadata hook; no generic consumer edit.""" + + def source_dataset(self, plate_path): + dataset = super().source_dataset(plate_path) + return replace( + dataset, + candidates=tuple( + replace(candidate, metadata={**candidate.metadata, "inspection_witness": "declared"}) + for candidate in dataset.candidates + ), + ) + + +def _fixture(root: Path, channels: int): + write_bioformats_manifest_fixture(root) + stack = np.arange(channels * 8 * 8, dtype=np.uint16).reshape(1, 1, channels, 8, 8) + np.save(root / "stack.npy", stack) + manifest_path = root / "bioformats_spw.json" + manifest = json.loads(manifest_path.read_text()) + image = manifest["images"][0] + image["channel_names"] = [f"engineering-{channel}" for channel in range(1, channels + 1)] + image["pixels"]["size_c"] = channels + image["pixels"]["planes"] = [ + {"c": channel, "z": 1, "t": 1, "index": channel - 1} + for channel in range(1, channels + 1) + ] + manifest_path.write_text(json.dumps(manifest)) + factory = DiskOnlyInspectionFileManagerFactory() + service = PlateInspectionService( + AgentPathPolicy.with_roots(readable_roots=(root,), writable_roots=(root,)), + filemanager_factory=factory, + ) + return stack, service, factory + + +def _query(service, root, *, microscope_type="bioformats"): + result = service.query_files( + PlateFileQueryRequest.from_fields( + plate_path=str(root), microscope_type=microscope_type, limit=10 + ) + ) + assert result.errors == () + return result + + +def _sample(service, root, image_path, *, microscope_type="bioformats"): + result = service.sample_image( + PlateImageSampleRequest.from_fields( + plate_path=str(root), + image_path=image_path, + microscope_type=microscope_type, + resolution_index=0, + y=2, + x=3, + height=2, + width=2, + max_array_elements=4, + ) + ) + assert result.errors == () + return result + + +@pytest.mark.parametrize("channels", (3, 4)) +def test_original_physical_candidates_are_sampleable_before_projection(tmp_path, channels): + stack, service, _ = _fixture(tmp_path, channels) + query = _query(service, tmp_path) + assert query.total_count == channels + record = next(record for record in query.records if int(record.metadata["channel"]) == 3) + sample = _sample(service, tmp_path, record.virtual_path) + assert int(sample.source_metadata["channel"]) == 3 + assert sample.sample_values == stack[0, 0, 2, 2:4, 3:5].tolist() + assert not (tmp_path / "openhcs_metadata.json").exists() + + +@pytest.mark.parametrize("channels", (3, 4)) +def test_physical_c3_remains_available_after_c1_only_preparation(tmp_path, channels): + stack, service, factory = _fixture(tmp_path, channels) + bindings = SourceBindingsConfig( + bindings=( + NamedSourceBinding( + alias="engineering_selected", + selector=SourceSelector(metadata=(MetadataSelector("channel", "1"),)), + ), + ) + ) + filemanager = factory.create() + BioFormatsHandler(filemanager, source_bindings_config=bindings).initialize_workspace( + tmp_path, filemanager + ) + metadata_path = tmp_path / "openhcs_metadata.json" + before = metadata_path.read_bytes() + persisted = json.loads(before)["subdirectories"]["."] + assert len(persisted["image_files"]) == 1 + assert {int(value["channel"]) for value in persisted["source_metadata"].values()} == {1} + try: + # Ordinary auto/workspace routing must keep the persisted C1 selection. + selected = _query(service, tmp_path, microscope_type="auto") + assert selected.total_count == 1 + assert {int(record.metadata["channel"]) for record in selected.records} == {1} + sample = _sample( + service, tmp_path, str(tmp_path / "stack.npy"), microscope_type="auto" + ) + assert int(sample.source_metadata["channel"]) == 1 + assert sample.sample_values == stack[0, 0, 0, 2:4, 3:5].tolist() + + query = _query(service, tmp_path) + observed_channels = {int(record.metadata["channel"]) for record in query.records} + # No invented C3 path, receipt, source-ref override, or projection mutation. + assert 3 in observed_channels, ( + "Explicit physical-source inspection hides C3 after C1 preparation: " + f"observed_channels={sorted(observed_channels)}, total_count={query.total_count}" + ) + assert observed_channels == set(range(1, channels + 1)) + record = next(record for record in query.records if int(record.metadata["channel"]) == 3) + physical = _sample(service, tmp_path, record.virtual_path) + assert int(physical.source_metadata["channel"]) == 3 + assert physical.sample_values == stack[0, 0, 2, 2:4, 3:5].tolist() + assert physical.source_path == str(tmp_path / "stack.npy") + + # The original generic (pipeline/Image Browser) constructor still gives + # the selected projection precedence even over an exact acquisition owner. + physical_handler = BioFormatsHandler(filemanager) + projection = VirtualWorkspaceSourceProjectionAuthority.from_plate_metadata( + plate_path=tmp_path, + metadata_handler=physical_handler.metadata_handler, + filemanager=filemanager, + ).projection_if_available() + assert projection is not None + selected_inventory = PlateImageInventory.from_handler( + plate_path=tmp_path, + handler=physical_handler, + filemanager=filemanager, + backend=Backend.DISK.value, + source_projection=projection, + ) + assert len(selected_inventory.records) == 1 + assert int(selected_inventory.records[0].metadata["channel"]) == 1 + + # Exercise the existing stream's source preparation, not a viewer/mock + # or a new decoder. The actual native launch remains parent-owned. + from openhcs.agent.services.plate_streaming_service import PlateStreamingService + from openhcs.core.runtime_image_values import image_payload_data, image_payload_metadata + from openhcs.core.viewer_streaming_service import ViewerStreamingSource + + context, errors, _ = service.open_context( + PlatePathInspectionRequest(plate_path=str(tmp_path), microscope_type="bioformats") + ) + assert errors == () + inventory, warnings = service.file_inventory(context, kind=PlateFileKind.IMAGE) + assert warnings == () + stream_record = inventory.require_stream_record(record.virtual_path) + stream_projection = PlateStreamingService._inventory_source_projection( + (stream_record,), context + ) + assert stream_projection is not None + stream_source = ViewerStreamingSource( + filemanager=context.filemanager, + microscope_handler=context.handler, + plate_path=str(tmp_path), + ) + image = stream_source.load_image( + stream_record.streamable_image_path, + stream_record.source_ref.backend, + source_projection=stream_projection, + component_metadata=stream_record.metadata, + ) + np.testing.assert_array_equal(image_payload_data(image), stack[0, 0, 2]) + metadata = image_payload_metadata(image) + assert metadata.source_voxel_spacing == SourceVoxelSpacing((0.5, 0.5)) + + ambiguous = service.sample_image( + PlateImageSampleRequest.from_fields( + plate_path=str(tmp_path), + image_path=str(tmp_path / "stack.npy"), + microscope_type="bioformats", + resolution_index=0, + height=2, + width=2, + max_array_elements=4, + ) + ) + assert ambiguous.errors + assert ambiguous.errors[0].code == "plate_image_sample_failed" + assert "matched multiple records" in ambiguous.errors[0].message + assert ambiguous.sample_included is False + assert ambiguous.sample_values == () + finally: + assert metadata_path.read_bytes() == before + + +def test_independent_metadata_hook_flows_through_unchanged_inventory_consumer(tmp_path): + _, service, factory = _fixture(tmp_path, 4) + filemanager = factory.create() + handler = BioFormatsHandler(filemanager) + handler.metadata_handler = DeclaredInspectionMetadata(filemanager) + warnings = [] + inventory = service._image_inventory(handler, tmp_path, filemanager, warnings) + assert warnings == [] + assert len(inventory.records) == 4 + assert {record.metadata["inspection_witness"] for record in inventory.records} == {"declared"} + assert {int(record.metadata["channel"]) for record in inventory.records} == {1, 2, 3, 4} + assert all(record.source_ref is not None for record in inventory.records) + assert not (tmp_path / "openhcs_metadata.json").exists()