From 0fe05cedb53c83f5b1e91c64a3432340953ee0c2 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 14 Nov 2026 11:56:43 +0000 Subject: [PATCH] Refuse RGB units only for requests naming their channels (experiment-v1 section 8) A_RGB_NOT_SUPPORTED applies, per the Spec, to "a request on a described run [that] includes the channels of an RGB unit". The resolver refused any request whose scope touched an rgb unit, mask-only requests included, so a brick's mask-only features (MorphoObjects shapes, MorphoRelate relations and skeletons) could not run on an RGB slide without declaring rgb_support. It now counts only the rgb images whose channels the request names. Owner decision of 14 November 2026: fix the code to the Spec; no new capability value. Test: on Spec v28, a mask-only request is accepted for any consumer; naming one of R, G, B is still A_RGB_NOT_SUPPORTED. tools/check.py: 5495 passed, 4 skipped (case-sensitive file system x3, running as root), 0 failed. Ruff and code rules are clean after shortening two lines. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01VbytvF3Gsa85ix8Ne2yMDW --- .../run_inputs_request.py | 7 ++++++- .../run_inputs_resolve.py | 4 +++- tests/test_spec_resolve_request.py | 14 ++++++++++++++ 3 files changed, 23 insertions(+), 2 deletions(-) diff --git a/src/morphobricks_preprocessing/run_inputs_request.py b/src/morphobricks_preprocessing/run_inputs_request.py index 1c6d130..62ec0d9 100644 --- a/src/morphobricks_preprocessing/run_inputs_request.py +++ b/src/morphobricks_preprocessing/run_inputs_request.py @@ -514,7 +514,12 @@ def unchecked_refusal(mask_id: str, unchecked: Iterable[str], total: int) -> Ref def rgb_refusal(capabilities: Capabilities, rgb_images: Iterable[str]) -> Refusal | None: - """Return ``A_RGB_NOT_SUPPORTED`` when ``rgb`` units are requested without the capability.""" + """Return ``A_RGB_NOT_SUPPORTED`` when ``rgb`` units are requested without the capability. + + ``rgb_images`` are the images of ``rgb`` units whose channels the request names (experiment-v1 + §8: the refusal applies to a request that includes an RGB unit's channels); a mask-only request + names none, so it is never refused for RGB. + """ images = tuple(sorted(rgb_images)) if not images or capabilities.rgb_support: return None diff --git a/src/morphobricks_preprocessing/run_inputs_resolve.py b/src/morphobricks_preprocessing/run_inputs_resolve.py index 72c4ef7..0483bba 100644 --- a/src/morphobricks_preprocessing/run_inputs_resolve.py +++ b/src/morphobricks_preprocessing/run_inputs_resolve.py @@ -113,7 +113,9 @@ def resolve_request(self, request: Request) -> ResolvedRequest: found, more = self._mask(mask, description, fovs, repeats, masks_table) refusals.extend(found) covered.extend(more) - rgb = [f.relative_path for f in fovs if f.image_type == RGB] + # A mask-only request names no channel: never refused for RGB (§8 A_RGB_NOT_SUPPORTED). + asked = set(request.channel_ids) + rgb = [f.relative_path for f in fovs if f.image_type == RGB and asked & set(f.channel_ids)] refusals.extend(_some(rgb_refusal(request.capabilities, rgb))) refusals.extend(_some(volume_refusal(request))) selections = () if refusals else self._selections(request, description, fovs) diff --git a/tests/test_spec_resolve_request.py b/tests/test_spec_resolve_request.py index 3b7b383..384fc92 100644 --- a/tests/test_spec_resolve_request.py +++ b/tests/test_spec_resolve_request.py @@ -295,6 +295,20 @@ def test_v28_rgb_units_are_refused_unless_the_consumer_declares_rgb_support(tmp_ assert accepted.selections[0].channel_indices == (0, 1, 2) +@pytest.mark.parametrize("channels", [(), ("G",)]) +def test_v28_only_a_request_naming_rgb_channels_needs_rgb_support(tmp_path, channels): + """experiment-v1 §8: A_RGB_NOT_SUPPORTED is for a request naming an RGB unit's channels. + + A mask-only request (no channel) is accepted for any consumer; naming one of R, G, B is not. + """ + inputs, _ = _open(tmp_path, "v28") + resolved = inputs.resolve_request(Request(channels)) + if channels: + assert _codes(resolved) == [("A_RGB_NOT_SUPPORTED", "rgb")] + else: + assert not resolved.refused and len(resolved.selections) == 32 + + def test_v01_channel_units_are_channels_and_need_no_capability(tmp_path): inputs, _ = _open(tmp_path, "v01") resolved = inputs.resolve_request(Request(("DNA",)))